Add a deactivated_at flag on users
This commit is contained in:
@@ -21,14 +21,15 @@ pub struct User {
|
||||
pub sub: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub locked_at: Option<DateTime<Utc>>,
|
||||
pub deactivated_at: Option<DateTime<Utc>>,
|
||||
pub can_request_admin: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Returns `true` unless the user is locked.
|
||||
/// Returns `true` unless the user is locked or deactivated.
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.locked_at.is_none()
|
||||
self.locked_at.is_none() && self.deactivated_at.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ impl User {
|
||||
sub: "123-456".to_owned(),
|
||||
created_at: now,
|
||||
locked_at: None,
|
||||
deactivated_at: None,
|
||||
can_request_admin: false,
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -327,6 +327,7 @@ mod tests {
|
||||
sub: "123-456".to_owned(),
|
||||
created_at: now,
|
||||
locked_at: None,
|
||||
deactivated_at: None,
|
||||
can_request_admin: false,
|
||||
};
|
||||
|
||||
@@ -336,6 +337,7 @@ mod tests {
|
||||
sub: "123-456".to_owned(),
|
||||
created_at: now,
|
||||
locked_at: None,
|
||||
deactivated_at: None,
|
||||
can_request_admin: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , can_request_admin\n FROM users\n WHERE username = $1\n ",
|
||||
"query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE username = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,6 +25,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "can_request_admin",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -39,8 +44,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e1a18bd82d28fd86d8b8da8a6ac6eddf224ab32cf96e9c28706dd9aa1d09332b"
|
||||
"hash": "48213d718a256a12540c0aec595ca3e436be423f2d0c868700c6397745ed0455"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , can_request_admin\n FROM users\n WHERE user_id = $1\n ",
|
||||
"query": "\n SELECT user_id\n , username\n , created_at\n , locked_at\n , deactivated_at\n , can_request_admin\n FROM users\n WHERE user_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,6 +25,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "can_request_admin",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -39,8 +44,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "86767be88b7594cc9a98a2f1f1c61cf66118f2fda4b4b0415de15087524f1356"
|
||||
"hash": "cc332eda5965715607ffa4eeeacc1b6532cbd8fe49904ccdb1afe315804d348d"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , s.last_active_at AS \"user_session_last_active_at\"\n , s.last_active_ip AS \"user_session_last_active_ip: IpAddr\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n , u.can_request_admin AS \"user_can_request_admin\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ",
|
||||
"query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , s.user_agent AS \"user_session_user_agent\"\n , s.last_active_at AS \"user_session_last_active_at\"\n , s.last_active_ip AS \"user_session_last_active_ip: IpAddr\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n , u.deactivated_at AS \"user_deactivated_at\"\n , u.can_request_admin AS \"user_can_request_admin\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -55,6 +55,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "user_deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "user_can_request_admin",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -75,8 +80,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7ea1a668480cbfda1439ba80fbd6ef2d751a3bb781e30260383eee3579f3a962"
|
||||
"hash": "f924db60febad26c9fff24881b05dd1e1f7ba288d7b2f2f8e30a1ea43e98b8c8"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
ALTER TABLE users
|
||||
-- Track when a user was deactivated.
|
||||
ADD COLUMN deactivated_at TIMESTAMP WITH TIME ZONE;
|
||||
@@ -25,6 +25,7 @@ pub enum Users {
|
||||
Username,
|
||||
CreatedAt,
|
||||
LockedAt,
|
||||
DeactivatedAt,
|
||||
CanRequestAdmin,
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ mod priv_ {
|
||||
pub(super) username: String,
|
||||
pub(super) created_at: DateTime<Utc>,
|
||||
pub(super) locked_at: Option<DateTime<Utc>>,
|
||||
pub(super) deactivated_at: Option<DateTime<Utc>>,
|
||||
pub(super) can_request_admin: bool,
|
||||
}
|
||||
}
|
||||
@@ -87,6 +88,7 @@ impl From<UserLookup> for User {
|
||||
sub: id.to_string(),
|
||||
created_at: value.created_at,
|
||||
locked_at: value.locked_at,
|
||||
deactivated_at: value.deactivated_at,
|
||||
can_request_admin: value.can_request_admin,
|
||||
}
|
||||
}
|
||||
@@ -96,10 +98,18 @@ impl Filter for UserFilter<'_> {
|
||||
fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
|
||||
sea_query::Condition::all()
|
||||
.add_option(self.state().map(|state| {
|
||||
if state.is_locked() {
|
||||
Expr::col((Users::Table, Users::LockedAt)).is_not_null()
|
||||
} else {
|
||||
Expr::col((Users::Table, Users::LockedAt)).is_null()
|
||||
match state {
|
||||
mas_storage::user::UserState::Deactivated => {
|
||||
Expr::col((Users::Table, Users::DeactivatedAt)).is_not_null()
|
||||
}
|
||||
mas_storage::user::UserState::Locked => {
|
||||
Expr::col((Users::Table, Users::LockedAt)).is_not_null()
|
||||
}
|
||||
mas_storage::user::UserState::Active => {
|
||||
Expr::col((Users::Table, Users::LockedAt))
|
||||
.is_null()
|
||||
.and(Expr::col((Users::Table, Users::DeactivatedAt)).is_null())
|
||||
}
|
||||
}
|
||||
}))
|
||||
.add_option(self.can_request_admin().map(|can_request_admin| {
|
||||
@@ -129,6 +139,7 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
, username
|
||||
, created_at
|
||||
, locked_at
|
||||
, deactivated_at
|
||||
, can_request_admin
|
||||
FROM users
|
||||
WHERE user_id = $1
|
||||
@@ -161,6 +172,7 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
, username
|
||||
, created_at
|
||||
, locked_at
|
||||
, deactivated_at
|
||||
, can_request_admin
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
@@ -220,6 +232,7 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
sub: id.to_string(),
|
||||
created_at,
|
||||
locked_at: None,
|
||||
deactivated_at: None,
|
||||
can_request_admin: false,
|
||||
})
|
||||
}
|
||||
@@ -382,6 +395,10 @@ impl UserRepository for PgUserRepository<'_> {
|
||||
Expr::col((Users::Table, Users::LockedAt)),
|
||||
UserLookupIden::LockedAt,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((Users::Table, Users::DeactivatedAt)),
|
||||
UserLookupIden::DeactivatedAt,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((Users::Table, Users::CanRequestAdmin)),
|
||||
UserLookupIden::CanRequestAdmin,
|
||||
|
||||
@@ -59,6 +59,7 @@ struct SessionLookup {
|
||||
user_username: String,
|
||||
user_created_at: DateTime<Utc>,
|
||||
user_locked_at: Option<DateTime<Utc>>,
|
||||
user_deactivated_at: Option<DateTime<Utc>>,
|
||||
user_can_request_admin: bool,
|
||||
}
|
||||
|
||||
@@ -73,6 +74,7 @@ impl TryFrom<SessionLookup> for BrowserSession {
|
||||
sub: id.to_string(),
|
||||
created_at: value.user_created_at,
|
||||
locked_at: value.user_locked_at,
|
||||
deactivated_at: value.user_deactivated_at,
|
||||
can_request_admin: value.user_can_request_admin,
|
||||
};
|
||||
|
||||
@@ -173,6 +175,7 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> {
|
||||
, u.username AS "user_username"
|
||||
, u.created_at AS "user_created_at"
|
||||
, u.locked_at AS "user_locked_at"
|
||||
, u.deactivated_at AS "user_deactivated_at"
|
||||
, u.can_request_admin AS "user_can_request_admin"
|
||||
FROM user_sessions s
|
||||
INNER JOIN users u
|
||||
@@ -356,6 +359,10 @@ impl BrowserSessionRepository for PgBrowserSessionRepository<'_> {
|
||||
Expr::col((Users::Table, Users::LockedAt)),
|
||||
SessionLookupIden::UserLockedAt,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((Users::Table, Users::DeactivatedAt)),
|
||||
SessionLookupIden::UserDeactivatedAt,
|
||||
)
|
||||
.expr_as(
|
||||
Expr::col((Users::Table, Users::CanRequestAdmin)),
|
||||
SessionLookupIden::UserCanRequestAdmin,
|
||||
|
||||
@@ -32,6 +32,9 @@ pub use self::{
|
||||
/// The state of a user account
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UserState {
|
||||
/// The account is deactivated, it has the `deactivated_at` timestamp set
|
||||
Deactivated,
|
||||
|
||||
/// The account is locked, it has the `locked_at` timestamp set
|
||||
Locked,
|
||||
|
||||
@@ -48,6 +51,14 @@ impl UserState {
|
||||
matches!(self, Self::Locked)
|
||||
}
|
||||
|
||||
/// Returns `true` if the user state is [`Deactivated`].
|
||||
///
|
||||
/// [`Deactivated`]: UserState::Deactivated
|
||||
#[must_use]
|
||||
pub fn is_deactivated(&self) -> bool {
|
||||
matches!(self, Self::Deactivated)
|
||||
}
|
||||
|
||||
/// Returns `true` if the user state is [`Active`].
|
||||
///
|
||||
/// [`Active`]: UserState::Active
|
||||
@@ -86,6 +97,13 @@ impl UserFilter<'_> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter for deactivated users
|
||||
#[must_use]
|
||||
pub fn deactivated_only(mut self) -> Self {
|
||||
self.state = Some(UserState::Deactivated);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter for users that can request admin privileges
|
||||
#[must_use]
|
||||
pub fn can_request_admin_only(mut self) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user