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(|edge| {
129 Edge::new(
130 OpaqueCursor(NodeCursor(NodeType::CompatSsoLogin, edge.cursor)),
131 CompatSsoLogin(edge.node),
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.edges.extend(page.edges.into_iter().map(|edge| {
223 let (session, sso_login) = edge.node;
224 Edge::new(
225 OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
226 CompatSession::new(session).with_loaded_sso_login(sso_login),
227 )
228 }));
229
230 Ok::<_, async_graphql::Error>(connection)
231 },
232 )
233 .await
234 }
235
236 async fn browser_sessions(
238 &self,
239 ctx: &Context<'_>,
240
241 #[graphql(name = "state", desc = "List only sessions in the given state.")]
242 state_param: Option<SessionState>,
243
244 #[graphql(
245 name = "lastActive",
246 desc = "List only sessions with a last active time is between the given bounds."
247 )]
248 last_active: Option<DateFilter>,
249
250 #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
251 after: Option<String>,
252 #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
253 before: Option<String>,
254 #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
255 #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
256 ) -> Result<Connection<Cursor, BrowserSession, PreloadedTotalCount>, async_graphql::Error> {
257 let state = ctx.state();
258 let mut repo = state.repository().await?;
259 let last_active = last_active.unwrap_or_default();
260
261 query(
262 after,
263 before,
264 first,
265 last,
266 async |after, before, first, last| {
267 let after_id = after
268 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::BrowserSession))
269 .transpose()?;
270 let before_id = before
271 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::BrowserSession))
272 .transpose()?;
273 let pagination = Pagination::try_new(before_id, after_id, first, last)?;
274
275 let filter = BrowserSessionFilter::new().for_user(&self.0);
276 let filter = match state_param {
277 Some(SessionState::Active) => filter.active_only(),
278 Some(SessionState::Finished) => filter.finished_only(),
279 None => filter,
280 };
281
282 let filter = match last_active.after {
283 Some(after) => filter.with_last_active_after(after),
284 None => filter,
285 };
286 let filter = match last_active.before {
287 Some(before) => filter.with_last_active_before(before),
288 None => filter,
289 };
290
291 let page = repo.browser_session().list(filter, pagination).await?;
292
293 let count = if ctx.look_ahead().field("totalCount").exists() {
295 Some(repo.browser_session().count(filter).await?)
296 } else {
297 None
298 };
299
300 repo.cancel().await?;
301
302 let mut connection = Connection::with_additional_fields(
303 page.has_previous_page,
304 page.has_next_page,
305 PreloadedTotalCount(count),
306 );
307 connection.edges.extend(page.edges.into_iter().map(|edge| {
308 Edge::new(
309 OpaqueCursor(NodeCursor(NodeType::BrowserSession, edge.cursor)),
310 BrowserSession(edge.node),
311 )
312 }));
313
314 Ok::<_, async_graphql::Error>(connection)
315 },
316 )
317 .await
318 }
319
320 async fn emails(
322 &self,
323 ctx: &Context<'_>,
324
325 #[graphql(
326 deprecation = "Emails are always confirmed, and have only one state",
327 name = "state",
328 desc = "List only emails in the given state."
329 )]
330 state_param: Option<UserEmailState>,
331
332 #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
333 after: Option<String>,
334 #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
335 before: Option<String>,
336 #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
337 #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
338 ) -> Result<Connection<Cursor, UserEmail, PreloadedTotalCount>, async_graphql::Error> {
339 let state = ctx.state();
340 let mut repo = state.repository().await?;
341 let _ = state_param;
342
343 query(
344 after,
345 before,
346 first,
347 last,
348 async |after, before, first, last| {
349 let after_id = after
350 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
351 .transpose()?;
352 let before_id = before
353 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::UserEmail))
354 .transpose()?;
355 let pagination = Pagination::try_new(before_id, after_id, first, last)?;
356
357 let filter = UserEmailFilter::new().for_user(&self.0);
358
359 let page = repo.user_email().list(filter, pagination).await?;
360
361 let count = if ctx.look_ahead().field("totalCount").exists() {
363 Some(repo.user_email().count(filter).await?)
364 } else {
365 None
366 };
367
368 repo.cancel().await?;
369
370 let mut connection = Connection::with_additional_fields(
371 page.has_previous_page,
372 page.has_next_page,
373 PreloadedTotalCount(count),
374 );
375 connection.edges.extend(page.edges.into_iter().map(|edge| {
376 Edge::new(
377 OpaqueCursor(NodeCursor(NodeType::UserEmail, edge.cursor)),
378 UserEmail(edge.node),
379 )
380 }));
381
382 Ok::<_, async_graphql::Error>(connection)
383 },
384 )
385 .await
386 }
387
388 #[allow(clippy::too_many_arguments)]
390 async fn oauth2_sessions(
391 &self,
392 ctx: &Context<'_>,
393
394 #[graphql(name = "state", desc = "List only sessions in the given state.")]
395 state_param: Option<SessionState>,
396
397 #[graphql(desc = "List only sessions for the given client.")] client: Option<ID>,
398
399 #[graphql(
400 name = "lastActive",
401 desc = "List only sessions with a last active time is between the given bounds."
402 )]
403 last_active: Option<DateFilter>,
404
405 #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
406 after: Option<String>,
407 #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
408 before: Option<String>,
409 #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
410 #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
411 ) -> Result<Connection<Cursor, OAuth2Session, PreloadedTotalCount>, async_graphql::Error> {
412 let state = ctx.state();
413 let mut repo = state.repository().await?;
414 let last_active = last_active.unwrap_or_default();
415
416 query(
417 after,
418 before,
419 first,
420 last,
421 async |after, before, first, last| {
422 let after_id = after
423 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
424 .transpose()?;
425 let before_id = before
426 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::OAuth2Session))
427 .transpose()?;
428 let pagination = Pagination::try_new(before_id, after_id, first, last)?;
429
430 let client = if let Some(id) = client {
431 let id = NodeType::OAuth2Client.extract_ulid(&id)?;
433 let client = repo
434 .oauth2_client()
435 .lookup(id)
436 .await?
437 .ok_or(async_graphql::Error::new("Unknown client ID"))?;
438
439 Some(client)
440 } else {
441 None
442 };
443
444 let filter = OAuth2SessionFilter::new().for_user(&self.0);
445
446 let filter = match state_param {
447 Some(SessionState::Active) => filter.active_only(),
448 Some(SessionState::Finished) => filter.finished_only(),
449 None => filter,
450 };
451
452 let filter = match client.as_ref() {
453 Some(client) => filter.for_client(client),
454 None => filter,
455 };
456
457 let filter = match last_active.after {
458 Some(after) => filter.with_last_active_after(after),
459 None => filter,
460 };
461 let filter = match last_active.before {
462 Some(before) => filter.with_last_active_before(before),
463 None => filter,
464 };
465
466 let page = repo.oauth2_session().list(filter, pagination).await?;
467
468 let count = if ctx.look_ahead().field("totalCount").exists() {
469 Some(repo.oauth2_session().count(filter).await?)
470 } else {
471 None
472 };
473
474 repo.cancel().await?;
475
476 let mut connection = Connection::with_additional_fields(
477 page.has_previous_page,
478 page.has_next_page,
479 PreloadedTotalCount(count),
480 );
481
482 connection.edges.extend(page.edges.into_iter().map(|edge| {
483 Edge::new(
484 OpaqueCursor(NodeCursor(NodeType::OAuth2Session, edge.cursor)),
485 OAuth2Session(edge.node),
486 )
487 }));
488
489 Ok::<_, async_graphql::Error>(connection)
490 },
491 )
492 .await
493 }
494
495 async fn upstream_oauth2_links(
497 &self,
498 ctx: &Context<'_>,
499
500 #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
501 after: Option<String>,
502 #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
503 before: Option<String>,
504 #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
505 #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
506 ) -> Result<Connection<Cursor, UpstreamOAuth2Link, PreloadedTotalCount>, async_graphql::Error>
507 {
508 let state = ctx.state();
509 let mut repo = state.repository().await?;
510
511 query(
512 after,
513 before,
514 first,
515 last,
516 async |after, before, first, last| {
517 let after_id = after
518 .map(|x: OpaqueCursor<NodeCursor>| {
519 x.extract_for_type(NodeType::UpstreamOAuth2Link)
520 })
521 .transpose()?;
522 let before_id = before
523 .map(|x: OpaqueCursor<NodeCursor>| {
524 x.extract_for_type(NodeType::UpstreamOAuth2Link)
525 })
526 .transpose()?;
527 let pagination = Pagination::try_new(before_id, after_id, first, last)?;
528
529 let filter = UpstreamOAuthLinkFilter::new()
530 .for_user(&self.0)
531 .enabled_providers_only();
532
533 let page = repo.upstream_oauth_link().list(filter, pagination).await?;
534
535 let count = if ctx.look_ahead().field("totalCount").exists() {
537 Some(repo.upstream_oauth_link().count(filter).await?)
538 } else {
539 None
540 };
541
542 repo.cancel().await?;
543
544 let mut connection = Connection::with_additional_fields(
545 page.has_previous_page,
546 page.has_next_page,
547 PreloadedTotalCount(count),
548 );
549 connection.edges.extend(page.edges.into_iter().map(|edge| {
550 Edge::new(
551 OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Link, edge.cursor)),
552 UpstreamOAuth2Link::new(edge.node),
553 )
554 }));
555
556 Ok::<_, async_graphql::Error>(connection)
557 },
558 )
559 .await
560 }
561
562 #[allow(clippy::too_many_arguments)]
565 async fn app_sessions(
566 &self,
567 ctx: &Context<'_>,
568
569 #[graphql(name = "state", desc = "List only sessions in the given state.")]
570 state_param: Option<SessionState>,
571
572 #[graphql(name = "device", desc = "List only sessions for the given device.")]
573 device_param: Option<String>,
574
575 #[graphql(
576 name = "lastActive",
577 desc = "List only sessions with a last active time is between the given bounds."
578 )]
579 last_active: Option<DateFilter>,
580
581 #[graphql(
582 name = "browserSession",
583 desc = "List only sessions for the given session."
584 )]
585 browser_session_param: Option<ID>,
586
587 #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
588 after: Option<String>,
589 #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
590 before: Option<String>,
591 #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
592 #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
593 ) -> Result<Connection<Cursor, AppSession, PreloadedTotalCount>, async_graphql::Error> {
594 let state = ctx.state();
595 let requester = ctx.requester();
596 let mut repo = state.repository().await?;
597 let last_active = last_active.unwrap_or_default();
598
599 query(
600 after,
601 before,
602 first,
603 last,
604 async |after, before, first, last| {
605 let after_id = after
606 .map(|x: OpaqueCursor<NodeCursor>| {
607 x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
608 })
609 .transpose()?;
610 let before_id = before
611 .map(|x: OpaqueCursor<NodeCursor>| {
612 x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
613 })
614 .transpose()?;
615 let pagination = Pagination::try_new(before_id, after_id, first, last)?;
616
617 let device_param = device_param.map(Device::try_from).transpose()?;
618
619 let filter = AppSessionFilter::new().for_user(&self.0);
620
621 let filter = match state_param {
622 Some(SessionState::Active) => filter.active_only(),
623 Some(SessionState::Finished) => filter.finished_only(),
624 None => filter,
625 };
626
627 let filter = match device_param.as_ref() {
628 Some(device) => filter.for_device(device),
629 None => filter,
630 };
631
632 let maybe_session = match browser_session_param {
633 Some(id) => {
634 let id = NodeType::BrowserSession
636 .extract_ulid(&id)
637 .context("Invalid browser_session parameter")?;
638
639 let Some(session) = repo
640 .browser_session()
641 .lookup(id)
642 .await?
643 .filter(|u| requester.is_owner_or_admin(u))
644 else {
645 return Ok(Connection::with_additional_fields(
648 false,
649 false,
650 PreloadedTotalCount(Some(0)),
651 ));
652 };
653
654 Some(session)
655 }
656 None => None,
657 };
658
659 let filter = match maybe_session {
660 Some(ref session) => filter.for_browser_session(session),
661 None => filter,
662 };
663
664 let filter = match last_active.after {
665 Some(after) => filter.with_last_active_after(after),
666 None => filter,
667 };
668 let filter = match last_active.before {
669 Some(before) => filter.with_last_active_before(before),
670 None => filter,
671 };
672
673 let page = repo.app_session().list(filter, pagination).await?;
674
675 let count = if ctx.look_ahead().field("totalCount").exists() {
676 Some(repo.app_session().count(filter).await?)
677 } else {
678 None
679 };
680
681 repo.cancel().await?;
682
683 let mut connection = Connection::with_additional_fields(
684 page.has_previous_page,
685 page.has_next_page,
686 PreloadedTotalCount(count),
687 );
688
689 connection
690 .edges
691 .extend(page.edges.into_iter().map(|edge| match edge.node {
692 mas_storage::app_session::AppSession::Compat(session) => Edge::new(
693 OpaqueCursor(NodeCursor(NodeType::CompatSession, edge.cursor)),
694 AppSession::CompatSession(Box::new(CompatSession::new(*session))),
695 ),
696 mas_storage::app_session::AppSession::OAuth2(session) => Edge::new(
697 OpaqueCursor(NodeCursor(NodeType::OAuth2Session, edge.cursor)),
698 AppSession::OAuth2Session(Box::new(OAuth2Session(*session))),
699 ),
700 }));
701
702 Ok::<_, async_graphql::Error>(connection)
703 },
704 )
705 .await
706 }
707
708 async fn has_password(&self, ctx: &Context<'_>) -> Result<bool, async_graphql::Error> {
710 let state = ctx.state();
711 let mut repo = state.repository().await?;
712
713 let password = repo.user_password().active(&self.0).await?;
714
715 Ok(password.is_some())
716 }
717}
718
719#[derive(Union)]
721pub enum AppSession {
722 CompatSession(Box<CompatSession>),
723 OAuth2Session(Box<OAuth2Session>),
724}
725
726#[derive(Description)]
728pub struct UserEmail(pub mas_data_model::UserEmail);
729
730#[Object(use_type_description)]
731impl UserEmail {
732 pub async fn id(&self) -> ID {
734 NodeType::UserEmail.id(self.0.id)
735 }
736
737 async fn email(&self) -> &str {
739 &self.0.email
740 }
741
742 pub async fn created_at(&self) -> DateTime<Utc> {
744 self.0.created_at
745 }
746
747 #[graphql(deprecation = "Emails are always confirmed now.")]
750 async fn confirmed_at(&self) -> Option<DateTime<Utc>> {
751 Some(self.0.created_at)
752 }
753}
754
755#[derive(Enum, Copy, Clone, Eq, PartialEq)]
757pub enum UserEmailState {
758 Pending,
760
761 Confirmed,
763}
764
765#[derive(Description)]
767pub struct UserRecoveryTicket(pub mas_data_model::UserRecoveryTicket);
768
769#[derive(Enum, Copy, Clone, Eq, PartialEq)]
771pub enum UserRecoveryTicketStatus {
772 Valid,
774
775 Expired,
777
778 Consumed,
780}
781
782#[Object(use_type_description)]
783impl UserRecoveryTicket {
784 pub async fn id(&self) -> ID {
786 NodeType::UserRecoveryTicket.id(self.0.id)
787 }
788
789 pub async fn created_at(&self) -> DateTime<Utc> {
791 self.0.created_at
792 }
793
794 pub async fn status(
796 &self,
797 context: &Context<'_>,
798 ) -> Result<UserRecoveryTicketStatus, async_graphql::Error> {
799 let state = context.state();
800 let clock = state.clock();
801 let mut repo = state.repository().await?;
802
803 let session = repo
805 .user_recovery()
806 .lookup_session(self.0.user_recovery_session_id)
807 .await?
808 .context("Failed to lookup session")?;
809 repo.cancel().await?;
810
811 if session.consumed_at.is_some() {
812 return Ok(UserRecoveryTicketStatus::Consumed);
813 }
814
815 if self.0.expires_at < clock.now() {
816 return Ok(UserRecoveryTicketStatus::Expired);
817 }
818
819 Ok(UserRecoveryTicketStatus::Valid)
820 }
821
822 pub async fn username(&self, ctx: &Context<'_>) -> Result<String, async_graphql::Error> {
824 let state = ctx.state();
828 let mut repo = state.repository().await?;
829 let user_email = repo
830 .user_email()
831 .lookup(self.0.user_email_id)
832 .await?
833 .context("Failed to lookup user email")?;
834
835 let user = repo
836 .user()
837 .lookup(user_email.user_id)
838 .await?
839 .context("Failed to lookup user")?;
840 repo.cancel().await?;
841
842 Ok(user.username)
843 }
844
845 pub async fn email(&self, ctx: &Context<'_>) -> Result<String, async_graphql::Error> {
847 let state = ctx.state();
851 let mut repo = state.repository().await?;
852 let user_email = repo
853 .user_email()
854 .lookup(self.0.user_email_id)
855 .await?
856 .context("Failed to lookup user email")?;
857 repo.cancel().await?;
858
859 Ok(user_email.email)
860 }
861}
862
863#[derive(Description)]
865pub struct UserEmailAuthentication(pub mas_data_model::UserEmailAuthentication);
866
867#[Object(use_type_description)]
868impl UserEmailAuthentication {
869 pub async fn id(&self) -> ID {
871 NodeType::UserEmailAuthentication.id(self.0.id)
872 }
873
874 pub async fn created_at(&self) -> DateTime<Utc> {
876 self.0.created_at
877 }
878
879 pub async fn completed_at(&self) -> Option<DateTime<Utc>> {
881 self.0.completed_at
882 }
883
884 pub async fn email(&self) -> &str {
886 &self.0.email
887 }
888}