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 src src
|
||||||
COPY Cargo.toml Cargo.toml
|
COPY Cargo.toml Cargo.toml
|
||||||
COPY Rocket.toml Rocket.toml
|
COPY Rocket.toml Rocket.toml
|
||||||
COPY static static
|
|
||||||
COPY templates templates
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
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 runtime assets which may or may not exist
|
||||||
COPY --from=build /build/Rocket.toml ./Rocket.toml
|
COPY --from=build /build/Rocket.toml ./Rocket.toml
|
||||||
COPY --from=build /build/static ./static
|
|
||||||
COPY --from=build /build/cdn ./cdn
|
COPY --from=build /build/cdn ./cdn
|
||||||
COPY --from=build /build/template[s] ./templates
|
|
||||||
|
|
||||||
## ensure the container listens globally on port 8000
|
## ensure the container listens globally on port 8000
|
||||||
ENV ROCKET_ADDRESS=0.0.0.0
|
ENV ROCKET_ADDRESS=0.0.0.0
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
container_name: chatapp_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:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
+51
-1
@@ -37,7 +37,7 @@ pub async fn login(
|
|||||||
|
|
||||||
#[post("/invite", data = "<form>")]
|
#[post("/invite", data = "<form>")]
|
||||||
pub async fn generate_invite(
|
pub async fn generate_invite(
|
||||||
session: Session,
|
session: AdminSession,
|
||||||
form: Json<AccessTokenForm>,
|
form: Json<AccessTokenForm>,
|
||||||
svc: &State<AccessTokenService>,
|
svc: &State<AccessTokenService>,
|
||||||
) -> ApiResult<String> {
|
) -> 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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub sub: i32,
|
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::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 crate::svc::chat_svc::ChatService;
|
||||||
|
use rocket::State;
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[get("/spaces")]
|
#[get("/spaces")]
|
||||||
pub async fn list_spaces(
|
pub async fn list_spaces(space_repo: &State<Arc<dyn SpaceRepo>>) -> ApiResult<Json<Vec<Space>>> {
|
||||||
space_repo: &State<Arc<dyn SpaceRepo>>
|
|
||||||
) -> ApiResult<Json<Vec<Space>>> {
|
|
||||||
let spaces = space_repo.get_all().await?;
|
let spaces = space_repo.get_all().await?;
|
||||||
Ok(Json(spaces))
|
Ok(Json(spaces))
|
||||||
}
|
}
|
||||||
@@ -19,7 +17,7 @@ pub async fn list_spaces(
|
|||||||
#[get("/spaces/<space_id>/channels")]
|
#[get("/spaces/<space_id>/channels")]
|
||||||
pub async fn list_channels(
|
pub async fn list_channels(
|
||||||
space_id: i64,
|
space_id: i64,
|
||||||
channel_repo: &State<Arc<dyn ChannelRepo>>
|
channel_repo: &State<Arc<dyn ChannelRepo>>,
|
||||||
) -> ApiResult<Json<Vec<Channel>>> {
|
) -> ApiResult<Json<Vec<Channel>>> {
|
||||||
let channels = channel_repo.get_by_space_id(space_id).await?;
|
let channels = channel_repo.get_by_space_id(space_id).await?;
|
||||||
Ok(Json(channels))
|
Ok(Json(channels))
|
||||||
@@ -28,9 +26,8 @@ pub async fn list_channels(
|
|||||||
#[get("/accessible_channels")]
|
#[get("/accessible_channels")]
|
||||||
pub async fn get_accessible_channels(
|
pub async fn get_accessible_channels(
|
||||||
session: Session,
|
session: Session,
|
||||||
svc: &State<ChatService>
|
svc: &State<ChatService>,
|
||||||
) -> ApiResult<Json<Vec<SpaceDto>>> {
|
) -> ApiResult<Json<Vec<SpaceDto>>> {
|
||||||
let space = svc.get_accessible_channels(session.uid).await?;
|
let space = svc.get_accessible_channels(session.uid).await?;
|
||||||
println!("{:?}", space);
|
|
||||||
Ok(Json(space))
|
Ok(Json(space))
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-6
@@ -31,6 +31,14 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub fn rocket() -> rocket::Rocket<rocket::Build> {
|
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");
|
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
println!("Running with database URL: {}", db_url);
|
println!("Running with database URL: {}", db_url);
|
||||||
@@ -81,7 +89,9 @@ pub fn rocket_builder(
|
|||||||
.map(From::from)
|
.map(From::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
.allow_credentials(true);
|
.allow_credentials(true)
|
||||||
|
.to_cors()
|
||||||
|
.expect("unable to create cors");
|
||||||
|
|
||||||
let access_token_svc = AccessTokenService::new(token_repo.clone());
|
let access_token_svc = AccessTokenService::new(token_repo.clone());
|
||||||
let auth_service = AuthService::new(user_repo.clone(), access_token_svc.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(chat_service)
|
||||||
.manage(auth_service)
|
.manage(auth_service)
|
||||||
.manage(settings_service)
|
.manage(settings_service)
|
||||||
|
.manage(access_token_svc)
|
||||||
.manage(user_service)
|
.manage(user_service)
|
||||||
.manage(space_repo)
|
.manage(space_repo)
|
||||||
.manage(channel_repo)
|
.manage(channel_repo)
|
||||||
.attach(cors.to_cors().unwrap())
|
.attach(cors)
|
||||||
.mount(
|
.mount(
|
||||||
"/api",
|
"/api",
|
||||||
routes![
|
routes![
|
||||||
@@ -104,6 +115,7 @@ pub fn rocket_builder(
|
|||||||
// basic auth
|
// basic auth
|
||||||
api::auth::login,
|
api::auth::login,
|
||||||
api::auth::signup,
|
api::auth::signup,
|
||||||
|
api::auth::generate_invite,
|
||||||
// 2fa
|
// 2fa
|
||||||
api::totp::confirm_totp,
|
api::totp::confirm_totp,
|
||||||
api::totp::disable_totp,
|
api::totp::disable_totp,
|
||||||
@@ -124,8 +136,8 @@ pub fn rocket_builder(
|
|||||||
api::space::get_accessible_channels
|
api::space::get_accessible_channels
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.register(
|
// .register(
|
||||||
"/",
|
// "/",
|
||||||
catchers![error::handle_401, error::handle_404, error::handle_default,],
|
// 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::error::ApiResult;
|
||||||
use crate::svc::user_svc::UserService;
|
use crate::svc::user_svc::UserService;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
use crate::api::totp::TotpStatus;
|
use crate::api::totp::TotpStatus;
|
||||||
@@ -20,6 +21,14 @@ pub struct User {
|
|||||||
pub updated_at: Option<DateTime<Utc>>,
|
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 {}
|
// pub struct UserCache {}
|
||||||
//
|
//
|
||||||
// impl UserCache {
|
// impl UserCache {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::repo::{UserRepo, AccessTokenRepoTrait};
|
use crate::repo::{UserRepo, AccessTokenRepoTrait};
|
||||||
use crate::model::user::User;
|
use crate::model::user::{User, UserRole};
|
||||||
use crate::model::auth::AccessToken;
|
use crate::model::auth::AccessToken;
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -59,6 +59,20 @@ impl UserRepo for MockUserRepo {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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> {
|
async fn new_user(&self, email: &str, username: &str, pass_hash: &str) -> Result<i64, sqlx::Error> {
|
||||||
let mut users = self.users.lock().unwrap();
|
let mut users = self.users.lock().unwrap();
|
||||||
let id = users.len() as i64 + 1;
|
let id = users.len() as i64 + 1;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::model::auth::AccessToken;
|
use crate::model::auth::AccessToken;
|
||||||
use crate::model::user::User;
|
use crate::model::user::{User, UserRole};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use crate::api::totp::TotpStatus;
|
use crate::api::totp::TotpStatus;
|
||||||
use crate::model::space::Space;
|
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 get_by_id(&self, id: i64) -> Option<User>;
|
||||||
async fn save(&self, user: &User) -> Result<(), sqlx::Error>;
|
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 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 get_by_username(&self, username: &str) -> Result<Option<User>, sqlx::Error>;
|
||||||
async fn delete_by_id(&self, id: i64) -> 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<String>) -> 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::repo::{Repo, UserRepo};
|
||||||
use crate::model::user::User;
|
use crate::model::user::{User, UserRole};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use crate::api::totp::TotpStatus;
|
use crate::api::totp::TotpStatus;
|
||||||
|
use crate::model::user::UserRole::Admin;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UserRepository {
|
pub struct UserRepository {
|
||||||
@@ -66,6 +67,15 @@ impl UserRepo for UserRepository {
|
|||||||
Ok(())
|
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> {
|
async fn new_user(&self, email: &str, username: &str, passhash: &str) -> Result<i64, sqlx::Error> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO users (email, username, passhash) VALUES ($1, $2, $3) RETURNING id",
|
"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 chrono::{DateTime, Utc};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::api::totp::TotpStatus::{Disabled, Enabled};
|
use crate::api::totp::TotpStatus::{Disabled, Enabled};
|
||||||
|
use crate::model::user::UserRole::Admin;
|
||||||
use crate::svc::access_token_svc::AccessTokenService;
|
use crate::svc::access_token_svc::AccessTokenService;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[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> {
|
pub async fn get_totp_status(&self, uid: i64) -> ApiResult<bool> {
|
||||||
Ok(
|
Ok(
|
||||||
self.users.get_totp_secret(uid).await?.is_some()
|
self.users.get_totp_secret(uid).await?.is_some()
|
||||||
|
|||||||
Reference in New Issue
Block a user