Compare commits
5 Commits
3f252a8abd
...
feature/fr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7b70e039 | ||
|
|
873b4a9d3a | ||
|
|
596eb8faea | ||
|
|
0cd6e837fc | ||
|
|
be63fcbc37 |
@@ -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())),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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(()));
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user