8 Commits

Author SHA1 Message Date
GW_MC
dc7b70e039 Fix trailing whitespace
All checks were successful
Test / test-frontend (pull_request) Successful in 23s
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 59s
Verify / verify-openapi-spec (pull_request) Successful in 1m1s
Verify / verify-frontend-api-client (pull_request) Successful in 20s
Test / lint (pull_request) Successful in 1m3s
2025-12-20 18:48:35 +08:00
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
GW_MC
3f252a8abd feat: add required asterisk indicator to TextField component
All checks were successful
Test / test-frontend (pull_request) Successful in 22s
Test / lint-frontend (pull_request) Successful in 25s
Test / frontend-build (pull_request) Successful in 29s
Verify / verify-generated-code (pull_request) Successful in 56s
Test / test (pull_request) Successful in 46s
Verify / verify-openapi-spec (pull_request) Successful in 57s
Verify / verify-frontend-api-client (pull_request) Successful in 22s
Test / lint (pull_request) Successful in 1m6s
2025-12-20 16:20:31 +08:00
GW_MC
0740072a60 Fix query message display code instead of message 2025-12-20 16:17:59 +08:00
GW_MC
ff752985c6 fix: update ESLint ignores to include 'build' and '.react-router'
All checks were successful
Test / test-frontend (pull_request) Successful in 30s
Test / lint-frontend (pull_request) Successful in 33s
Test / frontend-build (pull_request) Successful in 34s
Verify / verify-generated-code (pull_request) Successful in 8m33s
Verify / verify-openapi-spec (pull_request) Successful in 8m38s
Verify / verify-frontend-api-client (pull_request) Successful in 22s
Test / test (pull_request) Successful in 8m58s
Test / lint (pull_request) Successful in 1m8s
2025-12-20 14:34:01 +08:00
17 changed files with 108 additions and 15 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,9 @@ 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 +18,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 +26,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 +89,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 +117,17 @@ 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>,
} }

View File

@@ -2,6 +2,7 @@ import type { AnyFieldMeta } from '@tanstack/react-form';
import { LucideEye, LucideEyeClosed } from 'lucide-react'; import { LucideEye, LucideEyeClosed } from 'lucide-react';
import { useCallback, useId, useState } from 'react'; import { useCallback, useId, useState } from 'react';
import { InfoIcon, type InfoIconProps } from '../info'; import { InfoIcon, type InfoIconProps } from '../info';
import { Text } from '@radix-ui/themes';
export type TextFieldProps = { export type TextFieldProps = {
label?: string; label?: string;
@@ -32,6 +33,11 @@ export function TextField({ label, value, onChange, labelProps, labelDivProps, s
{label && ( {label && (
<div style={{ fontSize: 12, color: 'var(--gray-9)', marginBottom: 6, display: 'flex', alignItems: 'center' }} {...labelDivProps}> <div style={{ fontSize: 12, color: 'var(--gray-9)', marginBottom: 6, display: 'flex', alignItems: 'center' }} {...labelDivProps}>
{label} {label}
{rest?.required && (
<Text size="3" style={{ color: 'var(--red-9)', marginLeft: 2 }}>
*
</Text>
)}
{infoIconProps && <InfoIcon {...infoIconProps} style={{ marginLeft: 4, verticalAlign: 'middle' }} />} {infoIconProps && <InfoIcon {...infoIconProps} style={{ marginLeft: 4, verticalAlign: 'middle' }} />}
</div> </div>
)} )}

View File

@@ -44,7 +44,7 @@ export function useQueryMessage(
messageStr.current = queryMessageString; messageStr.current = queryMessageString;
if (displayMessages) { if (displayMessages) {
toast[queryMessage.type](queryMessage.code, { toast[queryMessage.type](queryMessage.message, {
position: 'top-center', position: 'top-center',
autoClose: 5000, autoClose: 5000,
hideProgressBar: false, hideProgressBar: false,

View File

@@ -7,7 +7,7 @@ import pluginReactHooks from 'eslint-plugin-react-hooks';
export default tseslint.config( export default tseslint.config(
{ {
// Ignore files and directories // Ignore files and directories
ignores: ['dist', 'node_modules', 'app/generated'], ignores: ['node_modules', 'app/generated', 'build', '.react-router'],
}, },
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,