data model: Add personal sessions with mpt_ prefix

This commit is contained in:
Olivier 'reivilibre
2025-10-07 16:08:43 +01:00
parent 87950bb18a
commit b54a657c32
5 changed files with 158 additions and 1 deletions
+1
View File
@@ -11,6 +11,7 @@ use thiserror::Error;
pub mod clock;
pub(crate) mod compat;
pub mod oauth2;
pub mod personal;
pub(crate) mod policy_data;
mod site_config;
pub(crate) mod tokens;
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
pub mod session;
use chrono::{DateTime, Utc};
use ulid::Ulid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersonalAccessToken {
pub id: Ulid,
pub session_id: Ulid,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub revoked_at: Option<DateTime<Utc>>,
}
impl PersonalAccessToken {
#[must_use]
pub fn is_valid(&self, now: DateTime<Utc>) -> bool {
if self.revoked_at.is_some() {
return false;
}
if let Some(expires_at) = self.expires_at {
expires_at > now
} else {
true
}
}
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
use std::net::IpAddr;
use chrono::{DateTime, Utc};
use oauth2_types::scope::Scope;
use serde::Serialize;
use ulid::Ulid;
use crate::InvalidTransitionError;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub enum SessionState {
#[default]
Valid,
Revoked {
revoked_at: DateTime<Utc>,
},
}
impl SessionState {
/// Returns `true` if the session state is [`Valid`].
///
/// [`Valid`]: SessionState::Valid
#[must_use]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
/// Returns `true` if the session state is [`Revoked`].
///
/// [`Revoked`]: SessionState::Revoked
#[must_use]
pub fn is_revoked(&self) -> bool {
matches!(self, Self::Revoked { .. })
}
/// Transitions the session state to [`Revoked`].
///
/// # Parameters
///
/// * `revoked_at` - The time at which the session was revoked.
///
/// # Errors
///
/// Returns an error if the session state is already [`Revoked`].
///
/// [`Revoked`]: SessionState::Revoked
pub fn revoke(self, revoked_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
match self {
Self::Valid => Ok(Self::Revoked { revoked_at }),
Self::Revoked { .. } => Err(InvalidTransitionError),
}
}
/// Returns the time the session was revoked, if any
///
/// Returns `None` if the session is still [`Valid`].
///
/// [`Valid`]: SessionState::Valid
#[must_use]
pub fn revoked_at(&self) -> Option<DateTime<Utc>> {
match self {
Self::Valid => None,
Self::Revoked { revoked_at } => Some(*revoked_at),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PersonalSession {
pub id: Ulid,
pub state: SessionState,
pub owner_user_id: Ulid,
pub actor_user_id: Ulid,
pub human_name: String,
/// The scope for the session, identical to OAuth2 sessions.
/// May or may not include a device scope
/// (personal sessions can be deviceless).
pub scope: Scope,
pub created_at: DateTime<Utc>,
pub last_active_at: Option<DateTime<Utc>>,
pub last_active_ip: Option<IpAddr>,
}
impl std::ops::Deref for PersonalSession {
type Target = SessionState;
fn deref(&self) -> &Self::Target {
&self.state
}
}
impl PersonalSession {
/// Marks the session as revoked.
///
/// # Parameters
///
/// * `revoked_at` - The time at which the session was finished.
///
/// # Errors
///
/// Returns an error if the session is already finished.
pub fn finish(mut self, revoked_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
self.state = self.state.revoke(revoked_at)?;
Ok(self)
}
}
+9 -1
View File
@@ -240,6 +240,9 @@ pub enum TokenType {
/// A legacy refresh token
CompatRefreshToken,
/// A personal access token.
PersonalAccessToken,
}
impl std::fmt::Display for TokenType {
@@ -249,6 +252,7 @@ impl std::fmt::Display for TokenType {
TokenType::RefreshToken => write!(f, "refresh token"),
TokenType::CompatAccessToken => write!(f, "compat access token"),
TokenType::CompatRefreshToken => write!(f, "compat refresh token"),
TokenType::PersonalAccessToken => write!(f, "personal access token"),
}
}
}
@@ -260,6 +264,7 @@ impl TokenType {
TokenType::RefreshToken => "mar",
TokenType::CompatAccessToken => "mct",
TokenType::CompatRefreshToken => "mcr",
TokenType::PersonalAccessToken => "mpt",
}
}
@@ -269,6 +274,7 @@ impl TokenType {
"mar" => Some(TokenType::RefreshToken),
"mct" | "syt" => Some(TokenType::CompatAccessToken),
"mcr" | "syr" => Some(TokenType::CompatRefreshToken),
"mpt" => Some(TokenType::PersonalAccessToken),
_ => None,
}
}
@@ -335,7 +341,9 @@ impl PartialEq<OAuthTokenTypeHint> for TokenType {
matches!(
(self, other),
(
TokenType::AccessToken | TokenType::CompatAccessToken,
TokenType::AccessToken
| TokenType::CompatAccessToken
| TokenType::PersonalAccessToken,
OAuthTokenTypeHint::AccessToken
) | (
TokenType::RefreshToken | TokenType::CompatRefreshToken,
@@ -625,6 +625,11 @@ pub(crate) async fn post(
device_id: session.device.map(Device::into),
}
}
TokenType::PersonalAccessToken => {
// TODO
return Err(RouteError::UnknownToken(TokenType::PersonalAccessToken));
}
};
repo.save().await?;