1use 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)]
32pub 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 pub async fn id(&self) -> ID {
51 NodeType::User.id(self.0.id)
52 }
53
54 async fn username(&self) -> &str {
56 &self.0.username
57 }
58
59 pub async fn created_at(&self) -> DateTime<Utc> {
61 self.0.created_at
62 }
63
64 pub async fn locked_at(&self) -> Option<DateTime<Utc>> {
66 self.0.locked_at
67 }
68
69 pub async fn can_request_admin(&self) -> bool {
71 self.0.can_request_admin
72 }
73
74 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 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 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 #[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 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 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 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 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 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 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 #[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 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 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 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 #[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 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 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 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#[derive(Union)]
722pub enum AppSession {
723 CompatSession(Box<CompatSession>),
724 OAuth2Session(Box<OAuth2Session>),
725}
726
727#[derive(Description)]
729pub struct UserEmail(pub mas_data_model::UserEmail);
730
731#[Object(use_type_description)]
732impl UserEmail {
733 pub async fn id(&self) -> ID {
735 NodeType::UserEmail.id(self.0.id)
736 }
737
738 async fn email(&self) -> &str {
740 &self.0.email
741 }
742
743 pub async fn created_at(&self) -> DateTime<Utc> {
745 self.0.created_at
746 }
747
748 #[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#[derive(Enum, Copy, Clone, Eq, PartialEq)]
758pub enum UserEmailState {
759 Pending,
761
762 Confirmed,
764}
765
766#[derive(Description)]
768pub struct UserRecoveryTicket(pub mas_data_model::UserRecoveryTicket);
769
770#[derive(Enum, Copy, Clone, Eq, PartialEq)]
772pub enum UserRecoveryTicketStatus {
773 Valid,
775
776 Expired,
778
779 Consumed,
781}
782
783#[Object(use_type_description)]
784impl UserRecoveryTicket {
785 pub async fn id(&self) -> ID {
787 NodeType::UserRecoveryTicket.id(self.0.id)
788 }
789
790 pub async fn created_at(&self) -> DateTime<Utc> {
792 self.0.created_at
793 }
794
795 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 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 pub async fn username(&self, ctx: &Context<'_>) -> Result<String, async_graphql::Error> {
825 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 pub async fn email(&self, ctx: &Context<'_>) -> Result<String, async_graphql::Error> {
848 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#[derive(Description)]
866pub struct UserEmailAuthentication(pub mas_data_model::UserEmailAuthentication);
867
868#[Object(use_type_description)]
869impl UserEmailAuthentication {
870 pub async fn id(&self) -> ID {
872 NodeType::UserEmailAuthentication.id(self.0.id)
873 }
874
875 pub async fn created_at(&self) -> DateTime<Utc> {
877 self.0.created_at
878 }
879
880 pub async fn completed_at(&self) -> Option<DateTime<Utc>> {
882 self.0.completed_at
883 }
884
885 pub async fn email(&self) -> &str {
887 &self.0.email
888 }
889}