You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Add a way to lock users
This commit is contained in:
@ -69,6 +69,18 @@ enum Subcommand {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Lock a user
|
||||||
|
LockUser {
|
||||||
|
/// User to lock
|
||||||
|
username: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Unlock a user
|
||||||
|
UnlockUser {
|
||||||
|
/// User to unlock
|
||||||
|
username: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
@ -330,6 +342,46 @@ impl Options {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SC::LockUser { username } => {
|
||||||
|
let _span = info_span!("cli.manage.lock_user", user.username = username).entered();
|
||||||
|
let config: DatabaseConfig = root.load_config()?;
|
||||||
|
let pool = database_from_config(&config).await?;
|
||||||
|
let mut repo = PgRepository::from_pool(&pool).await?.boxed();
|
||||||
|
|
||||||
|
let user = repo
|
||||||
|
.user()
|
||||||
|
.find_by_username(&username)
|
||||||
|
.await?
|
||||||
|
.context("User not found")?;
|
||||||
|
|
||||||
|
info!(%user.id, "Locking user");
|
||||||
|
|
||||||
|
repo.user().lock(&clock, user).await?;
|
||||||
|
repo.save().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
SC::UnlockUser { username } => {
|
||||||
|
let _span = info_span!("cli.manage.lock_user", user.username = username).entered();
|
||||||
|
let config: DatabaseConfig = root.load_config()?;
|
||||||
|
let pool = database_from_config(&config).await?;
|
||||||
|
let mut repo = PgRepository::from_pool(&pool).await?.boxed();
|
||||||
|
|
||||||
|
let user = repo
|
||||||
|
.user()
|
||||||
|
.find_by_username(&username)
|
||||||
|
.await?
|
||||||
|
.context("User not found")?;
|
||||||
|
|
||||||
|
info!(%user.id, "Unlocking user");
|
||||||
|
|
||||||
|
repo.user().unlock(user).await?;
|
||||||
|
repo.save().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,16 @@ pub struct User {
|
|||||||
pub username: String,
|
pub username: String,
|
||||||
pub sub: String,
|
pub sub: String,
|
||||||
pub primary_user_email_id: Option<Ulid>,
|
pub primary_user_email_id: Option<Ulid>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub locked_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
/// Returns `true` unless the user is locked.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.locked_at.is_none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
@ -35,6 +45,8 @@ impl User {
|
|||||||
username: "john".to_owned(),
|
username: "john".to_owned(),
|
||||||
sub: "123-456".to_owned(),
|
sub: "123-456".to_owned(),
|
||||||
primary_user_email_id: None,
|
primary_user_email_id: None,
|
||||||
|
created_at: now,
|
||||||
|
locked_at: None,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +77,7 @@ pub struct BrowserSession {
|
|||||||
impl BrowserSession {
|
impl BrowserSession {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn active(&self) -> bool {
|
pub fn active(&self) -> bool {
|
||||||
self.finished_at.is_none()
|
self.finished_at.is_none() && self.user.is_valid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,6 +335,7 @@ async fn token_login(
|
|||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(session.user_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
.ok_or(RouteError::UserNotFound)?;
|
.ok_or(RouteError::UserNotFound)?;
|
||||||
|
|
||||||
repo.compat_sso_login().exchange(clock, login).await?;
|
repo.compat_sso_login().exchange(clock, login).await?;
|
||||||
@ -355,6 +356,7 @@ async fn user_password_login(
|
|||||||
.user()
|
.user()
|
||||||
.find_by_username(&username)
|
.find_by_username(&username)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
.ok_or(RouteError::UserNotFound)?;
|
.ok_or(RouteError::UserNotFound)?;
|
||||||
|
|
||||||
// Lookup its password
|
// Lookup its password
|
||||||
|
@ -188,6 +188,7 @@ pub(crate) async fn post(
|
|||||||
.browser_session()
|
.browser_session()
|
||||||
.lookup(session.user_session_id)
|
.lookup(session.user_session_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(|b| b.user.is_valid())
|
||||||
// XXX: is that the right error to bubble up?
|
// XXX: is that the right error to bubble up?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
@ -227,6 +228,7 @@ pub(crate) async fn post(
|
|||||||
.browser_session()
|
.browser_session()
|
||||||
.lookup(session.user_session_id)
|
.lookup(session.user_session_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(|b| b.user.is_valid())
|
||||||
// XXX: is that the right error to bubble up?
|
// XXX: is that the right error to bubble up?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
@ -265,6 +267,7 @@ pub(crate) async fn post(
|
|||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(session.user_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
// XXX: is that the right error to bubble up?
|
// XXX: is that the right error to bubble up?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
@ -311,6 +314,7 @@ pub(crate) async fn post(
|
|||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(session.user_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
// XXX: is that the right error to bubble up?
|
// XXX: is that the right error to bubble up?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ use mas_axum_utils::{
|
|||||||
csrf::{CsrfExt, ProtectedForm},
|
csrf::{CsrfExt, ProtectedForm},
|
||||||
SessionInfoExt,
|
SessionInfoExt,
|
||||||
};
|
};
|
||||||
use mas_data_model::UpstreamOAuthProviderImportPreference;
|
use mas_data_model::{UpstreamOAuthProviderImportPreference, User};
|
||||||
use mas_jose::jwt::Jwt;
|
use mas_jose::jwt::Jwt;
|
||||||
use mas_keystore::Encrypter;
|
use mas_keystore::Encrypter;
|
||||||
use mas_storage::{
|
use mas_storage::{
|
||||||
@ -239,6 +239,8 @@ pub(crate) async fn get(
|
|||||||
.user()
|
.user()
|
||||||
.lookup(user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
|
// XXX: is that right?
|
||||||
|
.filter(User::is_valid)
|
||||||
.ok_or(RouteError::UserNotFound)?;
|
.ok_or(RouteError::UserNotFound)?;
|
||||||
|
|
||||||
let ctx = UpstreamExistingLinkContext::new(user)
|
let ctx = UpstreamExistingLinkContext::new(user)
|
||||||
@ -263,6 +265,7 @@ pub(crate) async fn get(
|
|||||||
.user()
|
.user()
|
||||||
.lookup(user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
.ok_or(RouteError::UserNotFound)?;
|
.ok_or(RouteError::UserNotFound)?;
|
||||||
|
|
||||||
let ctx = UpstreamExistingLinkContext::new(user).with_csrf(csrf_token.form_value());
|
let ctx = UpstreamExistingLinkContext::new(user).with_csrf(csrf_token.form_value());
|
||||||
@ -390,6 +393,7 @@ pub(crate) async fn post(
|
|||||||
.user()
|
.user()
|
||||||
.lookup(user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
.ok_or(RouteError::UserNotFound)?;
|
.ok_or(RouteError::UserNotFound)?;
|
||||||
|
|
||||||
repo.browser_session().add(&mut rng, &clock, &user).await?
|
repo.browser_session().add(&mut rng, &clock, &user).await?
|
||||||
|
@ -202,6 +202,7 @@ async fn login(
|
|||||||
.find_by_username(username)
|
.find_by_username(username)
|
||||||
.await
|
.await
|
||||||
.map_err(|_e| FormError::Internal)?
|
.map_err(|_e| FormError::Internal)?
|
||||||
|
.filter(mas_data_model::User::is_valid)
|
||||||
.ok_or(FormError::InvalidCredentials)?;
|
.ok_or(FormError::InvalidCredentials)?;
|
||||||
|
|
||||||
// And its password
|
// And its password
|
||||||
|
14
crates/storage-pg/.sqlx/query-22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc.json
generated
Normal file
14
crates/storage-pg/.sqlx/query-22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc.json
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE users\n SET locked_at = NULL\n WHERE user_id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc"
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"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 , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\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 , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -32,6 +32,16 @@
|
|||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"name": "user_primary_user_email_id",
|
"name": "user_primary_user_email_id",
|
||||||
"type_info": "Uuid"
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "user_created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "user_locked_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -45,8 +55,10 @@
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e"
|
"hash": "73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d"
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n FROM users\n WHERE username = $1\n ",
|
"query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n , locked_at\n FROM users\n WHERE username = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -22,6 +22,11 @@
|
|||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "locked_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -33,8 +38,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false
|
false,
|
||||||
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c"
|
"hash": "bfa5eaeaa5b4574bb083c86711eb4599f6374c96bb4a6827d400acb22fb0fd39"
|
||||||
}
|
}
|
15
crates/storage-pg/.sqlx/query-c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1.json
generated
Normal file
15
crates/storage-pg/.sqlx/query-c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1.json
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE users\n SET locked_at = $1\n WHERE user_id = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Timestamptz",
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1"
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n FROM users\n WHERE user_id = $1\n ",
|
"query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n , locked_at\n FROM users\n WHERE user_id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -22,6 +22,11 @@
|
|||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "locked_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -33,8 +38,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false
|
false,
|
||||||
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35"
|
"hash": "e0ea7d93ab3f565828b2faab4cc5e1a6ac868c95bfaee3a6960df1cf484d53da"
|
||||||
}
|
}
|
19
crates/storage-pg/migrations/20230728154304_user_lock.sql
Normal file
19
crates/storage-pg/migrations/20230728154304_user_lock.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
--
|
||||||
|
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
-- you may not use this file except in compliance with the License.
|
||||||
|
-- You may obtain a copy of the License at
|
||||||
|
--
|
||||||
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
--
|
||||||
|
-- Unless required by applicable law or agreed to in writing, software
|
||||||
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
-- See the License for the specific language governing permissions and
|
||||||
|
-- limitations under the License.
|
||||||
|
|
||||||
|
-- Add a new column in on the `users` to record when an account gets locked
|
||||||
|
ALTER TABLE "users"
|
||||||
|
ADD COLUMN "locked_at"
|
||||||
|
TIMESTAMP WITH TIME ZONE
|
||||||
|
DEFAULT NULL;
|
@ -29,6 +29,8 @@ pub enum Users {
|
|||||||
UserId,
|
UserId,
|
||||||
Username,
|
Username,
|
||||||
PrimaryUserEmailId,
|
PrimaryUserEmailId,
|
||||||
|
CreatedAt,
|
||||||
|
LockedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sea_query::Iden)]
|
#[derive(sea_query::Iden)]
|
||||||
|
@ -55,9 +55,8 @@ struct UserLookup {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
primary_user_email_id: Option<Uuid>,
|
primary_user_email_id: Option<Uuid>,
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
|
locked_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UserLookup> for User {
|
impl From<UserLookup> for User {
|
||||||
@ -68,6 +67,8 @@ impl From<UserLookup> for User {
|
|||||||
username: value.username,
|
username: value.username,
|
||||||
sub: id.to_string(),
|
sub: id.to_string(),
|
||||||
primary_user_email_id: value.primary_user_email_id.map(Into::into),
|
primary_user_email_id: value.primary_user_email_id.map(Into::into),
|
||||||
|
created_at: value.created_at,
|
||||||
|
locked_at: value.locked_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +94,7 @@ impl<'c> UserRepository for PgUserRepository<'c> {
|
|||||||
, username
|
, username
|
||||||
, primary_user_email_id
|
, primary_user_email_id
|
||||||
, created_at
|
, created_at
|
||||||
|
, locked_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
"#,
|
"#,
|
||||||
@ -124,6 +126,7 @@ impl<'c> UserRepository for PgUserRepository<'c> {
|
|||||||
, username
|
, username
|
||||||
, primary_user_email_id
|
, primary_user_email_id
|
||||||
, created_at
|
, created_at
|
||||||
|
, locked_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = $1
|
WHERE username = $1
|
||||||
"#,
|
"#,
|
||||||
@ -176,6 +179,8 @@ impl<'c> UserRepository for PgUserRepository<'c> {
|
|||||||
username,
|
username,
|
||||||
sub: id.to_string(),
|
sub: id.to_string(),
|
||||||
primary_user_email_id: None,
|
primary_user_email_id: None,
|
||||||
|
created_at,
|
||||||
|
locked_at: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,4 +208,72 @@ impl<'c> UserRepository for PgUserRepository<'c> {
|
|||||||
|
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "db.user.lock",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
db.statement,
|
||||||
|
%user.id,
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
async fn lock(&mut self, clock: &dyn Clock, mut user: User) -> Result<User, Self::Error> {
|
||||||
|
if user.locked_at.is_some() {
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
let locked_at = clock.now();
|
||||||
|
let res = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE users
|
||||||
|
SET locked_at = $1
|
||||||
|
WHERE user_id = $2
|
||||||
|
"#,
|
||||||
|
locked_at,
|
||||||
|
Uuid::from(user.id),
|
||||||
|
)
|
||||||
|
.traced()
|
||||||
|
.execute(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||||
|
|
||||||
|
user.locked_at = Some(locked_at);
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "db.user.unlock",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
db.statement,
|
||||||
|
%user.id,
|
||||||
|
),
|
||||||
|
err,
|
||||||
|
)]
|
||||||
|
async fn unlock(&mut self, mut user: User) -> Result<User, Self::Error> {
|
||||||
|
if user.locked_at.is_none() {
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE users
|
||||||
|
SET locked_at = NULL
|
||||||
|
WHERE user_id = $1
|
||||||
|
"#,
|
||||||
|
Uuid::from(user.id),
|
||||||
|
)
|
||||||
|
.traced()
|
||||||
|
.execute(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
DatabaseError::ensure_affected_rows(&res, 1)?;
|
||||||
|
|
||||||
|
user.locked_at = None;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ struct SessionLookup {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
user_username: String,
|
user_username: String,
|
||||||
user_primary_user_email_id: Option<Uuid>,
|
user_primary_user_email_id: Option<Uuid>,
|
||||||
|
user_created_at: DateTime<Utc>,
|
||||||
|
user_locked_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<SessionLookup> for BrowserSession {
|
impl TryFrom<SessionLookup> for BrowserSession {
|
||||||
@ -65,6 +67,8 @@ impl TryFrom<SessionLookup> for BrowserSession {
|
|||||||
username: value.user_username,
|
username: value.user_username,
|
||||||
sub: id.to_string(),
|
sub: id.to_string(),
|
||||||
primary_user_email_id: value.user_primary_user_email_id.map(Into::into),
|
primary_user_email_id: value.user_primary_user_email_id.map(Into::into),
|
||||||
|
created_at: value.user_created_at,
|
||||||
|
locked_at: value.user_locked_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BrowserSession {
|
Ok(BrowserSession {
|
||||||
@ -99,6 +103,8 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
, u.user_id
|
, u.user_id
|
||||||
, u.username AS "user_username"
|
, u.username AS "user_username"
|
||||||
, u.primary_user_email_id AS "user_primary_user_email_id"
|
, u.primary_user_email_id AS "user_primary_user_email_id"
|
||||||
|
, u.created_at AS "user_created_at"
|
||||||
|
, u.locked_at AS "user_locked_at"
|
||||||
FROM user_sessions s
|
FROM user_sessions s
|
||||||
INNER JOIN users u
|
INNER JOIN users u
|
||||||
USING (user_id)
|
USING (user_id)
|
||||||
@ -232,6 +238,14 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> {
|
|||||||
Expr::col((Users::Table, Users::PrimaryUserEmailId)),
|
Expr::col((Users::Table, Users::PrimaryUserEmailId)),
|
||||||
SessionLookupIden::UserPrimaryUserEmailId,
|
SessionLookupIden::UserPrimaryUserEmailId,
|
||||||
)
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((Users::Table, Users::CreatedAt)),
|
||||||
|
SessionLookupIden::UserCreatedAt,
|
||||||
|
)
|
||||||
|
.expr_as(
|
||||||
|
Expr::col((Users::Table, Users::LockedAt)),
|
||||||
|
SessionLookupIden::UserLockedAt,
|
||||||
|
)
|
||||||
.from(UserSessions::Table)
|
.from(UserSessions::Table)
|
||||||
.inner_join(
|
.inner_join(
|
||||||
Users::Table,
|
Users::Table,
|
||||||
|
@ -96,6 +96,33 @@ pub trait UserRepository: Send + Sync {
|
|||||||
///
|
///
|
||||||
/// Returns [`Self::Error`] if the underlying repository fails
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
|
async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
|
||||||
|
|
||||||
|
/// Lock a [`User`]
|
||||||
|
///
|
||||||
|
/// Returns the locked [`User`]
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `clock`: The clock used to generate timestamps
|
||||||
|
/// * `user`: The [`User`] to lock
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
|
async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
|
||||||
|
|
||||||
|
/// Unlock a [`User`]
|
||||||
|
///
|
||||||
|
/// Returns the unlocked [`User`]
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
///
|
||||||
|
/// * `user`: The [`User`] to unlock
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Self::Error`] if the underlying repository fails
|
||||||
|
async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
repository_impl!(UserRepository:
|
repository_impl!(UserRepository:
|
||||||
@ -108,4 +135,6 @@ repository_impl!(UserRepository:
|
|||||||
username: String,
|
username: String,
|
||||||
) -> Result<User, Self::Error>;
|
) -> Result<User, Self::Error>;
|
||||||
async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
|
async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
|
||||||
|
async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
|
||||||
|
async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user