Compare commits
2 Commits
66b29b96ee
...
ec81d3228b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec81d3228b | ||
|
|
8111aaf672 |
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -1,3 +1,12 @@
|
||||
{
|
||||
"cSpell.words": ["YANPM"]
|
||||
"cSpell.words": ["chrono", "jsonwebtoken", "oneshot", "utoipa", "YANPM"],
|
||||
"sqltools.useNodeRuntime": true,
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"driver": "SQLite",
|
||||
"database": "${workspaceFolder:yet-another-nginx-proxy-manager}/apps/container/generated/sqlite/sqlite.db",
|
||||
"name": "YANPM"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::{
|
||||
authentication::{AuthenticationServiceImpl, strategies::password::PasswordStrategy},
|
||||
user::UserServiceImpl,
|
||||
},
|
||||
server_state::ServerStateService,
|
||||
settings::SettingsService,
|
||||
},
|
||||
tasks,
|
||||
@@ -145,6 +146,7 @@ fn get_app_state(
|
||||
AppState {
|
||||
database_connection: db_connection.clone(),
|
||||
service: Arc::new(AppService {
|
||||
server_state: Arc::new(ServerStateService::new(db_connection.clone())),
|
||||
settings: Arc::new(SettingsService::new(db_connection.clone())),
|
||||
auth_state: routes::AuthState {
|
||||
strategy: routes::AuthStrategy {
|
||||
|
||||
@@ -15,16 +15,15 @@ use crate::{
|
||||
authentication::{AuthenticationService, strategies::password::PasswordStrategy},
|
||||
user::UserService,
|
||||
},
|
||||
server_state::ServerStateStore,
|
||||
settings::SettingsStore,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
// TODO: remove dead_code allowances when fields are used
|
||||
#[allow(dead_code)]
|
||||
pub database_connection: Arc<DatabaseConnection>,
|
||||
// TODO: remove dead_code allowances when fields are used
|
||||
#[allow(dead_code)]
|
||||
pub service: Arc<AppService>,
|
||||
}
|
||||
@@ -44,6 +43,7 @@ pub struct AppService {
|
||||
pub settings: ServiceState<dyn SettingsStore>,
|
||||
pub auth_state: AuthState,
|
||||
pub user: ServiceState<dyn UserService>,
|
||||
pub server_state: ServiceState<dyn ServerStateStore>,
|
||||
}
|
||||
|
||||
pub fn get_root_router(state: impl Into<Arc<AppState>>) -> Router {
|
||||
|
||||
@@ -13,7 +13,7 @@ use axum::{Router, response::IntoResponse, routing::any};
|
||||
|
||||
pub fn get_api_router(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.nest("/health", health::get_health_router())
|
||||
.nest("/health", health::get_health_router(state.clone()))
|
||||
.merge(auth::get_basic_auth_router(state.clone()))
|
||||
.merge(restricted::get_restricted_router(state.clone()))
|
||||
// explicit fallback for unmatched API routes
|
||||
|
||||
@@ -3,10 +3,7 @@ pub mod login;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use axum::{Router, routing::post};
|
||||
|
||||
use crate::routes::AppState;
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ pub async fn login(State(state): State<Arc<AppState>>, Json(payload): Json<Value
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
error!("Error building response: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR).into_response();
|
||||
(StatusCode::INTERNAL_SERVER_ERROR).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ use std::sync::Arc;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
pub fn get_health_router() -> Router {
|
||||
use crate::routes::{AppState, api::health::state::AppStateWithHealth};
|
||||
|
||||
pub fn get_health_router(app_state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/info", get(info::get_health_info))
|
||||
.with_state(Arc::new(state::HealthState::default()))
|
||||
.with_state(Arc::new(AppStateWithHealth {
|
||||
app_state: app_state.clone(),
|
||||
health_state: Arc::new(state::HealthState::default()),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::sync::Arc;
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
|
||||
use crate::routes::api::{health::state::HealthState, openapi::tag::HEALTH_TAG};
|
||||
use crate::routes::api::{health::state::AppStateWithHealth, openapi::tag::HEALTH_TAG};
|
||||
|
||||
const STATUS_HEALTHY: &str = "healthy";
|
||||
const STATUS_UNHEALTHY: &str = "unhealthy";
|
||||
@@ -20,6 +21,8 @@ pub struct HealthInfo {
|
||||
pub up_since: DateTime<Utc>,
|
||||
/// List of error messages if unhealthy
|
||||
pub errors: Option<Vec<String>>,
|
||||
/// Is initialized
|
||||
pub is_initialized: bool,
|
||||
}
|
||||
|
||||
/// Health check endpoint
|
||||
@@ -35,12 +38,23 @@ pub struct HealthInfo {
|
||||
tag = HEALTH_TAG,
|
||||
)]
|
||||
pub async fn get_health_info(
|
||||
State(state): State<Arc<HealthState>>,
|
||||
State(app_state_with_health): State<Arc<AppStateWithHealth>>,
|
||||
) -> (StatusCode, Json<HealthInfo>) {
|
||||
#[allow(unused_mut)]
|
||||
let mut errors = vec![];
|
||||
|
||||
let is_healthy = errors.is_empty();
|
||||
let health_state = &app_state_with_health.health_state;
|
||||
let app_state = &app_state_with_health.app_state;
|
||||
|
||||
let is_initialized = match app_state.service.server_state.is_server_initialized().await {
|
||||
Ok(initialized) => initialized,
|
||||
Err(err) => {
|
||||
errors.push("Failed to determine if server is initialized".to_string());
|
||||
error!("Error checking server initialization status: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
if is_healthy {
|
||||
@@ -55,14 +69,29 @@ pub async fn get_health_info(
|
||||
STATUS_UNHEALTHY.into()
|
||||
},
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
up_since: *state.get_start_at(),
|
||||
up_since: *health_state.get_start_at(),
|
||||
errors: if is_healthy { None } else { Some(errors) },
|
||||
is_initialized,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
routes::{AppState, api::health::state::HealthState},
|
||||
services::{
|
||||
auth::{
|
||||
authentication::{
|
||||
AuthenticationServiceImpl, strategies::password::PasswordStrategy,
|
||||
},
|
||||
user::UserServiceImpl,
|
||||
},
|
||||
server_state::ServerStateService,
|
||||
settings::SettingsService,
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use axum::body::to_bytes;
|
||||
use axum::{
|
||||
@@ -70,14 +99,38 @@ mod test {
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use sea_orm::MockDatabase;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_health_info() {
|
||||
let health_state = Arc::new(HealthState::default());
|
||||
let db = MockDatabase::new(sea_orm::DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
let db = Arc::new(db);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
database_connection: db.clone(),
|
||||
service: Arc::new(crate::routes::AppService {
|
||||
settings: Arc::new(SettingsService::new(db.clone())),
|
||||
auth_state: crate::routes::AuthState {
|
||||
strategy: crate::routes::AuthStrategy {
|
||||
password: Arc::new(PasswordStrategy::new(db.clone())),
|
||||
},
|
||||
authentication: Arc::new(AuthenticationServiceImpl::new(None)),
|
||||
},
|
||||
user: Arc::new(UserServiceImpl::new(db.clone())),
|
||||
server_state: Arc::new(ServerStateService::new(db.clone())),
|
||||
}),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/info", axum::routing::get(get_health_info))
|
||||
.with_state(health_state);
|
||||
.with_state(Arc::new(AppStateWithHealth {
|
||||
app_state: app_state.clone(),
|
||||
health_state: health_state.clone(),
|
||||
}));
|
||||
|
||||
let response = app
|
||||
.oneshot(Request::builder().uri("/info").body(Body::empty()).unwrap())
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::routes::AppState;
|
||||
|
||||
pub struct AppStateWithHealth {
|
||||
pub app_state: Arc<AppState>,
|
||||
pub health_state: Arc<HealthState>,
|
||||
}
|
||||
|
||||
pub struct HealthState {
|
||||
start_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
use axum::Router;
|
||||
|
||||
use crate::{middlewares::require_auth::require_auth, routes::AppState};
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod server_state;
|
||||
pub mod settings;
|
||||
|
||||
@@ -14,6 +14,7 @@ use tokio::sync::RwLock;
|
||||
use crate::errors::service_error::ServiceError;
|
||||
|
||||
// Number of requests between invalidation cache cleanups
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
const INVALIDATE_CACHE_CLEANUP_INTERVAL_REQUESTS: usize = 100; // Cleanup every 100 for invalidation checks
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -38,10 +39,15 @@ pub trait AuthenticationService: Send + Sync {
|
||||
token: &str,
|
||||
target_sub: Option<String>,
|
||||
) -> Result<Option<Claims>, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn parse_jwt(&self, token: &str) -> Result<Claims, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn invalidate_jwt(&self, token: &str) -> Result<(), ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn refresh_jwt(&self, token: &str, duration_secs: u64) -> Result<String, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn logout(&self, token: &str) -> Result<(), ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn cleanup_invalidation_cache(&self);
|
||||
}
|
||||
|
||||
@@ -54,7 +60,9 @@ struct InvalidationEntry {
|
||||
|
||||
pub struct AuthenticationServiceImpl {
|
||||
secret: String,
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
invalidation_cache: Arc<RwLock<HashSet<InvalidationEntry>>>,
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
cache_cleanup_counter: Arc<RwLock<usize>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ impl PasswordStrategy {
|
||||
|
||||
Ok(user.id)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub async fn revoke_identity(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
@@ -126,7 +126,7 @@ impl PasswordStrategy {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub async fn update_password(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
@@ -368,7 +368,7 @@ mod test {
|
||||
user_id: Uuid::new_v4(),
|
||||
email: None,
|
||||
provider: PASSWORD_PROVIDER.to_string(),
|
||||
password_hash: Some("somehash".to_string()),
|
||||
password_hash: Some("some_hash".to_string()),
|
||||
metadata: None,
|
||||
is_revoked: false,
|
||||
revoked_at: None,
|
||||
@@ -413,7 +413,7 @@ mod test {
|
||||
user_id,
|
||||
email: None,
|
||||
provider: PASSWORD_PROVIDER.to_string(),
|
||||
password_hash: Some("oldhash".to_string()),
|
||||
password_hash: Some("old_hash".to_string()),
|
||||
metadata: None,
|
||||
is_revoked: false,
|
||||
revoked_at: None,
|
||||
@@ -430,7 +430,7 @@ mod test {
|
||||
user_id,
|
||||
email: None,
|
||||
provider: PASSWORD_PROVIDER.to_string(),
|
||||
password_hash: Some("newhash".to_string()),
|
||||
password_hash: Some("new_hash".to_string()),
|
||||
metadata: None,
|
||||
is_revoked: false,
|
||||
revoked_at: None,
|
||||
|
||||
@@ -17,11 +17,13 @@ pub trait UserService: Send + Sync {
|
||||
user_id: Uuid,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<User, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn is_admin(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<bool, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn user_exists(
|
||||
&self,
|
||||
username: &str,
|
||||
@@ -32,12 +34,14 @@ pub trait UserService: Send + Sync {
|
||||
user: NewUser,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<User, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn update_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
user: UpdateUser,
|
||||
tx: Option<&mut DatabaseTransaction>,
|
||||
) -> Result<User, ServiceError>;
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
async fn delete_user(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
@@ -47,7 +51,9 @@ pub trait UserService: Send + Sync {
|
||||
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub username: String,
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
@@ -67,12 +73,16 @@ pub struct NewUser {
|
||||
}
|
||||
|
||||
pub struct UpdateUser {
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub username: Option<String>,
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub is_admin: Option<bool>,
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
impl UpdateUser {
|
||||
#[allow(dead_code)] // TODO: remove when used
|
||||
fn apply_to_active_model(&self, model: &mut UserActiveModel) {
|
||||
if let Some(username) = &self.username {
|
||||
model.name = ActiveValue::Set(username.clone());
|
||||
|
||||
36
apps/api/src/services/server_state.rs
Normal file
36
apps/api/src/services/server_state.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sea_orm::{DatabaseConnection, prelude::*};
|
||||
|
||||
use crate::errors::service_error::ServiceError;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ServerStateStore: Send + Sync {
|
||||
async fn is_server_initialized(&self) -> Result<bool, ServiceError>;
|
||||
}
|
||||
|
||||
pub struct ServerStateService {
|
||||
connection: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl ServerStateService {
|
||||
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||
Self { connection }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ServerStateStore for ServerStateService {
|
||||
async fn is_server_initialized(&self) -> Result<bool, ServiceError> {
|
||||
// For example, check if any admin user exists to determine if the server is initialized
|
||||
let admin_exists = database::generated::entities::user::Entity::find()
|
||||
.filter(database::generated::entities::user::Column::IsAdmin.eq(true))
|
||||
.filter(database::generated::entities::user::Column::IsActive.eq(true))
|
||||
.one(&*self.connection)
|
||||
.await
|
||||
.map_err(ServiceError::from)?
|
||||
.is_some();
|
||||
|
||||
Ok(admin_exists)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,8 @@
|
||||
"required": [
|
||||
"status",
|
||||
"version",
|
||||
"up_since"
|
||||
"up_since",
|
||||
"is_initialized"
|
||||
],
|
||||
"properties": {
|
||||
"errors": {
|
||||
@@ -148,6 +149,10 @@
|
||||
},
|
||||
"description": "List of error messages if unhealthy"
|
||||
},
|
||||
"is_initialized": {
|
||||
"type": "boolean",
|
||||
"description": "Is initialized"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Health status: \"healthy\" or \"unhealthy\""
|
||||
|
||||
@@ -3,6 +3,7 @@ export namespace Schemas {
|
||||
export type AdminInitRequest = { password: string; setup_secret: string; username: string };
|
||||
export type HealthInfo = {
|
||||
errors?: (Array<string> | null) | undefined;
|
||||
is_initialized: boolean;
|
||||
status: string;
|
||||
up_since: string;
|
||||
version: string;
|
||||
|
||||
Reference in New Issue
Block a user