feat: inline frontend submodule and add auth + access control improvements

This commit is contained in:
ParrotXray 2026-05-25 02:34:58 +00:00
parent 7875fa61b5
commit 8ffe5a8427
5 changed files with 95 additions and 15 deletions

@ -1 +1 @@
Subproject commit 718f593495bba431b097b2b9bce5da5ff7143043
Subproject commit fd1ca155e745e1e2121b4a0c2add3a06a71aec42

View File

@ -5,6 +5,7 @@ use argon2::Argon2;
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng};
use macros::log;
use rusqlite::{Connection, params};
use uuid::Uuid;
use crate::model::error::Error;
use crate::model::error::auth::AuthError;
@ -55,22 +56,24 @@ impl AppDB {
Ok(Self { conn: Mutex::new(conn) })
}
pub fn ensure_default_admin(&self, default_password: &str) -> Result<(), Error> {
pub fn has_any_account(&self) -> Result<bool, Error> {
let conn = self.conn.lock().unwrap();
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
Ok(count > 0)
}
if count == 0 {
let hash = hash_password(default_password)?;
let now = chrono::Utc::now().timestamp();
conn.execute(
"INSERT INTO accounts (id, username, password_hash, role, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
params!["admin", "admin", hash, "admin", now],
)
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
log!(AuthLog::DefaultAdminCreated);
}
pub fn create_account(&self, username: &str, password: &str, role: &str) -> Result<(), Error> {
let hash = hash_password(password)?;
let id = Uuid::new_v4().to_string();
let now = chrono::Utc::now().timestamp();
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT INTO accounts (id, username, password_hash, role, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
params![id, username, hash, role, now],
)
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
Ok(())
}

View File

@ -105,7 +105,6 @@ impl AppServices {
let app_db = if let Some(ref auth) = app_config.auth {
let db_path = PathBuf::from(env!("DB_PATH")).join("app.db");
let db = AppDB::open(db_path, &auth.db_key)?;
db.ensure_default_admin(&auth.default_admin_password)?;
Some(Arc::new(db))
} else {
None

View File

@ -6,8 +6,8 @@ loggable! {
#[error("Auth DB initialized at {path}")]
DbInitialized { path: String } => tracing::Level::INFO,
#[error("Default admin account created")]
DefaultAdminCreated => tracing::Level::INFO,
#[error("First admin account registered: {username}")]
FirstAdminRegistered { username: String } => tracing::Level::INFO,
#[error("Login successful for user: {username}")]
LoginSuccess { username: String } => tracing::Level::INFO,

View File

@ -18,11 +18,24 @@ use crate::web::middleware::auth::{AuthenticatedUser, Claims};
pub fn router() -> Router<AppState> {
Router::new()
.route("/status", get(status))
.route("/register", post(register))
.route("/login", post(login))
.route("/me", get(me))
.route("/logout", post(logout))
}
#[derive(Serialize)]
struct StatusResponse {
initialized: bool,
}
#[derive(Deserialize)]
struct RegisterRequest {
username: String,
password: String,
}
#[derive(Deserialize)]
struct LoginRequest {
username: String,
@ -41,6 +54,71 @@ struct MeResponse {
role: String,
}
async fn status(State(state): State<AppState>) -> impl IntoResponse {
if state.app_config.auth.is_none() {
return (StatusCode::NOT_IMPLEMENTED, "Auth not configured").into_response();
}
let db = match &state.app_db {
Some(db) => db,
None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not available").into_response(),
};
match db.has_any_account() {
Ok(initialized) => Json(StatusResponse { initialized }).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
}
}
async fn register(State(state): State<AppState>, Json(body): Json<RegisterRequest>) -> impl IntoResponse {
let auth_cfg = match state.app_config.auth.as_ref() {
Some(c) => c,
None => return (StatusCode::NOT_IMPLEMENTED, "Auth not configured").into_response(),
};
let db = match &state.app_db {
Some(db) => db,
None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not available").into_response(),
};
match db.has_any_account() {
Ok(true) => return (StatusCode::FORBIDDEN, "System already initialized").into_response(),
Ok(false) => {}
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
}
if body.username.trim().is_empty() || body.password.len() < 8 {
return (StatusCode::BAD_REQUEST, "Username required and password must be at least 8 characters").into_response();
}
if db.create_account(body.username.trim(), &body.password, "admin").is_err() {
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create account").into_response();
}
log!(AuthLog::FirstAdminRegistered {
username: body.username.trim().to_string()
});
let account = match db.find_account_by_username(body.username.trim()) {
Ok(Some(a)) => a,
_ => return (StatusCode::INTERNAL_SERVER_ERROR, "Account lookup failed").into_response(),
};
let exp = (Utc::now().timestamp() as usize) + (auth_cfg.token_ttl_secs as usize);
let claims = Claims {
sub: account.id,
username: account.username,
role: account.role,
exp,
};
match encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(auth_cfg.jwt_secret.as_bytes()),
) {
Ok(token) => Json(LoginResponse { token }).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Token generation failed").into_response(),
}
}
async fn login(State(state): State<AppState>, Json(body): Json<LoginRequest>) -> impl IntoResponse {
let auth_cfg = match state.app_config.auth.as_ref() {
Some(c) => c,