style: cargo fmt --all
This commit is contained in:
@@ -197,10 +197,7 @@ async fn login(
|
||||
auth_response.expires_in,
|
||||
state.core.config.auth.refresh_token_expiry_secs,
|
||||
);
|
||||
cookie_auth::append_csrf_cookie(
|
||||
response.headers_mut(),
|
||||
auth_response.expires_in,
|
||||
);
|
||||
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
|
||||
Ok(response)
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -253,10 +250,7 @@ async fn refresh_token(
|
||||
auth_response.expires_in,
|
||||
state.core.config.auth.refresh_token_expiry_secs,
|
||||
);
|
||||
cookie_auth::append_csrf_cookie(
|
||||
response.headers_mut(),
|
||||
auth_response.expires_in,
|
||||
);
|
||||
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -522,9 +516,6 @@ async fn oidc_exchange(
|
||||
auth_response.expires_in,
|
||||
state.core.config.auth.refresh_token_expiry_secs,
|
||||
);
|
||||
cookie_auth::append_csrf_cookie(
|
||||
response.headers_mut(),
|
||||
auth_response.expires_in,
|
||||
);
|
||||
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -52,11 +52,10 @@ pub fn caldav_routes() -> Router<Arc<AppState>> {
|
||||
/// Creates RFC 6764 well-known discovery routes.
|
||||
/// These are public (no auth) and simply redirect to the CalDAV root.
|
||||
pub fn well_known_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/.well-known/caldav",
|
||||
axum::routing::any(handle_well_known_caldav),
|
||||
)
|
||||
Router::new().route(
|
||||
"/.well-known/caldav",
|
||||
axum::routing::any(handle_well_known_caldav),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_well_known_caldav() -> Response<Body> {
|
||||
@@ -114,13 +113,9 @@ fn extract_caldav_path(uri_path: &str) -> String {
|
||||
} else if uri_path.ends_with("/caldav") {
|
||||
""
|
||||
} else {
|
||||
uri_path
|
||||
.trim_start_matches('/')
|
||||
.trim_end_matches('/')
|
||||
uri_path.trim_start_matches('/').trim_end_matches('/')
|
||||
};
|
||||
percent_decode_str(encoded)
|
||||
.decode_utf8_lossy()
|
||||
.into_owned()
|
||||
percent_decode_str(encoded).decode_utf8_lossy().into_owned()
|
||||
}
|
||||
|
||||
// ─── Helper: extract user from request ───────────────────────────────
|
||||
@@ -198,9 +193,7 @@ async fn handle_propfind(
|
||||
calendar_service
|
||||
.list_my_calendars(&user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
||||
})?
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list calendars: {}", e)))?
|
||||
};
|
||||
|
||||
let base_href = "/caldav/";
|
||||
@@ -221,9 +214,7 @@ async fn handle_propfind(
|
||||
.unwrap())
|
||||
} else if path.starts_with("principals/") || path == "principals" {
|
||||
// Principal resource — return user principal properties
|
||||
let username = path
|
||||
.strip_prefix("principals/")
|
||||
.unwrap_or(&user.username);
|
||||
let username = path.strip_prefix("principals/").unwrap_or(&user.username);
|
||||
let username = if username.is_empty() {
|
||||
&user.username
|
||||
} else {
|
||||
@@ -255,9 +246,7 @@ async fn handle_propfind(
|
||||
|
||||
if parts.len() == 1 {
|
||||
// Single path segment: try as calendar ID first, fall back to user home
|
||||
let calendar_result = calendar_service
|
||||
.get_calendar(first_segment, &user.id)
|
||||
.await;
|
||||
let calendar_result = calendar_service.get_calendar(first_segment, &user.id).await;
|
||||
|
||||
if let Ok(calendar) = calendar_result {
|
||||
// Valid calendar ID — return calendar collection
|
||||
@@ -281,9 +270,7 @@ async fn handle_propfind(
|
||||
base_href,
|
||||
&depth,
|
||||
)
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to generate XML: {}", e))
|
||||
})?;
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
@@ -293,12 +280,13 @@ async fn handle_propfind(
|
||||
} else {
|
||||
// Not a calendar ID — treat as user calendar home (e.g. /caldav/{username}/)
|
||||
// List all calendars for this user
|
||||
let calendars = calendar_service
|
||||
.list_my_calendars(&user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
||||
})?;
|
||||
let calendars =
|
||||
calendar_service
|
||||
.list_my_calendars(&user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
||||
})?;
|
||||
|
||||
let base_href = &format!("/caldav/{}/", first_segment);
|
||||
let mut response_body = Vec::new();
|
||||
@@ -309,9 +297,7 @@ async fn handle_propfind(
|
||||
&propfind_request,
|
||||
base_href,
|
||||
)
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to generate XML: {}", e))
|
||||
})?;
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
@@ -324,9 +310,7 @@ async fn handle_propfind(
|
||||
let rest = parts[1];
|
||||
|
||||
// Check if first_segment is a valid calendar ID
|
||||
let calendar_result = calendar_service
|
||||
.get_calendar(first_segment, &user.id)
|
||||
.await;
|
||||
let calendar_result = calendar_service.get_calendar(first_segment, &user.id).await;
|
||||
|
||||
let (calendar_id, event_path) = if calendar_result.is_ok() {
|
||||
// first_segment is a calendar ID, rest is event path
|
||||
@@ -341,9 +325,7 @@ async fn handle_propfind(
|
||||
let cal = calendar_service
|
||||
.get_calendar(sub_parts[0], &user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::not_found(format!("Calendar not found: {}", e))
|
||||
})?;
|
||||
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
|
||||
|
||||
let events = if depth != "0" {
|
||||
calendar_service
|
||||
@@ -354,8 +336,7 @@ async fn handle_propfind(
|
||||
vec![]
|
||||
};
|
||||
|
||||
let base_href =
|
||||
&format!("/caldav/{}/{}/", first_segment, sub_parts[0]);
|
||||
let base_href = &format!("/caldav/{}/{}/", first_segment, sub_parts[0]);
|
||||
let mut response_body = Vec::new();
|
||||
|
||||
CalDavAdapter::generate_calendar_collection_propfind(
|
||||
@@ -387,16 +368,12 @@ async fn handle_propfind(
|
||||
let events = calendar_service
|
||||
.list_events(calendar_id, None, None, &user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to list events: {}", e))
|
||||
})?;
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
||||
|
||||
let event = events
|
||||
.iter()
|
||||
.find(|e| e.ical_uid == ical_uid)
|
||||
.ok_or_else(|| {
|
||||
AppError::not_found(format!("Event not found: {}", ical_uid))
|
||||
})?;
|
||||
.ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?;
|
||||
|
||||
let base_href = &format!("/caldav/{}/", calendar_id);
|
||||
let report_type = CalDavReportType::CalendarMultiget {
|
||||
@@ -411,9 +388,7 @@ async fn handle_propfind(
|
||||
&report_type,
|
||||
base_href,
|
||||
)
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to generate XML: {}", e))
|
||||
})?;
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to generate XML: {}", e)))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::MULTI_STATUS)
|
||||
@@ -718,7 +693,10 @@ fn generate_event_ical(event: &crate::application::dtos::calendar_dto::CalendarE
|
||||
}
|
||||
|
||||
/// Writes a VEVENT block directly into `buf` — zero intermediate allocations.
|
||||
fn write_vevent(buf: &mut String, event: &crate::application::dtos::calendar_dto::CalendarEventDto) {
|
||||
fn write_vevent(
|
||||
buf: &mut String,
|
||||
event: &crate::application::dtos::calendar_dto::CalendarEventDto,
|
||||
) {
|
||||
let _ = write!(
|
||||
buf,
|
||||
"BEGIN:VEVENT\r\nUID:{}\r\nSUMMARY:{}\r\nDTSTART:{}\r\nDTEND:{}\r\n",
|
||||
|
||||
@@ -99,9 +99,7 @@ fn extract_carddav_path(uri_path: &str) -> String {
|
||||
} else if uri_path.ends_with("/carddav") {
|
||||
""
|
||||
} else {
|
||||
uri_path
|
||||
.trim_start_matches('/')
|
||||
.trim_end_matches('/')
|
||||
uri_path.trim_start_matches('/').trim_end_matches('/')
|
||||
};
|
||||
percent_encoding::percent_decode_str(encoded)
|
||||
.decode_utf8_lossy()
|
||||
|
||||
@@ -1,227 +1,225 @@
|
||||
//! HTTP handlers for OAuth 2.0 Device Authorization Grant (RFC 8628).
|
||||
//!
|
||||
//! Endpoints:
|
||||
//! POST /api/auth/device/authorize — Client starts the device flow (public)
|
||||
//! GET /api/auth/device/verify — Check user_code validity (authenticated)
|
||||
//! POST /api/auth/device/verify — User approves/denies (authenticated)
|
||||
//! POST /api/auth/device/token — Client polls for tokens (public)
|
||||
//! GET /api/auth/device/devices — List user's authorized devices (authenticated)
|
||||
//! DELETE /api/auth/device/devices/{id} — Revoke a device (authenticated)
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Json, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::dtos::device_auth_dto::*;
|
||||
use crate::application::services::device_auth_service::DeviceAuthService;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Create the device auth router.
|
||||
///
|
||||
/// Public endpoints (no auth middleware): authorize, token
|
||||
/// Protected endpoints (behind auth middleware): verify (GET+POST), devices
|
||||
pub fn device_auth_public_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// Client-facing endpoints (no auth needed — the client doesn't have tokens yet)
|
||||
.route("/authorize", post(device_authorize))
|
||||
.route("/token", post(device_token))
|
||||
}
|
||||
|
||||
pub fn device_auth_protected_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// User-facing endpoints (require valid session)
|
||||
.route("/verify", get(device_verify_info))
|
||||
.route("/verify", post(device_verify_action))
|
||||
.route("/devices", get(list_devices))
|
||||
.route("/devices/{id}", delete(revoke_device))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST /api/auth/device/authorize — Client initiates the device flow
|
||||
// ============================================================================
|
||||
|
||||
/// Client sends: `{ "client_name": "rclone", "scope": "webdav" }`
|
||||
/// Server returns: device_code, user_code, verification_uri, etc.
|
||||
async fn device_authorize(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<DeviceAuthorizeRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
let response = device_service.initiate(body).await.map_err(|e| {
|
||||
tracing::error!("Device authorize failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST /api/auth/device/token — Client polls for tokens
|
||||
// ============================================================================
|
||||
|
||||
/// Client sends: `{ "device_code": "...", "grant_type": "urn:ietf:params:oauth:grant-type:device_code" }`
|
||||
/// Returns tokens on success, or RFC 8628 error codes while pending.
|
||||
async fn device_token(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<DeviceTokenRequestDto>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let device_service = match get_device_service(&state) {
|
||||
Ok(svc) => svc,
|
||||
Err(e) => return Err(e.into_response()),
|
||||
};
|
||||
|
||||
// Validate grant_type if provided (RFC compliance)
|
||||
if !body.grant_type.is_empty()
|
||||
&& body.grant_type != "urn:ietf:params:oauth:grant-type:device_code"
|
||||
{
|
||||
let error_body = serde_json::json!({
|
||||
"error": "unsupported_grant_type",
|
||||
"error_description": "grant_type must be urn:ietf:params:oauth:grant-type:device_code"
|
||||
});
|
||||
return Err((StatusCode::BAD_REQUEST, Json(error_body)).into_response());
|
||||
}
|
||||
|
||||
match device_service.poll(&body.device_code).await {
|
||||
Ok(tokens) => Ok((StatusCode::OK, Json(tokens)).into_response()),
|
||||
Err(poll_err) => {
|
||||
let status = StatusCode::from_u16(poll_err.http_status())
|
||||
.unwrap_or(StatusCode::BAD_REQUEST);
|
||||
let error_body = serde_json::json!({
|
||||
"error": poll_err.error_code(),
|
||||
"error_description": poll_err.description()
|
||||
});
|
||||
Err((status, Json(error_body)).into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET /api/auth/device/verify?code=ABCD-1234 — Check if user_code is valid
|
||||
// ============================================================================
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct VerifyQuery {
|
||||
#[serde(default)]
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
async fn device_verify_info(
|
||||
State(state): State<Arc<AppState>>,
|
||||
_auth_user: AuthUser,
|
||||
Query(query): Query<VerifyQuery>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
let info = device_service
|
||||
.verify_user_code(&query.code)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Device verify lookup failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::OK, Json(info)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST /api/auth/device/verify — User approves or denies
|
||||
// ============================================================================
|
||||
|
||||
async fn device_verify_action(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(body): Json<DeviceVerifyRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
match body.action.to_lowercase().as_str() {
|
||||
"approve" | "allow" | "accept" => {
|
||||
device_service
|
||||
.approve(&body.user_code, &auth_user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Device approve failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "approved" })),
|
||||
))
|
||||
}
|
||||
"deny" | "reject" | "cancel" => {
|
||||
device_service.deny(&body.user_code).await.map_err(|e| {
|
||||
tracing::error!("Device deny failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "denied" })),
|
||||
))
|
||||
}
|
||||
_ => Err(AppError::bad_request(
|
||||
"action must be 'approve' or 'deny'",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET /api/auth/device/devices — List user's authorized devices
|
||||
// ============================================================================
|
||||
|
||||
async fn list_devices(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
let devices = device_service
|
||||
.list_user_devices(&auth_user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("List devices failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::OK, Json(devices)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DELETE /api/auth/device/devices/{id} — Revoke a device authorization
|
||||
// ============================================================================
|
||||
|
||||
async fn revoke_device(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(device_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
device_service
|
||||
.revoke_device(&device_id, &auth_user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Revoke device failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper
|
||||
// ============================================================================
|
||||
|
||||
fn get_device_service(state: &AppState) -> Result<&Arc<DeviceAuthService>, AppError> {
|
||||
state
|
||||
.device_auth_service
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Device authorization service not configured"))
|
||||
}
|
||||
//! HTTP handlers for OAuth 2.0 Device Authorization Grant (RFC 8628).
|
||||
//!
|
||||
//! Endpoints:
|
||||
//! POST /api/auth/device/authorize — Client starts the device flow (public)
|
||||
//! GET /api/auth/device/verify — Check user_code validity (authenticated)
|
||||
//! POST /api/auth/device/verify — User approves/denies (authenticated)
|
||||
//! POST /api/auth/device/token — Client polls for tokens (public)
|
||||
//! GET /api/auth/device/devices — List user's authorized devices (authenticated)
|
||||
//! DELETE /api/auth/device/devices/{id} — Revoke a device (authenticated)
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Json, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::dtos::device_auth_dto::*;
|
||||
use crate::application::services::device_auth_service::DeviceAuthService;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Create the device auth router.
|
||||
///
|
||||
/// Public endpoints (no auth middleware): authorize, token
|
||||
/// Protected endpoints (behind auth middleware): verify (GET+POST), devices
|
||||
pub fn device_auth_public_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// Client-facing endpoints (no auth needed — the client doesn't have tokens yet)
|
||||
.route("/authorize", post(device_authorize))
|
||||
.route("/token", post(device_token))
|
||||
}
|
||||
|
||||
pub fn device_auth_protected_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// User-facing endpoints (require valid session)
|
||||
.route("/verify", get(device_verify_info))
|
||||
.route("/verify", post(device_verify_action))
|
||||
.route("/devices", get(list_devices))
|
||||
.route("/devices/{id}", delete(revoke_device))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST /api/auth/device/authorize — Client initiates the device flow
|
||||
// ============================================================================
|
||||
|
||||
/// Client sends: `{ "client_name": "rclone", "scope": "webdav" }`
|
||||
/// Server returns: device_code, user_code, verification_uri, etc.
|
||||
async fn device_authorize(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<DeviceAuthorizeRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
let response = device_service.initiate(body).await.map_err(|e| {
|
||||
tracing::error!("Device authorize failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST /api/auth/device/token — Client polls for tokens
|
||||
// ============================================================================
|
||||
|
||||
/// Client sends: `{ "device_code": "...", "grant_type": "urn:ietf:params:oauth:grant-type:device_code" }`
|
||||
/// Returns tokens on success, or RFC 8628 error codes while pending.
|
||||
async fn device_token(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<DeviceTokenRequestDto>,
|
||||
) -> Result<impl IntoResponse, impl IntoResponse> {
|
||||
let device_service = match get_device_service(&state) {
|
||||
Ok(svc) => svc,
|
||||
Err(e) => return Err(e.into_response()),
|
||||
};
|
||||
|
||||
// Validate grant_type if provided (RFC compliance)
|
||||
if !body.grant_type.is_empty()
|
||||
&& body.grant_type != "urn:ietf:params:oauth:grant-type:device_code"
|
||||
{
|
||||
let error_body = serde_json::json!({
|
||||
"error": "unsupported_grant_type",
|
||||
"error_description": "grant_type must be urn:ietf:params:oauth:grant-type:device_code"
|
||||
});
|
||||
return Err((StatusCode::BAD_REQUEST, Json(error_body)).into_response());
|
||||
}
|
||||
|
||||
match device_service.poll(&body.device_code).await {
|
||||
Ok(tokens) => Ok((StatusCode::OK, Json(tokens)).into_response()),
|
||||
Err(poll_err) => {
|
||||
let status =
|
||||
StatusCode::from_u16(poll_err.http_status()).unwrap_or(StatusCode::BAD_REQUEST);
|
||||
let error_body = serde_json::json!({
|
||||
"error": poll_err.error_code(),
|
||||
"error_description": poll_err.description()
|
||||
});
|
||||
Err((status, Json(error_body)).into_response())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET /api/auth/device/verify?code=ABCD-1234 — Check if user_code is valid
|
||||
// ============================================================================
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct VerifyQuery {
|
||||
#[serde(default)]
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
async fn device_verify_info(
|
||||
State(state): State<Arc<AppState>>,
|
||||
_auth_user: AuthUser,
|
||||
Query(query): Query<VerifyQuery>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
let info = device_service
|
||||
.verify_user_code(&query.code)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Device verify lookup failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::OK, Json(info)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POST /api/auth/device/verify — User approves or denies
|
||||
// ============================================================================
|
||||
|
||||
async fn device_verify_action(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(body): Json<DeviceVerifyRequestDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
match body.action.to_lowercase().as_str() {
|
||||
"approve" | "allow" | "accept" => {
|
||||
device_service
|
||||
.approve(&body.user_code, &auth_user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Device approve failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "approved" })),
|
||||
))
|
||||
}
|
||||
"deny" | "reject" | "cancel" => {
|
||||
device_service.deny(&body.user_code).await.map_err(|e| {
|
||||
tracing::error!("Device deny failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "status": "denied" })),
|
||||
))
|
||||
}
|
||||
_ => Err(AppError::bad_request("action must be 'approve' or 'deny'")),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET /api/auth/device/devices — List user's authorized devices
|
||||
// ============================================================================
|
||||
|
||||
async fn list_devices(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
let devices = device_service
|
||||
.list_user_devices(&auth_user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("List devices failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok((StatusCode::OK, Json(devices)))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DELETE /api/auth/device/devices/{id} — Revoke a device authorization
|
||||
// ============================================================================
|
||||
|
||||
async fn revoke_device(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Path(device_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let device_service = get_device_service(&state)?;
|
||||
|
||||
device_service
|
||||
.revoke_device(&device_id, &auth_user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Revoke device failed: {}", e);
|
||||
AppError::from(e)
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper
|
||||
// ============================================================================
|
||||
|
||||
fn get_device_service(state: &AppState) -> Result<&Arc<DeviceAuthService>, AppError> {
|
||||
state
|
||||
.device_auth_service
|
||||
.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Device authorization service not configured"))
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ pub mod admin_handler;
|
||||
pub mod app_password_handler;
|
||||
pub mod auth_handler;
|
||||
pub mod batch_handler;
|
||||
pub mod device_auth_handler;
|
||||
pub mod caldav_handler;
|
||||
pub mod carddav_handler;
|
||||
pub mod chunked_upload_handler;
|
||||
pub mod dedup_handler;
|
||||
pub mod device_auth_handler;
|
||||
pub mod favorites_handler;
|
||||
pub mod file_handler;
|
||||
pub mod folder_handler;
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::common::di::AppState;
|
||||
use crate::infrastructure::services::path_resolver_service::ResolvedResource;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC, AsciiSet};
|
||||
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Characters that MUST NOT be percent-encoded inside a URI path segment.
|
||||
@@ -115,9 +115,7 @@ fn extract_webdav_path(uri: &axum::http::Uri) -> String {
|
||||
trimmed.trim_end_matches('/')
|
||||
};
|
||||
// Decode percent-encoded characters (e.g. %20 → space)
|
||||
percent_decode_str(encoded)
|
||||
.decode_utf8_lossy()
|
||||
.into_owned()
|
||||
percent_decode_str(encoded).decode_utf8_lossy().into_owned()
|
||||
}
|
||||
|
||||
async fn handle_webdav_methods_root(
|
||||
@@ -324,8 +322,13 @@ async fn handle_propfind(
|
||||
let mut xml_writer = Writer::new(&mut buf);
|
||||
WebDavAdapter::write_multistatus_start(&mut xml_writer)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
WebDavAdapter::write_file_entry(&mut xml_writer, &file, &propfind_request, &base_href)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
WebDavAdapter::write_file_entry(
|
||||
&mut xml_writer,
|
||||
&file,
|
||||
&propfind_request,
|
||||
&base_href,
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
WebDavAdapter::write_multistatus_end(&mut xml_writer)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
}
|
||||
@@ -358,8 +361,13 @@ async fn handle_propfind(
|
||||
let mut xml_writer = Writer::new(&mut buf);
|
||||
WebDavAdapter::write_multistatus_start(&mut xml_writer)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
WebDavAdapter::write_file_entry(&mut xml_writer, &file, &propfind_request, &base_href)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
WebDavAdapter::write_file_entry(
|
||||
&mut xml_writer,
|
||||
&file,
|
||||
&propfind_request,
|
||||
&base_href,
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
WebDavAdapter::write_multistatus_end(&mut xml_writer)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
}
|
||||
@@ -910,13 +918,17 @@ async fn handle_delete(
|
||||
folder_service
|
||||
.delete_folder(&folder.id, caller_id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to delete folder: {}", e))
|
||||
})?;
|
||||
}
|
||||
Ok(ResolvedResource::File(file)) => {
|
||||
file_management_service
|
||||
.delete_file(&file.id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to delete file: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to delete file: {}", e))
|
||||
})?;
|
||||
}
|
||||
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", path))),
|
||||
}
|
||||
@@ -1002,8 +1014,14 @@ async fn handle_move(
|
||||
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
||||
resolver.exists(&destination_path).await.unwrap_or(false)
|
||||
} else {
|
||||
folder_service.get_folder_by_path(&destination_path).await.is_ok()
|
||||
|| file_retrieval_service.get_file_by_path(&destination_path).await.is_ok()
|
||||
folder_service
|
||||
.get_folder_by_path(&destination_path)
|
||||
.await
|
||||
.is_ok()
|
||||
|| file_retrieval_service
|
||||
.get_file_by_path(&destination_path)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
if dest_exists {
|
||||
return Err(AppError::precondition_failed(
|
||||
@@ -1044,7 +1062,9 @@ async fn handle_move(
|
||||
folder.owner_id.as_deref().unwrap_or("webdav"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to move folder: {}", e))
|
||||
})?;
|
||||
|
||||
if folder.name != dest_folder_name {
|
||||
let rename_dto = crate::application::dtos::folder_dto::RenameFolderDto {
|
||||
@@ -1057,7 +1077,9 @@ async fn handle_move(
|
||||
folder.owner_id.as_deref().unwrap_or("webdav"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(ResolvedResource::File(file)) => {
|
||||
@@ -1080,16 +1102,25 @@ async fn handle_move(
|
||||
file_management_service
|
||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to move file: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to move file: {}", e))
|
||||
})?;
|
||||
}
|
||||
if file.name != dest_filename {
|
||||
file_management_service
|
||||
.rename_file(&file.id, dest_filename)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to rename file: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to rename file: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", source_path))),
|
||||
Err(_) => {
|
||||
return Err(AppError::not_found(format!(
|
||||
"Resource not found: {}",
|
||||
source_path
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: legacy double-query path
|
||||
@@ -1137,13 +1168,17 @@ async fn handle_move(
|
||||
folder.owner_id.as_deref().unwrap_or("webdav"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
let file = file_retrieval_service
|
||||
.get_file_by_path(&source_path)
|
||||
.await
|
||||
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
|
||||
.map_err(|_e| {
|
||||
AppError::not_found(format!("Resource not found: {}", source_path))
|
||||
})?;
|
||||
|
||||
let dest_filename = destination_path
|
||||
.split('/')
|
||||
@@ -1170,7 +1205,9 @@ async fn handle_move(
|
||||
file_management_service
|
||||
.rename_file(&file.id, dest_filename)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to rename file: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to rename file: {}", e))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1240,8 +1277,14 @@ async fn handle_copy(
|
||||
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
||||
resolver.exists(&destination_path).await.unwrap_or(false)
|
||||
} else {
|
||||
folder_service.get_folder_by_path(&destination_path).await.is_ok()
|
||||
|| file_retrieval_service.get_file_by_path(&destination_path).await.is_ok()
|
||||
folder_service
|
||||
.get_folder_by_path(&destination_path)
|
||||
.await
|
||||
.is_ok()
|
||||
|| file_retrieval_service
|
||||
.get_file_by_path(&destination_path)
|
||||
.await
|
||||
.is_ok()
|
||||
};
|
||||
if dest_exists {
|
||||
return Err(AppError::precondition_failed(
|
||||
@@ -1296,7 +1339,10 @@ async fn handle_copy(
|
||||
.create_folder(create_dto)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to create destination folder: {}", e))
|
||||
AppError::internal_error(format!(
|
||||
"Failed to create destination folder: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -1322,7 +1368,12 @@ async fn handle_copy(
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to copy file: {}", e)))?;
|
||||
}
|
||||
Err(_) => return Err(AppError::not_found(format!("Resource not found: {}", source_path))),
|
||||
Err(_) => {
|
||||
return Err(AppError::not_found(format!(
|
||||
"Resource not found: {}",
|
||||
source_path
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: legacy double-query path
|
||||
@@ -1371,14 +1422,19 @@ async fn handle_copy(
|
||||
.create_folder(create_dto)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::internal_error(format!("Failed to create destination folder: {}", e))
|
||||
AppError::internal_error(format!(
|
||||
"Failed to create destination folder: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
let file = file_retrieval_service
|
||||
.get_file_by_path(&source_path)
|
||||
.await
|
||||
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", source_path)))?;
|
||||
.map_err(|_e| {
|
||||
AppError::not_found(format!("Resource not found: {}", source_path))
|
||||
})?;
|
||||
|
||||
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
||||
&destination_path[..idx]
|
||||
|
||||
Reference in New Issue
Block a user