use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use serde::{Deserialize, Serialize}; use sqlx::Row; use crate::{ app_state::AppState, jwt_auth::{self, AuthClaims}, }; #[derive(Debug, Serialize)] pub struct AdminUserRecord { pub user_id: String, pub email: String, pub username: String, pub display_name: String, pub role: String, pub status: String, pub created_at: String, pub last_login_at: Option, } #[derive(Debug, Serialize)] pub struct AdminSessionRecord { pub session_id: String, pub user_id: String, pub username: String, pub display_name: String, pub role: String, pub ip_address: Option, pub user_agent: Option, pub created_at: String, pub expires_at: String, pub revoked_at: Option, } #[derive(Debug, Serialize)] pub struct CredentialAuditRecord { pub id: String, pub camera_id: String, pub action: String, pub actor_principal_type: Option, pub actor_principal_id: Option, pub encryption_key_id: Option, pub metadata: serde_json::Value, pub created_at: String, } #[derive(Debug, Deserialize)] pub struct CreateUserRequest { pub email: String, pub username: String, pub display_name: String, pub password: String, pub role: String, } #[derive(Debug, Serialize)] pub struct AdminUsersResponse { pub summary: String, pub users: Vec, } #[derive(Debug, Serialize)] pub struct AdminSessionsResponse { pub summary: String, pub sessions: Vec, } #[derive(Debug, Serialize)] pub struct CredentialAuditResponse { pub summary: String, pub items: Vec, } pub async fn list_users( State(state): State, claims: axum::extract::Extension, ) -> Result, (StatusCode, Json)> { ensure_owner(&claims.0)?; let pool = pool(&state)?; let rows = sqlx::query( r#" SELECT id, email, username, display_name, role::text AS role, status::text AS status, created_at, last_login_at FROM users WHERE deleted_at IS NULL ORDER BY created_at DESC "#, ) .fetch_all(pool) .await .map_err(internal_error)?; let users = rows .into_iter() .map(|row| AdminUserRecord { user_id: row.get::("id").to_string(), email: row.get("email"), username: row.get("username"), display_name: row.get("display_name"), role: row.get("role"), status: row.get("status"), created_at: row.get::, _>("created_at").to_rfc3339(), last_login_at: row .get::>, _>("last_login_at") .map(|value| value.to_rfc3339()), }) .collect::>(); Ok(Json(AdminUsersResponse { summary: format!("Atrasti {} lietotāji.", users.len()), users, })) } pub async fn create_user( State(state): State, claims: axum::extract::Extension, Json(req): Json, ) -> Result, (StatusCode, Json)> { ensure_owner(&claims.0)?; validate_created_role(&req.role)?; let pool = pool(&state)?; let password_hash = jwt_auth::hash_password(&req.password) .map_err(|err| error(StatusCode::BAD_REQUEST, &err.to_string()))?; let row = sqlx::query( r#" INSERT INTO users (email, username, display_name, password_hash, role) VALUES ($1, $2, $3, $4, $5::user_role) RETURNING id, email, username, display_name, role::text AS role, status::text AS status, created_at, last_login_at "#, ) .bind(req.email.trim()) .bind(req.username.trim()) .bind(req.display_name.trim()) .bind(password_hash) .bind(req.role.trim()) .fetch_one(pool) .await .map_err(internal_error)?; Ok(Json(AdminUserRecord { user_id: row.get::("id").to_string(), email: row.get("email"), username: row.get("username"), display_name: row.get("display_name"), role: row.get("role"), status: row.get("status"), created_at: row.get::, _>("created_at").to_rfc3339(), last_login_at: row .get::>, _>("last_login_at") .map(|value| value.to_rfc3339()), })) } pub async fn list_sessions( State(state): State, claims: axum::extract::Extension, ) -> Result, (StatusCode, Json)> { ensure_owner(&claims.0)?; let pool = pool(&state)?; let rows = sqlx::query( r#" SELECT auth_sessions.id, auth_sessions.user_id, auth_sessions.ip_address::TEXT AS ip_address, auth_sessions.user_agent, auth_sessions.created_at, auth_sessions.expires_at, auth_sessions.revoked_at, users.username, users.display_name, users.role::text AS role FROM auth_sessions JOIN users ON users.id = auth_sessions.user_id WHERE users.deleted_at IS NULL ORDER BY auth_sessions.created_at DESC LIMIT 200 "#, ) .fetch_all(pool) .await .map_err(internal_error)?; let sessions = rows .into_iter() .map(|row| AdminSessionRecord { session_id: row.get::("id").to_string(), user_id: row.get::("user_id").to_string(), username: row.get("username"), display_name: row.get("display_name"), role: row.get("role"), ip_address: row.get("ip_address"), user_agent: row.get("user_agent"), created_at: row.get::, _>("created_at").to_rfc3339(), expires_at: row.get::, _>("expires_at").to_rfc3339(), revoked_at: row .get::>, _>("revoked_at") .map(|value| value.to_rfc3339()), }) .collect::>(); Ok(Json(AdminSessionsResponse { summary: format!("Atrastas {} sesijas.", sessions.len()), sessions, })) } pub async fn revoke_session( State(state): State, claims: axum::extract::Extension, Path(session_id): Path, ) -> Result)> { ensure_owner(&claims.0)?; let pool = pool(&state)?; jwt_auth::revoke_session(pool, &session_id) .await .map_err(internal_error)?; Ok(StatusCode::NO_CONTENT) } pub async fn list_credential_audit( State(state): State, claims: axum::extract::Extension, ) -> Result, (StatusCode, Json)> { ensure_owner(&claims.0)?; let pool = pool(&state)?; let rows = sqlx::query( r#" SELECT id, camera_id, action, actor_principal_type, actor_principal_id, encryption_key_id, metadata_json, created_at FROM vision_camera_credential_audit ORDER BY created_at DESC LIMIT 100 "#, ) .fetch_all(pool) .await .map_err(internal_error)?; let items = rows .into_iter() .map(|row| CredentialAuditRecord { id: row.get::("id").to_string(), camera_id: row.get("camera_id"), action: row.get("action"), actor_principal_type: row.get("actor_principal_type"), actor_principal_id: row.get("actor_principal_id"), encryption_key_id: row.get("encryption_key_id"), metadata: row.get("metadata_json"), created_at: row.get::, _>("created_at").to_rfc3339(), }) .collect::>(); Ok(Json(CredentialAuditResponse { summary: format!("Atrasti {} credential audit ieraksti.", items.len()), items, })) } fn ensure_owner(claims: &AuthClaims) -> Result<(), (StatusCode, Json)> { if claims.role == "owner" && claims.owner { return Ok(()); } Err(error( StatusCode::FORBIDDEN, "Admin vadība pieejama tikai konfigurētajam Maris īpašniekam", )) } fn validate_created_role(role: &str) -> Result<(), (StatusCode, Json)> { match role.trim() { "member" | "service" => Ok(()), _ => Err(error( StatusCode::BAD_REQUEST, "Var izveidot tikai member vai service lietotājus. Vienīgais admin ir konfigurētais īpašnieks.", )), } } #[cfg(test)] mod tests { use super::validate_created_role; #[test] fn rejects_privileged_roles_for_new_users() { assert!(validate_created_role("member").is_ok()); assert!(validate_created_role("service").is_ok()); assert!(validate_created_role("admin").is_err()); assert!(validate_created_role("owner").is_err()); } } fn pool(state: &AppState) -> Result<&sqlx::PgPool, (StatusCode, Json)> { state .postgres .as_ref() .ok_or_else(|| error(StatusCode::SERVICE_UNAVAILABLE, "PostgreSQL nav pieejams")) } fn internal_error(err: impl std::fmt::Display) -> (StatusCode, Json) { error(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()) } fn error(status: StatusCode, message: &str) -> (StatusCode, Json) { (status, Json(serde_json::json!({ "error": message }))) }