frontend v0.4.1

- fixed most of the bugs with the rewrite. should be ready to deploy now
This commit is contained in:
2026-04-08 00:00:28 +01:00
parent 5291e7dee6
commit 529d09aabc
10 changed files with 124 additions and 29 deletions
-4
View File
@@ -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
+1 -1
View File
@@ -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:
+51 -1
View File
@@ -37,7 +37,7 @@ pub async fn login(
#[post("/invite", data = "<form>")]
pub async fn generate_invite(
session: Session,
session: AdminSession,
form: Json<AccessTokenForm>,
svc: &State<AccessTokenService>,
) -> ApiResult<String> {
@@ -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<Self, Self::Error> {
// 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<AuthService>>().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,
+10 -13
View File
@@ -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<Arc<dyn SpaceRepo>>
) -> ApiResult<Json<Vec<Space>>> {
pub async fn list_spaces(space_repo: &State<Arc<dyn SpaceRepo>>) -> ApiResult<Json<Vec<Space>>> {
let spaces = space_repo.get_all().await?;
Ok(Json(spaces))
}
@@ -19,7 +17,7 @@ pub async fn list_spaces(
#[get("/spaces/<space_id>/channels")]
pub async fn list_channels(
space_id: i64,
channel_repo: &State<Arc<dyn ChannelRepo>>
channel_repo: &State<Arc<dyn ChannelRepo>>,
) -> ApiResult<Json<Vec<Channel>>> {
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<ChatService>
svc: &State<ChatService>,
) -> ApiResult<Json<Vec<SpaceDto>>> {
let space = svc.get_accessible_channels(session.uid).await?;
println!("{:?}", space);
Ok(Json(space))
}
+18 -6
View File
@@ -31,6 +31,14 @@ use std::sync::Arc;
use std::time::Duration;
pub fn rocket() -> rocket::Rocket<rocket::Build> {
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,],
// )
}
+9
View File
@@ -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<DateTime<Utc>>,
}
#[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 {
+15 -1
View File
@@ -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<UserRole, Error> {
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<i64, sqlx::Error> {
let mut users = self.users.lock().unwrap();
let id = users.len() as i64 + 1;
+3 -1
View File
@@ -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<User>;
async fn save(&self, user: &User) -> Result<(), sqlx::Error>;
async fn new_user(&self, email: &str, username: &str, pass_hash: &str) -> Result<i64, sqlx::Error>;
async fn is_admin(&self, uid: i64) -> Result<UserRole, sqlx::Error>;
async fn get_by_username(&self, username: &str) -> Result<Option<User>, sqlx::Error>;
async fn delete_by_id(&self, id: i64) -> Result<(), sqlx::Error>;
async fn set_display_name(&self, id: i64, display_name: Option<String>) -> Result<(), sqlx::Error>;
+11 -1
View File
@@ -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<UserRole, sqlx::Error> {
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<i64, sqlx::Error> {
sqlx::query!(
"INSERT INTO users (email, username, passhash) VALUES ($1, $2, $3) RETURNING id",
+5
View File
@@ -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<bool> {
Ok(self.users.is_admin(uid).await? == Admin)
}
pub async fn get_totp_status(&self, uid: i64) -> ApiResult<bool> {
Ok(
self.users.get_totp_secret(uid).await?.is_some()