4 Commits

Author SHA1 Message Date
GW_MC
873b4a9d3a refactor: remove dead code annotations from UserService and SettingsStore traits
Some checks failed
Test / test-frontend (pull_request) Successful in 21s
Test / lint-frontend (pull_request) Successful in 25s
Test / frontend-build (pull_request) Successful in 29s
Test / test (pull_request) Successful in 46s
Verify / verify-generated-code (pull_request) Successful in 1m0s
Verify / verify-openapi-spec (pull_request) Successful in 1m0s
Verify / verify-frontend-api-client (pull_request) Successful in 20s
Test / lint (pull_request) Failing after 1m4s
2025-12-20 18:23:43 +08:00
GW_MC
596eb8faea feat: add mock implementations for configuration settings and update AppState to include config 2025-12-20 18:22:33 +08:00
GW_MC
0cd6e837fc fix: include InvalidSignature in JWT validation error handling 2025-12-20 18:21:54 +08:00
GW_MC
be63fcbc37 feat: fix incorrect JWT cookie key 2025-12-20 16:40:41 +08:00
14 changed files with 101 additions and 14 deletions

View File

@@ -147,6 +147,7 @@ fn get_app_state(
) -> AppState { ) -> AppState {
AppState { AppState {
database_connection: db_connection.clone(), database_connection: db_connection.clone(),
config: Arc::new(settings.clone()),
service: Arc::new(AppService { service: Arc::new(AppService {
server_state: Arc::new(ServerStateService::new(db_connection.clone())), server_state: Arc::new(ServerStateService::new(db_connection.clone())),
settings: Arc::new(SettingsService::new(db_connection.clone())), settings: Arc::new(SettingsService::new(db_connection.clone())),

View File

@@ -11,6 +11,8 @@ use tracing::{debug, error};
pub trait FromConfig: Sized { pub trait FromConfig: Sized {
fn from_config(config: &Config) -> Result<Self, String>; fn from_config(config: &Config) -> Result<Self, String>;
fn validate(&self) -> Result<(), String>; fn validate(&self) -> Result<(), String>;
#[cfg(test)]
fn mock() -> Self;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -40,6 +42,16 @@ impl FromConfig for ProgramSettings {
self.auth.validate()?; self.auth.validate()?;
Ok(()) Ok(())
} }
#[cfg(test)]
fn mock() -> Self {
ProgramSettings {
logging: logging::LoggingSettings::mock(),
database: database::DatabaseSettings::mock(),
server: server::ServerSettings::mock(),
auth: auth::AuthSettings::mock(),
}
}
} }
pub fn get_program_settings() -> ProgramSettings { pub fn get_program_settings() -> ProgramSettings {

View File

@@ -48,4 +48,13 @@ impl FromConfig for AuthSettings {
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
Ok(()) Ok(())
} }
#[cfg(test)]
fn mock() -> Self {
AuthSettings {
jwt_secret: Some("mock_jwt_secret".to_string()),
default_admin_username: Some("admin".to_string()),
default_admin_password: Some("password".to_string()),
}
}
} }

View File

@@ -50,4 +50,13 @@ impl FromConfig for DatabaseSettings {
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
Ok(()) Ok(())
} }
#[cfg(test)]
fn mock() -> Self {
DatabaseSettings {
url: "sqlite::memory:".to_string(),
max_connections: 5,
migrate_on_startup: true,
}
}
} }

View File

@@ -5,6 +5,7 @@ pub(crate) const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS";
pub(crate) const SERVER_PORT_KEY: &str = "SERVER.PORT"; pub(crate) const SERVER_PORT_KEY: &str = "SERVER.PORT";
pub(crate) const SERVER_SERVE_OPENAPI_KEY: &str = "SERVER.SERVE_OPENAPI"; pub(crate) const SERVER_SERVE_OPENAPI_KEY: &str = "SERVER.SERVE_OPENAPI";
pub(crate) const SERVER_CORS_ALLOWED_ORIGINS_KEY: &str = "SERVER.CORS.ALLOWED_ORIGINS"; pub(crate) const SERVER_CORS_ALLOWED_ORIGINS_KEY: &str = "SERVER.CORS.ALLOWED_ORIGINS";
pub(crate) const SERVER_COOKIES_SECURE_KEY: &str = "SERVER.COOKIES.SECURE";
// //
pub(crate) const DATABASE_URL_KEY: &str = "DATABASE.URL"; pub(crate) const DATABASE_URL_KEY: &str = "DATABASE.URL";
pub(crate) const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS"; pub(crate) const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS";

View File

@@ -49,4 +49,12 @@ impl FromConfig for LoggingSettings {
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
Ok(()) Ok(())
} }
#[cfg(test)]
fn mock() -> Self {
LoggingSettings {
level: Level::INFO,
utc: false,
}
}
} }

View File

@@ -3,7 +3,7 @@ use std::net::IpAddr;
use config::{Config, ConfigError}; use config::{Config, ConfigError};
use tracing::warn; use tracing::warn;
use crate::configs::key::{SERVER_CORS_ALLOWED_ORIGINS_KEY, SERVER_SERVE_OPENAPI_KEY}; use crate::configs::key::{SERVER_COOKIES_SECURE_KEY, SERVER_CORS_ALLOWED_ORIGINS_KEY, SERVER_SERVE_OPENAPI_KEY};
use super::{ use super::{
FromConfig, FromConfig,
@@ -16,6 +16,7 @@ pub struct ServerSettings {
pub port: u16, pub port: u16,
pub serve_openapi: bool, pub serve_openapi: bool,
pub cors: CORSSettings, pub cors: CORSSettings,
pub cookies: CookiesSettings,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -23,6 +24,11 @@ pub struct CORSSettings {
pub allowed_origins: Vec<String>, pub allowed_origins: Vec<String>,
} }
#[derive(Debug, Clone)]
pub struct CookiesSettings {
pub secure: bool,
}
impl FromConfig for ServerSettings { impl FromConfig for ServerSettings {
fn from_config(_config: &Config) -> Result<Self, String> { fn from_config(_config: &Config) -> Result<Self, String> {
Ok(ServerSettings { Ok(ServerSettings {
@@ -81,6 +87,24 @@ impl FromConfig for ServerSettings {
}) })
.collect(), .collect(),
}, },
cookies: CookiesSettings {
secure: _config
.get_bool(SERVER_COOKIES_SECURE_KEY)
.inspect(|is_secure| {
if !*is_secure {
warn!("Cookie 'secure' flag is disabled; this is not recommended in production environments.");
}
})
.unwrap_or_else(|err| {
const DEFAULT_COOKIES_SECURE: bool = true;
warn!(
"{} not set or invalid in configuration, defaulting to {}. Error: {}",
SERVER_COOKIES_SECURE_KEY, DEFAULT_COOKIES_SECURE, err
);
DEFAULT_COOKIES_SECURE
}),
},
}) })
} }
@@ -91,4 +115,19 @@ impl FromConfig for ServerSettings {
} }
Ok(()) Ok(())
} }
}
#[cfg(test)]
fn mock() -> Self {
ServerSettings {
address: "0.0.0.0".parse().unwrap(),
port: 8080,
serve_openapi: false,
cors: CORSSettings {
allowed_origins: vec![],
},
cookies: CookiesSettings {
secure: true,
},
}
}
}

View File

@@ -7,6 +7,7 @@ use axum::{
response::Response, response::Response,
}; };
use axum_extra::extract::cookie::CookieJar; use axum_extra::extract::cookie::CookieJar;
use tracing::debug;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@@ -25,6 +26,7 @@ pub async fn require_auth(
let token = if let Some(cookie) = cookies.get(JWT_COOKIE_NAME) { let token = if let Some(cookie) = cookies.get(JWT_COOKIE_NAME) {
cookie.value().to_string() cookie.value().to_string()
} else { } else {
debug!("No JWT cookie found. cookies: {:?}", cookies);
return handle_unauthenticated().await; return handle_unauthenticated().await;
}; };

View File

@@ -9,7 +9,7 @@ use axum::{Extension, Router};
use migration::sea_orm::DatabaseConnection; use migration::sea_orm::DatabaseConnection;
use crate::{ use crate::{
configs::server::CORSSettings, configs::{ProgramSettings, server::CORSSettings},
middlewares, middlewares,
services::{ services::{
auth::{ auth::{
@@ -23,10 +23,9 @@ use crate::{
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
#[allow(dead_code)]
pub database_connection: Arc<DatabaseConnection>, pub database_connection: Arc<DatabaseConnection>,
#[allow(dead_code)]
pub service: Arc<AppService>, pub service: Arc<AppService>,
pub config: Arc<ProgramSettings>,
} }
pub type ServiceState<T> = Arc<T>; pub type ServiceState<T> = Arc<T>;

View File

@@ -11,7 +11,10 @@ use serde::{Deserialize, Serialize};
use serde_json::{Value, from_value}; use serde_json::{Value, from_value};
use tracing::{error, warn}; use tracing::{error, warn};
use crate::routes::{AppState, api::openapi::tag::AUTH_TAG}; use crate::{
helpers::constants::JWT_COOKIE_NAME,
routes::{AppState, api::openapi::tag::AUTH_TAG},
};
/// Login request payload /// Login request payload
#[derive(Serialize, Deserialize, utoipa::ToSchema)] #[derive(Serialize, Deserialize, utoipa::ToSchema)]
@@ -81,9 +84,15 @@ pub async fn login(State(state): State<Arc<AppState>>, Json(payload): Json<Value
.header( .header(
SET_COOKIE, SET_COOKIE,
format!( format!(
"token={}; HttpOnly; Path=/; Max-Age={}; SameSite=Strict;", "{}={}; HttpOnly; Path=/; Max-Age={}; SameSite=Strict;{}",
JWT_COOKIE_NAME,
jwt, jwt,
claims.exp - claims.iat claims.exp - claims.iat,
if state.config.server.cookies.secure {
" Secure;"
} else {
""
}
), ),
) )
.body(Body::from(())); .body(Body::from(()));

View File

@@ -78,6 +78,7 @@ pub async fn get_health_info(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::configs::FromConfig;
use crate::{ use crate::{
routes::{AppState, api::health::state::HealthState}, routes::{AppState, api::health::state::HealthState},
services::{ services::{
@@ -112,6 +113,7 @@ mod test {
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
database_connection: db.clone(), database_connection: db.clone(),
config: Arc::new(crate::configs::ProgramSettings::mock()),
service: Arc::new(crate::routes::AppService { service: Arc::new(crate::routes::AppService {
settings: Arc::new(SettingsService::new(db.clone())), settings: Arc::new(SettingsService::new(db.clone())),
auth_state: crate::routes::AuthState { auth_state: crate::routes::AuthState {

View File

@@ -5,7 +5,7 @@ use std::{collections::HashSet, sync::Arc};
use argon2::password_hash::{SaltString, rand_core::OsRng}; use argon2::password_hash::{SaltString, rand_core::OsRng};
use jsonwebtoken::{ use jsonwebtoken::{
DecodingKey, EncodingKey, Header, Validation, decode, encode, DecodingKey, EncodingKey, Header, Validation, decode, encode,
errors::ErrorKind::{ExpiredSignature, InvalidSubject, InvalidToken}, errors::ErrorKind::{ExpiredSignature, InvalidSignature, InvalidSubject, InvalidToken},
}; };
use sea_orm::prelude::Uuid; use sea_orm::prelude::Uuid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -124,7 +124,7 @@ impl AuthenticationService for AuthenticationServiceImpl {
match decode::<Claims>(token, &decoding_key, &validation) { match decode::<Claims>(token, &decoding_key, &validation) {
Ok(data) => Ok(Some(data.claims)), Ok(data) => Ok(Some(data.claims)),
Err(err) => match *err.kind() { Err(err) => match *err.kind() {
InvalidToken | InvalidSubject | ExpiredSignature => Ok(None), InvalidToken | InvalidSubject | ExpiredSignature | InvalidSignature => Ok(None),
_ => Err(ServiceError::InternalError(format!( _ => Err(ServiceError::InternalError(format!(
"JWT validation error: {}", "JWT validation error: {}",
err err

View File

@@ -51,7 +51,6 @@ pub trait UserService: Send + Sync {
pub struct User { pub struct User {
pub id: Uuid, pub id: Uuid,
#[allow(dead_code)] // TODO: remove when used
pub username: String, pub username: String,
#[allow(dead_code)] // TODO: remove when used #[allow(dead_code)] // TODO: remove when used
pub is_admin: bool, pub is_admin: bool,

View File

@@ -11,14 +11,11 @@ use crate::errors::service_error::ServiceError;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait SettingsStore: Send + Sync { pub trait SettingsStore: Send + Sync {
#[allow(dead_code)] // TODO: remove when used
async fn get_setting(&self, key: &str) -> Result<String, ServiceError>; async fn get_setting(&self, key: &str) -> Result<String, ServiceError>;
#[allow(dead_code)] // TODO: remove when used
async fn set_setting(&self, key: &str, value: String) -> Result<(), ServiceError>; async fn set_setting(&self, key: &str, value: String) -> Result<(), ServiceError>;
} }
pub struct SettingsService { pub struct SettingsService {
#[allow(dead_code)] // TODO: remove when used
connection: Arc<DatabaseConnection>, connection: Arc<DatabaseConnection>,
} }