mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 18:50:28 +09:00
feat: inline frontend submodule and add auth + access control improvements
This commit is contained in:
parent
7875fa61b5
commit
8ffe5a8427
@ -1 +1 @@
|
||||
Subproject commit 718f593495bba431b097b2b9bce5da5ff7143043
|
||||
Subproject commit fd1ca155e745e1e2121b4a0c2add3a06a71aec42
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user