Extend CLI's user/admin capabilities and disallow change/delete operations on admins from UI. #83

This commit is contained in:
Sebastian Jeltsch
2025-07-11 11:23:15 +02:00
parent 63b6c50311
commit 84ac7a0463
20 changed files with 312 additions and 183 deletions

View File

@@ -1,3 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Request changes to user with given `id`.
*
* NOTE: We don't allow admin promotions and especially demotions, since they could easily be
* abused. Instead we relegate such critical actions to the CLI, which limits them to sys
* admins over mere TrailBase admins.
*/
export type UpdateUserRequest = { id: string, email: string | null, password: string | null, verified: boolean | null, };

View File

@@ -60,12 +60,12 @@ pub enum SubCommands {
/// Optional suffix used for the generated migration file: U<timetamp>__<suffix>.sql.
suffix: Option<String>,
},
/// Simple admin management (use dashboard for everything else).
/// Manage admin users (list, demote, promote).
Admin {
#[command(subcommand)]
cmd: Option<AdminSubCommands>,
},
/// Simple user management (use dashboard for everything else).
/// Manage users. Unlike the admin UI this will also let you change admin users.
User {
#[command(subcommand)]
cmd: Option<UserSubCommands>,
@@ -160,26 +160,54 @@ pub enum AdminSubCommands {
List,
/// Demotes admin user to normal user.
Demote {
/// E-mail of the admin who's demoted.
email: String,
/// Admin in question, either email or UUID.
user: String,
},
/// Promotes user to admin.
Promote {
/// E-mail of the user who's promoted to admin.
email: String,
/// User in question, either email or UUID.
user: String,
},
}
// TODO: Add "create user" (low priority since users can be created via the UI).
#[derive(Subcommand, Debug, Clone)]
pub enum UserSubCommands {
// TODO: create new user. Low prio, use dashboard.
/// Resets a users password.
ResetPassword {
/// E-mail of the user who's password is being reset.
email: String,
/// Password to set.
/// Change a user's password.
ChangePassword {
/// User in question, either email or UUID.
user: String,
/// New password to set for user.
password: String,
},
/// Mint auth tokens for the given user.
MintToken { email: String },
/// Change a user's email.
ChangeEmail {
/// User in question, either email or UUID.
user: String,
/// New email address to set for user.
new_email: String,
},
/// Delete a user.
Delete {
/// User in question, either email or UUID.
user: String,
},
/// Change a user's verification state.
Verify {
/// User in question, either email or UUID.
user: String,
/// User's verification state to set.
#[arg(default_value = "true")]
verified: bool,
},
/// Invalidate user session, thus requiring them to re-auth when their auth token expires.
InvalidateSession {
/// User in question, either email or UUID.
user: String,
},
/// Mint auth token for the given user.
MintToken {
/// User in question, either email or UUID.
user: String,
},
}

View File

@@ -5,13 +5,12 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use chrono::TimeZone;
use clap::{CommandFactory, Parser};
use log::*;
use serde::Deserialize;
use std::rc::Rc;
use tokio::{fs, io::AsyncWriteExt};
use trailbase::{
DataDir, Server, ServerOptions,
api::{self, Email, InitArgs, JsonSchemaMode, TokenClaims, init_app_state},
api::{self, Email, InitArgs, JsonSchemaMode, init_app_state},
constants::USER_TABLE,
};
use utoipa::OpenApi;
@@ -41,7 +40,6 @@ fn init_logger(dev: bool) {
struct DbUser {
id: [u8; 16],
email: String,
verified: bool,
created: i64,
updated: i64,
}
@@ -52,19 +50,6 @@ impl DbUser {
}
}
async fn get_user_by_email(conn: &api::Connection, email: &str) -> Result<DbUser, BoxError> {
if let Some(user) = conn
.read_query_value::<DbUser>(
format!("SELECT * FROM {USER_TABLE} WHERE email = $1"),
(email.to_string(),),
)
.await?
{
return Ok(user);
}
return Err("not found".into());
}
async fn async_main() -> Result<(), BoxError> {
let args = DefaultCommandLineArgs::parse();
let data_dir = DataDir(args.data_dir.clone());
@@ -171,25 +156,13 @@ async fn async_main() -> Result<(), BoxError> {
);
}
}
Some(AdminSubCommands::Demote { email }) => {
conn
.execute(
format!("UPDATE {USER_TABLE} SET admin = FALSE WHERE email = $1"),
(email.clone(),),
)
.await?;
println!("'{email}' has been demoted");
Some(AdminSubCommands::Demote { user }) => {
let id = api::cli::demote_admin_to_user(&conn, to_user_reference(user)).await?;
println!("Demoted admin to user for '{id}'");
}
Some(AdminSubCommands::Promote { email }) => {
conn
.execute(
format!("UPDATE {USER_TABLE} SET admin = TRUE WHERE email = $1"),
(email.clone(),),
)
.await?;
println!("'{email}' is now an admin");
Some(AdminSubCommands::Promote { user }) => {
let id = api::cli::promote_user_to_admin(&conn, to_user_reference(user)).await?;
println!("Promoted user to admin for '{id}'");
}
None => {
DefaultCommandLineArgs::command()
@@ -205,31 +178,30 @@ async fn async_main() -> Result<(), BoxError> {
let (conn, _) = api::init_main_db(Some(&data_dir), None, None)?;
match cmd {
Some(UserSubCommands::ResetPassword { email, password }) => {
if get_user_by_email(&conn, &email).await.is_err() {
return Err(format!("User with email='{email}' not found.").into());
}
api::force_password_reset(&conn, email.clone(), password).await?;
println!("Password updated for '{email}'");
Some(UserSubCommands::ChangePassword { user, password }) => {
let id = api::cli::change_password(&conn, to_user_reference(user), &password).await?;
println!("Updated password for '{id}'");
}
Some(UserSubCommands::MintToken { email }) => {
let user = get_user_by_email(&conn, &email).await?;
let jwt = api::JwtHelper::init_from_path(&data_dir).await?;
if !user.verified {
warn!("User '{email}' not verified");
}
let claims = TokenClaims::new(
user.verified,
user.uuid(),
user.email,
chrono::Duration::hours(12),
);
let token = jwt.encode(&claims)?;
println!("Bearer {token}");
Some(UserSubCommands::ChangeEmail { user, new_email }) => {
let id = api::cli::change_email(&conn, to_user_reference(user), &new_email).await?;
println!("Updated email for '{id}'");
}
Some(UserSubCommands::Delete { user }) => {
api::cli::delete_user(&conn, to_user_reference(user.clone())).await?;
println!("Deleted user '{user}'");
}
Some(UserSubCommands::Verify { user, verified }) => {
let id = api::cli::set_verified(&conn, to_user_reference(user), verified).await?;
println!("Set verified={verified} for '{id}'");
}
Some(UserSubCommands::InvalidateSession { user }) => {
api::cli::invalidate_sessions(&conn, to_user_reference(user.clone())).await?;
println!("Sessions invalidated for '{user}'");
}
Some(UserSubCommands::MintToken { user }) => {
let auth_token =
api::cli::mint_auth_token(&data_dir, &conn, to_user_reference(user.clone())).await?;
println!("Bearer {auth_token}");
}
None => {
DefaultCommandLineArgs::command()
@@ -269,6 +241,13 @@ async fn async_main() -> Result<(), BoxError> {
Ok(())
}
fn to_user_reference(user: String) -> api::cli::UserReference {
if user.contains("@") {
return api::cli::UserReference::Email(user);
}
return api::cli::UserReference::Id(user);
}
fn main() -> Result<(), BoxError> {
let runtime = Rc::new(
tokio::runtime::Builder::new_multi_thread()

View File

@@ -10,8 +10,8 @@ use ts_rs::TS;
use crate::admin::AdminError as Error;
use crate::admin::rows::delete_row;
use crate::admin::user::is_demo_admin;
use crate::app_state::AppState;
use crate::auth::util::is_admin;
use crate::util::uuid_to_b64;
#[derive(Debug, Deserialize, Default, TS)]
@@ -24,8 +24,10 @@ pub async fn delete_user_handler(
State(state): State<AppState>,
Json(request): Json<DeleteUserRequest>,
) -> Result<Response, Error> {
if state.demo_mode() && is_demo_admin(&state, &request.id).await {
return Err(Error::Precondition("Deleting demo admin forbidden".into()));
if is_admin(&state, &request.id).await {
return Err(Error::Precondition(
"Admins can only be deleted using the CLI to prevent abuse".into(),
));
}
delete_row(

View File

@@ -1,9 +1,3 @@
use trailbase_sqlite::params;
use uuid::Uuid;
use crate::AppState;
use crate::constants::USER_TABLE;
mod create_user;
mod delete_user;
mod list_users;
@@ -14,26 +8,6 @@ pub(super) use delete_user::delete_user_handler;
pub(super) use list_users::list_users_handler;
pub(super) use update_user::update_user_handler;
pub async fn is_demo_admin(state: &AppState, id: &Uuid) -> bool {
assert!(state.demo_mode());
let userid: [u8; 16] = id.into_bytes();
return match state
.user_conn()
.read_query_value(
format!("SELECT EXISTS(SELECT * FROM {USER_TABLE} WHERE id=$1 AND email='admin@localhost')"),
params!(userid),
)
.await
{
Ok(value) => value.unwrap_or(true),
Err(err) => {
log::error!("{err}");
true
}
};
}
#[cfg(test)]
pub(crate) use create_user::create_user_for_test;

View File

@@ -10,11 +10,16 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::admin::AdminError as Error;
use crate::admin::user::is_demo_admin;
use crate::app_state::AppState;
use crate::auth::password::hash_password;
use crate::auth::util::is_admin;
use crate::constants::USER_TABLE;
/// Request changes to user with given `id`.
///
/// NOTE: We don't allow admin promotions and especially demotions, since they could easily be
/// abused. Instead we relegate such critical actions to the CLI, which limits them to sys
/// admins over mere TrailBase admins.
#[derive(Debug, Serialize, Deserialize, Default, TS)]
#[ts(export)]
pub struct UpdateUserRequest {
@@ -29,8 +34,10 @@ pub async fn update_user_handler(
State(state): State<AppState>,
Json(request): Json<UpdateUserRequest>,
) -> Result<Response, Error> {
if state.demo_mode() && is_demo_admin(&state, &request.id).await {
return Err(Error::Precondition("Updating demo admin forbidden".into()));
if is_admin(&state, &request.id).await {
return Err(Error::Precondition(
"Admins can only be updated using the CLI to prevent abuse".into(),
));
}
let hashed_password = match &request.password {

View File

@@ -24,7 +24,7 @@ pub(crate) async fn delete_handler(
user: User,
cookies: Cookies,
) -> Result<Response, AuthError> {
let _ = delete_all_sessions_for_user(&state, user.uuid).await;
let _ = delete_all_sessions_for_user(state.user_conn(), user.uuid).await;
lazy_static! {
static ref QUERY: String = format!(r#"DELETE FROM "{USER_TABLE}" WHERE id = $1"#);

View File

@@ -285,7 +285,10 @@ pub async fn login_with_password(
normalized_email: &str,
password: &str,
) -> Result<NewTokens, AuthError> {
let db_user: DbUser = user_by_email(state, normalized_email).await?;
let db_user: DbUser = user_by_email(state, normalized_email).await.map_err(|_| {
// Don't leak if user wasn't found or password was wrong.
return AuthError::Unauthorized;
})?;
// Validate password.
check_user_password(&db_user, password, state.demo_mode())?;
@@ -293,14 +296,7 @@ pub async fn login_with_password(
let (auth_token_ttl, _refresh_token_ttl) = state.access_config(|c| c.auth.token_ttls());
let user_id = db_user.uuid();
let tokens = mint_new_tokens(
state,
db_user.verified,
user_id,
db_user.email,
auth_token_ttl,
)
.await?;
let tokens = mint_new_tokens(state.user_conn(), &db_user, auth_token_ttl).await?;
return Ok(NewTokens {
id: user_id,

View File

@@ -44,7 +44,7 @@ pub async fn logout_handler(
remove_all_cookies(&cookies);
if let Some(user) = user {
delete_all_sessions_for_user(&state, user.uuid).await?;
delete_all_sessions_for_user(state.user_conn(), user.uuid).await?;
}
return Ok(Redirect::to(redirect.as_deref().unwrap_or_else(|| {

View File

@@ -8,7 +8,6 @@ use serde::Deserialize;
use trailbase_sqlite::params;
use ts_rs::TS;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::app_state::AppState;
use crate::constants::USER_TABLE;
@@ -169,21 +168,3 @@ pub async fn reset_password_update_handler(
}
};
}
pub async fn force_password_reset(
user_conn: &trailbase_sqlite::Connection,
email: String,
password: String,
) -> Result<Uuid, AuthError> {
let hashed_password = hash_password(&password)?;
lazy_static! {
static ref UPDATE_PASSWORD_QUERY: String =
format!("UPDATE '{USER_TABLE}' SET password_hash = $1 WHERE email = $2 RETURNING id");
}
return user_conn
.write_query_value(&*UPDATE_PASSWORD_QUERY, params!(hashed_password, email))
.await?
.ok_or(AuthError::NotFound);
}

View File

@@ -86,16 +86,8 @@ pub(crate) async fn auth_code_to_token_handler(
};
let (auth_token_ttl, _refresh_token_ttl) = state.access_config(|c| c.auth.token_ttls());
let user_id = db_user.uuid();
let tokens = mint_new_tokens(
&state,
db_user.verified,
user_id,
db_user.email,
auth_token_ttl,
)
.await?;
let tokens = mint_new_tokens(state.user_conn(), &db_user, auth_token_ttl).await?;
let auth_token = state
.jwt()
.encode(&tokens.auth_token_claims)

View File

@@ -0,0 +1,180 @@
use base64::prelude::*;
use lazy_static::lazy_static;
use trailbase_sqlite::{Connection, params};
use uuid::Uuid;
use crate::DataDir;
use crate::auth::AuthError;
use crate::auth::password::hash_password;
use crate::auth::tokens::mint_new_tokens;
use crate::auth::user::DbUser;
use crate::auth::util::{get_user_by_email, get_user_by_id};
use crate::constants::USER_TABLE;
pub enum UserReference {
Email(String),
Id(String),
}
impl UserReference {
async fn lookup_user(&self, user_conn: &Connection) -> Result<DbUser, AuthError> {
return match self {
Self::Email(email) => get_user_by_email(user_conn, email).await,
Self::Id(id) => {
let decoded_id = Uuid::parse_str(id).or_else(|_| {
let bytes = BASE64_URL_SAFE.decode(id).map_err(|err| {
AuthError::FailedDependency(format!("Failed to parse Base64: {err}").into())
})?;
return Uuid::from_slice(&bytes).map_err(|err| {
AuthError::FailedDependency(format!("Failed to parse UUID from slice: {err}").into())
});
})?;
get_user_by_id(user_conn, &decoded_id).await
}
};
}
}
pub async fn change_password(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
password: &str,
) -> Result<Uuid, AuthError> {
let db_user = user.lookup_user(user_conn).await?;
let hashed_password = hash_password(password)?;
lazy_static! {
static ref UPDATE_PASSWORD_QUERY: String =
format!("UPDATE '{USER_TABLE}' SET password_hash = $1 WHERE id = $2 RETURNING id");
}
return user_conn
.write_query_value(
&*UPDATE_PASSWORD_QUERY,
params!(hashed_password, db_user.id),
)
.await?
.ok_or(AuthError::NotFound);
}
pub async fn change_email(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
new_email: &str,
) -> Result<Uuid, AuthError> {
let db_user = user.lookup_user(user_conn).await?;
lazy_static! {
static ref UPDATE_EMAIL_QUERY: String =
format!("UPDATE '{USER_TABLE}' SET email = $1 WHERE id = $2 RETURNING id");
}
return user_conn
.write_query_value(
&*UPDATE_EMAIL_QUERY,
params!(new_email.to_string(), db_user.id),
)
.await?
.ok_or(AuthError::NotFound);
}
pub async fn delete_user(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
) -> Result<(), AuthError> {
let db_user = user.lookup_user(user_conn).await?;
lazy_static! {
static ref DELETE_QUERY: String = format!(r#"DELETE FROM "{USER_TABLE}" WHERE id = $1"#);
}
let rows_affected = user_conn
.execute(&*DELETE_QUERY, params!(db_user.id))
.await?;
if rows_affected > 0 {
return Ok(());
}
return Err(AuthError::NotFound);
}
pub async fn set_verified(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
verified: bool,
) -> Result<Uuid, AuthError> {
let db_user = user.lookup_user(user_conn).await?;
lazy_static! {
static ref SET_VERIFIED_QUERY: String =
format!("UPDATE '{USER_TABLE}' SET verified = $1 WHERE id = $2 RETURNING id");
}
return user_conn
.write_query_value(&*SET_VERIFIED_QUERY, params!(verified, db_user.id))
.await?
.ok_or(AuthError::NotFound);
}
pub async fn invalidate_sessions(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
) -> Result<(), AuthError> {
let db_user = user.lookup_user(user_conn).await?;
crate::auth::util::delete_all_sessions_for_user(user_conn, Uuid::from_bytes(db_user.id)).await?;
return Ok(());
}
pub async fn mint_auth_token(
data_dir: &DataDir,
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
) -> Result<String, AuthError> {
let jwt = crate::api::JwtHelper::init_from_path(data_dir)
.await
.map_err(|err| AuthError::FailedDependency(err.into()))?;
let db_user = user.lookup_user(user_conn).await?;
let tokens = mint_new_tokens(user_conn, &db_user, chrono::Duration::hours(12)).await?;
let auth_token = jwt
.encode(&tokens.auth_token_claims)
.map_err(|err| AuthError::Internal(err.into()))?;
return Ok(auth_token);
}
pub async fn promote_user_to_admin(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
) -> Result<Uuid, AuthError> {
let db_user = user.lookup_user(user_conn).await?;
lazy_static! {
static ref PROMOTE_ADMIN_QUERY: String =
format!("UPDATE {USER_TABLE} SET admin = TRUE WHERE id = $1 RETURNING id");
}
return user_conn
.write_query_value(&*PROMOTE_ADMIN_QUERY, params!(db_user.id))
.await?
.ok_or(AuthError::NotFound);
}
pub async fn demote_admin_to_user(
user_conn: &trailbase_sqlite::Connection,
user: UserReference,
) -> Result<Uuid, AuthError> {
let db_user = user.lookup_user(user_conn).await?;
lazy_static! {
static ref DEMOTE_ADMIN_QUERY: String =
format!("UPDATE {USER_TABLE} SET admin = FALSE WHERE id = $1 RETURNING id");
}
return user_conn
.write_query_value(&*DEMOTE_ADMIN_QUERY, params!(db_user.id))
.await?
.ok_or(AuthError::NotFound);
}

View File

@@ -8,8 +8,6 @@ use thiserror::Error;
pub enum AuthError {
#[error("Unauthorized")]
Unauthorized,
#[error("Unauthorized")]
UnauthorizedExt(Box<dyn std::error::Error + Send + Sync>),
#[error("Forbidden")]
Forbidden,
#[error("Conflict")]
@@ -66,10 +64,6 @@ impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, body) = match self {
Self::Unauthorized => (StatusCode::UNAUTHORIZED, None),
Self::UnauthorizedExt(msg) if cfg!(debug_assertions) => {
(StatusCode::UNAUTHORIZED, Some(msg.to_string()))
}
Self::UnauthorizedExt(_msg) => (StatusCode::UNAUTHORIZED, None),
Self::Forbidden => (StatusCode::FORBIDDEN, None),
Self::Conflict => (StatusCode::CONFLICT, None),
Self::NotFound => (StatusCode::NOT_FOUND, None),

View File

@@ -4,6 +4,7 @@ use axum::{
};
use utoipa::OpenApi;
pub mod cli;
pub mod jwt;
pub mod user;
@@ -17,7 +18,6 @@ pub(crate) mod util;
mod error;
mod ui;
pub use api::reset_password::force_password_reset;
pub use error::AuthError;
pub use jwt::{JwtHelper, TokenClaims};
pub(crate) use ui::auth_ui_router;

View File

@@ -131,14 +131,7 @@ pub(crate) async fn callback_from_external_auth_provider(
auth_token_claims,
refresh_token,
..
} = mint_new_tokens(
&state,
db_user.verified,
db_user.uuid(),
db_user.email,
expires_in,
)
.await?;
} = mint_new_tokens(state.user_conn(), &db_user, expires_in).await?;
let auth_token = state
.jwt()

View File

@@ -5,7 +5,7 @@ use axum::{
use chrono::Duration;
use lazy_static::lazy_static;
use tower_cookies::Cookies;
use trailbase_sqlite::params;
use trailbase_sqlite::{Connection, params};
use crate::app_state::AppState;
use crate::auth::AuthError;
@@ -159,20 +159,19 @@ pub struct FreshTokens {
}
pub(crate) async fn mint_new_tokens(
state: &AppState,
verified: bool,
user_id: uuid::Uuid,
user_email: String,
user_conn: &Connection,
db_user: &DbUser,
expires_in: Duration,
) -> Result<FreshTokens, AuthError> {
assert!(verified);
let verified = db_user.verified;
if !verified {
return Err(AuthError::Internal(
"Cannot mint tokens for unverified user".into(),
));
}
let claims = TokenClaims::new(verified, user_id, user_email, expires_in);
let user_id = db_user.uuid();
let claims = TokenClaims::new(verified, user_id, db_user.email.clone(), expires_in);
// Unlike JWT auth tokens, refresh tokens are opaque.
let refresh_token = generate_random_string(REFRESH_TOKEN_LENGTH);
@@ -181,8 +180,7 @@ pub(crate) async fn mint_new_tokens(
format!("INSERT INTO '{SESSION_TABLE}' (user, refresh_token) VALUES ($1, $2)");
}
state
.user_conn()
user_conn
.execute(
&*QUERY,
params!(user_id.into_bytes().to_vec(), refresh_token.clone(),),

View File

@@ -105,8 +105,7 @@ impl User {
/// Construct new verified [User] from [TokenClaims]. This is used when picking
/// credentials/tokens from headers/cookies.
pub(crate) fn from_token_claims(claims: TokenClaims) -> Result<Self, AuthError> {
let uuid = b64_to_uuid(&claims.sub)
.map_err(|_err| AuthError::UnauthorizedExt("invalid user id".into()))?;
let uuid = b64_to_uuid(&claims.sub).map_err(|_err| AuthError::BadRequest("invalid user id"))?;
return Ok(Self {
id: claims.sub,

View File

@@ -6,12 +6,12 @@ use tower_cookies::{
Cookie, Cookies,
cookie::{self, SameSite},
};
use trailbase_sqlite::params;
use trailbase_sqlite::{Connection, params};
use validator::ValidateEmail;
use crate::AppState;
use crate::auth::AuthError;
use crate::auth::user::{DbUser, User};
use crate::auth::user::DbUser;
use crate::constants::{
COOKIE_AUTH_TOKEN, COOKIE_OAUTH_STATE, COOKIE_REFRESH_TOKEN, SESSION_TABLE, USER_TABLE,
};
@@ -157,16 +157,16 @@ pub async fn get_user_by_email(
let db_user = user_conn
.read_query_value::<DbUser>(&*QUERY, params!(email.to_string()))
.await
.map_err(|_err| AuthError::UnauthorizedExt("user not found by email".into()))?;
.map_err(|_err| AuthError::NotFound)?;
return db_user.ok_or_else(|| AuthError::UnauthorizedExt("invalid user".into()));
return db_user.ok_or_else(|| AuthError::NotFound);
}
pub async fn user_by_id(state: &AppState, id: &uuid::Uuid) -> Result<DbUser, AuthError> {
return get_user_by_id(state.user_conn(), id).await;
}
async fn get_user_by_id(
pub async fn get_user_by_id(
user_conn: &trailbase_sqlite::Connection,
id: &uuid::Uuid,
) -> Result<DbUser, AuthError> {
@@ -176,9 +176,9 @@ async fn get_user_by_id(
let db_user = user_conn
.read_query_value::<DbUser>(&*QUERY, params!(id.into_bytes()))
.await
.map_err(|_err| AuthError::UnauthorizedExt("User not found by id".into()))?;
.map_err(|_err| AuthError::NotFound)?;
return db_user.ok_or_else(|| AuthError::UnauthorizedExt("invalid user".into()));
return db_user.ok_or_else(|| AuthError::NotFound);
}
pub async fn user_exists(state: &AppState, email: &str) -> Result<bool, AuthError> {
@@ -193,14 +193,14 @@ pub async fn user_exists(state: &AppState, email: &str) -> Result<bool, AuthErro
.ok_or_else(|| AuthError::Internal("query should return".into()));
}
pub(crate) async fn is_admin(state: &AppState, user: &User) -> bool {
pub(crate) async fn is_admin(state: &AppState, user_id: &uuid::Uuid) -> bool {
lazy_static! {
static ref QUERY: String = format!(r#"SELECT admin FROM "{USER_TABLE}" WHERE id = $1"#);
};
let Ok(Some(row)) = state
.user_conn()
.read_query_row_f(&*QUERY, params!(user.uuid.as_bytes().to_vec()), |row| {
.read_query_row_f(&*QUERY, params!(user_id.as_bytes().to_vec()), |row| {
row.get(0)
})
.await
@@ -212,7 +212,7 @@ pub(crate) async fn is_admin(state: &AppState, user: &User) -> bool {
}
pub(crate) async fn delete_all_sessions_for_user(
state: &AppState,
user_conn: &Connection,
user_id: uuid::Uuid,
) -> Result<usize, AuthError> {
lazy_static! {
@@ -220,8 +220,7 @@ pub(crate) async fn delete_all_sessions_for_user(
};
return Ok(
state
.user_conn()
user_conn
.execute(
&*QUERY,
[trailbase_sqlite::Value::Blob(user_id.into_bytes().to_vec())],

View File

@@ -63,7 +63,7 @@ pub mod openapi {
pub mod api {
pub use crate::admin::user::{CreateUserRequest, create_user_handler};
pub use crate::auth::api::login::login_with_password;
pub use crate::auth::{JwtHelper, TokenClaims, force_password_reset};
pub use crate::auth::{JwtHelper, TokenClaims, cli};
pub use crate::connection::{Connection, init_main_db};
pub use crate::email::{Email, EmailError};
pub use crate::migrations::new_unique_migration_filename;

View File

@@ -383,7 +383,7 @@ async fn assert_admin_api_access(
) -> Result<Response, AuthError> {
let user = req.extract_parts_with_state::<User, _>(&state).await?;
if !is_admin(&state, &user).await {
if !is_admin(&state, &user.uuid).await {
return Err(AuthError::Forbidden);
}