From 529d09aabcd8184b10dc0dee17b8a94f0eafb71b Mon Sep 17 00:00:00 2001 From: zxq5 Date: Wed, 8 Apr 2026 00:00:28 +0100 Subject: [PATCH] frontend v0.4.1 - fixed most of the bugs with the rewrite. should be ready to deploy now --- backend/Dockerfile | 4 --- backend/docker-compose.yml | 2 +- backend/src/api/auth.rs | 52 ++++++++++++++++++++++++++++++++++- backend/src/api/space.rs | 25 ++++++++--------- backend/src/lib.rs | 24 ++++++++++++---- backend/src/model/user.rs | 9 ++++++ backend/src/repo/mock.rs | 16 ++++++++++- backend/src/repo/mod.rs | 4 ++- backend/src/repo/user_repo.rs | 12 +++++++- backend/src/svc/auth_svc.rs | 5 ++++ 10 files changed, 124 insertions(+), 29 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 94ad6fc..1c17bf6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -12,8 +12,6 @@ COPY cdn cdn COPY src src COPY Cargo.toml Cargo.toml COPY Rocket.toml Rocket.toml -COPY static static -COPY templates templates RUN apt-get update && apt-get install -y libssl-dev pkg-config @@ -37,9 +35,7 @@ COPY --from=build /build/main ./ ## copy runtime assets which may or may not exist COPY --from=build /build/Rocket.toml ./Rocket.toml -COPY --from=build /build/static ./static COPY --from=build /build/cdn ./cdn -COPY --from=build /build/template[s] ./templates ## ensure the container listens globally on port 8000 ENV ROCKET_ADDRESS=0.0.0.0 diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 4bef3b6..603491e 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,7 +1,7 @@ services: backend: container_name: chatapp_backend - image: git.zxq5.dev/zxq5/chatapp-backend:v0.4.0 + image: git.zxq5.dev/zxq5/chatapp-backend:v0.4.1 ports: - "8000:8000" depends_on: diff --git a/backend/src/api/auth.rs b/backend/src/api/auth.rs index f5320d4..b4c5bc9 100644 --- a/backend/src/api/auth.rs +++ b/backend/src/api/auth.rs @@ -37,7 +37,7 @@ pub async fn login( #[post("/invite", data = "
")] pub async fn generate_invite( - session: Session, + session: AdminSession, form: Json, svc: &State, ) -> ApiResult { @@ -86,6 +86,56 @@ impl<'r> FromRequest<'r> for Session { } } +pub struct AdminSession { + pub uid: i64, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for AdminSession { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + // First verify the session is valid + match Claims::from_request(req).await { + Outcome::Success(user) if user.scope == TokenScope::Full => { + let uid = user.sub as i64; + + // Get AuthService from Rocket state + let auth_svc = match req.guard::<&State>().await { + Outcome::Success(svc) => svc, + Outcome::Error(err) => { + tracing::error!("AdminSession: Failed to get AuthService from state"); + return Outcome::Error(err); + } + _ => unreachable!("forward should never be called"), + }; + + // Check if user is admin + match auth_svc.is_admin(uid).await { + Ok(true) => Outcome::Success(AdminSession { uid }), + Ok(false) => { + tracing::debug!("non-admin user attempted to access admin session"); + Outcome::Error((Status::Forbidden, ())) + } + Err(err) => { + tracing::error!("AdminSession: is_admin check failed: {:?}", err); + Outcome::Error((Status::InternalServerError, ())) + } + } + } + Outcome::Success(_) => { + tracing::debug!("warning: user with scope other than Full attempted to access admin session"); + Outcome::Error((Status::Forbidden, ())) + } + Outcome::Error(err) => { + tracing::debug!("AdminSession request guard failed: {:?}", err); + Outcome::Error(err) + } + _ => unreachable!("forward should never be called"), + } + } +} + #[derive(Serialize, Deserialize)] pub struct Claims { pub sub: i32, diff --git a/backend/src/api/space.rs b/backend/src/api/space.rs index 85dca5c..90d499e 100644 --- a/backend/src/api/space.rs +++ b/backend/src/api/space.rs @@ -1,17 +1,15 @@ -use crate::error::ApiResult; -use crate::model::space::{Space, SpaceDto}; -use crate::model::space::Channel; -use crate::repo::{SpaceRepo, ChannelRepo}; -use rocket::serde::json::Json; -use rocket::State; -use std::sync::Arc; use crate::api::auth::Session; +use crate::error::ApiResult; +use crate::model::space::Channel; +use crate::model::space::{Space, SpaceDto}; +use crate::repo::{ChannelRepo, SpaceRepo}; use crate::svc::chat_svc::ChatService; +use rocket::State; +use rocket::serde::json::Json; +use std::sync::Arc; #[get("/spaces")] -pub async fn list_spaces( - space_repo: &State> -) -> ApiResult>> { +pub async fn list_spaces(space_repo: &State>) -> ApiResult>> { let spaces = space_repo.get_all().await?; Ok(Json(spaces)) } @@ -19,7 +17,7 @@ pub async fn list_spaces( #[get("/spaces//channels")] pub async fn list_channels( space_id: i64, - channel_repo: &State> + channel_repo: &State>, ) -> ApiResult>> { let channels = channel_repo.get_by_space_id(space_id).await?; Ok(Json(channels)) @@ -28,9 +26,8 @@ pub async fn list_channels( #[get("/accessible_channels")] pub async fn get_accessible_channels( session: Session, - svc: &State + svc: &State, ) -> ApiResult>> { let space = svc.get_accessible_channels(session.uid).await?; - println!("{:?}", space); Ok(Json(space)) -} \ No newline at end of file +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 5fe2849..3c9068d 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -31,6 +31,14 @@ use std::sync::Arc; use std::time::Duration; pub fn rocket() -> rocket::Rocket { + + if let Ok(var) = std::env::var("RELEASE_MODE") && var == "1" { + + } else { + dotenv::dotenv().expect("Failed to load .env file"); + } + + let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); println!("Running with database URL: {}", db_url); @@ -81,7 +89,9 @@ pub fn rocket_builder( .map(From::from) .collect(), ) - .allow_credentials(true); + .allow_credentials(true) + .to_cors() + .expect("unable to create cors"); let access_token_svc = AccessTokenService::new(token_repo.clone()); let auth_service = AuthService::new(user_repo.clone(), access_token_svc.clone()); @@ -92,10 +102,11 @@ pub fn rocket_builder( .manage(chat_service) .manage(auth_service) .manage(settings_service) + .manage(access_token_svc) .manage(user_service) .manage(space_repo) .manage(channel_repo) - .attach(cors.to_cors().unwrap()) + .attach(cors) .mount( "/api", routes![ @@ -104,6 +115,7 @@ pub fn rocket_builder( // basic auth api::auth::login, api::auth::signup, + api::auth::generate_invite, // 2fa api::totp::confirm_totp, api::totp::disable_totp, @@ -124,8 +136,8 @@ pub fn rocket_builder( api::space::get_accessible_channels ], ) - .register( - "/", - catchers![error::handle_401, error::handle_404, error::handle_default,], - ) + // .register( + // "/", + // catchers![error::handle_401, error::handle_404, error::handle_default,], + // ) } diff --git a/backend/src/model/user.rs b/backend/src/model/user.rs index 63c0220..e76b83a 100644 --- a/backend/src/model/user.rs +++ b/backend/src/model/user.rs @@ -2,6 +2,7 @@ use crate::api::auth::Session; use crate::error::ApiResult; use crate::svc::user_svc::UserService; use chrono::{DateTime, Utc}; +use rocket::serde::{Deserialize, Serialize}; use rocket::State; use sqlx::FromRow; use crate::api::totp::TotpStatus; @@ -20,6 +21,14 @@ pub struct User { pub updated_at: Option>, } +#[derive(Debug, sqlx::Type, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +#[sqlx(type_name = "user_role", rename_all = "lowercase")] +pub enum UserRole { + User, + Admin, +} + // pub struct UserCache {} // // impl UserCache { diff --git a/backend/src/repo/mock.rs b/backend/src/repo/mock.rs index 34daf6a..3fa0ded 100644 --- a/backend/src/repo/mock.rs +++ b/backend/src/repo/mock.rs @@ -1,5 +1,5 @@ use crate::repo::{UserRepo, AccessTokenRepoTrait}; -use crate::model::user::User; +use crate::model::user::{User, UserRole}; use crate::model::auth::AccessToken; use rocket::async_trait; use std::sync::Mutex; @@ -59,6 +59,20 @@ impl UserRepo for MockUserRepo { } Ok(()) } + + async fn is_admin(&self, uid: i64) -> Result { + let user = self.users.lock().unwrap().iter().find(|u| u.id == uid).cloned(); + if let Some(user) = user { + if user.id == 1 { + return Ok(UserRole::Admin); + } else { + return Ok(UserRole::User); + } + } + + panic!("user not found in test") + } + async fn new_user(&self, email: &str, username: &str, pass_hash: &str) -> Result { let mut users = self.users.lock().unwrap(); let id = users.len() as i64 + 1; diff --git a/backend/src/repo/mod.rs b/backend/src/repo/mod.rs index 2e48f94..b0a6850 100644 --- a/backend/src/repo/mod.rs +++ b/backend/src/repo/mod.rs @@ -1,5 +1,5 @@ use crate::model::auth::AccessToken; -use crate::model::user::User; +use crate::model::user::{User, UserRole}; use chrono::{DateTime, Utc}; use crate::api::totp::TotpStatus; use crate::model::space::Space; @@ -25,6 +25,8 @@ pub trait UserRepo: Send + Sync { async fn get_by_id(&self, id: i64) -> Option; async fn save(&self, user: &User) -> Result<(), sqlx::Error>; async fn new_user(&self, email: &str, username: &str, pass_hash: &str) -> Result; + + async fn is_admin(&self, uid: i64) -> Result; async fn get_by_username(&self, username: &str) -> Result, sqlx::Error>; async fn delete_by_id(&self, id: i64) -> Result<(), sqlx::Error>; async fn set_display_name(&self, id: i64, display_name: Option) -> Result<(), sqlx::Error>; diff --git a/backend/src/repo/user_repo.rs b/backend/src/repo/user_repo.rs index 3794b40..2794718 100644 --- a/backend/src/repo/user_repo.rs +++ b/backend/src/repo/user_repo.rs @@ -1,7 +1,8 @@ use crate::repo::{Repo, UserRepo}; -use crate::model::user::User; +use crate::model::user::{User, UserRole}; use sqlx::PgPool; use crate::api::totp::TotpStatus; +use crate::model::user::UserRole::Admin; #[derive(Clone)] pub struct UserRepository { @@ -66,6 +67,15 @@ impl UserRepo for UserRepository { Ok(()) } + async fn is_admin(&self, uid: i64) -> Result { + sqlx::query!( + "SELECT role AS \"user_role!: UserRole\" FROM users WHERE id = $1", uid + ) + .fetch_one(&self.pool) + .await + .map(|row| row.user_role) + } + async fn new_user(&self, email: &str, username: &str, passhash: &str) -> Result { sqlx::query!( "INSERT INTO users (email, username, passhash) VALUES ($1, $2, $3) RETURNING id", diff --git a/backend/src/svc/auth_svc.rs b/backend/src/svc/auth_svc.rs index 07a2636..ded7f67 100644 --- a/backend/src/svc/auth_svc.rs +++ b/backend/src/svc/auth_svc.rs @@ -10,6 +10,7 @@ use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use chrono::{DateTime, Utc}; use uuid::Uuid; use crate::api::totp::TotpStatus::{Disabled, Enabled}; +use crate::model::user::UserRole::Admin; use crate::svc::access_token_svc::AccessTokenService; #[derive(Clone)] @@ -103,6 +104,10 @@ impl AuthService { }) } + pub async fn is_admin(&self, uid: i64) -> ApiResult { + Ok(self.users.is_admin(uid).await? == Admin) + } + pub async fn get_totp_status(&self, uid: i64) -> ApiResult { Ok( self.users.get_totp_secret(uid).await?.is_some()