Skip to main content

mas_storage_pg/user/
registration.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2025 New Vector Ltd.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use std::net::IpAddr;
8
9use async_trait::async_trait;
10use chrono::{DateTime, Utc};
11use mas_data_model::{
12    Clock, UlidExt as _, UpstreamOAuthAuthorizationSession, UserEmailAuthentication,
13    UserRegistration, UserRegistrationPassword, UserRegistrationToken,
14};
15use mas_storage::user::UserRegistrationRepository;
16use rand::Rng;
17use sqlx::PgConnection;
18use ulid::Ulid;
19use url::Url;
20use uuid::Uuid;
21
22use crate::{DatabaseError, DatabaseInconsistencyError, ExecuteExt as _};
23
24/// An implementation of [`UserRegistrationRepository`] for a PostgreSQL
25/// connection
26pub struct PgUserRegistrationRepository<'c> {
27    conn: &'c mut PgConnection,
28}
29
30impl<'c> PgUserRegistrationRepository<'c> {
31    /// Create a new [`PgUserRegistrationRepository`] from an active PostgreSQL
32    /// connection
33    pub fn new(conn: &'c mut PgConnection) -> Self {
34        Self { conn }
35    }
36}
37
38struct UserRegistrationLookup {
39    user_registration_id: Uuid,
40    ip_address: Option<IpAddr>,
41    user_agent: Option<String>,
42    post_auth_action: Option<serde_json::Value>,
43    username: String,
44    display_name: Option<String>,
45    terms_url: Option<String>,
46    email_authentication_id: Option<Uuid>,
47    user_registration_token_id: Option<Uuid>,
48    hashed_password: Option<String>,
49    hashed_password_version: Option<i32>,
50    upstream_oauth_authorization_session_id: Option<Uuid>,
51    created_at: DateTime<Utc>,
52    completed_at: Option<DateTime<Utc>>,
53}
54
55impl TryFrom<UserRegistrationLookup> for UserRegistration {
56    type Error = DatabaseInconsistencyError;
57
58    fn try_from(value: UserRegistrationLookup) -> Result<Self, Self::Error> {
59        let id = Ulid::from(value.user_registration_id);
60
61        let password = match (value.hashed_password, value.hashed_password_version) {
62            (Some(hashed_password), Some(version)) => {
63                let version = version.try_into().map_err(|e| {
64                    DatabaseInconsistencyError::on("user_registrations")
65                        .column("hashed_password_version")
66                        .row(id)
67                        .source(e)
68                })?;
69
70                Some(UserRegistrationPassword {
71                    hashed_password,
72                    version,
73                })
74            }
75            (None, None) => None,
76            _ => {
77                return Err(DatabaseInconsistencyError::on("user_registrations")
78                    .column("hashed_password")
79                    .row(id));
80            }
81        };
82
83        let terms_url = value
84            .terms_url
85            .map(|u| u.parse())
86            .transpose()
87            .map_err(|e| {
88                DatabaseInconsistencyError::on("user_registrations")
89                    .column("terms_url")
90                    .row(id)
91                    .source(e)
92            })?;
93
94        Ok(UserRegistration {
95            id,
96            ip_address: value.ip_address,
97            user_agent: value.user_agent,
98            post_auth_action: value.post_auth_action,
99            username: value.username,
100            display_name: value.display_name,
101            terms_url,
102            email_authentication_id: value.email_authentication_id.map(Ulid::from),
103            user_registration_token_id: value.user_registration_token_id.map(Ulid::from),
104            password,
105            upstream_oauth_authorization_session_id: value
106                .upstream_oauth_authorization_session_id
107                .map(Ulid::from),
108            created_at: value.created_at,
109            completed_at: value.completed_at,
110        })
111    }
112}
113
114#[async_trait]
115impl UserRegistrationRepository for PgUserRegistrationRepository<'_> {
116    type Error = DatabaseError;
117
118    #[tracing::instrument(
119        name = "db.user_registration.lookup",
120        skip_all,
121        fields(
122            db.query.text,
123            user_registration.id = %id,
124        ),
125    )]
126    async fn lookup(&mut self, id: Ulid) -> Result<Option<UserRegistration>, Self::Error> {
127        let res = sqlx::query_as!(
128            UserRegistrationLookup,
129            r#"
130                SELECT user_registration_id
131                     , ip_address as "ip_address: IpAddr"
132                     , user_agent
133                     , post_auth_action
134                     , username
135                     , display_name
136                     , terms_url
137                     , email_authentication_id
138                     , user_registration_token_id
139                     , hashed_password
140                     , hashed_password_version
141                     , upstream_oauth_authorization_session_id
142                     , created_at
143                     , completed_at
144                FROM user_registrations
145                WHERE user_registration_id = $1
146            "#,
147            Uuid::from(id),
148        )
149        .traced()
150        .fetch_optional(&mut *self.conn)
151        .await?;
152
153        let Some(res) = res else { return Ok(None) };
154
155        Ok(Some(res.try_into()?))
156    }
157
158    #[tracing::instrument(
159        name = "db.user_registration.add",
160        skip_all,
161        fields(
162            db.query.text,
163            user_registration.id,
164        ),
165    )]
166    async fn add(
167        &mut self,
168        rng: &mut (dyn Rng + Send),
169        clock: &dyn Clock,
170        username: String,
171        ip_address: Option<IpAddr>,
172        user_agent: Option<String>,
173        post_auth_action: Option<serde_json::Value>,
174    ) -> Result<UserRegistration, Self::Error> {
175        let created_at = clock.now();
176        let id = Ulid::from_datetime_with_rng(created_at, rng);
177        tracing::Span::current().record("user_registration.id", tracing::field::display(id));
178
179        sqlx::query!(
180            r#"
181                INSERT INTO user_registrations
182                  ( user_registration_id
183                  , ip_address
184                  , user_agent
185                  , post_auth_action
186                  , username
187                  , created_at
188                  )
189                VALUES ($1, $2, $3, $4, $5, $6)
190            "#,
191            Uuid::from(id),
192            ip_address as Option<IpAddr>,
193            user_agent.as_deref(),
194            post_auth_action,
195            username,
196            created_at,
197        )
198        .traced()
199        .execute(&mut *self.conn)
200        .await?;
201
202        Ok(UserRegistration {
203            id,
204            ip_address,
205            user_agent,
206            post_auth_action,
207            created_at,
208            completed_at: None,
209            username,
210            display_name: None,
211            terms_url: None,
212            email_authentication_id: None,
213            user_registration_token_id: None,
214            password: None,
215            upstream_oauth_authorization_session_id: None,
216        })
217    }
218
219    #[tracing::instrument(
220        name = "db.user_registration.set_display_name",
221        skip_all,
222        fields(
223            db.query.text,
224            user_registration.id = %user_registration.id,
225            user_registration.display_name = display_name,
226        ),
227    )]
228    async fn set_display_name(
229        &mut self,
230        mut user_registration: UserRegistration,
231        display_name: String,
232    ) -> Result<UserRegistration, Self::Error> {
233        let res = sqlx::query!(
234            r#"
235                UPDATE user_registrations
236                SET display_name = $2
237                WHERE user_registration_id = $1 AND completed_at IS NULL
238            "#,
239            Uuid::from(user_registration.id),
240            display_name,
241        )
242        .traced()
243        .execute(&mut *self.conn)
244        .await?;
245
246        DatabaseError::ensure_affected_rows(&res, 1)?;
247
248        user_registration.display_name = Some(display_name);
249
250        Ok(user_registration)
251    }
252
253    #[tracing::instrument(
254        name = "db.user_registration.set_terms_url",
255        skip_all,
256        fields(
257            db.query.text,
258            user_registration.id = %user_registration.id,
259            user_registration.terms_url = %terms_url,
260        ),
261    )]
262    async fn set_terms_url(
263        &mut self,
264        mut user_registration: UserRegistration,
265        terms_url: Url,
266    ) -> Result<UserRegistration, Self::Error> {
267        let res = sqlx::query!(
268            r#"
269                UPDATE user_registrations
270                SET terms_url = $2
271                WHERE user_registration_id = $1 AND completed_at IS NULL
272            "#,
273            Uuid::from(user_registration.id),
274            terms_url.as_str(),
275        )
276        .traced()
277        .execute(&mut *self.conn)
278        .await?;
279
280        DatabaseError::ensure_affected_rows(&res, 1)?;
281
282        user_registration.terms_url = Some(terms_url);
283
284        Ok(user_registration)
285    }
286
287    #[tracing::instrument(
288        name = "db.user_registration.set_email_authentication",
289        skip_all,
290        fields(
291            db.query.text,
292            %user_registration.id,
293            %user_email_authentication.id,
294            %user_email_authentication.email,
295        ),
296    )]
297    async fn set_email_authentication(
298        &mut self,
299        mut user_registration: UserRegistration,
300        user_email_authentication: &UserEmailAuthentication,
301    ) -> Result<UserRegistration, Self::Error> {
302        let res = sqlx::query!(
303            r#"
304                UPDATE user_registrations
305                SET email_authentication_id = $2
306                WHERE user_registration_id = $1 AND completed_at IS NULL
307            "#,
308            Uuid::from(user_registration.id),
309            Uuid::from(user_email_authentication.id),
310        )
311        .traced()
312        .execute(&mut *self.conn)
313        .await?;
314
315        DatabaseError::ensure_affected_rows(&res, 1)?;
316
317        user_registration.email_authentication_id = Some(user_email_authentication.id);
318
319        Ok(user_registration)
320    }
321
322    #[tracing::instrument(
323        name = "db.user_registration.set_password",
324        skip_all,
325        fields(
326            db.query.text,
327            user_registration.id = %user_registration.id,
328            user_registration.hashed_password = hashed_password,
329            user_registration.hashed_password_version = version,
330        ),
331    )]
332    async fn set_password(
333        &mut self,
334        mut user_registration: UserRegistration,
335        hashed_password: String,
336        version: u16,
337    ) -> Result<UserRegistration, Self::Error> {
338        let res = sqlx::query!(
339            r#"
340                UPDATE user_registrations
341                SET hashed_password = $2, hashed_password_version = $3
342                WHERE user_registration_id = $1 AND completed_at IS NULL
343            "#,
344            Uuid::from(user_registration.id),
345            hashed_password,
346            i32::from(version),
347        )
348        .traced()
349        .execute(&mut *self.conn)
350        .await?;
351
352        DatabaseError::ensure_affected_rows(&res, 1)?;
353
354        user_registration.password = Some(UserRegistrationPassword {
355            hashed_password,
356            version,
357        });
358
359        Ok(user_registration)
360    }
361
362    #[tracing::instrument(
363        name = "db.user_registration.set_registration_token",
364        skip_all,
365        fields(
366            db.query.text,
367            %user_registration.id,
368            %user_registration_token.id,
369        ),
370    )]
371    async fn set_registration_token(
372        &mut self,
373        mut user_registration: UserRegistration,
374        user_registration_token: &UserRegistrationToken,
375    ) -> Result<UserRegistration, Self::Error> {
376        let res = sqlx::query!(
377            r#"
378                UPDATE user_registrations
379                SET user_registration_token_id = $2
380                WHERE user_registration_id = $1 AND completed_at IS NULL
381            "#,
382            Uuid::from(user_registration.id),
383            Uuid::from(user_registration_token.id),
384        )
385        .traced()
386        .execute(&mut *self.conn)
387        .await?;
388
389        DatabaseError::ensure_affected_rows(&res, 1)?;
390
391        user_registration.user_registration_token_id = Some(user_registration_token.id);
392
393        Ok(user_registration)
394    }
395
396    #[tracing::instrument(
397        name = "db.user_registration.set_upstream_oauth_authorization_session",
398        skip_all,
399        fields(
400            db.query.text,
401            %user_registration.id,
402            %upstream_oauth_authorization_session.id,
403        ),
404    )]
405    async fn set_upstream_oauth_authorization_session(
406        &mut self,
407        mut user_registration: UserRegistration,
408        upstream_oauth_authorization_session: &UpstreamOAuthAuthorizationSession,
409    ) -> Result<UserRegistration, Self::Error> {
410        let res = sqlx::query!(
411            r#"
412                UPDATE user_registrations
413                SET upstream_oauth_authorization_session_id = $2
414                WHERE user_registration_id = $1 AND completed_at IS NULL
415            "#,
416            Uuid::from(user_registration.id),
417            Uuid::from(upstream_oauth_authorization_session.id),
418        )
419        .traced()
420        .execute(&mut *self.conn)
421        .await?;
422
423        DatabaseError::ensure_affected_rows(&res, 1)?;
424
425        user_registration.upstream_oauth_authorization_session_id =
426            Some(upstream_oauth_authorization_session.id);
427
428        Ok(user_registration)
429    }
430
431    #[tracing::instrument(
432        name = "db.user_registration.complete",
433        skip_all,
434        fields(
435            db.query.text,
436            user_registration.id = %user_registration.id,
437        ),
438    )]
439    async fn complete(
440        &mut self,
441        clock: &dyn Clock,
442        mut user_registration: UserRegistration,
443    ) -> Result<UserRegistration, Self::Error> {
444        let completed_at = clock.now();
445        let res = sqlx::query!(
446            r#"
447                UPDATE user_registrations
448                SET completed_at = $2
449                WHERE user_registration_id = $1 AND completed_at IS NULL
450            "#,
451            Uuid::from(user_registration.id),
452            completed_at,
453        )
454        .traced()
455        .execute(&mut *self.conn)
456        .await?;
457
458        DatabaseError::ensure_affected_rows(&res, 1)?;
459
460        user_registration.completed_at = Some(completed_at);
461
462        Ok(user_registration)
463    }
464
465    #[tracing::instrument(
466        name = "db.user_registration.cleanup",
467        skip_all,
468        fields(
469            db.query.text,
470        ),
471    )]
472    async fn cleanup(
473        &mut self,
474        since: Option<Ulid>,
475        until: Ulid,
476        limit: usize,
477    ) -> Result<(usize, Option<Ulid>), Self::Error> {
478        // `MAX(uuid)` isn't a thing in Postgres, so we can't just re-select the
479        // deleted rows and do a MAX on the `user_registration_id`.
480        // Instead, we do the aggregation on the client side, which is a little
481        // less efficient, but good enough.
482        let res = sqlx::query_scalar!(
483            r#"
484                WITH to_delete AS (
485                    SELECT user_registration_id
486                    FROM user_registrations
487                    WHERE ($1::uuid IS NULL OR user_registration_id > $1)
488                    AND user_registration_id <= $2
489                    ORDER BY user_registration_id
490                    LIMIT $3
491                )
492                DELETE FROM user_registrations
493                USING to_delete
494                WHERE user_registrations.user_registration_id = to_delete.user_registration_id
495                RETURNING user_registrations.user_registration_id
496            "#,
497            since.map(Uuid::from),
498            Uuid::from(until),
499            i64::try_from(limit).unwrap_or(i64::MAX)
500        )
501        .traced()
502        .fetch_all(&mut *self.conn)
503        .await?;
504
505        let count = res.len();
506        let max_id = res.into_iter().max();
507
508        Ok((count, max_id.map(Ulid::from)))
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use std::net::{IpAddr, Ipv4Addr};
515
516    use mas_data_model::{
517        Clock, UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode,
518        UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode,
519        UpstreamOAuthProviderTokenAuthMethod, UserRegistrationPassword, clock::MockClock,
520    };
521    use mas_iana::jose::JsonWebSignatureAlg;
522    use mas_storage::upstream_oauth2::UpstreamOAuthProviderParams;
523    use oauth2_types::scope::Scope;
524    use rand::SeedableRng;
525    use rand_chacha::ChaChaRng;
526    use sqlx::PgPool;
527
528    use crate::PgRepository;
529
530    #[sqlx::test(migrator = "crate::MIGRATOR")]
531    async fn test_create_lookup_complete(pool: PgPool) {
532        let mut rng = ChaChaRng::seed_from_u64(42);
533        let clock = MockClock::default();
534
535        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
536
537        let registration = repo
538            .user_registration()
539            .add(&mut rng, &clock, "alice".to_owned(), None, None, None)
540            .await
541            .unwrap();
542
543        assert_eq!(registration.created_at, clock.now());
544        assert_eq!(registration.completed_at, None);
545        assert_eq!(registration.username, "alice");
546        assert_eq!(registration.display_name, None);
547        assert_eq!(registration.terms_url, None);
548        assert_eq!(registration.email_authentication_id, None);
549        assert_eq!(registration.password, None);
550        assert_eq!(registration.user_agent, None);
551        assert_eq!(registration.ip_address, None);
552        assert_eq!(registration.post_auth_action, None);
553
554        let lookup = repo
555            .user_registration()
556            .lookup(registration.id)
557            .await
558            .unwrap()
559            .unwrap();
560
561        assert_eq!(lookup.id, registration.id);
562        assert_eq!(lookup.created_at, registration.created_at);
563        assert_eq!(lookup.completed_at, registration.completed_at);
564        assert_eq!(lookup.username, registration.username);
565        assert_eq!(lookup.display_name, registration.display_name);
566        assert_eq!(lookup.terms_url, registration.terms_url);
567        assert_eq!(
568            lookup.email_authentication_id,
569            registration.email_authentication_id
570        );
571        assert_eq!(lookup.password, registration.password);
572        assert_eq!(lookup.user_agent, registration.user_agent);
573        assert_eq!(lookup.ip_address, registration.ip_address);
574        assert_eq!(lookup.post_auth_action, registration.post_auth_action);
575
576        // Mark the registration as completed
577        let registration = repo
578            .user_registration()
579            .complete(&clock, registration)
580            .await
581            .unwrap();
582        assert_eq!(registration.completed_at, Some(clock.now()));
583
584        // Lookup the registration again
585        let lookup = repo
586            .user_registration()
587            .lookup(registration.id)
588            .await
589            .unwrap()
590            .unwrap();
591        assert_eq!(lookup.completed_at, registration.completed_at);
592
593        // Do it again, it should fail
594        let res = repo
595            .user_registration()
596            .complete(&clock, registration)
597            .await;
598        assert!(res.is_err());
599    }
600
601    #[sqlx::test(migrator = "crate::MIGRATOR")]
602    async fn test_create_useragent_ipaddress(pool: PgPool) {
603        let mut rng = ChaChaRng::seed_from_u64(42);
604        let clock = MockClock::default();
605
606        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
607
608        let registration = repo
609            .user_registration()
610            .add(
611                &mut rng,
612                &clock,
613                "alice".to_owned(),
614                Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
615                Some("Mozilla/5.0".to_owned()),
616                Some(serde_json::json!({"action": "continue_compat_sso_login", "id": "01FSHN9AG0MKGTBNZ16RDR3PVY"})),
617            )
618            .await
619            .unwrap();
620
621        assert_eq!(registration.user_agent, Some("Mozilla/5.0".to_owned()));
622        assert_eq!(
623            registration.ip_address,
624            Some(IpAddr::V4(Ipv4Addr::LOCALHOST))
625        );
626        assert_eq!(
627            registration.post_auth_action,
628            Some(
629                serde_json::json!({"action": "continue_compat_sso_login", "id": "01FSHN9AG0MKGTBNZ16RDR3PVY"})
630            )
631        );
632
633        let lookup = repo
634            .user_registration()
635            .lookup(registration.id)
636            .await
637            .unwrap()
638            .unwrap();
639
640        assert_eq!(lookup.user_agent, registration.user_agent);
641        assert_eq!(lookup.ip_address, registration.ip_address);
642        assert_eq!(lookup.post_auth_action, registration.post_auth_action);
643    }
644
645    #[sqlx::test(migrator = "crate::MIGRATOR")]
646    async fn test_set_display_name(pool: PgPool) {
647        let mut rng = ChaChaRng::seed_from_u64(42);
648        let clock = MockClock::default();
649
650        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
651
652        let registration = repo
653            .user_registration()
654            .add(&mut rng, &clock, "alice".to_owned(), None, None, None)
655            .await
656            .unwrap();
657
658        assert_eq!(registration.display_name, None);
659
660        let registration = repo
661            .user_registration()
662            .set_display_name(registration, "Alice".to_owned())
663            .await
664            .unwrap();
665
666        assert_eq!(registration.display_name, Some("Alice".to_owned()));
667
668        let lookup = repo
669            .user_registration()
670            .lookup(registration.id)
671            .await
672            .unwrap()
673            .unwrap();
674
675        assert_eq!(lookup.display_name, registration.display_name);
676
677        // Setting it again should work
678        let registration = repo
679            .user_registration()
680            .set_display_name(registration, "Bob".to_owned())
681            .await
682            .unwrap();
683
684        assert_eq!(registration.display_name, Some("Bob".to_owned()));
685
686        let lookup = repo
687            .user_registration()
688            .lookup(registration.id)
689            .await
690            .unwrap()
691            .unwrap();
692
693        assert_eq!(lookup.display_name, registration.display_name);
694
695        // Can't set it once completed
696        let registration = repo
697            .user_registration()
698            .complete(&clock, registration)
699            .await
700            .unwrap();
701
702        let res = repo
703            .user_registration()
704            .set_display_name(registration, "Charlie".to_owned())
705            .await;
706        assert!(res.is_err());
707    }
708
709    #[sqlx::test(migrator = "crate::MIGRATOR")]
710    async fn test_set_terms_url(pool: PgPool) {
711        let mut rng = ChaChaRng::seed_from_u64(42);
712        let clock = MockClock::default();
713
714        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
715
716        let registration = repo
717            .user_registration()
718            .add(&mut rng, &clock, "alice".to_owned(), None, None, None)
719            .await
720            .unwrap();
721
722        assert_eq!(registration.terms_url, None);
723
724        let registration = repo
725            .user_registration()
726            .set_terms_url(registration, "https://example.com/terms".parse().unwrap())
727            .await
728            .unwrap();
729
730        assert_eq!(
731            registration.terms_url,
732            Some("https://example.com/terms".parse().unwrap())
733        );
734
735        let lookup = repo
736            .user_registration()
737            .lookup(registration.id)
738            .await
739            .unwrap()
740            .unwrap();
741
742        assert_eq!(lookup.terms_url, registration.terms_url);
743
744        // Setting it again should work
745        let registration = repo
746            .user_registration()
747            .set_terms_url(registration, "https://example.com/terms2".parse().unwrap())
748            .await
749            .unwrap();
750
751        assert_eq!(
752            registration.terms_url,
753            Some("https://example.com/terms2".parse().unwrap())
754        );
755
756        let lookup = repo
757            .user_registration()
758            .lookup(registration.id)
759            .await
760            .unwrap()
761            .unwrap();
762
763        assert_eq!(lookup.terms_url, registration.terms_url);
764
765        // Can't set it once completed
766        let registration = repo
767            .user_registration()
768            .complete(&clock, registration)
769            .await
770            .unwrap();
771
772        let res = repo
773            .user_registration()
774            .set_terms_url(registration, "https://example.com/terms3".parse().unwrap())
775            .await;
776        assert!(res.is_err());
777    }
778
779    #[sqlx::test(migrator = "crate::MIGRATOR")]
780    async fn test_set_email_authentication(pool: PgPool) {
781        let mut rng = ChaChaRng::seed_from_u64(42);
782        let clock = MockClock::default();
783
784        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
785
786        let registration = repo
787            .user_registration()
788            .add(&mut rng, &clock, "alice".to_owned(), None, None, None)
789            .await
790            .unwrap();
791
792        assert_eq!(registration.email_authentication_id, None);
793
794        let authentication = repo
795            .user_email()
796            .add_authentication_for_registration(
797                &mut rng,
798                &clock,
799                "alice@example.com".to_owned(),
800                &registration,
801            )
802            .await
803            .unwrap();
804
805        let registration = repo
806            .user_registration()
807            .set_email_authentication(registration, &authentication)
808            .await
809            .unwrap();
810
811        assert_eq!(
812            registration.email_authentication_id,
813            Some(authentication.id)
814        );
815
816        let lookup = repo
817            .user_registration()
818            .lookup(registration.id)
819            .await
820            .unwrap()
821            .unwrap();
822
823        assert_eq!(
824            lookup.email_authentication_id,
825            registration.email_authentication_id
826        );
827
828        // Setting it again should work
829        let registration = repo
830            .user_registration()
831            .set_email_authentication(registration, &authentication)
832            .await
833            .unwrap();
834
835        assert_eq!(
836            registration.email_authentication_id,
837            Some(authentication.id)
838        );
839
840        let lookup = repo
841            .user_registration()
842            .lookup(registration.id)
843            .await
844            .unwrap()
845            .unwrap();
846
847        assert_eq!(
848            lookup.email_authentication_id,
849            registration.email_authentication_id
850        );
851
852        // Can't set it once completed
853        let registration = repo
854            .user_registration()
855            .complete(&clock, registration)
856            .await
857            .unwrap();
858
859        let res = repo
860            .user_registration()
861            .set_email_authentication(registration, &authentication)
862            .await;
863        assert!(res.is_err());
864    }
865
866    #[sqlx::test(migrator = "crate::MIGRATOR")]
867    async fn test_set_password(pool: PgPool) {
868        let mut rng = ChaChaRng::seed_from_u64(42);
869        let clock = MockClock::default();
870
871        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
872
873        let registration = repo
874            .user_registration()
875            .add(&mut rng, &clock, "alice".to_owned(), None, None, None)
876            .await
877            .unwrap();
878
879        assert_eq!(registration.password, None);
880
881        let registration = repo
882            .user_registration()
883            .set_password(registration, "fakehashedpassword".to_owned(), 1)
884            .await
885            .unwrap();
886
887        assert_eq!(
888            registration.password,
889            Some(UserRegistrationPassword {
890                hashed_password: "fakehashedpassword".to_owned(),
891                version: 1,
892            })
893        );
894
895        let lookup = repo
896            .user_registration()
897            .lookup(registration.id)
898            .await
899            .unwrap()
900            .unwrap();
901
902        assert_eq!(lookup.password, registration.password);
903
904        // Setting it again should work
905        let registration = repo
906            .user_registration()
907            .set_password(registration, "fakehashedpassword2".to_owned(), 2)
908            .await
909            .unwrap();
910
911        assert_eq!(
912            registration.password,
913            Some(UserRegistrationPassword {
914                hashed_password: "fakehashedpassword2".to_owned(),
915                version: 2,
916            })
917        );
918
919        let lookup = repo
920            .user_registration()
921            .lookup(registration.id)
922            .await
923            .unwrap()
924            .unwrap();
925
926        assert_eq!(lookup.password, registration.password);
927
928        // Can't set it once completed
929        let registration = repo
930            .user_registration()
931            .complete(&clock, registration)
932            .await
933            .unwrap();
934
935        let res = repo
936            .user_registration()
937            .set_password(registration, "fakehashedpassword3".to_owned(), 3)
938            .await;
939        assert!(res.is_err());
940    }
941
942    #[sqlx::test(migrator = "crate::MIGRATOR")]
943    async fn test_set_upstream_oauth_session(pool: PgPool) {
944        let mut rng = ChaChaRng::seed_from_u64(42);
945        let clock = MockClock::default();
946
947        let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
948
949        let registration = repo
950            .user_registration()
951            .add(&mut rng, &clock, "alice".to_owned(), None, None, None)
952            .await
953            .unwrap();
954
955        assert_eq!(registration.upstream_oauth_authorization_session_id, None);
956
957        let provider = repo
958            .upstream_oauth_provider()
959            .add(
960                &mut rng,
961                &clock,
962                UpstreamOAuthProviderParams {
963                    issuer: Some("https://example.com/".to_owned()),
964                    human_name: Some("Example Ltd.".to_owned()),
965                    brand_name: None,
966                    scope: Scope::from_iter([oauth2_types::scope::OPENID]),
967                    token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
968                    token_endpoint_signing_alg: None,
969                    id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
970                    client_id: "client".to_owned(),
971                    encrypted_client_secret: None,
972                    claims_imports: UpstreamOAuthProviderClaimsImports::default(),
973                    authorization_endpoint_override: None,
974                    token_endpoint_override: None,
975                    userinfo_endpoint_override: None,
976                    fetch_userinfo: false,
977                    userinfo_signed_response_alg: None,
978                    jwks_uri_override: None,
979                    discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
980                    pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
981                    response_mode: None,
982                    additional_authorization_parameters: Vec::new(),
983                    forward_login_hint: false,
984                    ui_order: 0,
985                    on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
986                    registration_token_required: false,
987                },
988            )
989            .await
990            .unwrap();
991
992        let session = repo
993            .upstream_oauth_session()
994            .add(&mut rng, &clock, &provider, "state".to_owned(), None, None)
995            .await
996            .unwrap();
997
998        let registration = repo
999            .user_registration()
1000            .set_upstream_oauth_authorization_session(registration, &session)
1001            .await
1002            .unwrap();
1003
1004        assert_eq!(
1005            registration.upstream_oauth_authorization_session_id,
1006            Some(session.id)
1007        );
1008
1009        let lookup = repo
1010            .user_registration()
1011            .lookup(registration.id)
1012            .await
1013            .unwrap()
1014            .unwrap();
1015
1016        assert_eq!(
1017            lookup.upstream_oauth_authorization_session_id,
1018            registration.upstream_oauth_authorization_session_id
1019        );
1020
1021        // Setting it again should work
1022        let registration = repo
1023            .user_registration()
1024            .set_upstream_oauth_authorization_session(registration, &session)
1025            .await
1026            .unwrap();
1027
1028        assert_eq!(
1029            registration.upstream_oauth_authorization_session_id,
1030            Some(session.id)
1031        );
1032
1033        let lookup = repo
1034            .user_registration()
1035            .lookup(registration.id)
1036            .await
1037            .unwrap()
1038            .unwrap();
1039
1040        assert_eq!(
1041            lookup.upstream_oauth_authorization_session_id,
1042            registration.upstream_oauth_authorization_session_id
1043        );
1044
1045        // Can't set it once completed
1046        let registration = repo
1047            .user_registration()
1048            .complete(&clock, registration)
1049            .await
1050            .unwrap();
1051
1052        let res = repo
1053            .user_registration()
1054            .set_upstream_oauth_authorization_session(registration, &session)
1055            .await;
1056        assert!(res.is_err());
1057    }
1058}