Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f391d11db | ||
|
|
8c70783d5a | ||
|
|
b4d6f01432 | ||
|
|
d48b15a5f4 | ||
|
|
d1726f0160 | ||
|
|
bd1841b788 | ||
|
|
bde35d1d31 | ||
|
|
8d6a1be777 | ||
|
|
56f34ba362 | ||
|
|
4d329e046f | ||
|
|
f3977153fb | ||
|
|
274bedd186 | ||
|
|
2e4dbe7f7f | ||
|
|
0334e443eb |
223
CHANGELOG.md
@@ -1,6 +1,226 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Changes 4/27/2025 1.2.7
|
## 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
|
||||||
|
|
||||||
|
- **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** `autofocus` attribute to the login form’s username input (`#loginUsername`) so the cursor is ready for typing on page load.
|
||||||
|
- **Enhanced** login initialization with a `DOMContentLoaded` fallback that calls `loginUsername.focus()` (via `setTimeout`) if needed.
|
||||||
|
- **Set** focus to the “New Username” field (`#newUsername`) when entering setup mode, hiding the login form and showing the Add-User modal.
|
||||||
|
- **Implemented** Enter-key support in setup mode by attaching `attachEnterKeyListener("addUserModal", "saveUserBtn")`, allowing users to press Enter to submit the Add-User form.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes 4/28/2025
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
- **Custom expiration** option to File Share modal
|
||||||
|
- Users can specify a value + unit (seconds, minutes, hours, days)
|
||||||
|
- Displays a warning when a custom duration is selected
|
||||||
|
- **Custom expiration** option to Folder Share modal (same value+unit picker and warning)
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
- **API parameters** for both endpoints:
|
||||||
|
- Replaced `expirationMinutes` with `expirationValue` + `expirationUnit`
|
||||||
|
- Front-end now sends `{ expirationValue, expirationUnit }`
|
||||||
|
- Back-end converts those into total seconds before saving
|
||||||
|
- **UI**
|
||||||
|
- FileShare and FolderShare modals updated to handle “Custom…” selection
|
||||||
|
|
||||||
|
**Updated Models & Controllers**
|
||||||
|
|
||||||
|
- **FileModel::createShareLink** now accepts expiration in seconds
|
||||||
|
- **FolderModel::createShareFolderLink** now accepts expiration in seconds
|
||||||
|
- **createShareLink.php** & **createShareFolderLink.php** updated to parse and convert new parameters
|
||||||
|
|
||||||
|
**Documentation**
|
||||||
|
|
||||||
|
- OpenAPI annotations for both endpoints updated to require `expirationValue` + `expirationUnit` (enum: seconds, minutes, hours, days)
|
||||||
|
|
||||||
|
## Changes 4/27/2025 v1.2.7
|
||||||
|
|
||||||
- **Select-All** checkbox now correctly toggles all `.file-checkbox` inputs
|
- **Select-All** checkbox now correctly toggles all `.file-checkbox` inputs
|
||||||
- Updated `toggleAllCheckboxes(masterCheckbox)` to call `updateRowHighlight()` on each row so selections get the `.row-selected` highlight
|
- Updated `toggleAllCheckboxes(masterCheckbox)` to call `updateRowHighlight()` on each row so selections get the `.row-selected` highlight
|
||||||
@@ -19,6 +239,7 @@
|
|||||||
- Added `.toggle-btn` CSS for blue header-style toggle button and applied it in JS
|
- Added `.toggle-btn` CSS for blue header-style toggle button and applied it in JS
|
||||||
- Added `.pagination a:hover { background-color: #0056b3; }` to match button hover
|
- Added `.pagination a:hover { background-color: #0056b3; }` to match button hover
|
||||||
- Tweaked `body` padding and `header h1` margins to reduce whitespace above header
|
- Tweaked `body` padding and `header h1` margins to reduce whitespace above header
|
||||||
|
- Refactored `sharedFolderView.js:renderGalleryView()` to eliminate `innerHTML` usage; now uses `document.createElement` and `textContent` so filenames and URLs are fully escaped and CSP-safe
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
@@ -9,6 +9,21 @@
|
|||||||
<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" />
|
||||||
@@ -165,10 +180,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 +223,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 />
|
|
||||||
</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;">
|
||||||
@@ -428,7 +444,7 @@
|
|||||||
<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"
|
||||||
style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
class="editor-close-btn">×</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,24 +455,36 @@
|
|||||||
<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>
|
||||||
<label for="newUsername" data-i18n-key="username">Username:</label>
|
<!-- 1) Add a form around these fields -->
|
||||||
<input type="text" id="newUsername" class="form-control" />
|
<form id="addUserForm">
|
||||||
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
<label for="newUsername" data-i18n-key="username">Username:</label>
|
||||||
<input type="password" id="addUserPassword" class="form-control" />
|
<input type="text" id="newUsername" class="form-control" required />
|
||||||
<div id="adminCheckboxContainer">
|
|
||||||
<input type="checkbox" id="isAdmin" />
|
<label for="addUserPassword" data-i18n-key="password">Password:</label>
|
||||||
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
<input type="password" id="addUserPassword" class="form-control" required />
|
||||||
</div>
|
|
||||||
<div class="button-container">
|
<div id="adminCheckboxContainer">
|
||||||
<button id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
<input type="checkbox" id="isAdmin" />
|
||||||
<button id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">Save User</button>
|
<label for="isAdmin" data-i18n-key="grant_admin">Grant Admin Access</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<!-- Cancel stays type="button" -->
|
||||||
|
<button type="button" id="cancelUserBtn" class="btn btn-secondary" data-i18n-key="cancel">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<!-- Save becomes type="submit" -->
|
||||||
|
<button type="submit" id="saveUserBtn" class="btn btn-primary" data-i18n-key="save_user">
|
||||||
|
Save User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
@@ -467,7 +495,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.3";
|
||||||
|
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,16 @@ import {
|
|||||||
openUserPanel,
|
openUserPanel,
|
||||||
openTOTPModal,
|
openTOTPModal,
|
||||||
closeTOTPModal,
|
closeTOTPModal,
|
||||||
openAdminPanel,
|
|
||||||
closeAdminPanel,
|
|
||||||
setLastLoginData
|
setLastLoginData
|
||||||
} from './authModals.js';
|
} from './authModals.js';
|
||||||
|
import { openAdminPanel } from './adminPanel.js';
|
||||||
|
import { initializeApp } 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,10 +125,24 @@ 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.style.display = disableFormLogin ? "none" : "block";
|
(authForm) {
|
||||||
|
authForm.style.display = disableFormLogin ? "none" : "block";
|
||||||
|
setTimeout(() => {
|
||||||
|
const loginInput = document.getElementById('loginUsername');
|
||||||
|
if (loginInput) loginInput.focus();
|
||||||
|
}, 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";
|
||||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
@@ -139,7 +153,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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +169,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();
|
||||||
|
|
||||||
@@ -183,11 +200,16 @@ function insertAfter(newNode, referenceNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateAuthenticatedUI(data) {
|
function updateAuthenticatedUI(data) {
|
||||||
|
document.getElementById('loadingOverlay').remove();
|
||||||
|
|
||||||
|
// show the wrapper (so the login form can be visible)
|
||||||
|
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("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";
|
||||||
@@ -257,6 +279,7 @@ function updateAuthenticatedUI(data) {
|
|||||||
userPanelBtn.style.display = "block";
|
userPanelBtn.style.display = "block";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
initializeApp();
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
updateItemsPerPageSelect();
|
updateItemsPerPageSelect();
|
||||||
updateLoginOptionsUIFromStorage();
|
updateLoginOptionsUIFromStorage();
|
||||||
@@ -266,6 +289,11 @@ 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) {
|
||||||
|
document.getElementById('loadingOverlay').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);
|
||||||
@@ -277,11 +305,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");
|
||||||
}
|
}
|
||||||
@@ -292,8 +322,13 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
updateAuthenticatedUI(data);
|
updateAuthenticatedUI(data);
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
|
document.getElementById('loadingOverlay').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);
|
||||||
@@ -443,33 +478,48 @@ function initAuth() {
|
|||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
|
||||||
|
// remove your old saveUserBtn click-handler…
|
||||||
|
|
||||||
|
// instead:
|
||||||
|
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();
|
const newUsername = document.getElementById("newUsername").value.trim();
|
||||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||||
const isAdmin = document.getElementById("isAdmin").checked;
|
const isAdmin = document.getElementById("isAdmin").checked;
|
||||||
|
|
||||||
if (!newUsername || !newPassword) {
|
if (!newUsername || !newPassword) {
|
||||||
showToast("Username and password are required!");
|
showToast("Username and password are required!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = "/api/addUser.php";
|
let url = "/api/addUser.php";
|
||||||
if (window.setupMode) url += "?setup=1";
|
if (window.setupMode) url += "?setup=1";
|
||||||
|
|
||||||
fetchWithCsrf(url, {
|
fetchWithCsrf(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast("User added successfully!");
|
showToast("User added successfully!");
|
||||||
closeAddUserModal();
|
closeAddUserModal();
|
||||||
checkAuthentication(false);
|
checkAuthentication(false);
|
||||||
|
if (window.setupMode) {
|
||||||
|
toggleVisibility("loginForm", true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast("Error: " + (data.error || "Could not add user"));
|
showToast("Error: " + (data.error || "Could not add user"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => {
|
||||||
|
showToast("Error: Could not add user");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { sendRequest } from './networkUtils.js';
|
|||||||
import { t, applyTranslations, setLocale } from './i18n.js';
|
import { t, applyTranslations, setLocale } from './i18n.js';
|
||||||
import { loadAdminConfigFunc } from './auth.js';
|
import { loadAdminConfigFunc } from './auth.js';
|
||||||
|
|
||||||
const version = "v1.2.7"; // Update this version string as needed
|
|
||||||
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
|
||||||
|
|
||||||
let lastLoginData = null;
|
let lastLoginData = null;
|
||||||
export function setLastLoginData(data) {
|
export function setLastLoginData(data) {
|
||||||
@@ -32,7 +30,7 @@ export function openTOTPLoginModal() {
|
|||||||
`;
|
`;
|
||||||
totpLoginModal.innerHTML = `
|
totpLoginModal.innerHTML = `
|
||||||
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
|
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
|
||||||
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
<span id="closeTOTPLoginModal" class="editor-close-btn">×</span>
|
||||||
<div id="totpSection">
|
<div id="totpSection">
|
||||||
<h3>${t("enter_totp_code")}</h3>
|
<h3>${t("enter_totp_code")}</h3>
|
||||||
<input type="text" id="totpLoginInput" maxlength="6"
|
<input type="text" id="totpLoginInput" maxlength="6"
|
||||||
@@ -174,11 +172,13 @@ export function openUserPanel() {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: fixed;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 400px !important;
|
overflow-x: hidden;
|
||||||
|
max-height: 383px !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
||||||
transform: none;
|
box-sizing: border-box;
|
||||||
transition: none;
|
transition: none;
|
||||||
`;
|
`;
|
||||||
const savedLanguage = localStorage.getItem("language") || "en";
|
const savedLanguage = localStorage.getItem("language") || "en";
|
||||||
@@ -188,19 +188,17 @@ export function openUserPanel() {
|
|||||||
userPanelModal.id = "userPanelModal";
|
userPanelModal.id = "userPanelModal";
|
||||||
userPanelModal.style.cssText = `
|
userPanelModal.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: ${overlayBackground};
|
background-color: ${overlayBackground};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 3000;
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
userPanelModal.innerHTML = `
|
userPanelModal.innerHTML = `
|
||||||
<div class="modal-content user-panel-content" style="${modalContentStyles}">
|
<div class="modal-content user-panel-content" style="${modalContentStyles}">
|
||||||
<span id="closeUserPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
<span id="closeUserPanel" class="editor-close-btn">×</span>
|
||||||
<h3>${t("user_panel")} (${username})</h3>
|
<h3>${t("user_panel")} (${username})</h3>
|
||||||
|
|
||||||
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">
|
<button type="button" id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom: 15px;">
|
||||||
@@ -371,7 +369,7 @@ export function openTOTPModal() {
|
|||||||
`;
|
`;
|
||||||
totpModal.innerHTML = `
|
totpModal.innerHTML = `
|
||||||
<div class="modal-content" style="${modalContentStyles}">
|
<div class="modal-content" style="${modalContentStyles}">
|
||||||
<span id="closeTOTPModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
<span id="closeTOTPModal" class="editor-close-btn">×</span>
|
||||||
<h3>${t("totp_setup")}</h3>
|
<h3>${t("totp_setup")}</h3>
|
||||||
<p>${t("scan_qr_code")}</p>
|
<p>${t("scan_qr_code")}</p>
|
||||||
<!-- Create an image placeholder without the CSRF token in the src -->
|
<!-- Create an image placeholder without the CSRF token in the src -->
|
||||||
@@ -545,538 +543,3 @@ export function closeTOTPModal(disable = true) {
|
|||||||
.catch(() => { showToast(t("error_disabling_totp_setting")); });
|
.catch(() => { showToast(t("error_disabling_totp_setting")); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global variable to hold the initial state of the admin form.
|
|
||||||
let originalAdminConfig = {};
|
|
||||||
|
|
||||||
// Capture the initial state of the admin form fields.
|
|
||||||
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,
|
|
||||||
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare current values to the captured initial state.
|
|
||||||
function hasUnsavedChanges() {
|
|
||||||
return (
|
|
||||||
document.getElementById("headerTitle").value.trim() !== originalAdminConfig.headerTitle ||
|
|
||||||
document.getElementById("oidcProviderUrl").value.trim() !== originalAdminConfig.oidcProviderUrl ||
|
|
||||||
document.getElementById("oidcClientId").value.trim() !== originalAdminConfig.oidcClientId ||
|
|
||||||
document.getElementById("oidcClientSecret").value.trim() !== originalAdminConfig.oidcClientSecret ||
|
|
||||||
document.getElementById("oidcRedirectUri").value.trim() !== originalAdminConfig.oidcRedirectUri ||
|
|
||||||
document.getElementById("disableFormLogin").checked !== originalAdminConfig.disableFormLogin ||
|
|
||||||
document.getElementById("disableBasicAuth").checked !== originalAdminConfig.disableBasicAuth ||
|
|
||||||
document.getElementById("disableOIDCLogin").checked !== originalAdminConfig.disableOIDCLogin ||
|
|
||||||
document.getElementById("globalOtpauthUrl").value.trim() !== originalAdminConfig.globalOtpauthUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use your custom confirmation modal.
|
|
||||||
function showCustomConfirmModal(message) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Get modal elements from DOM.
|
|
||||||
const modal = document.getElementById("customConfirmModal");
|
|
||||||
const messageElem = document.getElementById("confirmMessage");
|
|
||||||
const yesBtn = document.getElementById("confirmYesBtn");
|
|
||||||
const noBtn = document.getElementById("confirmNoBtn");
|
|
||||||
|
|
||||||
// Set the message in the modal.
|
|
||||||
messageElem.textContent = message;
|
|
||||||
modal.style.display = "block";
|
|
||||||
|
|
||||||
// Define event handlers.
|
|
||||||
function onYes() {
|
|
||||||
cleanup();
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
function onNo() {
|
|
||||||
cleanup();
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
// Remove event listeners and hide modal after choice.
|
|
||||||
function cleanup() {
|
|
||||||
yesBtn.removeEventListener("click", onYes);
|
|
||||||
noBtn.removeEventListener("click", onNo);
|
|
||||||
modal.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
yesBtn.addEventListener("click", onYes);
|
|
||||||
noBtn.addEventListener("click", onNo);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openAdminPanel() {
|
|
||||||
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(config => {
|
|
||||||
if (config.header_title) {
|
|
||||||
document.querySelector(".header-title h1").textContent = config.header_title;
|
|
||||||
window.headerTitle = config.header_title || "FileRise";
|
|
||||||
}
|
|
||||||
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
|
||||||
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
|
||||||
|
|
||||||
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: 600px;
|
|
||||||
width: 90%;
|
|
||||||
border-radius: 8px;
|
|
||||||
position: relative;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 90vh;
|
|
||||||
border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"};
|
|
||||||
`;
|
|
||||||
|
|
||||||
let adminModal = document.getElementById("adminPanelModal");
|
|
||||||
|
|
||||||
if (!adminModal) {
|
|
||||||
adminModal = document.createElement("div");
|
|
||||||
adminModal.id = "adminPanelModal";
|
|
||||||
adminModal.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: 3000;
|
|
||||||
`;
|
|
||||||
adminModal.innerHTML = `
|
|
||||||
<div class="modal-content" style="${modalContentStyles}">
|
|
||||||
<span id="closeAdminPanel" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
|
||||||
<h3>${adminTitle}</h3>
|
|
||||||
<form id="adminPanelForm">
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>${t("user_management")}</legend>
|
|
||||||
<div style="display: flex; gap: 10px;">
|
|
||||||
<button type="button" id="adminOpenAddUser" class="btn btn-success">${t("add_user")}</button>
|
|
||||||
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">${t("remove_user")}</button>
|
|
||||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>Header Settings</legend>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="headerTitle">Header Title:</label>
|
|
||||||
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle}" />
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>${t("login_options")}</legend>
|
|
||||||
<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>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<!-- New WebDAV setting -->
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>WebDAV Access</legend>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="checkbox" id="enableWebDAV" />
|
|
||||||
<label for="enableWebDAV">Enable WebDAV</label>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<!-- End WebDAV setting -->
|
|
||||||
|
|
||||||
<!-- New Shared Max Upload Size setting -->
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>Shared Max Upload Size (bytes)</legend>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" id="sharedMaxUploadSize" class="form-control"
|
|
||||||
placeholder="e.g. 52428800" />
|
|
||||||
<small>Enter maximum bytes allowed for shared-folder uploads</small>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<!-- End Shared Max Upload Size setting -->
|
|
||||||
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>${t("oidc_configuration")}</legend>
|
|
||||||
<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>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset style="margin-bottom: 15px;">
|
|
||||||
<legend>${t("global_totp_settings")}</legend>
|
|
||||||
<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>
|
|
||||||
</fieldset>
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
<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(adminModal);
|
|
||||||
|
|
||||||
// Bind closing
|
|
||||||
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
|
||||||
adminModal.addEventListener("click", e => { if (e.target === adminModal) closeAdminPanel(); });
|
|
||||||
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
|
||||||
|
|
||||||
// Bind other buttons
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save handler
|
|
||||||
document.getElementById("saveAdminSettings").addEventListener("click", () => {
|
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
|
||||||
const enableWebDAVCheckbox = document.getElementById("enableWebDAV");
|
|
||||||
const sharedMaxUploadSizeInput = document.getElementById("sharedMaxUploadSize");
|
|
||||||
|
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
|
||||||
.filter(cb => cb.checked).length;
|
|
||||||
if (totalDisabled === 3) {
|
|
||||||
showToast(t("at_least_one_login_method"));
|
|
||||||
disableOIDCLoginCheckbox.checked = false;
|
|
||||||
localStorage.setItem("disableOIDCLogin", "false");
|
|
||||||
if (typeof window.updateLoginOptionsUI === "function") {
|
|
||||||
window.updateLoginOptionsUI({
|
|
||||||
disableFormLogin: disableFormLoginCheckbox.checked,
|
|
||||||
disableBasicAuth: disableBasicAuthCheckbox.checked,
|
|
||||||
disableOIDCLogin: disableOIDCLoginCheckbox.checked
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newHeaderTitle = document.getElementById("headerTitle").value.trim();
|
|
||||||
const newOIDCConfig = {
|
|
||||||
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 disableFormLogin = disableFormLoginCheckbox.checked;
|
|
||||||
const disableBasicAuth = disableBasicAuthCheckbox.checked;
|
|
||||||
const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
|
|
||||||
const enableWebDAV = enableWebDAVCheckbox.checked;
|
|
||||||
const sharedMaxUploadSize = parseInt(sharedMaxUploadSizeInput.value, 10) || 0;
|
|
||||||
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
|
|
||||||
|
|
||||||
sendRequest("/api/admin/updateConfig.php", "POST", {
|
|
||||||
header_title: newHeaderTitle,
|
|
||||||
oidc: newOIDCConfig,
|
|
||||||
disableFormLogin,
|
|
||||||
disableBasicAuth,
|
|
||||||
disableOIDCLogin,
|
|
||||||
enableWebDAV,
|
|
||||||
sharedMaxUploadSize,
|
|
||||||
globalOtpauthUrl
|
|
||||||
}, { "X-CSRF-Token": window.csrfToken })
|
|
||||||
.then(response => {
|
|
||||||
if (response.success) {
|
|
||||||
showToast(t("settings_updated_successfully"));
|
|
||||||
localStorage.setItem("disableFormLogin", disableFormLogin);
|
|
||||||
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
|
||||||
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
|
||||||
localStorage.setItem("enableWebDAV", enableWebDAV);
|
|
||||||
localStorage.setItem("sharedMaxUploadSize", sharedMaxUploadSize);
|
|
||||||
if (typeof window.updateLoginOptionsUI === "function") {
|
|
||||||
window.updateLoginOptionsUI({
|
|
||||||
disableFormLogin,
|
|
||||||
disableBasicAuth,
|
|
||||||
disableOIDCLogin
|
|
||||||
});
|
|
||||||
}
|
|
||||||
captureInitialAdminConfig();
|
|
||||||
closeAdminPanel();
|
|
||||||
loadAdminConfigFunc();
|
|
||||||
} else {
|
|
||||||
showToast(t("error_updating_settings") + ": " + (response.error || t("unknown_error")));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => { });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enforce login option constraints.
|
|
||||||
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
|
||||||
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
|
||||||
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
|
||||||
function enforceLoginOptionConstraint(changedCheckbox) {
|
|
||||||
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
|
||||||
.filter(cb => cb.checked).length;
|
|
||||||
if (changedCheckbox.checked && totalDisabled === 3) {
|
|
||||||
showToast(t("at_least_one_login_method"));
|
|
||||||
changedCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disableFormLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
|
||||||
disableBasicAuthCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
|
||||||
disableOIDCLoginCheckbox.addEventListener("change", function () { enforceLoginOptionConstraint(this); });
|
|
||||||
|
|
||||||
// Initial checkbox and input states
|
|
||||||
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("enableWebDAV").checked = config.enableWebDAV === true;
|
|
||||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
|
||||||
|
|
||||||
captureInitialAdminConfig();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Update existing modal and show
|
|
||||||
adminModal.style.backgroundColor = overlayBackground;
|
|
||||||
const modalContent = adminModal.querySelector(".modal-content");
|
|
||||||
if (modalContent) {
|
|
||||||
modalContent.style.background = isDarkMode ? "#2c2c2c" : "#fff";
|
|
||||||
modalContent.style.color = isDarkMode ? "#e0e0e0" : "#000";
|
|
||||||
modalContent.style.border = isDarkMode ? "1px solid #444" : "1px solid #ccc";
|
|
||||||
}
|
|
||||||
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 || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise';
|
|
||||||
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("enableWebDAV").checked = config.enableWebDAV === true;
|
|
||||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
|
||||||
adminModal.style.display = "flex";
|
|
||||||
captureInitialAdminConfig();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
let adminModal = document.getElementById("adminPanelModal");
|
|
||||||
if (adminModal) {
|
|
||||||
adminModal.style.backgroundColor = "rgba(0,0,0,0.5)";
|
|
||||||
const modalContent = adminModal.querySelector(".modal-content");
|
|
||||||
if (modalContent) {
|
|
||||||
modalContent.style.background = "#fff";
|
|
||||||
modalContent.style.color = "#000";
|
|
||||||
modalContent.style.border = "1px solid #ccc";
|
|
||||||
}
|
|
||||||
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 || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise';
|
|
||||||
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
|
||||||
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
|
||||||
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
|
||||||
document.getElementById("enableWebDAV").checked = localStorage.getItem("enableWebDAV") === "true";
|
|
||||||
document.getElementById("sharedMaxUploadSize").value = localStorage.getItem("sharedMaxUploadSize") || "";
|
|
||||||
adminModal.style.display = "flex";
|
|
||||||
captureInitialAdminConfig();
|
|
||||||
} else {
|
|
||||||
openAdminPanel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function closeAdminPanel() {
|
|
||||||
if (hasUnsavedChanges()) {
|
|
||||||
const userConfirmed = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
|
||||||
if (!userConfirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const adminModal = document.getElementById("adminPanelModal");
|
|
||||||
if (adminModal) adminModal.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" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</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>";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -4,36 +4,68 @@ import { fileData } from './fileListView.js';
|
|||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|
||||||
export function openShareModal(file, folder) {
|
export function openShareModal(file, folder) {
|
||||||
|
// Remove any existing modal
|
||||||
const existing = document.getElementById("shareModal");
|
const existing = document.getElementById("shareModal");
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
// Build the modal
|
||||||
const modal = document.createElement("div");
|
const modal = document.createElement("div");
|
||||||
modal.id = "shareModal";
|
modal.id = "shareModal";
|
||||||
modal.classList.add("modal");
|
modal.classList.add("modal");
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
|
<div class="modal-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
<h3>${t("share_file")}: ${escapeHTML(file.name)}</h3>
|
||||||
<span class="close-image-modal" id="closeShareModal" title="Close">×</span>
|
<span id="closeShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>${t("set_expiration")}</p>
|
<p>${t("set_expiration")}</p>
|
||||||
<select id="shareExpiration">
|
<select id="shareExpiration" style="width:100%;padding:5px;">
|
||||||
<option value="30">30 minutes</option>
|
<option value="30">30 ${t("minutes")}</option>
|
||||||
<option value="60" selected>60 minutes</option>
|
<option value="60" selected>60 ${t("minutes")}</option>
|
||||||
<option value="120">120 minutes</option>
|
<option value="120">120 ${t("minutes")}</option>
|
||||||
<option value="180">180 minutes</option>
|
<option value="180">180 ${t("minutes")}</option>
|
||||||
<option value="240">240 minutes</option>
|
<option value="240">240 ${t("minutes")}</option>
|
||||||
<option value="1440">1 Day</option>
|
<option value="1440">1 ${t("day")}</option>
|
||||||
|
<option value="custom">${t("custom")}…</option>
|
||||||
</select>
|
</select>
|
||||||
<p>${t("password_optional")}</p>
|
|
||||||
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
|
<div id="customExpirationContainer" style="display:none;margin-top:10px;">
|
||||||
<br>
|
<label for="customExpirationValue">${t("duration")}:</label>
|
||||||
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
|
<input type="number" id="customExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||||
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
|
<select id="customExpirationUnit">
|
||||||
|
<option value="seconds">${t("seconds")}</option>
|
||||||
|
<option value="minutes" selected>${t("minutes")}</option>
|
||||||
|
<option value="hours">${t("hours")}</option>
|
||||||
|
<option value="days">${t("days")}</option>
|
||||||
|
</select>
|
||||||
|
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||||
|
${t("custom_duration_warning")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="sharePassword"
|
||||||
|
placeholder="${t("password_optional")}"
|
||||||
|
style="width:100%;padding:5px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="generateShareLinkBtn"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="margin-top:15px;"
|
||||||
|
>
|
||||||
|
${t("generate_share_link")}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="shareLinkDisplay" style="margin-top:15px;display:none;">
|
||||||
<p>${t("shareable_link")}</p>
|
<p>${t("shareable_link")}</p>
|
||||||
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
|
<input type="text" id="shareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||||
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("copy_link")}</button>
|
<button id="copyShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||||
|
${t("copy_link")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,52 +73,72 @@ export function openShareModal(file, folder) {
|
|||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.style.display = "block";
|
modal.style.display = "block";
|
||||||
|
|
||||||
document.getElementById("closeShareModal").addEventListener("click", () => {
|
// Close handler
|
||||||
modal.remove();
|
document.getElementById("closeShareModal")
|
||||||
});
|
.addEventListener("click", () => modal.remove());
|
||||||
|
|
||||||
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
|
// Show/hide custom-duration inputs
|
||||||
const expiration = document.getElementById("shareExpiration").value;
|
document.getElementById("shareExpiration")
|
||||||
const password = document.getElementById("sharePassword").value;
|
.addEventListener("change", e => {
|
||||||
fetch("/api/file/createShareLink.php", {
|
const container = document.getElementById("customExpirationContainer");
|
||||||
method: "POST",
|
container.style.display = e.target.value === "custom" ? "block" : "none";
|
||||||
credentials: "include",
|
});
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
// Generate share link
|
||||||
"X-CSRF-Token": window.csrfToken
|
document.getElementById("generateShareLinkBtn")
|
||||||
},
|
.addEventListener("click", () => {
|
||||||
body: JSON.stringify({
|
const sel = document.getElementById("shareExpiration");
|
||||||
folder: folder,
|
let value, unit;
|
||||||
file: file.name,
|
|
||||||
expirationMinutes: parseInt(expiration),
|
if (sel.value === "custom") {
|
||||||
password: password
|
value = parseInt(document.getElementById("customExpirationValue").value, 10);
|
||||||
|
unit = document.getElementById("customExpirationUnit").value;
|
||||||
|
} else {
|
||||||
|
value = parseInt(sel.value, 10);
|
||||||
|
unit = "minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = document.getElementById("sharePassword").value;
|
||||||
|
|
||||||
|
fetch("/api/file/createShareLink.php", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-Token": window.csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
folder,
|
||||||
|
file: file.name,
|
||||||
|
expirationValue: value,
|
||||||
|
expirationUnit: unit,
|
||||||
|
password
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.then(res => res.json())
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
const shareEndpoint = `${window.location.origin}/api/file/share.php`;
|
const url = `${window.location.origin}/api/file/share.php?token=${encodeURIComponent(data.token)}`;
|
||||||
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
|
document.getElementById("shareLinkInput").value = url;
|
||||||
const displayDiv = document.getElementById("shareLinkDisplay");
|
document.getElementById("shareLinkDisplay").style.display = "block";
|
||||||
const inputField = document.getElementById("shareLinkInput");
|
|
||||||
inputField.value = shareUrl;
|
|
||||||
displayDiv.style.display = "block";
|
|
||||||
} else {
|
} else {
|
||||||
showToast("Error generating share link: " + (data.error || "Unknown error"));
|
showToast(t("error_generating_share") + ": " + (data.error||"Unknown"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error generating share link:", err);
|
console.error(err);
|
||||||
showToast("Error generating share link.");
|
showToast(t("error_generating_share"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
|
// Copy to clipboard
|
||||||
const input = document.getElementById("shareLinkInput");
|
document.getElementById("copyShareLinkBtn")
|
||||||
input.select();
|
.addEventListener("click", () => {
|
||||||
document.execCommand("copy");
|
const input = document.getElementById("shareLinkInput");
|
||||||
showToast("Link copied to clipboard!");
|
input.select();
|
||||||
});
|
document.execCommand("copy");
|
||||||
|
showToast(t("link_copied"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewFile(fileUrl, fileName) {
|
export function previewFile(fileUrl, fileName) {
|
||||||
@@ -364,16 +416,21 @@ export function previewFile(fileUrl, fileName) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle non-image file previews.
|
// Handle non-image file previews.
|
||||||
if (extension === "pdf") {
|
if (extension === "pdf") {
|
||||||
const embed = document.createElement("embed");
|
// build a cache‐busted URL
|
||||||
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
|
const separator = fileUrl.includes('?') ? '&' : '?';
|
||||||
embed.src = fileUrl + separator + 't=' + new Date().getTime();
|
const urlWithTs = fileUrl + separator + 't=' + Date.now();
|
||||||
embed.type = "application/pdf";
|
|
||||||
embed.style.width = "80vw";
|
// open in a new tab (avoids CSP frame-ancestors)
|
||||||
embed.style.height = "80vh";
|
window.open(urlWithTs, "_blank");
|
||||||
embed.style.border = "none";
|
|
||||||
container.appendChild(embed);
|
// tear down the just-created modal
|
||||||
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
const modal = document.getElementById("filePreviewModal");
|
||||||
|
if (modal) modal.remove();
|
||||||
|
|
||||||
|
// stop further preview logic
|
||||||
|
return;
|
||||||
|
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
video.src = fileUrl;
|
video.src = fileUrl;
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,44 +1,75 @@
|
|||||||
// folderShareModal.js
|
// js/folderShareModal.js
|
||||||
import { escapeHTML, showToast } from './domUtils.js';
|
import { escapeHTML, showToast } from './domUtils.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|
||||||
export function openFolderShareModal(folder) {
|
export function openFolderShareModal(folder) {
|
||||||
// Remove any existing folder share modal
|
// Remove any existing modal
|
||||||
const existing = document.getElementById("folderShareModal");
|
const existing = document.getElementById("folderShareModal");
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
// Create the modal container
|
// Build modal
|
||||||
const modal = document.createElement("div");
|
const modal = document.createElement("div");
|
||||||
modal.id = "folderShareModal";
|
modal.id = "folderShareModal";
|
||||||
modal.classList.add("modal");
|
modal.classList.add("modal");
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-content share-modal-content" style="width: 600px; max-width: 90vw;">
|
<div class="modal-content share-modal-content" style="width:600px;max-width:90vw;">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
|
<h3>${t("share_folder")}: ${escapeHTML(folder)}</h3>
|
||||||
<span class="close-image-modal" id="closeFolderShareModal" title="Close">×</span>
|
<span id="closeFolderShareModal" title="${t("close")}" class="close-image-modal">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>${t("set_expiration")}</p>
|
<p>${t("set_expiration")}</p>
|
||||||
<select id="folderShareExpiration">
|
<select id="folderShareExpiration" style="width:100%;padding:5px;">
|
||||||
<option value="30">30 ${t("minutes")}</option>
|
<option value="30">30 ${t("minutes")}</option>
|
||||||
<option value="60" selected>60 ${t("minutes")}</option>
|
<option value="60" selected>60 ${t("minutes")}</option>
|
||||||
<option value="120">120 ${t("minutes")}</option>
|
<option value="120">120 ${t("minutes")}</option>
|
||||||
<option value="180">180 ${t("minutes")}</option>
|
<option value="180">180 ${t("minutes")}</option>
|
||||||
<option value="240">240 ${t("minutes")}</option>
|
<option value="240">240 ${t("minutes")}</option>
|
||||||
<option value="1440">1 ${t("day")}</option>
|
<option value="1440">1 ${t("day")}</option>
|
||||||
|
<option value="custom">${t("custom")}…</option>
|
||||||
</select>
|
</select>
|
||||||
<p>${t("password_optional")}</p>
|
|
||||||
<input type="text" id="folderSharePassword" placeholder="${t("enter_password")}" style="width: 100%;"/>
|
<div id="customFolderExpirationContainer" style="display:none;margin-top:10px;">
|
||||||
<br>
|
<label for="customFolderExpirationValue">${t("duration")}:</label>
|
||||||
<label>
|
<input type="number" id="customFolderExpirationValue" min="1" value="1" style="width:60px;margin:0 8px;"/>
|
||||||
<input type="checkbox" id="folderShareAllowUpload"> ${t("allow_uploads")}
|
<select id="customFolderExpirationUnit">
|
||||||
|
<option value="seconds">${t("seconds")}</option>
|
||||||
|
<option value="minutes" selected>${t("minutes")}</option>
|
||||||
|
<option value="hours">${t("hours")}</option>
|
||||||
|
<option value="days">${t("days")}</option>
|
||||||
|
</select>
|
||||||
|
<p class="share-warning" style="color:#a33;font-size:0.9em;margin-top:5px;">
|
||||||
|
${t("custom_duration_warning")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:15px;">${t("password_optional")}</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="folderSharePassword"
|
||||||
|
placeholder="${t("enter_password")}"
|
||||||
|
style="width:100%;padding:5px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label style="margin-top:10px;display:block;">
|
||||||
|
<input type="checkbox" id="folderShareAllowUpload" />
|
||||||
|
${t("allow_uploads")}
|
||||||
</label>
|
</label>
|
||||||
<br><br>
|
|
||||||
<button id="generateFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 10px;">${t("generate_share_link")}</button>
|
<button
|
||||||
<div id="folderShareLinkDisplay" style="margin-top: 10px; display: none;">
|
id="generateFolderShareLinkBtn"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="margin-top:15px;"
|
||||||
|
>
|
||||||
|
${t("generate_share_link")}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="folderShareLinkDisplay" style="margin-top:15px;display:none;">
|
||||||
<p>${t("shareable_link")}</p>
|
<p>${t("shareable_link")}</p>
|
||||||
<input type="text" id="folderShareLinkInput" readonly style="width: 100%;"/>
|
<input type="text" id="folderShareLinkInput" readonly style="width:100%;padding:5px;"/>
|
||||||
<button id="copyFolderShareLinkBtn" class="btn btn-primary" style="margin-top: 5px;">${t("copy_link")}</button>
|
<button id="copyFolderShareLinkBtn" class="btn btn-secondary" style="margin-top:5px;">
|
||||||
|
${t("copy_link")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,62 +77,75 @@ export function openFolderShareModal(folder) {
|
|||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.style.display = "block";
|
modal.style.display = "block";
|
||||||
|
|
||||||
// Close button handler
|
// Close
|
||||||
document.getElementById("closeFolderShareModal").addEventListener("click", () => {
|
document.getElementById("closeFolderShareModal")
|
||||||
modal.remove();
|
.addEventListener("click", () => modal.remove());
|
||||||
});
|
|
||||||
|
|
||||||
// Handler for generating the share link
|
// Toggle custom inputs
|
||||||
document.getElementById("generateFolderShareLinkBtn").addEventListener("click", () => {
|
document.getElementById("folderShareExpiration")
|
||||||
const expiration = document.getElementById("folderShareExpiration").value;
|
.addEventListener("change", e => {
|
||||||
const password = document.getElementById("folderSharePassword").value;
|
document.getElementById("customFolderExpirationContainer")
|
||||||
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
.style.display = e.target.value === "custom" ? "block" : "none";
|
||||||
|
});
|
||||||
|
|
||||||
// Retrieve the CSRF token from the meta tag.
|
// Generate link
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
document.getElementById("generateFolderShareLinkBtn")
|
||||||
if (!csrfToken) {
|
.addEventListener("click", () => {
|
||||||
showToast(t("csrf_error"));
|
const sel = document.getElementById("folderShareExpiration");
|
||||||
return;
|
let value, unit;
|
||||||
}
|
if (sel.value === "custom") {
|
||||||
// Post to the createFolderShareLink endpoint.
|
value = parseInt(document.getElementById("customFolderExpirationValue").value, 10);
|
||||||
fetch("/api/folder/createShareFolderLink.php", {
|
unit = document.getElementById("customFolderExpirationUnit").value;
|
||||||
method: "POST",
|
} else {
|
||||||
credentials: "include",
|
value = parseInt(sel.value, 10);
|
||||||
headers: {
|
unit = "minutes";
|
||||||
"Content-Type": "application/json",
|
}
|
||||||
"X-CSRF-Token": csrfToken
|
|
||||||
},
|
const password = document.getElementById("folderSharePassword").value;
|
||||||
body: JSON.stringify({
|
const allowUpload = document.getElementById("folderShareAllowUpload").checked ? 1 : 0;
|
||||||
folder: folder,
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
||||||
expirationMinutes: parseInt(expiration, 10),
|
if (!csrfToken) {
|
||||||
password: password,
|
showToast(t("csrf_error"));
|
||||||
allowUpload: allowUpload
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("/api/folder/createShareFolderLink.php", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-Token": csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
folder,
|
||||||
|
expirationValue: value,
|
||||||
|
expirationUnit: unit,
|
||||||
|
password,
|
||||||
|
allowUpload
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.then(r => r.json())
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.token && data.link) {
|
if (data.token && data.link) {
|
||||||
const shareUrl = data.link;
|
document.getElementById("folderShareLinkInput").value = data.link;
|
||||||
const displayDiv = document.getElementById("folderShareLinkDisplay");
|
document.getElementById("folderShareLinkDisplay").style.display = "block";
|
||||||
const inputField = document.getElementById("folderShareLinkInput");
|
|
||||||
inputField.value = shareUrl;
|
|
||||||
displayDiv.style.display = "block";
|
|
||||||
showToast(t("share_link_generated"));
|
showToast(t("share_link_generated"));
|
||||||
} else {
|
} else {
|
||||||
showToast(t("error_generating_share_link") + ": " + (data.error || t("unknown_error")));
|
showToast(t("error_generating_share_link") + ": " + (data.error||t("unknown_error")));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Error generating folder share link:", err);
|
console.error(err);
|
||||||
showToast(t("error_generating_share_link") + ": " + (err.error || t("unknown_error")));
|
showToast(t("error_generating_share_link") + ": " + t("unknown_error"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy share link button handler
|
// Copy
|
||||||
document.getElementById("copyFolderShareLinkBtn").addEventListener("click", () => {
|
document.getElementById("copyFolderShareLinkBtn")
|
||||||
const input = document.getElementById("folderShareLinkInput");
|
.addEventListener("click", () => {
|
||||||
input.select();
|
const inp = document.getElementById("folderShareLinkInput");
|
||||||
document.execCommand("copy");
|
inp.select();
|
||||||
showToast(t("link_copied"));
|
document.execCommand("copy");
|
||||||
});
|
showToast(t("link_copied"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
@@ -150,6 +151,13 @@ const translations = {
|
|||||||
"allow_uploads": "Allow Uploads",
|
"allow_uploads": "Allow Uploads",
|
||||||
"share_link_generated": "Share Link Generated",
|
"share_link_generated": "Share Link Generated",
|
||||||
"error_generating_share_link": "Error Generating Share Link",
|
"error_generating_share_link": "Error Generating Share Link",
|
||||||
|
"custom": "Custom",
|
||||||
|
"duration": "Duration",
|
||||||
|
"seconds": "Seconds",
|
||||||
|
"minutes": "Minutes",
|
||||||
|
"hours": "Hours",
|
||||||
|
"days": "Days",
|
||||||
|
"custom_duration_warning": "⚠️ Using a long expiration may pose security risks. Use with caution.",
|
||||||
|
|
||||||
// Folder
|
// Folder
|
||||||
"folder_share": "Share Folder",
|
"folder_share": "Share Folder",
|
||||||
@@ -166,12 +174,8 @@ const translations = {
|
|||||||
"user": "User:",
|
"user": "User:",
|
||||||
"unknown_error": "Unknown Error",
|
"unknown_error": "Unknown Error",
|
||||||
"link_copied": "Link Copied to Clipboard",
|
"link_copied": "Link Copied to Clipboard",
|
||||||
"minutes": "minutes",
|
|
||||||
"hours": "hours",
|
|
||||||
"days": "days",
|
|
||||||
"weeks": "weeks",
|
"weeks": "weeks",
|
||||||
"months": "months",
|
"months": "months",
|
||||||
"seconds": "seconds",
|
|
||||||
|
|
||||||
// Dark Mode Toggle
|
// Dark Mode Toggle
|
||||||
"dark_mode_toggle": "Dark Mode",
|
"dark_mode_toggle": "Dark Mode",
|
||||||
@@ -179,6 +183,22 @@ 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",
|
||||||
@@ -239,7 +259,7 @@ const translations = {
|
|||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
"items_per_page": "items per page",
|
"items_per_page": "items per page",
|
||||||
"columns":"Columns",
|
"columns": "Columns",
|
||||||
"api_docs": "API Docs"
|
"api_docs": "API Docs"
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
@@ -297,6 +317,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,6 +14,28 @@ 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() {
|
||||||
|
window.currentFolder = "root";
|
||||||
|
initTagSearch();
|
||||||
|
loadFileList(window.currentFolder);
|
||||||
|
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', {
|
||||||
@@ -100,30 +122,8 @@ 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";
|
document.getElementById('loadingOverlay').remove();
|
||||||
initTagSearch();
|
initializeApp();
|
||||||
loadFileList(window.currentFolder);
|
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,17 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
let viewMode = 'list';
|
let viewMode = 'list';
|
||||||
const payload = JSON.parse(
|
const payload = JSON.parse(
|
||||||
document.getElementById('shared-data').textContent
|
document.getElementById('shared-data').textContent
|
||||||
);
|
);
|
||||||
const token = payload.token;
|
const token = payload.token;
|
||||||
const filesData = payload.files;
|
const filesData = payload.files;
|
||||||
const downloadBase = `${window.location.origin}/api/folder/downloadSharedFile.php?token=${encodeURIComponent(token)}&file=`;
|
const downloadBase = `${window.location.origin}/api/folder/downloadSharedFile.php?token=${encodeURIComponent(token)}&file=`;
|
||||||
|
const btn = document.getElementById('toggleBtn');
|
||||||
|
if (btn) btn.classList.add('toggle-btn');
|
||||||
|
|
||||||
function toggleViewMode() {
|
function toggleViewMode() {
|
||||||
const listEl = document.getElementById('listViewContainer');
|
const listEl = document.getElementById('listViewContainer');
|
||||||
const galleryEl = document.getElementById('galleryViewContainer');
|
const galleryEl = document.getElementById('galleryViewContainer');
|
||||||
const btn = document.getElementById('toggleBtn');
|
|
||||||
if (btn) {
|
|
||||||
btn.classList.add('toggle-btn');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewMode === 'list') {
|
if (viewMode === 'list') {
|
||||||
viewMode = 'gallery';
|
viewMode = 'gallery';
|
||||||
@@ -30,30 +28,62 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('toggleBtn').addEventListener('click', toggleViewMode);
|
btn.addEventListener('click', toggleViewMode);
|
||||||
|
|
||||||
function renderGalleryView() {
|
function renderGalleryView() {
|
||||||
const galleryContainer = document.getElementById('galleryViewContainer');
|
const container = document.getElementById('galleryViewContainer');
|
||||||
let html = '<div class="shared-gallery-container">';
|
// clear previous
|
||||||
|
while (container.firstChild) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
const grid = document.createElement('div');
|
||||||
|
grid.className = 'shared-gallery-container';
|
||||||
|
|
||||||
filesData.forEach(file => {
|
filesData.forEach(file => {
|
||||||
const url = downloadBase + encodeURIComponent(file);
|
const url = downloadBase + encodeURIComponent(file);
|
||||||
const ext = file.split('.').pop().toLowerCase();
|
const ext = file.split('.').pop().toLowerCase();
|
||||||
const thumb = /^(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/.test(ext)
|
const isImg = /^(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/.test(ext);
|
||||||
? `<img src="${url}" alt="${file}">`
|
|
||||||
: `<span class="material-icons">insert_drive_file</span>`;
|
|
||||||
html += `
|
|
||||||
<div class="shared-gallery-card">
|
|
||||||
<div class="gallery-preview" data-url="${url}" style="cursor:pointer;">${thumb}</div>
|
|
||||||
<div class="gallery-info"><span class="gallery-file-name">${file}</span></div>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
galleryContainer.innerHTML = html;
|
|
||||||
|
|
||||||
galleryContainer.querySelectorAll('.gallery-preview')
|
// card
|
||||||
.forEach(el => el.addEventListener('click', () => {
|
const card = document.createElement('div');
|
||||||
window.location.href = el.dataset.url;
|
card.className = 'shared-gallery-card';
|
||||||
}));
|
|
||||||
|
// preview
|
||||||
|
const preview = document.createElement('div');
|
||||||
|
preview.className = 'gallery-preview';
|
||||||
|
preview.style.cursor = 'pointer';
|
||||||
|
preview.dataset.url = url;
|
||||||
|
|
||||||
|
if (isImg) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.alt = file; // safe, file is not HTML
|
||||||
|
preview.appendChild(img);
|
||||||
|
} else {
|
||||||
|
const icon = document.createElement('span');
|
||||||
|
icon.className = 'material-icons';
|
||||||
|
icon.textContent = 'insert_drive_file';
|
||||||
|
preview.appendChild(icon);
|
||||||
|
}
|
||||||
|
card.appendChild(preview);
|
||||||
|
|
||||||
|
// info
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.className = 'gallery-info';
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'gallery-file-name';
|
||||||
|
nameSpan.textContent = file; // textContent escapes any HTML
|
||||||
|
info.appendChild(nameSpan);
|
||||||
|
card.appendChild(info);
|
||||||
|
|
||||||
|
grid.appendChild(card);
|
||||||
|
|
||||||
|
preview.addEventListener('click', () => {
|
||||||
|
window.location.href = preview.dataset.url;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.renderGalleryView = renderGalleryView;
|
window.renderGalleryView = renderGalleryView;
|
||||||
|
|||||||
|
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)
|
// TOTP enabled? (same logic as below)
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
$totp = false;
|
$totp = false;
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
$parts = explode(':', trim($line));
|
$parts = explode(':', trim($line));
|
||||||
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
||||||
$totp = true;
|
$totp = true;
|
||||||
break;
|
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;
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<?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';
|
||||||
|
|
||||||
class FileController {
|
class FileController
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/copyFiles.php",
|
* path="/api/file/copyFiles.php",
|
||||||
@@ -50,7 +51,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function copyFiles() {
|
public function copyFiles()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
@@ -153,7 +155,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function deleteFiles() {
|
public function deleteFiles()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
@@ -201,7 +204,7 @@ class FileController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/moveFiles.php",
|
* path="/api/file/moveFiles.php",
|
||||||
* summary="Move files between folders",
|
* summary="Move files between folders",
|
||||||
@@ -246,7 +249,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function moveFiles() {
|
public function moveFiles()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
@@ -304,7 +308,7 @@ class FileController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/renameFile.php",
|
* path="/api/file/renameFile.php",
|
||||||
* summary="Rename a file",
|
* summary="Rename a file",
|
||||||
@@ -346,7 +350,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs a JSON response.
|
* @return void Outputs a JSON response.
|
||||||
*/
|
*/
|
||||||
public function renameFile() {
|
public function renameFile()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||||
header("Pragma: no-cache");
|
header("Pragma: no-cache");
|
||||||
@@ -405,7 +410,7 @@ class FileController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/saveFile.php",
|
* path="/api/file/saveFile.php",
|
||||||
* summary="Save a file",
|
* summary="Save a file",
|
||||||
@@ -446,7 +451,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs a JSON response.
|
* @return void Outputs a JSON response.
|
||||||
*/
|
*/
|
||||||
public function saveFile() {
|
public function saveFile()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
@@ -555,7 +561,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs file content with appropriate headers.
|
* @return void Outputs file content with appropriate headers.
|
||||||
*/
|
*/
|
||||||
public function downloadFile() {
|
public function downloadFile()
|
||||||
|
{
|
||||||
// Check if the user is authenticated.
|
// Check if the user is authenticated.
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
@@ -578,7 +585,7 @@ class FileController {
|
|||||||
// Retrieve download info from the model.
|
// Retrieve download info from the model.
|
||||||
$downloadInfo = FileModel::getDownloadInfo($folder, $file);
|
$downloadInfo = FileModel::getDownloadInfo($folder, $file);
|
||||||
if (isset($downloadInfo['error'])) {
|
if (isset($downloadInfo['error'])) {
|
||||||
http_response_code( (in_array($downloadInfo['error'], ["File not found.", "Access forbidden."])) ? 404 : 400 );
|
http_response_code((in_array($downloadInfo['error'], ["File not found.", "Access forbidden."])) ? 404 : 400);
|
||||||
echo json_encode(["error" => $downloadInfo['error']]);
|
echo json_encode(["error" => $downloadInfo['error']]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -601,7 +608,7 @@ class FileController {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/downloadZip.php",
|
* path="/api/file/downloadZip.php",
|
||||||
* summary="Download a ZIP archive of selected files",
|
* summary="Download a ZIP archive of selected files",
|
||||||
@@ -649,7 +656,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs the ZIP file for download.
|
* @return void Outputs the ZIP file for download.
|
||||||
*/
|
*/
|
||||||
public function downloadZip() {
|
public function downloadZip()
|
||||||
|
{
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
$receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : '';
|
||||||
@@ -723,7 +731,7 @@ class FileController {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/extractZip.php",
|
* path="/api/file/extractZip.php",
|
||||||
* summary="Extract ZIP files",
|
* summary="Extract ZIP files",
|
||||||
@@ -768,7 +776,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function extractZip() {
|
public function extractZip()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// --- CSRF Protection ---
|
// --- CSRF Protection ---
|
||||||
@@ -815,7 +824,7 @@ class FileController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Get(
|
* @OA\Get(
|
||||||
* path="/api/file/share.php",
|
* path="/api/file/share.php",
|
||||||
* summary="Access a shared file",
|
* summary="Access a shared file",
|
||||||
@@ -860,7 +869,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs either HTML (password form) or serves the file.
|
* @return void Outputs either HTML (password form) or serves the file.
|
||||||
*/
|
*/
|
||||||
public function shareFile() {
|
public function shareFile()
|
||||||
|
{
|
||||||
// Retrieve and sanitize GET parameters.
|
// Retrieve and sanitize GET parameters.
|
||||||
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
|
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
|
||||||
$providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING);
|
$providedPass = filter_input(INPUT_GET, 'pass', FILTER_SANITIZE_STRING);
|
||||||
@@ -892,9 +902,10 @@ class FileController {
|
|||||||
// If a password is required and not provided, show an HTML form.
|
// If a password is required and not provided, show an HTML form.
|
||||||
if (!empty($record['password']) && empty($providedPass)) {
|
if (!empty($record['password']) && empty($providedPass)) {
|
||||||
header("Content-Type: text/html; charset=utf-8");
|
header("Content-Type: text/html; charset=utf-8");
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@@ -906,14 +917,16 @@ class FileController {
|
|||||||
background-color: #f4f4f4;
|
background-color: #f4f4f4;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 40px auto;
|
margin: 40px auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="password"] {
|
input[type="password"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -921,6 +934,7 @@ class FileController {
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: #007BFF;
|
background: #007BFF;
|
||||||
@@ -929,11 +943,13 @@ class FileController {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background: #0056b3;
|
background: #0056b3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h2>This file is protected by a password.</h2>
|
<h2>This file is protected by a password.</h2>
|
||||||
<form method="get" action="/api/file/share.php">
|
<form method="get" action="/api/file/share.php">
|
||||||
@@ -943,8 +959,9 @@ class FileController {
|
|||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
<?php
|
<?php
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +1003,7 @@ class FileController {
|
|||||||
$mimeType = mime_content_type($realFilePath);
|
$mimeType = mime_content_type($realFilePath);
|
||||||
header("Content-Type: " . $mimeType);
|
header("Content-Type: " . $mimeType);
|
||||||
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'])) {
|
||||||
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
|
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
|
||||||
} else {
|
} else {
|
||||||
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
||||||
@@ -999,20 +1016,26 @@ class FileController {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/createShareLink.php",
|
* path="/api/file/createShareLink.php",
|
||||||
* summary="Create a share link for a file",
|
* summary="Create a share link for a file",
|
||||||
* description="Generates a secure share link token for a specific file with an optional password protection and expiration time.",
|
* description="Generates a secure share link token for a specific file with optional password protection and a custom expiration time.",
|
||||||
* operationId="createShareLink",
|
* operationId="createShareLink",
|
||||||
* tags={"Files"},
|
* tags={"Files"},
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
* required={"folder", "file"},
|
* required={"folder", "file", "expirationValue", "expirationUnit"},
|
||||||
* @OA\Property(property="folder", type="string", example="Documents"),
|
* @OA\Property(property="folder", type="string", example="Documents"),
|
||||||
* @OA\Property(property="file", type="string", example="report.pdf"),
|
* @OA\Property(property="file", type="string", example="report.pdf"),
|
||||||
* @OA\Property(property="expirationMinutes", type="integer", example=60),
|
* @OA\Property(property="expirationValue", type="integer", example=1),
|
||||||
|
* @OA\Property(
|
||||||
|
* property="expirationUnit",
|
||||||
|
* type="string",
|
||||||
|
* enum={"seconds","minutes","hours","days"},
|
||||||
|
* example="minutes"
|
||||||
|
* ),
|
||||||
* @OA\Property(property="password", type="string", example="secret")
|
* @OA\Property(property="password", type="string", example="secret")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
@@ -1042,7 +1065,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function createShareLink() {
|
public function createShareLink()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// Ensure user is authenticated.
|
||||||
@@ -1055,7 +1079,7 @@ class FileController {
|
|||||||
// Check user permissions.
|
// Check user permissions.
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if ($username && !empty($userPermissions['readOnly'])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Read-only users are not allowed to create share links."]);
|
echo json_encode(["error" => "Read-only users are not allowed to create share links."]);
|
||||||
exit;
|
exit;
|
||||||
@@ -1071,23 +1095,42 @@ class FileController {
|
|||||||
|
|
||||||
// Extract parameters.
|
// Extract parameters.
|
||||||
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
$folder = isset($input['folder']) ? trim($input['folder']) : "";
|
||||||
$file = isset($input['file']) ? basename($input['file']) : "";
|
$file = isset($input['file']) ? basename($input['file']) : "";
|
||||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
$value = isset($input['expirationValue']) ? intval($input['expirationValue']) : 60;
|
||||||
|
$unit = isset($input['expirationUnit']) ? $input['expirationUnit'] : 'minutes';
|
||||||
$password = isset($input['password']) ? $input['password'] : "";
|
$password = isset($input['password']) ? $input['password'] : "";
|
||||||
|
|
||||||
// Validate folder.
|
// Validate folder name.
|
||||||
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid folder name."]);
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert the provided value+unit into seconds
|
||||||
|
switch ($unit) {
|
||||||
|
case 'seconds':
|
||||||
|
$expirationSeconds = $value;
|
||||||
|
break;
|
||||||
|
case 'hours':
|
||||||
|
$expirationSeconds = $value * 3600;
|
||||||
|
break;
|
||||||
|
case 'days':
|
||||||
|
$expirationSeconds = $value * 86400;
|
||||||
|
break;
|
||||||
|
case 'minutes':
|
||||||
|
default:
|
||||||
|
$expirationSeconds = $value * 60;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Delegate share link creation to the model.
|
// Delegate share link creation to the model.
|
||||||
$result = FileModel::createShareLink($folder, $file, $expirationMinutes, $password);
|
$result = FileModel::createShareLink($folder, $file, $expirationSeconds, $password);
|
||||||
|
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Get(
|
* @OA\Get(
|
||||||
* path="/api/file/getTrashItems.php",
|
* path="/api/file/getTrashItems.php",
|
||||||
* summary="Get trash items",
|
* summary="Get trash items",
|
||||||
@@ -1109,7 +1152,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response with trash items.
|
* @return void Outputs JSON response with trash items.
|
||||||
*/
|
*/
|
||||||
public function getTrashItems() {
|
public function getTrashItems()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// Ensure user is authenticated.
|
||||||
@@ -1164,7 +1208,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function restoreFiles() {
|
public function restoreFiles()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// CSRF Protection.
|
// CSRF Protection.
|
||||||
@@ -1196,7 +1241,7 @@ class FileController {
|
|||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/deleteTrashFiles.php",
|
* path="/api/file/deleteTrashFiles.php",
|
||||||
* summary="Delete trash files",
|
* summary="Delete trash files",
|
||||||
@@ -1247,7 +1292,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs a JSON response.
|
* @return void Outputs a JSON response.
|
||||||
*/
|
*/
|
||||||
public function deleteTrashFiles() {
|
public function deleteTrashFiles()
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// CSRF Protection.
|
// CSRF Protection.
|
||||||
@@ -1306,7 +1352,7 @@ class FileController {
|
|||||||
// Build a human‑friendly success or error message
|
// Build a human‑friendly success or error message
|
||||||
if (!empty($result['deleted'])) {
|
if (!empty($result['deleted'])) {
|
||||||
$count = count($result['deleted']);
|
$count = count($result['deleted']);
|
||||||
$msg = "Trash item" . ($count===1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
|
$msg = "Trash item" . ($count === 1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
|
||||||
echo json_encode(["success" => $msg]);
|
echo json_encode(["success" => $msg]);
|
||||||
} elseif (!empty($result['error'])) {
|
} elseif (!empty($result['error'])) {
|
||||||
echo json_encode(["error" => $result['error']]);
|
echo json_encode(["error" => $result['error']]);
|
||||||
@@ -1316,7 +1362,7 @@ class FileController {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Get(
|
* @OA\Get(
|
||||||
* path="/api/file/getFileTag.php",
|
* path="/api/file/getFileTag.php",
|
||||||
* summary="Retrieve file tags",
|
* summary="Retrieve file tags",
|
||||||
@@ -1337,7 +1383,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response with file tags.
|
* @return void Outputs JSON response with file tags.
|
||||||
*/
|
*/
|
||||||
public function getFileTags(): void {
|
public function getFileTags(): void
|
||||||
|
{
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
$tags = FileModel::getFileTags();
|
$tags = FileModel::getFileTags();
|
||||||
@@ -1345,7 +1392,7 @@ class FileController {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @OA\Post(
|
* @OA\Post(
|
||||||
* path="/api/file/saveFileTag.php",
|
* path="/api/file/saveFileTag.php",
|
||||||
* summary="Save file tags",
|
* summary="Save file tags",
|
||||||
@@ -1397,7 +1444,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function saveFileTag(): void {
|
public function saveFileTag(): void
|
||||||
|
{
|
||||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||||
header("Pragma: no-cache");
|
header("Pragma: no-cache");
|
||||||
header("Expires: 0");
|
header("Expires: 0");
|
||||||
@@ -1496,7 +1544,8 @@ class FileController {
|
|||||||
*
|
*
|
||||||
* @return void Outputs JSON response.
|
* @return void Outputs JSON response.
|
||||||
*/
|
*/
|
||||||
public function getFileList(): void {
|
public function getFileList(): void
|
||||||
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// Ensure user is authenticated.
|
||||||
@@ -1522,4 +1571,59 @@ 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,7 +565,7 @@ class FolderController
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -847,38 +851,63 @@ class FolderController
|
|||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Ensure user is authenticated.
|
// Auth check
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (empty($_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the user is not read-only.
|
// Read-only check
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
$userPermissions = loadUserPermissions($username);
|
$perms = loadUserPermissions($username);
|
||||||
if ($username && isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if ($username && !empty($perms['readOnly'])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
echo json_encode(["error" => "Read-only users are not allowed to create share folders."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve and decode POST input.
|
// Input
|
||||||
$input = json_decode(file_get_contents("php://input"), true);
|
$in = json_decode(file_get_contents("php://input"), true);
|
||||||
if (!$input || !isset($input['folder'])) {
|
if (!$in || !isset($in['folder'])) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Invalid input."]);
|
echo json_encode(["error" => "Invalid input."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$folder = trim($input['folder']);
|
$folder = trim($in['folder']);
|
||||||
$expirationMinutes = isset($input['expirationMinutes']) ? intval($input['expirationMinutes']) : 60;
|
$value = isset($in['expirationValue']) ? intval($in['expirationValue']) : 60;
|
||||||
$password = isset($input['password']) ? $input['password'] : "";
|
$unit = $in['expirationUnit'] ?? 'minutes';
|
||||||
$allowUpload = isset($input['allowUpload']) ? intval($input['allowUpload']) : 0;
|
$password = $in['password'] ?? '';
|
||||||
|
$allowUpload = intval($in['allowUpload'] ?? 0);
|
||||||
|
|
||||||
// Delegate to the model.
|
// Folder name validation
|
||||||
$result = FolderModel::createShareFolderLink($folder, $expirationMinutes, $password, $allowUpload);
|
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
echo json_encode($result);
|
http_response_code(400);
|
||||||
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to seconds
|
||||||
|
switch ($unit) {
|
||||||
|
case 'seconds':
|
||||||
|
$seconds = $value;
|
||||||
|
break;
|
||||||
|
case 'hours':
|
||||||
|
$seconds = $value * 3600;
|
||||||
|
break;
|
||||||
|
case 'days':
|
||||||
|
$seconds = $value * 86400;
|
||||||
|
break;
|
||||||
|
case 'minutes':
|
||||||
|
default:
|
||||||
|
$seconds = $value * 60;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate
|
||||||
|
$res = FolderModel::createShareFolderLink($folder, $seconds, $password, $allowUpload);
|
||||||
|
echo json_encode($res);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1049,4 +1078,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';
|
||||||
@@ -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' => [
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
|||||||
* @return array Returns an associative array with keys "token" and "expires" on success,
|
* @return array Returns an associative array with keys "token" and "expires" on success,
|
||||||
* or "error" on failure.
|
* or "error" on failure.
|
||||||
*/
|
*/
|
||||||
public static function createShareLink($folder, $file, $expirationMinutes = 60, $password = "") {
|
public static function createShareLink($folder, $file, $expirationSeconds = 3600, $password = "") {
|
||||||
// Validate folder if necessary (this can also be done in the controller).
|
// Validate folder if necessary (this can also be done in the controller).
|
||||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name."];
|
return ["error" => "Invalid folder name."];
|
||||||
@@ -746,7 +746,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
|
|||||||
$token = bin2hex(random_bytes(16));
|
$token = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
// Calculate expiration (Unix timestamp).
|
// Calculate expiration (Unix timestamp).
|
||||||
$expires = time() + ($expirationMinutes * 60);
|
$expires = time() + $expirationSeconds;
|
||||||
|
|
||||||
// Hash the password if provided.
|
// Hash the password if provided.
|
||||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||||
@@ -1253,4 +1253,29 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
require_once PROJECT_ROOT . '/config/config.php';
|
require_once PROJECT_ROOT . '/config/config.php';
|
||||||
|
|
||||||
class FolderModel {
|
class FolderModel
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a folder under the specified parent (or in root) and creates an empty metadata file.
|
* Creates a folder under the specified parent (or in root) and creates an empty metadata file.
|
||||||
*
|
*
|
||||||
@@ -12,7 +13,8 @@ class FolderModel {
|
|||||||
* @return array Returns an array with a "success" key if the folder was created,
|
* @return array Returns an array with a "success" key if the folder was created,
|
||||||
* or an "error" key if an error occurred.
|
* or an "error" key if an error occurred.
|
||||||
*/
|
*/
|
||||||
public static function createFolder(string $folderName, string $parent = ""): array {
|
public static function createFolder(string $folderName, string $parent = ""): array
|
||||||
|
{
|
||||||
$folderName = trim($folderName);
|
$folderName = trim($folderName);
|
||||||
$parent = trim($parent);
|
$parent = trim($parent);
|
||||||
|
|
||||||
@@ -57,20 +59,22 @@ class FolderModel {
|
|||||||
* @param string $folder The relative folder path.
|
* @param string $folder The relative folder path.
|
||||||
* @return string The metadata file path.
|
* @return string The metadata file path.
|
||||||
*/
|
*/
|
||||||
private static function getMetadataFilePath(string $folder): string {
|
private static function getMetadataFilePath(string $folder): string
|
||||||
|
{
|
||||||
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
if (strtolower($folder) === 'root' || trim($folder) === '') {
|
||||||
return META_DIR . "root_metadata.json";
|
return META_DIR . "root_metadata.json";
|
||||||
}
|
}
|
||||||
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a folder if it is empty and removes its corresponding metadata.
|
* Deletes a folder if it is empty and removes its corresponding metadata.
|
||||||
*
|
*
|
||||||
* @param string $folder The folder name (relative to the upload directory).
|
* @param string $folder The folder name (relative to the upload directory).
|
||||||
* @return array An associative array with "success" on success or "error" on failure.
|
* @return array An associative array with "success" on success or "error" on failure.
|
||||||
*/
|
*/
|
||||||
public static function deleteFolder(string $folder): array {
|
public static function deleteFolder(string $folder): array
|
||||||
|
{
|
||||||
// Prevent deletion of "root".
|
// Prevent deletion of "root".
|
||||||
if (strtolower($folder) === 'root') {
|
if (strtolower($folder) === 'root') {
|
||||||
return ["error" => "Cannot delete root folder."];
|
return ["error" => "Cannot delete root folder."];
|
||||||
@@ -109,14 +113,15 @@ class FolderModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a folder and updates related metadata files.
|
* Renames a folder and updates related metadata files.
|
||||||
*
|
*
|
||||||
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
* @param string $oldFolder The current folder name (relative to UPLOAD_DIR).
|
||||||
* @param string $newFolder The new folder name.
|
* @param string $newFolder The new folder name.
|
||||||
* @return array Returns an associative array with "success" on success or "error" on failure.
|
* @return array Returns an associative array with "success" on success or "error" on failure.
|
||||||
*/
|
*/
|
||||||
public static function renameFolder(string $oldFolder, string $newFolder): array {
|
public static function renameFolder(string $oldFolder, string $newFolder): array
|
||||||
|
{
|
||||||
// Sanitize and trim folder names.
|
// Sanitize and trim folder names.
|
||||||
$oldFolder = trim($oldFolder, "/\\ ");
|
$oldFolder = trim($oldFolder, "/\\ ");
|
||||||
$newFolder = trim($newFolder, "/\\ ");
|
$newFolder = trim($newFolder, "/\\ ");
|
||||||
@@ -134,7 +139,8 @@ class FolderModel {
|
|||||||
// Validate that the old folder exists and new folder does not.
|
// Validate that the old folder exists and new folder does not.
|
||||||
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
||||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) {
|
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0
|
||||||
|
) {
|
||||||
return ["error" => "Invalid folder path."];
|
return ["error" => "Invalid folder path."];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +177,8 @@ class FolderModel {
|
|||||||
* @param string $relative The relative path from the base directory.
|
* @param string $relative The relative path from the base directory.
|
||||||
* @return array An array of folder paths (relative to the base).
|
* @return array An array of folder paths (relative to the base).
|
||||||
*/
|
*/
|
||||||
private static function getSubfolders(string $dir, string $relative = ''): array {
|
private static function getSubfolders(string $dir, string $relative = ''): array
|
||||||
|
{
|
||||||
$folders = [];
|
$folders = [];
|
||||||
$items = scandir($dir);
|
$items = scandir($dir);
|
||||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||||
@@ -198,7 +205,8 @@ class FolderModel {
|
|||||||
*
|
*
|
||||||
* @return array An array of folder information arrays.
|
* @return array An array of folder information arrays.
|
||||||
*/
|
*/
|
||||||
public static function getFolderList(): array {
|
public static function getFolderList(): array
|
||||||
|
{
|
||||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
$folderInfoList = [];
|
$folderInfoList = [];
|
||||||
|
|
||||||
@@ -240,13 +248,14 @@ class FolderModel {
|
|||||||
return $folderInfoList;
|
return $folderInfoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the share folder record for a given token.
|
* Retrieves the share folder record for a given token.
|
||||||
*
|
*
|
||||||
* @param string $token The share folder token.
|
* @param string $token The share folder token.
|
||||||
* @return array|null The share folder record, or null if not found.
|
* @return array|null The share folder record, or null if not found.
|
||||||
*/
|
*/
|
||||||
public static function getShareFolderRecord(string $token): ?array {
|
public static function getShareFolderRecord(string $token): ?array
|
||||||
|
{
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -258,7 +267,7 @@ class FolderModel {
|
|||||||
return $shareLinks[$token];
|
return $shareLinks[$token];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves shared folder data based on a share token.
|
* Retrieves shared folder data based on a share token.
|
||||||
*
|
*
|
||||||
* @param string $token The share folder token.
|
* @param string $token The share folder token.
|
||||||
@@ -274,7 +283,8 @@ class FolderModel {
|
|||||||
* - 'totalPages': total pages,
|
* - 'totalPages': total pages,
|
||||||
* or an 'error' key on failure.
|
* or an 'error' key on failure.
|
||||||
*/
|
*/
|
||||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array {
|
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array
|
||||||
|
{
|
||||||
// Load the share folder record.
|
// Load the share folder record.
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
@@ -314,7 +324,7 @@ class FolderModel {
|
|||||||
return ["error" => "Shared folder not found."];
|
return ["error" => "Shared folder not found."];
|
||||||
}
|
}
|
||||||
// Scan for files (only files).
|
// Scan for files (only files).
|
||||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
|
$allFiles = array_values(array_filter(scandir($realFolderPath), function ($item) use ($realFolderPath) {
|
||||||
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
||||||
}));
|
}));
|
||||||
sort($allFiles);
|
sort($allFiles);
|
||||||
@@ -334,81 +344,72 @@ class FolderModel {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a share link for a folder.
|
* Creates a share link for a folder.
|
||||||
*
|
*
|
||||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||||
* @param int $expirationMinutes The duration (in minutes) until the link expires.
|
* @param int $expirationSeconds How many seconds until expiry.
|
||||||
* @param string $password Optional password for the share.
|
* @param string $password Optional password.
|
||||||
* @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed.
|
* @param int $allowUpload 0 or 1 whether uploads are allowed.
|
||||||
* @return array An associative array with "token", "expires", and "link" on success, or "error" on failure.
|
* @return array ["token","expires","link"] on success, or ["error"].
|
||||||
*/
|
*/
|
||||||
public static function createShareFolderLink(string $folder, int $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array {
|
public static function createShareFolderLink(string $folder, int $expirationSeconds = 3600, string $password = "", int $allowUpload = 0): array
|
||||||
// Validate folder name.
|
{
|
||||||
|
// Validate folder
|
||||||
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
|
||||||
return ["error" => "Invalid folder name."];
|
return ["error" => "Invalid folder name."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate secure token.
|
// Token
|
||||||
try {
|
try {
|
||||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
$token = bin2hex(random_bytes(16));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
return ["error" => "Could not generate token."];
|
return ["error" => "Could not generate token."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expiration time.
|
// Expiry
|
||||||
$expires = time() + ($expirationMinutes * 60);
|
$expires = time() + $expirationSeconds;
|
||||||
|
|
||||||
// Hash the password if provided.
|
// Password hash
|
||||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
$hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||||
|
|
||||||
// Define the share folder links file.
|
// Load existing
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
$shareLinks = [];
|
$links = file_exists($shareFile)
|
||||||
if (file_exists($shareFile)) {
|
? json_decode(file_get_contents($shareFile), true) ?? []
|
||||||
$data = file_get_contents($shareFile);
|
: [];
|
||||||
$shareLinks = json_decode($data, true);
|
|
||||||
if (!is_array($shareLinks)) {
|
// Cleanup
|
||||||
$shareLinks = [];
|
$now = time();
|
||||||
|
foreach ($links as $k => $v) {
|
||||||
|
if (!empty($v['expires']) && $v['expires'] < $now) {
|
||||||
|
unset($links[$k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up expired share links.
|
// Add new
|
||||||
$currentTime = time();
|
$links[$token] = [
|
||||||
foreach ($shareLinks as $key => $link) {
|
"folder" => $folder,
|
||||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
"expires" => $expires,
|
||||||
unset($shareLinks[$key]);
|
"password" => $hashedPassword,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new share record.
|
|
||||||
$shareLinks[$token] = [
|
|
||||||
"folder" => $folder,
|
|
||||||
"expires" => $expires,
|
|
||||||
"password" => $hashedPassword,
|
|
||||||
"allowUpload" => $allowUpload
|
"allowUpload" => $allowUpload
|
||||||
];
|
];
|
||||||
|
|
||||||
// Save the updated share links.
|
// Save
|
||||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) {
|
if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) {
|
||||||
return ["error" => "Could not save share link."];
|
return ["error" => "Could not save share link."];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the base URL.
|
// Build URL
|
||||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||||
$baseUrl = rtrim(BASE_URL, '/');
|
$host = $_SERVER['HTTP_HOST'] ?? gethostbyname(gethostname());
|
||||||
} else {
|
$baseUrl = $protocol . '://' . rtrim($host, '/');
|
||||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||||
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
|
|
||||||
$baseUrl = $protocol . "://" . $host;
|
|
||||||
}
|
|
||||||
// The share URL points to the shared folder page.
|
|
||||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
|
||||||
|
|
||||||
return ["token" => $token, "expires" => $expires, "link" => $link];
|
return ["token" => $token, "expires" => $expires, "link" => $link];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves information for a shared file from a shared folder link.
|
* Retrieves information for a shared file from a shared folder link.
|
||||||
*
|
*
|
||||||
* @param string $token The share folder token.
|
* @param string $token The share folder token.
|
||||||
@@ -418,7 +419,8 @@ class FolderModel {
|
|||||||
* - "realFilePath": the absolute path to the file,
|
* - "realFilePath": the absolute path to the file,
|
||||||
* - "mimeType": the detected MIME type.
|
* - "mimeType": the detected MIME type.
|
||||||
*/
|
*/
|
||||||
public static function getSharedFileInfo(string $token, string $file): array {
|
public static function getSharedFileInfo(string $token, string $file): array
|
||||||
|
{
|
||||||
// Load the share folder record.
|
// Load the share folder record.
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
if (!file_exists($shareFile)) {
|
if (!file_exists($shareFile)) {
|
||||||
@@ -479,10 +481,11 @@ class FolderModel {
|
|||||||
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
* @param array $fileUpload The $_FILES['fileToUpload'] array.
|
||||||
* @return array An associative array with "success" on success or "error" on failure.
|
* @return array An associative array with "success" on success or "error" on failure.
|
||||||
*/
|
*/
|
||||||
public static function uploadToSharedFolder(string $token, array $fileUpload): array {
|
public static function uploadToSharedFolder(string $token, array $fileUpload): array
|
||||||
|
{
|
||||||
// Define maximum file size and allowed extensions.
|
// Define maximum file size and allowed extensions.
|
||||||
$maxSize = 50 * 1024 * 1024; // 50 MB
|
$maxSize = 50 * 1024 * 1024; // 50 MB
|
||||||
$allowedExtensions = ['jpg','jpeg','png','gif','pdf','doc','docx','txt','xls','xlsx','ppt','pptx','mp4','webm','mp3','mkv'];
|
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'mp4', 'webm', 'mp3', 'mkv'];
|
||||||
|
|
||||||
// Load the share folder record.
|
// Load the share folder record.
|
||||||
$shareFile = META_DIR . "share_folder_links.json";
|
$shareFile = META_DIR . "share_folder_links.json";
|
||||||
@@ -567,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||