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:
@@ -0,0 +1,87 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::di::AppState;
|
||||
|
||||
/// GET /index.php/avatar/{user}/{size}
|
||||
///
|
||||
/// Returns an SVG avatar with the user's initials on a colored background.
|
||||
pub async fn handle_avatar(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Path((username, size)): Path<(String, u32)>,
|
||||
) -> Response {
|
||||
let size = size.clamp(16, 1024);
|
||||
let initials = extract_initials(&username);
|
||||
let color = pick_color(&username);
|
||||
let font_size = (size as f32 * 0.45) as u32;
|
||||
|
||||
let safe_initials = xml_escape(&initials);
|
||||
|
||||
let svg = format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{s}" height="{s}" viewBox="0 0 {s} {s}">
|
||||
<rect width="{s}" height="{s}" rx="{r}" fill="{c}"/>
|
||||
<text x="50%" y="50%" dy="0.36em" fill="#fff" font-family="-apple-system,BlinkMacSystemFont,sans-serif" font-size="{fs}" font-weight="600" text-anchor="middle">{i}</text>
|
||||
</svg>"##,
|
||||
s = size,
|
||||
r = size / 2,
|
||||
c = color,
|
||||
fs = font_size,
|
||||
i = safe_initials,
|
||||
);
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/svg+xml"),
|
||||
(header::CACHE_CONTROL, "public, max-age=86400, immutable"),
|
||||
(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
"default-src 'none'; style-src 'unsafe-inline'",
|
||||
),
|
||||
],
|
||||
svg,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// Escape XML special characters to prevent XSS in SVG output.
|
||||
fn xml_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn extract_initials(username: &str) -> String {
|
||||
let parts: Vec<&str> = username.split_whitespace().collect();
|
||||
match parts.len() {
|
||||
0 => "?".to_string(),
|
||||
1 => parts[0]
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap_or('?')
|
||||
.to_uppercase()
|
||||
.to_string(),
|
||||
_ => {
|
||||
let first = parts[0].chars().next().unwrap_or('?');
|
||||
let last = parts[parts.len() - 1].chars().next().unwrap_or('?');
|
||||
format!("{}{}", first.to_uppercase(), last.to_uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_color(username: &str) -> &'static str {
|
||||
const PALETTE: [&str; 10] = [
|
||||
"#0082c9", "#e9322d", "#2d8a0f", "#c37200", "#6c2d9e", "#007a87", "#b02e7c", "#465a64",
|
||||
"#a65d00", "#3b5998",
|
||||
];
|
||||
let hash: u32 = username
|
||||
.bytes()
|
||||
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
|
||||
PALETTE[(hash as usize) % PALETTE.len()]
|
||||
}
|
||||
Reference in New Issue
Block a user