mas_handlers/graphql/model/
users.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use anyhow::Context as _;
8use async_graphql::{
9    Context, Description, Enum, ID, Object, Union,
10    connection::{Connection, Edge, OpaqueCursor, query},
11};
12use chrono::{DateTime, Utc};
13use mas_data_model::Device;
14use mas_storage::{
15    Pagination, RepositoryAccess,
16    app_session::AppSessionFilter,
17    compat::{CompatSessionFilter, CompatSsoLoginFilter, CompatSsoLoginRepository},
18    oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
19    upstream_oauth2::{UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository},
20    user::{BrowserSessionFilter, BrowserSessionRepository, UserEmailFilter, UserEmailRepository},
21};
22
23use super::{
24    BrowserSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session,
25    PreloadedTotalCount, SessionState, UpstreamOAuth2Link,
26    compat_sessions::{CompatSessionType, CompatSsoLogin},
27    matrix::MatrixUser,
28};
29use crate::graphql::{DateFilter, state::ContextExt};
30
31#[derive(Description)]
32/// A user is an individual's account.
33pub struct User(pub mas_data_model::User);
34
35impl From<mas_data_model::User> for User {
36    fn from(v: mas_data_model::User) -> Self {
37        Self(v)
38    }
39}
40
41impl From<mas_data_model::BrowserSession> for User {
42    fn from(v: mas_data_model::BrowserSession) -> Self {
43        Self(v.user)
44    }
45}
46
47#[Object(use_type_description)]
48impl User {
49    /// ID of the object.
50    pub async fn id(&self) -> ID {
51        NodeType::User.id(self.0.id)
52    }
53
54    /// Username chosen by the user.
55    async fn username(&self) -> &str {
56        &self.0.username
57    }
58
59    /// When the object was created.
60    pub async fn created_at(&self) -> DateTime<Utc> {
61        self.0.created_at
62    }
63
64    /// When the user was locked out.
65    pub async fn locked_at(&self) -> Option<DateTime<Utc>> {
66        self.0.locked_at
67    }
68
69    /// Whether the user can request admin privileges.
70    pub async fn can_request_admin(&self) -> bool {
71        self.0.can_request_admin
72    }
73
74    /// Access to the user's Matrix account information.
75    async fn matrix(&self, ctx: &Context<'_>) -> Result<MatrixUser, async_graphql::Error> {
76        let state = ctx.state();
77        let conn = state.homeserver_connection();
78        Ok(MatrixUser::load(conn, &self.0.username).await?)
79    }
80
81    /// Get the list of compatibility SSO logins, chronologically sorted
82    async fn compat_sso_logins(
83        &self,
84        ctx: &Context<'_>,
85
86        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
87        after: Option<String>,
88        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
89        before: Option<String>,
90        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
91        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
92    ) -> Result<Connection<Cursor, CompatSsoLogin, PreloadedTotalCount>, async_graphql::Error> {
93        let state = ctx.state();
94        let mut repo = state.repository().await?;
95
96        query(
97            after,
98            before,
99            first,
100            last,
101            async |after, before, first, last| {
102                let after_id = after
103                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
104                    .transpose()?;
105                let before_id = before
106                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSsoLogin))
107                    .transpose()?;
108                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
109
110                let filter = CompatSsoLoginFilter::new().for_user(&self.0);
111
112                let page = repo.compat_sso_login().list(filter, pagination).await?;
113
114                // Preload the total count if requested
115                let count = if ctx.look_ahead().field("totalCount").exists() {
116                    Some(repo.compat_sso_login().count(filter).await?)
117                } else {
118                    None
119                };
120
121                repo.cancel().await?;
122
123                let mut connection = Connection::with_additional_fields(
124                    page.has_previous_page,
125                    page.has_next_page,
126                    PreloadedTotalCount(count),
127                );
128                connection.edges.extend(page.edges.into_iter().map(|u| {
129                    Edge::new(
130                        OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, u.id)),
131                        CompatSsoLogin(u),
132                    )
133                }));
134
135                Ok::<_, async_graphql::Error>(connection)
136            },
137        )
138        .await
139    }
140
141    /// Get the list of compatibility sessions, chronologically sorted
142    #[allow(clippy::too_many_arguments)]
143    async fn compat_sessions(
144        &self,
145        ctx: &Context<'_>,
146
147        #[graphql(name = "state", desc = "List only sessions with the given state.")]
148        state_param: Option<SessionState>,
149
150        #[graphql(name = "type", desc = "List only sessions with the given type.")]
151        type_param: Option<CompatSessionType>,
152
153        #[graphql(
154            name = "lastActive",
155            desc = "List only sessions with a last active time is between the given bounds."
156        )]
157        last_active: Option<DateFilter>,
158
159        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
160        after: Option<String>,
161        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
162        before: Option<String>,
163        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
164        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
165    ) -> Result<Connection<Cursor, CompatSession, PreloadedTotalCount>, async_graphql::Error> {
166        let state = ctx.state();
167        let mut repo = state.repository().await?;
168        let last_active = last_active.unwrap_or_default();
169
170        query(
171            after,
172            before,
173            first,
174            last,
175            async |after, before, first, last| {
176                let after_id = after
177                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSession))
178                    .transpose()?;
179                let before_id = before
180                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::CompatSession))
181                    .transpose()?;
182                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
183
184                // Build the query filter
185                let filter = CompatSessionFilter::new().for_user(&self.0);
186                let filter = match state_param {
187                    Some(SessionState::Active) => filter.active_only(),
188                    Some(SessionState::Finished) => filter.finished_only(),
189                    None => filter,
190                };
191                let filter = match type_param {
192                    Some(CompatSessionType::SsoLogin) => filter.sso_login_only(),
193                    Some(CompatSessionType::Unknown) => filter.unknown_only(),
194                    None => filter,
195                };
196
197                let filter = match last_active.after {
198                    Some(after) => filter.with_last_active_after(after),
199                    None => filter,
200                };
201                let filter = match last_active.before {
202                    Some(before) => filter.with_last_active_before(before),
203                    None => filter,
204                };
205
206                let page = repo.compat_session().list(filter, pagination).await?;
207
208                // Preload the total count if requested
209                let count = if ctx.look_ahead().field("totalCount").exists() {
210                    Some(repo.compat_session().count(filter).await?)
211                } else {
212                    None
213                };
214
215                repo.cancel().await?;
216
217                let mut connection = Connection::with_additional_fields(
218                    page.has_previous_page,
219                    page.has_next_page,
220                    PreloadedTotalCount(count),
221                );
222                connection
223                    .edges
224                    .extend(page.edges.into_iter().map(|(session, sso_login)| {
225                        Edge::new(
226                            OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
227                            CompatSession::new(session).with_loaded_sso_login(sso_login),
228                        )
229                    }));
230
231                Ok::<_, async_graphql::Error>(connection)
232            },
233        )
234        .await
235    }
236
237    /// Get the list of active browser sessions, chronologically sorted
238    async fn browser_sessions(
239        &self,
240        ctx: &Context<'_>,
241
242        #[graphql(name = "state", desc = "List only sessions in the given state.")]
243        state_param: Option<SessionState>,
244
245        #[graphql(
246            name = "lastActive",
247            desc = "List only sessions with a last active time is between the given bounds."
248        )]
249        last_active: Option<DateFilter>,
250
251        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
252        after: Option<String>,
253        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
254        before: Option<String>,
255        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
256        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
257    ) -> Result<Connection<Cursor, BrowserSession, PreloadedTotalCount>, async_graphql::Error> {
258        let state = ctx.state();
259        let mut repo = state.repository().await?;
260        let last_active = last_active.unwrap_or_default();
261
262        query(
263            after,
264            before,
265            first,
266            last,
267            async |after, before, first, last| {
268                let after_id = after
269                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::BrowserSession))
270                    .transpose()?;
271                let before_id = before
272                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::BrowserSession))
273                    .transpose()?;
274                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
275
276                let filter = BrowserSessionFilter::new().for_user(&self.0);
277                let filter = match state_param {
278                    Some(SessionState::Active) => filter.active_only(),
279                    Some(SessionState::Finished) => filter.finished_only(),
280                    None => filter,
281                };
282
283                let filter = match last_active.after {
284                    Some(after) => filter.with_last_active_after(after),
285                    None => filter,
286                };
287                let filter = match last_active.before {
288                    Some(before) => filter.with_last_active_before(before),
289                    None => filter,
290                };
291
292                let page = repo.browser_session().list(filter, pagination).await?;
293
294                // Preload the total count if requested
295                let count = if ctx.look_ahead().field("totalCount").exists() {
296                    Some(repo.browser_session().count(filter).await?)
297                } else {
298                    None
299                };
300
301                repo.cancel().await?;
302
303                let mut connection = Connection::with_additional_fields(
304                    page.has_previous_page,
305                    page.has_next_page,
306                    PreloadedTotalCount(count),
307                );
308                connection.edges.extend(page.edges.into_iter().map(|u| {
309                    Edge::new(
310                        OpaqueCursor(NodeCursor(NodeType::BrowserSession, u.id)),
311                        BrowserSession(u),
312                    )
313                }));
314
315                Ok::<_, async_graphql::Error>(connection)
316            },
317        )
318        .await
319    }
320
321    /// Get the list of emails, chronologically sorted
322    async fn emails(
323        &self,
324        ctx: &Context<'_>,
325
326        #[graphql(
327            deprecation = "Emails are always confirmed, and have only one state",
328            name = "state",
329            desc = "List only emails in the given state."
330        )]
331        state_param: Option<UserEmailState>,
332
333        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
334        after: Option<String>,
335        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
336        before: Option<String>,
337        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
338        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
339    ) -> Result<Connection<Cursor, UserEmail, PreloadedTotalCount>, async_graphql::Error> {
340        let state = ctx.state();
341        let mut repo = state.repository().await?;
342        let _ = state_param;
343
344        query(
345            after,
346            before,
347            first,
348            last,
349            async |after, before, first, last| {
350                let after_id = after
351                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
352                    .transpose()?;
353                let before_id = before
354                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
355                    .transpose()?;
356                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
357
358                let filter = UserEmailFilter::new().for_user(&self.0);
359
360                let page = repo.user_email().list(filter, pagination).await?;
361
362                // Preload the total count if requested
363                let count = if ctx.look_ahead().field("totalCount").exists() {
364                    Some(repo.user_email().count(filter).await?)
365                } else {
366                    None
367                };
368
369                repo.cancel().await?;
370
371                let mut connection = Connection::with_additional_fields(
372                    page.has_previous_page,
373                    page.has_next_page,
374                    PreloadedTotalCount(count),
375                );
376                connection.edges.extend(page.edges.into_iter().map(|u| {
377                    Edge::new(
378                        OpaqueCursor(NodeCursor(NodeType::UserEmail, u.id)),
379                        UserEmail(u),
380                    )
381                }));
382
383                Ok::<_, async_graphql::Error>(connection)
384            },
385        )
386        .await
387    }
388
389    /// Get the list of OAuth 2.0 sessions, chronologically sorted
390    #[allow(clippy::too_many_arguments)]
391    async fn oauth2_sessions(
392        &self,
393        ctx: &Context<'_>,
394
395        #[graphql(name = "state", desc = "List only sessions in the given state.")]
396        state_param: Option<SessionState>,
397
398        #[graphql(desc = "List only sessions for the given client.")] client: Option<ID>,
399
400        #[graphql(
401            name = "lastActive",
402            desc = "List only sessions with a last active time is between the given bounds."
403        )]
404        last_active: Option<DateFilter>,
405
406        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
407        after: Option<String>,
408        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
409        before: Option<String>,
410        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
411        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
412    ) -> Result<Connection<Cursor, OAuth2Session, PreloadedTotalCount>, async_graphql::Error> {
413        let state = ctx.state();
414        let mut repo = state.repository().await?;
415        let last_active = last_active.unwrap_or_default();
416
417        query(
418            after,
419            before,
420            first,
421            last,
422            async |after, before, first, last| {
423                let after_id = after
424                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
425                    .transpose()?;
426                let before_id = before
427                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
428                    .transpose()?;
429                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
430
431                let client = if let Some(id) = client {
432                    // Load the client if we're filtering by it
433                    let id = NodeType::OAuth2Client.extract_ulid(&id)?;
434                    let client = repo
435                        .oauth2_client()
436                        .lookup(id)
437                        .await?
438                        .ok_or(async_graphql::Error::new("Unknown client ID"))?;
439
440                    Some(client)
441                } else {
442                    None
443                };
444
445                let filter = OAuth2SessionFilter::new().for_user(&self.0);
446
447                let filter = match state_param {
448                    Some(SessionState::Active) => filter.active_only(),
449                    Some(SessionState::Finished) => filter.finished_only(),
450                    None => filter,
451                };
452
453                let filter = match client.as_ref() {
454                    Some(client) => filter.for_client(client),
455                    None => filter,
456                };
457
458                let filter = match last_active.after {
459                    Some(after) => filter.with_last_active_after(after),
460                    None => filter,
461                };
462                let filter = match last_active.before {
463                    Some(before) => filter.with_last_active_before(before),
464                    None => filter,
465                };
466
467                let page = repo.oauth2_session().list(filter, pagination).await?;
468
469                let count = if ctx.look_ahead().field("totalCount").exists() {
470                    Some(repo.oauth2_session().count(filter).await?)
471                } else {
472                    None
473                };
474
475                repo.cancel().await?;
476
477                let mut connection = Connection::with_additional_fields(
478                    page.has_previous_page,
479                    page.has_next_page,
480                    PreloadedTotalCount(count),
481                );
482
483                connection.edges.extend(page.edges.into_iter().map(|s| {
484                    Edge::new(
485                        OpaqueCursor(NodeCursor(NodeType::OAuth2Session, s.id)),
486                        OAuth2Session(s),
487                    )
488                }));
489
490                Ok::<_, async_graphql::Error>(connection)
491            },
492        )
493        .await
494    }
495
496    /// Get the list of upstream OAuth 2.0 links
497    async fn upstream_oauth2_links(
498        &self,
499        ctx: &Context<'_>,
500
501        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
502        after: Option<String>,
503        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
504        before: Option<String>,
505        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
506        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
507    ) -> Result<Connection<Cursor, UpstreamOAuth2Link, PreloadedTotalCount>, async_graphql::Error>
508    {
509        let state = ctx.state();
510        let mut repo = state.repository().await?;
511
512        query(
513            after,
514            before,
515            first,
516            last,
517            async |after, before, first, last| {
518                let after_id = after
519                    .map(|x: OpaqueCursor<NodeCursor>| {
520                        x.extract_for_type(NodeType::UpstreamOAuth2Link)
521                    })
522                    .transpose()?;
523                let before_id = before
524                    .map(|x: OpaqueCursor<NodeCursor>| {
525                        x.extract_for_type(NodeType::UpstreamOAuth2Link)
526                    })
527                    .transpose()?;
528                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
529
530                let filter = UpstreamOAuthLinkFilter::new()
531                    .for_user(&self.0)
532                    .enabled_providers_only();
533
534                let page = repo.upstream_oauth_link().list(filter, pagination).await?;
535
536                // Preload the total count if requested
537                let count = if ctx.look_ahead().field("totalCount").exists() {
538                    Some(repo.upstream_oauth_link().count(filter).await?)
539                } else {
540                    None
541                };
542
543                repo.cancel().await?;
544
545                let mut connection = Connection::with_additional_fields(
546                    page.has_previous_page,
547                    page.has_next_page,
548                    PreloadedTotalCount(count),
549                );
550                connection.edges.extend(page.edges.into_iter().map(|s| {
551                    Edge::new(
552                        OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, s.id)),
553                        UpstreamOAuth2Link::new(s),
554                    )
555                }));
556
557                Ok::<_, async_graphql::Error>(connection)
558            },
559        )
560        .await
561    }
562
563    /// Get the list of both compat and OAuth 2.0 sessions, chronologically
564    /// sorted
565    #[allow(clippy::too_many_arguments)]
566    async fn app_sessions(
567        &self,
568        ctx: &Context<'_>,
569
570        #[graphql(name = "state", desc = "List only sessions in the given state.")]
571        state_param: Option<SessionState>,
572
573        #[graphql(name = "device", desc = "List only sessions for the given device.")]
574        device_param: Option<String>,
575
576        #[graphql(
577            name = "lastActive",
578            desc = "List only sessions with a last active time is between the given bounds."
579        )]
580        last_active: Option<DateFilter>,
581
582        #[graphql(
583            name = "browserSession",
584            desc = "List only sessions for the given session."
585        )]
586        browser_session_param: Option<ID>,
587
588        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
589        after: Option<String>,
590        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
591        before: Option<String>,
592        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
593        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
594    ) -> Result<Connection<Cursor, AppSession, PreloadedTotalCount>, async_graphql::Error> {
595        let state = ctx.state();
596        let requester = ctx.requester();
597        let mut repo = state.repository().await?;
598        let last_active = last_active.unwrap_or_default();
599
600        query(
601            after,
602            before,
603            first,
604            last,
605            async |after, before, first, last| {
606                let after_id = after
607                    .map(|x: OpaqueCursor<NodeCursor>| {
608                        x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
609                    })
610                    .transpose()?;
611                let before_id = before
612                    .map(|x: OpaqueCursor<NodeCursor>| {
613                        x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
614                    })
615                    .transpose()?;
616                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
617
618                let device_param = device_param.map(Device::try_from).transpose()?;
619
620                let filter = AppSessionFilter::new().for_user(&self.0);
621
622                let filter = match state_param {
623                    Some(SessionState::Active) => filter.active_only(),
624                    Some(SessionState::Finished) => filter.finished_only(),
625                    None => filter,
626                };
627
628                let filter = match device_param.as_ref() {
629                    Some(device) => filter.for_device(device),
630                    None => filter,
631                };
632
633                let maybe_session = match browser_session_param {
634                    Some(id) => {
635                        // This might fail, but we're probably alright with it
636                        let id = NodeType::BrowserSession
637                            .extract_ulid(&id)
638                            .context("Invalid browser_session parameter")?;
639
640                        let Some(session) = repo
641                            .browser_session()
642                            .lookup(id)
643                            .await?
644                            .filter(|u| requester.is_owner_or_admin(u))
645                        else {
646                            // If we couldn't find the session or if the requester can't access it,
647                            // return an empty list
648                            return Ok(Connection::with_additional_fields(
649                                false,
650                                false,
651                                PreloadedTotalCount(Some(0)),
652                            ));
653                        };
654
655                        Some(session)
656                    }
657                    None => None,
658                };
659
660                let filter = match maybe_session {
661                    Some(ref session) => filter.for_browser_session(session),
662                    None => filter,
663                };
664
665                let filter = match last_active.after {
666                    Some(after) => filter.with_last_active_after(after),
667                    None => filter,
668                };
669                let filter = match last_active.before {
670                    Some(before) => filter.with_last_active_before(before),
671                    None => filter,
672                };
673
674                let page = repo.app_session().list(filter, pagination).await?;
675
676                let count = if ctx.look_ahead().field("totalCount").exists() {
677                    Some(repo.app_session().count(filter).await?)
678                } else {
679                    None
680                };
681
682                repo.cancel().await?;
683
684                let mut connection = Connection::with_additional_fields(
685                    page.has_previous_page,
686                    page.has_next_page,
687                    PreloadedTotalCount(count),
688                );
689
690                connection
691                    .edges
692                    .extend(page.edges.into_iter().map(|s| match s {
693                        mas_storage::app_session::AppSession::Compat(session) => Edge::new(
694                            OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
695                            AppSession::CompatSession(Box::new(CompatSession::new(*session))),
696                        ),
697                        mas_storage::app_session::AppSession::OAuth2(session) => Edge::new(
698                            OpaqueCursor(NodeCursor(NodeType::OAuth2Session, session.id)),
699                            AppSession::OAuth2Session(Box::new(OAuth2Session(*session))),
700                        ),
701                    }));
702
703                Ok::<_, async_graphql::Error>(connection)
704            },
705        )
706        .await
707    }
708
709    /// Check if the user has a password set.
710    async fn has_password(&self, ctx: &Context<'_>) -> Result<bool, async_graphql::Error> {
711        let state = ctx.state();
712        let mut repo = state.repository().await?;
713
714        let password = repo.user_password().active(&self.0).await?;
715
716        Ok(password.is_some())
717    }
718}
719
720/// A session in an application, either a compatibility or an OAuth 2.0 one
721#[derive(Union)]
722pub enum AppSession {
723    CompatSession(Box<CompatSession>),
724    OAuth2Session(Box<OAuth2Session>),
725}
726
727/// A user email address
728#[derive(Description)]
729pub struct UserEmail(pub mas_data_model::UserEmail);
730
731#[Object(use_type_description)]
732impl UserEmail {
733    /// ID of the object.
734    pub async fn id(&self) -> ID {
735        NodeType::UserEmail.id(self.0.id)
736    }
737
738    /// Email address
739    async fn email(&self) -> &str {
740        &self.0.email
741    }
742
743    /// When the object was created.
744    pub async fn created_at(&self) -> DateTime<Utc> {
745        self.0.created_at
746    }
747
748    /// When the email address was confirmed. Is `null` if the email was never
749    /// verified by the user.
750    #[graphql(deprecation = "Emails are always confirmed now.")]
751    async fn confirmed_at(&self) -> Option<DateTime<Utc>> {
752        Some(self.0.created_at)
753    }
754}
755
756/// The state of a compatibility session.
757#[derive(Enum, Copy, Clone, Eq, PartialEq)]
758pub enum UserEmailState {
759    /// The email address is pending confirmation.
760    Pending,
761
762    /// The email address has been confirmed.
763    Confirmed,
764}
765
766/// A recovery ticket
767#[derive(Description)]
768pub struct UserRecoveryTicket(pub mas_data_model::UserRecoveryTicket);
769
770/// The status of a recovery ticket
771#[derive(Enum, Copy, Clone, Eq, PartialEq)]
772pub enum UserRecoveryTicketStatus {
773    /// The ticket is valid
774    Valid,
775
776    /// The ticket has expired
777    Expired,
778
779    /// The ticket has been consumed
780    Consumed,
781}
782
783#[Object(use_type_description)]
784impl UserRecoveryTicket {
785    /// ID of the object.
786    pub async fn id(&self) -> ID {
787        NodeType::UserRecoveryTicket.id(self.0.id)
788    }
789
790    /// When the object was created.
791    pub async fn created_at(&self) -> DateTime<Utc> {
792        self.0.created_at
793    }
794
795    /// The status of the ticket
796    pub async fn status(
797        &self,
798        context: &Context<'_>,
799    ) -> Result<UserRecoveryTicketStatus, async_graphql::Error> {
800        let state = context.state();
801        let clock = state.clock();
802        let mut repo = state.repository().await?;
803
804        // Lookup the session associated with the ticket
805        let session = repo
806            .user_recovery()
807            .lookup_session(self.0.user_recovery_session_id)
808            .await?
809            .context("Failed to lookup session")?;
810        repo.cancel().await?;
811
812        if session.consumed_at.is_some() {
813            return Ok(UserRecoveryTicketStatus::Consumed);
814        }
815
816        if self.0.expires_at < clock.now() {
817            return Ok(UserRecoveryTicketStatus::Expired);
818        }
819
820        Ok(UserRecoveryTicketStatus::Valid)
821    }
822
823    /// The username associated with this ticket
824    pub async fn username(&self, ctx: &Context<'_>) -> Result<String, async_graphql::Error> {
825        // We could expose the UserEmail, then the User, but this is unauthenticated, so
826        // we don't want to risk leaking too many objects. Instead, we just give the
827        // username as a property of the UserRecoveryTicket
828        let state = ctx.state();
829        let mut repo = state.repository().await?;
830        let user_email = repo
831            .user_email()
832            .lookup(self.0.user_email_id)
833            .await?
834            .context("Failed to lookup user email")?;
835
836        let user = repo
837            .user()
838            .lookup(user_email.user_id)
839            .await?
840            .context("Failed to lookup user")?;
841        repo.cancel().await?;
842
843        Ok(user.username)
844    }
845
846    /// The email address associated with this ticket
847    pub async fn email(&self, ctx: &Context<'_>) -> Result<String, async_graphql::Error> {
848        // We could expose the UserEmail directly, but this is unauthenticated, so we
849        // don't want to risk leaking too many objects. Instead, we just give
850        // the email as a property of the UserRecoveryTicket
851        let state = ctx.state();
852        let mut repo = state.repository().await?;
853        let user_email = repo
854            .user_email()
855            .lookup(self.0.user_email_id)
856            .await?
857            .context("Failed to lookup user email")?;
858        repo.cancel().await?;
859
860        Ok(user_email.email)
861    }
862}
863
864/// A email authentication session
865#[derive(Description)]
866pub struct UserEmailAuthentication(pub mas_data_model::UserEmailAuthentication);
867
868#[Object(use_type_description)]
869impl UserEmailAuthentication {
870    /// ID of the object.
871    pub async fn id(&self) -> ID {
872        NodeType::UserEmailAuthentication.id(self.0.id)
873    }
874
875    /// When the object was created.
876    pub async fn created_at(&self) -> DateTime<Utc> {
877        self.0.created_at
878    }
879
880    /// When the object was last updated.
881    pub async fn completed_at(&self) -> Option<DateTime<Utc>> {
882        self.0.completed_at
883    }
884
885    /// The email address associated with this session
886    pub async fn email(&self) -> &str {
887        &self.0.email
888    }
889}