feat(nextcloud): add Nextcloud-compatible API layer
Implement a complete Nextcloud client compatibility layer so that Nextcloud desktop/mobile sync clients can connect to OxiCloud. Key additions: - Login Flow v2 (device auth) with OIDC bridge support - WebDAV handler compatible with Nextcloud clients (PROPFIND, GET, PUT, DELETE, MKCOL, MOVE, COPY, HEAD, PROPPATCH) - OCS API endpoints (user info, capabilities, notifications stubs, sharees, unified search) - Basic Auth middleware with app password verification, account lockout integration, and blake3-keyed auth cache - App password management: create, list, revoke via both native API (JWT-authenticated profile page) and Nextcloud OCS endpoints - Nextcloud file ID mapping (oc:fileid) with persistent DB storage - Chunked upload support (Nextcloud v2 chunking protocol) - Trashbin WebDAV interface - Avatar (SVG placeholder) and preview (redirect) handlers - User profile page with app password management UI - URL user validation on all DAV routes (403 on mismatch) - Database schema for app_passwords and nextcloud_object_ids tables All services are behind a `nextcloud.enabled` config flag and cleanly separated under src/interfaces/nextcloud/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -369,6 +369,57 @@ impl UserRepository for UserPgRepository {
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
async fn search_users(&self, query: &str, limit: i64) -> UserRepositoryResult<Vec<User>> {
|
||||
let pattern = format!("%{}%", query);
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id, username, email, password_hash, role::text as role_text,
|
||||
storage_quota_bytes, storage_used_bytes,
|
||||
created_at, updated_at, last_login_at, active,
|
||||
oidc_provider, oidc_subject
|
||||
FROM auth.users
|
||||
WHERE username ILIKE $1 OR email ILIKE $1
|
||||
ORDER BY username
|
||||
LIMIT $2
|
||||
"#,
|
||||
)
|
||||
.bind(&pattern)
|
||||
.bind(limit)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let users = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let role_str: Option<String> = row.try_get("role_text").unwrap_or(None);
|
||||
let role = match role_str.as_deref() {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
User::from_data_full(
|
||||
row.get("id"),
|
||||
row.get("username"),
|
||||
row.get("email"),
|
||||
row.get("password_hash"),
|
||||
role,
|
||||
row.get("storage_quota_bytes"),
|
||||
row.get("storage_used_bytes"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
row.get("last_login_at"),
|
||||
row.get("active"),
|
||||
row.get("oidc_provider"),
|
||||
row.get("oidc_subject"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
/// Activates or deactivates a user
|
||||
async fn set_user_active_status(
|
||||
&self,
|
||||
@@ -664,6 +715,12 @@ impl UserStoragePort for UserPgRepository {
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn search_users(&self, query: &str, limit: i64) -> Result<Vec<User>, DomainError> {
|
||||
UserRepository::search_users(self, query, limit)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError> {
|
||||
UserRepository::list_users_by_role(self, role)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user