Files
Oxicloud/src/application/dtos/settings_dto.rs
T

390 lines
16 KiB
Rust
Raw Normal View History

2026-02-14 01:29:34 +01:00
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
2026-02-14 01:29:34 +01:00
// ============================================================================
// OIDC Settings DTOs (Admin Panel)
// ============================================================================
/// Current OIDC settings returned to admin UI (secrets masked)
#[derive(Debug, Serialize, Deserialize)]
pub struct OidcSettingsDto {
pub enabled: bool,
pub issuer_url: String,
pub client_id: String,
/// True if a client secret is configured (never reveals the actual value)
pub client_secret_set: bool,
pub scopes: String,
pub auto_provision: bool,
pub admin_groups: String,
pub disable_password_login: bool,
pub provider_name: String,
/// Auto-generated callback URL the admin must register in their IdP
pub callback_url: String,
/// Field names overridden by environment variables (read-only in UI)
pub env_overrides: Vec<String>,
}
/// Request body for saving OIDC settings from the admin panel
#[derive(Debug, Serialize, Deserialize, ToSchema)]
2026-02-14 01:29:34 +01:00
pub struct SaveOidcSettingsDto {
pub enabled: bool,
pub issuer_url: String,
pub client_id: String,
/// Only update if provided and non-empty (None = keep existing)
pub client_secret: Option<String>,
pub scopes: Option<String>,
pub auto_provision: Option<bool>,
pub admin_groups: Option<String>,
pub disable_password_login: Option<bool>,
pub provider_name: Option<String>,
}
/// Request body for testing OIDC discovery
#[derive(Debug, Serialize, Deserialize)]
pub struct TestOidcConnectionDto {
pub issuer_url: String,
}
/// Result of OIDC connection test
#[derive(Debug, Serialize, Deserialize)]
pub struct OidcTestResultDto {
pub success: bool,
pub message: String,
pub issuer: Option<String>,
pub authorization_endpoint: Option<String>,
pub token_endpoint: Option<String>,
pub userinfo_endpoint: Option<String>,
/// Suggested provider name (derived from issuer hostname)
pub provider_name_suggestion: Option<String>,
}
// ============================================================================
// Admin User Management DTOs
// ============================================================================
/// Request body for updating a user's role
#[derive(Debug, Serialize, Deserialize, ToSchema)]
2026-02-14 01:29:34 +01:00
pub struct UpdateUserRoleDto {
pub role: String,
}
/// Request body for updating a user's active status
#[derive(Debug, Serialize, Deserialize, ToSchema)]
2026-02-14 01:29:34 +01:00
pub struct UpdateUserActiveDto {
pub active: bool,
}
/// Request body for updating a user's storage quota
#[derive(Debug, Serialize, Deserialize, ToSchema)]
2026-02-14 01:29:34 +01:00
pub struct UpdateUserQuotaDto {
/// Quota in bytes. Use 0 for unlimited.
pub quota_bytes: i64,
}
/// Request body for admin-created users
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
2026-02-14 01:29:34 +01:00
pub struct AdminCreateUserDto {
pub username: String,
pub password: String,
/// Optional — if omitted, a placeholder email is generated
pub email: Option<String>,
/// "admin" or "user"; defaults to "user"
pub role: Option<String>,
/// Storage quota in bytes; 0 = unlimited. If omitted, uses role default.
/// Ignored when `is_external = true` (external users have no storage).
2026-02-14 01:29:34 +01:00
pub quota_bytes: Option<i64>,
/// Whether the account is active; defaults to true
pub active: Option<bool>,
/// `true` to create a grant-only external user (no home folder, no
/// storage quota). Defaults to `false` (internal user). External
/// users authenticate via magic-link / OIDC / OCM federation —
/// password is set but never used.
pub is_external: Option<bool>,
2026-02-14 01:29:34 +01:00
}
/// Request body for admin password reset
#[derive(Debug, Serialize, Deserialize, ToSchema)]
2026-02-14 01:29:34 +01:00
pub struct AdminResetPasswordDto {
pub new_password: String,
}
/// Query parameters for listing users
#[derive(Debug, Serialize, Deserialize)]
pub struct ListUsersQueryDto {
pub limit: Option<i64>,
pub offset: Option<i64>,
2026-07-22 02:06:04 +02:00
/// Return only the fields rendered by the paginated management table.
/// Defaults to `false` so existing API clients keep the full user shape.
pub summary: Option<bool>,
2026-02-14 01:29:34 +01:00
}
/// Dashboard statistics
#[derive(Debug, Serialize, Deserialize)]
pub struct DashboardStatsDto {
// System info
pub server_version: String,
pub auth_enabled: bool,
pub oidc_configured: bool,
pub quotas_enabled: bool,
// User stats
pub total_users: i64,
pub active_users: i64,
pub admin_users: i64,
// Storage stats
pub total_quota_bytes: i64,
pub total_used_bytes: i64,
pub storage_usage_percent: f64,
pub users_over_80_percent: i64,
pub users_over_quota: i64,
pub registration_enabled: bool,
}
// ============================================================================
// Storage Settings DTOs (Admin Panel)
// ============================================================================
2026-08-01 13:54:00 +02:00
/// Current storage settings returned to admin UI (secrets masked).
///
/// Post-multi-entry (`docs/plan/storage-multi-entry.md`) this carries
/// two shapes side-by-side: the legacy flat storage-config fields
/// (for the pre-multi-entry admin UI, until slice 6 completes the
/// form retirement), plus the new `entries` / `active_entry_name` /
/// `migration_readonly` view the multi-entry UI drives its entries-list,
/// migration-target-dropdown, and readonly-banner from.
#[derive(Debug, Serialize, Deserialize)]
pub struct StorageSettingsDto {
2026-08-01 13:54:00 +02:00
/// Active backend type: "local" or "s3". Legacy flat-config field
/// — mirrors `entries[i where is_active].backend` for the active
/// entry when multi-entry is in use.
pub backend: String,
pub s3_endpoint_url: Option<String>,
pub s3_bucket: Option<String>,
pub s3_region: Option<String>,
/// True if an access key is configured (never reveals the actual value)
pub s3_access_key_set: bool,
/// True if a secret key is configured (never reveals the actual value)
pub s3_secret_key_set: bool,
pub s3_force_path_style: bool,
/// Field names overridden by environment variables (read-only in UI)
pub env_overrides: Vec<String>,
// ── Current stats ──
pub current_backend: String,
pub total_blobs: u64,
pub total_bytes_stored: u64,
pub dedup_ratio: f64,
2026-08-01 13:54:00 +02:00
// ── Multi-entry view (slice 6) ──
/// All named storage entries declared in env. Empty when running
/// in legacy single-backend mode (`OXICLOUD_STORAGE_ENTRIES`
/// unset AND no legacy synthesis happened). Order matches
/// `_ENTRIES`.
pub entries: Vec<StorageEntrySummaryDto>,
/// Name of the entry the LIVE backend is currently bound to.
/// Populated as the boot-selected name (per
/// `CoreServices.active_backend_name`). Empty string for the
/// zero-entries legacy path (`"legacy"` sentinel).
pub active_entry_name: String,
/// Global read-only flag — when true, all write-adjacent
/// AuthZ checks refuse. Set by the migration handler at run
/// start; cleared by the boot-clear rule after operator
/// restart. Frontend renders a banner on the storage tab when
/// true.
pub migration_readonly: bool,
}
/// Per-entry summary emitted in `StorageSettingsDto.entries`. Never
/// carries credentials — those live in env vars only. `is_active`
/// marks which entry the LIVE backend uses right now (matches
/// `active_entry_name` on the parent DTO).
#[derive(Debug, Serialize, Deserialize)]
pub struct StorageEntrySummaryDto {
pub name: String,
/// Backend type — "local" / "s3" / "azure".
pub backend: String,
/// True for exactly one entry (the entry the LIVE backend is on).
/// Frontend uses this to badge the active row and to exclude it
/// from the migration-target dropdown.
pub is_active: bool,
/// True when the entry has a per-entry encryption key. UI shows
/// a lock icon. Presence-only — the key bytes never leave the
/// server.
pub encryption_enabled: bool,
/// Human-readable physical location hint, if the backend surfaces
/// one (`root_dir` for Local, `bucket` for S3, `container` for
/// Azure). Cosmetic — helps the admin distinguish two Local
/// entries pointing at different disks.
pub location_hint: Option<String>,
}
/// Request body for saving storage settings from the admin panel
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct SaveStorageSettingsDto {
pub backend: String,
pub s3_endpoint_url: Option<String>,
pub s3_bucket: Option<String>,
pub s3_region: Option<String>,
/// Only update if provided and non-empty (None = keep existing)
pub s3_access_key: Option<String>,
/// Only update if provided and non-empty (None = keep existing)
pub s3_secret_key: Option<String>,
pub s3_force_path_style: Option<bool>,
}
/// Request body for testing a storage connection
#[derive(Debug, Serialize, Deserialize)]
pub struct TestStorageConnectionDto {
pub backend: String,
pub s3_endpoint_url: Option<String>,
pub s3_bucket: Option<String>,
pub s3_region: Option<String>,
pub s3_access_key: Option<String>,
pub s3_secret_key: Option<String>,
pub s3_force_path_style: Option<bool>,
}
/// Result of a storage connection + round-trip test.
///
/// `connected` is TRUE when the backend was reachable (health-check
/// passed — HEAD bucket / statfs). `roundtrip_passed` is TRUE when
/// the subsequent PUT → GET → verify → DELETE cycle succeeded — it
/// validates the exact permissions the migration job needs
/// (`s3:PutObject` + `s3:GetObject` + `s3:DeleteObject` on S3, disk
/// write permission on Local). All round-trip fields are `None` when
/// reachability failed (we don't attempt the round-trip if we can't
/// even HEAD the bucket).
///
/// `phase_reached` names the last step that succeeded — on
/// `roundtrip_passed = false` it pinpoints where the failure hit
/// (`put_ok` → wrote but couldn't confirm; `exists_ok` → wrote +
/// confirmed but GET failed; etc.). `cleanup_ok = false` means the
/// backend was readable + writable but the test object may be
/// orphaned on it (~100 B, content-addressed — harmless, admin can
/// reap by hash).
#[derive(Debug, Serialize, Deserialize)]
pub struct StorageTestResultDto {
pub connected: bool,
pub message: String,
pub backend_type: String,
pub available_bytes: Option<u64>,
/// Set only when reachability passed AND a round-trip was
/// attempted. `Some(true)` = full write + read + verify success;
/// `Some(false)` = reachability OK, round-trip failed at
/// `phase_reached`; `None` = round-trip not attempted (typically
/// because reachability failed).
#[serde(skip_serializing_if = "Option::is_none")]
pub roundtrip_passed: Option<bool>,
/// Last round-trip phase completed successfully — one of
/// `initialize`, `put_ok`, `exists_ok`, `get_ok`, `verify_ok`,
/// `cleanup_ok`. `None` when round-trip wasn't attempted.
#[serde(skip_serializing_if = "Option::is_none")]
pub phase_reached: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes_written: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bytes_read: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roundtrip_elapsed_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cleanup_ok: Option<bool>,
}
// ============================================================================
// Migration DTOs (Admin Panel — Storage Migration)
// ============================================================================
/// Migration progress returned by `GET /api/admin/storage/migration`.
/// Re-exports the `MigrationState` shape for the admin UI.
#[derive(Debug, Serialize, Deserialize)]
pub struct MigrationStateDto {
pub status: String,
pub total_blobs: u64,
pub migrated_blobs: u64,
pub migrated_bytes: u64,
pub failed_blobs: Vec<String>,
pub started_at: Option<String>,
pub completed_at: Option<String>,
/// Estimated throughput in bytes/sec (for UI ETA calculation).
pub throughput_bytes_per_sec: Option<f64>,
}
/// Request body for `POST /api/admin/storage/migration/start`.
///
/// **Multi-entry contract** (see `docs/plan/storage-multi-entry.md`):
/// `target_name` is REQUIRED — it names the storage entry the copy
/// job will move blobs INTO. The admin picks it from the entries
/// declared in `OXICLOUD_STORAGE_ENTRIES`. The trigger endpoint
/// rejects the request when the name doesn't exist or equals the
/// currently-active entry (no-op guard).
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct StartMigrationDto {
/// Name of the storage entry to migrate blobs INTO. Must be
/// present in `OXICLOUD_STORAGE_ENTRIES` and must differ from
/// the currently-active entry.
pub target_name: String,
/// How many blobs to copy in parallel (default: 4).
///
/// **Currently ignored** — the recoverable copy loop is
/// sequential (one blob at a time within the batch). Kept in
/// the DTO for wire-compat with the admin UI form; will be
/// honoured once per-batch fan-out lands (dual-write /
/// concurrent-copy future slice).
pub concurrency: Option<usize>,
}
2026-08-01 13:54:00 +02:00
// VerifyMigrationDto retired in slice 7 of
// docs/plan/storage-multi-entry.md — the corresponding endpoint's
// sample-based check is superseded by
// `blobs_consistency?storage=<name>`, a full walk that emits
// structured findings per mismatch.
// ============================================================================
// SMTP Settings DTOs (Admin Panel)
// ============================================================================
/// Read-only SMTP info shown on the admin SMTP page. SMTP configuration
/// is sourced exclusively from environment variables — these fields are
/// for display only and any change has to happen by updating the env
/// and restarting the server.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct SmtpInfoDto {
/// Whether `OXICLOUD_SMTP_HOST` is set and SMTP construction succeeded.
pub enabled: bool,
/// `OXICLOUD_SMTP_HOST`. Empty string when unset.
pub host: String,
/// `OXICLOUD_SMTP_PORT`. Default 587.
pub port: u16,
/// Transport encryption mode: `"starttls"`, `"tls"`, or `"none"`.
pub tls: String,
/// `OXICLOUD_SMTP_FROM` mailbox. Empty when unset.
pub from: String,
/// `<set>` if a SASL user is configured, `<anon>` otherwise.
/// Never echoes the username — admins compare against the
/// runtime config without having to look in `.env`.
pub user_state: &'static str,
}
/// Request body for `POST /api/admin/smtp/test`: send a hardcoded
/// diagnostic email to the given recipient.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct SendSmtpTestDto {
pub to: String,
}
/// Result of a `POST /api/admin/smtp/test` invocation. `success=true`
/// carries the SMTP server's response code + first reply line; on
/// failure the relevant error message goes in `error`. Always 200 OK
/// so the frontend can render both outcomes in one place — the SMTP
/// failure is a normal operational state, not an HTTP error.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct SmtpTestResultDto {
pub success: bool,
/// SMTP status code (e.g. 250). Only set on success.
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<u16>,
/// First line of the SMTP server's reply. Only set on success.
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
/// Human-readable error message. Only set on failure.
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}