From a1a3bd1b2b51c2aa1151bc20595d27ded45f6760 Mon Sep 17 00:00:00 2001 From: Diocrafts Date: Fri, 20 Feb 2026 12:27:52 +0100 Subject: [PATCH] fix: folder trash/delete operations & frontend refactoring - Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' does not exist) - Fix folder deletion: delete descendant files before folder to avoid 'duplicate key violates unique constraint idx_files_unique_name_at_root' - Simplify trash model: only mark the folder as trashed, not child files (implicit trash via parent) - Update trash_items view: filter to show only top-level trashed items - Update schema.sql: change files.folder_id FK from ON DELETE SET NULL to ON DELETE CASCADE - Fix trash view icons: folders and files now show correct visual icons (folder-icon, pdf-icon, etc.) in trash view - Frontend refactoring: extract inline CSS/JS from admin.html and profile.html into dedicated external files - Frontend cleanup: replace all inline style attributes with CSS classes - Frontend cleanup: replace style.display JS - Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' d --- db/schema.sql | 28 +- .../repositories/pg/folder_db_repository.rs | 73 +- static/admin.html | 1170 +++-------- static/css/admin.css | 299 +++ static/css/profile.css | 148 ++ static/identifier.sh | 8 - static/index.html | 43 +- static/js/app.js | 1843 ----------------- static/js/app/authSession.js | 224 ++ static/js/app/bootstrap.js | 14 + static/js/app/filesView.js | 127 ++ static/js/app/main.js | 603 ++++++ static/js/app/navigation.js | 196 ++ static/js/app/searchView.js | 54 + static/js/app/state.js | 26 + static/js/app/trashView.js | 157 ++ static/js/{ => app}/ui.js | 112 +- static/js/app/uiFileTypes.js | 78 + static/js/app/uiNotifications.js | 55 + static/js/app/userMenu.js | 243 +++ static/js/core/formatters.js | 63 + static/js/{ => core}/i18n.js | 0 static/js/{ => core}/icons.js | 0 static/js/{ => core}/languageSelector.js | 0 static/js/{ => core}/modal.js | 0 static/js/{ => core}/notifications.js | 0 static/js/{ => features/auth}/auth.js | 0 .../js/{ => features/files}/contextMenus.js | 0 .../js/{ => features/files}/fileOperations.js | 0 .../js/{ => features/files}/inlineViewer.js | 0 static/js/{ => features/files}/multiSelect.js | 0 static/js/{ => features/files}/search.js | 0 static/js/{ => features/library}/favorites.js | 0 static/js/{ => features/library}/recent.js | 0 .../js/{ => features/sharing}/fileSharing.js | 0 static/js/views/admin/admin.js | 397 ++++ static/js/views/profile/profile.js | 138 ++ static/js/{ => views/shared}/shared.js | 0 .../shared}/sharedView.js | 0 static/login.html | 6 +- static/profile.html | 536 ++--- static/shared.html | 11 +- static/test.html | 68 - 43 files changed, 3304 insertions(+), 3416 deletions(-) create mode 100644 static/css/admin.css create mode 100644 static/css/profile.css delete mode 100644 static/identifier.sh delete mode 100644 static/js/app.js create mode 100644 static/js/app/authSession.js create mode 100644 static/js/app/bootstrap.js create mode 100644 static/js/app/filesView.js create mode 100644 static/js/app/main.js create mode 100644 static/js/app/navigation.js create mode 100644 static/js/app/searchView.js create mode 100644 static/js/app/state.js create mode 100644 static/js/app/trashView.js rename static/js/{ => app}/ui.js (90%) create mode 100644 static/js/app/uiFileTypes.js create mode 100644 static/js/app/uiNotifications.js create mode 100644 static/js/app/userMenu.js create mode 100644 static/js/core/formatters.js rename static/js/{ => core}/i18n.js (100%) rename static/js/{ => core}/icons.js (100%) rename static/js/{ => core}/languageSelector.js (100%) rename static/js/{ => core}/modal.js (100%) rename static/js/{ => core}/notifications.js (100%) rename static/js/{ => features/auth}/auth.js (100%) rename static/js/{ => features/files}/contextMenus.js (100%) rename static/js/{ => features/files}/fileOperations.js (100%) rename static/js/{ => features/files}/inlineViewer.js (100%) rename static/js/{ => features/files}/multiSelect.js (100%) rename static/js/{ => features/files}/search.js (100%) rename static/js/{ => features/library}/favorites.js (100%) rename static/js/{ => features/library}/recent.js (100%) rename static/js/{ => features/sharing}/fileSharing.js (100%) create mode 100644 static/js/views/admin/admin.js create mode 100644 static/js/views/profile/profile.js rename static/js/{ => views/shared}/shared.js (100%) rename static/js/{components => views/shared}/sharedView.js (100%) delete mode 100644 static/test.html diff --git a/db/schema.sql b/db/schema.sql index 29b13ae9..d0ebd486 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -424,7 +424,7 @@ CREATE OR REPLACE TRIGGER trg_folders_cascade_path CREATE TABLE IF NOT EXISTS storage.files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, - folder_id UUID REFERENCES storage.folders(id) ON DELETE SET NULL, + folder_id UUID REFERENCES storage.folders(id) ON DELETE CASCADE, user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, blob_hash VARCHAR(64) NOT NULL, size BIGINT NOT NULL DEFAULT 0, @@ -448,15 +448,27 @@ CREATE INDEX IF NOT EXISTS idx_files_blob_hash ON storage.files(blob_hash); CREATE INDEX IF NOT EXISTS idx_files_trashed ON storage.files(user_id, is_trashed); CREATE INDEX IF NOT EXISTS idx_files_name_search ON storage.files(user_id, name text_pattern_ops); --- Trash view combining trashed files and folders for the TrashRepository +-- Trash view combining trashed files and folders for the TrashRepository. +-- Only shows top-level trashed items: excludes files/folders whose parent +-- is also trashed (they are implicitly in trash as children of a trashed folder). CREATE OR REPLACE VIEW storage.trash_items AS - SELECT id, name, 'file' AS item_type, user_id, trashed_at, - original_folder_id AS original_parent_id, created_at - FROM storage.files WHERE is_trashed = TRUE + SELECT f.id, f.name, 'file' AS item_type, f.user_id, f.trashed_at, + f.original_folder_id AS original_parent_id, f.created_at + FROM storage.files f + WHERE f.is_trashed = TRUE + AND (f.folder_id IS NULL + OR NOT EXISTS ( + SELECT 1 FROM storage.folders p + WHERE p.id = f.folder_id AND p.is_trashed = TRUE)) UNION ALL - SELECT id, name, 'folder' AS item_type, user_id, trashed_at, - original_parent_id, created_at - FROM storage.folders WHERE is_trashed = TRUE; + SELECT fo.id, fo.name, 'folder' AS item_type, fo.user_id, fo.trashed_at, + fo.original_parent_id, fo.created_at + FROM storage.folders fo + WHERE fo.is_trashed = TRUE + AND (fo.parent_id IS NULL + OR NOT EXISTS ( + SELECT 1 FROM storage.folders p + WHERE p.id = fo.parent_id AND p.is_trashed = TRUE)); COMMENT ON TABLE storage.folders IS 'Virtual folder hierarchy with ltree — no physical directories on disk'; COMMENT ON TABLE storage.files IS 'File metadata pointing to content-addressable blobs'; diff --git a/src/infrastructure/repositories/pg/folder_db_repository.rs b/src/infrastructure/repositories/pg/folder_db_repository.rs index eea07a31..b6855d04 100644 --- a/src/infrastructure/repositories/pg/folder_db_repository.rs +++ b/src/infrastructure/repositories/pg/folder_db_repository.rs @@ -457,7 +457,24 @@ impl FolderRepository for FolderDbRepository { } async fn delete_folder(&self, id: &str) -> Result<(), DomainError> { - // Hard delete folder and all descendants (CASCADE handles children) + // First, delete all files in this folder and descendant folders + // to avoid constraint violations from ON DELETE SET NULL + sqlx::query( + r#" + WITH RECURSIVE descendants AS ( + SELECT id FROM storage.folders WHERE id = $1::uuid + UNION ALL + SELECT f.id FROM storage.folders f JOIN descendants d ON f.parent_id = d.id + ) + DELETE FROM storage.files WHERE folder_id IN (SELECT id FROM descendants) + "#, + ) + .bind(id) + .execute(self.pool()) + .await + .map_err(|e| DomainError::internal_error("FolderDb", format!("delete files: {e}")))?; + + // Then delete the folder (CASCADE will remove descendant folders) let result = sqlx::query("DELETE FROM storage.folders WHERE id = $1::uuid") .bind(id) .execute(self.pool()) @@ -501,9 +518,10 @@ impl FolderRepository for FolderDbRepository { // ── Trash operations ── async fn move_to_trash(&self, folder_id: &str) -> Result<(), DomainError> { - // Atomic CTE: trash folder + all descendant files in a single statement. - // PostgreSQL executes the entire CTE as one atomic operation — no - // intermediate state where the folder is trashed but files are not. + // Only mark the folder itself as trashed. + // Child files and sub-folders are implicitly hidden because their + // ancestor is trashed — list queries already filter NOT is_trashed, + // and folder navigation won't reach a trashed folder's children. let result = sqlx::query_scalar::<_, i64>( r#" WITH trash_folder AS ( @@ -513,17 +531,6 @@ impl FolderRepository for FolderDbRepository { original_parent_id = parent_id, updated_at = NOW() WHERE id = $1::uuid AND NOT is_trashed - RETURNING id - ), - descendants AS ( - SELECT id FROM trash_folder - UNION ALL - SELECT f.id FROM storage.folders f JOIN descendants d ON f.parent_id = d.id - ), - trash_files AS ( - UPDATE storage.files - SET is_trashed = TRUE, trashed_at = NOW(), original_folder_id = folder_id - WHERE folder_id IN (SELECT id FROM descendants) AND NOT is_trashed RETURNING 1 ) SELECT COUNT(*) FROM trash_folder @@ -546,7 +553,9 @@ impl FolderRepository for FolderDbRepository { folder_id: &str, _original_path: &str, ) -> Result<(), DomainError> { - // Atomic CTE: restore folder + all descendant files in a single statement. + // Only restore the folder itself. + // Child files were never marked as trashed — they become visible + // again automatically once their parent folder is un-trashed. // The BEFORE UPDATE trigger on parent_id will recompute path/lpath // automatically when original_parent_id is restored. let result = sqlx::query_scalar::<_, i64>( @@ -559,20 +568,6 @@ impl FolderRepository for FolderDbRepository { original_parent_id = NULL, updated_at = NOW() WHERE id = $1::uuid AND is_trashed - RETURNING id - ), - descendants AS ( - SELECT id FROM restore_folder - UNION ALL - SELECT f.id FROM storage.folders f JOIN descendants d ON f.parent_id = d.id - ), - restore_files AS ( - UPDATE storage.files - SET is_trashed = FALSE, - trashed_at = NULL, - folder_id = COALESCE(original_folder_id, folder_id), - original_folder_id = NULL - WHERE folder_id IN (SELECT id FROM descendants) AND is_trashed RETURNING 1 ) SELECT COUNT(*) FROM restore_folder @@ -591,7 +586,23 @@ impl FolderRepository for FolderDbRepository { } async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError> { - // Permanently delete — CASCADE handles children + // First, delete all files in this folder and descendant folders + sqlx::query( + r#" + WITH RECURSIVE descendants AS ( + SELECT id FROM storage.folders WHERE id = $1::uuid + UNION ALL + SELECT f.id FROM storage.folders f JOIN descendants d ON f.parent_id = d.id + ) + DELETE FROM storage.files WHERE folder_id IN (SELECT id FROM descendants) + "#, + ) + .bind(folder_id) + .execute(self.pool()) + .await + .map_err(|e| DomainError::internal_error("FolderDb", format!("perm delete files: {e}")))?; + + // Then permanently delete folder — CASCADE handles descendant folders let result = sqlx::query("DELETE FROM storage.folders WHERE id = $1::uuid") .bind(folder_id) .execute(self.pool()) diff --git a/static/admin.html b/static/admin.html index dc637a83..cb0d918b 100644 --- a/static/admin.html +++ b/static/admin.html @@ -1,910 +1,260 @@ - - - - - -OxiCloud — Admin Panel - - - - - - - - -
- - -
- -
-
Loading…
-
-
-

Access Denied

-

Administrator privileges required to access this panel.

- Sign in -
- - -
- - - - - - - - - - - - - \ No newline at end of file + + + + + +OxiCloud — Admin Panel + + + + + + +
+ + +
+ +
+
Loading…
+
+
+

Access Denied

+

Administrator privileges required to access this panel.

+ Sign in +
+ +
+
+ + + +
+ +
+
+
—
Total Users
+
—
Active Users
+
—
Admins
+
—
Version
+
+ +
+

Storage Overview

+
+
—
Used
+
—
Total Quota
+
—
Usage %
+
+
+
+
+
0
Users >80% quota
+
0
Users over quota
+
+
+
+ +
+

System

+
+
—
Auth
+
—
OIDC
+
—
Quotas
+
+
+ + +
+
Public registration is disabled. Only admins can create new users.
+
+
+ +
+
+

User Management

+
+ + + + + + + + + + + + +
UserRoleStatusStorageLast LoginActions
Loading users…
+
+ +
+
+ +
+
+

Single Sign-On (OIDC / SSO)

+
+ + +
+
+
+ + +
+
+ + + OpenID Connect issuer URL of your identity provider +
+
+ +
+
+
+ + +
+
+ + + A client secret is already configured +
+
+ +
—
+
+
+ Advanced Settings +
+ + +
+
+ + +
+
+ + + Comma-separated OIDC group names that map to admin role +
+
+ + +
+
This will prevent ALL password-based logins!
+
+
+ + +
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/static/css/admin.css b/static/css/admin.css new file mode 100644 index 00000000..a444da70 --- /dev/null +++ b/static/css/admin.css @@ -0,0 +1,299 @@ +*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif} +body{background:#f5f7fa;color:#1e293b;min-height:100vh;display:flex;flex-direction:column} + +.hidden{display:none !important} +.show-block{display:block !important} +.show-flex{display:flex !important} +.link-reset-flex{text-decoration:none;color:inherit;display:flex;align-items:center;gap:14px} +.width-zero{width:0%} +.mt-14{margin-top:14px} +.toggle-row-strong{margin-top:10px;padding:12px 0;border-top:1px solid #e2e8f0} +.icon-muted-right{color:#64748b;margin-right:6px} +.h2-space-between{justify-content:space-between} +.flex-gap-6{display:flex;gap:6px} +.oidc-discover-wrap{margin-bottom:12px} +.secret-icon{color:#059669} +.small-muted{font-weight:400;color:#94a3b8} +.summary-icon-right{margin-right:6px} +.oidc-actions{display:flex;gap:10px;margin-top:24px;justify-content:flex-end} +.modal-title-icon{color:#ff5e3a;margin-right:8px} +.quota-input-row{display:flex;gap:10px;align-items:center} +.flex-1{flex:1} +.select-qm-unit{width:90px;padding:10px 14px;border:2px solid #e2e8f0;border-radius:10px;font-size:14px;background:#f8fafc} +.form-row{display:flex;gap:14px} +.select-cu-role{width:100%;padding:10px 14px;border:2px solid #e2e8f0;border-radius:10px;font-size:14px;background:#f8fafc} +.quota-row{display:flex;gap:6px} +.select-cu-quota-unit{width:70px;padding:10px 8px;border:2px solid #e2e8f0;border-radius:10px;font-size:13px;background:#f8fafc} +.alert-no-margin{margin-top:0} +.table-loading-cell{text-align:center;padding:28px;color:#94a3b8} +.table-status-error{color:#991b1b;padding:20px} +.table-status-empty{text-align:center;padding:28px;color:#94a3b8} +.user-self-badge{color:#94a3b8;font-weight:400} +.badge-admin-icon-small{font-size:10px} +.quota-progress-fixed{width:80px} +.user-last-login-cell{font-size:12px;color:#94a3b8} + +/* ── Scrollbar ── */ +::-webkit-scrollbar{width:8px} +::-webkit-scrollbar-track{background:transparent} +::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px} +::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25)} +*{scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent} + +/* ── Top Header Bar ── */ +.admin-header{ + background:linear-gradient(135deg,#2a3042 0%,#232838 100%); + padding:0 32px;height:64px;display:flex;align-items:center;justify-content:space-between; + box-shadow:0 2px 12px rgba(0,0,0,.15);position:sticky;top:0;z-index:100; +} +.admin-header-left{display:flex;align-items:center;gap:14px} +.admin-logo{ + width:38px;height:38px;background:linear-gradient(135deg,#ff5e3a,#ff2d55);border-radius:11px; + display:flex;align-items:center;justify-content:center; + box-shadow:0 3px 10px rgba(255,94,58,.35); +} +.admin-logo svg{width:20px;height:20px;fill:#fff} +.admin-title-text{font-size:17px;font-weight:700;color:#fff;letter-spacing:.3px} +.admin-title-separator{font-size:17px;color:rgba(255,255,255,.5);font-weight:400;margin-left:6px} +.admin-header-right a{ + color:rgba(255,255,255,.65);text-decoration:none;font-size:13px;font-weight:500; + display:flex;align-items:center;gap:6px;transition:color .2s; +} +.admin-header-right a:hover{color:#fff} + +/* ── Container ── */ +.admin-container{max-width:1080px;margin:0 auto;padding:28px 24px 60px;width:100%} + +/* ── Tabs ── */ +.admin-tabs{display:flex;gap:6px;margin-bottom:28px;background:#fff;border-radius:14px;padding:6px;box-shadow:0 1px 4px rgba(0,0,0,.06)} +.admin-tab{ + padding:10px 22px;cursor:pointer;font-weight:600;font-size:13.5px;color:#64748b; + border:none;background:none;border-radius:10px;transition:all .2s;display:flex;align-items:center;gap:8px; +} +.admin-tab:hover{color:#1e293b;background:#f8fafc} +.admin-tab.active{color:#fff;background:linear-gradient(135deg,#ff5e3a,#ff2d55);box-shadow:0 3px 12px rgba(255,94,58,.25)} +.admin-tab.active i{color:#fff} +.admin-tab i{font-size:14px;width:16px;text-align:center} +.tab-content{display:none} +.tab-content.active{display:block} + +/* ── Cards ── */ +.admin-card{background:#fff;border-radius:16px;box-shadow:0 1px 4px rgba(0,0,0,.06),0 0 0 1px rgba(0,0,0,.03);padding:28px;margin-bottom:22px} +.admin-card h2{font-size:16px;font-weight:700;margin-bottom:20px;color:#1e293b;display:flex;align-items:center;gap:10px} +.admin-card h2 i{color:#ff5e3a;font-size:17px} + +#main-content, +#ds-warn-card, +#ds-danger-card, +#registration-warning, +#oidc-form, +#secret-hint, +#password-warning, +#quota-modal, +#create-user-modal, +#reset-pw-modal{display:none} + +/* ── Stats Grid ── */ +.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:22px} +.stat-card{ + padding:20px;background:#f8fafc;border-radius:14px;text-align:center; + border:1px solid #e2e8f0;transition:transform .15s,box-shadow .15s; +} +.stat-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.06)} +.stat-value{font-size:1.75rem;font-weight:800;color:#1e293b} +.stat-label{font-size:11.5px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;margin-top:4px;font-weight:600} +.stat-card.warn{border-color:#fbbf24;background:#fffbeb} +.stat-card.danger{border-color:#ef4444;background:#fef2f2} +.text-blue{color:#3b82f6!important} +.text-green{color:#059669!important} +.text-orange{color:#d97706!important} +.text-red{color:#dc2626!important} + +/* ── Progress bar ── */ +.progress-bar{width:100%;height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden} +.progress-fill{height:100%;border-radius:4px;transition:width .5s ease} +.progress-fill.green{background:linear-gradient(90deg,#059669,#10b981)} +.progress-fill.orange{background:linear-gradient(90deg,#d97706,#f59e0b)} +.progress-fill.red{background:linear-gradient(90deg,#dc2626,#ef4444)} + +/* ── Table ── */ +.table-wrap{overflow-x:auto;border-radius:12px;border:1px solid #e2e8f0} +table{width:100%;border-collapse:collapse;font-size:13.5px} +th{text-align:left;padding:12px 16px;background:#f8fafc;color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:700;white-space:nowrap;border-bottom:1px solid #e2e8f0} +td{padding:14px 16px;border-bottom:1px solid #f1f5f9;vertical-align:middle} +tr:last-child td{border-bottom:none} +tr:hover{background:#fafbfd} +.user-info{display:flex;flex-direction:column;gap:2px} +.user-name{font-weight:600;color:#1e293b} +.user-email{font-size:12px;color:#94a3b8} + +/* ── Badges ── */ +.badge{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:3px 10px;border-radius:20px;font-weight:600} +.badge-admin{background:#dbeafe;color:#1d4ed8} +.badge-user{background:#f1f5f9;color:#64748b} +.badge-active{background:#d1fae5;color:#065f46} +.badge-inactive{background:#fee2e2;color:#991b1b} +.badge-oidc{background:#ede9fe;color:#6d28d9} +.badge-env{background:#fef3c7;color:#92400e;font-size:10px;padding:1px 6px;margin-left:4px} + +/* ── Buttons ── */ +.btn{ + padding:8px 18px;border:none;border-radius:10px;font-size:13px;font-weight:600; + cursor:pointer;transition:all .15s;white-space:nowrap;display:inline-flex;align-items:center;gap:6px; +} +.btn-sm{padding:6px 12px;font-size:12px;border-radius:8px} +.btn-primary{background:linear-gradient(135deg,#ff5e3a,#ff2d55);color:#fff;box-shadow:0 2px 8px rgba(255,94,58,.25)} +.btn-primary:hover{box-shadow:0 4px 14px rgba(255,94,58,.35);transform:translateY(-1px)} +.btn-secondary{background:#fff;color:#334155;border:1px solid #e2e8f0} +.btn-secondary:hover{background:#f8fafc;border-color:#cbd5e1} +.btn-danger{background:#fef2f2;color:#991b1b;border:1px solid #fecaca} +.btn-danger:hover{background:#fee2e2} +.btn-success{background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0} +.btn-success:hover{background:#d1fae5} +.btn:disabled{opacity:.4;cursor:not-allowed;transform:none!important} +.actions-row{display:flex;gap:6px;flex-wrap:wrap} + +/* ── Forms ── */ +.form-group{margin-bottom:16px} +.form-group label{display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:#334155} +.form-group input[type="text"],.form-group input[type="password"],.form-group input[type="url"],.form-group input[type="number"],.form-group select{ + width:100%;padding:10px 14px;border:2px solid #e2e8f0;border-radius:10px;font-size:14px; + background:#f8fafc;transition:all .2s;font-family:inherit;color:#1e293b; +} +.form-group input:focus,.form-group select:focus{outline:none;border-color:#ff5e3a;background:#fff;box-shadow:0 0 0 3px rgba(255,94,58,.1)} +.form-group small{color:#94a3b8;font-size:12px;display:block;margin-top:3px} + +.toggle-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0} +.toggle-row label{font-size:14px;font-weight:500;color:#334155} +.switch{position:relative;width:44px;height:24px;flex-shrink:0} +.switch input{opacity:0;width:0;height:0} +.slider{position:absolute;cursor:pointer;inset:0;background:#d1d5db;border-radius:24px;transition:.3s} +.slider:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s;box-shadow:0 1px 3px rgba(0,0,0,.15)} +.switch input:checked+.slider{background:linear-gradient(135deg,#ff5e3a,#ff2d55)} +.switch input:checked+.slider:before{transform:translateX(20px)} + +.readonly-field{ + display:flex;align-items:center;gap:8px;background:#f8fafc;border:2px solid #e2e8f0;border-radius:10px; + padding:10px 14px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:13px;word-break:break-all;color:#334155; +} +.readonly-field button{ + flex-shrink:0;padding:6px 10px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;cursor:pointer; + font-size:12px;transition:all .15s;color:#64748b; +} +.readonly-field button:hover{background:#f1f5f9;color:#334155} + +/* ── Alerts ── */ +.alert{padding:12px 16px;border-radius:10px;font-size:13px;margin-top:14px;display:none;font-weight:500} +.alert-success{background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0;display:block} +.alert-error{background:#fef2f2;color:#991b1b;border:1px solid #fecaca;display:block} +.alert-info{background:#eff6ff;color:#1e40af;border:1px solid #bfdbfe;display:block} +.warning{background:#fffbeb;border:1px solid #fde68a;border-radius:10px;padding:10px 14px;font-size:13px;color:#92400e;margin-top:8px;display:flex;align-items:center;gap:6px;font-weight:500} + +/* ── Discovery ── */ +.discovery-result{margin:12px 0;padding:14px;border-radius:10px;font-size:13px} +.discovery-result.ok{background:#ecfdf5;border:1px solid #a7f3d0;color:#065f46} +.discovery-result.fail{background:#fef2f2;border:1px solid #fecaca;color:#991b1b} +.discovery-result dt{font-weight:700;margin-top:6px} +.discovery-result dd{margin-left:0;word-break:break-all} + +/* ── Details ── */ +details{margin-top:14px;border-top:1px solid #e2e8f0;padding-top:12px} +details summary{cursor:pointer;font-weight:600;font-size:14px;color:#64748b;padding:6px 0;user-select:none;transition:color .2s} +details summary:hover{color:#ff5e3a} +details[open] summary{margin-bottom:14px;color:#ff5e3a} + +/* ── Modal ── */ +.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:1000} +.modal{background:#fff;border-radius:20px;padding:28px;width:420px;max-width:90vw;box-shadow:0 20px 60px rgba(0,0,0,.2);animation:modalIn .2s ease-out} +@keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}} +.modal h3{margin-bottom:18px;font-size:17px;color:#1e293b;display:flex;align-items:center} +.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:18px} + +/* ── Quota bar inline ── */ +.quota-bar{display:flex;align-items:center;gap:10px} +.quota-bar .progress-bar{flex:1;height:6px} +.quota-text{font-size:12px;color:#94a3b8;white-space:nowrap} + +/* ── Access / Loading ── */ +#access-denied{display:none;text-align:center;padding:80px 20px} +#access-denied .access-icon{width:80px;height:80px;background:#fef2f2;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 20px} +#access-denied .access-icon i{font-size:32px;color:#ef4444} +#access-denied h2{color:#991b1b;margin-bottom:8px;font-size:20px} +#access-denied p{color:#64748b;margin-bottom:20px;font-size:14px} +#access-denied a{display:inline-flex;align-items:center;gap:6px;padding:10px 24px;background:linear-gradient(135deg,#ff5e3a,#ff2d55);color:#fff;text-decoration:none;border-radius:10px;font-weight:600;font-size:14px;box-shadow:0 3px 12px rgba(255,94,58,.3);transition:all .2s} +#access-denied a:hover{transform:translateY(-1px);box-shadow:0 5px 18px rgba(255,94,58,.4)} +#loading{text-align:center;padding:80px;color:#94a3b8;font-size:15px} +#loading i{font-size:32px;color:#ff5e3a;display:block;margin-bottom:12px;animation:spin 1s linear infinite} +@keyframes spin{to{transform:rotate(360deg)}} + +/* ── Pagination ── */ +.pagination{display:flex;align-items:center;justify-content:space-between;margin-top:14px;font-size:13px;color:#94a3b8;padding:0 4px} +.pagination button{padding:6px 14px} + +/* ── Dark Mode ── */ +[data-theme="dark"] body{background:#0f172a;color:#e2e8f0} +[data-theme="dark"] ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15)} +[data-theme="dark"] *{scrollbar-color:rgba(255,255,255,.15) transparent} +[data-theme="dark"] .admin-tabs{background:#1e293b;box-shadow:0 1px 4px rgba(0,0,0,.2)} +[data-theme="dark"] .admin-tab{color:#94a3b8} +[data-theme="dark"] .admin-tab:hover{color:#f1f5f9;background:#162032} +[data-theme="dark"] .admin-card{background:#1e293b;box-shadow:0 1px 4px rgba(0,0,0,.2),0 0 0 1px rgba(255,255,255,.03)} +[data-theme="dark"] .admin-card h2{color:#f1f5f9} +[data-theme="dark"] .stat-card{background:#162032;border-color:#334155} +[data-theme="dark"] .stat-card:hover{box-shadow:0 4px 12px rgba(0,0,0,.15)} +[data-theme="dark"] .stat-value{color:#f1f5f9} +[data-theme="dark"] .stat-label{color:#64748b} +[data-theme="dark"] .stat-card.warn{border-color:#92400e;background:#422006} +[data-theme="dark"] .stat-card.danger{border-color:#991b1b;background:#3b1111} +[data-theme="dark"] .progress-bar{background:#334155} +[data-theme="dark"] .table-wrap{border-color:#334155} +[data-theme="dark"] th{background:#162032;color:#64748b;border-bottom-color:#334155} +[data-theme="dark"] td{border-bottom-color:#334155;color:#e2e8f0} +[data-theme="dark"] tr:hover{background:#162032} +[data-theme="dark"] .user-name{color:#f1f5f9} +[data-theme="dark"] .user-email{color:#64748b} +[data-theme="dark"] .badge-admin{background:#1e3a5f;color:#60a5fa} +[data-theme="dark"] .badge-user{background:#334155;color:#94a3b8} +[data-theme="dark"] .badge-active{background:#052e16;color:#86efac} +[data-theme="dark"] .badge-inactive{background:#3b1111;color:#fca5a5} +[data-theme="dark"] .badge-oidc{background:#2e1065;color:#c4b5fd} +[data-theme="dark"] .btn-secondary{background:#1e293b;color:#e2e8f0;border-color:#334155} +[data-theme="dark"] .btn-secondary:hover{background:#334155;border-color:#475569} +[data-theme="dark"] .btn-danger{background:#3b1111;color:#fca5a5;border-color:#991b1b} +[data-theme="dark"] .btn-danger:hover{background:#4a1515} +[data-theme="dark"] .btn-success{background:#052e16;color:#86efac;border-color:#065f46} +[data-theme="dark"] .btn-success:hover{background:#064e27} +[data-theme="dark"] .form-group label{color:#94a3b8} +[data-theme="dark"] .form-group input[type="text"], +[data-theme="dark"] .form-group input[type="password"], +[data-theme="dark"] .form-group input[type="url"], +[data-theme="dark"] .form-group input[type="number"], +[data-theme="dark"] .form-group select{background:#0f172a;border-color:#334155;color:#e2e8f0} +[data-theme="dark"] .form-group input:focus, +[data-theme="dark"] .form-group select:focus{border-color:#ff5e3a;background:#0f172a;box-shadow:0 0 0 3px rgba(255,94,58,.15)} +[data-theme="dark"] .form-group small{color:#64748b} +[data-theme="dark"] .toggle-row label{color:#94a3b8} +[data-theme="dark"] .slider{background:#475569} +[data-theme="dark"] .readonly-field{background:#0f172a;border-color:#334155;color:#e2e8f0} +[data-theme="dark"] .readonly-field button{background:#1e293b;border-color:#334155;color:#94a3b8} +[data-theme="dark"] .readonly-field button:hover{background:#334155;color:#f1f5f9} +[data-theme="dark"] .warning{background:#422006;border-color:#92400e;color:#fbbf24} +[data-theme="dark"] .alert-success{background:#052e16;color:#86efac;border-color:#065f46} +[data-theme="dark"] .alert-error{background:#3b1111;color:#fca5a5;border-color:#991b1b} +[data-theme="dark"] .alert-info{background:#0c2d48;color:#93c5fd;border-color:#1d4ed8} +[data-theme="dark"] .discovery-result.ok{background:#052e16;border-color:#065f46;color:#86efac} +[data-theme="dark"] .discovery-result.fail{background:#3b1111;border-color:#991b1b;color:#fca5a5} +[data-theme="dark"] details{border-top-color:#334155} +[data-theme="dark"] details summary{color:#94a3b8} +[data-theme="dark"] details summary:hover{color:#ff5e3a} +[data-theme="dark"] details[open] summary{color:#ff5e3a} +[data-theme="dark"] .modal{background:#1e293b;box-shadow:0 20px 60px rgba(0,0,0,.4)} +[data-theme="dark"] .modal h3{color:#f1f5f9} +[data-theme="dark"] .modal-overlay{background:rgba(0,0,0,.6);backdrop-filter:blur(4px)} +[data-theme="dark"] .quota-text{color:#64748b} +[data-theme="dark"] .pagination{color:#64748b} +[data-theme="dark"] #access-denied h2{color:#fca5a5} +[data-theme="dark"] #access-denied p{color:#94a3b8} +[data-theme="dark"] #access-denied .access-icon{background:#3b1111} +[data-theme="dark"] #loading{color:#64748b} +[data-theme="dark"] .toggle-row{border-top-color:#334155} diff --git a/static/css/profile.css b/static/css/profile.css new file mode 100644 index 00000000..a9966e17 --- /dev/null +++ b/static/css/profile.css @@ -0,0 +1,148 @@ +*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif} +body{background:#f5f7fa;color:#1e293b;min-height:100vh;display:flex;flex-direction:column} + +.link-reset-flex{text-decoration:none;color:inherit;display:flex;align-items:center;gap:14px} +.width-zero{width:0%} + +#main-content{display:none} + +/* ── Scrollbar ── */ +::-webkit-scrollbar{width:8px} +::-webkit-scrollbar-track{background:transparent} +::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px} +*{scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent} + +/* ── Header ── */ +.profile-header{ + background:linear-gradient(135deg,#2a3042 0%,#232838 100%); + padding:0 32px;height:64px;display:flex;align-items:center;justify-content:space-between; + box-shadow:0 2px 12px rgba(0,0,0,.15);position:sticky;top:0;z-index:100; +} +.profile-header-left{display:flex;align-items:center;gap:14px} +.profile-logo{ + width:38px;height:38px;background:linear-gradient(135deg,#ff5e3a,#ff2d55);border-radius:11px; + display:flex;align-items:center;justify-content:center; + box-shadow:0 3px 10px rgba(255,94,58,.35); +} +.profile-logo svg{width:20px;height:20px;fill:#fff} +.profile-title-text{font-size:17px;font-weight:700;color:#fff;letter-spacing:.3px} +.profile-title-separator{font-size:17px;color:rgba(255,255,255,.5);font-weight:400;margin-left:6px} +.profile-header-right a{ + color:rgba(255,255,255,.65);text-decoration:none;font-size:13px;font-weight:500; + display:flex;align-items:center;gap:6px;transition:color .2s; +} +.profile-header-right a:hover{color:#fff} + +/* ── Container ── */ +.profile-container{max-width:720px;margin:0 auto;padding:32px 24px 60px;width:100%} + +/* ── Card ── */ +.profile-card{background:#fff;border-radius:16px;box-shadow:0 1px 4px rgba(0,0,0,.06),0 0 0 1px rgba(0,0,0,.03);padding:32px;margin-bottom:22px} +.profile-card h2{font-size:16px;font-weight:700;margin-bottom:20px;color:#1e293b;display:flex;align-items:center;gap:10px} +.profile-card h2 i{color:#ff5e3a;font-size:17px} + +/* ── Avatar Section ── */ +.avatar-section{display:flex;align-items:center;gap:24px;margin-bottom:8px} +.avatar-large{ + width:88px;height:88px;border-radius:50%; + background:linear-gradient(135deg,#ff5e3a,#ff2d55); + display:flex;align-items:center;justify-content:center; + color:#fff;font-size:32px;font-weight:700;letter-spacing:1px; + box-shadow:0 6px 20px rgba(255,94,58,.3);flex-shrink:0; +} +.avatar-info h1{font-size:22px;font-weight:700;color:#1e293b;margin-bottom:4px} +.avatar-info .email{font-size:14px;color:#64748b;margin-bottom:8px} +.avatar-info .role-badge{ + display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600; + padding:4px 12px;border-radius:20px; +} +.role-badge-admin{background:#dbeafe;color:#1d4ed8} +.role-badge-user{background:#f1f5f9;color:#64748b} + +/* ── Info Grid ── */ +.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px} +@media(max-width:560px){.info-grid{grid-template-columns:1fr}} +.info-item{padding:16px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0} +.info-item .info-label{font-size:11.5px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;font-weight:700;margin-bottom:4px;display:flex;align-items:center;gap:6px} +.info-item .info-label i{font-size:12px;color:#cbd5e1} +.info-item .info-value{font-size:15px;font-weight:600;color:#1e293b} + +/* ── Storage ── */ +.storage-section{margin-top:4px} +.storage-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:16px} +@media(max-width:560px){.storage-stats{grid-template-columns:1fr}} +.storage-stat{text-align:center;padding:16px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0} +.storage-stat .stat-value{font-size:1.5rem;font-weight:800;color:#1e293b} +.storage-stat .stat-label{font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;font-weight:600;margin-top:2px} +.storage-bar-wrap{margin-top:4px} +.storage-bar{height:10px;background:#e2e8f0;border-radius:5px;overflow:hidden} +.storage-fill{height:100%;border-radius:5px;transition:width .6s ease} +.storage-fill.green{background:linear-gradient(90deg,#059669,#10b981)} +.storage-fill.orange{background:linear-gradient(90deg,#d97706,#f59e0b)} +.storage-fill.red{background:linear-gradient(90deg,#dc2626,#ef4444)} +.storage-text{font-size:12px;color:#94a3b8;text-align:right;margin-top:6px} + +/* ── Change Password ── */ +.form-group{margin-bottom:16px} +.form-group label{display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:#334155} +.form-group input{ + width:100%;padding:10px 14px;border:2px solid #e2e8f0;border-radius:10px;font-size:14px; + background:#f8fafc;transition:all .2s;font-family:inherit;color:#1e293b; +} +.form-group input:focus{outline:none;border-color:#ff5e3a;background:#fff;box-shadow:0 0 0 3px rgba(255,94,58,.1)} +.form-group small{color:#94a3b8;font-size:12px;display:block;margin-top:3px} + +.btn{ + padding:10px 22px;border:none;border-radius:10px;font-size:14px;font-weight:600; + cursor:pointer;transition:all .15s;white-space:nowrap;display:inline-flex;align-items:center;gap:6px; +} +.btn-primary{background:linear-gradient(135deg,#ff5e3a,#ff2d55);color:#fff;box-shadow:0 2px 8px rgba(255,94,58,.25)} +.btn-primary:hover{box-shadow:0 4px 14px rgba(255,94,58,.35);transform:translateY(-1px)} +.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none!important} + +.alert{padding:12px 16px;border-radius:10px;font-size:13px;margin-top:14px;font-weight:500} +.alert-success{background:#ecfdf5;color:#065f46;border:1px solid #a7f3d0} +.alert-error{background:#fef2f2;color:#991b1b;border:1px solid #fecaca} + +/* ── Loading / Error ── */ +#loading{text-align:center;padding:80px;color:#94a3b8;font-size:15px} +#loading i{font-size:32px;color:#ff5e3a;display:block;margin-bottom:12px;animation:spin 1s linear infinite} +@keyframes spin{to{transform:rotate(360deg)}} +#auth-error{display:none;text-align:center;padding:80px 20px} +#auth-error .err-icon{width:80px;height:80px;background:#fef2f2;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 20px} +#auth-error .err-icon i{font-size:32px;color:#ef4444} +#auth-error h2{color:#991b1b;margin-bottom:8px;font-size:20px} +#auth-error p{color:#64748b;margin-bottom:20px;font-size:14px} +#auth-error a{display:inline-flex;align-items:center;gap:6px;padding:10px 24px;background:linear-gradient(135deg,#ff5e3a,#ff2d55);color:#fff;text-decoration:none;border-radius:10px;font-weight:600;font-size:14px;box-shadow:0 3px 12px rgba(255,94,58,.3);transition:all .2s} +#auth-error a:hover{transform:translateY(-1px);box-shadow:0 5px 18px rgba(255,94,58,.4)} + +/* ── Dark Mode ── */ +[data-theme="dark"] body{background:#0f172a;color:#e2e8f0} +[data-theme="dark"] ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15)} +[data-theme="dark"] *{scrollbar-color:rgba(255,255,255,.15) transparent} +[data-theme="dark"] .profile-card{background:#1e293b;box-shadow:0 1px 4px rgba(0,0,0,.2),0 0 0 1px rgba(255,255,255,.03)} +[data-theme="dark"] .profile-card h2{color:#f1f5f9} +[data-theme="dark"] .avatar-info h1{color:#f1f5f9} +[data-theme="dark"] .avatar-info .email{color:#94a3b8} +[data-theme="dark"] .role-badge-admin{background:#1e3a5f;color:#60a5fa} +[data-theme="dark"] .role-badge-user{background:#334155;color:#94a3b8} +[data-theme="dark"] .info-item{background:#162032;border-color:#334155} +[data-theme="dark"] .info-item .info-label{color:#64748b} +[data-theme="dark"] .info-item .info-label i{color:#475569} +[data-theme="dark"] .info-item .info-value{color:#f1f5f9} +[data-theme="dark"] .storage-stat{background:#162032;border-color:#334155} +[data-theme="dark"] .storage-stat .stat-value{color:#f1f5f9} +[data-theme="dark"] .storage-stat .stat-label{color:#64748b} +[data-theme="dark"] .storage-bar{background:#334155} +[data-theme="dark"] .storage-text{color:#64748b} +[data-theme="dark"] .form-group label{color:#94a3b8} +[data-theme="dark"] .form-group input{background:#0f172a;border-color:#334155;color:#e2e8f0} +[data-theme="dark"] .form-group input:focus{border-color:#ff5e3a;background:#0f172a;box-shadow:0 0 0 3px rgba(255,94,58,.15)} +[data-theme="dark"] .form-group small{color:#64748b} +[data-theme="dark"] .alert-success{background:#052e16;color:#86efac;border-color:#065f46} +[data-theme="dark"] .alert-error{background:#3b1111;color:#fca5a5;border-color:#991b1b} +[data-theme="dark"] #auth-error{background:transparent} +[data-theme="dark"] #auth-error .err-icon{background:#3b1111} +[data-theme="dark"] #auth-error h2{color:#fca5a5} +[data-theme="dark"] #auth-error p{color:#94a3b8} +[data-theme="dark"] #loading{color:#64748b} diff --git a/static/identifier.sh b/static/identifier.sh deleted file mode 100644 index eaec9d05..00000000 --- a/static/identifier.sh +++ /dev/null @@ -1,8 +0,0 @@ -while IFS= read -r -d '' file; do - if grep -Iq . "$file"; then - echo "===== $file =====" - cat "$file" - echo -e "\n" - fi -done < <(find . -type f -print0) - diff --git a/static/index.html b/static/index.html index 75250b3a..feb71bd1 100644 --- a/static/index.html +++ b/static/index.html @@ -14,22 +14,33 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + +
diff --git a/static/profile.html b/static/profile.html index 7d12affb..1d831a00 100644 --- a/static/profile.html +++ b/static/profile.html @@ -1,416 +1,120 @@ - - - - - -OxiCloud — My Profile - - - - - - - - - - -
-
Loading…
-
-
-

Not Authenticated

-

Please sign in to view your profile.

- Sign in -
- - -
- - - - + + + + + +OxiCloud — My Profile + + + + + + + + +
+
Loading…
+
+
+

Not Authenticated

+

Please sign in to view your profile.

+ Sign in +
+ +
+
+
+
—
+
+

—

+ + — +
+
+
+ +
+

Account Details

+
+
+
Username
+
—
+
+
+
Email
+
—
+
+
+
Role
+
—
+
+
+
Last Login
+
—
+
+
+
+ +
+

Storage

+
+
+
—
+
Used
+
+
+
—
+
Quota
+
+
+
—
+
Usage
+
+
+
+
+
—
+
+
+ +
+

Change Password

+
+
+ + +
+
+ + + At least 8 characters +
+
+ + +
+ +
+
+
+
+
+ + + + diff --git a/static/shared.html b/static/shared.html index 6f41d14f..348d939d 100644 --- a/static/shared.html +++ b/static/shared.html @@ -8,7 +8,7 @@ - + @@ -247,9 +247,10 @@
- - - + + + + - + diff --git a/static/test.html b/static/test.html deleted file mode 100644 index a68b8d38..00000000 --- a/static/test.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - Test OxiCloud - - -

OxiCloud Test Page

-
- - - - - - \ No newline at end of file