mas_data_model/personal/
session.rs

1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
4// Please see LICENSE files in the repository root for full details.
5
6use std::net::IpAddr;
7
8use chrono::{DateTime, Utc};
9use oauth2_types::scope::Scope;
10use serde::Serialize;
11use ulid::Ulid;
12
13use crate::{Client, InvalidTransitionError, User};
14
15#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
16pub enum SessionState {
17    #[default]
18    Valid,
19    Revoked {
20        revoked_at: DateTime<Utc>,
21    },
22}
23
24impl SessionState {
25    /// Returns `true` if the session state is [`Valid`].
26    ///
27    /// [`Valid`]: SessionState::Valid
28    #[must_use]
29    pub fn is_valid(&self) -> bool {
30        matches!(self, Self::Valid)
31    }
32
33    /// Returns `true` if the session state is [`Revoked`].
34    ///
35    /// [`Revoked`]: SessionState::Revoked
36    #[must_use]
37    pub fn is_revoked(&self) -> bool {
38        matches!(self, Self::Revoked { .. })
39    }
40
41    /// Transitions the session state to [`Revoked`].
42    ///
43    /// # Parameters
44    ///
45    /// * `revoked_at` - The time at which the session was revoked.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if the session state is already [`Revoked`].
50    ///
51    /// [`Revoked`]: SessionState::Revoked
52    pub fn revoke(self, revoked_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
53        match self {
54            Self::Valid => Ok(Self::Revoked { revoked_at }),
55            Self::Revoked { .. } => Err(InvalidTransitionError),
56        }
57    }
58
59    /// Returns the time the session was revoked, if any
60    ///
61    /// Returns `None` if the session is still [`Valid`].
62    ///
63    /// [`Valid`]: SessionState::Valid
64    #[must_use]
65    pub fn revoked_at(&self) -> Option<DateTime<Utc>> {
66        match self {
67            Self::Valid => None,
68            Self::Revoked { revoked_at } => Some(*revoked_at),
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
74pub struct PersonalSession {
75    pub id: Ulid,
76    pub state: SessionState,
77    pub owner: PersonalSessionOwner,
78    pub actor_user_id: Ulid,
79    pub human_name: String,
80    /// The scope for the session, identical to OAuth 2 sessions.
81    /// May or may not include a device scope
82    /// (personal sessions can be deviceless).
83    pub scope: Scope,
84    pub created_at: DateTime<Utc>,
85    pub last_active_at: Option<DateTime<Utc>>,
86    pub last_active_ip: Option<IpAddr>,
87}
88
89#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
90pub enum PersonalSessionOwner {
91    /// The personal session is owned by the user with the given `user_id`.
92    User(Ulid),
93    /// The personal session is owned by the OAuth 2 Client with the given
94    /// `oauth2_client_id`.
95    OAuth2Client(Ulid),
96}
97
98impl<'a> From<&'a User> for PersonalSessionOwner {
99    fn from(value: &'a User) -> Self {
100        PersonalSessionOwner::User(value.id)
101    }
102}
103
104impl<'a> From<&'a Client> for PersonalSessionOwner {
105    fn from(value: &'a Client) -> Self {
106        PersonalSessionOwner::OAuth2Client(value.id)
107    }
108}
109
110impl std::ops::Deref for PersonalSession {
111    type Target = SessionState;
112
113    fn deref(&self) -> &Self::Target {
114        &self.state
115    }
116}
117
118impl PersonalSession {
119    /// Marks the session as revoked.
120    ///
121    /// # Parameters
122    ///
123    /// * `revoked_at` - The time at which the session was finished.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the session is already finished.
128    pub fn finish(mut self, revoked_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
129        self.state = self.state.revoke(revoked_at)?;
130        Ok(self)
131    }
132}