feat(frontend): i18n expansion, admin/profile i18n, grid/list view fix, empty state
- Add 5 new locales (hi, ar, ru, ja, ko) — now 14 total - Admin panel: 117 i18n keys, confirm modal, animated tabs, no inline handlers - Profile page: 58 i18n keys with data-i18n attributes - Fix i18n safeT() shadowing bug and translationsLoaded timing - Fix grid/list view: list header no longer shows in grid mode on login - Fix classList.toggle hidden sync for view switching across all nav functions - Revert .hidden important that broke login page rendering - Add files empty state (no_files + empty_hint) with translations - Fix language selector dropdown scroll and styling - Fix admin panel scroll with sticky tabs
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
use crate::application::dtos::app_password_dto::CreateAppPasswordRequestDto;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
use axum::extract::State;
|
||||
use axum::routing::{delete, get, post};
|
||||
use axum::{Json, Router};
|
||||
@@ -26,7 +26,7 @@ pub fn app_password_routes() -> Router<Arc<AppState>> {
|
||||
/// Returns the plain-text password ONCE. The user must copy it immediately.
|
||||
async fn create_app_password(
|
||||
State(state): State<Arc<AppState>>,
|
||||
axum::Extension(user): axum::Extension<CurrentUser>,
|
||||
user: AuthUser,
|
||||
Json(request): Json<CreateAppPasswordRequestDto>,
|
||||
) -> Result<Json<crate::application::dtos::app_password_dto::AppPasswordCreatedResponseDto>, AppError>
|
||||
{
|
||||
@@ -48,7 +48,7 @@ async fn create_app_password(
|
||||
/// Never returns plain-text passwords (only prefix + metadata).
|
||||
async fn list_app_passwords(
|
||||
State(state): State<Arc<AppState>>,
|
||||
axum::Extension(user): axum::Extension<CurrentUser>,
|
||||
user: AuthUser,
|
||||
) -> Result<Json<crate::application::dtos::app_password_dto::AppPasswordListResponseDto>, AppError>
|
||||
{
|
||||
let service = state
|
||||
@@ -64,7 +64,7 @@ async fn list_app_passwords(
|
||||
/// DELETE /api/auth/app-passwords/:id — Revoke an app password.
|
||||
async fn revoke_app_password(
|
||||
State(state): State<Arc<AppState>>,
|
||||
axum::Extension(user): axum::Extension<CurrentUser>,
|
||||
user: AuthUser,
|
||||
axum::extract::Path(id): axum::extract::Path<String>,
|
||||
) -> Result<Json<crate::application::dtos::app_password_dto::AppPasswordRevokeResponseDto>, AppError>
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::application::ports::calendar_ports::CalendarUseCase;
|
||||
use crate::application::services::calendar_service::CalendarService;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
|
||||
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||
|
||||
@@ -138,10 +138,11 @@ fn reject_path_traversal(path: &str) -> Result<(), AppError> {
|
||||
|
||||
// ─── Helper: extract user from request ───────────────────────────────
|
||||
|
||||
fn extract_user(req: &Request<Body>) -> Result<CurrentUser, AppError> {
|
||||
fn extract_user(req: &Request<Body>) -> Result<AuthUser, AppError> {
|
||||
req.extensions()
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|arc| (**arc).clone())
|
||||
.cloned()
|
||||
.map(AuthUser)
|
||||
.ok_or_else(|| AppError::unauthorized("Authentication required"))
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCas
|
||||
use crate::common::di::AppState;
|
||||
use crate::infrastructure::adapters::contact_storage_adapter::ContactStorageAdapter;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
|
||||
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||
|
||||
@@ -126,10 +126,11 @@ fn reject_path_traversal(path: &str) -> Result<(), AppError> {
|
||||
|
||||
// ─── Helper: extract user from request ───────────────────────────────
|
||||
|
||||
fn extract_user(req: &Request<Body>) -> Result<CurrentUser, AppError> {
|
||||
fn extract_user(req: &Request<Body>) -> Result<AuthUser, AppError> {
|
||||
req.extensions()
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|arc| (**arc).clone())
|
||||
.cloned()
|
||||
.map(AuthUser)
|
||||
.ok_or_else(|| AppError::unauthorized("Authentication required"))
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::application::services::folder_service::FolderService;
|
||||
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 crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -93,10 +93,11 @@ const PROPFIND_BATCH_SIZE: i64 = 500;
|
||||
/// Every mutating or data-returning WebDAV handler **must** call this so
|
||||
/// that the real `user.id` is available for ownership checks and for the
|
||||
/// user-scoped `PathResolverService` methods.
|
||||
fn extract_user(req: &Request<Body>) -> Result<CurrentUser, AppError> {
|
||||
fn extract_user(req: &Request<Body>) -> Result<AuthUser, AppError> {
|
||||
req.extensions()
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|arc| (**arc).clone())
|
||||
.cloned()
|
||||
.map(AuthUser)
|
||||
.ok_or_else(|| AppError::unauthorized("Authentication required"))
|
||||
}
|
||||
|
||||
|
||||
@@ -413,14 +413,12 @@ async fn authorize_wopi_access<S: FileRetrievalUseCase>(
|
||||
/// This endpoint is behind normal auth middleware. The authenticated user
|
||||
/// requests a WOPI session for a specific file.
|
||||
pub async fn get_editor_url(
|
||||
AuthUser {
|
||||
id: user_id,
|
||||
username,
|
||||
..
|
||||
}: AuthUser,
|
||||
auth_user: AuthUser,
|
||||
Query(params): Query<EditorUrlParams>,
|
||||
State(state): State<WopiState>,
|
||||
) -> Response {
|
||||
let user_id = auth_user.id;
|
||||
let username = &auth_user.username;
|
||||
// Verify the caller owns the file (SQL-level check, no existence leak).
|
||||
let (file, can_write) = match authorize_wopi_access(
|
||||
state.app_state.applications.file_retrieval_service.as_ref(),
|
||||
|
||||
@@ -20,12 +20,18 @@ use crate::application::ports::auth_ports::TokenServicePort;
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CookieAuthenticated;
|
||||
|
||||
// Structure for use in Axum extractors
|
||||
// Newtype over Arc<CurrentUser> for zero-allocation extraction.
|
||||
// `Deref<Target = CurrentUser>` lets handlers access `.id`, `.username`,
|
||||
// `.email`, `.role` transparently — no signature changes needed.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthUser {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub role: String,
|
||||
pub struct AuthUser(pub Arc<CurrentUser>);
|
||||
|
||||
impl std::ops::Deref for AuthUser {
|
||||
type Target = CurrentUser;
|
||||
#[inline]
|
||||
fn deref(&self) -> &CurrentUser {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Reusable extractor that gets the user_id of the authenticated user.
|
||||
@@ -38,7 +44,8 @@ pub struct AuthUser {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CurrentUserId(pub Uuid);
|
||||
|
||||
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
|
||||
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers.
|
||||
// Cost: 1 atomic increment (~1 ns) instead of 3 String clones (~100 ns + 3 mallocs).
|
||||
impl<S> FromRequestParts<S> for AuthUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
@@ -49,29 +56,8 @@ where
|
||||
parts
|
||||
.extensions
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|cu| AuthUser {
|
||||
id: cu.id,
|
||||
username: cu.username.clone(),
|
||||
role: cu.role.clone(),
|
||||
})
|
||||
.ok_or(AuthError::UserNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement FromRequestParts for CurrentUser — full user extractor from extensions
|
||||
// The middleware inserts Arc<CurrentUser>; this extractor cheaply clones the Arc
|
||||
// (~1 ns atomic increment) instead of deep-cloning 4 Strings (~60-100 ns).
|
||||
impl<S> FromRequestParts<S> for CurrentUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extensions
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|arc| (**arc).clone())
|
||||
.cloned()
|
||||
.map(AuthUser)
|
||||
.ok_or(AuthError::UserNotFound)
|
||||
}
|
||||
}
|
||||
@@ -113,27 +99,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional auth user extractor – never fails.
|
||||
/// Yields `Some(AuthUser)` when auth middleware ran, `None` otherwise.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OptionalAuthUser(pub Option<AuthUser>);
|
||||
|
||||
impl<S> FromRequestParts<S> for OptionalAuthUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(OptionalAuthUser(parts.extensions.get::<Arc<CurrentUser>>().map(
|
||||
|cu| AuthUser {
|
||||
id: cu.id,
|
||||
username: cu.username.clone(),
|
||||
role: cu.role.clone(),
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// Error for authentication operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
|
||||
use crate::application::ports::inbound::SearchUseCase;
|
||||
use crate::application::ports::storage_ports::StorageUsagePort;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Build an OCS success response with the given statuscode and data.
|
||||
fn ocs_ok(statuscode: u16, data: serde_json::Value) -> serde_json::Value {
|
||||
@@ -45,7 +45,7 @@ pub async fn handle_capabilities_v2(State(state): State<Arc<AppState>>) -> Respo
|
||||
Json(payload).into_response()
|
||||
}
|
||||
|
||||
pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: CurrentUser) -> Response {
|
||||
pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: AuthUser) -> Response {
|
||||
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
|
||||
Some(service) => match service.get_user_storage_info(user.id).await {
|
||||
Ok((used, total)) => (used, total),
|
||||
@@ -86,7 +86,7 @@ pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: CurrentU
|
||||
pub async fn handle_user_provisioning_v1(
|
||||
state: State<Arc<AppState>>,
|
||||
path: Path<String>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
) -> Response {
|
||||
user_provisioning_response(state, path, user, 1).await
|
||||
}
|
||||
@@ -95,7 +95,7 @@ pub async fn handle_user_provisioning_v1(
|
||||
pub async fn handle_user_provisioning_v2(
|
||||
state: State<Arc<AppState>>,
|
||||
path: Path<String>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
) -> Response {
|
||||
user_provisioning_response(state, path, user, 2).await
|
||||
}
|
||||
@@ -105,7 +105,7 @@ pub async fn handle_user_provisioning_v2(
|
||||
async fn user_provisioning_response(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(userid): Path<String>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
ocs_version: u8,
|
||||
) -> Response {
|
||||
let statuscode = if ocs_version == 1 { 100 } else { 200 };
|
||||
@@ -197,7 +197,7 @@ async fn user_provisioning_response(
|
||||
|
||||
pub async fn handle_revoke_apppassword(
|
||||
State(state): State<Arc<AppState>>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let nextcloud = match state.nextcloud.as_ref() {
|
||||
@@ -249,7 +249,7 @@ pub async fn handle_recommendations() -> Response {
|
||||
/// this endpoint and expects a well-formed OCS response rather than a 404.
|
||||
pub async fn handle_sharees_search(
|
||||
State(state): State<Arc<AppState>>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
axum::extract::Query(params): axum::extract::Query<ShareeSearchParams>,
|
||||
) -> Response {
|
||||
let search = params.search.unwrap_or_default();
|
||||
@@ -343,7 +343,7 @@ pub async fn handle_search(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(provider_id): Path<String>,
|
||||
axum::extract::Query(params): axum::extract::Query<UnifiedSearchParams>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
) -> Response {
|
||||
// Only the "files" provider is supported
|
||||
if provider_id != "files" {
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::application::ports::file_ports::FileRetrievalUseCase;
|
||||
use crate::application::ports::storage_ports::FileReadPort;
|
||||
use crate::application::ports::thumbnail_ports::{ThumbnailPort, ThumbnailSize};
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PreviewParams {
|
||||
@@ -34,7 +34,7 @@ pub struct PreviewParams {
|
||||
/// - Size selection based on request dimensions and forceIcon param
|
||||
pub async fn handle_preview(
|
||||
State(state): State<Arc<AppState>>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
Query(params): Query<PreviewParams>,
|
||||
) -> impl IntoResponse {
|
||||
// Parse the Nextcloud file ID — the NC app may append an instance suffix
|
||||
|
||||
@@ -10,7 +10,7 @@ use axum::{
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
use crate::interfaces::middleware::rate_limit::{RateLimiter, rate_limit_login};
|
||||
use crate::interfaces::nextcloud::avatar_handler;
|
||||
use crate::interfaces::nextcloud::basic_auth_middleware::basic_auth_middleware;
|
||||
@@ -176,7 +176,7 @@ fn verify_url_user(url_user: &str, auth_user: &CurrentUser) -> Result<(), Respon
|
||||
async fn handle_dav_files(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((url_user, subpath)): Path<(String, String)>,
|
||||
user_ext: CurrentUser,
|
||||
user_ext: AuthUser,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
verify_url_user(&url_user, &user_ext)?;
|
||||
@@ -188,7 +188,7 @@ async fn handle_dav_files(
|
||||
async fn handle_dav_files_root(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(url_user): Path<String>,
|
||||
user_ext: CurrentUser,
|
||||
user_ext: AuthUser,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
verify_url_user(&url_user, &user_ext)?;
|
||||
@@ -200,7 +200,7 @@ async fn handle_dav_files_root(
|
||||
async fn handle_dav_uploads(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((url_user, upload_id, rest)): Path<(String, String, String)>,
|
||||
user_ext: CurrentUser,
|
||||
user_ext: AuthUser,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
verify_url_user(&url_user, &user_ext)?;
|
||||
@@ -212,7 +212,7 @@ async fn handle_dav_uploads(
|
||||
async fn handle_dav_uploads_root(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((url_user, upload_id)): Path<(String, String)>,
|
||||
user_ext: CurrentUser,
|
||||
user_ext: AuthUser,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
verify_url_user(&url_user, &user_ext)?;
|
||||
@@ -222,7 +222,7 @@ async fn handle_dav_uploads_root(
|
||||
}
|
||||
|
||||
/// Legacy /remote.php/webdav/* — redirect to /remote.php/dav/files/{user}/*
|
||||
async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: CurrentUser) -> Response {
|
||||
async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: AuthUser) -> Response {
|
||||
let location = format!("/remote.php/dav/files/{}/{}", user_ext.username, subpath);
|
||||
Response::builder()
|
||||
.status(StatusCode::MOVED_PERMANENTLY)
|
||||
@@ -231,7 +231,7 @@ async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: CurrentUser
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn handle_legacy_webdav_root(user_ext: CurrentUser) -> Response {
|
||||
async fn handle_legacy_webdav_root(user_ext: AuthUser) -> Response {
|
||||
let location = format!("/remote.php/dav/files/{}/", user_ext.username);
|
||||
Response::builder()
|
||||
.status(StatusCode::MOVED_PERMANENTLY)
|
||||
@@ -243,7 +243,7 @@ async fn handle_legacy_webdav_root(user_ext: CurrentUser) -> Response {
|
||||
async fn handle_dav_trashbin(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path((url_user, subpath)): Path<(String, String)>,
|
||||
user_ext: CurrentUser,
|
||||
user_ext: AuthUser,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
verify_url_user(&url_user, &user_ext)?;
|
||||
@@ -255,7 +255,7 @@ async fn handle_dav_trashbin(
|
||||
async fn handle_dav_trashbin_root(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(url_user): Path<String>,
|
||||
user_ext: CurrentUser,
|
||||
user_ext: AuthUser,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response, Response> {
|
||||
verify_url_user(&url_user, &user_ext)?;
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::sync::Arc;
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
use crate::interfaces::nextcloud::webdav_handler::{
|
||||
format_oc_id, resolve_file_id, resolve_folder_id, write_text_element,
|
||||
};
|
||||
@@ -25,7 +25,7 @@ const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
|
||||
pub async fn handle_nc_trashbin(
|
||||
state: Arc<AppState>,
|
||||
req: Request<Body>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
subpath: String,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let method = req.method().clone();
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseC
|
||||
use crate::common::di::AppState;
|
||||
use crate::common::mime_detect::{filename_from_path, refine_content_type_from_file};
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
|
||||
/// Dispatch Nextcloud chunked upload WebDAV requests.
|
||||
///
|
||||
@@ -21,7 +21,7 @@ use crate::interfaces::middleware::auth::CurrentUser;
|
||||
pub async fn handle_nc_uploads(
|
||||
state: Arc<AppState>,
|
||||
req: Request<Body>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
upload_id: String,
|
||||
rest: String, // chunk name or ".file" or empty
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::common::di::AppState;
|
||||
use crate::common::mime_detect::{filename_from_path, refine_content_type};
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
|
||||
|
||||
/// Extension trait to map XML write errors to `String` concisely.
|
||||
trait XmlResultExt<T> {
|
||||
@@ -89,7 +89,7 @@ pub fn nc_href(username: &str, subpath: &str) -> String {
|
||||
pub async fn handle_nc_webdav(
|
||||
state: Arc<AppState>,
|
||||
req: Request<Body>,
|
||||
user: CurrentUser,
|
||||
user: AuthUser,
|
||||
subpath: String,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let method = req.method().clone();
|
||||
|
||||
Reference in New Issue
Block a user