|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
use axum::{
|
|
|
|
|
Router,
|
|
|
|
|
extract::{DefaultBodyLimit, Json, Multipart, Path, Query, State},
|
|
|
|
|
http::{HeaderMap, StatusCode},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::{
|
|
|
|
|
IntoResponse,
|
|
|
|
|
sse::{Event, KeepAlive, Sse},
|
|
|
|
@@ -29,7 +29,7 @@ use crate::domain::repositories::drive_repository::DriveRepository;
|
|
|
|
|
use crate::domain::services::authorization::{Resource, Subject};
|
|
|
|
|
use crate::interfaces::api::handlers::search_handler::clear_search_cache;
|
|
|
|
|
use crate::interfaces::errors::AppError;
|
|
|
|
|
use crate::interfaces::middleware::admin::require_admin;
|
|
|
|
|
use crate::interfaces::middleware::auth::AuthUser;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
@@ -128,14 +128,13 @@ pub fn admin_routes() -> Router<Arc<AppState>> {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate JWT and require admin role. Returns (user_id, role).
|
|
|
|
|
///
|
|
|
|
|
/// Thin wrapper over the shared `require_admin` middleware helper so this
|
|
|
|
|
/// handler keeps a stable signature while the implementation lives next to
|
|
|
|
|
/// the new `subject_group_handler` that also needs it.
|
|
|
|
|
async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, String), AppError> {
|
|
|
|
|
require_admin(state, headers).await
|
|
|
|
|
}
|
|
|
|
|
// Every route under `/api/admin/*` is gated by the
|
|
|
|
|
// `require_admin` middleware layer wired at the router nest point
|
|
|
|
|
// (`routes.rs::admin_router`). Handlers no longer need an inline
|
|
|
|
|
// guard call — the caller is guaranteed to be admin by construction.
|
|
|
|
|
// Callers that need the caller's id read it from the `AuthUser`
|
|
|
|
|
// extractor (`middleware::auth::AuthUser`), populated by the outer
|
|
|
|
|
// `auth_middleware`.
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
|
|
|
|
#[utoipa::path(
|
|
|
|
@@ -151,9 +150,7 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, Str
|
|
|
|
|
)]
|
|
|
|
|
pub async fn get_oidc_settings(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
@@ -182,10 +179,10 @@ pub async fn get_oidc_settings(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn save_oidc_settings(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Json(dto): Json<SaveOidcSettingsDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (user_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let user_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
@@ -207,10 +204,8 @@ pub async fn save_oidc_settings(
|
|
|
|
|
/// POST /api/admin/settings/oidc/test — test OIDC discovery
|
|
|
|
|
async fn test_oidc_connection(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<TestOidcConnectionDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.admin_settings_service
|
|
|
|
@@ -243,9 +238,7 @@ async fn test_oidc_connection(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn get_storage_settings(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.storage_settings_service
|
|
|
|
@@ -274,10 +267,10 @@ pub async fn get_storage_settings(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn save_storage_settings(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Json(dto): Json<SaveStorageSettingsDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (user_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let user_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.storage_settings_service
|
|
|
|
@@ -299,10 +292,8 @@ pub async fn save_storage_settings(
|
|
|
|
|
/// POST /api/admin/settings/storage/test — test storage backend connection
|
|
|
|
|
async fn test_storage_connection(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<TestStorageConnectionDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let svc = state
|
|
|
|
|
.storage_settings_service
|
|
|
|
@@ -335,9 +326,7 @@ async fn test_storage_connection(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn get_migration_status(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let s = state.migration_state.read().await;
|
|
|
|
|
Ok(Json(migration_state_to_dto(&s)))
|
|
|
|
|
}
|
|
|
|
@@ -357,12 +346,10 @@ pub async fn get_migration_status(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn start_migration(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<StartMigrationDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
use crate::infrastructure::services::migration_blob_backend::MigrationStatus;
|
|
|
|
|
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
// Check not already running.
|
|
|
|
|
{
|
|
|
|
@@ -435,10 +422,8 @@ pub async fn start_migration(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn pause_migration(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
use crate::infrastructure::services::migration_blob_backend::MigrationStatus;
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let mut s = state.migration_state.write().await;
|
|
|
|
|
if s.status != MigrationStatus::Running {
|
|
|
|
@@ -466,10 +451,8 @@ pub async fn pause_migration(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn resume_migration(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
use crate::infrastructure::services::migration_blob_backend::MigrationStatus;
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
// Set status back to Running — the background task checks on each blob.
|
|
|
|
|
let mut s = state.migration_state.write().await;
|
|
|
|
@@ -498,10 +481,8 @@ pub async fn resume_migration(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn complete_migration(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
use crate::infrastructure::services::migration_blob_backend::MigrationStatus;
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let s = state.migration_state.read().await;
|
|
|
|
|
if s.status != MigrationStatus::Completed {
|
|
|
|
@@ -538,10 +519,8 @@ pub async fn complete_migration(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn verify_migration(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<VerifyMigrationDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let pool = state
|
|
|
|
|
.db_pool
|
|
|
|
@@ -614,12 +593,7 @@ fn migration_state_to_dto(
|
|
|
|
|
security(("bearerAuth" = [])),
|
|
|
|
|
tag = "admin"
|
|
|
|
|
)]
|
|
|
|
|
pub async fn generate_encryption_key(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
pub async fn generate_encryption_key() -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let key =
|
|
|
|
|
crate::infrastructure::services::encrypted_blob_backend::EncryptedBlobBackend::generate_key(
|
|
|
|
|
);
|
|
|
|
@@ -677,9 +651,7 @@ fn build_backend_from_config(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn get_dashboard_stats(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
@@ -768,10 +740,8 @@ pub async fn get_dashboard_stats(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn list_users(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Query(query): Query<ListUsersQueryDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
@@ -817,10 +787,8 @@ pub async fn list_users(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn get_user(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
|
|
|
@@ -854,10 +822,10 @@ pub async fn get_user(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn delete_user(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
|
|
|
@@ -904,11 +872,11 @@ pub async fn delete_user(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn update_user_role(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<UpdateUserRoleDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
|
|
|
@@ -955,11 +923,11 @@ pub async fn update_user_role(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn update_user_active(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<UpdateUserActiveDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
|
|
|
@@ -1010,11 +978,9 @@ pub async fn update_user_active(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn update_user_quota(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<UpdateUserQuotaDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
|
|
|
@@ -1056,10 +1022,8 @@ pub async fn update_user_quota(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn create_user(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Json(dto): Json<AdminCreateUserDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let auth = state
|
|
|
|
|
.auth_service
|
|
|
|
@@ -1097,11 +1061,9 @@ pub async fn create_user(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn reset_user_password(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<AdminResetPasswordDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
|
|
|
|
|
|
|
|
@@ -1148,10 +1110,10 @@ pub async fn reset_user_password(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn set_registration_setting(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Json(body): Json<serde_json::Value>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let enabled = body
|
|
|
|
|
.get("registration_enabled")
|
|
|
|
@@ -1184,9 +1146,7 @@ pub async fn set_registration_setting(
|
|
|
|
|
|
|
|
|
|
async fn reextract_audio_metadata(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let audio_service = state
|
|
|
|
|
.applications
|
|
|
|
@@ -1214,9 +1174,7 @@ async fn reextract_audio_metadata(
|
|
|
|
|
/// Photos timeline by real capture date. Safe to re-run (idempotent upsert).
|
|
|
|
|
async fn reextract_image_metadata(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let result = state
|
|
|
|
|
.applications
|
|
|
|
@@ -1262,9 +1220,7 @@ async fn reextract_image_metadata(
|
|
|
|
|
)]
|
|
|
|
|
async fn get_smtp_info(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
let smtp = &state.core.config.smtp;
|
|
|
|
|
let info = SmtpInfoDto {
|
|
|
|
@@ -1294,10 +1250,8 @@ async fn get_smtp_info(
|
|
|
|
|
/// returns 404 to keep the endpoint inert.
|
|
|
|
|
async fn get_captured_email(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Query(params): Query<CapturedEmailQuery>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
|
|
|
|
|
if !std::env::var("OXICLOUD_SMTP_MOCK")
|
|
|
|
|
.map(|v| v == "true" || v == "1")
|
|
|
|
@@ -1354,10 +1308,10 @@ struct CapturedEmailQuery {
|
|
|
|
|
)]
|
|
|
|
|
async fn send_smtp_test(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Json(dto): Json<SendSmtpTestDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
|
|
|
|
|
let recipient = dto.to.trim().to_string();
|
|
|
|
|
if recipient.is_empty() {
|
|
|
|
@@ -1469,9 +1423,7 @@ fn map_mgmt_err(err: &PluginMgmtError) -> AppError {
|
|
|
|
|
/// GET /api/admin/plugins — list installed plugins.
|
|
|
|
|
pub async fn list_plugins(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
let plugins: Vec<PluginInfoDto> = mgmt.list().into_iter().map(PluginInfoDto::from).collect();
|
|
|
|
|
// `enabled` reports that the plugin *subsystem* is active (reaching here
|
|
|
|
@@ -1486,11 +1438,11 @@ pub async fn list_plugins(
|
|
|
|
|
/// PUT /api/admin/plugins/{id}/enabled — enable or disable a plugin.
|
|
|
|
|
pub async fn set_plugin_enabled(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<SetEnabledDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
mgmt.set_enabled(&id, dto.enabled)
|
|
|
|
|
.map_err(|e| map_mgmt_err(&e))?;
|
|
|
|
@@ -1527,10 +1479,10 @@ pub async fn set_plugin_enabled(
|
|
|
|
|
/// single `bundle` part: a `.zip` containing `plugin.toml` and its `.wasm`.
|
|
|
|
|
pub async fn install_plugin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
mut multipart: Multipart,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
|
|
|
|
|
let mut bundle: Option<Vec<u8>> = None;
|
|
|
|
@@ -1591,10 +1543,10 @@ pub async fn install_plugin(
|
|
|
|
|
/// DELETE /api/admin/plugins/{id} — uninstall a plugin and delete its files.
|
|
|
|
|
pub async fn delete_plugin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
mgmt.remove(&id).map_err(|e| map_mgmt_err(&e))?;
|
|
|
|
|
|
|
|
|
@@ -1616,11 +1568,9 @@ pub async fn delete_plugin(
|
|
|
|
|
/// structured log entries (newest first).
|
|
|
|
|
pub async fn get_plugin_logs(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Query(q): Query<PluginLogQueryDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
|
|
|
|
|
let limit = q.limit.unwrap_or(50).clamp(1, 500);
|
|
|
|
@@ -1644,10 +1594,10 @@ pub async fn get_plugin_logs(
|
|
|
|
|
/// DELETE /api/admin/plugins/{id}/logs — wipe a plugin's persisted logs.
|
|
|
|
|
pub async fn clear_plugin_logs(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
mgmt.clear_logs(&id).await.map_err(|e| map_mgmt_err(&e))?;
|
|
|
|
|
|
|
|
|
@@ -1671,13 +1621,11 @@ pub async fn clear_plugin_logs(
|
|
|
|
|
/// so `EventSource` works without setting headers.
|
|
|
|
|
pub async fn stream_plugin_logs(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
use tokio_stream::StreamExt;
|
|
|
|
|
use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
|
|
|
|
|
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
if !mgmt.list().iter().any(|p| p.id == id) {
|
|
|
|
|
return Err(AppError::not_found("Plugin not found"));
|
|
|
|
@@ -1705,10 +1653,8 @@ pub async fn stream_plugin_logs(
|
|
|
|
|
/// GET /api/admin/plugins/{id}/retention — the plugin's effective retention.
|
|
|
|
|
pub async fn get_plugin_retention(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
let settings = mgmt
|
|
|
|
|
.get_retention(&id)
|
|
|
|
@@ -1720,11 +1666,11 @@ pub async fn get_plugin_retention(
|
|
|
|
|
/// PUT /api/admin/plugins/{id}/retention — set the plugin's retention policy.
|
|
|
|
|
pub async fn set_plugin_retention(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(dto): Json<PluginRetentionDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let mgmt = plugin_mgmt(&state)?;
|
|
|
|
|
mgmt.set_retention(&id, dto.into())
|
|
|
|
|
.await
|
|
|
|
@@ -1768,9 +1714,7 @@ pub async fn set_plugin_retention(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn list_all_drives(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let drives = state
|
|
|
|
|
.drive_repo
|
|
|
|
|
.list_all()
|
|
|
|
@@ -1806,10 +1750,8 @@ pub async fn list_all_drives(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn list_drive_members_admin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
axum::extract::Path(drive_id): axum::extract::Path<Uuid>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
admin_guard(&state, &headers).await?;
|
|
|
|
|
let grants = state
|
|
|
|
|
.authorization
|
|
|
|
|
.list_grants_on_resource(Resource::Drive(drive_id))
|
|
|
|
@@ -1869,11 +1811,11 @@ fn admin_parse_subject(kind: SubjectTypeDto, id: Uuid) -> Subject {
|
|
|
|
|
)]
|
|
|
|
|
pub async fn add_drive_member_admin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
axum::extract::Path(drive_id): axum::extract::Path<Uuid>,
|
|
|
|
|
Json(dto): Json<AdminAddDriveMemberDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let subject = admin_parse_subject(dto.subject.kind, dto.subject.id);
|
|
|
|
|
let grant = state
|
|
|
|
|
.drive_management_service
|
|
|
|
@@ -1914,7 +1856,7 @@ pub async fn add_drive_member_admin(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn update_drive_member_admin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
axum::extract::Path((drive_id, kind, subject_id)): axum::extract::Path<(
|
|
|
|
|
Uuid,
|
|
|
|
|
SubjectTypeDto,
|
|
|
|
@@ -1922,7 +1864,7 @@ pub async fn update_drive_member_admin(
|
|
|
|
|
)>,
|
|
|
|
|
Json(dto): Json<AdminUpdateDriveMemberDto>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let subject = admin_parse_subject(kind, subject_id);
|
|
|
|
|
let grant = state
|
|
|
|
|
.drive_management_service
|
|
|
|
@@ -1961,14 +1903,14 @@ pub async fn update_drive_member_admin(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn remove_drive_member_admin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
axum::extract::Path((drive_id, kind, subject_id)): axum::extract::Path<(
|
|
|
|
|
Uuid,
|
|
|
|
|
SubjectTypeDto,
|
|
|
|
|
Uuid,
|
|
|
|
|
)>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
let subject = admin_parse_subject(kind, subject_id);
|
|
|
|
|
state
|
|
|
|
|
.drive_management_service
|
|
|
|
@@ -2003,10 +1945,10 @@ pub async fn remove_drive_member_admin(
|
|
|
|
|
)]
|
|
|
|
|
pub async fn delete_drive_admin(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
auth_user: AuthUser,
|
|
|
|
|
axum::extract::Path(drive_id): axum::extract::Path<Uuid>,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
|
|
|
|
let admin_id = auth_user.id;
|
|
|
|
|
state
|
|
|
|
|
.drive_management_service
|
|
|
|
|
.delete_drive(admin_id, true, drive_id)
|
|
|
|
@@ -2062,15 +2004,11 @@ fn internal_endpoints_disabled() -> axum::response::Response {
|
|
|
|
|
)]
|
|
|
|
|
pub async fn internal_trigger_sweep(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
) -> axum::response::Response {
|
|
|
|
|
use axum::response::IntoResponse;
|
|
|
|
|
if !state.core.config.features.enable_admin_internal_endpoints {
|
|
|
|
|
return internal_endpoints_disabled();
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) = admin_guard(&state, &headers).await {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
let svc = match state.storage_usage_service.as_ref() {
|
|
|
|
|
Some(s) => s,
|
|
|
|
|
None => {
|
|
|
|
@@ -2142,16 +2080,12 @@ pub struct InternalTriggerGcQuery {
|
|
|
|
|
)]
|
|
|
|
|
pub async fn internal_trigger_gc(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Query(query): Query<InternalTriggerGcQuery>,
|
|
|
|
|
) -> axum::response::Response {
|
|
|
|
|
use axum::response::IntoResponse;
|
|
|
|
|
if !state.core.config.features.enable_admin_internal_endpoints {
|
|
|
|
|
return internal_endpoints_disabled();
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) = admin_guard(&state, &headers).await {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
let result = if query.force {
|
|
|
|
|
state.core.dedup_service.garbage_collect_force().await
|
|
|
|
|
} else {
|
|
|
|
@@ -2218,16 +2152,12 @@ pub struct InternalTriggerGrantCleanupQuery {
|
|
|
|
|
)]
|
|
|
|
|
pub async fn internal_trigger_grant_cleanup(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
headers: HeaderMap,
|
|
|
|
|
Query(query): Query<InternalTriggerGrantCleanupQuery>,
|
|
|
|
|
) -> axum::response::Response {
|
|
|
|
|
use axum::response::IntoResponse;
|
|
|
|
|
if !state.core.config.features.enable_admin_internal_endpoints {
|
|
|
|
|
return internal_endpoints_disabled();
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) = admin_guard(&state, &headers).await {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
// Daemon may be disabled by config even when the internal-endpoint
|
|
|
|
|
// gate is on. Return 503 (rather than 404 or 500) so integration
|
|
|
|
|
// tests can distinguish "surface not exposed" from "surface
|
|
|
|
|