frontend v0.4.1
- fixed most of the bugs with the rewrite. should be ready to deploy now
This commit is contained in:
@@ -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,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
@@ -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,
|
||||
|
||||
+11
-14
@@ -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
@@ -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,],
|
||||
// )
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user