Skip to content

Commit df336da

Browse files
author
Diocrafts
committed
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
1 parent f409a9e commit df336da

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+6645
-2043
lines changed

src/interfaces/api/handlers/app_password_handler.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use crate::application::dtos::app_password_dto::CreateAppPasswordRequestDto;
77
use crate::common::di::AppState;
88
use crate::interfaces::errors::AppError;
9-
use crate::interfaces::middleware::auth::CurrentUser;
9+
use crate::interfaces::middleware::auth::AuthUser;
1010
use axum::extract::State;
1111
use axum::routing::{delete, get, post};
1212
use axum::{Json, Router};
@@ -26,7 +26,7 @@ pub fn app_password_routes() -> Router<Arc<AppState>> {
2626
/// Returns the plain-text password ONCE. The user must copy it immediately.
2727
async fn create_app_password(
2828
State(state): State<Arc<AppState>>,
29-
axum::Extension(user): axum::Extension<CurrentUser>,
29+
user: AuthUser,
3030
Json(request): Json<CreateAppPasswordRequestDto>,
3131
) -> Result<Json<crate::application::dtos::app_password_dto::AppPasswordCreatedResponseDto>, AppError>
3232
{
@@ -48,7 +48,7 @@ async fn create_app_password(
4848
/// Never returns plain-text passwords (only prefix + metadata).
4949
async fn list_app_passwords(
5050
State(state): State<Arc<AppState>>,
51-
axum::Extension(user): axum::Extension<CurrentUser>,
51+
user: AuthUser,
5252
) -> Result<Json<crate::application::dtos::app_password_dto::AppPasswordListResponseDto>, AppError>
5353
{
5454
let service = state
@@ -64,7 +64,7 @@ async fn list_app_passwords(
6464
/// DELETE /api/auth/app-passwords/:id — Revoke an app password.
6565
async fn revoke_app_password(
6666
State(state): State<Arc<AppState>>,
67-
axum::Extension(user): axum::Extension<CurrentUser>,
67+
user: AuthUser,
6868
axum::extract::Path(id): axum::extract::Path<String>,
6969
) -> Result<Json<crate::application::dtos::app_password_dto::AppPasswordRevokeResponseDto>, AppError>
7070
{

src/interfaces/api/handlers/caldav_handler.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use crate::application::ports::calendar_ports::CalendarUseCase;
3535
use crate::application::services::calendar_service::CalendarService;
3636
use crate::common::di::AppState;
3737
use crate::interfaces::errors::AppError;
38-
use crate::interfaces::middleware::auth::CurrentUser;
38+
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
3939

4040
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
4141

@@ -138,10 +138,11 @@ fn reject_path_traversal(path: &str) -> Result<(), AppError> {
138138

139139
// ─── Helper: extract user from request ───────────────────────────────
140140

141-
fn extract_user(req: &Request<Body>) -> Result<CurrentUser, AppError> {
141+
fn extract_user(req: &Request<Body>) -> Result<AuthUser, AppError> {
142142
req.extensions()
143143
.get::<Arc<CurrentUser>>()
144-
.map(|arc| (**arc).clone())
144+
.cloned()
145+
.map(AuthUser)
145146
.ok_or_else(|| AppError::unauthorized("Authentication required"))
146147
}
147148

src/interfaces/api/handlers/carddav_handler.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCas
3535
use crate::common::di::AppState;
3636
use crate::infrastructure::adapters::contact_storage_adapter::ContactStorageAdapter;
3737
use crate::interfaces::errors::AppError;
38-
use crate::interfaces::middleware::auth::CurrentUser;
38+
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
3939

4040
const HEADER_DAV: HeaderName = HeaderName::from_static("dav");
4141

@@ -126,10 +126,11 @@ fn reject_path_traversal(path: &str) -> Result<(), AppError> {
126126

127127
// ─── Helper: extract user from request ───────────────────────────────
128128

129-
fn extract_user(req: &Request<Body>) -> Result<CurrentUser, AppError> {
129+
fn extract_user(req: &Request<Body>) -> Result<AuthUser, AppError> {
130130
req.extensions()
131131
.get::<Arc<CurrentUser>>()
132-
.map(|arc| (**arc).clone())
132+
.cloned()
133+
.map(AuthUser)
133134
.ok_or_else(|| AppError::unauthorized("Authentication required"))
134135
}
135136

src/interfaces/api/handlers/webdav_handler.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::application::services::folder_service::FolderService;
2828
use crate::common::di::AppState;
2929
use crate::infrastructure::services::path_resolver_service::ResolvedResource;
3030
use crate::interfaces::errors::AppError;
31-
use crate::interfaces::middleware::auth::CurrentUser;
31+
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
3232
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
3333
use std::sync::Arc;
3434

@@ -93,10 +93,11 @@ const PROPFIND_BATCH_SIZE: i64 = 500;
9393
/// Every mutating or data-returning WebDAV handler **must** call this so
9494
/// that the real `user.id` is available for ownership checks and for the
9595
/// user-scoped `PathResolverService` methods.
96-
fn extract_user(req: &Request<Body>) -> Result<CurrentUser, AppError> {
96+
fn extract_user(req: &Request<Body>) -> Result<AuthUser, AppError> {
9797
req.extensions()
9898
.get::<Arc<CurrentUser>>()
99-
.map(|arc| (**arc).clone())
99+
.cloned()
100+
.map(AuthUser)
100101
.ok_or_else(|| AppError::unauthorized("Authentication required"))
101102
}
102103

src/interfaces/api/handlers/wopi_handler.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -413,14 +413,12 @@ async fn authorize_wopi_access<S: FileRetrievalUseCase>(
413413
/// This endpoint is behind normal auth middleware. The authenticated user
414414
/// requests a WOPI session for a specific file.
415415
pub async fn get_editor_url(
416-
AuthUser {
417-
id: user_id,
418-
username,
419-
..
420-
}: AuthUser,
416+
auth_user: AuthUser,
421417
Query(params): Query<EditorUrlParams>,
422418
State(state): State<WopiState>,
423419
) -> Response {
420+
let user_id = auth_user.id;
421+
let username = &auth_user.username;
424422
// Verify the caller owns the file (SQL-level check, no existence leak).
425423
let (file, can_write) = match authorize_wopi_access(
426424
state.app_state.applications.file_retrieval_service.as_ref(),

src/interfaces/middleware/auth.rs

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ use crate::application::ports::auth_ports::TokenServicePort;
2020
#[derive(Clone, Copy, Debug)]
2121
pub struct CookieAuthenticated;
2222

23-
// Structure for use in Axum extractors
23+
// Newtype over Arc<CurrentUser> for zero-allocation extraction.
24+
// `Deref<Target = CurrentUser>` lets handlers access `.id`, `.username`,
25+
// `.email`, `.role` transparently — no signature changes needed.
2426
#[derive(Clone, Debug)]
25-
pub struct AuthUser {
26-
pub id: Uuid,
27-
pub username: String,
28-
pub role: String,
27+
pub struct AuthUser(pub Arc<CurrentUser>);
28+
29+
impl std::ops::Deref for AuthUser {
30+
type Target = CurrentUser;
31+
#[inline]
32+
fn deref(&self) -> &CurrentUser {
33+
&self.0
34+
}
2935
}
3036

3137
/// Reusable extractor that gets the user_id of the authenticated user.
@@ -38,7 +44,8 @@ pub struct AuthUser {
3844
#[derive(Clone, Debug)]
3945
pub struct CurrentUserId(pub Uuid);
4046

41-
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
47+
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers.
48+
// Cost: 1 atomic increment (~1 ns) instead of 3 String clones (~100 ns + 3 mallocs).
4249
impl<S> FromRequestParts<S> for AuthUser
4350
where
4451
S: Send + Sync,
@@ -49,29 +56,8 @@ where
4956
parts
5057
.extensions
5158
.get::<Arc<CurrentUser>>()
52-
.map(|cu| AuthUser {
53-
id: cu.id,
54-
username: cu.username.clone(),
55-
role: cu.role.clone(),
56-
})
57-
.ok_or(AuthError::UserNotFound)
58-
}
59-
}
60-
61-
// Implement FromRequestParts for CurrentUser — full user extractor from extensions
62-
// The middleware inserts Arc<CurrentUser>; this extractor cheaply clones the Arc
63-
// (~1 ns atomic increment) instead of deep-cloning 4 Strings (~60-100 ns).
64-
impl<S> FromRequestParts<S> for CurrentUser
65-
where
66-
S: Send + Sync,
67-
{
68-
type Rejection = AuthError;
69-
70-
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
71-
parts
72-
.extensions
73-
.get::<Arc<CurrentUser>>()
74-
.map(|arc| (**arc).clone())
59+
.cloned()
60+
.map(AuthUser)
7561
.ok_or(AuthError::UserNotFound)
7662
}
7763
}
@@ -113,27 +99,7 @@ where
11399
}
114100
}
115101

116-
/// Optional auth user extractor – never fails.
117-
/// Yields `Some(AuthUser)` when auth middleware ran, `None` otherwise.
118-
#[derive(Clone, Debug)]
119-
pub struct OptionalAuthUser(pub Option<AuthUser>);
120-
121-
impl<S> FromRequestParts<S> for OptionalAuthUser
122-
where
123-
S: Send + Sync,
124-
{
125-
type Rejection = Infallible;
126102

127-
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
128-
Ok(OptionalAuthUser(parts.extensions.get::<Arc<CurrentUser>>().map(
129-
|cu| AuthUser {
130-
id: cu.id,
131-
username: cu.username.clone(),
132-
role: cu.role.clone(),
133-
},
134-
)))
135-
}
136-
}
137103

138104
// Error for authentication operations
139105
#[derive(Debug, thiserror::Error)]

src/interfaces/nextcloud/ocs_handler.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
1111
use crate::application::ports::inbound::SearchUseCase;
1212
use crate::application::ports::storage_ports::StorageUsagePort;
1313
use crate::common::di::AppState;
14-
use crate::interfaces::middleware::auth::CurrentUser;
14+
use crate::interfaces::middleware::auth::AuthUser;
1515

1616
/// Build an OCS success response with the given statuscode and data.
1717
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
4545
Json(payload).into_response()
4646
}
4747

48-
pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: CurrentUser) -> Response {
48+
pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: AuthUser) -> Response {
4949
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
5050
Some(service) => match service.get_user_storage_info(user.id).await {
5151
Ok((used, total)) => (used, total),
@@ -86,7 +86,7 @@ pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: CurrentU
8686
pub async fn handle_user_provisioning_v1(
8787
state: State<Arc<AppState>>,
8888
path: Path<String>,
89-
user: CurrentUser,
89+
user: AuthUser,
9090
) -> Response {
9191
user_provisioning_response(state, path, user, 1).await
9292
}
@@ -95,7 +95,7 @@ pub async fn handle_user_provisioning_v1(
9595
pub async fn handle_user_provisioning_v2(
9696
state: State<Arc<AppState>>,
9797
path: Path<String>,
98-
user: CurrentUser,
98+
user: AuthUser,
9999
) -> Response {
100100
user_provisioning_response(state, path, user, 2).await
101101
}
@@ -105,7 +105,7 @@ pub async fn handle_user_provisioning_v2(
105105
async fn user_provisioning_response(
106106
State(state): State<Arc<AppState>>,
107107
Path(userid): Path<String>,
108-
user: CurrentUser,
108+
user: AuthUser,
109109
ocs_version: u8,
110110
) -> Response {
111111
let statuscode = if ocs_version == 1 { 100 } else { 200 };
@@ -197,7 +197,7 @@ async fn user_provisioning_response(
197197

198198
pub async fn handle_revoke_apppassword(
199199
State(state): State<Arc<AppState>>,
200-
user: CurrentUser,
200+
user: AuthUser,
201201
headers: axum::http::HeaderMap,
202202
) -> Response {
203203
let nextcloud = match state.nextcloud.as_ref() {
@@ -249,7 +249,7 @@ pub async fn handle_recommendations() -> Response {
249249
/// this endpoint and expects a well-formed OCS response rather than a 404.
250250
pub async fn handle_sharees_search(
251251
State(state): State<Arc<AppState>>,
252-
user: CurrentUser,
252+
user: AuthUser,
253253
axum::extract::Query(params): axum::extract::Query<ShareeSearchParams>,
254254
) -> Response {
255255
let search = params.search.unwrap_or_default();
@@ -343,7 +343,7 @@ pub async fn handle_search(
343343
State(state): State<Arc<AppState>>,
344344
Path(provider_id): Path<String>,
345345
axum::extract::Query(params): axum::extract::Query<UnifiedSearchParams>,
346-
user: CurrentUser,
346+
user: AuthUser,
347347
) -> Response {
348348
// Only the "files" provider is supported
349349
if provider_id != "files" {

src/interfaces/nextcloud/preview_handler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::application::ports::file_ports::FileRetrievalUseCase;
1515
use crate::application::ports::storage_ports::FileReadPort;
1616
use crate::application::ports::thumbnail_ports::{ThumbnailPort, ThumbnailSize};
1717
use crate::common::di::AppState;
18-
use crate::interfaces::middleware::auth::CurrentUser;
18+
use crate::interfaces::middleware::auth::AuthUser;
1919

2020
#[derive(Debug, Deserialize)]
2121
pub struct PreviewParams {
@@ -34,7 +34,7 @@ pub struct PreviewParams {
3434
/// - Size selection based on request dimensions and forceIcon param
3535
pub async fn handle_preview(
3636
State(state): State<Arc<AppState>>,
37-
user: CurrentUser,
37+
user: AuthUser,
3838
Query(params): Query<PreviewParams>,
3939
) -> impl IntoResponse {
4040
// Parse the Nextcloud file ID — the NC app may append an instance suffix

src/interfaces/nextcloud/routes.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use axum::{
1010
use std::sync::Arc;
1111

1212
use crate::common::di::AppState;
13-
use crate::interfaces::middleware::auth::CurrentUser;
13+
use crate::interfaces::middleware::auth::{AuthUser, CurrentUser};
1414
use crate::interfaces::middleware::rate_limit::{RateLimiter, rate_limit_login};
1515
use crate::interfaces::nextcloud::avatar_handler;
1616
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
176176
async fn handle_dav_files(
177177
State(state): State<Arc<AppState>>,
178178
Path((url_user, subpath)): Path<(String, String)>,
179-
user_ext: CurrentUser,
179+
user_ext: AuthUser,
180180
req: Request<Body>,
181181
) -> Result<Response, Response> {
182182
verify_url_user(&url_user, &user_ext)?;
@@ -188,7 +188,7 @@ async fn handle_dav_files(
188188
async fn handle_dav_files_root(
189189
State(state): State<Arc<AppState>>,
190190
Path(url_user): Path<String>,
191-
user_ext: CurrentUser,
191+
user_ext: AuthUser,
192192
req: Request<Body>,
193193
) -> Result<Response, Response> {
194194
verify_url_user(&url_user, &user_ext)?;
@@ -200,7 +200,7 @@ async fn handle_dav_files_root(
200200
async fn handle_dav_uploads(
201201
State(state): State<Arc<AppState>>,
202202
Path((url_user, upload_id, rest)): Path<(String, String, String)>,
203-
user_ext: CurrentUser,
203+
user_ext: AuthUser,
204204
req: Request<Body>,
205205
) -> Result<Response, Response> {
206206
verify_url_user(&url_user, &user_ext)?;
@@ -212,7 +212,7 @@ async fn handle_dav_uploads(
212212
async fn handle_dav_uploads_root(
213213
State(state): State<Arc<AppState>>,
214214
Path((url_user, upload_id)): Path<(String, String)>,
215-
user_ext: CurrentUser,
215+
user_ext: AuthUser,
216216
req: Request<Body>,
217217
) -> Result<Response, Response> {
218218
verify_url_user(&url_user, &user_ext)?;
@@ -222,7 +222,7 @@ async fn handle_dav_uploads_root(
222222
}
223223

224224
/// Legacy /remote.php/webdav/* — redirect to /remote.php/dav/files/{user}/*
225-
async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: CurrentUser) -> Response {
225+
async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: AuthUser) -> Response {
226226
let location = format!("/remote.php/dav/files/{}/{}", user_ext.username, subpath);
227227
Response::builder()
228228
.status(StatusCode::MOVED_PERMANENTLY)
@@ -231,7 +231,7 @@ async fn handle_legacy_webdav(Path(subpath): Path<String>, user_ext: CurrentUser
231231
.unwrap()
232232
}
233233

234-
async fn handle_legacy_webdav_root(user_ext: CurrentUser) -> Response {
234+
async fn handle_legacy_webdav_root(user_ext: AuthUser) -> Response {
235235
let location = format!("/remote.php/dav/files/{}/", user_ext.username);
236236
Response::builder()
237237
.status(StatusCode::MOVED_PERMANENTLY)
@@ -243,7 +243,7 @@ async fn handle_legacy_webdav_root(user_ext: CurrentUser) -> Response {
243243
async fn handle_dav_trashbin(
244244
State(state): State<Arc<AppState>>,
245245
Path((url_user, subpath)): Path<(String, String)>,
246-
user_ext: CurrentUser,
246+
user_ext: AuthUser,
247247
req: Request<Body>,
248248
) -> Result<Response, Response> {
249249
verify_url_user(&url_user, &user_ext)?;
@@ -255,7 +255,7 @@ async fn handle_dav_trashbin(
255255
async fn handle_dav_trashbin_root(
256256
State(state): State<Arc<AppState>>,
257257
Path(url_user): Path<String>,
258-
user_ext: CurrentUser,
258+
user_ext: AuthUser,
259259
req: Request<Body>,
260260
) -> Result<Response, Response> {
261261
verify_url_user(&url_user, &user_ext)?;

0 commit comments

Comments
 (0)