MarisUK's picture
Maris AI model sync
f440f03 verified
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<String>,
}
#[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<String>,
pub user_agent: Option<String>,
pub created_at: String,
pub expires_at: String,
pub revoked_at: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CredentialAuditRecord {
pub id: String,
pub camera_id: String,
pub action: String,
pub actor_principal_type: Option<String>,
pub actor_principal_id: Option<String>,
pub encryption_key_id: Option<String>,
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<AdminUserRecord>,
}
#[derive(Debug, Serialize)]
pub struct AdminSessionsResponse {
pub summary: String,
pub sessions: Vec<AdminSessionRecord>,
}
#[derive(Debug, Serialize)]
pub struct CredentialAuditResponse {
pub summary: String,
pub items: Vec<CredentialAuditRecord>,
}
pub async fn list_users(
State(state): State<AppState>,
claims: axum::extract::Extension<AuthClaims>,
) -> Result<Json<AdminUsersResponse>, (StatusCode, Json<serde_json::Value>)> {
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::<uuid::Uuid, _>("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::<chrono::DateTime<chrono::Utc>, _>("created_at").to_rfc3339(),
last_login_at: row
.get::<Option<chrono::DateTime<chrono::Utc>>, _>("last_login_at")
.map(|value| value.to_rfc3339()),
})
.collect::<Vec<_>>();
Ok(Json(AdminUsersResponse {
summary: format!("Atrasti {} lietotāji.", users.len()),
users,
}))
}
pub async fn create_user(
State(state): State<AppState>,
claims: axum::extract::Extension<AuthClaims>,
Json(req): Json<CreateUserRequest>,
) -> Result<Json<AdminUserRecord>, (StatusCode, Json<serde_json::Value>)> {
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::<uuid::Uuid, _>("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::<chrono::DateTime<chrono::Utc>, _>("created_at").to_rfc3339(),
last_login_at: row
.get::<Option<chrono::DateTime<chrono::Utc>>, _>("last_login_at")
.map(|value| value.to_rfc3339()),
}))
}
pub async fn list_sessions(
State(state): State<AppState>,
claims: axum::extract::Extension<AuthClaims>,
) -> Result<Json<AdminSessionsResponse>, (StatusCode, Json<serde_json::Value>)> {
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::<uuid::Uuid, _>("id").to_string(),
user_id: row.get::<uuid::Uuid, _>("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::<chrono::DateTime<chrono::Utc>, _>("created_at").to_rfc3339(),
expires_at: row.get::<chrono::DateTime<chrono::Utc>, _>("expires_at").to_rfc3339(),
revoked_at: row
.get::<Option<chrono::DateTime<chrono::Utc>>, _>("revoked_at")
.map(|value| value.to_rfc3339()),
})
.collect::<Vec<_>>();
Ok(Json(AdminSessionsResponse {
summary: format!("Atrastas {} sesijas.", sessions.len()),
sessions,
}))
}
pub async fn revoke_session(
State(state): State<AppState>,
claims: axum::extract::Extension<AuthClaims>,
Path(session_id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<serde_json::Value>)> {
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<AppState>,
claims: axum::extract::Extension<AuthClaims>,
) -> Result<Json<CredentialAuditResponse>, (StatusCode, Json<serde_json::Value>)> {
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::<uuid::Uuid, _>("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::<chrono::DateTime<chrono::Utc>, _>("created_at").to_rfc3339(),
})
.collect::<Vec<_>>();
Ok(Json(CredentialAuditResponse {
summary: format!("Atrasti {} credential audit ieraksti.", items.len()),
items,
}))
}
fn ensure_owner(claims: &AuthClaims) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
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<serde_json::Value>)> {
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<serde_json::Value>)> {
state
.postgres
.as_ref()
.ok_or_else(|| error(StatusCode::SERVICE_UNAVAILABLE, "PostgreSQL nav pieejams"))
}
fn internal_error(err: impl std::fmt::Display) -> (StatusCode, Json<serde_json::Value>) {
error(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
}
fn error(status: StatusCode, message: &str) -> (StatusCode, Json<serde_json::Value>) {
(status, Json(serde_json::json!({ "error": message })))
}