mas_handlers/admin/
model.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
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 chrono::{DateTime, Utc};
10use mas_data_model::{
11    Device,
12    personal::{
13        PersonalAccessToken as DataModelPersonalAccessToken,
14        session::{PersonalSession as DataModelPersonalSession, PersonalSessionOwner},
15    },
16};
17use schemars::JsonSchema;
18use serde::Serialize;
19use thiserror::Error;
20use ulid::Ulid;
21use url::Url;
22
23/// A resource, with a type and an ID
24pub trait Resource {
25    /// The type of the resource
26    const KIND: &'static str;
27
28    /// The canonical path prefix for this kind of resource
29    const PATH: &'static str;
30
31    /// The ID of the resource
32    fn id(&self) -> Ulid;
33
34    /// The canonical path for this resource
35    ///
36    /// This is the concatenation of the canonical path prefix and the ID
37    fn path(&self) -> String {
38        format!("{}/{}", Self::PATH, self.id())
39    }
40}
41
42/// A user
43#[derive(Serialize, JsonSchema)]
44pub struct User {
45    #[serde(skip)]
46    id: Ulid,
47
48    /// The username (localpart) of the user
49    username: String,
50
51    /// When the user was created
52    created_at: DateTime<Utc>,
53
54    /// When the user was locked. If null, the user is not locked.
55    locked_at: Option<DateTime<Utc>>,
56
57    /// When the user was deactivated. If null, the user is not deactivated.
58    deactivated_at: Option<DateTime<Utc>>,
59
60    /// Whether the user can request admin privileges.
61    admin: bool,
62
63    /// Whether the user was a guest before migrating to MAS,
64    legacy_guest: bool,
65}
66
67impl User {
68    /// Samples of users with different properties for examples in the schema
69    pub fn samples() -> [Self; 3] {
70        [
71            Self {
72                id: Ulid::from_bytes([0x01; 16]),
73                username: "alice".to_owned(),
74                created_at: DateTime::default(),
75                locked_at: None,
76                deactivated_at: None,
77                admin: false,
78                legacy_guest: false,
79            },
80            Self {
81                id: Ulid::from_bytes([0x02; 16]),
82                username: "bob".to_owned(),
83                created_at: DateTime::default(),
84                locked_at: None,
85                deactivated_at: None,
86                admin: true,
87                legacy_guest: false,
88            },
89            Self {
90                id: Ulid::from_bytes([0x03; 16]),
91                username: "charlie".to_owned(),
92                created_at: DateTime::default(),
93                locked_at: Some(DateTime::default()),
94                deactivated_at: None,
95                admin: false,
96                legacy_guest: true,
97            },
98        ]
99    }
100}
101
102impl From<mas_data_model::User> for User {
103    fn from(user: mas_data_model::User) -> Self {
104        Self {
105            id: user.id,
106            username: user.username,
107            created_at: user.created_at,
108            locked_at: user.locked_at,
109            deactivated_at: user.deactivated_at,
110            admin: user.can_request_admin,
111            legacy_guest: user.is_guest,
112        }
113    }
114}
115
116impl Resource for User {
117    const KIND: &'static str = "user";
118    const PATH: &'static str = "/api/admin/v1/users";
119
120    fn id(&self) -> Ulid {
121        self.id
122    }
123}
124
125/// An email address for a user
126#[derive(Serialize, JsonSchema)]
127pub struct UserEmail {
128    #[serde(skip)]
129    id: Ulid,
130
131    /// When the object was created
132    created_at: DateTime<Utc>,
133
134    /// The ID of the user who owns this email address
135    #[schemars(with = "super::schema::Ulid")]
136    user_id: Ulid,
137
138    /// The email address
139    email: String,
140}
141
142impl Resource for UserEmail {
143    const KIND: &'static str = "user-email";
144    const PATH: &'static str = "/api/admin/v1/user-emails";
145
146    fn id(&self) -> Ulid {
147        self.id
148    }
149}
150
151impl From<mas_data_model::UserEmail> for UserEmail {
152    fn from(value: mas_data_model::UserEmail) -> Self {
153        Self {
154            id: value.id,
155            created_at: value.created_at,
156            user_id: value.user_id,
157            email: value.email,
158        }
159    }
160}
161
162impl UserEmail {
163    pub fn samples() -> [Self; 1] {
164        [Self {
165            id: Ulid::from_bytes([0x01; 16]),
166            created_at: DateTime::default(),
167            user_id: Ulid::from_bytes([0x02; 16]),
168            email: "alice@example.com".to_owned(),
169        }]
170    }
171}
172
173/// A compatibility session for legacy clients
174#[derive(Serialize, JsonSchema)]
175pub struct CompatSession {
176    #[serde(skip)]
177    pub id: Ulid,
178
179    /// The ID of the user that owns this session
180    #[schemars(with = "super::schema::Ulid")]
181    pub user_id: Ulid,
182
183    /// The Matrix device ID of this session
184    #[schemars(with = "super::schema::Device")]
185    pub device_id: Option<Device>,
186
187    /// The ID of the user session that started this session, if any
188    #[schemars(with = "super::schema::Ulid")]
189    pub user_session_id: Option<Ulid>,
190
191    /// The redirect URI used to login in the client, if it was an SSO login
192    pub redirect_uri: Option<Url>,
193
194    /// The time this session was created
195    pub created_at: DateTime<Utc>,
196
197    /// The user agent string that started this session, if any
198    pub user_agent: Option<String>,
199
200    /// The time this session was last active
201    pub last_active_at: Option<DateTime<Utc>>,
202
203    /// The last IP address recorded for this session
204    pub last_active_ip: Option<std::net::IpAddr>,
205
206    /// The time this session was finished
207    pub finished_at: Option<DateTime<Utc>>,
208
209    /// The user-provided name, if any
210    pub human_name: Option<String>,
211}
212
213impl
214    From<(
215        mas_data_model::CompatSession,
216        Option<mas_data_model::CompatSsoLogin>,
217    )> for CompatSession
218{
219    fn from(
220        (session, sso_login): (
221            mas_data_model::CompatSession,
222            Option<mas_data_model::CompatSsoLogin>,
223        ),
224    ) -> Self {
225        let finished_at = session.finished_at();
226        Self {
227            id: session.id,
228            user_id: session.user_id,
229            device_id: session.device,
230            user_session_id: session.user_session_id,
231            redirect_uri: sso_login.map(|sso| sso.redirect_uri),
232            created_at: session.created_at,
233            user_agent: session.user_agent,
234            last_active_at: session.last_active_at,
235            last_active_ip: session.last_active_ip,
236            finished_at,
237            human_name: session.human_name,
238        }
239    }
240}
241
242impl Resource for CompatSession {
243    const KIND: &'static str = "compat-session";
244    const PATH: &'static str = "/api/admin/v1/compat-sessions";
245
246    fn id(&self) -> Ulid {
247        self.id
248    }
249}
250
251impl CompatSession {
252    pub fn samples() -> [Self; 3] {
253        [
254            Self {
255                id: Ulid::from_bytes([0x01; 16]),
256                user_id: Ulid::from_bytes([0x01; 16]),
257                device_id: Some("AABBCCDDEE".to_owned().into()),
258                user_session_id: Some(Ulid::from_bytes([0x11; 16])),
259                redirect_uri: Some("https://example.com/redirect".parse().unwrap()),
260                created_at: DateTime::default(),
261                user_agent: Some("Mozilla/5.0".to_owned()),
262                last_active_at: Some(DateTime::default()),
263                last_active_ip: Some([1, 2, 3, 4].into()),
264                finished_at: None,
265                human_name: Some("Laptop".to_owned()),
266            },
267            Self {
268                id: Ulid::from_bytes([0x02; 16]),
269                user_id: Ulid::from_bytes([0x01; 16]),
270                device_id: Some("FFGGHHIIJJ".to_owned().into()),
271                user_session_id: Some(Ulid::from_bytes([0x12; 16])),
272                redirect_uri: None,
273                created_at: DateTime::default(),
274                user_agent: Some("Mozilla/5.0".to_owned()),
275                last_active_at: Some(DateTime::default()),
276                last_active_ip: Some([1, 2, 3, 4].into()),
277                finished_at: Some(DateTime::default()),
278                human_name: None,
279            },
280            Self {
281                id: Ulid::from_bytes([0x03; 16]),
282                user_id: Ulid::from_bytes([0x01; 16]),
283                device_id: None,
284                user_session_id: None,
285                redirect_uri: None,
286                created_at: DateTime::default(),
287                user_agent: None,
288                last_active_at: None,
289                last_active_ip: None,
290                finished_at: None,
291                human_name: None,
292            },
293        ]
294    }
295}
296
297/// A OAuth 2.0 session
298#[derive(Serialize, JsonSchema)]
299pub struct OAuth2Session {
300    #[serde(skip)]
301    id: Ulid,
302
303    /// When the object was created
304    created_at: DateTime<Utc>,
305
306    /// When the session was finished
307    finished_at: Option<DateTime<Utc>>,
308
309    /// The ID of the user who owns the session
310    #[schemars(with = "Option<super::schema::Ulid>")]
311    user_id: Option<Ulid>,
312
313    /// The ID of the browser session which started this session
314    #[schemars(with = "Option<super::schema::Ulid>")]
315    user_session_id: Option<Ulid>,
316
317    /// The ID of the client which requested this session
318    #[schemars(with = "super::schema::Ulid")]
319    client_id: Ulid,
320
321    /// The scope granted for this session
322    scope: String,
323
324    /// The user agent string of the client which started this session
325    user_agent: Option<String>,
326
327    /// The last time the session was active
328    last_active_at: Option<DateTime<Utc>>,
329
330    /// The last IP address used by the session
331    last_active_ip: Option<IpAddr>,
332
333    /// The user-provided name, if any
334    human_name: Option<String>,
335}
336
337impl From<mas_data_model::Session> for OAuth2Session {
338    fn from(session: mas_data_model::Session) -> Self {
339        Self {
340            id: session.id,
341            created_at: session.created_at,
342            finished_at: session.finished_at(),
343            user_id: session.user_id,
344            user_session_id: session.user_session_id,
345            client_id: session.client_id,
346            scope: session.scope.to_string(),
347            user_agent: session.user_agent,
348            last_active_at: session.last_active_at,
349            last_active_ip: session.last_active_ip,
350            human_name: session.human_name,
351        }
352    }
353}
354
355impl OAuth2Session {
356    /// Samples of OAuth 2.0 sessions
357    pub fn samples() -> [Self; 3] {
358        [
359            Self {
360                id: Ulid::from_bytes([0x01; 16]),
361                created_at: DateTime::default(),
362                finished_at: None,
363                user_id: Some(Ulid::from_bytes([0x02; 16])),
364                user_session_id: Some(Ulid::from_bytes([0x03; 16])),
365                client_id: Ulid::from_bytes([0x04; 16]),
366                scope: "openid".to_owned(),
367                user_agent: Some("Mozilla/5.0".to_owned()),
368                last_active_at: Some(DateTime::default()),
369                last_active_ip: Some("127.0.0.1".parse().unwrap()),
370                human_name: Some("Laptop".to_owned()),
371            },
372            Self {
373                id: Ulid::from_bytes([0x02; 16]),
374                created_at: DateTime::default(),
375                finished_at: None,
376                user_id: None,
377                user_session_id: None,
378                client_id: Ulid::from_bytes([0x05; 16]),
379                scope: "urn:mas:admin".to_owned(),
380                user_agent: None,
381                last_active_at: None,
382                last_active_ip: None,
383                human_name: None,
384            },
385            Self {
386                id: Ulid::from_bytes([0x03; 16]),
387                created_at: DateTime::default(),
388                finished_at: Some(DateTime::default()),
389                user_id: Some(Ulid::from_bytes([0x04; 16])),
390                user_session_id: Some(Ulid::from_bytes([0x05; 16])),
391                client_id: Ulid::from_bytes([0x06; 16]),
392                scope: "urn:matrix:client:api:*".to_owned(),
393                user_agent: Some("Mozilla/5.0".to_owned()),
394                last_active_at: Some(DateTime::default()),
395                last_active_ip: Some("127.0.0.1".parse().unwrap()),
396                human_name: None,
397            },
398        ]
399    }
400}
401
402impl Resource for OAuth2Session {
403    const KIND: &'static str = "oauth2-session";
404    const PATH: &'static str = "/api/admin/v1/oauth2-sessions";
405
406    fn id(&self) -> Ulid {
407        self.id
408    }
409}
410
411/// The browser (cookie) session for a user
412#[derive(Serialize, JsonSchema)]
413pub struct UserSession {
414    #[serde(skip)]
415    id: Ulid,
416
417    /// When the object was created
418    created_at: DateTime<Utc>,
419
420    /// When the session was finished
421    finished_at: Option<DateTime<Utc>>,
422
423    /// The ID of the user who owns the session
424    #[schemars(with = "super::schema::Ulid")]
425    user_id: Ulid,
426
427    /// The user agent string of the client which started this session
428    user_agent: Option<String>,
429
430    /// The last time the session was active
431    last_active_at: Option<DateTime<Utc>>,
432
433    /// The last IP address used by the session
434    last_active_ip: Option<IpAddr>,
435}
436
437impl From<mas_data_model::BrowserSession> for UserSession {
438    fn from(value: mas_data_model::BrowserSession) -> Self {
439        Self {
440            id: value.id,
441            created_at: value.created_at,
442            finished_at: value.finished_at,
443            user_id: value.user.id,
444            user_agent: value.user_agent,
445            last_active_at: value.last_active_at,
446            last_active_ip: value.last_active_ip,
447        }
448    }
449}
450
451impl UserSession {
452    /// Samples of user sessions
453    pub fn samples() -> [Self; 3] {
454        [
455            Self {
456                id: Ulid::from_bytes([0x01; 16]),
457                created_at: DateTime::default(),
458                finished_at: None,
459                user_id: Ulid::from_bytes([0x02; 16]),
460                user_agent: Some("Mozilla/5.0".to_owned()),
461                last_active_at: Some(DateTime::default()),
462                last_active_ip: Some("127.0.0.1".parse().unwrap()),
463            },
464            Self {
465                id: Ulid::from_bytes([0x02; 16]),
466                created_at: DateTime::default(),
467                finished_at: None,
468                user_id: Ulid::from_bytes([0x03; 16]),
469                user_agent: None,
470                last_active_at: None,
471                last_active_ip: None,
472            },
473            Self {
474                id: Ulid::from_bytes([0x03; 16]),
475                created_at: DateTime::default(),
476                finished_at: Some(DateTime::default()),
477                user_id: Ulid::from_bytes([0x04; 16]),
478                user_agent: Some("Mozilla/5.0".to_owned()),
479                last_active_at: Some(DateTime::default()),
480                last_active_ip: Some("127.0.0.1".parse().unwrap()),
481            },
482        ]
483    }
484}
485
486impl Resource for UserSession {
487    const KIND: &'static str = "user-session";
488    const PATH: &'static str = "/api/admin/v1/user-sessions";
489
490    fn id(&self) -> Ulid {
491        self.id
492    }
493}
494
495/// An upstream OAuth 2.0 link
496#[derive(Serialize, JsonSchema)]
497pub struct UpstreamOAuthLink {
498    #[serde(skip)]
499    id: Ulid,
500
501    /// When the object was created
502    created_at: DateTime<Utc>,
503
504    /// The ID of the provider
505    #[schemars(with = "super::schema::Ulid")]
506    provider_id: Ulid,
507
508    /// The subject of the upstream account, unique per provider
509    subject: String,
510
511    /// The ID of the user who owns this link, if any
512    #[schemars(with = "Option<super::schema::Ulid>")]
513    user_id: Option<Ulid>,
514
515    /// A human-readable name of the upstream account
516    human_account_name: Option<String>,
517}
518
519impl Resource for UpstreamOAuthLink {
520    const KIND: &'static str = "upstream-oauth-link";
521    const PATH: &'static str = "/api/admin/v1/upstream-oauth-links";
522
523    fn id(&self) -> Ulid {
524        self.id
525    }
526}
527
528impl From<mas_data_model::UpstreamOAuthLink> for UpstreamOAuthLink {
529    fn from(value: mas_data_model::UpstreamOAuthLink) -> Self {
530        Self {
531            id: value.id,
532            created_at: value.created_at,
533            provider_id: value.provider_id,
534            subject: value.subject,
535            user_id: value.user_id,
536            human_account_name: value.human_account_name,
537        }
538    }
539}
540
541impl UpstreamOAuthLink {
542    /// Samples of upstream OAuth 2.0 links
543    pub fn samples() -> [Self; 3] {
544        [
545            Self {
546                id: Ulid::from_bytes([0x01; 16]),
547                created_at: DateTime::default(),
548                provider_id: Ulid::from_bytes([0x02; 16]),
549                subject: "john-42".to_owned(),
550                user_id: Some(Ulid::from_bytes([0x03; 16])),
551                human_account_name: Some("john.doe@example.com".to_owned()),
552            },
553            Self {
554                id: Ulid::from_bytes([0x02; 16]),
555                created_at: DateTime::default(),
556                provider_id: Ulid::from_bytes([0x03; 16]),
557                subject: "jane-123".to_owned(),
558                user_id: None,
559                human_account_name: None,
560            },
561            Self {
562                id: Ulid::from_bytes([0x03; 16]),
563                created_at: DateTime::default(),
564                provider_id: Ulid::from_bytes([0x04; 16]),
565                subject: "bob@social.example.com".to_owned(),
566                user_id: Some(Ulid::from_bytes([0x05; 16])),
567                human_account_name: Some("bob".to_owned()),
568            },
569        ]
570    }
571}
572
573/// The policy data
574#[derive(Serialize, JsonSchema)]
575pub struct PolicyData {
576    #[serde(skip)]
577    id: Ulid,
578
579    /// The creation date of the policy data
580    created_at: DateTime<Utc>,
581
582    /// The policy data content
583    data: serde_json::Value,
584}
585
586impl From<mas_data_model::PolicyData> for PolicyData {
587    fn from(policy_data: mas_data_model::PolicyData) -> Self {
588        Self {
589            id: policy_data.id,
590            created_at: policy_data.created_at,
591            data: policy_data.data,
592        }
593    }
594}
595
596impl Resource for PolicyData {
597    const KIND: &'static str = "policy-data";
598    const PATH: &'static str = "/api/admin/v1/policy-data";
599
600    fn id(&self) -> Ulid {
601        self.id
602    }
603}
604
605impl PolicyData {
606    /// Samples of policy data
607    pub fn samples() -> [Self; 1] {
608        [Self {
609            id: Ulid::from_bytes([0x01; 16]),
610            created_at: DateTime::default(),
611            data: serde_json::json!({
612                "hello": "world",
613                "foo": 42,
614                "bar": true
615            }),
616        }]
617    }
618}
619
620/// A registration token
621#[derive(Serialize, JsonSchema)]
622pub struct UserRegistrationToken {
623    #[serde(skip)]
624    id: Ulid,
625
626    /// The token string
627    token: String,
628
629    /// Whether the token is valid
630    valid: bool,
631
632    /// Maximum number of times this token can be used
633    usage_limit: Option<u32>,
634
635    /// Number of times this token has been used
636    times_used: u32,
637
638    /// When the token was created
639    created_at: DateTime<Utc>,
640
641    /// When the token was last used. If null, the token has never been used.
642    last_used_at: Option<DateTime<Utc>>,
643
644    /// When the token expires. If null, the token never expires.
645    expires_at: Option<DateTime<Utc>>,
646
647    /// When the token was revoked. If null, the token is not revoked.
648    revoked_at: Option<DateTime<Utc>>,
649}
650
651impl UserRegistrationToken {
652    pub fn new(token: mas_data_model::UserRegistrationToken, now: DateTime<Utc>) -> Self {
653        Self {
654            id: token.id,
655            valid: token.is_valid(now),
656            token: token.token,
657            usage_limit: token.usage_limit,
658            times_used: token.times_used,
659            created_at: token.created_at,
660            last_used_at: token.last_used_at,
661            expires_at: token.expires_at,
662            revoked_at: token.revoked_at,
663        }
664    }
665}
666
667impl Resource for UserRegistrationToken {
668    const KIND: &'static str = "user-registration_token";
669    const PATH: &'static str = "/api/admin/v1/user-registration-tokens";
670
671    fn id(&self) -> Ulid {
672        self.id
673    }
674}
675
676impl UserRegistrationToken {
677    /// Samples of registration tokens
678    pub fn samples() -> [Self; 2] {
679        [
680            Self {
681                id: Ulid::from_bytes([0x01; 16]),
682                token: "abc123def456".to_owned(),
683                valid: true,
684                usage_limit: Some(10),
685                times_used: 5,
686                created_at: DateTime::default(),
687                last_used_at: Some(DateTime::default()),
688                expires_at: Some(DateTime::default() + chrono::Duration::days(30)),
689                revoked_at: None,
690            },
691            Self {
692                id: Ulid::from_bytes([0x02; 16]),
693                token: "xyz789abc012".to_owned(),
694                valid: false,
695                usage_limit: None,
696                times_used: 0,
697                created_at: DateTime::default(),
698                last_used_at: None,
699                expires_at: None,
700                revoked_at: Some(DateTime::default()),
701            },
702        ]
703    }
704}
705
706/// An upstream OAuth 2.0 provider
707#[derive(Serialize, JsonSchema)]
708pub struct UpstreamOAuthProvider {
709    #[serde(skip)]
710    id: Ulid,
711
712    /// The OIDC issuer of the provider
713    issuer: Option<String>,
714
715    /// A human-readable name for the provider
716    human_name: Option<String>,
717
718    /// A brand identifier, e.g. "apple" or "google"
719    brand_name: Option<String>,
720
721    /// When the provider was created
722    created_at: DateTime<Utc>,
723
724    /// When the provider was disabled. If null, the provider is enabled.
725    disabled_at: Option<DateTime<Utc>>,
726}
727
728impl From<mas_data_model::UpstreamOAuthProvider> for UpstreamOAuthProvider {
729    fn from(provider: mas_data_model::UpstreamOAuthProvider) -> Self {
730        Self {
731            id: provider.id,
732            issuer: provider.issuer,
733            human_name: provider.human_name,
734            brand_name: provider.brand_name,
735            created_at: provider.created_at,
736            disabled_at: provider.disabled_at,
737        }
738    }
739}
740
741impl Resource for UpstreamOAuthProvider {
742    const KIND: &'static str = "upstream-oauth-provider";
743    const PATH: &'static str = "/api/admin/v1/upstream-oauth-providers";
744
745    fn id(&self) -> Ulid {
746        self.id
747    }
748}
749
750impl UpstreamOAuthProvider {
751    /// Samples of upstream OAuth 2.0 providers
752    pub fn samples() -> [Self; 3] {
753        [
754            Self {
755                id: Ulid::from_bytes([0x01; 16]),
756                issuer: Some("https://accounts.google.com".to_owned()),
757                human_name: Some("Google".to_owned()),
758                brand_name: Some("google".to_owned()),
759                created_at: DateTime::default(),
760                disabled_at: None,
761            },
762            Self {
763                id: Ulid::from_bytes([0x02; 16]),
764                issuer: Some("https://appleid.apple.com".to_owned()),
765                human_name: Some("Apple ID".to_owned()),
766                brand_name: Some("apple".to_owned()),
767                created_at: DateTime::default(),
768                disabled_at: Some(DateTime::default()),
769            },
770            Self {
771                id: Ulid::from_bytes([0x03; 16]),
772                issuer: None,
773                human_name: Some("Custom OAuth Provider".to_owned()),
774                brand_name: None,
775                created_at: DateTime::default(),
776                disabled_at: None,
777            },
778        ]
779    }
780}
781
782/// An error that shouldn't happen in practice, but suggests database
783/// inconsistency.
784#[derive(Debug, Error)]
785#[error(
786    "personal session {session_id} in inconsistent state: not revoked but no valid access token"
787)]
788pub struct InconsistentPersonalSession {
789    pub session_id: Ulid,
790}
791
792// Note: we don't expose a separate concept of personal access tokens to the
793// admin API; we merge the relevant attributes into the personal session.
794/// A personal session (session using personal access tokens)
795#[derive(Serialize, JsonSchema)]
796pub struct PersonalSession {
797    #[serde(skip)]
798    id: Ulid,
799
800    /// When the session was created
801    created_at: DateTime<Utc>,
802
803    /// When the session was revoked, if applicable
804    revoked_at: Option<DateTime<Utc>>,
805
806    /// The ID of the user who owns this session (if user-owned)
807    #[schemars(with = "Option<super::schema::Ulid>")]
808    owner_user_id: Option<Ulid>,
809
810    /// The ID of the `OAuth2` client that owns this session (if client-owned)
811    #[schemars(with = "Option<super::schema::Ulid>")]
812    owner_client_id: Option<Ulid>,
813
814    /// The ID of the user that the session acts on behalf of
815    #[schemars(with = "super::schema::Ulid")]
816    actor_user_id: Ulid,
817
818    /// Human-readable name for the session
819    human_name: String,
820
821    /// `OAuth2` scopes for this session
822    scope: String,
823
824    /// When the session was last active
825    last_active_at: Option<DateTime<Utc>>,
826
827    /// IP address of last activity
828    last_active_ip: Option<IpAddr>,
829
830    /// When the current token for this session expires.
831    /// The session will need to be regenerated, producing a new access token,
832    /// after this time.
833    /// None if the current token won't expire or if the session is revoked.
834    expires_at: Option<DateTime<Utc>>,
835
836    /// The actual access token (only returned on creation)
837    #[serde(skip_serializing_if = "Option::is_none")]
838    access_token: Option<String>,
839}
840
841impl
842    TryFrom<(
843        DataModelPersonalSession,
844        Option<DataModelPersonalAccessToken>,
845    )> for PersonalSession
846{
847    type Error = InconsistentPersonalSession;
848
849    fn try_from(
850        (session, token): (
851            DataModelPersonalSession,
852            Option<DataModelPersonalAccessToken>,
853        ),
854    ) -> Result<Self, InconsistentPersonalSession> {
855        let expires_at = if let Some(token) = token {
856            token.expires_at
857        } else {
858            if !session.is_revoked() {
859                // No active token, but the session is not revoked.
860                return Err(InconsistentPersonalSession {
861                    session_id: session.id,
862                });
863            }
864            None
865        };
866
867        let (owner_user_id, owner_client_id) = match session.owner {
868            PersonalSessionOwner::User(id) => (Some(id), None),
869            PersonalSessionOwner::OAuth2Client(id) => (None, Some(id)),
870        };
871
872        Ok(Self {
873            id: session.id,
874            created_at: session.created_at,
875            revoked_at: session.revoked_at(),
876            owner_user_id,
877            owner_client_id,
878            actor_user_id: session.actor_user_id,
879            human_name: session.human_name,
880            scope: session.scope.to_string(),
881            last_active_at: session.last_active_at,
882            last_active_ip: session.last_active_ip,
883            expires_at,
884            // If relevant, the caller will populate using `with_token` afterwards.
885            access_token: None,
886        })
887    }
888}
889
890impl Resource for PersonalSession {
891    const KIND: &'static str = "personal-session";
892    const PATH: &'static str = "/api/admin/v1/personal-sessions";
893
894    fn id(&self) -> Ulid {
895        self.id
896    }
897}
898
899impl PersonalSession {
900    /// Sample personal sessions for documentation/testing
901    pub fn samples() -> [Self; 3] {
902        [
903            Self {
904                id: Ulid::from_string("01FSHN9AG0AJ6AC5HQ9X6H4RP4").unwrap(),
905                created_at: DateTime::from_timestamp(1_642_338_000, 0).unwrap(), /* 2022-01-16T14:
906                                                                                  * 40:00Z */
907                revoked_at: None,
908                owner_user_id: Some(Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap()),
909                owner_client_id: None,
910                actor_user_id: Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap(),
911                human_name: "Alice's Development Token".to_owned(),
912                scope: "openid urn:matrix:org.matrix.msc2967.client:api:*".to_owned(),
913                last_active_at: Some(DateTime::from_timestamp(1_642_347_000, 0).unwrap()), /* 2022-01-16T17:10:00Z */
914                last_active_ip: Some("192.168.1.100".parse().unwrap()),
915                expires_at: None,
916                access_token: None,
917            },
918            Self {
919                id: Ulid::from_string("01FSHN9AG0BJ6AC5HQ9X6H4RP5").unwrap(),
920                created_at: DateTime::from_timestamp(1_642_338_060, 0).unwrap(), /* 2022-01-16T14:
921                                                                                  * 41:00Z */
922                revoked_at: Some(DateTime::from_timestamp(1_642_350_000, 0).unwrap()), /* 2022-01-16T18:00:00Z */
923                owner_user_id: Some(Ulid::from_string("01FSHN9AG0NZAA6S4AF7CTV32F").unwrap()),
924                owner_client_id: None,
925                actor_user_id: Ulid::from_string("01FSHN9AG0NZAA6S4AF7CTV32F").unwrap(),
926                human_name: "Bob's Mobile App".to_owned(),
927                scope: "openid".to_owned(),
928                last_active_at: Some(DateTime::from_timestamp(1_642_349_000, 0).unwrap()), /* 2022-01-16T17:43:20Z */
929                last_active_ip: Some("10.0.0.50".parse().unwrap()),
930                expires_at: None,
931                access_token: None,
932            },
933            Self {
934                id: Ulid::from_string("01FSHN9AG0CJ6AC5HQ9X6H4RP6").unwrap(),
935                created_at: DateTime::from_timestamp(1_642_338_120, 0).unwrap(), /* 2022-01-16T14:
936                                                                                  * 42:00Z */
937                revoked_at: None,
938                owner_user_id: None,
939                owner_client_id: Some(Ulid::from_string("01FSHN9AG0DJ6AC5HQ9X6H4RP7").unwrap()),
940                actor_user_id: Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap(),
941                human_name: "CI/CD Pipeline Token".to_owned(),
942                scope: "openid urn:mas:admin".to_owned(),
943                last_active_at: Some(DateTime::from_timestamp(1_642_348_000, 0).unwrap()), /* 2022-01-16T17:26:40Z */
944                last_active_ip: Some("203.0.113.10".parse().unwrap()),
945                expires_at: Some(DateTime::from_timestamp(1_642_999_000, 0).unwrap()),
946                access_token: None,
947            },
948        ]
949    }
950
951    /// Add the actual token value (for use in creation responses)
952    pub fn with_token(mut self, access_token: String) -> Self {
953        self.access_token = Some(access_token);
954        self
955    }
956}