2025-03-26 18:33:22 +01:00
|
|
|
use crate::domain::entities::user::User;
|
2026-02-14 01:29:34 +01:00
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
2026-03-29 18:49:10 +02:00
|
|
|
use utoipa::ToSchema;
|
2026-03-07 14:59:32 +01:00
|
|
|
use uuid::Uuid;
|
2025-03-20 09:22:31 +01:00
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
2025-03-20 09:22:31 +01:00
|
|
|
pub struct UserDto {
|
|
|
|
|
pub id: String,
|
2026-06-02 21:21:24 +02:00
|
|
|
/// Optional handle. `None` for users who have not claimed one
|
|
|
|
|
/// (externals, fresh email-only signups). Frontend display callers
|
|
|
|
|
/// should walk `username → given/family → email` as their fallback
|
|
|
|
|
/// chain. Omitted from JSON when None (consistent with the existing
|
|
|
|
|
/// given_name / family_name fields).
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub username: Option<String>,
|
2025-03-20 09:22:31 +01:00
|
|
|
pub email: String,
|
|
|
|
|
pub role: String,
|
|
|
|
|
pub storage_quota_bytes: i64,
|
|
|
|
|
pub storage_used_bytes: i64,
|
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
|
pub updated_at: DateTime<Utc>,
|
|
|
|
|
pub last_login_at: Option<DateTime<Utc>>,
|
|
|
|
|
pub active: bool,
|
2026-02-21 20:26:21 +01:00
|
|
|
pub auth_provider: String,
|
2026-05-26 00:50:38 +02:00
|
|
|
pub image: Option<String>,
|
|
|
|
|
pub can_edit_image: bool,
|
2026-06-01 15:51:05 +02:00
|
|
|
/// `true` for grant-only external recipients (magic-link, OIDC-only,
|
|
|
|
|
/// future OCM federated). External users have no home folder and
|
|
|
|
|
/// can't own storage; their quota is always 0. Internal users
|
|
|
|
|
/// default to `false`.
|
|
|
|
|
pub is_external: bool,
|
2026-06-02 11:20:44 +02:00
|
|
|
/// Optional first/given name. Populated from the OIDC `given_name`
|
|
|
|
|
/// claim at JIT provisioning, or via a profile-edit endpoint.
|
|
|
|
|
/// `None` until explicitly set — `skip_serializing_if = "Option::is_none"`
|
|
|
|
|
/// keeps the wire format compact for the common case.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub given_name: Option<String>,
|
|
|
|
|
/// Optional last/family name. Same provenance + serde rules as
|
|
|
|
|
/// `given_name`.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub family_name: Option<String>,
|
2026-06-02 23:55:50 +02:00
|
|
|
/// When the user first demonstrated control of their email (PR 23).
|
|
|
|
|
/// `None` = unverified (omitted from JSON). Stamped on the first
|
|
|
|
|
/// successful magic-link redemption or OIDC JIT with verified
|
|
|
|
|
/// claim. Idempotent — the original timestamp is preserved on
|
|
|
|
|
/// subsequent verifications.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub email_verified_at: Option<DateTime<Utc>>,
|
2026-06-03 14:26:45 +02:00
|
|
|
/// User-chosen locale for server-rendered surfaces (emails,
|
|
|
|
|
/// future authenticated HTML). `None` = no preference (the server
|
|
|
|
|
/// resolves to `OXICLOUD_DEFAULT_LOCALE` when rendering). Round-trips
|
|
|
|
|
/// through `/api/auth/me` and `PATCH /api/auth/me/profile`.
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub preferred_locale: Option<String>,
|
2026-06-05 09:46:51 +02:00
|
|
|
/// Whether the user wants an email when someone shares a resource
|
|
|
|
|
/// with them. `true` (default) = receive share-notification mails;
|
|
|
|
|
/// `false` = grants are still created but no email is sent. Honored
|
|
|
|
|
/// only on the plain-notification path — magic-link first-invitations
|
|
|
|
|
/// to brand-new external users always send, otherwise the recipient
|
|
|
|
|
/// could never claim the share. Round-trips through `/api/auth/me`
|
|
|
|
|
/// and `PATCH /api/auth/me/profile`.
|
|
|
|
|
pub notify_on_share: bool,
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<User> for UserDto {
|
|
|
|
|
fn from(user: User) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: user.id().to_string(),
|
2026-06-02 21:21:24 +02:00
|
|
|
username: user.username().map(str::to_string),
|
2025-03-20 09:22:31 +01:00
|
|
|
email: user.email().to_string(),
|
|
|
|
|
role: format!("{}", user.role()),
|
|
|
|
|
storage_quota_bytes: user.storage_quota_bytes(),
|
|
|
|
|
storage_used_bytes: user.storage_used_bytes(),
|
|
|
|
|
created_at: user.created_at(),
|
|
|
|
|
updated_at: user.updated_at(),
|
|
|
|
|
last_login_at: user.last_login_at(),
|
|
|
|
|
active: user.is_active(),
|
2026-02-21 20:33:54 +01:00
|
|
|
auth_provider: user.oidc_provider().unwrap_or("local").to_string(),
|
2026-05-26 00:50:38 +02:00
|
|
|
image: user.image().map(|s| s.to_string()),
|
|
|
|
|
can_edit_image: !user.is_oidc_user(),
|
2026-06-01 15:51:05 +02:00
|
|
|
is_external: user.is_external(),
|
2026-06-02 11:20:44 +02:00
|
|
|
given_name: user.given_name().map(str::to_string),
|
|
|
|
|
family_name: user.family_name().map(str::to_string),
|
2026-06-02 23:55:50 +02:00
|
|
|
email_verified_at: user.email_verified_at(),
|
2026-06-03 14:26:45 +02:00
|
|
|
preferred_locale: user.preferred_locale().map(str::to_string),
|
2026-06-05 09:46:51 +02:00
|
|
|
notify_on_share: user.notify_on_share(),
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
2025-03-20 09:22:31 +01:00
|
|
|
pub struct LoginDto {
|
2026-06-02 21:46:01 +02:00
|
|
|
/// Identifier the user typed. Accepts BOTH a username (no `@`) and
|
|
|
|
|
/// an email address (`@` present). The server dispatches on
|
|
|
|
|
/// `@`-in-input: with `@` it looks up by email; without, by
|
|
|
|
|
/// username. The two namespaces are provably disjoint (PR 16
|
|
|
|
|
/// forbids `@` in usernames), so a single field handles both
|
|
|
|
|
/// without ambiguity. The frontend submits whatever the user
|
|
|
|
|
/// typed in the "Username or email" field as-is.
|
2025-03-20 09:22:31 +01:00
|
|
|
pub username: String,
|
|
|
|
|
pub password: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
2025-03-20 09:22:31 +01:00
|
|
|
pub struct RegisterDto {
|
2026-06-02 22:26:11 +02:00
|
|
|
/// Optional handle (2-64 chars, no `@`). When omitted, the user can
|
|
|
|
|
/// claim one later via the profile-edit endpoint. Users without a
|
|
|
|
|
/// username cannot use NextCloud clients or create app passwords
|
|
|
|
|
/// (Basic-Auth resolves users by username); web UI / native API
|
|
|
|
|
/// works fine without one.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub username: Option<String>,
|
2025-03-20 09:22:31 +01:00
|
|
|
pub email: String,
|
2026-06-02 22:26:11 +02:00
|
|
|
/// Optional password (≥8 chars when present). When omitted, a
|
|
|
|
|
/// welcome magic-link is mailed to `email` for first-session
|
|
|
|
|
/// bootstrap. The user can later set a password via the
|
|
|
|
|
/// change-password endpoint to switch to classic username/email +
|
|
|
|
|
/// password login.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub password: Option<String>,
|
2026-03-04 14:14:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// DTO for the one-time initial admin setup endpoint (`/api/setup`).
|
2026-03-05 22:12:21 +01:00
|
|
|
/// Available only when the system is not yet initialized (no admin exists).
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
2026-03-04 14:14:40 +01:00
|
|
|
pub struct SetupAdminDto {
|
|
|
|
|
pub username: String,
|
|
|
|
|
pub email: String,
|
|
|
|
|
pub password: String,
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:19:31 +02:00
|
|
|
/// Partial-update body for `PATCH /api/auth/me/profile` (PR 24).
|
|
|
|
|
///
|
|
|
|
|
/// Each field is **optional**:
|
|
|
|
|
/// - **absent** → no change to that field.
|
|
|
|
|
/// - **present** → set / claim.
|
|
|
|
|
///
|
|
|
|
|
/// **Username is claim-once, immutable.** This endpoint accepts
|
|
|
|
|
/// `username` only when the caller currently has none — passing it
|
|
|
|
|
/// when one is already claimed is rejected with `409 UsernameImmutable`.
|
|
|
|
|
/// The immutability avoids the NextCloud / DAV client breakage that
|
|
|
|
|
/// would otherwise come from renaming (paths under
|
|
|
|
|
/// `/remote.php/dav/files/{user}/…` and the `verify_url_user` check
|
|
|
|
|
/// both bake the username in as a stable identifier). If a user really
|
|
|
|
|
/// typoed their handle and needs to fix it, an admin override is the
|
|
|
|
|
/// escape hatch.
|
|
|
|
|
///
|
|
|
|
|
/// **Given / family name** are freely settable. Any non-empty value
|
|
|
|
|
/// replaces the current one. Clearing back to `None` is out of scope
|
|
|
|
|
/// for v1.
|
|
|
|
|
///
|
|
|
|
|
/// **OIDC-linked users are rejected wholesale with 403** — their
|
|
|
|
|
/// profile fields are managed at the IdP. The IdP is the source of
|
|
|
|
|
/// truth; mirroring writes here would just create a divergence.
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, Default)]
|
|
|
|
|
pub struct UpdateProfileDto {
|
|
|
|
|
/// Handle to claim (2-64 chars, `[A-Za-z0-9._-]+`, no `@`).
|
|
|
|
|
/// Accepted only when the caller currently has no username. Once
|
|
|
|
|
/// claimed the handle is permanent for the lifetime of the
|
|
|
|
|
/// account; subsequent attempts to set or change it via this
|
|
|
|
|
/// endpoint are rejected with 409. Admin override (via the
|
|
|
|
|
/// admin-create-user / admin-update-user surface, future PR) is
|
|
|
|
|
/// the escape hatch for genuine typos.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub username: Option<String>,
|
|
|
|
|
/// New first/given name. Any non-empty value sets/replaces the
|
|
|
|
|
/// current value. Absent → no change.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub given_name: Option<String>,
|
|
|
|
|
/// New last/family name. Same semantics as `given_name`.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub family_name: Option<String>,
|
2026-06-03 14:26:45 +02:00
|
|
|
/// New preferred locale (BCP-47 shape, e.g. `"fr"`, `"zh-TW"`).
|
|
|
|
|
/// Must resolve against the server's `LocaleRegistry` — unknown
|
|
|
|
|
/// codes are rejected with 400. Pass an empty string to clear the
|
|
|
|
|
/// preference back to the server default (the application layer
|
|
|
|
|
/// normalises `""` → `None`).
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub preferred_locale: Option<String>,
|
2026-06-05 09:46:51 +02:00
|
|
|
/// Whether to receive an email when someone shares a resource with
|
|
|
|
|
/// the user. Absent → no change (existing setting preserved). Pass
|
|
|
|
|
/// `true` to opt in, `false` to opt out. Honored only on the
|
|
|
|
|
/// plain-notification path; magic-link first-invitations to externals
|
|
|
|
|
/// always send.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub notify_on_share: Option<bool>,
|
2026-06-03 00:19:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
2025-03-20 09:22:31 +01:00
|
|
|
pub struct AuthResponseDto {
|
|
|
|
|
pub user: UserDto,
|
|
|
|
|
pub access_token: String,
|
|
|
|
|
pub refresh_token: String,
|
|
|
|
|
pub token_type: String,
|
|
|
|
|
pub expires_in: i64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2025-03-20 09:22:31 +01:00
|
|
|
pub struct ChangePasswordDto {
|
|
|
|
|
pub current_password: String,
|
|
|
|
|
pub new_password: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2025-03-20 09:22:31 +01:00
|
|
|
pub struct RefreshTokenDto {
|
|
|
|
|
pub refresh_token: String,
|
2026-02-02 23:56:40 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
/// Authenticated current user data (for use in application services)
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
2026-02-02 23:56:40 +01:00
|
|
|
pub struct CurrentUser {
|
2026-03-07 14:59:32 +01:00
|
|
|
pub id: Uuid,
|
2026-02-02 23:56:40 +01:00
|
|
|
pub username: String,
|
|
|
|
|
pub email: String,
|
|
|
|
|
pub role: String,
|
2026-02-10 20:32:32 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 14:02:15 +01:00
|
|
|
// ============================================================================
|
|
|
|
|
// App Password DTOs
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-03-04 14:02:15 +01:00
|
|
|
pub struct CreateAppPasswordDto {
|
|
|
|
|
pub label: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-03-04 14:02:15 +01:00
|
|
|
pub struct AppPasswordCreatedDto {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub password: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-03-04 14:02:15 +01:00
|
|
|
pub struct AppPasswordDto {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
|
pub last_used_at: Option<DateTime<Utc>>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:32:32 +01:00
|
|
|
// ============================================================================
|
|
|
|
|
// OIDC DTOs
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// Response with the OIDC authorization URL for client redirect
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-02-10 20:32:32 +01:00
|
|
|
pub struct OidcAuthorizeResponseDto {
|
|
|
|
|
pub authorize_url: String,
|
|
|
|
|
pub state: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Query parameters received on the OIDC callback
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-02-10 20:32:32 +01:00
|
|
|
pub struct OidcCallbackQueryDto {
|
|
|
|
|
pub code: String,
|
|
|
|
|
pub state: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 00:37:47 +01:00
|
|
|
/// Request body for the OIDC one-time code exchange endpoint
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-02-11 00:37:47 +01:00
|
|
|
pub struct OidcExchangeDto {
|
|
|
|
|
pub code: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 20:32:32 +01:00
|
|
|
/// Information about available OIDC providers
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
2026-02-10 20:32:32 +01:00
|
|
|
pub struct OidcProviderInfoDto {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub provider_name: String,
|
|
|
|
|
pub authorize_endpoint: String,
|
|
|
|
|
pub password_login_enabled: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Claims extracted from the validated OIDC ID token
|
2026-03-29 18:49:10 +02:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
2026-02-10 20:32:32 +01:00
|
|
|
pub struct OidcUserInfoDto {
|
|
|
|
|
pub sub: String,
|
|
|
|
|
pub preferred_username: Option<String>,
|
|
|
|
|
pub email: Option<String>,
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
pub groups: Vec<String>,
|
2026-02-14 01:29:34 +01:00
|
|
|
}
|