From 3bdca881446a9a3ad496bb458e0359693ddb3657 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 29 Oct 2025 00:53:59 +0100 Subject: [PATCH] stuff --- .../down.sql | 2 + .../up.sql | 2 + backend/src/auth/webdav_tokens.rs | 49 +-------------- backend/src/documents/asset.rs | 13 ++++ backend/src/models.rs | 2 - backend/src/openapi.rs | 1 - backend/src/routes/profile.rs | 5 +- backend/src/schema.rs | 1 - backend/src/workers/ocr.rs | 20 ++++++- backend/src/workers/thumbnails.rs | 59 ++++++++++++++++++- frontend/src/DesktopWorkspace.css | 4 +- frontend/src/index.jsx | 4 +- frontend/src/settings/SettingsModal.jsx | 5 -- frontend/src/styles.css | 46 +++++++-------- 14 files changed, 122 insertions(+), 91 deletions(-) create mode 100644 backend/migrations/202512010110_drop_webdav_token_scopes/down.sql create mode 100644 backend/migrations/202512010110_drop_webdav_token_scopes/up.sql diff --git a/backend/migrations/202512010110_drop_webdav_token_scopes/down.sql b/backend/migrations/202512010110_drop_webdav_token_scopes/down.sql new file mode 100644 index 0000000..c375e1e --- /dev/null +++ b/backend/migrations/202512010110_drop_webdav_token_scopes/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE webdav_tokens + ADD COLUMN scopes JSONB NOT NULL DEFAULT '["webdav"]'::jsonb; diff --git a/backend/migrations/202512010110_drop_webdav_token_scopes/up.sql b/backend/migrations/202512010110_drop_webdav_token_scopes/up.sql new file mode 100644 index 0000000..04fe456 --- /dev/null +++ b/backend/migrations/202512010110_drop_webdav_token_scopes/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE webdav_tokens + DROP COLUMN IF EXISTS scopes; diff --git a/backend/src/auth/webdav_tokens.rs b/backend/src/auth/webdav_tokens.rs index 4e5e7b4..7a0610f 100644 --- a/backend/src/auth/webdav_tokens.rs +++ b/backend/src/auth/webdav_tokens.rs @@ -6,7 +6,6 @@ use chrono::{NaiveDateTime, Utc}; use diesel::prelude::*; use rand::rngs::OsRng; use rand::RngCore; -use serde_json::json; use uuid::Uuid; use crate::{ @@ -16,7 +15,6 @@ use crate::{ state::PgPooledConnection, }; -const WEB_DAV_SCOPE: &str = "webdav"; const TOKEN_PREFIX_LENGTH: usize = 12; const TOKEN_SECRET_LENGTH: usize = 32; @@ -30,16 +28,11 @@ pub fn create_webdav_token( user_id: Uuid, tenant_id: Uuid, label: Option, - scopes: Option>, expires_at: Option, ) -> Result { let raw_secret = generate_secret()?; let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string(); let token_hash = hash_secret(&raw_secret)?; - let scopes_value = scopes - .map(|scopes| json!(scopes)) - .unwrap_or_else(|| json!([WEB_DAV_SCOPE])); - let new_token = NewWebdavToken { id: Uuid::new_v4(), user_id, @@ -47,7 +40,6 @@ pub fn create_webdav_token( token_prefix, token_hash, label, - scopes: scopes_value, expires_at, }; @@ -112,7 +104,7 @@ pub fn find_active_token_by_secret( let candidates = query.load::(conn)?; for token in candidates { - if verify_token_secret(secret, &token.token_hash)? && token_allows_webdav(&token.scopes) { + if verify_token_secret(secret, &token.token_hash)? { return Ok(Some(token)); } } @@ -168,28 +160,6 @@ fn hash_secret(secret: &str) -> Result { Ok(hash.to_string()) } -pub fn parse_scopes(scopes: &serde_json::Value) -> Vec { - match scopes { - serde_json::Value::Array(values) => values - .iter() - .filter_map(|value| value.as_str().map(|s| s.to_string())) - .collect(), - serde_json::Value::String(value) => value - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect(), - _ => vec![], - } -} - -pub fn token_allows_webdav(scopes: &serde_json::Value) -> bool { - parse_scopes(scopes) - .into_iter() - .any(|scope| scope == WEB_DAV_SCOPE) -} - fn _ensure_constants() { assert!(TOKEN_PREFIX_LENGTH < TOKEN_SECRET_LENGTH * 2); } @@ -211,21 +181,4 @@ mod tests { assert!(verify_token_secret(&secret, &hash).unwrap()); assert!(!verify_token_secret("wrong", &hash).unwrap()); } - - #[test] - fn parse_scopes_handles_strings_and_arrays() { - let from_array = parse_scopes(&json!(["webdav", "other"])); - assert_eq!(from_array, vec!["webdav", "other"]); - - let from_string = parse_scopes(&json!("webdav, other")); - assert_eq!(from_string, vec!["webdav", "other"]); - - assert!(parse_scopes(&serde_json::Value::Null).is_empty()); - } - - #[test] - fn token_allows_webdav_matches_scope() { - assert!(token_allows_webdav(&json!(["webdav"]))); - assert!(!token_allows_webdav(&json!(["api"]))); - } } diff --git a/backend/src/documents/asset.rs b/backend/src/documents/asset.rs index 8054213..e2c3d36 100644 --- a/backend/src/documents/asset.rs +++ b/backend/src/documents/asset.rs @@ -128,6 +128,19 @@ pub fn to_asset_object_response( } } +pub fn delete_asset(state: &AppState, tenant_id: Uuid, asset_id: Uuid) -> AppResult<()> { + let mut conn = state.db_for_tenant(tenant_id)?; + + diesel::delete( + document_assets::table + .filter(document_assets::id.eq(asset_id)) + .filter(document_assets::tenant_id.eq(tenant_id)), + ) + .execute(&mut conn)?; + + Ok(()) +} + pub async fn load_asset_responses( state: &AppState, tenant_id: Uuid, diff --git a/backend/src/models.rs b/backend/src/models.rs index 59d70d8..851452d 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -69,7 +69,6 @@ pub struct WebdavToken { pub token_prefix: String, pub token_hash: String, pub label: Option, - pub scopes: serde_json::Value, pub created_at: NaiveDateTime, pub last_used_at: Option, pub expires_at: Option, @@ -85,7 +84,6 @@ pub struct NewWebdavToken { pub token_prefix: String, pub token_hash: String, pub label: Option, - pub scopes: serde_json::Value, pub expires_at: Option, } diff --git a/backend/src/openapi.rs b/backend/src/openapi.rs index 595001d..7988549 100644 --- a/backend/src/openapi.rs +++ b/backend/src/openapi.rs @@ -1050,7 +1050,6 @@ pub mod schemas { pub tenant_id: Uuid, #[schema(nullable)] pub label: Option, - pub scopes: Vec, pub created_at: String, #[schema(nullable)] pub last_used_at: Option, diff --git a/backend/src/routes/profile.rs b/backend/src/routes/profile.rs index 28b6945..d5fb107 100644 --- a/backend/src/routes/profile.rs +++ b/backend/src/routes/profile.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::auth::{ webdav_tokens::{ - create_webdav_token as issue_token, list_webdav_tokens as load_tokens, parse_scopes, + create_webdav_token as issue_token, list_webdav_tokens as load_tokens, revoke_webdav_token as revoke_token, }, TenantScopedConn, @@ -19,7 +19,6 @@ pub struct WebdavTokenResponse { pub id: Uuid, pub tenant_id: Uuid, pub label: Option, - pub scopes: Vec, pub created_at: String, pub last_used_at: Option, pub expires_at: Option, @@ -70,7 +69,6 @@ pub async fn create_webdav_token( user_id, tenant_id, payload.label.clone(), - None, expires_at, )?; @@ -97,7 +95,6 @@ fn webdav_token_to_response(token: WebdavToken) -> WebdavTokenResponse { id: token.id, tenant_id: token.tenant_id, label: token.label, - scopes: parse_scopes(&token.scopes), created_at: to_iso(token.created_at), last_used_at: token.last_used_at.map(to_iso), expires_at: token.expires_at.map(to_iso), diff --git a/backend/src/schema.rs b/backend/src/schema.rs index d80d47c..8efb5d6 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -191,7 +191,6 @@ diesel::table! { token_prefix -> Text, token_hash -> Text, label -> Nullable, - scopes -> Jsonb, created_at -> Timestamptz, last_used_at -> Nullable, expires_at -> Nullable, diff --git a/backend/src/workers/ocr.rs b/backend/src/workers/ocr.rs index f2971f4..47e2393 100644 --- a/backend/src/workers/ocr.rs +++ b/backend/src/workers/ocr.rs @@ -18,6 +18,7 @@ use tracing::{error, info, warn}; use uuid::Uuid; use crate::{ + documents::asset::delete_asset, jobs::{enqueue_job, JOB_GENERATE_OCR_TEXT, JOB_INDEX_DOCUMENT_TEXT}, models::{ Document, DocumentAsset, DocumentAssetObject, DocumentVersion, NewDocumentAsset, @@ -133,12 +134,29 @@ impl JobHandler for GenerateOcrTextJob { }; }; - if context.existing_asset.is_some() { + if let Some(existing_asset) = &context.existing_asset { for object in &context.existing_objects { if let Err(err) = storage.delete_object(&object.s3_key).await { warn!(job_id = %job.id, error = %err, s3_key = %object.s3_key, "failed to delete existing ocr asset object"); } } + + let tenant_id = context.document.tenant_id; + let asset_id = existing_asset.id; + let state_clone = state.clone(); + match task::spawn_blocking(move || { + delete_asset(state_clone.as_ref(), tenant_id, asset_id) + }) + .await + { + Ok(Ok(())) => {} + Ok(Err(err)) => { + warn!(job_id = %job.id, error = ?err, asset_id = %asset_id, "failed to remove ocr asset metadata after deletion"); + } + Err(join_err) => { + warn!(job_id = %job.id, error = %join_err, asset_id = %asset_id, "failed to remove ocr asset metadata: task panicked"); + } + } } let asset_id = Uuid::new_v4(); diff --git a/backend/src/workers/thumbnails.rs b/backend/src/workers/thumbnails.rs index 121814f..f205ade 100644 --- a/backend/src/workers/thumbnails.rs +++ b/backend/src/workers/thumbnails.rs @@ -12,6 +12,7 @@ use tracing::{error, info, warn}; use uuid::Uuid; use crate::{ + documents::asset::delete_asset, jobs::JOB_GENERATE_THUMBNAILS, models::{ Document, DocumentAsset, DocumentAssetObject, DocumentVersion, NewDocumentAsset, @@ -152,7 +153,7 @@ impl JobHandler for GenerateThumbnailsJob { } } - if initial.existing_preview.is_some() { + if let Some(existing_preview) = &initial.existing_preview { for object in &initial.existing_preview_objects { if let Err(err) = storage.delete_object(&object.s3_key).await { warn!( @@ -163,9 +164,36 @@ impl JobHandler for GenerateThumbnailsJob { ); } } + + let tenant_id = initial.document.tenant_id; + let asset_id = existing_preview.id; + let state_clone = state.clone(); + match task::spawn_blocking(move || { + delete_asset(state_clone.as_ref(), tenant_id, asset_id) + }) + .await + { + Ok(Ok(())) => {} + Ok(Err(err)) => { + warn!( + job_id = %job.id, + error = ?err, + asset_id = %asset_id, + "failed to remove preview metadata after deletion" + ); + } + Err(join_err) => { + warn!( + job_id = %job.id, + error = %join_err, + asset_id = %asset_id, + "failed to remove preview metadata: task panicked" + ); + } + } } - if initial.existing_thumbnail.is_some() { + if let Some(existing_thumbnail) = &initial.existing_thumbnail { for object in &initial.existing_thumbnail_objects { if let Err(err) = storage.delete_object(&object.s3_key).await { warn!( @@ -176,6 +204,33 @@ impl JobHandler for GenerateThumbnailsJob { ); } } + + let tenant_id = initial.document.tenant_id; + let asset_id = existing_thumbnail.id; + let state_clone = state.clone(); + match task::spawn_blocking(move || { + delete_asset(state_clone.as_ref(), tenant_id, asset_id) + }) + .await + { + Ok(Ok(())) => {} + Ok(Err(err)) => { + warn!( + job_id = %job.id, + error = ?err, + asset_id = %asset_id, + "failed to remove thumbnail metadata after deletion" + ); + } + Err(join_err) => { + warn!( + job_id = %job.id, + error = %join_err, + asset_id = %asset_id, + "failed to remove thumbnail metadata: task panicked" + ); + } + } } let preview_asset_id = Uuid::new_v4(); diff --git a/frontend/src/DesktopWorkspace.css b/frontend/src/DesktopWorkspace.css index 698c0bd..8604524 100644 --- a/frontend/src/DesktopWorkspace.css +++ b/frontend/src/DesktopWorkspace.css @@ -205,8 +205,8 @@ body.skeuo-cursor-remove * { .skeuo-item__card--empty { background: - radial-gradient(circle at 28% 24%, rgba(255, 255, 255, 0.32), transparent 60%), - radial-gradient(circle at 72% 78%, rgba(0, 0, 0, 0.08), transparent 65%), + radial-gradient(circle at 28% 24%, color-mix(in oklch, white 32%, transparent), transparent 60%), + radial-gradient(circle at 72% 78%, color-mix(in oklch, black 8%, transparent), transparent 65%), linear-gradient(135deg, #e6e1d6 0%, #d2cdc2 100%); width: 100%; height: 100%; diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index d7da00d..88345c3 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -1805,7 +1805,7 @@ const AppLayout = () => { setDeletingWebdavTokenId(tokenId); try { await api.delete(`/profile/webdav-tokens/${tokenId}`); - setWebdavTokens((previous) => previous.filter((entry) => entry.id !== tokenId)); + await refreshWebdavTokens(); setStatusMessage('WebDAV token revoked.', 'success'); return true; } catch (error) { @@ -1815,7 +1815,7 @@ const AppLayout = () => { setDeletingWebdavTokenId(null); } }, - [api, notifyApiError, setStatusMessage], + [api, refreshWebdavTokens, notifyApiError, setStatusMessage], ); const dismissCreatedWebdavToken = useCallback(() => { diff --git a/frontend/src/settings/SettingsModal.jsx b/frontend/src/settings/SettingsModal.jsx index 0a55277..72fb77b 100644 --- a/frontend/src/settings/SettingsModal.jsx +++ b/frontend/src/settings/SettingsModal.jsx @@ -181,23 +181,18 @@ const SettingsModal = ({ Created Last used Expires - Scopes Actions {tokens.map((token) => { const isRevoked = Boolean(token?.revoked_at); - const scopes = Array.isArray(token?.scopes) - ? token.scopes.join(', ') - : '—'; return ( {token.label || '—'} {formatDateTime(token.created_at)} {formatDateTime(token.last_used_at)} {formatDateTime(token.expires_at)} - {scopes || '—'} {isRevoked ? ( Revoked diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 092efa4..26faa22 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -757,7 +757,7 @@ button.danger:hover:not([disabled]) { .preview-workspace__metadata { background: var(--surface-subtle); padding: 1rem; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); + box-shadow: inset 0 0 0 1px var(--outline-subtle); font-size: 0.85rem; overflow: auto; max-height: 40vh; @@ -778,7 +778,7 @@ button.danger:hover:not([disabled]) { flex: 1; min-width: 0; background: var(--surface-subtle); - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); + box-shadow: inset 0 0 0 1px var(--outline-subtle); display: flex; position: relative; overflow: hidden; @@ -975,8 +975,8 @@ button.danger:hover:not([disabled]) { top: 50%; left: 50%; transform: translate(-50%, -50%); - color: #fff; - background: rgba(0, 0, 0, 0.65); + color: var(--on-accent); + background: var(--overlay-dark); padding: 0.5rem 1rem; border-radius: 999px; font-size: 0.9rem; @@ -1192,7 +1192,7 @@ button.danger:hover:not([disabled]) { background: var(--surface); border: 1px solid var(--border-muted, var(--border)); border-radius: 0.5rem; - box-shadow: 0 12px 28px rgba(15, 23, 42, 0.18); + box-shadow: 0 12px 28px var(--shadow-strong); min-width: 220px; z-index: 20; overflow: hidden; @@ -1313,7 +1313,7 @@ button.danger:hover:not([disabled]) { .menu__logout:hover, .menu__logout:focus-visible { - background: rgba(209, 67, 67, 0.12); + background: var(--surface-danger-subtle); } .sidebar__search { @@ -1465,12 +1465,12 @@ button.danger:hover:not([disabled]) { color: var(--fg); transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, opacity 0.12s ease; - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); + box-shadow: 0 1px 2px var(--shadow-faint); } .sidebar-tag-pill:hover { transform: none; - box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16); + box-shadow: 0 2px 6px var(--shadow-pop); border-color: var(--accent-soft); } @@ -1511,7 +1511,7 @@ button.danger:hover:not([disabled]) { .sidebar-correspondent-item:hover { background: var(--sidebar-hover-bg); - box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); + box-shadow: 0 1px 3px var(--shadow-soft); color: var(--fg); } @@ -1521,8 +1521,8 @@ button.danger:hover:not([disabled]) { } .sidebar-correspondent-item.active { - background: var(--sidebar-active-bg, rgba(59, 130, 246, 0.18)); - box-shadow: 0 0 0 1px var(--sidebar-active-bg, rgba(59, 130, 246, 0.24)); + background: var(--sidebar-active-bg); + box-shadow: 0 0 0 1px var(--sidebar-active-pill-border); color: var(--fg); } @@ -2015,7 +2015,7 @@ button.danger:hover:not([disabled]) { .tag-chip--more { background: transparent; - border-color: var(--border-strong, rgba(15, 23, 42, 0.24)); + border-color: var(--border-strong); color: var(--muted); } @@ -2126,7 +2126,7 @@ button.danger:hover:not([disabled]) { padding: 0.75rem; background: var(--surface-soft); border-radius: 6px; - box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.05); + box-shadow: inset 0 1px 2px var(--shadow-faint); } .detail-ocr__content pre { @@ -2144,7 +2144,7 @@ button.danger:hover:not([disabled]) { height: 60vh; background: var(--surface-subtle); border-radius: 4px; - box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.05); + box-shadow: inset 0 1px 2px var(--shadow-faint); } .modal__body { @@ -2357,15 +2357,15 @@ button.danger:hover:not([disabled]) { height: 2.2em; padding: 0.45em; border-radius: 999px; - background: rgba(0, 0, 0, 0.55); - color: #fff; + background: var(--preview-nav-bg); + color: var(--preview-nav-fg); cursor: pointer; transition: background 0.15s ease, opacity 0.15s ease; pointer-events: auto; } .preview-pane__nav-button:hover:not([disabled]) { - background: rgba(0, 0, 0, 0.75); + background: var(--preview-nav-bg-hover); } .preview-pane__nav-button:disabled { @@ -2603,7 +2603,7 @@ form.inline { } .settings-modal__sidebar button.active { - background: var(--surface-soft, rgba(148, 163, 184, 0.18)); + background: var(--surface-soft); font-weight: 600; } @@ -2651,7 +2651,7 @@ form.inline { padding: 0.45rem 0.6rem; border: 1px solid var(--border); border-radius: 0.35rem; - background: var(--surface-soft, rgba(148, 163, 184, 0.12)); + background: var(--surface-soft); color: inherit; } @@ -2704,14 +2704,14 @@ form.inline { .settings-notice { border: 1px solid var(--accent); - background: rgba(37, 99, 235, 0.08); + background: var(--accent-soft); padding: 0.9rem 1rem; border-radius: 0.5rem; margin-bottom: 1rem; } .token-display { - background: rgba(15, 23, 42, 0.08); + background: var(--surface-ink-soft); padding: 0.5rem 0.65rem; border-radius: 0.35rem; font-family: var(--font-mono); @@ -2869,8 +2869,8 @@ form.inline { } .login-card__tenant-button:hover:not([disabled]) { - background: var(--surface-hover, rgba(255, 255, 255, 0.1)); - border-color: var(--border-strong, var(--border)); + background: var(--surface-hover); + border-color: var(--border-strong); } .login-card__tenant-button:disabled {