Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9209f7a582 | ||
|
|
4a736b0224 | ||
|
|
f162a7d0d7 | ||
|
|
3fc526df7f | ||
|
|
20422cf5a7 | ||
|
|
492bab36ca | ||
|
|
f2f7697994 | ||
|
|
13aa011632 | ||
|
|
1add160f5d | ||
|
|
87368143b5 | ||
|
|
939aa032f0 | ||
|
|
fbd21a035b | ||
|
|
2f391d11db | ||
|
|
8c70783d5a | ||
|
|
b4d6f01432 | ||
|
|
d48b15a5f4 | ||
|
|
d1726f0160 | ||
|
|
bd1841b788 | ||
|
|
bde35d1d31 | ||
|
|
8d6a1be777 | ||
|
|
56f34ba362 | ||
|
|
4d329e046f | ||
|
|
f3977153fb |
361
CHANGELOG.md
@@ -1,5 +1,366 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 5/22/2025 v1.3.7
|
||||||
|
|
||||||
|
- `.folder-strip-container .folder-name` css added to center text below folder material icon.
|
||||||
|
- Override file share_url to always use current origin
|
||||||
|
- Update `fileList` css to keep file name wrapping tight.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/21/2025
|
||||||
|
|
||||||
|
- **Drag & Drop to Folder Strip**
|
||||||
|
- Enabled dragging files from the file list directly onto the folder-strip items.
|
||||||
|
- Hooked up `folderDragOverHandler`, `folderDragLeaveHandler`, and `folderDropHandler` to `.folder-strip-container .folder-item`.
|
||||||
|
- On drop, files are moved via `/api/file/moveFiles.php` and the file list is refreshed.
|
||||||
|
|
||||||
|
- **Restore files from trash Toast Message**
|
||||||
|
- Changed the restore handlers so that the toast always reports the actual file(s) restored (e.g. “Restored file: foo.txt”) instead of “No trash record found.”
|
||||||
|
- Removed reliance on backend message payload and now generate the confirmation text client-side based on selected items.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/20/2025 v1.3.6
|
||||||
|
|
||||||
|
- **domUtils.js**
|
||||||
|
- `updateFileActionButtons`
|
||||||
|
- Hide selection buttons (`Delete Files`, `Copy Files`, `Move Files` & `Download ZIP`) until file is selected.
|
||||||
|
- Hide `Extract ZIP` until selecting zip files
|
||||||
|
- Hide `Create File` button when file list items are selected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/19/2025 v1.3.5
|
||||||
|
|
||||||
|
### Added Folder strip & Create File
|
||||||
|
|
||||||
|
- **Folder strip in file list**
|
||||||
|
- `loadFileList` now fetches sub-folders in parallel from `/api/folder/getFolderList.php`.
|
||||||
|
- Filters to only *direct* children of the current folder, hiding `profile_pics` and `trash`.
|
||||||
|
- Injects a new `.folder-strip-container` just below the Files In above (summary + slider).
|
||||||
|
- Clicking a folder in the strip updates:
|
||||||
|
- the breadcrumb (via `updateBreadcrumbTitle`)
|
||||||
|
- the tree selection highlight
|
||||||
|
- reloads `loadFileList` for the chosen folder.
|
||||||
|
|
||||||
|
- **Create File feature**
|
||||||
|
- New “Create New File” button added to the file-actions toolbar and context menu.
|
||||||
|
- New endpoint `public/api/file/createFile.php` (handled by `FileController`/`FileModel`):
|
||||||
|
- Creates an empty file if it doesn’t already exist.
|
||||||
|
- Appends an entry to `<folder>_metadata.json` with `uploaded` timestamp and `uploader`.
|
||||||
|
- `fileActions.js`:
|
||||||
|
- Implemented `handleCreateFile()` to show a modal, POST to the new endpoint, and refresh the list.
|
||||||
|
- Added translations for `create_new_file` and `newfile_placeholder`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changees 5/15/2025
|
||||||
|
|
||||||
|
### Drag‐and‐Drop Upload extended to File List
|
||||||
|
|
||||||
|
- **Forward file‐list drops**
|
||||||
|
Dropping files onto the file‐list area (`#fileListContainer`) now re‐dispatches the same `drop` event to the upload card’s drop zone (`#uploadDropArea`)
|
||||||
|
- **Visual feedback**
|
||||||
|
Added a `.drop-hover` class on `#fileListContainer` during drag‐over for a dashed‐border + light‐background hover state to indicate it accepts file drops.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/14/2025 v1.3.4
|
||||||
|
|
||||||
|
### 1. Button Grouping (Bootstrap)
|
||||||
|
|
||||||
|
- Converted individual action buttons (`download`, `edit`, `rename`, `share`) in both **table view** and **gallery view** into a single Bootstrap button group for a cleaner, more compact UI.
|
||||||
|
- Applied `btn-group` and `btn-sm` classes for consistent sizing and spacing.
|
||||||
|
|
||||||
|
### 2. Header Dropdown Replacement
|
||||||
|
|
||||||
|
- Replaced the standalone “User Panel” icon button with a **dropdown wrapper** (`.user-dropdown`) in the header.
|
||||||
|
- Dropdown toggle now shows:
|
||||||
|
- **Profile picture** (if set) or the Material “account_circle” icon
|
||||||
|
- **Username** text (between avatar and caret)
|
||||||
|
- Down-arrow caret span.
|
||||||
|
|
||||||
|
### 3. Menu Items Moved to Dropdown
|
||||||
|
|
||||||
|
- Moved previously standalone header buttons into the dropdown menu:
|
||||||
|
- **User Panel** opens the modal
|
||||||
|
- **Admin Panel** only shown when `data.isAdmin` *and* on `demo.filerise.net`
|
||||||
|
- **API Docs** calls `openApiModal()`
|
||||||
|
- **Logout** calls `triggerLogout()`
|
||||||
|
- Each menu item now has a matching Material icon (e.g. `person`, `admin_panel_settings`, `description`, `logout`).
|
||||||
|
|
||||||
|
### 4. Profile Picture Support
|
||||||
|
|
||||||
|
- Added a new `/api/profile/uploadPicture.php` endpoint + `UserController::uploadPicture()` + corresponding `UserModel::setProfilePicture()`.
|
||||||
|
- On **Open User Panel**, display:
|
||||||
|
- Default avatar if none set
|
||||||
|
- Current profile picture if available
|
||||||
|
- In the **User Panel** modal:
|
||||||
|
- Stylish “edit” overlay icon on the avatar to launch file picker
|
||||||
|
- Auto-upload on file selection (no “Save” button click needed)
|
||||||
|
- Preview updates immediately and header avatar refreshes live
|
||||||
|
- Persisted in `users.txt` and re-fetched via `getCurrentUser.php`
|
||||||
|
|
||||||
|
### 5. API Docs & Logout Relocation
|
||||||
|
|
||||||
|
- Removed API Docs from User Panel
|
||||||
|
- Removed “Logout” buttons from the header toolbar.
|
||||||
|
- Both are now menu entries in the **User Dropdown**.
|
||||||
|
|
||||||
|
### 6. Admin Panel Conditional
|
||||||
|
|
||||||
|
- The **Admin Panel** button was:
|
||||||
|
- Kept in the dropdown only when `data.isAdmin`
|
||||||
|
- Removed entirely elsewhere.
|
||||||
|
|
||||||
|
### 7. Utility & Styling Tweaks
|
||||||
|
|
||||||
|
- Introduced a small `normalizePicUrl()` helper to strip stray colons and ensure a leading slash.
|
||||||
|
- Hidden the scrollbar in the User Panel modal via:
|
||||||
|
- Inline CSS (`scrollbar-width: none; -ms-overflow-style: none;`)
|
||||||
|
- Global/WebKit rule for `::-webkit-scrollbar { display: none; }`
|
||||||
|
- Made the User Panel modal fully responsive and vertically centered, with smooth dark-mode support.
|
||||||
|
|
||||||
|
### 8. File/List View & Gallery View Sliders
|
||||||
|
|
||||||
|
- **Unified “View‐Mode” Slider**
|
||||||
|
Added a single slider panel (`#viewSliderContainer`) in the file‐list actions toolbar that switches behavior based on the current view mode:
|
||||||
|
- **Table View**: shows a **Row Height** slider (min 31px, max 60px).
|
||||||
|
- Adjusts the CSS variable `--file-row-height` to resize all `<tr>` heights.
|
||||||
|
- Persists the chosen height in `localStorage`.
|
||||||
|
- **Gallery View**: shows a **Columns** slider (min 1, max 6).
|
||||||
|
- Updates the grid’s `grid-template-columns: repeat(N, 1fr)`.
|
||||||
|
- Persists the chosen column count in `localStorage`.
|
||||||
|
|
||||||
|
- **Injection Point**
|
||||||
|
The slider container is dynamically inserted (or updated) just before the folder summary (`#fileSummary`) in `loadFileList()`, ensuring a consistent position across both view modes.
|
||||||
|
|
||||||
|
- **Live Updates**
|
||||||
|
Moving the slider thumb immediately updates the visible table row heights or gallery column layout without a full re‐render.
|
||||||
|
|
||||||
|
- **Styling & Alignment**
|
||||||
|
- `#viewSliderContainer` uses `inline-flex` and `align-items: center` so that label, slider, and value text are vertically aligned with the other toolbar elements.
|
||||||
|
- Reset margins/padding on the label and value span within `#viewSliderContainer` to eliminate any vertical misalignment.
|
||||||
|
|
||||||
|
### 9. Fixed new issues with Undefined username in header on profile pic change & TOTP Enabled not checked
|
||||||
|
|
||||||
|
**openUserPanel**
|
||||||
|
|
||||||
|
- **Rewritten entirely with DOM APIs** instead of `innerHTML` for any user-supplied text to eliminates “DOM text reinterpreted as HTML” warnings.
|
||||||
|
- **Default avatar fallback**: now uses `'/assets/default-avatar.png'` whenever `profile_picture` is empty.
|
||||||
|
- **TOTP checkbox initial state** is now set from the `totp_enabled` value returned by the server.
|
||||||
|
- **Modal title sync** on reopen now updates the `(username)` correctly (no more “undefined” until refresh).
|
||||||
|
- **Re-sync on reopen**: background color, avatar, TOTP checkbox and language selector all update when reopen the panel.
|
||||||
|
|
||||||
|
**updateAuthenticatedUI**
|
||||||
|
|
||||||
|
- **Username fix**: dropdown toggle now always uses `data.username` so the name never becomes `undefined` after uploading a picture.
|
||||||
|
- **Profile URL update** via `fetchProfilePicture()` always writes into `localStorage` before rebuilding the header, ensuring avatar+name stay in sync instantly.
|
||||||
|
- **Dropdown rebuild logic** tweaked to update the toggle’s innerHTML with both avatar and username on every call.
|
||||||
|
|
||||||
|
**UserModel::getUser**
|
||||||
|
|
||||||
|
- Switched to `explode(':', $line, 4)` to the fourth “profile_picture” field without clobbering the TOTP secret.
|
||||||
|
- **Strip trailing colons** from the stored URL (`rtrim($parts[3], ':')`) so we never send `…png:` back to the client.
|
||||||
|
- Returns an array with both `'username'` and `'profile_picture'`, matching what `getCurrentUser.php` needs.
|
||||||
|
|
||||||
|
### 10. setAttribute + encodeURI to avoid “DOM text reinterpreted as HTML” alerts
|
||||||
|
|
||||||
|
### 11. Fix duplicated Upload & Folder cards if they were added to header and page was refreshed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/8/2025
|
||||||
|
|
||||||
|
### Docker 🐳
|
||||||
|
|
||||||
|
- Ensure `/var/www/config` exists and is owned by `www-data` (chmod 750) so that `start.sh`’s `sed -i` updates to `config.php` work reliably
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/8/2025 v1.3.3
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- **Admin API** (`updateConfig.php`):
|
||||||
|
- Now merges incoming payload onto existing on-disk settings instead of overwriting blanks.
|
||||||
|
- Preserves `clientId`, `clientSecret`, `providerUrl` and `redirectUri` when those fields are omitted or empty in the request.
|
||||||
|
|
||||||
|
- **Admin API** (`getConfig.php`):
|
||||||
|
- Returns only a safe subset of admin settings (omits `clientSecret`) to prevent accidental exposure of sensitive data.
|
||||||
|
|
||||||
|
- **Frontend** (`auth.js`):
|
||||||
|
- Update UI based on merged loginOptions from the server, ensuring blank or missing fields no longer revert your existing config.
|
||||||
|
|
||||||
|
- **Auth API** (`auth.php`):
|
||||||
|
- Added `$oidc->addScope(['openid','profile','email']);` to OIDC flow. (This should resolve authentik issue)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/8/2025 v1.3.2
|
||||||
|
|
||||||
|
### config/config.php
|
||||||
|
|
||||||
|
- Added a default `define('AUTH_BYPASS', false)` at the top so the constant always exists.
|
||||||
|
- Removed the static `AUTH_HEADER` fallback; instead read the adminConfig.json at the end of the file and:
|
||||||
|
- Overwrote `AUTH_BYPASS` with the `loginOptions.authBypass` setting from disk.
|
||||||
|
- Defined `AUTH_HEADER` (normalized, e.g. `"X_REMOTE_USER"`) based on `loginOptions.authHeaderName`.
|
||||||
|
- Inserted a **proxy-only auto-login** block *before* the usual session/auth checks:
|
||||||
|
If `AUTH_BYPASS` is true and the trusted header (`$_SERVER['HTTP_' . AUTH_HEADER]`) is present, bump the session, mark the user authenticated/admin, load their permissions, and skip straight to JSON output.
|
||||||
|
- Relax filename validation regex to allow broader Unicode and special chars
|
||||||
|
|
||||||
|
### src/controllers/AdminController.php
|
||||||
|
|
||||||
|
- Ensured the returned `loginOptions` object always contains:
|
||||||
|
- `authBypass` (boolean, default false)
|
||||||
|
- `authHeaderName` (string, default `"X-Remote-User"`)
|
||||||
|
- Read `authBypass` and `authHeaderName` from the nested `loginOptions` in the request payload.
|
||||||
|
- Validated them (`authBypass` → bool; `authHeaderName` → non-empty string, fallback to `"X-Remote-User"`).
|
||||||
|
- Included them when building the `$configUpdate` array to pass to the model.
|
||||||
|
|
||||||
|
### src/models/AdminModel.php
|
||||||
|
|
||||||
|
- Normalized `loginOptions.authBypass` to a boolean (default false).
|
||||||
|
- Validated/truncated `loginOptions.authHeaderName` to a non-empty trimmed string (default `"X-Remote-User"`).
|
||||||
|
- JSON-encoded and encrypted the full config, now including the two new fields.
|
||||||
|
- After decrypting & decoding, normalized the loaded `loginOptions` to always include:
|
||||||
|
- `authBypass` (bool)
|
||||||
|
- `authHeaderName` (string, default `"X-Remote-User"`)
|
||||||
|
- Left all existing defaults & validations for the original flags intact.
|
||||||
|
|
||||||
|
### public/js/adminPanel.js
|
||||||
|
|
||||||
|
- **Login Options** section:
|
||||||
|
- Added a checkbox for **Disable All Built-in Logins (proxy only)** (`authBypass`).
|
||||||
|
- Added a text input for **Auth Header Name** (`authHeaderName`).
|
||||||
|
- In `handleSave()`:
|
||||||
|
- Included the new `authBypass` and `authHeaderName` values in the payload sent to `updateConfig.php`.
|
||||||
|
- In `openAdminPanel()`:
|
||||||
|
- Initialized those inputs from `config.loginOptions.authBypass` and `config.loginOptions.authHeaderName`.
|
||||||
|
|
||||||
|
### public/js/auth.js
|
||||||
|
|
||||||
|
- In `loadAdminConfigFunc()`:
|
||||||
|
- Stored `authBypass` and `authHeaderName` in `localStorage`.
|
||||||
|
- In `checkAuthentication()`:
|
||||||
|
- After a successful login check, called a new helper (`applyProxyBypassUI()`) which reads `localStorage.authBypass` and conditionally hides the entire login form/UI.
|
||||||
|
- In the “not authenticated” branch, only shows the login form if `authBypass` is false.
|
||||||
|
- No other core fetch/token logic changed; all existing flows remain intact.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **Admin API**: `getConfig.php` now returns only a safe subset of admin settings (omits `clientSecret`) to prevent accidental exposure of sensitive data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/4/2025 v1.3.1
|
||||||
|
|
||||||
|
### Modals
|
||||||
|
|
||||||
|
- **Added** a shared `.editor-close-btn` component for all modals:
|
||||||
|
- File Tags
|
||||||
|
- User Panel
|
||||||
|
- TOTP Login & Setup
|
||||||
|
- Change Password
|
||||||
|
- **Truncated** long filenames in the File Tags modal header using CSS `text-overflow: ellipsis`.
|
||||||
|
- **Resized** File Tags modal from 400px to 450px wide (with `max-width: 90vw` fallback).
|
||||||
|
- **Capped** User Panel height at 381px and hidden scrollbars to eliminate layout jumps on hover.
|
||||||
|
|
||||||
|
### HTML
|
||||||
|
|
||||||
|
- **Moved** `<div id="loginForm">…</div>` out of `.main-wrapper` so the login form can show independently of the app shell.
|
||||||
|
- **Added** `<div id="loadingOverlay"></div>` immediately inside `<body>` to cover the UI during auth checks.
|
||||||
|
- **Inserted** inline `<style>` in `<head>` to:
|
||||||
|
- Hide `.main-wrapper` by default.
|
||||||
|
- Style `#loadingOverlay` as a full-viewport white overlay.
|
||||||
|
|
||||||
|
- **Added** `addUserModal`, `removeUserModal` & `renameFileModal` modals to `style="display:none;"`
|
||||||
|
|
||||||
|
### `main.js`
|
||||||
|
|
||||||
|
- **Extracted** `initializeApp()` helper to centralize post-auth startup (tag search, file list, drag-and-drop, folder tree, upload, trash/restore, admin config).
|
||||||
|
- **Updated** DOMContentLoaded `checkAuthentication()` flow to call `initializeApp()` when already authenticated.
|
||||||
|
- **Extended** `updateAuthenticatedUI()` to call `initializeApp()` after a fresh login so all UI modules re-hydrate.
|
||||||
|
- **Enhanced** setup-mode in `checkAuthentication()`:
|
||||||
|
- Show `#addUserModal` as a flex overlay (`style.display = 'flex'`).
|
||||||
|
- Keep `.main-wrapper` hidden until setup completes.
|
||||||
|
- **Added** post-setup handler in the Add-User modal’s save button:
|
||||||
|
- Hide setup modal.
|
||||||
|
- Show login form.
|
||||||
|
- Keep app shell hidden.
|
||||||
|
- Pre-fill and focus the new username in the login inputs.
|
||||||
|
|
||||||
|
### `auth.js` / Auth Logic
|
||||||
|
|
||||||
|
- **Refactored** `checkAuthentication()` to handle three states:
|
||||||
|
1. **`data.setup`** remove overlay, hide main UI, show setup modal.
|
||||||
|
2. **`data.authenticated`** remove overlay, call `updateAuthenticatedUI()`.
|
||||||
|
3. **not authenticated** remove overlay, show login form, keep main UI hidden.
|
||||||
|
- **Refined** `updateAuthenticatedUI()` to:
|
||||||
|
- Remove loading overlay.
|
||||||
|
- Show `.main-wrapper` and main operations.
|
||||||
|
- Hide `#loginForm`.
|
||||||
|
- Reveal header buttons.
|
||||||
|
- Initialize dynamic header buttons (restore, admin, user-panel).
|
||||||
|
- Call `initializeApp()` to load all modules after login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 5/3/2025 v1.3.0
|
||||||
|
|
||||||
|
**Admin Panel Refactor & Enhancements**
|
||||||
|
|
||||||
|
### Moved from `authModals.js` to `adminPanel.js`
|
||||||
|
|
||||||
|
- Extracted all admin-related UI and logic out of `authModals.js`
|
||||||
|
- Created a standalone `adminPanel.js` module
|
||||||
|
- Initialized `openAdminPanel()` and `closeAdminPanel()` exports
|
||||||
|
|
||||||
|
### Responsive, Collapsible Sections
|
||||||
|
|
||||||
|
- Injected new CSS via JS (`adminPanelStyles`)
|
||||||
|
- Default modal width: 50%
|
||||||
|
- Small-screen override (`@media (max-width: 600px)`) to 90% width
|
||||||
|
- Introduced `.section-header` / `.section-content` pattern
|
||||||
|
- Click header to expand/collapse its content
|
||||||
|
- Animated arrow via Material Icons
|
||||||
|
- Indented and padded expanded content
|
||||||
|
|
||||||
|
### “Manage Shared Links” Feature
|
||||||
|
|
||||||
|
- Added new **Manage Shared Links** section to Admin Panel
|
||||||
|
- Endpoint **GET** `/api/admin/readMetadata.php?file=…`
|
||||||
|
- Reads `share_folder_links.json` & `share_links.json` under `META_DIR`
|
||||||
|
- Endpoint **POST**
|
||||||
|
- `/api/folder/deleteShareFolderLink.php`
|
||||||
|
- `/api/file/deleteShareLink.php`
|
||||||
|
- `loadShareLinksSection()` AJAX loader
|
||||||
|
- Displays folder & file shares, expiry dates, upload-allowed, and 🔒 if password-protected
|
||||||
|
- “🗑️” delete buttons refresh the list on success
|
||||||
|
|
||||||
|
### Dark-Mode & Theming Fixes
|
||||||
|
|
||||||
|
- Dark-mode CSS overrides for:
|
||||||
|
- Modal border
|
||||||
|
- `.btn-primary`, `.btn-secondary`
|
||||||
|
- `.form-control` backgrounds & placeholders
|
||||||
|
- Section headers & icons
|
||||||
|
- Close button restyled to use shared **.editor-close-btn** look
|
||||||
|
|
||||||
|
### API and Controller changes
|
||||||
|
|
||||||
|
- Updated all endpoints to use correct controller casing
|
||||||
|
- Renamed controller files to PascalCase (e.g. `adminController.php` to `AdminController.php`, `fileController.php` to `FileController.php`, `folderController.php` to `FolderController.php`)
|
||||||
|
- Adjusted endpoint paths to match controller filenames
|
||||||
|
- Fix FolderController readOnly create folder permission
|
||||||
|
|
||||||
|
### Additional changes
|
||||||
|
|
||||||
|
- Extend clean up expired shared entries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 4/30/2025 v1.2.8
|
## Changes 4/30/2025 v1.2.8
|
||||||
|
|
||||||
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.
|
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ COPY custom-php.ini /etc/php/8.3/apache2/conf.d/99-app-tuning.ini
|
|||||||
COPY --from=appsource /var/www /var/www
|
COPY --from=appsource /var/www /var/www
|
||||||
COPY --from=composer /app/vendor /var/www/vendor
|
COPY --from=composer /app/vendor /var/www/vendor
|
||||||
|
|
||||||
|
# ── ensure config/ is writable by www-data so sed -i can work ──
|
||||||
|
RUN mkdir -p /var/www/config \
|
||||||
|
&& chown -R www-data:www-data /var/www/config \
|
||||||
|
&& chmod 750 /var/www/config
|
||||||
|
|
||||||
# Secure permissions: code read-only, only data dirs writable
|
# Secure permissions: code read-only, only data dirs writable
|
||||||
RUN chown -R root:www-data /var/www && \
|
RUN chown -R root:www-data /var/www && \
|
||||||
find /var/www -type d -exec chmod 755 {} \; && \
|
find /var/www -type d -exec chmod 755 {} \; && \
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ Areas where you can help: translations, bug fixes, UI improvements, or building
|
|||||||
|
|
||||||
## Community and Support
|
## Community and Support
|
||||||
|
|
||||||
- **Reddit:** [r/selfhosted: FileRise Discussion](https://www.reddit.com/r/selfhosted/comments/1jl01pi/introducing_filerise_a_modern_selfhosted_file/) – (Announcement and user feedback thread).
|
- **Reddit:** [r/selfhosted: FileRise Discussion](https://www.reddit.com/r/selfhosted/comments/1kfxo9y/filerise_v131_major_updates_sneak_peek_at_whats/) – (Announcement and user feedback thread).
|
||||||
- **Unraid Forums:** [FileRise Support Thread](https://forums.unraid.net/topic/187337-support-filerise/) – for Unraid-specific support or issues.
|
- **Unraid Forums:** [FileRise Support Thread](https://forums.unraid.net/topic/187337-support-filerise/) – for Unraid-specific support or issues.
|
||||||
- **GitHub Discussions:** Use the Q&A category for any setup questions, and the Ideas category to suggest enhancements.
|
- **GitHub Discussions:** Use the Q&A category for any setup questions, and the Ideas category to suggest enhancements.
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ define('DATE_TIME_FORMAT','m/d/y h:iA');
|
|||||||
define('TOTAL_UPLOAD_SIZE','5G');
|
define('TOTAL_UPLOAD_SIZE','5G');
|
||||||
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
define('REGEX_FOLDER_NAME', '/^[\p{L}\p{N}_\-\s\/\\\\]+$/u');
|
||||||
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
define('PATTERN_FOLDER_NAME','[\p{L}\p{N}_\-\s\/\\\\]+');
|
||||||
define('REGEX_FILE_NAME', '/^[\p{L}\p{N}\p{M}%\-\.\(\) _]+$/u');
|
define('REGEX_FILE_NAME', '/^[^\x00-\x1F\/\\\\]{1,255}$/u');
|
||||||
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
define('REGEX_USER', '/^[\p{L}\p{N}_\- ]+$/u');
|
||||||
|
|
||||||
date_default_timezone_set(TIMEZONE);
|
date_default_timezone_set(TIMEZONE);
|
||||||
|
|
||||||
|
|
||||||
// Encryption helpers
|
// Encryption helpers
|
||||||
function encryptData($data, $encryptionKey)
|
function encryptData($data, $encryptionKey)
|
||||||
{
|
{
|
||||||
@@ -114,6 +115,7 @@ if (empty($_SESSION['csrf_token'])) {
|
|||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Auto‑login via persistent token
|
// Auto‑login via persistent token
|
||||||
if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token'])) {
|
if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token'])) {
|
||||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
@@ -140,6 +142,60 @@ if (empty($_SESSION["authenticated"]) && !empty($_COOKIE['remember_me_token']))
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||||
|
|
||||||
|
// sane defaults:
|
||||||
|
$cfgAuthBypass = false;
|
||||||
|
$cfgAuthHeader = 'X_REMOTE_USER';
|
||||||
|
|
||||||
|
if (file_exists($adminConfigFile)) {
|
||||||
|
$encrypted = file_get_contents($adminConfigFile);
|
||||||
|
$decrypted = decryptData($encrypted, $encryptionKey);
|
||||||
|
$adminCfg = json_decode($decrypted, true) ?: [];
|
||||||
|
|
||||||
|
$loginOpts = $adminCfg['loginOptions'] ?? [];
|
||||||
|
|
||||||
|
// proxy-only bypass flag
|
||||||
|
$cfgAuthBypass = ! empty($loginOpts['authBypass']);
|
||||||
|
|
||||||
|
// header name (e.g. “X-Remote-User” → HTTP_X_REMOTE_USER)
|
||||||
|
$hdr = trim($loginOpts['authHeaderName'] ?? '');
|
||||||
|
if ($hdr === '') {
|
||||||
|
$hdr = 'X-Remote-User';
|
||||||
|
}
|
||||||
|
// normalize to PHP’s $_SERVER key format:
|
||||||
|
$cfgAuthHeader = 'HTTP_' . strtoupper(str_replace('-', '_', $hdr));
|
||||||
|
}
|
||||||
|
|
||||||
|
define('AUTH_BYPASS', $cfgAuthBypass);
|
||||||
|
define('AUTH_HEADER', $cfgAuthHeader);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// PROXY-ONLY AUTO–LOGIN now uses those constants:
|
||||||
|
if (AUTH_BYPASS) {
|
||||||
|
$hdrKey = AUTH_HEADER; // e.g. "HTTP_X_REMOTE_USER"
|
||||||
|
if (!empty($_SERVER[$hdrKey])) {
|
||||||
|
// regenerate once per session
|
||||||
|
if (empty($_SESSION['authenticated'])) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $_SERVER[$hdrKey];
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['username'] = $username;
|
||||||
|
|
||||||
|
// ◾ lookup actual role instead of forcing admin
|
||||||
|
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||||||
|
$role = AuthModel::getUserRole($username);
|
||||||
|
$_SESSION['isAdmin'] = ($role === '1');
|
||||||
|
|
||||||
|
// carry over any folder/read/upload perms
|
||||||
|
$perms = loadUserPermissions($username) ?: [];
|
||||||
|
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||||
|
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||||
|
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Share URL fallback
|
// Share URL fallback
|
||||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/addUser.php
|
// public/api/addUser.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->addUser();
|
$userController->addUser();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/admin/getConfig.php
|
// public/api/admin/getConfig.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||||
|
|
||||||
$adminController = new AdminController();
|
$adminController = new AdminController();
|
||||||
$adminController->getConfig();
|
$adminController->getConfig();
|
||||||
63
public/api/admin/readMetadata.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/admin/readMetadata.php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
|
||||||
|
// Only admins may read these
|
||||||
|
if (empty($_SESSION['isAdmin']) || $_SESSION['isAdmin'] !== true) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Forbidden']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must supply ?file=share_links.json or share_folder_links.json
|
||||||
|
if (empty($_GET['file'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Missing `file` parameter']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = basename($_GET['file']);
|
||||||
|
$allowed = ['share_links.json', 'share_folder_links.json'];
|
||||||
|
if (!in_array($file, $allowed, true)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Invalid file requested']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = META_DIR . $file;
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
// Return empty object so JS sees `{}` not an error
|
||||||
|
http_response_code(200);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode((object)[]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jsonData = file_get_contents($path);
|
||||||
|
$data = json_decode($jsonData, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Corrupted JSON']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Clean up expired entries ———
|
||||||
|
$now = time();
|
||||||
|
$changed = false;
|
||||||
|
foreach ($data as $token => $entry) {
|
||||||
|
if (!empty($entry['expires']) && $entry['expires'] < $now) {
|
||||||
|
unset($data[$token]);
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($changed) {
|
||||||
|
// overwrite file with cleaned data
|
||||||
|
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ——— Send cleaned data back ———
|
||||||
|
http_response_code(200);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/admin/updateConfig.php
|
// public/api/admin/updateConfig.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/adminController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AdminController.php';
|
||||||
|
|
||||||
$adminController = new AdminController();
|
$adminController = new AdminController();
|
||||||
$adminController->updateConfig();
|
$adminController->updateConfig();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||||
|
|
||||||
$authController = new AuthController();
|
$authController = new AuthController();
|
||||||
$authController->auth();
|
$authController->auth();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/checkAuth.php
|
// public/api/auth/checkAuth.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||||
|
|
||||||
$authController = new AuthController();
|
$authController = new AuthController();
|
||||||
$authController->checkAuth();
|
$authController->checkAuth();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/login_basic.php
|
// public/api/auth/login_basic.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||||
|
|
||||||
$authController = new AuthController();
|
$authController = new AuthController();
|
||||||
$authController->loginBasic();
|
$authController->loginBasic();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/logout.php
|
// public/api/auth/logout.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||||
|
|
||||||
$authController = new AuthController();
|
$authController = new AuthController();
|
||||||
$authController->logout();
|
$authController->logout();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/auth/token.php
|
// public/api/auth/token.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/authController.php';
|
require_once PROJECT_ROOT . '/src/controllers/AuthController.php';
|
||||||
|
|
||||||
$authController = new AuthController();
|
$authController = new AuthController();
|
||||||
$authController->getToken();
|
$authController->getToken();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/changePassword.php
|
// public/api/changePassword.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->changePassword();
|
$userController->changePassword();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/copyFiles.php
|
// public/api/file/copyFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->copyFiles();
|
$fileController->copyFiles();
|
||||||
15
public/api/file/createFile.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/file/createFile.php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
if (empty($_SESSION['authenticated'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success'=>false,'error'=>'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fc = new FileController();
|
||||||
|
$fc->createFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/createShareLink.php
|
// public/api/file/createShareLink.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->createShareLink();
|
$fileController->createShareLink();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/deleteFiles.php
|
// public/api/file/deleteFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->deleteFiles();
|
$fileController->deleteFiles();
|
||||||
6
public/api/file/deleteShareLink.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
|
$fileController = new FileController();
|
||||||
|
$fileController->deleteShareLink();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/deleteTrashFiles.php
|
// public/api/file/deleteTrashFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->deleteTrashFiles();
|
$fileController->deleteTrashFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/download.php
|
// public/api/file/download.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->downloadFile();
|
$fileController->downloadFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/downloadZip.php
|
// public/api/file/downloadZip.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->downloadZip();
|
$fileController->downloadZip();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/extractZip.php
|
// public/api/file/extractZip.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->extractZip();
|
$fileController->extractZip();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/getFileList.php
|
// public/api/file/getFileList.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->getFileList();
|
$fileController->getFileList();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/getFileTag.php
|
// public/api/file/getFileTag.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->getFileTags();
|
$fileController->getFileTags();
|
||||||
6
public/api/file/getShareLinks.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
|
$fileController = new FileController();
|
||||||
|
$fileController->getShareLinks();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/getTrashItems.php
|
// public/api/file/getTrashItems.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->getTrashItems();
|
$fileController->getTrashItems();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/moveFiles.php
|
// public/api/file/moveFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->moveFiles();
|
$fileController->moveFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/renameFile.php
|
// public/api/file/renameFile.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->renameFile();
|
$fileController->renameFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/restoreFiles.php
|
// public/api/file/restoreFiles.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->restoreFiles();
|
$fileController->restoreFiles();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/saveFile.php
|
// public/api/file/saveFile.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->saveFile();
|
$fileController->saveFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/saveFileTag.php
|
// public/api/file/saveFileTag.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->saveFileTag();
|
$fileController->saveFileTag();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/file/share.php
|
// public/api/file/share.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/fileController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FileController.php';
|
||||||
|
|
||||||
$fileController = new FileController();
|
$fileController = new FileController();
|
||||||
$fileController->shareFile();
|
$fileController->shareFile();
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cd /var/www/public
|
|
||||||
ln -s ../uploads uploads
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/createFolder.php
|
// public/api/folder/createFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->createFolder();
|
$folderController->createFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/createShareFolderLink.php
|
// public/api/folder/createShareFolderLink.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->createShareFolderLink();
|
$folderController->createShareFolderLink();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/deleteFolder.php
|
// public/api/folder/deleteFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->deleteFolder();
|
$folderController->deleteFolder();
|
||||||
6
public/api/folder/deleteShareFolderLink.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
|
$folderController = new FolderController();
|
||||||
|
$folderController->deleteShareFolderLink();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/downloadSharedFile.php
|
// public/api/folder/downloadSharedFile.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->downloadSharedFile();
|
$folderController->downloadSharedFile();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/getFolderList.php
|
// public/api/folder/getFolderList.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->getFolderList();
|
$folderController->getFolderList();
|
||||||
6
public/api/folder/getShareFolderLinks.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
|
$folderController = new FolderController();
|
||||||
|
$folderController->getShareFolderLinks();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/renameFolder.php
|
// public/api/folder/renameFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->renameFolder();
|
$folderController->renameFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/shareFolder.php
|
// public/api/folder/shareFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->shareFolder();
|
$folderController->shareFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/folder/uploadToSharedFolder.php
|
// public/api/folder/uploadToSharedFolder.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/folderController.php';
|
require_once PROJECT_ROOT . '/src/controllers/FolderController.php';
|
||||||
|
|
||||||
$folderController = new FolderController();
|
$folderController = new FolderController();
|
||||||
$folderController->uploadToSharedFolder();
|
$folderController->uploadToSharedFolder();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/getUserPermissions.php
|
// public/api/getUserPermissions.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->getUserPermissions();
|
$userController->getUserPermissions();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/getUsers.php
|
// public/api/getUsers.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->getUsers(); // This will output the JSON response
|
$userController->getUsers(); // This will output the JSON response
|
||||||
15
public/api/profile/getCurrentUser.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if (empty($_SESSION['authenticated'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error'=>'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $_SESSION['username'];
|
||||||
|
$data = UserModel::getUser($user);
|
||||||
|
echo json_encode($data);
|
||||||
17
public/api/profile/uploadPicture.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
|
// Always JSON, even on PHP notices
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userController = new UserController();
|
||||||
|
$userController->uploadPicture();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Exception: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/removeUser.php
|
// public/api/removeUser.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->removeUser();
|
$userController->removeUser();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->disableTOTP();
|
$userController->disableTOTP();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/totp_recover.php
|
// public/api/totp_recover.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->recoverTOTP();
|
$userController->recoverTOTP();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/totp_saveCode.php
|
// public/api/totp_saveCode.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->saveTOTPRecoveryCode();
|
$userController->saveTOTPRecoveryCode();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->setupTOTP();
|
$userController->setupTOTP();
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->verifyTOTP();
|
$userController->verifyTOTP();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/updateUserPanel.php
|
// public/api/updateUserPanel.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->updateUserPanel();
|
$userController->updateUserPanel();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/updateUserPermissions.php
|
// public/api/updateUserPermissions.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/userController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UserController.php';
|
||||||
|
|
||||||
$userController = new UserController();
|
$userController = new UserController();
|
||||||
$userController->updateUserPermissions();
|
$userController->updateUserPermissions();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// public/api/upload/removeChunks.php
|
// public/api/upload/removeChunks.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||||
|
|
||||||
$uploadController = new UploadController();
|
$uploadController = new UploadController();
|
||||||
$uploadController->removeChunks();
|
$uploadController->removeChunks();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// public/api/upload/upload.php
|
// public/api/upload/upload.php
|
||||||
require_once __DIR__ . '/../../../config/config.php';
|
require_once __DIR__ . '/../../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/controllers/uploadController.php';
|
require_once PROJECT_ROOT . '/src/controllers/UploadController.php';
|
||||||
|
|
||||||
$uploadController = new UploadController();
|
$uploadController = new UploadController();
|
||||||
$uploadController->handleUpload();
|
$uploadController->handleUpload();
|
||||||
BIN
public/assets/default-avatar.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -134,17 +134,27 @@ body.dark-mode header {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 9px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-buttons button:not(#userDropdownToggle) {
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#userDropdownToggle {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.header-buttons button:hover {
|
.header-buttons button:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
header {
|
header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -838,6 +848,11 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
|
|||||||
background-color: #00796B;
|
background-color: #00796B;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#createFileBtn {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
#fileList button.edit-btn {
|
#fileList button.edit-btn {
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -955,6 +970,29 @@ body.dark-mode #fileList table tr {
|
|||||||
padding: 8px 10px !important;
|
padding: 8px 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--file-row-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileList table.table tbody tr {
|
||||||
|
height: auto !important;
|
||||||
|
min-height: var(--file-row-height) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileList table.table tbody td:not(.file-name-cell) {
|
||||||
|
height: var(--file-row-height) !important;
|
||||||
|
line-height: var(--file-row-height) !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileList table.table tbody td.file-name-cell {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.2em !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
HEADINGS & FORM LABELS
|
HEADINGS & FORM LABELS
|
||||||
@@ -1328,26 +1366,6 @@ body.dark-mode .image-preview-modal-content {
|
|||||||
border-color: #444;
|
border-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-btn,
|
|
||||||
.download-btn,
|
|
||||||
.rename-btn,
|
|
||||||
.share-btn,
|
|
||||||
.edit-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-btn {
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 0px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-modal-img {
|
.image-modal-img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
@@ -2102,13 +2120,23 @@ body.dark-mode .header-drop-zone.drag-active {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
#fileSummary {
|
#fileSummary,
|
||||||
float: none !important;
|
#rowHeightSliderContainer,
|
||||||
margin: 0 auto !important;
|
#viewSliderContainer {
|
||||||
text-align: center !important;
|
float: none !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
text-align: center !important;
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#viewSliderContainer label,
|
||||||
|
#viewSliderContainer span {
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
body.dark-mode #fileSummary {
|
body.dark-mode #fileSummary {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -2165,4 +2193,100 @@ body.dark-mode #searchIcon .material-icons {
|
|||||||
body.dark-mode .btn-icon:hover,
|
body.dark-mode .btn-icon:hover,
|
||||||
body.dark-mode .btn-icon:focus {
|
body.dark-mode .btn-icon:focus {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .user-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
background: var(--bs-body-bg, #fff);
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 150px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .user-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .user-menu .item {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.user-dropdown .user-menu .item:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .dropdown-caret {
|
||||||
|
border-top: 5px solid currentColor;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .user-dropdown .user-menu {
|
||||||
|
background: #2c2c2c;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .user-dropdown .user-menu .item {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .user-dropdown .user-menu .item:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown .dropdown-username {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-strip-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
.folder-strip-container .folder-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 80px;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.folder-strip-container .folder-item i.material-icons {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.folder-strip-container .folder-name {
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
max-width: 80px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-strip-container .folder-item i.material-icons {
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-strip-container .folder-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,26 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||||
<meta name="csrf-token" content="">
|
<meta name="csrf-token" content="">
|
||||||
<meta name="share-url" content="">
|
<meta name="share-url" content="">
|
||||||
|
<style>
|
||||||
|
/* hide the app shell until JS says otherwise */
|
||||||
|
.main-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* full-screen white overlay while we check auth */
|
||||||
|
#loadingOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--bg-color, #fff);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<!-- Google Fonts and Material Icons -->
|
<!-- Google Fonts and Material Icons -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
@@ -120,9 +140,6 @@
|
|||||||
<!-- Your header drop zone -->
|
<!-- Your header drop zone -->
|
||||||
<div id="headerDropArea" class="header-drop-zone"></div>
|
<div id="headerDropArea" class="header-drop-zone"></div>
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
<button id="logoutBtn" data-i18n-title="logout">
|
|
||||||
<i class="material-icons">exit_to_app</i>
|
|
||||||
</button>
|
|
||||||
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
|
<button id="changePasswordBtn" data-i18n-title="change_password" style="display: none;">
|
||||||
<i class="material-icons">vpn_key</i>
|
<i class="material-icons">vpn_key</i>
|
||||||
</button>
|
</button>
|
||||||
@@ -165,10 +182,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div id="loadingOverlay"></div>
|
||||||
|
|
||||||
<!-- Custom Toast Container -->
|
<!-- Custom Toast Container -->
|
||||||
<div id="customToast"></div>
|
<div id="customToast"></div>
|
||||||
<div id="hiddenCardsContainer" style="display:none;"></div>
|
<div id="hiddenCardsContainer" style="display:none;"></div>
|
||||||
|
|
||||||
|
<div class="row mt-4" id="loginForm">
|
||||||
|
<div class="col-12">
|
||||||
|
<form id="authForm" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="loginUsername" data-i18n-key="user">User:</label>
|
||||||
|
<input type="text" class="form-control" id="loginUsername" name="username" required autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
||||||
|
<input type="password" class="form-control" id="loginPassword" name="password" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
|
||||||
|
<div class="form-group remember-me-container">
|
||||||
|
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||||
|
<label for="rememberMeCheckbox" data-i18n-key="remember_me">Remember me</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<!-- OIDC Login Option -->
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">Login with OIDC</button>
|
||||||
|
</div>
|
||||||
|
<!-- Basic HTTP Login Option -->
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic
|
||||||
|
HTTP
|
||||||
|
Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
|
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
|
||||||
@@ -176,37 +225,6 @@
|
|||||||
<!-- Main Column -->
|
<!-- Main Column -->
|
||||||
<div id="mainColumn" class="main-column">
|
<div id="mainColumn" class="main-column">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Login Form (unchanged) -->
|
|
||||||
<div class="row" id="loginForm">
|
|
||||||
<div class="col-12">
|
|
||||||
<form id="authForm" method="post">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="loginUsername" data-i18n-key="user">User:</label>
|
|
||||||
<input type="text" class="form-control" id="loginUsername" name="username" required autofocus />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="loginPassword" data-i18n-key="password">Password:</label>
|
|
||||||
<input type="password" class="form-control" id="loginPassword" name="password" required />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary btn-block btn-login" data-i18n-key="login">Login</button>
|
|
||||||
<div class="form-group remember-me-container">
|
|
||||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
|
||||||
<label for="rememberMeCheckbox" data-i18n-key="remember_me">Remember me</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<!-- OIDC Login Option -->
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<button id="oidcLoginBtn" class="btn btn-secondary" data-i18n-key="login_oidc">Login with OIDC</button>
|
|
||||||
</div>
|
|
||||||
<!-- Basic HTTP Login Option -->
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<a href="/api/auth/login_basic.php" class="btn btn-secondary" data-i18n-key="basic_http_login">Use Basic
|
|
||||||
HTTP
|
|
||||||
Login</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Operations: Upload and Folder Management -->
|
<!-- Main Operations: Upload and Folder Management -->
|
||||||
<div id="mainOperations">
|
<div id="mainOperations">
|
||||||
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
||||||
@@ -371,8 +389,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
|
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled
|
||||||
data-i18n-key="download_zip">Download ZIP</button>
|
data-i18n-key="download_zip">Download ZIP</button>
|
||||||
<button id="extractZipBtn" class="btn btn-sm btn-info" data-i18n-title="extract_zip"
|
<button id="extractZipBtn" class="btn action-btn btn-sm btn-info" data-i18n-title="extract_zip"
|
||||||
data-i18n-key="extract_zip_button">Extract Zip</button>
|
data-i18n-key="extract_zip_button">Extract Zip</button>
|
||||||
|
<button id="createFileBtn" class="btn action-btn" data-i18n-key="create_file">
|
||||||
|
${t('create_file')}
|
||||||
|
</button>
|
||||||
|
<!-- Create File Modal -->
|
||||||
|
<div id="createFileModal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4 data-i18n-key="create_new_file">Create New File</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="createFileNameInput"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Enter filename…"
|
||||||
|
data-i18n-placeholder="newfile_placeholder"
|
||||||
|
/>
|
||||||
|
<div class="modal-footer" style="margin-top:1rem; text-align:right;">
|
||||||
|
<button id="cancelCreateFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||||
|
<button id="confirmCreateFile" class="btn btn-primary" data-i18n-key="create">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
|
<h4 data-i18n-key="download_zip_title">Download Selected Files as Zip</h4>
|
||||||
@@ -427,8 +465,7 @@
|
|||||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||||
<span id="closeChangePasswordModal"
|
<span id="closeChangePasswordModal" class="editor-close-btn">×</span>
|
||||||
style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
|
||||||
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
<h3 data-i18n-key="change_password_title">Change Password</h3>
|
||||||
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
<input type="password" id="oldPassword" class="form-control" data-i18n-placeholder="old_password"
|
||||||
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
placeholder="Old Password" style="width:100%; margin: 5px 0;" />
|
||||||
@@ -439,22 +476,22 @@
|
|||||||
<button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
|
<button id="saveNewPasswordBtn" class="btn btn-primary" data-i18n-key="save" style="width:100%;">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="addUserModal" class="modal">
|
<div id="addUserModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
|
<h3 data-i18n-key="create_new_user_title">Create New User</h3>
|
||||||
<!-- 1) Add a form around these fields -->
|
<!-- 1) Add a form around these fields -->
|
||||||
<form id="addUserForm">
|
<form id="addUserForm">
|
||||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||||
<input type="text" id="newUsername" class="form-control" required />
|
<input type="text" id="newUsername" class="form-control" required />
|
||||||
|
|
||||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||||
<input type="password" id="addUserPassword" class="form-control" required />
|
<input type="password" id="addUserPassword" class="form-control" required />
|
||||||
|
|
||||||
<div id="adminCheckboxContainer">
|
<div id="adminCheckboxContainer">
|
||||||
<input type="checkbox" id="isAdmin" />
|
<input type="checkbox" id="isAdmin" />
|
||||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<!-- Cancel stays type="button" -->
|
<!-- Cancel stays type="button" -->
|
||||||
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
||||||
@@ -468,7 +505,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="removeUserModal" class="modal">
|
<div id="removeUserModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3 data-i18n-key="remove_user_title">Remove User</h3>
|
<h3 data-i18n-key="remove_user_title">Remove User</h3>
|
||||||
<label for="removeUsernameSelect" data-i18n-key="select_user_remove">Select a user to remove:</label>
|
<label for="removeUsernameSelect" data-i18n-key="select_user_remove">Select a user to remove:</label>
|
||||||
@@ -479,7 +516,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="renameFileModal" class="modal">
|
<div id="renameFileModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h4 data-i18n-key="rename_file_title">Rename File</h4>
|
<h4 data-i18n-key="rename_file_title">Rename File</h4>
|
||||||
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder"
|
<input type="text" id="newFileName" class="form-control" data-i18n-placeholder="rename_file_placeholder"
|
||||||
|
|||||||
706
public/js/adminPanel.js
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
import { t } from './i18n.js';
|
||||||
|
import { loadAdminConfigFunc } from './auth.js';
|
||||||
|
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
|
||||||
|
import { sendRequest } from './networkUtils.js';
|
||||||
|
|
||||||
|
const version = "v1.3.7";
|
||||||
|
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||||
|
|
||||||
|
// ————— Inject updated styles —————
|
||||||
|
(function () {
|
||||||
|
if (document.getElementById('adminPanelStyles')) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'adminPanelStyles';
|
||||||
|
style.textContent = `
|
||||||
|
/* Modal sizing */
|
||||||
|
#adminPanelModal .modal-content {
|
||||||
|
max-width: 1100px;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small phones: 90% width */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#adminPanelModal .modal-content {
|
||||||
|
width: 90% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark-mode fixes */
|
||||||
|
body.dark-mode #adminPanelModal .modal-content {
|
||||||
|
border-color: #555 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* enforce light‐mode styling */
|
||||||
|
#adminPanelModal .modal-content {
|
||||||
|
max-width: 1100px;
|
||||||
|
width: 50%;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border: 1px solid #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* enforce dark‐mode styling */
|
||||||
|
body.dark-mode #adminPanelModal .modal-content {
|
||||||
|
background: #2c2c2c !important;
|
||||||
|
color: #e0e0e0 !important;
|
||||||
|
border-color: #555 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* form controls in dark */
|
||||||
|
body.dark-mode .form-control {
|
||||||
|
background-color: #333;
|
||||||
|
border-color: #555;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
body.dark-mode .form-control::placeholder { color: #888; }
|
||||||
|
|
||||||
|
/* Section headers */
|
||||||
|
.section-header {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.section-header:first-of-type { margin-top: 0; }
|
||||||
|
.section-header.collapsed .material-icons { transform: rotate(-90deg); }
|
||||||
|
.section-header .material-icons { transition: transform .3s; color: #444; }
|
||||||
|
|
||||||
|
body.dark-mode .section-header {
|
||||||
|
background: #3a3a3a;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
body.dark-mode .section-header .material-icons { color: #ccc; }
|
||||||
|
|
||||||
|
/* Hidden by default */
|
||||||
|
.section-content {
|
||||||
|
display: none;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close button */
|
||||||
|
#adminPanelModal .editor-close-btn {
|
||||||
|
position: absolute; top:10px; right:10px;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
font-size:20px; font-weight:bold; cursor:pointer;
|
||||||
|
z-index:1000; width:32px; height:32px; border-radius:50%;
|
||||||
|
text-align:center; line-height:30px;
|
||||||
|
color:#ff4d4d; background:rgba(255,255,255,0.9);
|
||||||
|
border:2px solid transparent; transition:all .3s;
|
||||||
|
}
|
||||||
|
#adminPanelModal .editor-close-btn:hover {
|
||||||
|
color:white; background:#ff4d4d;
|
||||||
|
box-shadow:0 0 6px rgba(255,77,77,.8);
|
||||||
|
transform:scale(1.05);
|
||||||
|
}
|
||||||
|
body.dark-mode #adminPanelModal .editor-close-btn {
|
||||||
|
background:rgba(0,0,0,0.6);
|
||||||
|
color:#ff4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action-row */
|
||||||
|
.action-row {
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
margin-top:15px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
})();
|
||||||
|
// ————————————————————————————————————
|
||||||
|
|
||||||
|
let originalAdminConfig = {};
|
||||||
|
function captureInitialAdminConfig() {
|
||||||
|
originalAdminConfig = {
|
||||||
|
headerTitle: document.getElementById("headerTitle").value.trim(),
|
||||||
|
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||||
|
oidcClientId: document.getElementById("oidcClientId").value.trim(),
|
||||||
|
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||||
|
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
|
||||||
|
disableFormLogin: document.getElementById("disableFormLogin").checked,
|
||||||
|
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
|
||||||
|
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
|
||||||
|
enableWebDAV: document.getElementById("enableWebDAV").checked,
|
||||||
|
sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(),
|
||||||
|
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function hasUnsavedChanges() {
|
||||||
|
const o = originalAdminConfig;
|
||||||
|
return (
|
||||||
|
document.getElementById("headerTitle").value.trim() !== o.headerTitle ||
|
||||||
|
document.getElementById("oidcProviderUrl").value.trim() !== o.oidcProviderUrl ||
|
||||||
|
document.getElementById("oidcClientId").value.trim() !== o.oidcClientId ||
|
||||||
|
document.getElementById("oidcClientSecret").value.trim() !== o.oidcClientSecret ||
|
||||||
|
document.getElementById("oidcRedirectUri").value.trim() !== o.oidcRedirectUri ||
|
||||||
|
document.getElementById("disableFormLogin").checked !== o.disableFormLogin ||
|
||||||
|
document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth ||
|
||||||
|
document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin ||
|
||||||
|
document.getElementById("enableWebDAV").checked !== o.enableWebDAV ||
|
||||||
|
document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize ||
|
||||||
|
document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCustomConfirmModal(message) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const modal = document.getElementById("customConfirmModal");
|
||||||
|
const msg = document.getElementById("confirmMessage");
|
||||||
|
const yes = document.getElementById("confirmYesBtn");
|
||||||
|
const no = document.getElementById("confirmNoBtn");
|
||||||
|
msg.textContent = message;
|
||||||
|
modal.style.display = "block";
|
||||||
|
function clean() {
|
||||||
|
modal.style.display = "none";
|
||||||
|
yes.removeEventListener("click", onYes);
|
||||||
|
no.removeEventListener("click", onNo);
|
||||||
|
}
|
||||||
|
function onYes() { clean(); resolve(true); }
|
||||||
|
function onNo() { clean(); resolve(false); }
|
||||||
|
yes.addEventListener("click", onYes);
|
||||||
|
no.addEventListener("click", onNo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(id) {
|
||||||
|
const hdr = document.getElementById(id + "Header");
|
||||||
|
const cnt = document.getElementById(id + "Content");
|
||||||
|
const isCollapsedNow = hdr.classList.toggle("collapsed");
|
||||||
|
// collapsed class present => hide; absent => show
|
||||||
|
cnt.style.display = isCollapsedNow ? "none" : "block";
|
||||||
|
if (!isCollapsedNow && id === "shareLinks") {
|
||||||
|
loadShareLinksSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShareLinksSection() {
|
||||||
|
const container = document.getElementById("shareLinksContent");
|
||||||
|
container.textContent = t("loading") + "...";
|
||||||
|
|
||||||
|
// helper: fetch one metadata file, but never throw —
|
||||||
|
// on non-2xx (including 404) or network error, resolve to {}
|
||||||
|
function fetchMeta(fileName) {
|
||||||
|
return fetch(`/api/admin/readMetadata.php?file=${encodeURIComponent(fileName)}`, {
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
.then(resp => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
// 404 or any other non-OK → treat as empty
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return resp.json();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// network failure, parse error, etc → also empty
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
fetchMeta("share_folder_links.json"),
|
||||||
|
fetchMeta("share_links.json")
|
||||||
|
])
|
||||||
|
.then(([folders, files]) => {
|
||||||
|
// if *both* are empty, show "no shared links"
|
||||||
|
const hasAny = Object.keys(folders).length || Object.keys(files).length;
|
||||||
|
if (!hasAny) {
|
||||||
|
container.innerHTML = `<p>${t("no_shared_links_available")}</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `<h5>${t("folder_shares")}</h5><ul>`;
|
||||||
|
Object.entries(folders).forEach(([token, o]) => {
|
||||||
|
const lock = o.password ? "🔒 " : "";
|
||||||
|
html += `
|
||||||
|
<li>
|
||||||
|
${lock}<strong>${o.folder}</strong>
|
||||||
|
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
||||||
|
<button type="button"
|
||||||
|
data-key="${token}"
|
||||||
|
data-type="folder"
|
||||||
|
class="btn btn-sm btn-link delete-share">🗑️</button>
|
||||||
|
</li>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</ul><h5 style="margin-top:1em;">${t("file_shares")}</h5><ul>`;
|
||||||
|
Object.entries(files).forEach(([token, o]) => {
|
||||||
|
const lock = o.password ? "🔒 " : "";
|
||||||
|
html += `
|
||||||
|
<li>
|
||||||
|
${lock}<strong>${o.folder}/${o.file}</strong>
|
||||||
|
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
||||||
|
<button type="button"
|
||||||
|
data-key="${token}"
|
||||||
|
data-type="file"
|
||||||
|
class="btn btn-sm btn-link delete-share">🗑️</button>
|
||||||
|
</li>`;
|
||||||
|
});
|
||||||
|
html += `</ul>`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// wire up delete buttons
|
||||||
|
container.querySelectorAll(".delete-share").forEach(btn => {
|
||||||
|
btn.addEventListener("click", evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const token = btn.dataset.key;
|
||||||
|
const isFolder = btn.dataset.type === "folder";
|
||||||
|
const endpoint = isFolder
|
||||||
|
? "/api/folder/deleteShareFolderLink.php"
|
||||||
|
: "/api/file/deleteShareLink.php";
|
||||||
|
|
||||||
|
fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({ token })
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
if (json.success) {
|
||||||
|
showToast(t("share_deleted_successfully"));
|
||||||
|
loadShareLinksSection();
|
||||||
|
} else {
|
||||||
|
showToast(t("error_deleting_share") + ": " + (json.error || ""), "error");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Delete error:", err);
|
||||||
|
showToast(t("error_deleting_share"), "error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("loadShareLinksSection error:", err);
|
||||||
|
container.textContent = t("error_loading_share_links");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function openAdminPanel() {
|
||||||
|
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(config => {
|
||||||
|
// apply header title + globals
|
||||||
|
if (config.header_title) {
|
||||||
|
document.querySelector(".header-title h1").textContent = config.header_title;
|
||||||
|
window.headerTitle = config.header_title;
|
||||||
|
}
|
||||||
|
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||||
|
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||||
|
|
||||||
|
const dark = document.body.classList.contains("dark-mode");
|
||||||
|
const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
|
const inner = `
|
||||||
|
background:${dark ? "#2c2c2c" : "#fff"};
|
||||||
|
color:${dark ? "#e0e0e0" : "#000"};
|
||||||
|
padding:20px; max-width:1100px; width:50%;
|
||||||
|
border-radius:8px; position:relative;
|
||||||
|
max-height:90vh; overflow:auto;
|
||||||
|
border:1px solid ${dark ? "#555" : "#ccc"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
let mdl = document.getElementById("adminPanelModal");
|
||||||
|
if (!mdl) {
|
||||||
|
mdl = document.createElement("div");
|
||||||
|
mdl.id = "adminPanelModal";
|
||||||
|
mdl.style.cssText = `
|
||||||
|
position:fixed; top:0; left:0;
|
||||||
|
width:100vw; height:100vh;
|
||||||
|
background:${bg};
|
||||||
|
display:flex; justify-content:center; align-items:center;
|
||||||
|
z-index:3000;
|
||||||
|
`;
|
||||||
|
mdl.innerHTML = `
|
||||||
|
<div class="modal-content" style="${inner}">
|
||||||
|
<div class="editor-close-btn" id="closeAdminPanel">×</div>
|
||||||
|
<h3>${adminTitle}</h3>
|
||||||
|
<form id="adminPanelForm">
|
||||||
|
|
||||||
|
<!-- each section: header + content -->
|
||||||
|
${[
|
||||||
|
{ id: "userManagement", label: t("user_management") },
|
||||||
|
{ id: "headerSettings", label: t("header_settings") },
|
||||||
|
{ id: "loginOptions", label: t("login_options") },
|
||||||
|
{ id: "webdav", label: "WebDAV Access" },
|
||||||
|
{ id: "upload", label: t("shared_max_upload_size_bytes_title") },
|
||||||
|
{ id: "oidc", label: t("oidc_configuration") + " & TOTP" },
|
||||||
|
{ id: "shareLinks", label: t("manage_shared_links") }
|
||||||
|
].map(sec => `
|
||||||
|
<div id="${sec.id}Header" class="section-header collapsed">
|
||||||
|
${sec.label} <i class="material-icons">expand_more</i>
|
||||||
|
</div>
|
||||||
|
<div id="${sec.id}Content" class="section-content"></div>
|
||||||
|
`).join("")}
|
||||||
|
|
||||||
|
<div class="action-row">
|
||||||
|
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">${t("cancel")}</button>
|
||||||
|
<button type="button" id="saveAdminSettings" class="btn btn-primary">${t("save_settings")}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(mdl);
|
||||||
|
|
||||||
|
// Bind close & cancel
|
||||||
|
document.getElementById("closeAdminPanel")
|
||||||
|
.addEventListener("click", closeAdminPanel);
|
||||||
|
document.getElementById("cancelAdminSettings")
|
||||||
|
.addEventListener("click", closeAdminPanel);
|
||||||
|
|
||||||
|
// Section toggles
|
||||||
|
["userManagement", "headerSettings", "loginOptions", "webdav", "upload", "oidc", "shareLinks"]
|
||||||
|
.forEach(id => {
|
||||||
|
document.getElementById(id + "Header")
|
||||||
|
.addEventListener("click", () => toggleSection(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate each section’s CONTENT:
|
||||||
|
// — User Mgmt —
|
||||||
|
document.getElementById("userManagementContent").innerHTML = `
|
||||||
|
<button type="button" id="adminOpenAddUser" class="btn btn-success me-2">${t("add_user")}</button>
|
||||||
|
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger me-2">${t("remove_user")}</button>
|
||||||
|
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
||||||
|
`;
|
||||||
|
document.getElementById("adminOpenAddUser")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
toggleVisibility("addUserModal", true);
|
||||||
|
document.getElementById("newUsername").focus();
|
||||||
|
});
|
||||||
|
document.getElementById("adminOpenRemoveUser")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
if (typeof window.loadUserList === "function") window.loadUserList();
|
||||||
|
toggleVisibility("removeUserModal", true);
|
||||||
|
});
|
||||||
|
document.getElementById("adminOpenUserPermissions")
|
||||||
|
.addEventListener("click", openUserPermissionsModal);
|
||||||
|
|
||||||
|
// — Header Settings —
|
||||||
|
document.getElementById("headerSettingsContent").innerHTML = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="headerTitle">${t("header_title_text")}:</label>
|
||||||
|
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// — Login Options —
|
||||||
|
document.getElementById("loginOptionsContent").innerHTML = `
|
||||||
|
<div class="form-group"><input type="checkbox" id="disableFormLogin" /> <label for="disableFormLogin">${t("disable_login_form")}</label></div>
|
||||||
|
<div class="form-group"><input type="checkbox" id="disableBasicAuth" /> <label for="disableBasicAuth">${t("disable_basic_http_auth")}</label></div>
|
||||||
|
<div class="form-group"><input type="checkbox" id="disableOIDCLogin" /> <label for="disableOIDCLogin">${t("disable_oidc_login")}</label></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="authBypass" />
|
||||||
|
<label for="authBypass">Disable all built-in logins (proxy only)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="authHeaderName">Auth header name:</label>
|
||||||
|
<input type="text" id="authHeaderName" class="form-control" placeholder="e.g. X-Remote-User" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// — WebDAV —
|
||||||
|
document.getElementById("webdavContent").innerHTML = `
|
||||||
|
<div class="form-group"><input type="checkbox" id="enableWebDAV" /> <label for="enableWebDAV">Enable WebDAV</label></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// — Upload —
|
||||||
|
document.getElementById("uploadContent").innerHTML = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sharedMaxUploadSize">${t("shared_max_upload_size_bytes")}:</label>
|
||||||
|
<input type="number" id="sharedMaxUploadSize" class="form-control" placeholder="e.g. 52428800" />
|
||||||
|
<small>${t("max_bytes_shared_uploads_note")}</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// — OIDC & TOTP —
|
||||||
|
document.getElementById("oidcContent").innerHTML = `
|
||||||
|
<div class="form-text text-muted" style="margin-top:8px;">
|
||||||
|
<small>Note: OIDC credentials (Client ID/Secret) will show blank here after saving, but remain unchanged until you explicitly edit and save them.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label for="oidcProviderUrl">${t("oidc_provider_url")}:</label><input type="text" id="oidcProviderUrl" class="form-control" value="${window.currentOIDCConfig.providerUrl}" /></div>
|
||||||
|
<div class="form-group"><label for="oidcClientId">${t("oidc_client_id")}:</label><input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" /></div>
|
||||||
|
<div class="form-group"><label for="oidcClientSecret">${t("oidc_client_secret")}:</label><input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" /></div>
|
||||||
|
<div class="form-group"><label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label><input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" /></div>
|
||||||
|
<div class="form-group"><label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label><input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" /></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// — Share Links —
|
||||||
|
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
|
||||||
|
|
||||||
|
// — Save handler & constraints —
|
||||||
|
document.getElementById("saveAdminSettings")
|
||||||
|
.addEventListener("click", handleSave);
|
||||||
|
["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => {
|
||||||
|
document.getElementById(id)
|
||||||
|
.addEventListener("change", e => {
|
||||||
|
const chk = ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"]
|
||||||
|
.filter(i => document.getElementById(i).checked).length;
|
||||||
|
if (chk === 3) {
|
||||||
|
showToast(t("at_least_one_login_method"));
|
||||||
|
e.target.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// If authBypass is checked, clear the other three
|
||||||
|
document.getElementById("authBypass").addEventListener("change", e => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"]
|
||||||
|
.forEach(i => document.getElementById(i).checked = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize inputs from config + capture
|
||||||
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
|
document.getElementById("authBypass").checked = !!config.loginOptions.authBypass;
|
||||||
|
document.getElementById("authHeaderName").value = config.loginOptions.authHeaderName || "X-Remote-User";
|
||||||
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
|
captureInitialAdminConfig();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// modal already exists → just refresh values & re-show
|
||||||
|
mdl.style.display = "flex";
|
||||||
|
// update dark/light as above...
|
||||||
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
|
document.getElementById("authBypass").checked = !!config.loginOptions.authBypass;
|
||||||
|
document.getElementById("authHeaderName").value = config.loginOptions.authHeaderName || "X-Remote-User";
|
||||||
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
|
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||||
|
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||||
|
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||||
|
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||||
|
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || '';
|
||||||
|
captureInitialAdminConfig();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {/* if even fetching fails, open empty panel */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
const dFL = document.getElementById("disableFormLogin").checked;
|
||||||
|
const dBA = document.getElementById("disableBasicAuth").checked;
|
||||||
|
const dOIDC = document.getElementById("disableOIDCLogin").checked;
|
||||||
|
const aBypass= document.getElementById("authBypass").checked;
|
||||||
|
const aHeader= document.getElementById("authHeaderName").value.trim() || "X-Remote-User";
|
||||||
|
const eWD = document.getElementById("enableWebDAV").checked;
|
||||||
|
const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value, 10) || 0;
|
||||||
|
const nHT = document.getElementById("headerTitle").value.trim();
|
||||||
|
const nOIDC = {
|
||||||
|
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||||
|
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||||
|
clientSecret:document.getElementById("oidcClientSecret").value.trim(),
|
||||||
|
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||||
|
};
|
||||||
|
const gURL = document.getElementById("globalOtpauthUrl").value.trim();
|
||||||
|
|
||||||
|
if ([dFL, dBA, dOIDC].filter(x => x).length === 3) {
|
||||||
|
showToast(t("at_least_one_login_method"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||||
|
header_title: nHT,
|
||||||
|
oidc: nOIDC,
|
||||||
|
loginOptions: {
|
||||||
|
disableFormLogin: dFL,
|
||||||
|
disableBasicAuth: dBA,
|
||||||
|
disableOIDCLogin: dOIDC,
|
||||||
|
authBypass: aBypass,
|
||||||
|
authHeaderName: aHeader
|
||||||
|
},
|
||||||
|
enableWebDAV: eWD,
|
||||||
|
sharedMaxUploadSize: sMax,
|
||||||
|
globalOtpauthUrl: gURL
|
||||||
|
}, {
|
||||||
|
"X-CSRF-Token": window.csrfToken
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.success) {
|
||||||
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
|
captureInitialAdminConfig();
|
||||||
|
closeAdminPanel();
|
||||||
|
loadAdminConfigFunc();
|
||||||
|
} else {
|
||||||
|
showToast(t("error_updating_settings") + ": " + (res.error || t("unknown_error")), "error");
|
||||||
|
}
|
||||||
|
}).catch(() => {/*noop*/});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeAdminPanel() {
|
||||||
|
if (hasUnsavedChanges()) {
|
||||||
|
const ok = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
||||||
|
if (!ok) return;
|
||||||
|
}
|
||||||
|
document.getElementById("adminPanelModal").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- New: User Permissions Modal ---
|
||||||
|
export function openUserPermissionsModal() {
|
||||||
|
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||||
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
|
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
|
const modalContentStyles = `
|
||||||
|
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||||
|
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!userPermissionsModal) {
|
||||||
|
userPermissionsModal = document.createElement("div");
|
||||||
|
userPermissionsModal.id = "userPermissionsModal";
|
||||||
|
userPermissionsModal.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: ${overlayBackground};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 3500;
|
||||||
|
`;
|
||||||
|
userPermissionsModal.innerHTML = `
|
||||||
|
<div class="modal-content" style="${modalContentStyles}">
|
||||||
|
<span id="closeUserPermissionsModal" class="editor-close-btn">×</span>
|
||||||
|
<h3>${t("user_permissions")}</h3>
|
||||||
|
<div id="userPermissionsList" style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">
|
||||||
|
<!-- User rows will be loaded here -->
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
||||||
|
<button type="button" id="cancelUserPermissionsBtn" class="btn btn-secondary">${t("cancel")}</button>
|
||||||
|
<button type="button" id="saveUserPermissionsBtn" class="btn btn-primary">${t("save_permissions")}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(userPermissionsModal);
|
||||||
|
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||||
|
userPermissionsModal.style.display = "none";
|
||||||
|
});
|
||||||
|
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||||
|
userPermissionsModal.style.display = "none";
|
||||||
|
});
|
||||||
|
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||||
|
// Collect permissions data from each user row.
|
||||||
|
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||||
|
const permissionsData = [];
|
||||||
|
rows.forEach(row => {
|
||||||
|
const username = row.getAttribute("data-username");
|
||||||
|
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||||
|
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||||
|
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||||
|
permissionsData.push({
|
||||||
|
username,
|
||||||
|
folderOnly: folderOnlyCheckbox.checked,
|
||||||
|
readOnly: readOnlyCheckbox.checked,
|
||||||
|
disableUpload: disableUploadCheckbox.checked
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Send the permissionsData to the server.
|
||||||
|
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
showToast(t("user_permissions_updated_successfully"));
|
||||||
|
userPermissionsModal.style.display = "none";
|
||||||
|
} else {
|
||||||
|
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast(t("error_updating_permissions"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userPermissionsModal.style.display = "flex";
|
||||||
|
}
|
||||||
|
// Load the list of users into the modal.
|
||||||
|
loadUserPermissionsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUserPermissionsList() {
|
||||||
|
const listContainer = document.getElementById("userPermissionsList");
|
||||||
|
if (!listContainer) return;
|
||||||
|
listContainer.innerHTML = "";
|
||||||
|
|
||||||
|
// First, fetch the current permissions from the server.
|
||||||
|
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(permissionsData => {
|
||||||
|
// Then, fetch the list of users.
|
||||||
|
return fetch("/api/getUsers.php", { credentials: "include" })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(usersData => {
|
||||||
|
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||||
|
if (users.length === 0) {
|
||||||
|
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
users.forEach(user => {
|
||||||
|
// Skip admin users.
|
||||||
|
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||||
|
|
||||||
|
// Use stored permissions if available; otherwise fall back to defaults.
|
||||||
|
const defaultPerm = {
|
||||||
|
folderOnly: false,
|
||||||
|
readOnly: false,
|
||||||
|
disableUpload: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize the username key to match server storage (e.g., lowercase)
|
||||||
|
const usernameKey = user.username.toLowerCase();
|
||||||
|
|
||||||
|
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
||||||
|
? permissionsData[usernameKey]
|
||||||
|
: defaultPerm;
|
||||||
|
|
||||||
|
// Create a row for the user.
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.classList.add("user-permission-row");
|
||||||
|
row.setAttribute("data-username", user.username);
|
||||||
|
row.style.padding = "10px 0";
|
||||||
|
row.innerHTML = `
|
||||||
|
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<input type="checkbox" data-permission="folderOnly" ${userPerm.folderOnly ? "checked" : ""} />
|
||||||
|
${t("user_folder_only")}
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<input type="checkbox" data-permission="readOnly" ${userPerm.readOnly ? "checked" : ""} />
|
||||||
|
${t("read_only")}
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<input type="checkbox" data-permission="disableUpload" ${userPerm.disableUpload ? "checked" : ""} />
|
||||||
|
${t("disable_upload")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||||
|
`;
|
||||||
|
listContainer.appendChild(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,16 +15,17 @@ import {
|
|||||||
openUserPanel,
|
openUserPanel,
|
||||||
openTOTPModal,
|
openTOTPModal,
|
||||||
closeTOTPModal,
|
closeTOTPModal,
|
||||||
openAdminPanel,
|
setLastLoginData,
|
||||||
closeAdminPanel,
|
openApiModal
|
||||||
setLastLoginData
|
|
||||||
} from './authModals.js';
|
} from './authModals.js';
|
||||||
|
import { openAdminPanel } from './adminPanel.js';
|
||||||
|
import { initializeApp, triggerLogout } from './main.js';
|
||||||
|
|
||||||
// Production OIDC configuration (override via API as needed)
|
// Production OIDC configuration (override via API as needed)
|
||||||
const currentOIDCConfig = {
|
const currentOIDCConfig = {
|
||||||
providerUrl: "https://your-oidc-provider.com",
|
providerUrl: "https://your-oidc-provider.com",
|
||||||
clientId: "YOUR_CLIENT_ID",
|
clientId: "",
|
||||||
clientSecret: "YOUR_CLIENT_SECRET",
|
clientSecret: "",
|
||||||
redirectUri: "https://yourdomain.com/api/auth/auth.php?oidc=callback",
|
redirectUri: "https://yourdomain.com/api/auth/auth.php?oidc=callback",
|
||||||
globalOtpauthUrl: ""
|
globalOtpauthUrl: ""
|
||||||
};
|
};
|
||||||
@@ -125,16 +126,23 @@ function updateItemsPerPageSelect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyProxyBypassUI() {
|
||||||
|
const bypass = localStorage.getItem("authBypass") === "true";
|
||||||
|
const loginContainer = document.getElementById("loginForm");
|
||||||
|
if (loginContainer) {
|
||||||
|
loginContainer.style.display = bypass ? "none" : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||||
const authForm = document.getElementById("authForm");
|
const authForm = document.getElementById("authForm");
|
||||||
if
|
if
|
||||||
(authForm) {
|
(authForm) {
|
||||||
authForm.style.display = disableFormLogin ? "none" : "block";
|
authForm.style.display = disableFormLogin ? "none" : "block";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const loginInput = document.getElementById('loginUsername');
|
const loginInput = document.getElementById('loginUsername');
|
||||||
if (loginInput) loginInput.focus();
|
if (loginInput) loginInput.focus();
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']");
|
const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']");
|
||||||
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||||
@@ -146,7 +154,8 @@ function updateLoginOptionsUIFromStorage() {
|
|||||||
updateLoginOptionsUI({
|
updateLoginOptionsUI({
|
||||||
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
disableFormLogin: localStorage.getItem("disableFormLogin") === "true",
|
||||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true",
|
||||||
|
authBypass: localStorage.getItem("authBypass") === "true"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +170,8 @@ export function loadAdminConfigFunc() {
|
|||||||
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
||||||
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
||||||
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
|
||||||
|
localStorage.setItem("authBypass", String(!!config.loginOptions.authBypass));
|
||||||
|
localStorage.setItem("authHeaderName", config.loginOptions.authHeaderName || "X-Remote-User");
|
||||||
|
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
|
|
||||||
@@ -189,16 +200,48 @@ function insertAfter(newNode, referenceNode) {
|
|||||||
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
|
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAuthenticatedUI(data) {
|
async function fetchProfilePicture() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/profile/getCurrentUser.php', {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const info = await res.json();
|
||||||
|
let pic = info.profile_picture || '';
|
||||||
|
// --- take only what's after the *last* colon ---
|
||||||
|
const parts = pic.split(':');
|
||||||
|
pic = parts[parts.length - 1] || '';
|
||||||
|
// strip any stray leading colons
|
||||||
|
pic = pic.replace(/^:+/, '');
|
||||||
|
// ensure exactly one leading slash
|
||||||
|
if (pic && !pic.startsWith('/')) pic = '/' + pic;
|
||||||
|
return pic;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('fetchProfilePicture failed:', e);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAuthenticatedUI(data) {
|
||||||
|
// Save latest auth data for later reuse
|
||||||
|
window.__lastAuthData = data;
|
||||||
|
|
||||||
|
// 1) Remove loading overlay safely
|
||||||
|
const loading = document.getElementById('loadingOverlay');
|
||||||
|
if (loading) loading.remove();
|
||||||
|
|
||||||
|
// 2) Show main UI
|
||||||
|
document.querySelector('.main-wrapper').style.display = '';
|
||||||
|
document.getElementById('loginForm').style.display = 'none';
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
toggleVisibility("mainOperations", true);
|
toggleVisibility("mainOperations", true);
|
||||||
toggleVisibility("uploadFileForm", true);
|
toggleVisibility("uploadFileForm", true);
|
||||||
toggleVisibility("fileListContainer", true);
|
toggleVisibility("fileListContainer", true);
|
||||||
//attachEnterKeyListener("addUserModal", "saveUserBtn");
|
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
attachEnterKeyListener("changePasswordModal","saveNewPasswordBtn");
|
||||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
|
||||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||||
|
|
||||||
|
// 3) Persist auth flags (unchanged)
|
||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
@@ -206,64 +249,157 @@ function updateAuthenticatedUI(data) {
|
|||||||
localStorage.setItem("username", data.username);
|
localStorage.setItem("username", data.username);
|
||||||
}
|
}
|
||||||
if (typeof data.folderOnly !== "undefined") {
|
if (typeof data.folderOnly !== "undefined") {
|
||||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||||
localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
|
localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
|
||||||
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
|
localStorage.setItem("disableUpload",data.disableUpload? "true" : "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) Fetch up-to-date profile picture — ALWAYS overwrite localStorage
|
||||||
|
const profilePicUrl = await fetchProfilePicture();
|
||||||
|
localStorage.setItem("profilePicUrl", profilePicUrl);
|
||||||
|
|
||||||
|
// 5) Build / update header buttons
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
const headerButtons = document.querySelector(".header-buttons");
|
||||||
const firstButton = headerButtons.firstElementChild;
|
const firstButton = headerButtons.firstElementChild;
|
||||||
|
|
||||||
|
// a) restore-from-trash for admins
|
||||||
if (data.isAdmin) {
|
if (data.isAdmin) {
|
||||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
let r = document.getElementById("restoreFilesBtn");
|
||||||
if (!restoreBtn) {
|
if (!r) {
|
||||||
restoreBtn = document.createElement("button");
|
r = document.createElement("button");
|
||||||
restoreBtn.id = "restoreFilesBtn";
|
r.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
r.classList.add("btn","btn-warning");
|
||||||
restoreBtn.setAttribute("data-i18n-title", "trash_restore_delete");
|
r.setAttribute("data-i18n-title","trash_restore_delete");
|
||||||
restoreBtn.innerHTML = '<i class="material-icons">restore_from_trash</i>';
|
r.innerHTML = '<i class="material-icons">restore_from_trash</i>';
|
||||||
if (firstButton) insertAfter(restoreBtn, firstButton);
|
if (firstButton) insertAfter(r, firstButton);
|
||||||
else headerButtons.appendChild(restoreBtn);
|
else headerButtons.appendChild(r);
|
||||||
}
|
|
||||||
restoreBtn.style.display = "block";
|
|
||||||
|
|
||||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
|
||||||
if (!adminPanelBtn) {
|
|
||||||
adminPanelBtn = document.createElement("button");
|
|
||||||
adminPanelBtn.id = "adminPanelBtn";
|
|
||||||
adminPanelBtn.classList.add("btn", "btn-info");
|
|
||||||
adminPanelBtn.setAttribute("data-i18n-title", "admin_panel");
|
|
||||||
adminPanelBtn.innerHTML = '<i class="material-icons">admin_panel_settings</i>';
|
|
||||||
insertAfter(adminPanelBtn, restoreBtn);
|
|
||||||
adminPanelBtn.addEventListener("click", openAdminPanel);
|
|
||||||
} else {
|
|
||||||
adminPanelBtn.style.display = "block";
|
|
||||||
}
|
}
|
||||||
|
r.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
const r = document.getElementById("restoreFilesBtn");
|
||||||
if (restoreBtn) restoreBtn.style.display = "none";
|
if (r) r.style.display = "none";
|
||||||
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
|
||||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.location.hostname !== "demo.filerise.net") {
|
// b) admin panel button only on demo.filerise.net
|
||||||
let userPanelBtn = document.getElementById("userPanelBtn");
|
if (data.isAdmin && window.location.hostname === "demo.filerise.net") {
|
||||||
if (!userPanelBtn) {
|
let a = document.getElementById("adminPanelBtn");
|
||||||
userPanelBtn = document.createElement("button");
|
if (!a) {
|
||||||
userPanelBtn.id = "userPanelBtn";
|
a = document.createElement("button");
|
||||||
userPanelBtn.classList.add("btn", "btn-user");
|
a.id = "adminPanelBtn";
|
||||||
userPanelBtn.setAttribute("data-i18n-title", "user_panel");
|
a.classList.add("btn","btn-info");
|
||||||
userPanelBtn.innerHTML = '<i class="material-icons">account_circle</i>';
|
a.setAttribute("data-i18n-title","admin_panel");
|
||||||
|
a.innerHTML = '<i class="material-icons">admin_panel_settings</i>';
|
||||||
|
insertAfter(a, document.getElementById("restoreFilesBtn"));
|
||||||
|
a.addEventListener("click", openAdminPanel);
|
||||||
|
}
|
||||||
|
a.style.display = "block";
|
||||||
|
} else {
|
||||||
|
const a = document.getElementById("adminPanelBtn");
|
||||||
|
if (a) a.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// c) user dropdown on non-demo
|
||||||
|
if (window.location.hostname !== "demo.filerise.net") {
|
||||||
|
let dd = document.getElementById("userDropdown");
|
||||||
|
|
||||||
|
// choose icon *or* img
|
||||||
|
const avatarHTML = profilePicUrl
|
||||||
|
? `<img src="${profilePicUrl}" style="width:24px;height:24px;border-radius:50%;vertical-align:middle;">`
|
||||||
|
: `<i class="material-icons">account_circle</i>`;
|
||||||
|
|
||||||
|
// fallback username if missing
|
||||||
|
const usernameText = data.username
|
||||||
|
|| localStorage.getItem("username")
|
||||||
|
|| "";
|
||||||
|
|
||||||
|
if (!dd) {
|
||||||
|
dd = document.createElement("div");
|
||||||
|
dd.id = "userDropdown";
|
||||||
|
dd.classList.add("user-dropdown");
|
||||||
|
|
||||||
|
// toggle button
|
||||||
|
const toggle = document.createElement("button");
|
||||||
|
toggle.id = "userDropdownToggle";
|
||||||
|
toggle.classList.add("btn","btn-user");
|
||||||
|
toggle.setAttribute("title", t("user_settings"));
|
||||||
|
toggle.innerHTML = `
|
||||||
|
${avatarHTML}
|
||||||
|
<span class="dropdown-username">${usernameText}</span>
|
||||||
|
<span class="dropdown-caret"></span>
|
||||||
|
`;
|
||||||
|
dd.append(toggle);
|
||||||
|
|
||||||
|
// menu
|
||||||
|
const menu = document.createElement("div");
|
||||||
|
menu.classList.add("user-menu");
|
||||||
|
menu.innerHTML = `
|
||||||
|
<div class="item" id="menuUserPanel">
|
||||||
|
<i class="material-icons folder-icon">person</i> ${t("user_panel")}
|
||||||
|
</div>
|
||||||
|
${data.isAdmin ? `
|
||||||
|
<div class="item" id="menuAdminPanel">
|
||||||
|
<i class="material-icons folder-icon">admin_panel_settings</i> ${t("admin_panel")}
|
||||||
|
</div>` : ''}
|
||||||
|
<div class="item" id="menuApiDocs">
|
||||||
|
<i class="material-icons folder-icon">description</i> ${t("api_docs")}
|
||||||
|
</div>
|
||||||
|
<div class="item" id="menuLogout">
|
||||||
|
<i class="material-icons folder-icon">logout</i> ${t("logout")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
dd.append(menu);
|
||||||
|
|
||||||
|
// insert
|
||||||
|
const dm = document.getElementById("darkModeToggle");
|
||||||
|
if (dm) insertAfter(dd, dm);
|
||||||
|
else if (firstButton) insertAfter(dd, firstButton);
|
||||||
|
else headerButtons.appendChild(dd);
|
||||||
|
|
||||||
|
// open/close
|
||||||
|
toggle.addEventListener("click", e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
menu.classList.toggle("show");
|
||||||
|
});
|
||||||
|
document.addEventListener("click", () => menu.classList.remove("show"));
|
||||||
|
|
||||||
|
// actions
|
||||||
|
document.getElementById("menuUserPanel")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
menu.classList.remove("show");
|
||||||
|
openUserPanel();
|
||||||
|
});
|
||||||
|
if (data.isAdmin) {
|
||||||
|
document.getElementById("menuAdminPanel")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
menu.classList.remove("show");
|
||||||
|
openAdminPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById("menuApiDocs")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
menu.classList.remove("show");
|
||||||
|
openApiModal();
|
||||||
|
});
|
||||||
|
document.getElementById("menuLogout")
|
||||||
|
.addEventListener("click", () => {
|
||||||
|
menu.classList.remove("show");
|
||||||
|
triggerLogout();
|
||||||
|
});
|
||||||
|
|
||||||
const adminBtn = document.getElementById("adminPanelBtn");
|
|
||||||
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
|
|
||||||
else if (firstButton) insertAfter(userPanelBtn, firstButton);
|
|
||||||
else headerButtons.appendChild(userPanelBtn);
|
|
||||||
userPanelBtn.addEventListener("click", openUserPanel);
|
|
||||||
} else {
|
} else {
|
||||||
userPanelBtn.style.display = "block";
|
// update avatar & username only
|
||||||
|
const tog = dd.querySelector("#userDropdownToggle");
|
||||||
|
tog.innerHTML = `
|
||||||
|
${avatarHTML}
|
||||||
|
<span class="dropdown-username">${usernameText}</span>
|
||||||
|
<span class="dropdown-caret"></span>
|
||||||
|
`;
|
||||||
|
dd.style.display = "inline-block";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6) Finalize
|
||||||
|
initializeApp();
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
updateItemsPerPageSelect();
|
updateItemsPerPageSelect();
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
@@ -273,6 +409,12 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
return sendRequest("/api/auth/checkAuth.php")
|
return sendRequest("/api/auth/checkAuth.php")
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.setup) {
|
if (data.setup) {
|
||||||
|
const overlay = document.getElementById('loadingOverlay');
|
||||||
|
if (overlay) overlay.remove();
|
||||||
|
|
||||||
|
// show the wrapper (so the login form can be visible)
|
||||||
|
document.querySelector('.main-wrapper').style.display = '';
|
||||||
|
document.getElementById('loginForm').style.display = 'none';
|
||||||
window.setupMode = true;
|
window.setupMode = true;
|
||||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
@@ -284,11 +426,13 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
|
|
||||||
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
|
localStorage.setItem('isAdmin', data.isAdmin ? 'true' : 'false');
|
||||||
localStorage.setItem("folderOnly", data.folderOnly);
|
localStorage.setItem("folderOnly", data.folderOnly);
|
||||||
localStorage.setItem("readOnly", data.readOnly);
|
localStorage.setItem("readOnly", data.readOnly);
|
||||||
localStorage.setItem("disableUpload", data.disableUpload);
|
localStorage.setItem("disableUpload", data.disableUpload);
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
|
applyProxyBypassUI();
|
||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
@@ -299,8 +443,14 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
updateAuthenticatedUI(data);
|
updateAuthenticatedUI(data);
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
|
const overlay = document.getElementById('loadingOverlay');
|
||||||
|
if (overlay) overlay.remove();
|
||||||
|
|
||||||
|
// show the wrapper (so the login form can be visible)
|
||||||
|
document.querySelector('.main-wrapper').style.display = '';
|
||||||
|
document.getElementById('loginForm').style.display = '';
|
||||||
if (showLoginToast) showToast("Please log in to continue.");
|
if (showLoginToast) showToast("Please log in to continue.");
|
||||||
toggleVisibility("loginForm", true);
|
toggleVisibility("loginForm", !(localStorage.getItem("authBypass") === "true"));
|
||||||
toggleVisibility("mainOperations", false);
|
toggleVisibility("mainOperations", false);
|
||||||
toggleVisibility("uploadFileForm", false);
|
toggleVisibility("uploadFileForm", false);
|
||||||
toggleVisibility("fileListContainer", false);
|
toggleVisibility("fileListContainer", false);
|
||||||
@@ -444,52 +594,55 @@ function initAuth() {
|
|||||||
submitLogin(formData);
|
submitLogin(formData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove your old saveUserBtn click-handler…
|
|
||||||
|
|
||||||
// instead:
|
// remove your old saveUserBtn click-handler…
|
||||||
const addUserForm = document.getElementById("addUserForm");
|
|
||||||
addUserForm.addEventListener("submit", function (e) {
|
|
||||||
e.preventDefault(); // stop the browser from reloading the page
|
|
||||||
|
|
||||||
const newUsername = document.getElementById("newUsername").value.trim();
|
// instead:
|
||||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
const addUserForm = document.getElementById("addUserForm");
|
||||||
const isAdmin = document.getElementById("isAdmin").checked;
|
addUserForm.addEventListener("submit", function (e) {
|
||||||
|
e.preventDefault(); // stop the browser from reloading the page
|
||||||
|
|
||||||
if (!newUsername || !newPassword) {
|
const newUsername = document.getElementById("newUsername").value.trim();
|
||||||
showToast("Username and password are required!");
|
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||||
return;
|
const isAdmin = document.getElementById("isAdmin").checked;
|
||||||
}
|
|
||||||
|
|
||||||
let url = "/api/addUser.php";
|
if (!newUsername || !newPassword) {
|
||||||
if (window.setupMode) url += "?setup=1";
|
showToast("Username and password are required!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetchWithCsrf(url, {
|
let url = "/api/addUser.php";
|
||||||
method: "POST",
|
if (window.setupMode) url += "?setup=1";
|
||||||
credentials: "include",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
fetchWithCsrf(url, {
|
||||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
method: "POST",
|
||||||
})
|
credentials: "include",
|
||||||
.then(r => r.json())
|
headers: { "Content-Type": "application/json" },
|
||||||
.then(data => {
|
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||||
if (data.success) {
|
|
||||||
showToast("User added successfully!");
|
|
||||||
closeAddUserModal();
|
|
||||||
checkAuthentication(false);
|
|
||||||
} else {
|
|
||||||
showToast("Error: " + (data.error || "Could not add user"));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(r => r.json())
|
||||||
showToast("Error: Could not add user");
|
.then(data => {
|
||||||
});
|
if (data.success) {
|
||||||
});
|
showToast("User added successfully!");
|
||||||
|
closeAddUserModal();
|
||||||
|
checkAuthentication(false);
|
||||||
|
if (window.setupMode) {
|
||||||
|
toggleVisibility("loginForm", true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast("Error: " + (data.error || "Could not add user"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast("Error: Could not add user");
|
||||||
|
});
|
||||||
|
});
|
||||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||||
|
|
||||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||||
|
|||||||
@@ -33,54 +33,66 @@ export function toggleAllCheckboxes(masterCheckbox) {
|
|||||||
export function updateFileActionButtons() {
|
export function updateFileActionButtons() {
|
||||||
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
|
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
|
||||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||||
|
|
||||||
|
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||||
const copyBtn = document.getElementById("copySelectedBtn");
|
const copyBtn = document.getElementById("copySelectedBtn");
|
||||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
|
||||||
const zipBtn = document.getElementById("downloadZipBtn");
|
const zipBtn = document.getElementById("downloadZipBtn");
|
||||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||||
|
const createBtn = document.getElementById("createFileBtn");
|
||||||
|
|
||||||
// keep the “select all” in sync ——
|
const anyFiles = fileCheckboxes.length > 0;
|
||||||
const master = document.getElementById("selectAll");
|
const anySelected = selectedCheckboxes.length > 0;
|
||||||
if (master) {
|
const anyZip = Array.from(selectedCheckboxes)
|
||||||
if (selectedCheckboxes.length === fileCheckboxes.length) {
|
.some(cb => cb.value.toLowerCase().endsWith(".zip"));
|
||||||
master.checked = true;
|
|
||||||
master.indeterminate = false;
|
|
||||||
} else if (selectedCheckboxes.length === 0) {
|
|
||||||
master.checked = false;
|
|
||||||
master.indeterminate = false;
|
|
||||||
} else {
|
|
||||||
master.checked = false;
|
|
||||||
master.indeterminate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileCheckboxes.length === 0) {
|
// — Select All checkbox sync (unchanged) —
|
||||||
if (copyBtn) copyBtn.style.display = "none";
|
const master = document.getElementById("selectAll");
|
||||||
if (moveBtn) moveBtn.style.display = "none";
|
if (master) {
|
||||||
if (deleteBtn) deleteBtn.style.display = "none";
|
if (selectedCheckboxes.length === fileCheckboxes.length) {
|
||||||
if (zipBtn) zipBtn.style.display = "none";
|
master.checked = true;
|
||||||
if (extractZipBtn) extractZipBtn.style.display = "none";
|
master.indeterminate = false;
|
||||||
} else {
|
} else if (selectedCheckboxes.length === 0) {
|
||||||
if (copyBtn) copyBtn.style.display = "inline-block";
|
master.checked = false;
|
||||||
if (moveBtn) moveBtn.style.display = "inline-block";
|
master.indeterminate = false;
|
||||||
if (deleteBtn) deleteBtn.style.display = "inline-block";
|
} else {
|
||||||
if (zipBtn) zipBtn.style.display = "inline-block";
|
master.checked = false;
|
||||||
if (extractZipBtn) extractZipBtn.style.display = "inline-block";
|
master.indeterminate = true;
|
||||||
|
|
||||||
const anySelected = selectedCheckboxes.length > 0;
|
|
||||||
if (copyBtn) copyBtn.disabled = !anySelected;
|
|
||||||
if (moveBtn) moveBtn.disabled = !anySelected;
|
|
||||||
if (deleteBtn) deleteBtn.disabled = !anySelected;
|
|
||||||
if (zipBtn) zipBtn.disabled = !anySelected;
|
|
||||||
|
|
||||||
if (extractZipBtn) {
|
|
||||||
// Enable only if at least one selected file ends with .zip (case-insensitive).
|
|
||||||
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
|
|
||||||
chk.value.toLowerCase().endsWith(".zip")
|
|
||||||
);
|
|
||||||
extractZipBtn.disabled = !anyZipSelected;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete / Copy / Move: only show when something is selected
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.style.display = anySelected ? "" : "none";
|
||||||
|
}
|
||||||
|
if (copyBtn) {
|
||||||
|
copyBtn.style.display = anySelected ? "" : "none";
|
||||||
|
}
|
||||||
|
if (moveBtn) {
|
||||||
|
moveBtn.style.display = anySelected ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download ZIP: only show when something is selected
|
||||||
|
if (zipBtn) {
|
||||||
|
zipBtn.style.display = anySelected ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ZIP: only show when a selected file is a .zip
|
||||||
|
if (extractZipBtn) {
|
||||||
|
extractZipBtn.style.display = anyZip ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create File: only show when nothing is selected
|
||||||
|
if (createBtn) {
|
||||||
|
createBtn.style.display = anySelected ? "none" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally disable the ones that are shown but shouldn’t be clickable
|
||||||
|
if (deleteBtn) deleteBtn.disabled = !anySelected;
|
||||||
|
if (copyBtn) copyBtn.disabled = !anySelected;
|
||||||
|
if (moveBtn) moveBtn.disabled = !anySelected;
|
||||||
|
if (zipBtn) zipBtn.disabled = !anySelected;
|
||||||
|
if (extractZipBtn) extractZipBtn.disabled = !anyZip;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showToast(message, duration = 3000) {
|
export function showToast(message, duration = 3000) {
|
||||||
@@ -178,9 +190,14 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
||||||
}
|
}
|
||||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}" data-preview-name="${safeFileName}">
|
previewButton = `<button
|
||||||
${previewIcon}
|
type="button"
|
||||||
</button>`;
|
class="btn btn-sm btn-info preview-btn"
|
||||||
|
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
|
||||||
|
data-preview-name="${safeFileName}"
|
||||||
|
title="${t('preview')}">
|
||||||
|
${previewIcon}
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -194,19 +211,44 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
<td class="hide-small nowrap">${safeSize}</td>
|
<td class="hide-small nowrap">${safeSize}</td>
|
||||||
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
<td class="hide-small hide-medium nowrap">${safeUploader}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="button-wrap" style="display: flex; justify-content: left; gap: 5px;">
|
<div class="btn-group btn-group-sm" role="group" aria-label="File actions">
|
||||||
<button type="button" class="btn btn-sm btn-success download-btn" data-download-name="${file.name}" data-download-folder="${file.folder || 'root'}" title="${t('download')}">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success download-btn"
|
||||||
|
data-download-name="${file.name}"
|
||||||
|
data-download-folder="${file.folder || 'root'}"
|
||||||
|
title="${t('download')}">
|
||||||
<i class="material-icons">file_download</i>
|
<i class="material-icons">file_download</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
${file.editable ? `
|
${file.editable ? `
|
||||||
<button class="btn btn-sm edit-btn" data-edit-name="${file.name}" data-edit-folder="${file.folder || 'root'}" title="${t('edit')}">
|
<button
|
||||||
<i class="material-icons">edit</i>
|
type="button"
|
||||||
</button>
|
class="btn btn-sm btn-secondary edit-btn"
|
||||||
` : ""}
|
data-edit-name="${file.name}"
|
||||||
|
data-edit-folder="${file.folder || 'root'}"
|
||||||
|
title="${t('edit')}">
|
||||||
|
<i class="material-icons">edit</i>
|
||||||
|
</button>` : ""}
|
||||||
|
|
||||||
${previewButton}
|
${previewButton}
|
||||||
<button class="btn btn-sm btn-warning rename-btn" data-rename-name="${file.name}" data-rename-folder="${file.folder || 'root'}" title="${t('rename')}">
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-warning rename-btn"
|
||||||
|
data-rename-name="${file.name}"
|
||||||
|
data-rename-folder="${file.folder || 'root'}"
|
||||||
|
title="${t('rename')}">
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- share -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm share-btn ms-1"
|
||||||
|
data-file="${safeFileName}"
|
||||||
|
title="${t('share')}">
|
||||||
|
<i class="material-icons">share</i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -32,23 +32,33 @@ export function loadSidebarOrder() {
|
|||||||
updateSidebarVisibility();
|
updateSidebarVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Load header order from localStorage.
|
|
||||||
export function loadHeaderOrder() {
|
export function loadHeaderOrder() {
|
||||||
const headerDropArea = document.getElementById('headerDropArea');
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
if (!headerDropArea) return;
|
if (!headerDropArea) return;
|
||||||
const orderStr = localStorage.getItem('headerOrder');
|
|
||||||
if (orderStr) {
|
// 1) Clear out any icons that might already be in the drop area
|
||||||
const order = JSON.parse(orderStr);
|
headerDropArea.innerHTML = '';
|
||||||
if (order.length > 0) {
|
|
||||||
order.forEach(id => {
|
// 2) Read the saved array (or empty array if invalid/missing)
|
||||||
const card = document.getElementById(id);
|
let stored;
|
||||||
// Only load if card is not already in header drop zone.
|
try {
|
||||||
if (card && card.parentNode.id !== 'headerDropArea') {
|
stored = JSON.parse(localStorage.getItem('headerOrder') || '[]');
|
||||||
insertCardInHeader(card, null);
|
} catch {
|
||||||
}
|
stored = [];
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) Deduplicate IDs
|
||||||
|
const uniqueIds = Array.from(new Set(stored));
|
||||||
|
|
||||||
|
// 4) Re-insert exactly one icon per saved card ID
|
||||||
|
uniqueIds.forEach(id => {
|
||||||
|
const card = document.getElementById(id);
|
||||||
|
if (card) insertCardInHeader(card, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5) Persist the cleaned, deduped list back to storage
|
||||||
|
localStorage.setItem('headerOrder', JSON.stringify(uniqueIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal helper: update sidebar visibility based on its content.
|
// Internal helper: update sidebar visibility based on its content.
|
||||||
|
|||||||
@@ -76,6 +76,72 @@ export function handleDownloadZipSelected(e) {
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function handleCreateFileSelected(e) {
|
||||||
|
e.preventDefault(); e.stopImmediatePropagation();
|
||||||
|
const modal = document.getElementById('createFileModal');
|
||||||
|
modal.style.display = 'block';
|
||||||
|
setTimeout(() => {
|
||||||
|
const inp = document.getElementById('newFileCreateName');
|
||||||
|
if (inp) inp.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the “New File” modal
|
||||||
|
*/
|
||||||
|
export function openCreateFileModal() {
|
||||||
|
const modal = document.getElementById('createFileModal');
|
||||||
|
const input = document.getElementById('createFileNameInput');
|
||||||
|
if (!modal || !input) {
|
||||||
|
console.error('Create-file modal or input not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.value = '';
|
||||||
|
modal.style.display = 'block';
|
||||||
|
setTimeout(() => input.focus(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function handleCreateFile(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = document.getElementById('createFileNameInput');
|
||||||
|
if (!input) return console.error('Create-file input missing');
|
||||||
|
const name = input.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
showToast(t('newfile_placeholder')); // or a more explicit error
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = window.currentFolder || 'root';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/file/createFile.php', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type':'application/json',
|
||||||
|
'X-CSRF-Token': window.csrfToken
|
||||||
|
},
|
||||||
|
// ⚠️ must send `name`, not `filename`
|
||||||
|
body: JSON.stringify({ folder, name })
|
||||||
|
});
|
||||||
|
const js = await res.json();
|
||||||
|
if (!js.success) throw new Error(js.error);
|
||||||
|
showToast(t('file_created'));
|
||||||
|
loadFileList(folder);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || t('error_creating_file'));
|
||||||
|
} finally {
|
||||||
|
document.getElementById('createFileModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const cancel = document.getElementById('cancelCreateFile');
|
||||||
|
const confirm = document.getElementById('confirmCreateFile');
|
||||||
|
if (cancel) cancel.addEventListener('click', () => document.getElementById('createFileModal').style.display = 'none');
|
||||||
|
if (confirm) confirm.addEventListener('click', handleCreateFile);
|
||||||
|
});
|
||||||
|
|
||||||
export function openDownloadModal(fileName, folder) {
|
export function openDownloadModal(fileName, folder) {
|
||||||
// Store file details globally for the download confirmation function.
|
// Store file details globally for the download confirmation function.
|
||||||
window.singleFileToDownload = fileName;
|
window.singleFileToDownload = fileName;
|
||||||
@@ -197,6 +263,49 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const progressModal = document.getElementById("downloadProgressModal");
|
const progressModal = document.getElementById("downloadProgressModal");
|
||||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||||
|
const cancelCreate = document.getElementById('cancelCreateFile');
|
||||||
|
|
||||||
|
if (cancelCreate) {
|
||||||
|
cancelCreate.addEventListener('click', () => {
|
||||||
|
document.getElementById('createFileModal').style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmCreate = document.getElementById('confirmCreateFile');
|
||||||
|
if (confirmCreate) {
|
||||||
|
confirmCreate.addEventListener('click', async () => {
|
||||||
|
const name = document.getElementById('newFileCreateName').value.trim();
|
||||||
|
if (!name) {
|
||||||
|
showToast(t('please_enter_filename'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('createFileModal').style.display = 'none';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/file/createFile.php', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': window.csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
folder: window.currentFolder || 'root',
|
||||||
|
filename: name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const js = await res.json();
|
||||||
|
if (!res.ok || !js.success) {
|
||||||
|
throw new Error(js.error || t('error_creating_file'));
|
||||||
|
}
|
||||||
|
showToast(t('file_created_successfully'));
|
||||||
|
loadFileList(window.currentFolder);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showToast(err.message || t('error_creating_file'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
attachEnterKeyListener('createFileModal','confirmCreateFile');
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Cancel button hides the name modal
|
// 1) Cancel button hides the name modal
|
||||||
if (cancelZipBtn) {
|
if (cancelZipBtn) {
|
||||||
@@ -553,8 +662,14 @@ export function initFileActions() {
|
|||||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
||||||
}
|
}
|
||||||
|
const createBtn = document.getElementById('createFileBtn');
|
||||||
|
if (createBtn) {
|
||||||
|
createBtn.replaceWith(createBtn.cloneNode(true));
|
||||||
|
document.getElementById('createFileBtn').addEventListener('click', openCreateFileModal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Hook up the single‐file download modal buttons
|
// Hook up the single‐file download modal buttons
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ import { t } from './i18n.js';
|
|||||||
import { bindFileListContextMenu } from './fileMenu.js';
|
import { bindFileListContextMenu } from './fileMenu.js';
|
||||||
import { openDownloadModal } from './fileActions.js';
|
import { openDownloadModal } from './fileActions.js';
|
||||||
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
import { openTagModal, openMultiTagModal } from './fileTags.js';
|
||||||
|
import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation } from './folderManager.js';
|
||||||
|
import {
|
||||||
|
folderDragOverHandler,
|
||||||
|
folderDragLeaveHandler,
|
||||||
|
folderDropHandler
|
||||||
|
} from './fileDragDrop.js';
|
||||||
|
|
||||||
export let fileData = [];
|
export let fileData = [];
|
||||||
export let sortOrder = { column: "uploaded", ascending: true };
|
export let sortOrder = { column: "uploaded", ascending: true };
|
||||||
@@ -186,100 +192,231 @@ export function formatFolderName(folder) {
|
|||||||
window.toggleRowSelection = toggleRowSelection;
|
window.toggleRowSelection = toggleRowSelection;
|
||||||
window.updateRowHighlight = updateRowHighlight;
|
window.updateRowHighlight = updateRowHighlight;
|
||||||
|
|
||||||
/**
|
export async function loadFileList(folderParam) {
|
||||||
* --- FILE LIST & VIEW RENDERING ---
|
|
||||||
*/
|
|
||||||
export function loadFileList(folderParam) {
|
|
||||||
const folder = folderParam || "root";
|
const folder = folderParam || "root";
|
||||||
const fileListContainer = document.getElementById("fileList");
|
const fileListContainer = document.getElementById("fileList");
|
||||||
|
const actionsContainer = document.getElementById("fileListActions");
|
||||||
|
|
||||||
|
// 1) show loader
|
||||||
fileListContainer.style.visibility = "hidden";
|
fileListContainer.style.visibility = "hidden";
|
||||||
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
|
||||||
|
|
||||||
return fetch("/api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime())
|
try {
|
||||||
.then(response => {
|
// 2) fetch files + folders in parallel
|
||||||
if (response.status === 401) {
|
const [filesRes, foldersRes] = await Promise.all([
|
||||||
showToast("Session expired. Please log in again.");
|
fetch(`/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=1&t=${Date.now()}`),
|
||||||
window.location.href = "/api/auth/logout.php";
|
fetch(`/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`)
|
||||||
throw new Error("Unauthorized");
|
]);
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
fileListContainer.innerHTML = ""; // Clear loading message.
|
|
||||||
if (data.files && Object.keys(data.files).length > 0) {
|
|
||||||
// If the returned "files" is an object instead of an array, transform it.
|
|
||||||
if (!Array.isArray(data.files)) {
|
|
||||||
data.files = Object.entries(data.files).map(([name, meta]) => {
|
|
||||||
meta.name = name;
|
|
||||||
return meta;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Process each file – add computed properties.
|
|
||||||
data.files = data.files.map(file => {
|
|
||||||
file.fullName = (file.path || file.name).trim().toLowerCase();
|
|
||||||
file.editable = canEditFile(file.name);
|
|
||||||
file.folder = folder;
|
|
||||||
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
|
||||||
file.type = "image";
|
|
||||||
}
|
|
||||||
// OPTIONAL: For text documents, preload content (if available from backend)
|
|
||||||
// Example: if (/\.txt|html|md|js|css|json|xml$/i.test(file.name)) { file.content = file.content || ""; }
|
|
||||||
return file;
|
|
||||||
});
|
|
||||||
fileData = data.files;
|
|
||||||
|
|
||||||
// Update file summary.
|
if (filesRes.status === 401) {
|
||||||
const actionsContainer = document.getElementById("fileListActions");
|
window.location.href = "/api/auth/logout.php";
|
||||||
if (actionsContainer) {
|
throw new Error("Unauthorized");
|
||||||
let summaryElem = document.getElementById("fileSummary");
|
}
|
||||||
if (!summaryElem) {
|
const data = await filesRes.json();
|
||||||
summaryElem = document.createElement("div");
|
const folderRaw = await foldersRes.json();
|
||||||
summaryElem.id = "fileSummary";
|
|
||||||
summaryElem.style.float = "right";
|
|
||||||
summaryElem.style.marginLeft = "auto";
|
|
||||||
summaryElem.style.marginRight = "60px";
|
|
||||||
summaryElem.style.fontSize = "0.9em";
|
|
||||||
actionsContainer.appendChild(summaryElem);
|
|
||||||
} else {
|
|
||||||
summaryElem.style.display = "block";
|
|
||||||
}
|
|
||||||
summaryElem.innerHTML = buildFolderSummary(fileData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render view based on the view mode.
|
// --- build ONLY the *direct* children of current folder ---
|
||||||
if (window.viewMode === "gallery") {
|
let subfolders = [];
|
||||||
renderGalleryView(folder);
|
const hidden = new Set(["profile_pics", "trash"]);
|
||||||
updateFileActionButtons();
|
if (Array.isArray(folderRaw)) {
|
||||||
} else {
|
const allPaths = folderRaw.map(item => item.folder ?? item);
|
||||||
renderFileTable(folder);
|
const depth = folder === "root" ? 1 : folder.split("/").length + 1;
|
||||||
}
|
subfolders = allPaths
|
||||||
} else {
|
.filter(p => {
|
||||||
fileListContainer.textContent = t("no_files_found");
|
if (folder === "root") {
|
||||||
const summaryElem = document.getElementById("fileSummary");
|
return p.indexOf("/") === -1;
|
||||||
if (summaryElem) {
|
}
|
||||||
summaryElem.style.display = "none";
|
if (!p.startsWith(folder + "/")) return false;
|
||||||
}
|
return p.split("/").length === depth;
|
||||||
updateFileActionButtons();
|
})
|
||||||
}
|
.map(p => ({ name: p.split("/").pop(), full: p }));
|
||||||
return data.files || [];
|
}
|
||||||
})
|
subfolders = subfolders.filter(sf => !hidden.has(sf.name));
|
||||||
.catch(error => {
|
|
||||||
console.error("Error loading file list:", error);
|
// 3) clear loader
|
||||||
if (error.message !== "Unauthorized") {
|
fileListContainer.innerHTML = "";
|
||||||
fileListContainer.textContent = "Error loading files.";
|
|
||||||
}
|
// 4) handle “no files” case
|
||||||
|
if (!data.files || Object.keys(data.files).length === 0) {
|
||||||
|
fileListContainer.textContent = t("no_files_found");
|
||||||
|
|
||||||
|
// hide summary
|
||||||
|
const summaryElem = document.getElementById("fileSummary");
|
||||||
|
if (summaryElem) summaryElem.style.display = "none";
|
||||||
|
|
||||||
|
// hide slider
|
||||||
|
const sliderContainer = document.getElementById("viewSliderContainer");
|
||||||
|
if (sliderContainer) sliderContainer.style.display = "none";
|
||||||
|
|
||||||
|
// hide folder strip
|
||||||
|
const strip = document.getElementById("folderStripContainer");
|
||||||
|
if (strip) strip.style.display = "none";
|
||||||
|
|
||||||
|
updateFileActionButtons();
|
||||||
return [];
|
return [];
|
||||||
})
|
}
|
||||||
.finally(() => {
|
|
||||||
fileListContainer.style.visibility = "visible";
|
// 5) normalize files array
|
||||||
|
if (!Array.isArray(data.files)) {
|
||||||
|
data.files = Object.entries(data.files).map(([name, meta]) => {
|
||||||
|
meta.name = name;
|
||||||
|
return meta;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
data.files = data.files.map(f => {
|
||||||
|
f.fullName = (f.path || f.name).trim().toLowerCase();
|
||||||
|
f.editable = canEditFile(f.name);
|
||||||
|
f.folder = folder;
|
||||||
|
return f;
|
||||||
});
|
});
|
||||||
|
fileData = data.files;
|
||||||
|
|
||||||
|
// 6) inject summary + slider
|
||||||
|
if (actionsContainer) {
|
||||||
|
// a) summary
|
||||||
|
let summaryElem = document.getElementById("fileSummary");
|
||||||
|
if (!summaryElem) {
|
||||||
|
summaryElem = document.createElement("div");
|
||||||
|
summaryElem.id = "fileSummary";
|
||||||
|
summaryElem.style.cssText = "float:right; margin:0 60px 0 auto; font-size:0.9em;";
|
||||||
|
actionsContainer.appendChild(summaryElem);
|
||||||
|
}
|
||||||
|
summaryElem.style.display = "block";
|
||||||
|
summaryElem.innerHTML = buildFolderSummary(fileData);
|
||||||
|
|
||||||
|
// b) slider
|
||||||
|
const viewMode = window.viewMode || "table";
|
||||||
|
let sliderContainer = document.getElementById("viewSliderContainer");
|
||||||
|
if (!sliderContainer) {
|
||||||
|
sliderContainer = document.createElement("div");
|
||||||
|
sliderContainer.id = "viewSliderContainer";
|
||||||
|
sliderContainer.style.cssText = "display:inline-flex; align-items:center; margin-right:auto; font-size:0.9em;";
|
||||||
|
actionsContainer.insertBefore(sliderContainer, summaryElem);
|
||||||
|
} else {
|
||||||
|
sliderContainer.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === "gallery") {
|
||||||
|
const w = window.innerWidth;
|
||||||
|
let maxCols;
|
||||||
|
if (w < 600) maxCols = 1;
|
||||||
|
else if (w < 900) maxCols = 2;
|
||||||
|
else if (w < 1200) maxCols = 4;
|
||||||
|
else maxCols = 6;
|
||||||
|
|
||||||
|
const currentCols = Math.min(
|
||||||
|
parseInt(localStorage.getItem("galleryColumns") || "3", 10),
|
||||||
|
maxCols
|
||||||
|
);
|
||||||
|
|
||||||
|
sliderContainer.innerHTML = `
|
||||||
|
<label for="galleryColumnsSlider" style="margin-right:8px;line-height:1;">
|
||||||
|
${t("columns")}:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="galleryColumnsSlider"
|
||||||
|
min="1"
|
||||||
|
max="${maxCols}"
|
||||||
|
value="${currentCols}"
|
||||||
|
style="vertical-align:middle;"
|
||||||
|
>
|
||||||
|
<span id="galleryColumnsValue" style="margin-left:6px;line-height:1;">${currentCols}</span>
|
||||||
|
`;
|
||||||
|
const gallerySlider = document.getElementById("galleryColumnsSlider");
|
||||||
|
const galleryValue = document.getElementById("galleryColumnsValue");
|
||||||
|
gallerySlider.oninput = e => {
|
||||||
|
const v = +e.target.value;
|
||||||
|
localStorage.setItem("galleryColumns", v);
|
||||||
|
galleryValue.textContent = v;
|
||||||
|
document.querySelector(".gallery-container")
|
||||||
|
?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10);
|
||||||
|
sliderContainer.innerHTML = `
|
||||||
|
<label for="rowHeightSlider" style="margin-right:8px;line-height:1;">
|
||||||
|
${t("row_height")}:
|
||||||
|
</label>
|
||||||
|
<input type="range" id="rowHeightSlider" min="30" max="60" value="${currentHeight}" style="vertical-align:middle;">
|
||||||
|
<span id="rowHeightValue" style="margin-left:6px;line-height:1;">${currentHeight}px</span>
|
||||||
|
`;
|
||||||
|
const rowSlider = document.getElementById("rowHeightSlider");
|
||||||
|
const rowValue = document.getElementById("rowHeightValue");
|
||||||
|
rowSlider.oninput = e => {
|
||||||
|
const v = e.target.value;
|
||||||
|
document.documentElement.style.setProperty("--file-row-height", v + "px");
|
||||||
|
localStorage.setItem("rowHeight", v);
|
||||||
|
rowValue.textContent = v + "px";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) inject folder strip below actions, above file list
|
||||||
|
let strip = document.getElementById("folderStripContainer");
|
||||||
|
if (!strip) {
|
||||||
|
strip = document.createElement("div");
|
||||||
|
strip.id = "folderStripContainer";
|
||||||
|
strip.className = "folder-strip-container";
|
||||||
|
actionsContainer.parentNode.insertBefore(strip, actionsContainer);
|
||||||
|
}
|
||||||
|
if (window.showFoldersInList && subfolders.length) {
|
||||||
|
strip.innerHTML = subfolders.map(sf => `
|
||||||
|
<div class="folder-item" data-folder="${sf.full}" draggable="true">
|
||||||
|
<i class="material-icons">folder</i>
|
||||||
|
<div class="folder-name">${escapeHTML(sf.name)}</div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
strip.style.display = "flex";
|
||||||
|
|
||||||
|
strip.querySelectorAll(".folder-item").forEach(el => {
|
||||||
|
// click‐to‐navigate
|
||||||
|
el.addEventListener("click", () => {
|
||||||
|
const dest = el.dataset.folder;
|
||||||
|
window.currentFolder = dest;
|
||||||
|
localStorage.setItem("lastOpenedFolder", dest);
|
||||||
|
updateBreadcrumbTitle(dest);
|
||||||
|
document.querySelectorAll(".folder-option.selected")
|
||||||
|
.forEach(o => o.classList.remove("selected"));
|
||||||
|
document.querySelector(`.folder-option[data-folder="${dest}"]`)
|
||||||
|
?.classList.add("selected");
|
||||||
|
loadFileList(dest);
|
||||||
|
});
|
||||||
|
|
||||||
|
// drag & drop handlers
|
||||||
|
el.addEventListener("dragover", folderDragOverHandler);
|
||||||
|
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||||
|
el.addEventListener("drop", folderDropHandler);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
strip.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8) render files
|
||||||
|
if (window.viewMode === "gallery") {
|
||||||
|
renderGalleryView(folder);
|
||||||
|
} else {
|
||||||
|
renderFileTable(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileActionButtons();
|
||||||
|
return data.files;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading file list:", err);
|
||||||
|
if (err.message !== "Unauthorized") {
|
||||||
|
fileListContainer.textContent = "Error loading files.";
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
fileListContainer.style.visibility = "visible";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update renderFileTable so it writes its content into the provided container.
|
* Update renderFileTable so it writes its content into the provided container.
|
||||||
*/
|
*/
|
||||||
export function renderFileTable(folder, container) {
|
export function renderFileTable(folder, container, subfolders) {
|
||||||
const fileListContent = container || document.getElementById("fileList");
|
const fileListContent = container || document.getElementById("fileList");
|
||||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10);
|
||||||
@@ -327,9 +464,6 @@ export function renderFileTable(folder, container) {
|
|||||||
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
rowHTML = rowHTML.replace(/(<td class="file-name-cell">)(.*?)(<\/td>)/, (match, p1, p2, p3) => {
|
||||||
return p1 + p2 + tagBadgesHTML + p3;
|
return p1 + p2 + tagBadgesHTML + p3;
|
||||||
});
|
});
|
||||||
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="${t('share')}">
|
|
||||||
<i class="material-icons">share</i>
|
|
||||||
</button>$1`);
|
|
||||||
rowsHTML += rowHTML;
|
rowsHTML += rowHTML;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -340,6 +474,10 @@ export function renderFileTable(folder, container) {
|
|||||||
|
|
||||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||||
|
|
||||||
|
fileListContent.querySelectorAll('.folder-item').forEach(el => {
|
||||||
|
el.addEventListener('click', () => loadFileList(el.dataset.folder));
|
||||||
|
});
|
||||||
|
|
||||||
// pagination clicks
|
// pagination clicks
|
||||||
const prevBtn = document.getElementById("prevPageBtn");
|
const prevBtn = document.getElementById("prevPageBtn");
|
||||||
if (prevBtn) prevBtn.addEventListener("click", () => {
|
if (prevBtn) prevBtn.addEventListener("click", () => {
|
||||||
@@ -414,7 +552,7 @@ export function renderFileTable(folder, container) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5) Preview buttons (if you still have a .preview-btn)
|
// 5) Preview buttons
|
||||||
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
fileListContent.querySelectorAll(".preview-btn").forEach(btn => {
|
||||||
btn.addEventListener("click", e => {
|
btn.addEventListener("click", e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -441,6 +579,17 @@ export function renderFileTable(folder, container) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, 300));
|
}, 300));
|
||||||
}
|
}
|
||||||
|
const slider = document.getElementById('rowHeightSlider');
|
||||||
|
const valueDisplay = document.getElementById('rowHeightValue');
|
||||||
|
if (slider) {
|
||||||
|
slider.addEventListener('input', e => {
|
||||||
|
const v = +e.target.value; // slider value in px
|
||||||
|
document.documentElement.style.setProperty('--file-row-height', v + 'px');
|
||||||
|
localStorage.setItem('rowHeight', v);
|
||||||
|
valueDisplay.textContent = v + 'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
|
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
|
||||||
cell.addEventListener("click", function () {
|
cell.addEventListener("click", function () {
|
||||||
const column = this.getAttribute("data-column");
|
const column = this.getAttribute("data-column");
|
||||||
@@ -530,18 +679,17 @@ export function renderGalleryView(folder, container) {
|
|||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// --- Column slider ---
|
// --- Column slider with responsive max ---
|
||||||
const numColumns = window.galleryColumns || 3;
|
const numColumns = window.galleryColumns || 3;
|
||||||
galleryHTML += `
|
// clamp slider max to 1 on small (<600px), 2 on medium (<900px), else up to 6
|
||||||
<div class="gallery-slider" style="margin:10px; text-align:center;">
|
const w = window.innerWidth;
|
||||||
<label for="galleryColumnsSlider" style="margin-right:5px;">
|
let maxCols = 6;
|
||||||
${t('columns')}:
|
if (w < 600) maxCols = 1;
|
||||||
</label>
|
else if (w < 900) maxCols = 2;
|
||||||
<input type="range" id="galleryColumnsSlider" min="1" max="6"
|
|
||||||
value="${numColumns}" style="vertical-align:middle;">
|
// ensure current value doesn’t exceed the new max
|
||||||
<span id="galleryColumnsValue">${numColumns}</span>
|
const startCols = Math.min(numColumns, maxCols);
|
||||||
</div>
|
window.galleryColumns = startCols;
|
||||||
`;
|
|
||||||
|
|
||||||
// --- Start gallery grid ---
|
// --- Start gallery grid ---
|
||||||
galleryHTML += `
|
galleryHTML += `
|
||||||
@@ -627,32 +775,52 @@ export function renderGalleryView(folder, container) {
|
|||||||
</span>
|
</span>
|
||||||
${tagBadgesHTML}
|
${tagBadgesHTML}
|
||||||
|
|
||||||
<div class="button-wrap" style="display:flex; justify-content:center; gap:5px; margin-top:5px;">
|
<div
|
||||||
<button type="button" class="btn btn-sm btn-success download-btn"
|
class="btn-group btn-group-sm btn-group-hover"
|
||||||
data-download-name="${escapeHTML(file.name)}"
|
role="group"
|
||||||
data-download-folder="${file.folder || "root"}"
|
aria-label="File actions"
|
||||||
title="${t('download')}">
|
style="margin-top:5px;"
|
||||||
<i class="material-icons">file_download</i>
|
>
|
||||||
</button>
|
<button
|
||||||
${file.editable ? `
|
type="button"
|
||||||
<button type="button" class="btn btn-sm edit-btn"
|
class="btn btn-success py-1 download-btn"
|
||||||
data-edit-name="${escapeHTML(file.name)}"
|
data-download-name="${escapeHTML(file.name)}"
|
||||||
data-edit-folder="${file.folder || "root"}"
|
data-download-folder="${file.folder || "root"}"
|
||||||
title="${t('edit')}">
|
title="${t('download')}"
|
||||||
<i class="material-icons">edit</i>
|
>
|
||||||
</button>` : ""}
|
<i class="material-icons">file_download</i>
|
||||||
<button type="button" class="btn btn-sm btn-warning rename-btn"
|
</button>
|
||||||
data-rename-name="${escapeHTML(file.name)}"
|
|
||||||
data-rename-folder="${file.folder || "root"}"
|
${file.editable ? `
|
||||||
title="${t('rename')}">
|
<button
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
type="button"
|
||||||
</button>
|
class="btn btn-secondary py-1 edit-btn"
|
||||||
<button type="button" class="btn btn-sm btn-secondary share-btn"
|
data-edit-name="${escapeHTML(file.name)}"
|
||||||
data-file="${escapeHTML(file.name)}"
|
data-edit-folder="${file.folder || "root"}"
|
||||||
title="${t('share')}">
|
title="${t('edit')}"
|
||||||
<i class="material-icons">share</i>
|
>
|
||||||
</button>
|
<i class="material-icons">edit</i>
|
||||||
</div>
|
</button>` : ""}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-warning py-1 rename-btn"
|
||||||
|
data-rename-name="${escapeHTML(file.name)}"
|
||||||
|
data-rename-folder="${file.folder || "root"}"
|
||||||
|
title="${t('rename')}"
|
||||||
|
>
|
||||||
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary py-1 share-btn"
|
||||||
|
data-file="${escapeHTML(file.name)}"
|
||||||
|
title="${t('share')}"
|
||||||
|
>
|
||||||
|
<i class="material-icons">share</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// fileMenu.js
|
// fileMenu.js
|
||||||
import { updateRowHighlight, showToast } from './domUtils.js';
|
import { updateRowHighlight, showToast } from './domUtils.js';
|
||||||
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js';
|
import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile, openCreateFileModal } from './fileActions.js';
|
||||||
import { previewFile } from './filePreview.js';
|
import { previewFile } from './filePreview.js';
|
||||||
import { editFile } from './fileEditor.js';
|
import { editFile } from './fileEditor.js';
|
||||||
import { canEditFile, fileData } from './fileListView.js';
|
import { canEditFile, fileData } from './fileListView.js';
|
||||||
@@ -75,6 +75,7 @@ export function fileListContextMenuHandler(e) {
|
|||||||
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
|
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
|
||||||
|
|
||||||
let menuItems = [
|
let menuItems = [
|
||||||
|
{ label: t("create_file"), action: () => openCreateFileModal() },
|
||||||
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
|
{ label: t("delete_selected"), action: () => { handleDeleteSelected(new Event("click")); } },
|
||||||
{ label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
|
{ label: t("copy_selected"), action: () => { handleCopySelected(new Event("click")); } },
|
||||||
{ label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } },
|
{ label: t("move_selected"), action: () => { handleMoveSelected(new Event("click")); } },
|
||||||
|
|||||||
@@ -13,10 +13,19 @@ export function openTagModal(file) {
|
|||||||
modal.id = 'tagModal';
|
modal.id = 'tagModal';
|
||||||
modal.className = 'modal';
|
modal.className = 'modal';
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-content" style="width: 400px; max-width:90vw;">
|
<div class="modal-content" style="width: 450px; max-width:90vw;">
|
||||||
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
<h3 style="margin:0;">${t("tag_file")}: ${file.name}</h3>
|
<h3 style="
|
||||||
<span id="closeTagModal" style="cursor:pointer; font-size:24px;">×</span>
|
margin:0;
|
||||||
|
display:inline-block;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
white-space:nowrap;
|
||||||
|
">
|
||||||
|
${t("tag_file")}: ${escapeHTML(file.name)}
|
||||||
|
</h3>
|
||||||
|
<span id="closeTagModal" class="editor-close-btn">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="margin-top:10px;">
|
<div class="modal-body" style="margin-top:10px;">
|
||||||
<label for="tagNameInput">${t("tag_name")}</label>
|
<label for="tagNameInput">${t("tag_name")}</label>
|
||||||
@@ -83,10 +92,10 @@ export function openMultiTagModal(files) {
|
|||||||
modal.id = 'multiTagModal';
|
modal.id = 'multiTagModal';
|
||||||
modal.className = 'modal';
|
modal.className = 'modal';
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-content" style="width: 400px; max-width:90vw;">
|
<div class="modal-content" style="width: 450px; max-width:90vw;">
|
||||||
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
<h3 style="margin:0;">Tag Selected Files (${files.length})</h3>
|
<h3 style="margin:0;">Tag Selected Files (${files.length})</h3>
|
||||||
<span id="closeMultiTagModal" style="cursor:pointer; font-size:24px;">×</span>
|
<span id="closeMultiTagModal" class="editor-close-btn">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="margin-top:10px;">
|
<div class="modal-body" style="margin-top:10px;">
|
||||||
<label for="multiTagNameInput">Tag Name:</label>
|
<label for="multiTagNameInput">Tag Name:</label>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function saveFolderTreeState(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper for getting the parent folder.
|
// Helper for getting the parent folder.
|
||||||
function getParentFolder(folder) {
|
export function getParentFolder(folder) {
|
||||||
if (folder === "root") return "root";
|
if (folder === "root") return "root";
|
||||||
const lastSlash = folder.lastIndexOf("/");
|
const lastSlash = folder.lastIndexOf("/");
|
||||||
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
return lastSlash === -1 ? "root" : folder.substring(0, lastSlash);
|
||||||
@@ -236,7 +236,8 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
|
|||||||
const state = loadFolderTreeState();
|
const state = loadFolderTreeState();
|
||||||
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
|
let html = `<ul class="folder-tree ${defaultDisplay === 'none' ? 'collapsed' : 'expanded'}">`;
|
||||||
for (const folder in tree) {
|
for (const folder in tree) {
|
||||||
if (folder.toLowerCase() === "trash") continue;
|
const name = folder.toLowerCase();
|
||||||
|
if (name === "trash" || name === "profile_pics") continue;
|
||||||
const fullPath = parentPath ? parentPath + "/" + folder : folder;
|
const fullPath = parentPath ? parentPath + "/" + folder : folder;
|
||||||
const hasChildren = Object.keys(tree[folder]).length > 0;
|
const hasChildren = Object.keys(tree[folder]).length > 0;
|
||||||
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
const displayState = state[fullPath] !== undefined ? state[fullPath] : defaultDisplay;
|
||||||
@@ -360,7 +361,7 @@ function renderBreadcrumbFragment(folderPath) {
|
|||||||
return frag;
|
return frag;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBreadcrumbTitle(folder) {
|
export function updateBreadcrumbTitle(folder) {
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
titleEl.textContent = "";
|
titleEl.textContent = "";
|
||||||
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const translations = {
|
|||||||
// Additional keys for HTML translations:
|
// Additional keys for HTML translations:
|
||||||
"title": "FileRise",
|
"title": "FileRise",
|
||||||
"header_title": "FileRise",
|
"header_title": "FileRise",
|
||||||
|
"header_title_text": "Header Title",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"change_password": "Change Password",
|
"change_password": "Change Password",
|
||||||
"restore_text": "Restore or",
|
"restore_text": "Restore or",
|
||||||
@@ -182,9 +183,30 @@ const translations = {
|
|||||||
"switch_to_light_mode": "Switch to light mode",
|
"switch_to_light_mode": "Switch to light mode",
|
||||||
"switch_to_dark_mode": "Switch to dark mode",
|
"switch_to_dark_mode": "Switch to dark mode",
|
||||||
|
|
||||||
|
// Admin Panel
|
||||||
|
"header_settings": "Header Settings",
|
||||||
|
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
|
||||||
|
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
||||||
|
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
||||||
|
"manage_shared_links": "Manage Shared Links",
|
||||||
|
"folder_shares": "Folder Shares",
|
||||||
|
"file_shares": "File Shares",
|
||||||
|
"loading": "Loading…",
|
||||||
|
"error_loading_share_links": "Error loading share links",
|
||||||
|
"share_deleted_successfully": "Share deleted successfully",
|
||||||
|
"error_deleting_share": "Error deleting share",
|
||||||
|
"password_protected": "Password protected",
|
||||||
|
"no_shared_links_available": "No shared links available",
|
||||||
|
|
||||||
|
|
||||||
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
||||||
"admin_panel": "Admin Panel",
|
"admin_panel": "Admin Panel",
|
||||||
"user_panel": "User Panel",
|
"user_panel": "User Panel",
|
||||||
|
"user_settings": "User Settings",
|
||||||
|
"save_profile_picture": "Save Profile Picture",
|
||||||
|
"please_select_picture": "Please select a picture",
|
||||||
|
"profile_picture_updated": "Profile picture updated",
|
||||||
|
"error_updating_picture": "Error updating profile picture",
|
||||||
"trash_restore_delete": "Trash Restore/Delete",
|
"trash_restore_delete": "Trash Restore/Delete",
|
||||||
"totp_settings": "TOTP Settings",
|
"totp_settings": "TOTP Settings",
|
||||||
"enable_totp": "Enable TOTP",
|
"enable_totp": "Enable TOTP",
|
||||||
@@ -243,7 +265,17 @@ const translations = {
|
|||||||
"show": "Show",
|
"show": "Show",
|
||||||
"items_per_page": "items per page",
|
"items_per_page": "items per page",
|
||||||
"columns": "Columns",
|
"columns": "Columns",
|
||||||
"api_docs": "API Docs"
|
"row_height": "Row Height",
|
||||||
|
"api_docs": "API Docs",
|
||||||
|
"show_folders_above_files": "Show folders above files",
|
||||||
|
"display": "Display",
|
||||||
|
"create_file": "Create File",
|
||||||
|
"create_new_file": "Create New File",
|
||||||
|
"enter_file_name": "Enter file name",
|
||||||
|
"newfile_placeholder": "New file name",
|
||||||
|
"file_created_successfully": "File created successfully!",
|
||||||
|
"error_creating_file": "Error creating file",
|
||||||
|
"file_created": "File created successfully!"
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||||
@@ -300,6 +332,7 @@ const translations = {
|
|||||||
// Additional keys for HTML translations:
|
// Additional keys for HTML translations:
|
||||||
"title": "FileRise",
|
"title": "FileRise",
|
||||||
"header_title": "FileRise",
|
"header_title": "FileRise",
|
||||||
|
"header_title_text": "Header Title",
|
||||||
"logout": "Cerrar sesión",
|
"logout": "Cerrar sesión",
|
||||||
"change_password": "Cambiar contraseña",
|
"change_password": "Cambiar contraseña",
|
||||||
"restore_text": "Restaurar o",
|
"restore_text": "Restaurar o",
|
||||||
|
|||||||
@@ -14,37 +14,76 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
|
|||||||
import { editFile, saveFile } from './fileEditor.js';
|
import { editFile, saveFile } from './fileEditor.js';
|
||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
|
|
||||||
|
export function initializeApp() {
|
||||||
|
const saved = parseInt(localStorage.getItem('rowHeight') || '48', 10);
|
||||||
|
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||||||
|
window.currentFolder = "root";
|
||||||
|
initTagSearch();
|
||||||
|
loadFileList(window.currentFolder);
|
||||||
|
const stored = localStorage.getItem('showFoldersInList');
|
||||||
|
window.showFoldersInList = stored === null ? true : stored === 'true';
|
||||||
|
const fileListArea = document.getElementById('fileListContainer');
|
||||||
|
const uploadArea = document.getElementById('uploadDropArea');
|
||||||
|
if (fileListArea && uploadArea) {
|
||||||
|
fileListArea.addEventListener('dragover', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileListArea.classList.add('drop-hover');
|
||||||
|
});
|
||||||
|
fileListArea.addEventListener('dragleave', () => {
|
||||||
|
fileListArea.classList.remove('drop-hover');
|
||||||
|
});
|
||||||
|
fileListArea.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileListArea.classList.remove('drop-hover');
|
||||||
|
// re-dispatch the same drop into the real upload card
|
||||||
|
uploadArea.dispatchEvent(new DragEvent('drop', {
|
||||||
|
dataTransfer: e.dataTransfer,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initDragAndDrop();
|
||||||
|
loadSidebarOrder();
|
||||||
|
loadHeaderOrder();
|
||||||
|
initFileActions();
|
||||||
|
initUpload();
|
||||||
|
loadFolderTree();
|
||||||
|
setupTrashRestoreDelete();
|
||||||
|
loadAdminConfigFunc();
|
||||||
|
|
||||||
|
const helpBtn = document.getElementById("folderHelpBtn");
|
||||||
|
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||||
|
if (helpBtn && helpTooltip) {
|
||||||
|
helpBtn.addEventListener("click", () => {
|
||||||
|
helpTooltip.style.display =
|
||||||
|
helpTooltip.style.display === "block" ? "none" : "block";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function loadCsrfToken() {
|
export function loadCsrfToken() {
|
||||||
return fetchWithCsrf('/api/auth/token.php', {
|
return fetchWithCsrf('/api/auth/token.php', { method: 'GET' })
|
||||||
method: 'GET'
|
|
||||||
})
|
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error(`Token fetch failed with status ${res.status}`);
|
||||||
throw new Error(`Token fetch failed with status ${res.status}`);
|
|
||||||
}
|
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(({ csrf_token, share_url }) => {
|
.then(({ csrf_token, share_url }) => {
|
||||||
// Update global and <meta>
|
|
||||||
window.csrfToken = csrf_token;
|
window.csrfToken = csrf_token;
|
||||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
|
||||||
if (!meta) {
|
// update CSRF meta
|
||||||
meta = document.createElement('meta');
|
let meta = document.querySelector('meta[name="csrf-token"]') ||
|
||||||
meta.name = 'csrf-token';
|
Object.assign(document.head.appendChild(document.createElement('meta')), { name: 'csrf-token' });
|
||||||
document.head.appendChild(meta);
|
|
||||||
}
|
|
||||||
meta.content = csrf_token;
|
meta.content = csrf_token;
|
||||||
|
|
||||||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
// force share_url to match wherever we're browsing
|
||||||
if (!shareMeta) {
|
const actualShare = window.location.origin;
|
||||||
shareMeta = document.createElement('meta');
|
let shareMeta = document.querySelector('meta[name="share-url"]') ||
|
||||||
shareMeta.name = 'share-url';
|
Object.assign(document.head.appendChild(document.createElement('meta')), { name: 'share-url' });
|
||||||
document.head.appendChild(shareMeta);
|
shareMeta.content = actualShare;
|
||||||
}
|
|
||||||
shareMeta.content = share_url;
|
|
||||||
|
|
||||||
return { csrf_token, share_url };
|
return { csrf_token, share_url: actualShare };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,18 +94,14 @@ if (params.get('logout') === '1') {
|
|||||||
localStorage.removeItem("userTOTPEnabled");
|
localStorage.removeItem("userTOTPEnabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Wire up logoutBtn right away
|
export function triggerLogout() {
|
||||||
const logoutBtn = document.getElementById("logoutBtn");
|
fetch("/api/auth/logout.php", {
|
||||||
if (logoutBtn) {
|
method: "POST",
|
||||||
logoutBtn.addEventListener("click", () => {
|
credentials: "include",
|
||||||
fetch("/api/auth/logout.php", {
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
method: "POST",
|
})
|
||||||
credentials: "include",
|
.then(() => window.location.reload(true))
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
.catch(() => { });
|
||||||
})
|
|
||||||
.then(() => window.location.reload(true))
|
|
||||||
.catch(() => {});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -100,30 +135,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
// Continue with initializations that rely on a valid CSRF token:
|
// Continue with initializations that rely on a valid CSRF token:
|
||||||
checkAuthentication().then(authenticated => {
|
checkAuthentication().then(authenticated => {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
window.currentFolder = "root";
|
const overlay = document.getElementById('loadingOverlay');
|
||||||
initTagSearch();
|
if (overlay) overlay.remove();
|
||||||
loadFileList(window.currentFolder);
|
initializeApp();
|
||||||
initDragAndDrop();
|
|
||||||
loadSidebarOrder();
|
|
||||||
loadHeaderOrder();
|
|
||||||
initFileActions();
|
|
||||||
initUpload();
|
|
||||||
loadFolderTree();
|
|
||||||
setupTrashRestoreDelete();
|
|
||||||
loadAdminConfigFunc();
|
|
||||||
|
|
||||||
const helpBtn = document.getElementById("folderHelpBtn");
|
|
||||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
|
||||||
helpBtn.addEventListener("click", function () {
|
|
||||||
// Toggle display of the tooltip.
|
|
||||||
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
|
|
||||||
helpTooltip.style.display = "block";
|
|
||||||
} else {
|
|
||||||
helpTooltip.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn("User not authenticated. Data loading deferred.");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,7 +215,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Auto-scroll During Drag ---
|
// --- Auto-scroll During Drag ---
|
||||||
// Adjust these values as needed:
|
|
||||||
const SCROLL_THRESHOLD = 50; // pixels from edge to start scrolling
|
const SCROLL_THRESHOLD = 50; // pixels from edge to start scrolling
|
||||||
const SCROLL_SPEED = 20; // pixels to scroll per event
|
const SCROLL_SPEED = 20; // pixels to scroll per event
|
||||||
|
|
||||||
|
|||||||
@@ -79,15 +79,16 @@ export function setupTrashRestoreDelete() {
|
|||||||
body: JSON.stringify({ files })
|
body: JSON.stringify({ files })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(() => {
|
||||||
if (data.success) {
|
// Always report what we actually restored
|
||||||
showToast(data.success);
|
if (files.length === 1) {
|
||||||
toggleVisibility("restoreFilesModal", false);
|
showToast(`Restored file: ${files[0]}`);
|
||||||
loadFileList(window.currentFolder);
|
|
||||||
loadFolderTree(window.currentFolder);
|
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error);
|
showToast(`Restored files: ${files.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
toggleVisibility("restoreFilesModal", false);
|
||||||
|
loadFileList(window.currentFolder);
|
||||||
|
loadFolderTree(window.currentFolder);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error restoring files:", err);
|
console.error("Error restoring files:", err);
|
||||||
@@ -119,16 +120,15 @@ export function setupTrashRestoreDelete() {
|
|||||||
body: JSON.stringify({ files })
|
body: JSON.stringify({ files })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(() => {
|
||||||
if (data.success) {
|
if (files.length === 1) {
|
||||||
showToast(data.success);
|
showToast(`Restored file: ${files[0]}`);
|
||||||
toggleVisibility("restoreFilesModal", false);
|
|
||||||
loadFileList(window.currentFolder);
|
|
||||||
loadFolderTree(window.currentFolder);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error);
|
showToast(`Restored files: ${files.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
toggleVisibility("restoreFilesModal", false);
|
||||||
|
loadFileList(window.currentFolder);
|
||||||
|
loadFolderTree(window.currentFolder);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error restoring files:", err);
|
console.error("Error restoring files:", err);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 764 KiB |
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 412 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 403 KiB After Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 504 KiB |
BIN
resources/light-user-panel.png
Normal file
|
After Width: | Height: | Size: 426 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/adminController.php
|
// src/controllers/AdminController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||||||
@@ -54,12 +54,27 @@ class AdminController
|
|||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$config = AdminModel::getConfig();
|
$config = AdminModel::getConfig();
|
||||||
|
|
||||||
// If an error was encountered, send a 500 status.
|
|
||||||
if (isset($config['error'])) {
|
if (isset($config['error'])) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $config['error']]);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
echo json_encode($config);
|
|
||||||
|
// Build a safe subset for the front-end
|
||||||
|
$safe = [
|
||||||
|
'header_title' => $config['header_title'],
|
||||||
|
'loginOptions' => $config['loginOptions'],
|
||||||
|
'globalOtpauthUrl' => $config['globalOtpauthUrl'],
|
||||||
|
'enableWebDAV' => $config['enableWebDAV'],
|
||||||
|
'sharedMaxUploadSize' => $config['sharedMaxUploadSize'],
|
||||||
|
'oidc' => [
|
||||||
|
'providerUrl' => $config['oidc']['providerUrl'],
|
||||||
|
'redirectUri' => $config['oidc']['redirectUri'],
|
||||||
|
// clientSecret and clientId never exposed here
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode($safe);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,111 +137,106 @@ class AdminController
|
|||||||
* @return void Outputs a JSON response indicating success or failure.
|
* @return void Outputs a JSON response indicating success or failure.
|
||||||
*/
|
*/
|
||||||
public function updateConfig(): void
|
public function updateConfig(): void
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure the user is authenticated and is an admin.
|
// —– auth & CSRF checks —–
|
||||||
if (
|
if (
|
||||||
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||||
!isset($_SESSION['isAdmin']) || !$_SESSION['isAdmin']
|
!isset($_SESSION['isAdmin']) || !$_SESSION['isAdmin']
|
||||||
) {
|
) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['error' => 'Unauthorized access.']);
|
echo json_encode(['error' => 'Unauthorized access.']);
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate CSRF token.
|
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
|
||||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
|
||||||
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
|
||||||
http_response_code(403);
|
|
||||||
echo json_encode(['error' => 'Invalid CSRF token.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve and decode JSON input.
|
|
||||||
$input = file_get_contents('php://input');
|
|
||||||
$data = json_decode($input, true);
|
|
||||||
if (!is_array($data)) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['error' => 'Invalid input.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare existing settings
|
|
||||||
$headerTitle = isset($data['header_title']) ? trim($data['header_title']) : "";
|
|
||||||
$oidc = isset($data['oidc']) ? $data['oidc'] : [];
|
|
||||||
$oidcProviderUrl = isset($oidc['providerUrl']) ? filter_var($oidc['providerUrl'], FILTER_SANITIZE_URL) : '';
|
|
||||||
$oidcClientId = isset($oidc['clientId']) ? trim($oidc['clientId']) : '';
|
|
||||||
$oidcClientSecret = isset($oidc['clientSecret']) ? trim($oidc['clientSecret']) : '';
|
|
||||||
$oidcRedirectUri = isset($oidc['redirectUri']) ? filter_var($oidc['redirectUri'], FILTER_SANITIZE_URL) : '';
|
|
||||||
if (!$oidcProviderUrl || !$oidcClientId || !$oidcClientSecret || !$oidcRedirectUri) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['error' => 'Incomplete OIDC configuration.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$disableFormLogin = false;
|
|
||||||
if (isset($data['loginOptions']['disableFormLogin'])) {
|
|
||||||
$disableFormLogin = filter_var($data['loginOptions']['disableFormLogin'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
} elseif (isset($data['disableFormLogin'])) {
|
|
||||||
$disableFormLogin = filter_var($data['disableFormLogin'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
}
|
|
||||||
$disableBasicAuth = false;
|
|
||||||
if (isset($data['loginOptions']['disableBasicAuth'])) {
|
|
||||||
$disableBasicAuth = filter_var($data['loginOptions']['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
} elseif (isset($data['disableBasicAuth'])) {
|
|
||||||
$disableBasicAuth = filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
$disableOIDCLogin = false;
|
|
||||||
if (isset($data['loginOptions']['disableOIDCLogin'])) {
|
|
||||||
$disableOIDCLogin = filter_var($data['loginOptions']['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
} elseif (isset($data['disableOIDCLogin'])) {
|
|
||||||
$disableOIDCLogin = filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
}
|
|
||||||
$globalOtpauthUrl = isset($data['globalOtpauthUrl']) ? trim($data['globalOtpauthUrl']) : "";
|
|
||||||
|
|
||||||
// ── NEW: enableWebDAV flag ──────────────────────────────────────
|
|
||||||
$enableWebDAV = false;
|
|
||||||
if (array_key_exists('enableWebDAV', $data)) {
|
|
||||||
$enableWebDAV = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
} elseif (isset($data['features']['enableWebDAV'])) {
|
|
||||||
$enableWebDAV = filter_var($data['features']['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── NEW: sharedMaxUploadSize ──────────────────────────────────────
|
|
||||||
$sharedMaxUploadSize = null;
|
|
||||||
if (array_key_exists('sharedMaxUploadSize', $data)) {
|
|
||||||
$sharedMaxUploadSize = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
|
||||||
} elseif (isset($data['features']['sharedMaxUploadSize'])) {
|
|
||||||
$sharedMaxUploadSize = filter_var($data['features']['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
|
||||||
}
|
|
||||||
|
|
||||||
$configUpdate = [
|
|
||||||
'header_title' => $headerTitle,
|
|
||||||
'oidc' => [
|
|
||||||
'providerUrl' => $oidcProviderUrl,
|
|
||||||
'clientId' => $oidcClientId,
|
|
||||||
'clientSecret' => $oidcClientSecret,
|
|
||||||
'redirectUri' => $oidcRedirectUri,
|
|
||||||
],
|
|
||||||
'loginOptions' => [
|
|
||||||
'disableFormLogin' => $disableFormLogin,
|
|
||||||
'disableBasicAuth' => $disableBasicAuth,
|
|
||||||
'disableOIDCLogin' => $disableOIDCLogin,
|
|
||||||
],
|
|
||||||
'globalOtpauthUrl' => $globalOtpauthUrl,
|
|
||||||
'enableWebDAV' => $enableWebDAV,
|
|
||||||
'sharedMaxUploadSize' => $sharedMaxUploadSize // ← NEW
|
|
||||||
];
|
|
||||||
|
|
||||||
// Delegate to the model.
|
|
||||||
$result = AdminModel::updateConfig($configUpdate);
|
|
||||||
if (isset($result['error'])) {
|
|
||||||
http_response_code(500);
|
|
||||||
}
|
|
||||||
echo json_encode($result);
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|
$receivedToken = trim($headersArr['x-csrf-token'] ?? '');
|
||||||
|
if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Invalid CSRF token.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// —– fetch payload —–
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid input.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// —– load existing on-disk config —–
|
||||||
|
$existing = AdminModel::getConfig();
|
||||||
|
|
||||||
|
// —– start merge with existing as base —–
|
||||||
|
$merged = $existing;
|
||||||
|
|
||||||
|
// header_title
|
||||||
|
if (array_key_exists('header_title', $data)) {
|
||||||
|
$merged['header_title'] = trim($data['header_title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginOptions: inherit existing then override if provided
|
||||||
|
$merged['loginOptions'] = $existing['loginOptions'] ?? [
|
||||||
|
'disableFormLogin' => false,
|
||||||
|
'disableBasicAuth' => false,
|
||||||
|
'disableOIDCLogin'=> false,
|
||||||
|
'authBypass' => false,
|
||||||
|
'authHeaderName' => 'X-Remote-User'
|
||||||
|
];
|
||||||
|
foreach (['disableFormLogin','disableBasicAuth','disableOIDCLogin','authBypass'] as $flag) {
|
||||||
|
if (isset($data['loginOptions'][$flag])) {
|
||||||
|
$merged['loginOptions'][$flag] = filter_var(
|
||||||
|
$data['loginOptions'][$flag],
|
||||||
|
FILTER_VALIDATE_BOOLEAN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($data['loginOptions']['authHeaderName'])) {
|
||||||
|
$hdr = trim($data['loginOptions']['authHeaderName']);
|
||||||
|
if ($hdr !== '') {
|
||||||
|
$merged['loginOptions']['authHeaderName'] = $hdr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// globalOtpauthUrl
|
||||||
|
if (array_key_exists('globalOtpauthUrl', $data)) {
|
||||||
|
$merged['globalOtpauthUrl'] = trim($data['globalOtpauthUrl']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableWebDAV
|
||||||
|
if (array_key_exists('enableWebDAV', $data)) {
|
||||||
|
$merged['enableWebDAV'] = filter_var($data['enableWebDAV'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sharedMaxUploadSize
|
||||||
|
if (array_key_exists('sharedMaxUploadSize', $data)) {
|
||||||
|
$sms = filter_var($data['sharedMaxUploadSize'], FILTER_VALIDATE_INT);
|
||||||
|
if ($sms !== false) {
|
||||||
|
$merged['sharedMaxUploadSize'] = $sms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// oidc: only overwrite non-empty inputs
|
||||||
|
$merged['oidc'] = $existing['oidc'] ?? [
|
||||||
|
'providerUrl'=>'','clientId'=>'','clientSecret'=>'','redirectUri'=>''
|
||||||
|
];
|
||||||
|
foreach (['providerUrl','clientId','clientSecret','redirectUri'] as $f) {
|
||||||
|
if (!empty($data['oidc'][$f])) {
|
||||||
|
$val = trim($data['oidc'][$f]);
|
||||||
|
if ($f === 'providerUrl' || $f === 'redirectUri') {
|
||||||
|
$val = filter_var($val, FILTER_SANITIZE_URL);
|
||||||
|
}
|
||||||
|
$merged['oidc'][$f] = $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// —– persist merged config —–
|
||||||
|
$result = AdminModel::updateConfig($merged);
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
|
echo json_encode($result);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/authController.php
|
// src/controllers/AuthController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
require_once PROJECT_ROOT . '/src/models/AuthModel.php';
|
||||||
@@ -111,6 +111,8 @@ class AuthController
|
|||||||
$cfg['oidc']['clientSecret']
|
$cfg['oidc']['clientSecret']
|
||||||
);
|
);
|
||||||
$oidc->setRedirectURL($cfg['oidc']['redirectUri']);
|
$oidc->setRedirectURL($cfg['oidc']['redirectUri']);
|
||||||
|
$oidc->addScope(['openid','profile','email']);
|
||||||
|
|
||||||
|
|
||||||
if ($oidcAction === 'callback') {
|
if ($oidcAction === 'callback') {
|
||||||
try {
|
try {
|
||||||
@@ -342,48 +344,48 @@ class AuthController
|
|||||||
public function checkAuth(): void
|
public function checkAuth(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
// 1) Remember-me re-login
|
// 1) Remember-me re-login
|
||||||
if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) {
|
if (empty($_SESSION['authenticated']) && !empty($_COOKIE['remember_me_token'])) {
|
||||||
$payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']);
|
$payload = AuthModel::validateRememberToken($_COOKIE['remember_me_token']);
|
||||||
if ($payload) {
|
if ($payload) {
|
||||||
$old = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32));
|
$old = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32));
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['csrf_token'] = $old;
|
$_SESSION['csrf_token'] = $old;
|
||||||
$_SESSION['authenticated'] = true;
|
$_SESSION['authenticated'] = true;
|
||||||
$_SESSION['username'] = $payload['username'];
|
$_SESSION['username'] = $payload['username'];
|
||||||
$_SESSION['isAdmin'] = !empty($payload['isAdmin']);
|
$_SESSION['isAdmin'] = !empty($payload['isAdmin']);
|
||||||
$_SESSION['folderOnly'] = $payload['folderOnly'] ?? false;
|
$_SESSION['folderOnly'] = $payload['folderOnly'] ?? false;
|
||||||
$_SESSION['readOnly'] = $payload['readOnly'] ?? false;
|
$_SESSION['readOnly'] = $payload['readOnly'] ?? false;
|
||||||
$_SESSION['disableUpload'] = $payload['disableUpload'] ?? false;
|
$_SESSION['disableUpload'] = $payload['disableUpload'] ?? false;
|
||||||
// regenerate CSRF if you use one
|
// regenerate CSRF if you use one
|
||||||
|
|
||||||
|
|
||||||
// TOTP enabled? (same logic as below)
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
// TOTP enabled? (same logic as below)
|
||||||
$totp = false;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (file_exists($usersFile)) {
|
$totp = false;
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
if (file_exists($usersFile)) {
|
||||||
$parts = explode(':', trim($line));
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
$parts = explode(':', trim($line));
|
||||||
$totp = true;
|
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
||||||
break;
|
$totp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'authenticated' => true,
|
'authenticated' => true,
|
||||||
'csrf_token' => $_SESSION['csrf_token'],
|
'csrf_token' => $_SESSION['csrf_token'],
|
||||||
'isAdmin' => $_SESSION['isAdmin'],
|
'isAdmin' => $_SESSION['isAdmin'],
|
||||||
'totp_enabled' => $totp,
|
'totp_enabled' => $totp,
|
||||||
'username' => $_SESSION['username'],
|
'username' => $_SESSION['username'],
|
||||||
'folderOnly' => $_SESSION['folderOnly'],
|
'folderOnly' => $_SESSION['folderOnly'],
|
||||||
'readOnly' => $_SESSION['readOnly'],
|
'readOnly' => $_SESSION['readOnly'],
|
||||||
'disableUpload' => $_SESSION['disableUpload']
|
'disableUpload' => $_SESSION['disableUpload']
|
||||||
]);
|
]);
|
||||||
exit();
|
exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
@@ -453,11 +455,11 @@ class AuthController
|
|||||||
if (empty($_SESSION['csrf_token'])) {
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Emit headers
|
// 2) Emit headers
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||||||
|
|
||||||
// 3) Return JSON payload
|
// 3) Return JSON payload
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'csrf_token' => $_SESSION['csrf_token'],
|
'csrf_token' => $_SESSION['csrf_token'],
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/fileController.php
|
// src/controllers/FileController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/FileModel.php';
|
require_once PROJECT_ROOT . '/src/models/FileModel.php';
|
||||||
@@ -1571,4 +1571,86 @@ class FileController
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/file/getShareLinks.php
|
||||||
|
*/
|
||||||
|
public function getShareLinks()
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$shareFile = FileModel::getAllShareLinks();
|
||||||
|
echo json_encode($shareFile, JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllShareLinks(): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$shareFile = META_DIR . 'share_links.json';
|
||||||
|
$links = file_exists($shareFile)
|
||||||
|
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||||
|
: [];
|
||||||
|
$now = time();
|
||||||
|
$cleaned = [];
|
||||||
|
|
||||||
|
// remove expired
|
||||||
|
foreach ($links as $token => $record) {
|
||||||
|
if (!empty($record['expires']) && $record['expires'] < $now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$cleaned[$token] = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($cleaned) !== count($links)) {
|
||||||
|
file_put_contents($shareFile, json_encode($cleaned, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($cleaned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/file/deleteShareLink.php
|
||||||
|
*/
|
||||||
|
public function deleteShareLink()
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$token = $_POST['token'] ?? '';
|
||||||
|
if (!$token) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No token provided']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = FileModel::deleteShareLink($token);
|
||||||
|
if ($deleted) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Not found']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/file/createFile.php
|
||||||
|
*/
|
||||||
|
public function createFile(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
// Check user permissions (assuming loadUserPermissions() is available).
|
||||||
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
|
if (!empty($userPermissions['readOnly'])) {
|
||||||
|
echo json_encode(["error" => "Read-only users are not allowed to create files."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$folder = $body['folder'] ?? 'root';
|
||||||
|
$filename = $body['name'] ?? '';
|
||||||
|
|
||||||
|
$result = FileModel::createFile($folder, $filename, $_SESSION['username'] ?? 'Unknown');
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
http_response_code($result['code'] ?? 400);
|
||||||
|
echo json_encode(['success'=>false,'error'=>$result['error']]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success'=>true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/folderController.php
|
// src/controllers/FolderController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/FolderModel.php';
|
require_once PROJECT_ROOT . '/src/models/FolderModel.php';
|
||||||
@@ -76,7 +76,11 @@ class FolderController
|
|||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
echo json_encode(["error" => "Read-only users are not allowed to create folders."]);
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
"success" => false,
|
||||||
|
"error" => "Read-only users are not allowed to create folders."
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,16 +340,14 @@ class FolderController
|
|||||||
public function getFolderList(): void
|
public function getFolderList(): void
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
if (empty($_SESSION['authenticated'])) {
|
||||||
// Ensure user is authenticated.
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally, you might add further input validation if necessary.
|
$parent = $_GET['folder'] ?? null;
|
||||||
$folderList = FolderModel::getFolderList();
|
$folderList = FolderModel::getFolderList($parent);
|
||||||
echo json_encode($folderList);
|
echo json_encode($folderList);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -1074,4 +1076,53 @@ class FolderController
|
|||||||
header("Location: " . $redirectUrl);
|
header("Location: " . $redirectUrl);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/folder/getShareFolderLinks.php
|
||||||
|
*/
|
||||||
|
public function getAllShareFolderLinks(): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$shareFile = META_DIR . 'share_folder_links.json';
|
||||||
|
$links = file_exists($shareFile)
|
||||||
|
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||||
|
: [];
|
||||||
|
$now = time();
|
||||||
|
$cleaned = [];
|
||||||
|
|
||||||
|
// 1) Remove expired
|
||||||
|
foreach ($links as $token => $record) {
|
||||||
|
if (!empty($record['expires']) && $record['expires'] < $now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$cleaned[$token] = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Persist back if anything was pruned
|
||||||
|
if (count($cleaned) !== count($links)) {
|
||||||
|
file_put_contents($shareFile, json_encode($cleaned, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($cleaned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/folder/deleteShareFolderLink.php
|
||||||
|
*/
|
||||||
|
public function deleteShareFolderLink()
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$token = $_POST['token'] ?? '';
|
||||||
|
if (!$token) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No token provided']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = FolderModel::deleteShareFolderLink($token);
|
||||||
|
if ($deleted) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Not found']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// src/controllers/uploadController.php
|
// src/controllers/UploadController.php
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
require_once PROJECT_ROOT . '/src/models/UploadModel.php';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// userController.php located in src/controllers/
|
// UserController.php located in src/controllers/
|
||||||
|
|
||||||
require_once __DIR__ . '/../../config/config.php';
|
require_once __DIR__ . '/../../config/config.php';
|
||||||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||||||
@@ -867,123 +867,126 @@ class UserController
|
|||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function verifyTOTP()
|
public function verifyTOTP()
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||||||
|
|
||||||
// Rate-limit
|
// Rate-limit
|
||||||
if (!isset($_SESSION['totp_failures'])) {
|
if (!isset($_SESSION['totp_failures'])) {
|
||||||
$_SESSION['totp_failures'] = 0;
|
$_SESSION['totp_failures'] = 0;
|
||||||
}
|
}
|
||||||
if ($_SESSION['totp_failures'] >= 5) {
|
if ($_SESSION['totp_failures'] >= 5) {
|
||||||
http_response_code(429);
|
http_response_code(429);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
|
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be authenticated OR pending login
|
// Must be authenticated OR pending login
|
||||||
if (empty($_SESSION['authenticated']) && !isset($_SESSION['pending_login_user'])) {
|
if (empty($_SESSION['authenticated']) && !isset($_SESSION['pending_login_user'])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRF check
|
// CSRF check
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
|
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
|
||||||
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
|
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse & validate input
|
// Parse & validate input
|
||||||
$inputData = json_decode(file_get_contents("php://input"), true);
|
$inputData = json_decode(file_get_contents("php://input"), true);
|
||||||
$code = trim($inputData['totp_code'] ?? '');
|
$code = trim($inputData['totp_code'] ?? '');
|
||||||
if (!preg_match('/^\d{6}$/', $code)) {
|
if (!preg_match('/^\d{6}$/', $code)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
|
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TFA helper
|
// TFA helper
|
||||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||||
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
||||||
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
|
'FileRise',
|
||||||
);
|
6,
|
||||||
|
30,
|
||||||
// === Pending-login flow (we just came from auth and need to finish login) ===
|
\RobThree\Auth\Algorithm::Sha1
|
||||||
if (isset($_SESSION['pending_login_user'])) {
|
);
|
||||||
$username = $_SESSION['pending_login_user'];
|
|
||||||
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
// === Pending-login flow (we just came from auth and need to finish login) ===
|
||||||
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
if (isset($_SESSION['pending_login_user'])) {
|
||||||
|
$username = $_SESSION['pending_login_user'];
|
||||||
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
|
||||||
$_SESSION['totp_failures']++;
|
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
|
||||||
exit;
|
$_SESSION['totp_failures']++;
|
||||||
}
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||||||
// Issue “remember me” token if requested
|
exit;
|
||||||
if ($rememberMe) {
|
}
|
||||||
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
|
||||||
$token = bin2hex(random_bytes(32));
|
// Issue “remember me” token if requested
|
||||||
$expiry = time() + 30 * 24 * 60 * 60;
|
if ($rememberMe) {
|
||||||
$all = [];
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
if (file_exists($tokFile)) {
|
$token = bin2hex(random_bytes(32));
|
||||||
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
$all = json_decode($dec, true) ?: [];
|
$all = [];
|
||||||
}
|
if (file_exists($tokFile)) {
|
||||||
$all[$token] = [
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
'username' => $username,
|
$all = json_decode($dec, true) ?: [];
|
||||||
'expiry' => $expiry,
|
}
|
||||||
'isAdmin' => ((int)userModel::getUserRole($username) === 1),
|
$all[$token] = [
|
||||||
'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
|
'username' => $username,
|
||||||
'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
|
'expiry' => $expiry,
|
||||||
'disableUpload'=> loadUserPermissions($username)['disableUpload']?? false
|
'isAdmin' => ((int)userModel::getUserRole($username) === 1),
|
||||||
];
|
'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
|
||||||
file_put_contents(
|
'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
|
||||||
$tokFile,
|
'disableUpload' => loadUserPermissions($username)['disableUpload'] ?? false
|
||||||
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
];
|
||||||
LOCK_EX
|
file_put_contents(
|
||||||
);
|
$tokFile,
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
LOCK_EX
|
||||||
setcookie(session_name(), session_id(), $expiry, '/', '', $secure, true);
|
);
|
||||||
}
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
// === Finalize login into session exactly as finalizeLogin() would ===
|
setcookie(session_name(), session_id(), $expiry, '/', '', $secure, true);
|
||||||
session_regenerate_id(true);
|
}
|
||||||
$_SESSION['authenticated'] = true;
|
|
||||||
$_SESSION['username'] = $username;
|
// === Finalize login into session exactly as finalizeLogin() would ===
|
||||||
$_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
|
session_regenerate_id(true);
|
||||||
$perms = loadUserPermissions($username);
|
$_SESSION['authenticated'] = true;
|
||||||
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
$_SESSION['username'] = $username;
|
||||||
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
$_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
|
||||||
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
$perms = loadUserPermissions($username);
|
||||||
|
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||||
// Clean up pending markers
|
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||||
unset(
|
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||||
$_SESSION['pending_login_user'],
|
|
||||||
$_SESSION['pending_login_secret'],
|
// Clean up pending markers
|
||||||
$_SESSION['pending_login_remember_me'],
|
unset(
|
||||||
$_SESSION['totp_failures']
|
$_SESSION['pending_login_user'],
|
||||||
);
|
$_SESSION['pending_login_secret'],
|
||||||
|
$_SESSION['pending_login_remember_me'],
|
||||||
// Send back full login payload
|
$_SESSION['totp_failures']
|
||||||
echo json_encode([
|
);
|
||||||
'status' => 'ok',
|
|
||||||
'success' => 'Login successful',
|
// Send back full login payload
|
||||||
'isAdmin' => $_SESSION['isAdmin'],
|
echo json_encode([
|
||||||
'folderOnly' => $_SESSION['folderOnly'],
|
'status' => 'ok',
|
||||||
'readOnly' => $_SESSION['readOnly'],
|
'success' => 'Login successful',
|
||||||
'disableUpload' => $_SESSION['disableUpload'],
|
'isAdmin' => $_SESSION['isAdmin'],
|
||||||
'username' => $_SESSION['username']
|
'folderOnly' => $_SESSION['folderOnly'],
|
||||||
]);
|
'readOnly' => $_SESSION['readOnly'],
|
||||||
exit;
|
'disableUpload' => $_SESSION['disableUpload'],
|
||||||
}
|
'username' => $_SESSION['username']
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup/verification flow (not pending)
|
// Setup/verification flow (not pending)
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
@@ -1011,4 +1014,91 @@ class UserController
|
|||||||
unset($_SESSION['totp_failures']);
|
unset($_SESSION['totp_failures']);
|
||||||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function uploadPicture()
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// 1) Auth check
|
||||||
|
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) CSRF check
|
||||||
|
$headers = function_exists('getallheaders')
|
||||||
|
? array_change_key_case(getallheaders(), CASE_LOWER)
|
||||||
|
: [];
|
||||||
|
$csrf = $headers['x-csrf-token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
if (empty($_SESSION['csrf_token']) || $csrf !== $_SESSION['csrf_token']) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) File presence
|
||||||
|
if (empty($_FILES['profile_picture']) || $_FILES['profile_picture']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No file uploaded or error']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$file = $_FILES['profile_picture'];
|
||||||
|
|
||||||
|
// 4) Validate MIME & size
|
||||||
|
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'];
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
if (!isset($allowed[$mime])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid file type']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($file['size'] > 2 * 1024 * 1024) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'File too large']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Destination under public/uploads/profile_pics
|
||||||
|
$uploadDir = UPLOAD_DIR . '/profile_pics';
|
||||||
|
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Cannot create upload folder']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Move file
|
||||||
|
$ext = $allowed[$mime];
|
||||||
|
$user = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_SESSION['username']);
|
||||||
|
$filename = $user . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
|
||||||
|
$dest = "$uploadDir/$filename";
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Failed to save file']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) Build public URL
|
||||||
|
$url = '/uploads/profile_pics/' . $filename;
|
||||||
|
|
||||||
|
// ─── THIS IS WHERE WE PERSIST INTO users.txt ───
|
||||||
|
$result = UserModel::setProfilePicture($_SESSION['username'], $url);
|
||||||
|
if (!$result['success']) {
|
||||||
|
// on failure, remove the file we just wrote
|
||||||
|
@unlink($dest);
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Failed to save profile picture setting'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 8) Return success
|
||||||
|
echo json_encode(['success' => true, 'url' => $url]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,10 +16,14 @@ class AdminModel
|
|||||||
$unit = strtolower(substr($val, -1));
|
$unit = strtolower(substr($val, -1));
|
||||||
$num = (int) rtrim($val, 'bkmgtpezyBKMGTPESY');
|
$num = (int) rtrim($val, 'bkmgtpezyBKMGTPESY');
|
||||||
switch ($unit) {
|
switch ($unit) {
|
||||||
case 'g': return $num * 1024 ** 3;
|
case 'g':
|
||||||
case 'm': return $num * 1024 ** 2;
|
return $num * 1024 ** 3;
|
||||||
case 'k': return $num * 1024;
|
case 'm':
|
||||||
default: return $num;
|
return $num * 1024 ** 2;
|
||||||
|
case 'k':
|
||||||
|
return $num * 1024;
|
||||||
|
default:
|
||||||
|
return $num;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +67,24 @@ class AdminModel
|
|||||||
$configUpdate['sharedMaxUploadSize'] = $sms;
|
$configUpdate['sharedMaxUploadSize'] = $sms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── NEW: normalize authBypass & authHeaderName ─────────────────────────
|
||||||
|
if (!isset($configUpdate['loginOptions']['authBypass'])) {
|
||||||
|
$configUpdate['loginOptions']['authBypass'] = false;
|
||||||
|
}
|
||||||
|
$configUpdate['loginOptions']['authBypass'] = (bool)$configUpdate['loginOptions']['authBypass'];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isset($configUpdate['loginOptions']['authHeaderName'])
|
||||||
|
|| !is_string($configUpdate['loginOptions']['authHeaderName'])
|
||||||
|
|| trim($configUpdate['loginOptions']['authHeaderName']) === ''
|
||||||
|
) {
|
||||||
|
$configUpdate['loginOptions']['authHeaderName'] = 'X-Remote-User';
|
||||||
|
} else {
|
||||||
|
$configUpdate['loginOptions']['authHeaderName'] =
|
||||||
|
trim($configUpdate['loginOptions']['authHeaderName']);
|
||||||
|
}
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Convert configuration to JSON.
|
// Convert configuration to JSON.
|
||||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
if ($plainTextConfig === false) {
|
if ($plainTextConfig === false) {
|
||||||
@@ -128,6 +150,19 @@ class AdminModel
|
|||||||
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
$config['loginOptions']['disableOIDCLogin'] = (bool)$config['loginOptions']['disableOIDCLogin'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('authBypass', $config['loginOptions'])) {
|
||||||
|
$config['loginOptions']['authBypass'] = false;
|
||||||
|
} else {
|
||||||
|
$config['loginOptions']['authBypass'] = (bool)$config['loginOptions']['authBypass'];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!array_key_exists('authHeaderName', $config['loginOptions'])
|
||||||
|
|| !is_string($config['loginOptions']['authHeaderName'])
|
||||||
|
|| trim($config['loginOptions']['authHeaderName']) === ''
|
||||||
|
) {
|
||||||
|
$config['loginOptions']['authHeaderName'] = 'X-Remote-User';
|
||||||
|
}
|
||||||
|
|
||||||
// Default values for other keys
|
// Default values for other keys
|
||||||
if (!isset($config['globalOtpauthUrl'])) {
|
if (!isset($config['globalOtpauthUrl'])) {
|
||||||
$config['globalOtpauthUrl'] = "";
|
$config['globalOtpauthUrl'] = "";
|
||||||
@@ -151,8 +186,8 @@ class AdminModel
|
|||||||
'header_title' => "FileRise",
|
'header_title' => "FileRise",
|
||||||
'oidc' => [
|
'oidc' => [
|
||||||
'providerUrl' => 'https://your-oidc-provider.com',
|
'providerUrl' => 'https://your-oidc-provider.com',
|
||||||
'clientId' => 'YOUR_CLIENT_ID',
|
'clientId' => '',
|
||||||
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
'clientSecret' => '',
|
||||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback'
|
||||||
],
|
],
|
||||||
'loginOptions' => [
|
'loginOptions' => [
|
||||||
@@ -166,4 +201,4 @@ class AdminModel
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1253,4 +1253,89 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
|||||||
|
|
||||||
return ["files" => $fileList, "globalTags" => $globalTags];
|
return ["files" => $fileList, "globalTags" => $globalTags];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getAllShareLinks(): array
|
||||||
|
{
|
||||||
|
$shareFile = META_DIR . "share_links.json";
|
||||||
|
if (!file_exists($shareFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$links = json_decode(file_get_contents($shareFile), true);
|
||||||
|
return is_array($links) ? $links : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteShareLink(string $token): bool
|
||||||
|
{
|
||||||
|
$shareFile = META_DIR . "share_links.json";
|
||||||
|
if (!file_exists($shareFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$links = json_decode(file_get_contents($shareFile), true);
|
||||||
|
if (!is_array($links) || !isset($links[$token])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unset($links[$token]);
|
||||||
|
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an empty file plus metadata entry.
|
||||||
|
*
|
||||||
|
* @param string $folder
|
||||||
|
* @param string $filename
|
||||||
|
* @param string $uploader
|
||||||
|
* @return array ['success'=>bool, 'error'=>string, 'code'=>int]
|
||||||
|
*/
|
||||||
|
public static function createFile(string $folder, string $filename, string $uploader): array
|
||||||
|
{
|
||||||
|
// 1) basic validation
|
||||||
|
if (!preg_match('/^[\w\-. ]+$/', $filename)) {
|
||||||
|
return ['success'=>false,'error'=>'Invalid filename','code'=>400];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) build target path
|
||||||
|
$base = UPLOAD_DIR;
|
||||||
|
if ($folder !== 'root') {
|
||||||
|
$base = rtrim(UPLOAD_DIR, '/\\')
|
||||||
|
. DIRECTORY_SEPARATOR . $folder
|
||||||
|
. DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
if (!is_dir($base) && !mkdir($base, 0775, true)) {
|
||||||
|
return ['success'=>false,'error'=>'Cannot create folder','code'=>500];
|
||||||
|
}
|
||||||
|
$path = $base . $filename;
|
||||||
|
|
||||||
|
// 3) no overwrite
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return ['success'=>false,'error'=>'File already exists','code'=>400];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) touch the file
|
||||||
|
if (false === @file_put_contents($path, '')) {
|
||||||
|
return ['success'=>false,'error'=>'Could not create file','code'=>500];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) write metadata
|
||||||
|
$metaKey = ($folder === 'root') ? 'root' : $folder;
|
||||||
|
$metaName = str_replace(['/', '\\', ' '], '-', $metaKey) . '_metadata.json';
|
||||||
|
$metaPath = META_DIR . $metaName;
|
||||||
|
|
||||||
|
$collection = [];
|
||||||
|
if (file_exists($metaPath)) {
|
||||||
|
$json = file_get_contents($metaPath);
|
||||||
|
$collection = json_decode($json, true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$collection[$filename] = [
|
||||||
|
'uploaded' => date(DATE_TIME_FORMAT),
|
||||||
|
'uploader' => $uploader
|
||||||
|
];
|
||||||
|
|
||||||
|
if (false === file_put_contents($metaPath, json_encode($collection, JSON_PRETTY_PRINT))) {
|
||||||
|
return ['success'=>false,'error'=>'Failed to update metadata','code'=>500];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success'=>true];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -570,4 +570,29 @@ class FolderModel
|
|||||||
|
|
||||||
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getAllShareFolderLinks(): array
|
||||||
|
{
|
||||||
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
|
if (!file_exists($shareFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$links = json_decode(file_get_contents($shareFile), true);
|
||||||
|
return is_array($links) ? $links : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteShareFolderLink(string $token): bool
|
||||||
|
{
|
||||||
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
|
if (!file_exists($shareFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$links = json_decode(file_get_contents($shareFile), true);
|
||||||
|
if (!is_array($links) || !isset($links[$token])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unset($links[$token]);
|
||||||
|
file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
|
|
||||||
require_once PROJECT_ROOT . '/config/config.php';
|
require_once PROJECT_ROOT . '/config/config.php';
|
||||||
|
|
||||||
class userModel {
|
class userModel
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Retrieves all users from the users file.
|
* Retrieves all users from the users file.
|
||||||
*
|
*
|
||||||
* @return array Returns an array of users.
|
* @return array Returns an array of users.
|
||||||
*/
|
*/
|
||||||
public static function getAllUsers() {
|
public static function getAllUsers()
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
$users = [];
|
$users = [];
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
@@ -26,7 +28,7 @@ class userModel {
|
|||||||
}
|
}
|
||||||
return $users;
|
return $users;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new user.
|
* Adds a new user.
|
||||||
*
|
*
|
||||||
@@ -36,14 +38,15 @@ class userModel {
|
|||||||
* @param bool $setupMode If true, overwrite the users file.
|
* @param bool $setupMode If true, overwrite the users file.
|
||||||
* @return array Response containing either an error or a success message.
|
* @return array Response containing either an error or a success message.
|
||||||
*/
|
*/
|
||||||
public static function addUser($username, $password, $isAdmin, $setupMode) {
|
public static function addUser($username, $password, $isAdmin, $setupMode)
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
// Ensure users.txt exists.
|
// Ensure users.txt exists.
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
file_put_contents($usersFile, '');
|
file_put_contents($usersFile, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if username already exists.
|
// Check if username already exists.
|
||||||
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
foreach ($existingUsers as $line) {
|
foreach ($existingUsers as $line) {
|
||||||
@@ -52,40 +55,41 @@ class userModel {
|
|||||||
return ["error" => "User already exists"];
|
return ["error" => "User already exists"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the password.
|
// Hash the password.
|
||||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
// Prepare the new line.
|
// Prepare the new line.
|
||||||
$newUserLine = $username . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
|
$newUserLine = $username . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
|
||||||
|
|
||||||
// If setup mode, overwrite the file; otherwise, append.
|
// If setup mode, overwrite the file; otherwise, append.
|
||||||
if ($setupMode) {
|
if ($setupMode) {
|
||||||
file_put_contents($usersFile, $newUserLine);
|
file_put_contents($usersFile, $newUserLine);
|
||||||
} else {
|
} else {
|
||||||
file_put_contents($usersFile, $newUserLine, FILE_APPEND);
|
file_put_contents($usersFile, $newUserLine, FILE_APPEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ["success" => "User added successfully"];
|
return ["success" => "User added successfully"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the specified user from the users file and updates the userPermissions file.
|
* Removes the specified user from the users file and updates the userPermissions file.
|
||||||
*
|
*
|
||||||
* @param string $usernameToRemove The username to remove.
|
* @param string $usernameToRemove The username to remove.
|
||||||
* @return array An array with either an error message or a success message.
|
* @return array An array with either an error message or a success message.
|
||||||
*/
|
*/
|
||||||
public static function removeUser($usernameToRemove) {
|
public static function removeUser($usernameToRemove)
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return ["error" => "Users file not found"];
|
return ["error" => "Users file not found"];
|
||||||
}
|
}
|
||||||
|
|
||||||
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
$newUsers = [];
|
$newUsers = [];
|
||||||
$userFound = false;
|
$userFound = false;
|
||||||
|
|
||||||
// Loop through users; skip (remove) the specified user.
|
// Loop through users; skip (remove) the specified user.
|
||||||
foreach ($existingUsers as $line) {
|
foreach ($existingUsers as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
@@ -98,14 +102,14 @@ class userModel {
|
|||||||
}
|
}
|
||||||
$newUsers[] = $line;
|
$newUsers[] = $line;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$userFound) {
|
if (!$userFound) {
|
||||||
return ["error" => "User not found"];
|
return ["error" => "User not found"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the updated user list back to the file.
|
// Write the updated user list back to the file.
|
||||||
file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL);
|
file_put_contents($usersFile, implode(PHP_EOL, $newUsers) . PHP_EOL);
|
||||||
|
|
||||||
// Update the userPermissions.json file.
|
// Update the userPermissions.json file.
|
||||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||||
if (file_exists($permissionsFile)) {
|
if (file_exists($permissionsFile)) {
|
||||||
@@ -116,18 +120,19 @@ class userModel {
|
|||||||
file_put_contents($permissionsFile, json_encode($permissionsArray, JSON_PRETTY_PRINT));
|
file_put_contents($permissionsFile, json_encode($permissionsArray, JSON_PRETTY_PRINT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ["success" => "User removed successfully"];
|
return ["success" => "User removed successfully"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves permissions from the userPermissions.json file.
|
* Retrieves permissions from the userPermissions.json file.
|
||||||
* If the current user is an admin, returns all permissions.
|
* If the current user is an admin, returns all permissions.
|
||||||
* Otherwise, returns only the permissions for the current user.
|
* Otherwise, returns only the permissions for the current user.
|
||||||
*
|
*
|
||||||
* @return array|object Returns an associative array of permissions or an empty object if none are found.
|
* @return array|object Returns an associative array of permissions or an empty object if none are found.
|
||||||
*/
|
*/
|
||||||
public static function getUserPermissions() {
|
public static function getUserPermissions()
|
||||||
|
{
|
||||||
global $encryptionKey;
|
global $encryptionKey;
|
||||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||||
$permissionsArray = [];
|
$permissionsArray = [];
|
||||||
@@ -165,13 +170,14 @@ class userModel {
|
|||||||
return new stdClass();
|
return new stdClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates user permissions in the userPermissions.json file.
|
* Updates user permissions in the userPermissions.json file.
|
||||||
*
|
*
|
||||||
* @param array $permissions An array of permission updates.
|
* @param array $permissions An array of permission updates.
|
||||||
* @return array An associative array with a success or error message.
|
* @return array An associative array with a success or error message.
|
||||||
*/
|
*/
|
||||||
public static function updateUserPermissions($permissions) {
|
public static function updateUserPermissions($permissions)
|
||||||
|
{
|
||||||
global $encryptionKey;
|
global $encryptionKey;
|
||||||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||||||
$existingPermissions = [];
|
$existingPermissions = [];
|
||||||
@@ -185,7 +191,7 @@ class userModel {
|
|||||||
$existingPermissions = [];
|
$existingPermissions = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load user roles from the users file.
|
// Load user roles from the users file.
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
$userRoles = [];
|
$userRoles = [];
|
||||||
@@ -199,7 +205,7 @@ class userModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each permission update.
|
// Process each permission update.
|
||||||
foreach ($permissions as $perm) {
|
foreach ($permissions as $perm) {
|
||||||
if (!isset($perm['username'])) {
|
if (!isset($perm['username'])) {
|
||||||
@@ -208,12 +214,12 @@ class userModel {
|
|||||||
$username = $perm['username'];
|
$username = $perm['username'];
|
||||||
// Look up the user's role.
|
// Look up the user's role.
|
||||||
$role = isset($userRoles[strtolower($username)]) ? $userRoles[strtolower($username)] : null;
|
$role = isset($userRoles[strtolower($username)]) ? $userRoles[strtolower($username)] : null;
|
||||||
|
|
||||||
// Skip updating permissions for admin users.
|
// Skip updating permissions for admin users.
|
||||||
if ($role === "1") {
|
if ($role === "1") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update permissions: default any missing value to false.
|
// Update permissions: default any missing value to false.
|
||||||
$existingPermissions[strtolower($username)] = [
|
$existingPermissions[strtolower($username)] = [
|
||||||
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
|
'folderOnly' => isset($perm['folderOnly']) ? (bool)$perm['folderOnly'] : false,
|
||||||
@@ -221,7 +227,7 @@ class userModel {
|
|||||||
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false
|
'disableUpload' => isset($perm['disableUpload']) ? (bool)$perm['disableUpload'] : false
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the updated permissions array to JSON.
|
// Convert the updated permissions array to JSON.
|
||||||
$plainText = json_encode($existingPermissions, JSON_PRETTY_PRINT);
|
$plainText = json_encode($existingPermissions, JSON_PRETTY_PRINT);
|
||||||
// Encrypt the JSON.
|
// Encrypt the JSON.
|
||||||
@@ -231,11 +237,11 @@ class userModel {
|
|||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
return ["error" => "Failed to save user permissions."];
|
return ["error" => "Failed to save user permissions."];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ["success" => "User permissions updated successfully."];
|
return ["success" => "User permissions updated successfully."];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the password for the given user.
|
* Changes the password for the given user.
|
||||||
*
|
*
|
||||||
* @param string $username The username whose password is to be changed.
|
* @param string $username The username whose password is to be changed.
|
||||||
@@ -243,17 +249,18 @@ class userModel {
|
|||||||
* @param string $newPassword The new password.
|
* @param string $newPassword The new password.
|
||||||
* @return array An array with either a success or error message.
|
* @return array An array with either a success or error message.
|
||||||
*/
|
*/
|
||||||
public static function changePassword($username, $oldPassword, $newPassword) {
|
public static function changePassword($username, $oldPassword, $newPassword)
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return ["error" => "Users file not found"];
|
return ["error" => "Users file not found"];
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
$userFound = false;
|
$userFound = false;
|
||||||
$newLines = [];
|
$newLines = [];
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
// Expect at least 3 parts: username, hashed password, and role.
|
// Expect at least 3 parts: username, hashed password, and role.
|
||||||
@@ -266,7 +273,7 @@ class userModel {
|
|||||||
$storedRole = $parts[2];
|
$storedRole = $parts[2];
|
||||||
// Preserve TOTP secret if it exists.
|
// Preserve TOTP secret if it exists.
|
||||||
$totpSecret = (count($parts) >= 4) ? $parts[3] : "";
|
$totpSecret = (count($parts) >= 4) ? $parts[3] : "";
|
||||||
|
|
||||||
if ($storedUser === $username) {
|
if ($storedUser === $username) {
|
||||||
$userFound = true;
|
$userFound = true;
|
||||||
// Verify the old password.
|
// Verify the old password.
|
||||||
@@ -275,7 +282,7 @@ class userModel {
|
|||||||
}
|
}
|
||||||
// Hash the new password.
|
// Hash the new password.
|
||||||
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
$newHashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
// Rebuild the line, preserving TOTP secret if it exists.
|
// Rebuild the line, preserving TOTP secret if it exists.
|
||||||
if ($totpSecret !== "") {
|
if ($totpSecret !== "") {
|
||||||
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret;
|
$newLines[] = $username . ":" . $newHashedPassword . ":" . $storedRole . ":" . $totpSecret;
|
||||||
@@ -286,11 +293,11 @@ class userModel {
|
|||||||
$newLines[] = $line;
|
$newLines[] = $line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$userFound) {
|
if (!$userFound) {
|
||||||
return ["error" => "User not found."];
|
return ["error" => "User not found."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the updated users file.
|
// Save the updated users file.
|
||||||
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) {
|
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL)) {
|
||||||
return ["success" => "Password updated successfully."];
|
return ["success" => "Password updated successfully."];
|
||||||
@@ -299,25 +306,26 @@ class userModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the user panel settings by disabling the TOTP secret if TOTP is not enabled.
|
* Updates the user panel settings by disabling the TOTP secret if TOTP is not enabled.
|
||||||
*
|
*
|
||||||
* @param string $username The username whose panel settings are being updated.
|
* @param string $username The username whose panel settings are being updated.
|
||||||
* @param bool $totp_enabled Whether TOTP is enabled.
|
* @param bool $totp_enabled Whether TOTP is enabled.
|
||||||
* @return array An array indicating success or failure.
|
* @return array An array indicating success or failure.
|
||||||
*/
|
*/
|
||||||
public static function updateUserPanel($username, $totp_enabled) {
|
public static function updateUserPanel($username, $totp_enabled)
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return ["error" => "Users file not found"];
|
return ["error" => "Users file not found"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If TOTP is disabled, update the file to clear the TOTP secret.
|
// If TOTP is disabled, update the file to clear the TOTP secret.
|
||||||
if (!$totp_enabled) {
|
if (!$totp_enabled) {
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
$newLines = [];
|
$newLines = [];
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
// Leave lines with fewer than three parts unchanged.
|
// Leave lines with fewer than three parts unchanged.
|
||||||
@@ -325,7 +333,7 @@ class userModel {
|
|||||||
$newLines[] = $line;
|
$newLines[] = $line;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($parts[0] === $username) {
|
if ($parts[0] === $username) {
|
||||||
// If a fourth field (TOTP secret) exists, clear it; otherwise, append an empty field.
|
// If a fourth field (TOTP secret) exists, clear it; otherwise, append an empty field.
|
||||||
if (count($parts) >= 4) {
|
if (count($parts) >= 4) {
|
||||||
@@ -338,25 +346,26 @@ class userModel {
|
|||||||
$newLines[] = $line;
|
$newLines[] = $line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
|
$result = file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
return ["error" => "Failed to disable TOTP secret"];
|
return ["error" => "Failed to disable TOTP secret"];
|
||||||
}
|
}
|
||||||
return ["success" => "User panel updated: TOTP disabled"];
|
return ["success" => "User panel updated: TOTP disabled"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If TOTP is enabled, do nothing.
|
// If TOTP is enabled, do nothing.
|
||||||
return ["success" => "User panel updated: TOTP remains enabled"];
|
return ["success" => "User panel updated: TOTP remains enabled"];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables the TOTP secret for the specified user.
|
* Disables the TOTP secret for the specified user.
|
||||||
*
|
*
|
||||||
* @param string $username The user for whom TOTP should be disabled.
|
* @param string $username The user for whom TOTP should be disabled.
|
||||||
* @return bool True if the secret was cleared; false otherwise.
|
* @return bool True if the secret was cleared; false otherwise.
|
||||||
*/
|
*/
|
||||||
public static function disableTOTPSecret($username) {
|
public static function disableTOTPSecret($username)
|
||||||
|
{
|
||||||
global $encryptionKey; // In case it's used in this model context.
|
global $encryptionKey; // In case it's used in this model context.
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
@@ -391,14 +400,15 @@ class userModel {
|
|||||||
return $modified;
|
return $modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to recover TOTP for a user using the supplied recovery code.
|
* Attempts to recover TOTP for a user using the supplied recovery code.
|
||||||
*
|
*
|
||||||
* @param string $userId The user identifier.
|
* @param string $userId The user identifier.
|
||||||
* @param string $recoveryCode The recovery code provided by the user.
|
* @param string $recoveryCode The recovery code provided by the user.
|
||||||
* @return array An associative array with keys 'status' and 'message'.
|
* @return array An associative array with keys 'status' and 'message'.
|
||||||
*/
|
*/
|
||||||
public static function recoverTOTP($userId, $recoveryCode) {
|
public static function recoverTOTP($userId, $recoveryCode)
|
||||||
|
{
|
||||||
// --- Rate‑limit recovery attempts ---
|
// --- Rate‑limit recovery attempts ---
|
||||||
$attemptsFile = rtrim(USERS_DIR, '/\\') . '/recovery_attempts.json';
|
$attemptsFile = rtrim(USERS_DIR, '/\\') . '/recovery_attempts.json';
|
||||||
$attempts = is_file($attemptsFile) ? json_decode(file_get_contents($attemptsFile), true) : [];
|
$attempts = is_file($attemptsFile) ? json_decode(file_get_contents($attemptsFile), true) : [];
|
||||||
@@ -406,36 +416,36 @@ class userModel {
|
|||||||
$now = time();
|
$now = time();
|
||||||
if (isset($attempts[$key])) {
|
if (isset($attempts[$key])) {
|
||||||
// Prune attempts older than 15 minutes.
|
// Prune attempts older than 15 minutes.
|
||||||
$attempts[$key] = array_filter($attempts[$key], function($ts) use ($now) {
|
$attempts[$key] = array_filter($attempts[$key], function ($ts) use ($now) {
|
||||||
return $ts > $now - 900;
|
return $ts > $now - 900;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (count($attempts[$key] ?? []) >= 5) {
|
if (count($attempts[$key] ?? []) >= 5) {
|
||||||
return ['status' => 'error', 'message' => 'Too many attempts. Try again later.'];
|
return ['status' => 'error', 'message' => 'Too many attempts. Try again later.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Load user metadata file ---
|
// --- Load user metadata file ---
|
||||||
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
|
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
|
||||||
if (!file_exists($userFile)) {
|
if (!file_exists($userFile)) {
|
||||||
return ['status' => 'error', 'message' => 'User not found'];
|
return ['status' => 'error', 'message' => 'User not found'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Open and lock file ---
|
// --- Open and lock file ---
|
||||||
$fp = fopen($userFile, 'c+');
|
$fp = fopen($userFile, 'c+');
|
||||||
if (!$fp || !flock($fp, LOCK_EX)) {
|
if (!$fp || !flock($fp, LOCK_EX)) {
|
||||||
return ['status' => 'error', 'message' => 'Server error'];
|
return ['status' => 'error', 'message' => 'Server error'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$fileContents = stream_get_contents($fp);
|
$fileContents = stream_get_contents($fp);
|
||||||
$data = json_decode($fileContents, true) ?: [];
|
$data = json_decode($fileContents, true) ?: [];
|
||||||
|
|
||||||
// --- Check recovery code ---
|
// --- Check recovery code ---
|
||||||
if (empty($recoveryCode)) {
|
if (empty($recoveryCode)) {
|
||||||
flock($fp, LOCK_UN);
|
flock($fp, LOCK_UN);
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
return ['status' => 'error', 'message' => 'Recovery code required'];
|
return ['status' => 'error', 'message' => 'Recovery code required'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$storedHash = $data['totp_recovery_code'] ?? null;
|
$storedHash = $data['totp_recovery_code'] ?? null;
|
||||||
if (!$storedHash || !password_verify($recoveryCode, $storedHash)) {
|
if (!$storedHash || !password_verify($recoveryCode, $storedHash)) {
|
||||||
// Record failed attempt.
|
// Record failed attempt.
|
||||||
@@ -445,7 +455,7 @@ class userModel {
|
|||||||
fclose($fp);
|
fclose($fp);
|
||||||
return ['status' => 'error', 'message' => 'Invalid recovery code'];
|
return ['status' => 'error', 'message' => 'Invalid recovery code'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Invalidate recovery code ---
|
// --- Invalidate recovery code ---
|
||||||
$data['totp_recovery_code'] = null;
|
$data['totp_recovery_code'] = null;
|
||||||
rewind($fp);
|
rewind($fp);
|
||||||
@@ -454,17 +464,18 @@ class userModel {
|
|||||||
fflush($fp);
|
fflush($fp);
|
||||||
flock($fp, LOCK_UN);
|
flock($fp, LOCK_UN);
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
|
|
||||||
return ['status' => 'ok'];
|
return ['status' => 'ok'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a random recovery code.
|
* Generates a random recovery code.
|
||||||
*
|
*
|
||||||
* @param int $length Length of the recovery code.
|
* @param int $length Length of the recovery code.
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private static function generateRecoveryCode($length = 12) {
|
private static function generateRecoveryCode($length = 12)
|
||||||
|
{
|
||||||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
$max = strlen($chars) - 1;
|
$max = strlen($chars) - 1;
|
||||||
$code = '';
|
$code = '';
|
||||||
@@ -480,10 +491,11 @@ class userModel {
|
|||||||
* @param string $userId The username of the user.
|
* @param string $userId The username of the user.
|
||||||
* @return array An associative array with the status and recovery code (if successful).
|
* @return array An associative array with the status and recovery code (if successful).
|
||||||
*/
|
*/
|
||||||
public static function saveTOTPRecoveryCode($userId) {
|
public static function saveTOTPRecoveryCode($userId)
|
||||||
|
{
|
||||||
// Determine the user file path.
|
// Determine the user file path.
|
||||||
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
|
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
|
||||||
|
|
||||||
// Ensure the file exists; if not, create it with default data.
|
// Ensure the file exists; if not, create it with default data.
|
||||||
if (!file_exists($userFile)) {
|
if (!file_exists($userFile)) {
|
||||||
$defaultData = [];
|
$defaultData = [];
|
||||||
@@ -491,24 +503,24 @@ class userModel {
|
|||||||
return ['status' => 'error', 'message' => 'Server error: could not create user file'];
|
return ['status' => 'error', 'message' => 'Server error: could not create user file'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new recovery code.
|
// Generate a new recovery code.
|
||||||
$recoveryCode = self::generateRecoveryCode();
|
$recoveryCode = self::generateRecoveryCode();
|
||||||
$recoveryHash = password_hash($recoveryCode, PASSWORD_DEFAULT);
|
$recoveryHash = password_hash($recoveryCode, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
// Open the file, lock it, and update the totp_recovery_code field.
|
// Open the file, lock it, and update the totp_recovery_code field.
|
||||||
$fp = fopen($userFile, 'c+');
|
$fp = fopen($userFile, 'c+');
|
||||||
if (!$fp || !flock($fp, LOCK_EX)) {
|
if (!$fp || !flock($fp, LOCK_EX)) {
|
||||||
return ['status' => 'error', 'message' => 'Server error: could not lock user file'];
|
return ['status' => 'error', 'message' => 'Server error: could not lock user file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and decode the existing JSON.
|
// Read and decode the existing JSON.
|
||||||
$contents = stream_get_contents($fp);
|
$contents = stream_get_contents($fp);
|
||||||
$data = json_decode($contents, true) ?: [];
|
$data = json_decode($contents, true) ?: [];
|
||||||
|
|
||||||
// Update the totp_recovery_code field.
|
// Update the totp_recovery_code field.
|
||||||
$data['totp_recovery_code'] = $recoveryHash;
|
$data['totp_recovery_code'] = $recoveryHash;
|
||||||
|
|
||||||
// Write the new data.
|
// Write the new data.
|
||||||
rewind($fp);
|
rewind($fp);
|
||||||
ftruncate($fp, 0);
|
ftruncate($fp, 0);
|
||||||
@@ -516,25 +528,26 @@ class userModel {
|
|||||||
fflush($fp);
|
fflush($fp);
|
||||||
flock($fp, LOCK_UN);
|
flock($fp, LOCK_UN);
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
|
|
||||||
return ['status' => 'ok', 'recoveryCode' => $recoveryCode];
|
return ['status' => 'ok', 'recoveryCode' => $recoveryCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up TOTP for the specified user by retrieving or generating a TOTP secret,
|
* Sets up TOTP for the specified user by retrieving or generating a TOTP secret,
|
||||||
* then builds and returns a QR code image for the OTPAuth URL.
|
* then builds and returns a QR code image for the OTPAuth URL.
|
||||||
*
|
*
|
||||||
* @param string $username The username for which to set up TOTP.
|
* @param string $username The username for which to set up TOTP.
|
||||||
* @return array An associative array with keys 'imageData' and 'mimeType', or 'error'.
|
* @return array An associative array with keys 'imageData' and 'mimeType', or 'error'.
|
||||||
*/
|
*/
|
||||||
public static function setupTOTP($username) {
|
public static function setupTOTP($username)
|
||||||
|
{
|
||||||
global $encryptionKey;
|
global $encryptionKey;
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return ['error' => 'Users file not found'];
|
return ['error' => 'Users file not found'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for an existing TOTP secret.
|
// Look for an existing TOTP secret.
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
$totpSecret = null;
|
$totpSecret = null;
|
||||||
@@ -545,7 +558,7 @@ class userModel {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the TwoFactorAuth library to create a new secret if none found.
|
// Use the TwoFactorAuth library to create a new secret if none found.
|
||||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||||||
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), // QR code provider
|
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), // QR code provider
|
||||||
@@ -557,7 +570,7 @@ class userModel {
|
|||||||
if (!$totpSecret) {
|
if (!$totpSecret) {
|
||||||
$totpSecret = $tfa->createSecret();
|
$totpSecret = $tfa->createSecret();
|
||||||
$encryptedSecret = encryptData($totpSecret, $encryptionKey);
|
$encryptedSecret = encryptData($totpSecret, $encryptionKey);
|
||||||
|
|
||||||
// Update the user’s line with the new encrypted secret.
|
// Update the user’s line with the new encrypted secret.
|
||||||
$newLines = [];
|
$newLines = [];
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
@@ -575,7 +588,7 @@ class userModel {
|
|||||||
}
|
}
|
||||||
file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
|
file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the OTPAuth URL.
|
// Determine the OTPAuth URL.
|
||||||
// Try to load a global OTPAuth URL template from admin configuration.
|
// Try to load a global OTPAuth URL template from admin configuration.
|
||||||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||||
@@ -590,7 +603,7 @@ class userModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($globalOtpauthUrl)) {
|
if (!empty($globalOtpauthUrl)) {
|
||||||
$label = "FileRise:" . $username;
|
$label = "FileRise:" . $username;
|
||||||
$otpauthUrl = str_replace(["{label}", "{secret}"], [urlencode($label), $totpSecret], $globalOtpauthUrl);
|
$otpauthUrl = str_replace(["{label}", "{secret}"], [urlencode($label), $totpSecret], $globalOtpauthUrl);
|
||||||
@@ -599,26 +612,27 @@ class userModel {
|
|||||||
$issuer = urlencode("FileRise");
|
$issuer = urlencode("FileRise");
|
||||||
$otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}";
|
$otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the QR code image using the Endroid QR Code Builder.
|
// Build the QR code image using the Endroid QR Code Builder.
|
||||||
$result = \Endroid\QrCode\Builder\Builder::create()
|
$result = \Endroid\QrCode\Builder\Builder::create()
|
||||||
->writer(new \Endroid\QrCode\Writer\PngWriter())
|
->writer(new \Endroid\QrCode\Writer\PngWriter())
|
||||||
->data($otpauthUrl)
|
->data($otpauthUrl)
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'imageData' => $result->getString(),
|
'imageData' => $result->getString(),
|
||||||
'mimeType' => $result->getMimeType()
|
'mimeType' => $result->getMimeType()
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the decrypted TOTP secret for a given user.
|
* Retrieves the decrypted TOTP secret for a given user.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @return string|null Returns the TOTP secret if found, or null if not.
|
* @return string|null Returns the TOTP secret if found, or null if not.
|
||||||
*/
|
*/
|
||||||
public static function getTOTPSecret($username) {
|
public static function getTOTPSecret($username)
|
||||||
|
{
|
||||||
global $encryptionKey;
|
global $encryptionKey;
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
@@ -634,14 +648,15 @@ class userModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to get a user's role from users.txt.
|
* Helper to get a user's role from users.txt.
|
||||||
*
|
*
|
||||||
* @param string $username
|
* @param string $username
|
||||||
* @return string|null
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public static function getUserRole($username) {
|
public static function getUserRole($username)
|
||||||
|
{
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -654,4 +669,86 @@ class userModel {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static function getUser(string $username): array
|
||||||
|
{
|
||||||
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
if (! file_exists($usersFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
// split *all* the fields
|
||||||
|
$parts = explode(':', $line);
|
||||||
|
|
||||||
|
if ($parts[0] !== $username) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine admin & totp
|
||||||
|
$isAdmin = (isset($parts[2]) && $parts[2] === '1');
|
||||||
|
$totpEnabled = !empty($parts[3]);
|
||||||
|
// profile_picture is the 5th field if present
|
||||||
|
$pic = isset($parts[4]) ? $parts[4] : '';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'username' => $parts[0],
|
||||||
|
'isAdmin' => $isAdmin,
|
||||||
|
'totp_enabled' => $totpEnabled,
|
||||||
|
'profile_picture' => $pic,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return []; // user not found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistently set the profile picture URL for a given user,
|
||||||
|
* storing it in the 5th field so we leave the 4th (TOTP secret) untouched.
|
||||||
|
*
|
||||||
|
* users.txt format:
|
||||||
|
* username:hash:isAdmin:totp_secret:profile_picture
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $url The public URL (e.g. "/uploads/profile_pics/…")
|
||||||
|
* @return array ['success'=>true] or ['success'=>false,'error'=>'…']
|
||||||
|
*/
|
||||||
|
public static function setProfilePicture(string $username, string $url): array
|
||||||
|
{
|
||||||
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
if (! file_exists($usersFile)) {
|
||||||
|
return ['success' => false, 'error' => 'Users file not found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES);
|
||||||
|
$out = [];
|
||||||
|
$found = false;
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$parts = explode(':', $line);
|
||||||
|
if ($parts[0] === $username) {
|
||||||
|
$found = true;
|
||||||
|
// Ensure we have at least 5 fields
|
||||||
|
while (count($parts) < 5) {
|
||||||
|
$parts[] = '';
|
||||||
|
}
|
||||||
|
// Write profile_picture into the 5th field (index 4)
|
||||||
|
$parts[4] = ltrim($url, '/'); // or $url if leading slash is desired
|
||||||
|
// Re-assemble (this preserves parts[3] completely)
|
||||||
|
$line = implode(':', $parts);
|
||||||
|
}
|
||||||
|
$out[] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $found) {
|
||||||
|
return ['success' => false, 'error' => 'User not found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$newContent = implode(PHP_EOL, $out) . PHP_EOL;
|
||||||
|
if (file_put_contents($usersFile, $newContent, LOCK_EX) === false) {
|
||||||
|
return ['success' => false, 'error' => 'Failed to write users file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||