diff --git a/trailbase-assets/js/bindings/UpdateUserRequest.ts b/trailbase-assets/js/bindings/UpdateUserRequest.ts index 2fdcad09..edc76f77 100644 --- a/trailbase-assets/js/bindings/UpdateUserRequest.ts +++ b/trailbase-assets/js/bindings/UpdateUserRequest.ts @@ -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, }; diff --git a/trailbase-cli/src/args.rs b/trailbase-cli/src/args.rs index 4f694508..456bc67d 100644 --- a/trailbase-cli/src/args.rs +++ b/trailbase-cli/src/args.rs @@ -60,12 +60,12 @@ pub enum SubCommands { /// Optional suffix used for the generated migration file: U__.sql. suffix: Option, }, - /// Simple admin management (use dashboard for everything else). + /// Manage admin users (list, demote, promote). Admin { #[command(subcommand)] cmd: Option, }, - /// 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, @@ -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, + }, } diff --git a/trailbase-cli/src/bin/trail.rs b/trailbase-cli/src/bin/trail.rs index 745337f9..c3f4bd29 100644 --- a/trailbase-cli/src/bin/trail.rs +++ b/trailbase-cli/src/bin/trail.rs @@ -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 { - if let Some(user) = conn - .read_query_value::( - 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() diff --git a/trailbase-core/src/admin/user/delete_user.rs b/trailbase-core/src/admin/user/delete_user.rs index b7182c97..2ff0ac80 100644 --- a/trailbase-core/src/admin/user/delete_user.rs +++ b/trailbase-core/src/admin/user/delete_user.rs @@ -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, Json(request): Json, ) -> Result { - 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( diff --git a/trailbase-core/src/admin/user/mod.rs b/trailbase-core/src/admin/user/mod.rs index ad909c7c..f2d9b034 100644 --- a/trailbase-core/src/admin/user/mod.rs +++ b/trailbase-core/src/admin/user/mod.rs @@ -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; diff --git a/trailbase-core/src/admin/user/update_user.rs b/trailbase-core/src/admin/user/update_user.rs index 20977dd4..f718d978 100644 --- a/trailbase-core/src/admin/user/update_user.rs +++ b/trailbase-core/src/admin/user/update_user.rs @@ -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, Json(request): Json, ) -> Result { - 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 { diff --git a/trailbase-core/src/auth/api/delete.rs b/trailbase-core/src/auth/api/delete.rs index e7c5f257..f954be57 100644 --- a/trailbase-core/src/auth/api/delete.rs +++ b/trailbase-core/src/auth/api/delete.rs @@ -24,7 +24,7 @@ pub(crate) async fn delete_handler( user: User, cookies: Cookies, ) -> Result { - 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"#); diff --git a/trailbase-core/src/auth/api/login.rs b/trailbase-core/src/auth/api/login.rs index badaaef3..412d2cd4 100644 --- a/trailbase-core/src/auth/api/login.rs +++ b/trailbase-core/src/auth/api/login.rs @@ -285,7 +285,10 @@ pub async fn login_with_password( normalized_email: &str, password: &str, ) -> Result { - 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, diff --git a/trailbase-core/src/auth/api/logout.rs b/trailbase-core/src/auth/api/logout.rs index b91267e1..d7e5ffa1 100644 --- a/trailbase-core/src/auth/api/logout.rs +++ b/trailbase-core/src/auth/api/logout.rs @@ -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(|| { diff --git a/trailbase-core/src/auth/api/reset_password.rs b/trailbase-core/src/auth/api/reset_password.rs index 7eea614e..0b2962b9 100644 --- a/trailbase-core/src/auth/api/reset_password.rs +++ b/trailbase-core/src/auth/api/reset_password.rs @@ -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 { - 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); -} diff --git a/trailbase-core/src/auth/api/token.rs b/trailbase-core/src/auth/api/token.rs index 7428b285..7496cb9a 100644 --- a/trailbase-core/src/auth/api/token.rs +++ b/trailbase-core/src/auth/api/token.rs @@ -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) diff --git a/trailbase-core/src/auth/cli.rs b/trailbase-core/src/auth/cli.rs new file mode 100644 index 00000000..ee9bd106 --- /dev/null +++ b/trailbase-core/src/auth/cli.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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); +} diff --git a/trailbase-core/src/auth/error.rs b/trailbase-core/src/auth/error.rs index edccc4e7..9a4c363d 100644 --- a/trailbase-core/src/auth/error.rs +++ b/trailbase-core/src/auth/error.rs @@ -8,8 +8,6 @@ use thiserror::Error; pub enum AuthError { #[error("Unauthorized")] Unauthorized, - #[error("Unauthorized")] - UnauthorizedExt(Box), #[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), diff --git a/trailbase-core/src/auth/mod.rs b/trailbase-core/src/auth/mod.rs index 13c5ee06..ba5e7f39 100644 --- a/trailbase-core/src/auth/mod.rs +++ b/trailbase-core/src/auth/mod.rs @@ -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; diff --git a/trailbase-core/src/auth/oauth/callback.rs b/trailbase-core/src/auth/oauth/callback.rs index f0d0900f..a77efee4 100644 --- a/trailbase-core/src/auth/oauth/callback.rs +++ b/trailbase-core/src/auth/oauth/callback.rs @@ -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() diff --git a/trailbase-core/src/auth/tokens.rs b/trailbase-core/src/auth/tokens.rs index b77e0576..672ce9db 100644 --- a/trailbase-core/src/auth/tokens.rs +++ b/trailbase-core/src/auth/tokens.rs @@ -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 { - 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(),), diff --git a/trailbase-core/src/auth/user.rs b/trailbase-core/src/auth/user.rs index 7e250818..bcd801e9 100644 --- a/trailbase-core/src/auth/user.rs +++ b/trailbase-core/src/auth/user.rs @@ -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 { - 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, diff --git a/trailbase-core/src/auth/util.rs b/trailbase-core/src/auth/util.rs index 5d3154c9..e7e2bfc2 100644 --- a/trailbase-core/src/auth/util.rs +++ b/trailbase-core/src/auth/util.rs @@ -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::(&*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 { 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 { @@ -176,9 +176,9 @@ async fn get_user_by_id( let db_user = user_conn .read_query_value::(&*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 { @@ -193,14 +193,14 @@ pub async fn user_exists(state: &AppState, email: &str) -> Result 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 { 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())], diff --git a/trailbase-core/src/lib.rs b/trailbase-core/src/lib.rs index cd6a6e60..2a1872f8 100644 --- a/trailbase-core/src/lib.rs +++ b/trailbase-core/src/lib.rs @@ -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; diff --git a/trailbase-core/src/server/mod.rs b/trailbase-core/src/server/mod.rs index 10de355e..009a41dd 100644 --- a/trailbase-core/src/server/mod.rs +++ b/trailbase-core/src/server/mod.rs @@ -383,7 +383,7 @@ async fn assert_admin_api_access( ) -> Result { let user = req.extract_parts_with_state::(&state).await?; - if !is_admin(&state, &user).await { + if !is_admin(&state, &user.uuid).await { return Err(AuthError::Forbidden); }