treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
parent
764aafa88d
commit
3ccddce7fd
178 changed files with 58342 additions and 54241 deletions
|
|
@ -1,171 +1,176 @@
|
|||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
|
||||
use crate::dto::*;
|
||||
use crate::error::ApiError;
|
||||
use crate::state::AppState;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use pinakes_core::users::{CreateUserRequest, UpdateUserRequest, UserId};
|
||||
|
||||
use crate::{dto::*, error::ApiError, state::AppState};
|
||||
|
||||
/// List all users (admin only)
|
||||
pub async fn list_users(
|
||||
State(state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<UserResponse>>, ApiError> {
|
||||
let users = state.storage.list_users().await?;
|
||||
Ok(Json(users.into_iter().map(UserResponse::from).collect()))
|
||||
let users = state.storage.list_users().await?;
|
||||
Ok(Json(users.into_iter().map(UserResponse::from).collect()))
|
||||
}
|
||||
|
||||
/// Create a new user (admin only)
|
||||
pub async fn create_user(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<CreateUserRequest>,
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<CreateUserRequest>,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
// Validate username
|
||||
if req.username.is_empty() || req.username.len() > 255 {
|
||||
return Err(ApiError(
|
||||
pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"username must be 1-255 characters".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
// Validate username
|
||||
if req.username.is_empty() || req.username.len() > 255 {
|
||||
return Err(ApiError(
|
||||
pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"username must be 1-255 characters".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if req.password.len() < 8 {
|
||||
return Err(ApiError(
|
||||
pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"password must be at least 8 characters".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
// Validate password
|
||||
if req.password.len() < 8 {
|
||||
return Err(ApiError(
|
||||
pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"password must be at least 8 characters".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Hash password
|
||||
let password_hash = pinakes_core::users::auth::hash_password(&req.password)?;
|
||||
// Hash password
|
||||
let password_hash = pinakes_core::users::auth::hash_password(&req.password)?;
|
||||
|
||||
// Create user - rely on DB unique constraint for username to avoid TOCTOU race
|
||||
let user = state
|
||||
.storage
|
||||
.create_user(&req.username, &password_hash, req.role, req.profile)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Map unique constraint violations to a user-friendly conflict error
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("UNIQUE")
|
||||
|| err_str.contains("unique")
|
||||
|| err_str.contains("duplicate key")
|
||||
{
|
||||
ApiError(pinakes_core::error::PinakesError::DuplicateHash(
|
||||
"username already exists".into(),
|
||||
))
|
||||
} else {
|
||||
ApiError(e)
|
||||
}
|
||||
})?;
|
||||
// Create user - rely on DB unique constraint for username to avoid TOCTOU
|
||||
// race
|
||||
let user = state
|
||||
.storage
|
||||
.create_user(&req.username, &password_hash, req.role, req.profile)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Map unique constraint violations to a user-friendly conflict error
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("UNIQUE")
|
||||
|| err_str.contains("unique")
|
||||
|| err_str.contains("duplicate key")
|
||||
{
|
||||
ApiError(pinakes_core::error::PinakesError::DuplicateHash(
|
||||
"username already exists".into(),
|
||||
))
|
||||
} else {
|
||||
ApiError(e)
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
}
|
||||
|
||||
/// Get a specific user by ID
|
||||
pub async fn get_user(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user_id: UserId = id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
let user_id: UserId =
|
||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let user = state.storage.get_user(user_id).await?;
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
let user = state.storage.get_user(user_id).await?;
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
}
|
||||
|
||||
/// Update a user
|
||||
pub async fn update_user(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<UpdateUserRequest>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<UpdateUserRequest>,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user_id: UserId = id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
let user_id: UserId =
|
||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Hash password if provided
|
||||
let password_hash = if let Some(ref password) = req.password {
|
||||
if password.len() < 8 {
|
||||
return Err(ApiError(
|
||||
pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"password must be at least 8 characters".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
Some(pinakes_core::users::auth::hash_password(password)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Hash password if provided
|
||||
let password_hash = if let Some(ref password) = req.password {
|
||||
if password.len() < 8 {
|
||||
return Err(ApiError(
|
||||
pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"password must be at least 8 characters".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
Some(pinakes_core::users::auth::hash_password(password)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let user = state
|
||||
.storage
|
||||
.update_user(user_id, password_hash.as_deref(), req.role, req.profile)
|
||||
.await?;
|
||||
let user = state
|
||||
.storage
|
||||
.update_user(user_id, password_hash.as_deref(), req.role, req.profile)
|
||||
.await?;
|
||||
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
Ok(Json(UserResponse::from(user)))
|
||||
}
|
||||
|
||||
/// Delete a user (admin only)
|
||||
pub async fn delete_user(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user_id: UserId = id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
let user_id: UserId =
|
||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
state.storage.delete_user(user_id).await?;
|
||||
Ok(Json(serde_json::json!({"deleted": true})))
|
||||
state.storage.delete_user(user_id).await?;
|
||||
Ok(Json(serde_json::json!({"deleted": true})))
|
||||
}
|
||||
|
||||
/// Get user's accessible libraries
|
||||
pub async fn get_user_libraries(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<Vec<UserLibraryResponse>>, ApiError> {
|
||||
let user_id: UserId = id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
let user_id: UserId =
|
||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let libraries = state.storage.get_user_libraries(user_id).await?;
|
||||
Ok(Json(
|
||||
libraries
|
||||
.into_iter()
|
||||
.map(UserLibraryResponse::from)
|
||||
.collect(),
|
||||
))
|
||||
let libraries = state.storage.get_user_libraries(user_id).await?;
|
||||
Ok(Json(
|
||||
libraries
|
||||
.into_iter()
|
||||
.map(UserLibraryResponse::from)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Grant library access to a user (admin only)
|
||||
pub async fn grant_library_access(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<GrantLibraryAccessRequest>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<GrantLibraryAccessRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user_id: UserId = id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
let user_id: UserId =
|
||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.grant_library_access(user_id, &req.root_path, req.permission)
|
||||
.await?;
|
||||
state
|
||||
.storage
|
||||
.grant_library_access(user_id, &req.root_path, req.permission)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({"granted": true})))
|
||||
Ok(Json(serde_json::json!({"granted": true})))
|
||||
}
|
||||
|
||||
/// Revoke library access from a user (admin only)
|
||||
|
|
@ -173,19 +178,20 @@ pub async fn grant_library_access(
|
|||
/// Uses a JSON body instead of a path parameter because root_path may contain
|
||||
/// slashes that conflict with URL routing.
|
||||
pub async fn revoke_library_access(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<RevokeLibraryAccessRequest>,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<RevokeLibraryAccessRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let user_id: UserId = id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
let user_id: UserId =
|
||||
id.parse::<uuid::Uuid>().map(UserId::from).map_err(|_| {
|
||||
ApiError(pinakes_core::error::PinakesError::InvalidOperation(
|
||||
"Invalid user ID".into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.revoke_library_access(user_id, &req.root_path)
|
||||
.await?;
|
||||
Ok(Json(serde_json::json!({"revoked": true})))
|
||||
state
|
||||
.storage
|
||||
.revoke_library_access(user_id, &req.root_path)
|
||||
.await?;
|
||||
Ok(Json(serde_json::json!({"revoked": true})))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue