Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27de0a9a48 | ||
|
|
051544dc5a | ||
|
|
89777584cf | ||
|
|
ed47e3c3bc | ||
|
|
edd9094218 | ||
|
|
3b0083516b | ||
|
|
fee3b544dd | ||
|
|
99ed05d3de | ||
|
|
32469778dc | ||
|
|
ecb4ac2c75 | ||
|
|
4ae509acd2 | ||
|
|
b1cd4b7bdc | ||
|
|
d57687adee | ||
|
|
64d41af21b | ||
|
|
a8f5a6d3bc | ||
|
|
062cfc0dd4 | ||
|
|
32d25b1b69 | ||
|
|
56626aaa40 | ||
|
|
0697fcb1df | ||
|
|
c08c903810 |
92
CONTRIBUTING.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Contributing to FileRise
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to FileRise! We appreciate your help in making this self-hosted file manager even better.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
|
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||||
|
- [Pull Requests](#pull-requests)
|
||||||
|
- [Coding Guidelines](#coding-guidelines)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [Questions and Support](#questions-and-support)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. **Fork the Repository**
|
||||||
|
Click the **Fork** button on the top-right of the FileRise GitHub page to create your own copy.
|
||||||
|
|
||||||
|
2. **Clone Your Fork**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/FileRise.git
|
||||||
|
cd FileRise
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set Up a Local Environment**
|
||||||
|
FileRise runs on a standard LAMP stack. Ensure you have PHP, Apache, and the necessary dependencies installed. For frontend development, Node.js may be required for build tasks if applicable.
|
||||||
|
|
||||||
|
4. **Configuration**
|
||||||
|
Copy any example configuration files (if provided) and adjust them as needed for your local setup.
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
If you discover a bug, please open an issue on GitHub and include:
|
||||||
|
|
||||||
|
- A clear and descriptive title.
|
||||||
|
- Detailed steps to reproduce the bug.
|
||||||
|
- The expected and actual behavior.
|
||||||
|
- Screenshots or error logs (if applicable).
|
||||||
|
- Environment details (PHP version, Apache version, OS, etc.).
|
||||||
|
|
||||||
|
## Suggesting Enhancements
|
||||||
|
|
||||||
|
Have an idea for a new feature or improvement? Before opening a new issue, please check if a similar suggestion already exists. If not, open an issue with:
|
||||||
|
|
||||||
|
- A clear description of the enhancement.
|
||||||
|
- Use cases or examples of how it would be beneficial.
|
||||||
|
- Any potential drawbacks or alternatives.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
We welcome pull requests! To submit one, please follow these guidelines:
|
||||||
|
|
||||||
|
1. **Create a New Branch**
|
||||||
|
Always create a feature branch from master.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Make Your Changes**
|
||||||
|
Commit your changes with clear, descriptive messages. Make sure your code follows the project’s style guidelines.
|
||||||
|
|
||||||
|
3. **Write Tests**
|
||||||
|
If applicable, add tests to cover your changes to help us maintain code quality.
|
||||||
|
|
||||||
|
4. **Submit the Pull Request**
|
||||||
|
Push your branch to your fork and open a pull request against the master branch in the main repository. Provide a detailed description of your changes and why they’re needed.
|
||||||
|
|
||||||
|
## Coding Guidelines
|
||||||
|
|
||||||
|
- **Code Style:**
|
||||||
|
Follow the conventions used in the project. Consistent indentation, naming conventions, and clear code organization are key.
|
||||||
|
|
||||||
|
- **Documentation:**
|
||||||
|
Update documentation if your changes affect the usage or configuration of FileRise.
|
||||||
|
|
||||||
|
- **Commit Messages:**
|
||||||
|
Write meaningful commit messages that clearly describe the purpose of your changes.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
If you notice any areas in the documentation that need improvement or updating, please feel free to include those changes in your pull requests. Clear documentation is essential for helping others understand and use FileRise.
|
||||||
|
|
||||||
|
## Questions and Support
|
||||||
|
|
||||||
|
If you have any questions, ideas, or need support, please open an issue or join our discussion on [GitHub Discussions](https://github.com/error311/FileRise/discussions). We’re here to help and appreciate your contributions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for helping to improve FileRise and happy coding!
|
||||||
135
README.md
@@ -1,15 +1,15 @@
|
|||||||
# MFE - Lightweight Multi File Upload Editor
|
# FileRise - Elevate your File Management
|
||||||
|
|
||||||
**Video demo:**
|
**Video demo:**
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/179e6940-5798-4482-9a69-696f806c37de
|
https://github.com/user-attachments/assets/9546a76b-afb0-4068-875a-0eab478b514d
|
||||||
|
|
||||||
**Dark mode:**
|
**Dark mode:**
|
||||||

|

|
||||||
|
|
||||||
changelogs available here: <https://github.com/error311/multi-file-upload-editor-docker/>
|
changelogs available here: <https://github.com/error311/FileRise-docker/>
|
||||||
|
|
||||||
MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web application for uploading, syntax highlight editing, drag & drop and managing files. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive, dynamic file management interface. It serves as an alternative to solutions like FileGator TinyFileManager or ProjectSend, providing an easy-to-setup experience ideal for document management, image galleries, firmware file hosting, and more.
|
FileRise is a lightweight, secure, self-hosted web application for uploading, syntax-highlight editing, drag & drop file management, and more. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive and dynamic interface designed to simplify file handling. As an alternative to solutions like FileGator, TinyFileManager, or ProjectSend, FileRise provides an easy-to-set-up experience ideal for document management, image galleries, firmware hosting, and other file-intensive applications.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
- **Enhanced File Editing Check:** Files with a Content-Length of 0 KB are now allowed to be edited.
|
- **Enhanced File Editing Check:** Files with a Content-Length of 0 KB are now allowed to be edited.
|
||||||
|
|
||||||
- **Built-in File Preview:**
|
- **Built-in File Preview:**
|
||||||
- Users can quickly preview images, videos, and PDFs directly in modal popups without leaving the page.
|
- Users can quickly preview images, videos, audio and PDFs directly in modal popups without leaving the page.
|
||||||
- The preview modal supports inline display of images (with proper scaling) and videos with playback controls.
|
- The preview modal supports inline display of images (with proper scaling) and videos with playback controls.
|
||||||
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
|
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
|
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
|
||||||
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
||||||
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
|
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
|
||||||
- **Drag & Drop:** Easily move files by selecting them from the file list and simply dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
|
- **Drag & Drop (File Movement):** Easily move files by selecting them from the file list and dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
|
||||||
- **Enhanced Context Menu & Keyboard Shortcuts:**
|
- **Enhanced Context Menu & Keyboard Shortcuts:**
|
||||||
- **Right-Click Context Menu:**
|
- **Right-Click Context Menu:**
|
||||||
- A custom context menu appears on right-clicking within the file list.
|
- A custom context menu appears on right-clicking within the file list.
|
||||||
@@ -84,6 +84,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
- Passwords are hashed using PHP’s `password_hash()` for security.
|
- Passwords are hashed using PHP’s `password_hash()` for security.
|
||||||
- All state-changing endpoints include CSRF token validation.
|
- All state-changing endpoints include CSRF token validation.
|
||||||
- Change password supported for all users.
|
- Change password supported for all users.
|
||||||
|
- Basic Auth supported for login.
|
||||||
- **Persistent Login (Remember Me) with Encrypted Tokens:**
|
- **Persistent Login (Remember Me) with Encrypted Tokens:**
|
||||||
- Users can remain logged in across sessions securely.
|
- Users can remain logged in across sessions securely.
|
||||||
- Persistent tokens are encrypted using AES‑256‑CBC before being stored in a JSON file.
|
- Persistent tokens are encrypted using AES‑256‑CBC before being stored in a JSON file.
|
||||||
@@ -126,29 +127,63 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
|
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
|
||||||
- Material icons with tooltips visually represent the restore and delete actions.
|
- Material icons with tooltips visually represent the restore and delete actions.
|
||||||
|
|
||||||
|
- **Drag & Drop Cards with Dedicated Drop Zones:**
|
||||||
|
- **Sidebar Drop Zone:**
|
||||||
|
- Cards (such as the upload card or folder management card) can be dragged into a dedicated sidebar drop zone for quick access to frequently used operations.
|
||||||
|
- The sidebar drop zone expands dynamically to accept drops anywhere within its visual area.
|
||||||
|
- **Top Bar Drop Zone:**
|
||||||
|
- A top drop zone is available for reordering or managing cards quickly.
|
||||||
|
- Dragging a card to the top drop zone provides immediate visual feedback, ensuring a fluid and customizable workflow.
|
||||||
|
- **Seamless Interaction:**
|
||||||
|
- Both drop zones support smooth drag and drop interactions with animations and pointer event adjustments to prevent interference, ensuring that cards can be dropped reliably regardless of screen position.
|
||||||
|
|
||||||
|
### 🔒 Admin Panel & OpenID Connect (OIDC) Integration
|
||||||
|
|
||||||
|
- **Flexible Authentication:**
|
||||||
|
- Supports multiple authentication methods including Form-based Login, Basic Auth, and OpenID Connect (OIDC). Allow disable of only two login options.
|
||||||
|
|
||||||
|
- **Secure OIDC Authentication:**
|
||||||
|
- Integrates seamlessly with OIDC providers (e.g., Keycloak, Okta).
|
||||||
|
- Admin-configurable OIDC settings, including Provider URL, Client ID, Client Secret, and Redirect URI.
|
||||||
|
- All sensitive configurations are securely stored in an encrypted JSON file.
|
||||||
|
|
||||||
|
- **Dynamic Admin Panel:**
|
||||||
|
- Intuitive Admin Panel with Material Icons for quick recognition and access.
|
||||||
|
- Allows administrators to easily manage authentication settings, user management, and login methods.
|
||||||
|
- Real-time validation prevents disabling all authentication methods simultaneously, ensuring continuous secure access.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
**Light mode:**
|
**Light mode:**
|
||||||

|

|
||||||
|
|
||||||
|
**Light mode:**
|
||||||
|

|
||||||
|
|
||||||
|
**Dark mode default:**
|
||||||
|

|
||||||
|
|
||||||
**Dark editor:**
|
**Dark editor:**
|
||||||

|

|
||||||
|
|
||||||
**Dark preview**
|
**Light preview**
|
||||||

|

|
||||||
|
|
||||||
**Restore or Delete Trash:**
|
**Restore or Delete Trash:**
|
||||||

|

|
||||||
|
|
||||||
**Login page:**
|
**Dark Login page:**
|
||||||

|

|
||||||
|
|
||||||
|
**Gallery view:**
|
||||||
|

|
||||||
|
|
||||||
**iphone screenshots:**
|
**iphone screenshots:**
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-iphone.png" width="45%">
|
<img src="https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-iphone.png" width="45%">
|
||||||
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/light-preview-iphone.png" width="45%">
|
<img src="https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-preview-iphone.png" width="45%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -161,7 +196,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
- **Clone:**
|
- **Clone:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/error311/multi-file-upload-editor.git
|
git clone https://github.com/error311/FileRise.git
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Download:**
|
- **Download:**
|
||||||
@@ -169,9 +204,19 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
|
|
||||||
2. **Deploy to Your Web Server:**
|
2. **Deploy to Your Web Server:**
|
||||||
- Place the project files in your Apache web directory (e.g., `/var/www/html`).
|
- Place the project files in your Apache web directory (e.g., `/var/www/html`).
|
||||||
- Ensure PHP 8.1+ is installed along with the required extensions (php-json, php-curl, php-zip, etc.).
|
- Ensure PHP 8.1+ is installed along with the required extensions (`php-json`, `php-curl`, `php-zip`, etc.).
|
||||||
|
|
||||||
3. **Directory Setup & Permissions:**
|
3. **Install Composer Dependencies (Required for OIDC Support):**
|
||||||
|
- Install Composer if you haven't already ([Installation Guide](https://getcomposer.org/download/)).
|
||||||
|
- Navigate to the project directory and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
- This step will install necessary dependencies like `jumbojett/openid-connect-php` and `phpseclib/phpseclib`.
|
||||||
|
|
||||||
|
4. **Directory Setup & Permissions:**
|
||||||
- Create the following directories if they do not exist, and set appropriate permissions:
|
- Create the following directories if they do not exist, and set appropriate permissions:
|
||||||
- `uploads/` – for file storage.
|
- `uploads/` – for file storage.
|
||||||
- `users/` – to store `users.txt` (user authentication data).
|
- `users/` – to store `users.txt` (user authentication data).
|
||||||
@@ -183,11 +228,11 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
chmod -R 775 /var/www/uploads /var/www/users /var/www/metadata
|
chmod -R 775 /var/www/uploads /var/www/users /var/www/metadata
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Configure Apache:**
|
5. **Configure Apache:**
|
||||||
- Ensure that directory indexing is disabled (using `Options -Indexes` in your `.htaccess` or Apache configuration).
|
- Ensure that directory indexing is disabled (using `Options -Indexes` in your `.htaccess` or Apache configuration).
|
||||||
- Make sure the Apache configuration allows URL rewriting if needed.
|
- Make sure the Apache configuration allows URL rewriting if needed.
|
||||||
|
|
||||||
5. **Configuration File:**
|
6. **Configuration File:**
|
||||||
- Open `config.php` and adjust the following constants as necessary:
|
- Open `config.php` and adjust the following constants as necessary:
|
||||||
- `BASE_URL`: Set this to your web app’s base URL.
|
- `BASE_URL`: Set this to your web app’s base URL.
|
||||||
- `UPLOAD_DIR`: Adjust the directory path for uploads.
|
- `UPLOAD_DIR`: Adjust the directory path for uploads.
|
||||||
@@ -204,14 +249,22 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
|||||||
|
|
||||||
## Docker Usage
|
## Docker Usage
|
||||||
|
|
||||||
For users who prefer containerization, a Docker image is available
|
For users who prefer containerization, a Docker image is available.
|
||||||
|
|
||||||
|
**Note:** The Docker image already includes Composer dependencies pre-installed (including OIDC support).
|
||||||
|
|
||||||
### Quickstart
|
### Quickstart
|
||||||
|
|
||||||
1. **Pull the Docker Image:**
|
1. **Pull the Docker Image:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull error311/multi-file-upload-editor-docker:latest
|
docker pull error311/filerise-docker:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
macos M series:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull --platform linux/x86_64 error311/filerise-docker:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run the Container:**
|
2. **Run the Container:**
|
||||||
@@ -225,8 +278,8 @@ For users who prefer containerization, a Docker image is available
|
|||||||
-v /path/to/your/uploads:/var/www/uploads \
|
-v /path/to/your/uploads:/var/www/uploads \
|
||||||
-v /path/to/your/users:/var/www/users \
|
-v /path/to/your/users:/var/www/users \
|
||||||
-v /path/to/your/metadata:/var/www/metadata \
|
-v /path/to/your/metadata:/var/www/metadata \
|
||||||
--name multi-file-upload-editor \
|
--name FileRise \
|
||||||
error311/multi-file-upload-editor-docker:latest
|
error311/filerise-docker:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Using Docker Compose:**
|
3. **Using Docker Compose:**
|
||||||
@@ -236,19 +289,19 @@ For users who prefer containerization, a Docker image is available
|
|||||||
```yaml
|
```yaml
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: error311/multi-file-upload-editor-docker:latest
|
image: error311/filerise-docker:latest
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
environment:
|
environment:
|
||||||
TIMEZONE: "America/New_York"
|
TIMEZONE: "America/New_York"
|
||||||
TOTAL_UPLOAD_SIZE: "5G"
|
TOTAL_UPLOAD_SIZE: "5G"
|
||||||
SECURE: "false"
|
SECURE: "false"
|
||||||
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/your/uploads:/var/www/uploads
|
- /path/to/your/uploads:/var/www/uploads
|
||||||
- /path/to/your/users:/var/www/users
|
- /path/to/your/users:/var/www/users
|
||||||
- /path/to/your/metadata:/var/www/metadata
|
- /path/to/your/metadata:/var/www/metadata
|
||||||
```
|
```
|
||||||
|
|
||||||
**Then start the container with:**
|
**Then start the container with:**
|
||||||
@@ -291,4 +344,8 @@ The `config.php` file contains several key constants that may need adjustment fo
|
|||||||
- **Logging & Troubleshooting:**
|
- **Logging & Troubleshooting:**
|
||||||
Check Apache logs (located in `/var/log/apache2/`) for troubleshooting any issues during deployment or operation.
|
Check Apache logs (located in `/var/log/apache2/`) for troubleshooting any issues during deployment or operation.
|
||||||
|
|
||||||
Enjoy using the Multi File Upload Editor! For any issues or contributions, please refer to the [GitHub repository](https://github.com/error311/multi-file-upload-editor).
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please check out our [Contributing Guidelines](CONTRIBUTING.md) before getting started.
|
||||||
|
|||||||
390
auth.js
@@ -3,9 +3,14 @@ import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmM
|
|||||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { loadFolderTree } from './folderManager.js';
|
||||||
|
|
||||||
/**
|
// Default OIDC configuration (can be overridden via API in production)
|
||||||
* Updates the select element to reflect the stored items-per-page value.
|
const currentOIDCConfig = {
|
||||||
*/
|
providerUrl: "https://your-oidc-provider.com",
|
||||||
|
clientId: "YOUR_CLIENT_ID",
|
||||||
|
clientSecret: "YOUR_CLIENT_SECRET",
|
||||||
|
redirectUri: "https://yourdomain.com/auth.php?oidc=callback"
|
||||||
|
};
|
||||||
|
|
||||||
function updateItemsPerPageSelect() {
|
function updateItemsPerPageSelect() {
|
||||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||||
if (selectElem) {
|
if (selectElem) {
|
||||||
@@ -14,11 +19,45 @@ function updateItemsPerPageSelect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||||
* Updates the UI for an authenticated user.
|
const authForm = document.getElementById("authForm");
|
||||||
* This includes showing the main UI panels, attaching key listeners, updating header buttons,
|
if (authForm) {
|
||||||
* and displaying admin-only buttons if applicable.
|
authForm.style.display = disableFormLogin ? "none" : "block";
|
||||||
*/
|
}
|
||||||
|
const basicAuthLink = document.querySelector("a[href='login_basic.php']");
|
||||||
|
if (basicAuthLink) {
|
||||||
|
basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block";
|
||||||
|
}
|
||||||
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
|
if (oidcLoginBtn) {
|
||||||
|
oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLoginOptionsUIFromStorage() {
|
||||||
|
const disableFormLogin = localStorage.getItem("disableFormLogin") === "true";
|
||||||
|
const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
|
const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
|
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAdminConfigFunc() {
|
||||||
|
return fetch("getConfig.php", { credentials: "include" })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(config => {
|
||||||
|
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin);
|
||||||
|
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
|
||||||
|
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
|
||||||
|
updateLoginOptionsUIFromStorage();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
localStorage.setItem("disableFormLogin", "false");
|
||||||
|
localStorage.setItem("disableBasicAuth", "false");
|
||||||
|
localStorage.setItem("disableOIDCLogin", "false");
|
||||||
|
updateLoginOptionsUIFromStorage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateAuthenticatedUI(data) {
|
function updateAuthenticatedUI(data) {
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
toggleVisibility("mainOperations", true);
|
toggleVisibility("mainOperations", true);
|
||||||
@@ -29,45 +68,54 @@ function updateAuthenticatedUI(data) {
|
|||||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||||
|
|
||||||
// If admin, show admin-only buttons; otherwise hide them.
|
|
||||||
if (data.isAdmin) {
|
if (data.isAdmin) {
|
||||||
const addUserBtn = document.getElementById("addUserBtn");
|
|
||||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
|
||||||
if (addUserBtn) addUserBtn.style.display = "block";
|
|
||||||
if (removeUserBtn) removeUserBtn.style.display = "block";
|
|
||||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||||
if (!restoreBtn) {
|
if (!restoreBtn) {
|
||||||
restoreBtn = document.createElement("button");
|
restoreBtn = document.createElement("button");
|
||||||
restoreBtn.id = "restoreFilesBtn";
|
restoreBtn.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
restoreBtn.classList.add("btn", "btn-warning");
|
||||||
// Using a material icon for restore.
|
|
||||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
const headerButtons = document.querySelector(".header-buttons");
|
||||||
if (headerButtons) {
|
if (headerButtons) {
|
||||||
if (headerButtons.children.length >= 5) {
|
if (headerButtons.children.length >= 3) {
|
||||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
|
headerButtons.insertBefore(restoreBtn, headerButtons.children[3]);
|
||||||
} else {
|
} else {
|
||||||
headerButtons.appendChild(restoreBtn);
|
headerButtons.appendChild(restoreBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
restoreBtn.style.display = "block";
|
restoreBtn.style.display = "block";
|
||||||
|
|
||||||
|
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
|
if (!adminPanelBtn) {
|
||||||
|
adminPanelBtn = document.createElement("button");
|
||||||
|
adminPanelBtn.id = "adminPanelBtn";
|
||||||
|
adminPanelBtn.classList.add("btn", "btn-info");
|
||||||
|
// Use material icon for the admin panel button.
|
||||||
|
adminPanelBtn.innerHTML = '<i class="material-icons" title="Admin Panel">admin_panel_settings</i>';
|
||||||
|
const headerButtons = document.querySelector(".header-buttons");
|
||||||
|
if (headerButtons) {
|
||||||
|
// Insert the adminPanelBtn immediately after the restoreBtn.
|
||||||
|
if (restoreBtn.nextSibling) {
|
||||||
|
headerButtons.insertBefore(adminPanelBtn, restoreBtn.nextSibling);
|
||||||
|
} else {
|
||||||
|
headerButtons.appendChild(adminPanelBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adminPanelBtn.addEventListener("click", openAdminPanel);
|
||||||
|
} else {
|
||||||
|
adminPanelBtn.style.display = "block";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const addUserBtn = document.getElementById("addUserBtn");
|
|
||||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
|
||||||
if (addUserBtn) addUserBtn.style.display = "none";
|
|
||||||
if (removeUserBtn) removeUserBtn.style.display = "none";
|
|
||||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||||
if (restoreBtn) restoreBtn.style.display = "none";
|
if (restoreBtn) restoreBtn.style.display = "none";
|
||||||
|
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
|
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
||||||
}
|
}
|
||||||
updateItemsPerPageSelect();
|
updateItemsPerPageSelect();
|
||||||
|
updateLoginOptionsUIFromStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the user's authentication state and updates the UI accordingly.
|
|
||||||
* If in setup mode or not authenticated, it shows the proper UI elements.
|
|
||||||
* When authenticated, it calls updateAuthenticatedUI to handle the UI updates.
|
|
||||||
*/
|
|
||||||
function checkAuthentication(showLoginToast = true) {
|
function checkAuthentication(showLoginToast = true) {
|
||||||
return sendRequest("checkAuth.php")
|
return sendRequest("checkAuth.php")
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -78,7 +126,7 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
toggleVisibility("mainOperations", false);
|
toggleVisibility("mainOperations", false);
|
||||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById('newUsername').focus();
|
document.getElementById("newUsername").focus();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
@@ -95,22 +143,13 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(() => false);
|
||||||
console.error("Error checking authentication:", error);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes authentication by checking the user's state and setting up event listeners.
|
|
||||||
* The UI will update automatically based on the auth state.
|
|
||||||
*/
|
|
||||||
function initAuth() {
|
function initAuth() {
|
||||||
checkAuthentication(false).catch(error => {
|
checkAuthentication(false);
|
||||||
console.error("Error checking authentication:", error);
|
loadAdminConfigFunc();
|
||||||
});
|
|
||||||
|
|
||||||
// Attach login event listener.
|
|
||||||
const authForm = document.getElementById("authForm");
|
const authForm = document.getElementById("authForm");
|
||||||
if (authForm) {
|
if (authForm) {
|
||||||
authForm.addEventListener("submit", function (event) {
|
authForm.addEventListener("submit", function (event) {
|
||||||
@@ -126,7 +165,6 @@ function initAuth() {
|
|||||||
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
|
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log("✅ Login successful. Reloading page.");
|
|
||||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
|
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -145,11 +183,10 @@ function initAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("❌ Error logging in:", error));
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach logout event listener.
|
|
||||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||||
fetch("logout.php", {
|
fetch("logout.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -157,14 +194,20 @@ function initAuth() {
|
|||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
})
|
})
|
||||||
.then(() => window.location.reload(true))
|
.then(() => window.location.reload(true))
|
||||||
.catch(error => console.error("Logout error:", error));
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add User functionality.
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
|
if (oidcLoginBtn) {
|
||||||
|
oidcLoginBtn.addEventListener("click", function () {
|
||||||
|
window.location.href = "auth.php?oidc";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById('newUsername').focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||||
const newUsername = document.getElementById("newUsername").value.trim();
|
const newUsername = document.getElementById("newUsername").value.trim();
|
||||||
@@ -192,24 +235,19 @@ function initAuth() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast("User added successfully!");
|
showToast("User added successfully!");
|
||||||
closeAddUserModal();
|
closeAddUserModal();
|
||||||
// Re-check auth state to update the UI after adding a user.
|
|
||||||
checkAuthentication(false);
|
checkAuthentication(false);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error: " + (data.error || "Could not add user"));
|
showToast("Error: " + (data.error || "Could not add user"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error adding user:", error));
|
.catch(() => {});
|
||||||
});
|
|
||||||
document.getElementById("cancelUserBtn").addEventListener("click", function () {
|
|
||||||
closeAddUserModal();
|
|
||||||
});
|
});
|
||||||
|
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||||
|
|
||||||
// Remove User functionality.
|
|
||||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||||
loadUserList();
|
loadUserList();
|
||||||
toggleVisibility("removeUserModal", true);
|
toggleVisibility("removeUserModal", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
|
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
|
||||||
const selectElem = document.getElementById("removeUsernameSelect");
|
const selectElem = document.getElementById("removeUsernameSelect");
|
||||||
const usernameToRemove = selectElem.value;
|
const usernameToRemove = selectElem.value;
|
||||||
@@ -218,9 +256,7 @@ function initAuth() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||||
if (!confirmed) {
|
if (!confirmed) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetch("removeUser.php", {
|
fetch("removeUser.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -240,22 +276,17 @@ function initAuth() {
|
|||||||
showToast("Error: " + (data.error || "Could not remove user"));
|
showToast("Error: " + (data.error || "Could not remove user"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error removing user:", error));
|
.catch(() => {});
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
|
|
||||||
closeRemoveUserModal();
|
|
||||||
});
|
});
|
||||||
|
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||||
|
|
||||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||||
document.getElementById("changePasswordModal").style.display = "block";
|
document.getElementById("changePasswordModal").style.display = "block";
|
||||||
document.getElementById("oldPassword").focus();
|
document.getElementById("oldPassword").focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
||||||
document.getElementById("changePasswordModal").style.display = "none";
|
document.getElementById("changePasswordModal").style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
||||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
const oldPassword = document.getElementById("oldPassword").value.trim();
|
||||||
const newPassword = document.getElementById("newPassword").value.trim();
|
const newPassword = document.getElementById("newPassword").value.trim();
|
||||||
@@ -290,13 +321,240 @@ function initAuth() {
|
|||||||
showToast("Error: " + (result.error || "Could not change password."));
|
showToast("Error: " + (result.error || "Could not change password."));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(() => {
|
||||||
console.error("Error changing password:", error);
|
|
||||||
showToast("Error changing password.");
|
showToast("Error changing password.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadOIDCConfig() {
|
||||||
|
return fetch("getConfig.php", { credentials: "include" })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.oidc) {
|
||||||
|
Object.assign(currentOIDCConfig, config.oidc);
|
||||||
|
}
|
||||||
|
return currentOIDCConfig;
|
||||||
|
})
|
||||||
|
.catch(() => currentOIDCConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdminPanel() {
|
||||||
|
fetch("getConfig.php", { credentials: "include" })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.oidc) {
|
||||||
|
Object.assign(currentOIDCConfig, config.oidc);
|
||||||
|
}
|
||||||
|
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>Admin Panel</h3>
|
||||||
|
<form id="adminPanelForm">
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>OIDC Configuration</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidcProviderUrl">OIDC Provider URL:</label>
|
||||||
|
<input type="text" id="oidcProviderUrl" class="form-control" value="${currentOIDCConfig.providerUrl}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidcClientId">OIDC Client ID:</label>
|
||||||
|
<input type="text" id="oidcClientId" class="form-control" value="${currentOIDCConfig.clientId}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidcClientSecret">OIDC Client Secret:</label>
|
||||||
|
<input type="text" id="oidcClientSecret" class="form-control" value="${currentOIDCConfig.clientSecret}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidcRedirectUri">OIDC Redirect URI:</label>
|
||||||
|
<input type="text" id="oidcRedirectUri" class="form-control" value="${currentOIDCConfig.redirectUri}" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>Login Options</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="disableFormLogin" />
|
||||||
|
<label for="disableFormLogin">Disable Login Form</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="disableBasicAuth" />
|
||||||
|
<label for="disableBasicAuth">Disable Basic HTTP Auth</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="disableOIDCLogin" />
|
||||||
|
<label for="disableOIDCLogin">Disable OIDC Login</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset style="margin-bottom: 15px;">
|
||||||
|
<legend>User Management</legend>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button type="button" id="adminOpenAddUser" class="btn btn-success">Add User</button>
|
||||||
|
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger">Remove User</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<button type="button" id="cancelAdminSettings" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button type="button" id="saveAdminSettings" class="btn btn-primary">Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(adminModal);
|
||||||
|
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
|
||||||
|
adminModal.addEventListener("click", function (e) {
|
||||||
|
if (e.target === adminModal) {
|
||||||
|
closeAdminPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
|
||||||
|
document.getElementById("adminOpenAddUser").addEventListener("click", function () {
|
||||||
|
toggleVisibility("addUserModal", true);
|
||||||
|
document.getElementById("newUsername").focus();
|
||||||
|
});
|
||||||
|
document.getElementById("adminOpenRemoveUser").addEventListener("click", function () {
|
||||||
|
loadUserList();
|
||||||
|
toggleVisibility("removeUserModal", true);
|
||||||
|
});
|
||||||
|
document.getElementById("saveAdminSettings").addEventListener("click", function () {
|
||||||
|
const disableFormLoginCheckbox = document.getElementById("disableFormLogin");
|
||||||
|
const disableBasicAuthCheckbox = document.getElementById("disableBasicAuth");
|
||||||
|
const disableOIDCLoginCheckbox = document.getElementById("disableOIDCLogin");
|
||||||
|
const totalDisabled = [disableFormLoginCheckbox, disableBasicAuthCheckbox, disableOIDCLoginCheckbox]
|
||||||
|
.filter(cb => cb.checked).length;
|
||||||
|
if (totalDisabled === 3) {
|
||||||
|
showToast("At least one login method must remain enabled.");
|
||||||
|
disableOIDCLoginCheckbox.checked = false;
|
||||||
|
localStorage.setItem("disableOIDCLogin", "false");
|
||||||
|
updateLoginOptionsUI({
|
||||||
|
disableFormLogin: disableFormLoginCheckbox.checked,
|
||||||
|
disableBasicAuth: disableBasicAuthCheckbox.checked,
|
||||||
|
disableOIDCLogin: disableOIDCLoginCheckbox.checked
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
sendRequest("updateConfig.php", "POST", {
|
||||||
|
oidc: newOIDCConfig,
|
||||||
|
disableFormLogin,
|
||||||
|
disableBasicAuth,
|
||||||
|
disableOIDCLogin
|
||||||
|
}, { "X-CSRF-Token": window.csrfToken })
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
showToast("Settings updated successfully.");
|
||||||
|
localStorage.setItem("disableFormLogin", disableFormLogin);
|
||||||
|
localStorage.setItem("disableBasicAuth", disableBasicAuth);
|
||||||
|
localStorage.setItem("disableOIDCLogin", disableOIDCLogin);
|
||||||
|
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||||
|
closeAdminPanel();
|
||||||
|
} else {
|
||||||
|
showToast("Error updating settings: " + (response.error || "Unknown error"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
});
|
||||||
|
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("At least one login method must remain enabled.");
|
||||||
|
changedCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disableFormLoginCheckbox.addEventListener("change", function () {
|
||||||
|
enforceLoginOptionConstraint(this);
|
||||||
|
});
|
||||||
|
disableBasicAuthCheckbox.addEventListener("change", function () {
|
||||||
|
enforceLoginOptionConstraint(this);
|
||||||
|
});
|
||||||
|
disableOIDCLoginCheckbox.addEventListener("change", function () {
|
||||||
|
enforceLoginOptionConstraint(this);
|
||||||
|
});
|
||||||
|
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||||
|
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
|
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
|
} else {
|
||||||
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
|
const overlayBackground = isDarkMode ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.3)";
|
||||||
|
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 = currentOIDCConfig.providerUrl;
|
||||||
|
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
|
||||||
|
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
|
||||||
|
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
|
||||||
|
document.getElementById("disableFormLogin").checked = localStorage.getItem("disableFormLogin") === "true";
|
||||||
|
document.getElementById("disableBasicAuth").checked = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
|
document.getElementById("disableOIDCLogin").checked = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
|
adminModal.style.display = "flex";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
let adminModal = document.getElementById("adminPanelModal");
|
||||||
|
if (adminModal) {
|
||||||
|
document.getElementById("oidcProviderUrl").value = currentOIDCConfig.providerUrl;
|
||||||
|
document.getElementById("oidcClientId").value = currentOIDCConfig.clientId;
|
||||||
|
document.getElementById("oidcClientSecret").value = currentOIDCConfig.clientSecret;
|
||||||
|
document.getElementById("oidcRedirectUri").value = currentOIDCConfig.redirectUri;
|
||||||
|
adminModal.style.display = "flex";
|
||||||
|
} else {
|
||||||
|
openAdminPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAdminPanel() {
|
||||||
|
const adminModal = document.getElementById("adminPanelModal");
|
||||||
|
if (adminModal) {
|
||||||
|
adminModal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.changeItemsPerPage = function (value) {
|
window.changeItemsPerPage = function (value) {
|
||||||
localStorage.setItem("itemsPerPage", value);
|
localStorage.setItem("itemsPerPage", value);
|
||||||
const folder = window.currentFolder || "root";
|
const folder = window.currentFolder || "root";
|
||||||
@@ -307,6 +565,10 @@ window.changeItemsPerPage = function (value) {
|
|||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
updateItemsPerPageSelect();
|
updateItemsPerPageSelect();
|
||||||
|
const disableFormLogin = localStorage.getItem("disableFormLogin") === "true";
|
||||||
|
const disableBasicAuth = localStorage.getItem("disableBasicAuth") === "true";
|
||||||
|
const disableOIDCLogin = localStorage.getItem("disableOIDCLogin") === "true";
|
||||||
|
updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin });
|
||||||
});
|
});
|
||||||
|
|
||||||
function resetUserForm() {
|
function resetUserForm() {
|
||||||
@@ -342,7 +604,7 @@ function loadUserList() {
|
|||||||
closeRemoveUserModal();
|
closeRemoveUserModal();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error loading user list:", error));
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { initAuth, checkAuthentication };
|
export { initAuth, checkAuthentication };
|
||||||
117
auth.php
@@ -1,19 +1,83 @@
|
|||||||
<?php
|
<?php
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// --- OIDC Authentication Flow ---
|
||||||
|
if (isset($_GET['oidc'])) {
|
||||||
|
|
||||||
|
// Read and decrypt OIDC configuration from JSON file.
|
||||||
|
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||||
|
if (file_exists($adminConfigFile)) {
|
||||||
|
$encryptedContent = file_get_contents($adminConfigFile);
|
||||||
|
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||||
|
if ($decryptedContent === false) {
|
||||||
|
echo json_encode(['error' => 'Failed to decrypt admin configuration.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$adminConfig = json_decode($decryptedContent, true);
|
||||||
|
if (isset($adminConfig['oidc'])) {
|
||||||
|
$oidcConfig = $adminConfig['oidc'];
|
||||||
|
$oidc_provider_url = !empty($oidcConfig['providerUrl']) ? $oidcConfig['providerUrl'] : 'https://your-oidc-provider.com';
|
||||||
|
$oidc_client_id = !empty($oidcConfig['clientId']) ? $oidcConfig['clientId'] : 'YOUR_CLIENT_ID';
|
||||||
|
$oidc_client_secret = !empty($oidcConfig['clientSecret']) ? $oidcConfig['clientSecret'] : 'YOUR_CLIENT_SECRET';
|
||||||
|
$oidc_redirect_uri = !empty($oidcConfig['redirectUri']) ? $oidcConfig['redirectUri'] : 'https://yourdomain.com/auth.php?oidc=callback';
|
||||||
|
} else {
|
||||||
|
$oidc_provider_url = 'https://your-oidc-provider.com';
|
||||||
|
$oidc_client_id = 'YOUR_CLIENT_ID';
|
||||||
|
$oidc_client_secret = 'YOUR_CLIENT_SECRET';
|
||||||
|
$oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$oidc_provider_url = 'https://your-oidc-provider.com';
|
||||||
|
$oidc_client_id = 'YOUR_CLIENT_ID';
|
||||||
|
$oidc_client_secret = 'YOUR_CLIENT_SECRET';
|
||||||
|
$oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
|
||||||
|
}
|
||||||
|
|
||||||
|
$oidc = new Jumbojett\OpenIDConnectClient(
|
||||||
|
$oidc_provider_url,
|
||||||
|
$oidc_client_id,
|
||||||
|
$oidc_client_secret
|
||||||
|
);
|
||||||
|
$oidc->setRedirectURL($oidc_redirect_uri);
|
||||||
|
|
||||||
|
// Since PKCE is disabled in Keycloak, we do not set any PKCE parameters.
|
||||||
|
|
||||||
|
if ($_GET['oidc'] === 'callback') {
|
||||||
|
try {
|
||||||
|
$oidc->authenticate();
|
||||||
|
$username = $oidc->requestUserInfo('preferred_username');
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION["authenticated"] = true;
|
||||||
|
$_SESSION["username"] = $username;
|
||||||
|
$_SESSION["isAdmin"] = false;
|
||||||
|
header("Location: index.html");
|
||||||
|
exit();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(["error" => "Authentication failed: " . $e->getMessage()]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$oidc->authenticate();
|
||||||
|
exit();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(["error" => "Authentication initiation failed: " . $e->getMessage()]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fallback: Form-based Authentication ---
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
// --- Brute Force Protection Settings ---
|
|
||||||
$maxAttempts = 5;
|
$maxAttempts = 5;
|
||||||
$lockoutTime = 30 * 60; // 30 minutes in seconds
|
$lockoutTime = 30 * 60;
|
||||||
$attemptsFile = USERS_DIR . 'failed_logins.json'; // JSON file for tracking failed login attempts
|
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
||||||
$failedLogFile = USERS_DIR . 'failed_login.log'; // Plain text log for fail2ban
|
$failedLogFile = USERS_DIR . 'failed_login.log';
|
||||||
|
|
||||||
// Persistent tokens file for "Remember me"
|
|
||||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
|
|
||||||
// Load failed attempts data from file.
|
|
||||||
function loadFailedAttempts($file) {
|
function loadFailedAttempts($file) {
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
$data = json_decode(file_get_contents($file), true);
|
$data = json_decode(file_get_contents($file), true);
|
||||||
@@ -24,30 +88,23 @@ function loadFailedAttempts($file) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save failed attempts data to file.
|
|
||||||
function saveFailedAttempts($file, $data) {
|
function saveFailedAttempts($file, $data) {
|
||||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
|
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current IP address.
|
|
||||||
$ip = $_SERVER['REMOTE_ADDR'];
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
$currentTime = time();
|
$currentTime = time();
|
||||||
|
|
||||||
// Load failed attempts.
|
|
||||||
$failedAttempts = loadFailedAttempts($attemptsFile);
|
$failedAttempts = loadFailedAttempts($attemptsFile);
|
||||||
|
|
||||||
// Check if this IP is currently locked out.
|
|
||||||
if (isset($failedAttempts[$ip])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
$attemptData = $failedAttempts[$ip];
|
$attemptData = $failedAttempts[$ip];
|
||||||
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
||||||
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
|
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
|
||||||
exit;
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Authentication Function ---
|
function authenticate($username, $password) {
|
||||||
function authenticate($username, $password)
|
|
||||||
{
|
|
||||||
global $usersFile;
|
global $usersFile;
|
||||||
if (!file_exists($usersFile)) {
|
if (!file_exists($usersFile)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -56,51 +113,41 @@ function authenticate($username, $password)
|
|||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
|
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
|
||||||
if ($username === $storedUser && password_verify($password, $storedPass)) {
|
if ($username === $storedUser && password_verify($password, $storedPass)) {
|
||||||
return $storedRole; // Return the user's role
|
return $storedRole;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get JSON input.
|
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
$data = json_decode(file_get_contents("php://input"), true);
|
||||||
$username = trim($data["username"] ?? "");
|
$username = trim($data["username"] ?? "");
|
||||||
$password = trim($data["password"] ?? "");
|
$password = trim($data["password"] ?? "");
|
||||||
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
||||||
|
|
||||||
// Validate input: ensure both fields are provided.
|
|
||||||
if (!$username || !$password) {
|
if (!$username || !$password) {
|
||||||
echo json_encode(["error" => "Username and password are required"]);
|
echo json_encode(["error" => "Username and password are required"]);
|
||||||
exit;
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate username format.
|
|
||||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||||
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||||
exit;
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to authenticate the user.
|
|
||||||
$userRole = authenticate($username, $password);
|
$userRole = authenticate($username, $password);
|
||||||
if ($userRole !== false) {
|
if ($userRole !== false) {
|
||||||
// On successful login, reset failed attempts for this IP.
|
|
||||||
if (isset($failedAttempts[$ip])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
unset($failedAttempts[$ip]);
|
unset($failedAttempts[$ip]);
|
||||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||||
}
|
}
|
||||||
// Regenerate session ID to mitigate session fixation attacks.
|
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_SESSION["username"] = $username;
|
||||||
$_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin
|
$_SESSION["isAdmin"] = ($userRole === "1");
|
||||||
|
|
||||||
// If "Remember me" is checked, generate a persistent login token.
|
|
||||||
if ($rememberMe) {
|
if ($rememberMe) {
|
||||||
// Generate a secure random token.
|
|
||||||
$token = bin2hex(random_bytes(32));
|
$token = bin2hex(random_bytes(32));
|
||||||
$expiry = time() + (30 * 24 * 60 * 60); // 30 days
|
$expiry = time() + (30 * 24 * 60 * 60);
|
||||||
|
|
||||||
// Load existing persistent tokens.
|
|
||||||
$persistentTokens = [];
|
$persistentTokens = [];
|
||||||
if (file_exists($persistentTokensFile)) {
|
if (file_exists($persistentTokensFile)) {
|
||||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||||
@@ -110,7 +157,6 @@ if ($userRole !== false) {
|
|||||||
$persistentTokens = [];
|
$persistentTokens = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Save token along with username, expiry, and admin status.
|
|
||||||
$persistentTokens[$token] = [
|
$persistentTokens[$token] = [
|
||||||
"username" => $username,
|
"username" => $username,
|
||||||
"expiry" => $expiry,
|
"expiry" => $expiry,
|
||||||
@@ -118,13 +164,11 @@ if ($userRole !== false) {
|
|||||||
];
|
];
|
||||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
||||||
// Set the cookie. (Assuming $secure is defined in config.php.)
|
|
||||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
|
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
|
||||||
} else {
|
} else {
|
||||||
// On failed login, update failed attempts.
|
|
||||||
if (isset($failedAttempts[$ip])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
$failedAttempts[$ip]['count']++;
|
$failedAttempts[$ip]['count']++;
|
||||||
$failedAttempts[$ip]['last_attempt'] = $currentTime;
|
$failedAttempts[$ip]['last_attempt'] = $currentTime;
|
||||||
@@ -132,11 +176,8 @@ if ($userRole !== false) {
|
|||||||
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
|
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
|
||||||
}
|
}
|
||||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||||
|
|
||||||
// Log the failed attempt to the plain text log for fail2ban.
|
|
||||||
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
|
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
|
||||||
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
|
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
|
||||||
|
|
||||||
echo json_encode(["error" => "Invalid credentials"]);
|
echo json_encode(["error" => "Invalid credentials"]);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
35
config.php
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
// config.php
|
// config.php
|
||||||
|
|
||||||
// Define constants first.
|
// Define constants.
|
||||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||||
define('USERS_DIR', '/var/www/users/');
|
define('USERS_DIR', '/var/www/users/');
|
||||||
define('USERS_FILE', 'users.txt');
|
define('USERS_FILE', 'users.txt');
|
||||||
@@ -12,15 +12,14 @@ define('TIMEZONE', 'America/New_York');
|
|||||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||||
|
|
||||||
// Set the default timezone.
|
|
||||||
date_default_timezone_set(TIMEZONE);
|
date_default_timezone_set(TIMEZONE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts data using AES-256-CBC.
|
* Encrypts data using AES-256-CBC.
|
||||||
*
|
*
|
||||||
* @param string $data The plaintext data.
|
* @param string $data The plaintext.
|
||||||
* @param string $encryptionKey The secret encryption key.
|
* @param string $encryptionKey The encryption key.
|
||||||
* @return string Base64-encoded string containing IV and ciphertext.
|
* @return string Base64-encoded string containing IV and ciphertext.
|
||||||
*/
|
*/
|
||||||
function encryptData($data, $encryptionKey) {
|
function encryptData($data, $encryptionKey) {
|
||||||
$cipher = 'AES-256-CBC';
|
$cipher = 'AES-256-CBC';
|
||||||
@@ -33,9 +32,9 @@ function encryptData($data, $encryptionKey) {
|
|||||||
/**
|
/**
|
||||||
* Decrypts data encrypted with AES-256-CBC.
|
* Decrypts data encrypted with AES-256-CBC.
|
||||||
*
|
*
|
||||||
* @param string $encryptedData The Base64-encoded data containing IV and ciphertext.
|
* @param string $encryptedData Base64-encoded data containing IV and ciphertext.
|
||||||
* @param string $encryptionKey The secret encryption key.
|
* @param string $encryptionKey The encryption key.
|
||||||
* @return string|false The decrypted plaintext or false on failure.
|
* @return string|false The decrypted plaintext or false on failure.
|
||||||
*/
|
*/
|
||||||
function decryptData($encryptedData, $encryptionKey) {
|
function decryptData($encryptedData, $encryptionKey) {
|
||||||
$cipher = 'AES-256-CBC';
|
$cipher = 'AES-256-CBC';
|
||||||
@@ -46,16 +45,15 @@ function decryptData($encryptedData, $encryptionKey) {
|
|||||||
return openssl_decrypt($ciphertext, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
return openssl_decrypt($ciphertext, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load encryption key from an environment variable (default for testing; override in production)
|
// Load encryption key from environment (override in production).
|
||||||
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
|
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
|
||||||
if (!$encryptionKey) {
|
if (!$encryptionKey) {
|
||||||
die('Encryption key for persistent tokens is not set.');
|
die('Encryption key for persistent tokens is not set.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow an environment variable to override HTTPS detection.
|
// Determine whether HTTPS is used.
|
||||||
$envSecure = getenv('SECURE');
|
$envSecure = getenv('SECURE');
|
||||||
if ($envSecure !== false) {
|
if ($envSecure !== false) {
|
||||||
// Convert the environment variable value to a boolean.
|
|
||||||
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
|
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
|
||||||
} else {
|
} else {
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
@@ -64,22 +62,20 @@ if ($envSecure !== false) {
|
|||||||
$cookieParams = [
|
$cookieParams = [
|
||||||
'lifetime' => 7200,
|
'lifetime' => 7200,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'domain' => '', // Specify your domain if needed
|
'domain' => '', // Set your domain as needed.
|
||||||
'secure' => $secure,
|
'secure' => $secure,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
];
|
];
|
||||||
session_set_cookie_params($cookieParams);
|
session_set_cookie_params($cookieParams);
|
||||||
|
|
||||||
ini_set('session.gc_maxlifetime', 7200);
|
ini_set('session.gc_maxlifetime', 7200);
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Generate CSRF token if not already set.
|
|
||||||
if (empty($_SESSION['csrf_token'])) {
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-login via persistent token if session is not active.
|
// Auto-login via persistent token.
|
||||||
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
|
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
|
||||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
$persistentTokens = [];
|
$persistentTokens = [];
|
||||||
@@ -91,15 +87,12 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
|
|||||||
$persistentTokens = [];
|
$persistentTokens = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
if (isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
||||||
if ($tokenData['expiry'] >= time()) {
|
if ($tokenData['expiry'] >= time()) {
|
||||||
// Token is valid; auto-authenticate the user.
|
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $tokenData["username"];
|
$_SESSION["username"] = $tokenData["username"];
|
||||||
$_SESSION["isAdmin"] = $tokenData["isAdmin"]; // Restore admin status from the token
|
|
||||||
} else {
|
} else {
|
||||||
// Token expired; remove it and clear the cookie.
|
|
||||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||||
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
||||||
@@ -108,11 +101,8 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define BASE_URL (this should point to where index.html is, e.g. your uploads directory)
|
|
||||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||||
|
|
||||||
// If BASE_URL is still the default placeholder, use the server's HTTP_HOST.
|
|
||||||
// Otherwise, use BASE_URL and append share.php.
|
|
||||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||||
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
||||||
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
|
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
|
||||||
@@ -120,6 +110,5 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
|||||||
} else {
|
} else {
|
||||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
||||||
}
|
}
|
||||||
|
|
||||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||||
?>
|
?>
|
||||||
90
domUtils.js
@@ -136,18 +136,20 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||||
|
|
||||||
let previewButton = "";
|
let previewButton = "";
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|ogg)$/i.test(file.name)) {
|
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
let previewIcon = "";
|
let previewIcon = "";
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">image</i>`;
|
previewIcon = `<i class="material-icons">image</i>`;
|
||||||
} else if (/\.(mp4|webm|mov|ogg)$/i.test(file.name)) {
|
} else if (/\.(mp4|webm|mov)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">videocam</i>`;
|
previewIcon = `<i class="material-icons">videocam</i>`;
|
||||||
} else if (/\.pdf$/i.test(file.name)) {
|
} else if (/\.pdf$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
|
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
|
||||||
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
|
previewIcon = `<i class="material-icons">audiotrack</i>`;
|
||||||
}
|
}
|
||||||
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
previewButton = `<button class="btn btn-sm btn-info preview-btn" onclick="event.stopPropagation(); previewFile('${folderPath + encodeURIComponent(file.name)}', '${safeFileName}')">
|
||||||
${previewIcon}
|
${previewIcon}
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -231,86 +233,6 @@ export function toggleRowSelection(event, fileName) {
|
|||||||
updateFileActionButtons();
|
updateFileActionButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewFile(fileUrl, fileName) {
|
|
||||||
let modal = document.getElementById("filePreviewModal");
|
|
||||||
if (!modal) {
|
|
||||||
modal = document.createElement("div");
|
|
||||||
modal.id = "filePreviewModal";
|
|
||||||
Object.assign(modal.style, {
|
|
||||||
display: "none",
|
|
||||||
position: "fixed",
|
|
||||||
top: "0",
|
|
||||||
left: "0",
|
|
||||||
width: "100vw",
|
|
||||||
height: "100vh",
|
|
||||||
backgroundColor: "rgba(0,0,0,0.7)",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
zIndex: "1000"
|
|
||||||
});
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="modal-content image-preview-modal-content">
|
|
||||||
<span id="closeFileModal" class="close-image-modal">×</span>
|
|
||||||
<h4 class="image-modal-header"></h4>
|
|
||||||
<div class="file-preview-container"></div>
|
|
||||||
</div>`;
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
|
|
||||||
document.getElementById("closeFileModal").addEventListener("click", function () {
|
|
||||||
const video = modal.querySelector("video");
|
|
||||||
if (video) {
|
|
||||||
video.pause();
|
|
||||||
video.currentTime = 0;
|
|
||||||
}
|
|
||||||
modal.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
modal.addEventListener("click", function (e) {
|
|
||||||
if (e.target === modal) {
|
|
||||||
const video = modal.querySelector("video");
|
|
||||||
if (video) {
|
|
||||||
video.pause();
|
|
||||||
video.currentTime = 0;
|
|
||||||
}
|
|
||||||
modal.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.querySelector("h4").textContent = fileName;
|
|
||||||
const container = modal.querySelector(".file-preview-container");
|
|
||||||
container.innerHTML = "";
|
|
||||||
|
|
||||||
const extension = fileName.split('.').pop().toLowerCase();
|
|
||||||
|
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(fileName)) {
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.src = fileUrl;
|
|
||||||
img.className = "image-modal-img";
|
|
||||||
container.appendChild(img);
|
|
||||||
} else if (extension === "pdf") {
|
|
||||||
const embed = document.createElement("embed");
|
|
||||||
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
|
|
||||||
embed.src = fileUrl + separator + 't=' + new Date().getTime();
|
|
||||||
embed.type = "application/pdf";
|
|
||||||
embed.style.width = "80vw";
|
|
||||||
embed.style.height = "80vh";
|
|
||||||
embed.style.border = "none";
|
|
||||||
container.appendChild(embed);
|
|
||||||
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
|
|
||||||
const video = document.createElement("video");
|
|
||||||
video.src = fileUrl;
|
|
||||||
video.controls = true;
|
|
||||||
video.className = "image-modal-img";
|
|
||||||
container.appendChild(video);
|
|
||||||
} else {
|
|
||||||
container.textContent = "Preview not available for this file type.";
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.style.display = "flex";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function attachEnterKeyListener(modalId, buttonId) {
|
export function attachEnterKeyListener(modalId, buttonId) {
|
||||||
const modal = document.getElementById(modalId);
|
const modal = document.getElementById(modalId);
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
|||||||
364
dragAndDrop.js
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
// dragAndDrop.js
|
||||||
|
|
||||||
|
// Moves cards into the sidebar based on the saved order in localStorage.
|
||||||
|
export function loadSidebarOrder() {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (!sidebar) return;
|
||||||
|
const orderStr = localStorage.getItem('sidebarOrder');
|
||||||
|
if (orderStr) {
|
||||||
|
const order = JSON.parse(orderStr);
|
||||||
|
if (order.length > 0) {
|
||||||
|
// Ensure main wrapper is visible.
|
||||||
|
const mainWrapper = document.querySelector('.main-wrapper');
|
||||||
|
if (mainWrapper) {
|
||||||
|
mainWrapper.style.display = 'flex';
|
||||||
|
}
|
||||||
|
// For each saved ID, move the card into the sidebar.
|
||||||
|
order.forEach(id => {
|
||||||
|
const card = document.getElementById(id);
|
||||||
|
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||||
|
sidebar.appendChild(card);
|
||||||
|
// Animate vertical slide for sidebar card
|
||||||
|
animateVerticalSlide(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateSidebarVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper: update sidebar visibility based on its content.
|
||||||
|
function updateSidebarVisibility() {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
|
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
|
if (cards.length > 0) {
|
||||||
|
sidebar.classList.add('active');
|
||||||
|
sidebar.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
sidebar.classList.remove('active');
|
||||||
|
sidebar.style.display = 'none';
|
||||||
|
}
|
||||||
|
// Save the current order in localStorage.
|
||||||
|
saveSidebarOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper: update top zone layout (center a card if one column is empty).
|
||||||
|
function updateTopZoneLayout() {
|
||||||
|
const leftCol = document.getElementById('leftCol');
|
||||||
|
const rightCol = document.getElementById('rightCol');
|
||||||
|
|
||||||
|
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
|
||||||
|
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
|
||||||
|
|
||||||
|
if (leftIsEmpty && !rightIsEmpty) {
|
||||||
|
leftCol.style.display = 'none';
|
||||||
|
rightCol.style.margin = '0 auto';
|
||||||
|
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||||
|
rightCol.style.display = 'none';
|
||||||
|
leftCol.style.margin = '0 auto';
|
||||||
|
} else {
|
||||||
|
leftCol.style.display = '';
|
||||||
|
rightCol.style.display = '';
|
||||||
|
leftCol.style.margin = '';
|
||||||
|
rightCol.style.margin = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||||
|
function addTopZoneHighlight() {
|
||||||
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
|
if (topZone) {
|
||||||
|
topZone.classList.add('highlight');
|
||||||
|
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||||
|
topZone.style.minHeight = '375px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the drag ends, remove the extra min-height.
|
||||||
|
function removeTopZoneHighlight() {
|
||||||
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
|
if (topZone) {
|
||||||
|
topZone.classList.remove('highlight');
|
||||||
|
topZone.style.minHeight = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical slide/fade animation helper.
|
||||||
|
function animateVerticalSlide(card) {
|
||||||
|
card.style.transform = 'translateY(30px)';
|
||||||
|
card.style.opacity = '0';
|
||||||
|
// Force reflow.
|
||||||
|
card.offsetWidth;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.transition = '';
|
||||||
|
card.style.transform = '';
|
||||||
|
card.style.opacity = '';
|
||||||
|
}, 310);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||||
|
function insertCardInSidebar(card, event) {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (!sidebar) return;
|
||||||
|
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||||
|
let inserted = false;
|
||||||
|
for (const currentCard of existingCards) {
|
||||||
|
const rect = currentCard.getBoundingClientRect();
|
||||||
|
const midY = rect.top + rect.height / 2;
|
||||||
|
if (event.clientY < midY) {
|
||||||
|
sidebar.insertBefore(card, currentCard);
|
||||||
|
inserted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inserted) {
|
||||||
|
sidebar.appendChild(card);
|
||||||
|
}
|
||||||
|
// Ensure card fills the sidebar.
|
||||||
|
card.style.width = '100%';
|
||||||
|
animateVerticalSlide(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper: save the current sidebar card order to localStorage.
|
||||||
|
function saveSidebarOrder() {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
|
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
|
const order = Array.from(cards).map(card => card.id);
|
||||||
|
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||||
|
function moveSidebarCardsToTop() {
|
||||||
|
if (window.innerWidth < 1205) {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (!sidebar) return;
|
||||||
|
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||||
|
cards.forEach(card => {
|
||||||
|
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||||
|
if (orig) {
|
||||||
|
orig.appendChild(card);
|
||||||
|
animateVerticalSlide(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateSidebarVisibility();
|
||||||
|
updateTopZoneLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
if (window.innerWidth < 1205) {
|
||||||
|
moveSidebarCardsToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||||
|
function ensureTopZonePlaceholder() {
|
||||||
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
|
if (!topZone) return;
|
||||||
|
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||||
|
let placeholder = topZone.querySelector('.placeholder');
|
||||||
|
if (!placeholder) {
|
||||||
|
placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'placeholder';
|
||||||
|
placeholder.style.visibility = 'hidden';
|
||||||
|
placeholder.style.display = 'block';
|
||||||
|
placeholder.style.width = '100%';
|
||||||
|
placeholder.style.height = '375px';
|
||||||
|
topZone.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const placeholder = topZone.querySelector('.placeholder');
|
||||||
|
if (placeholder) placeholder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This sets up all drag-and-drop event listeners for cards.
|
||||||
|
export function initDragAndDrop() {
|
||||||
|
function run() {
|
||||||
|
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
|
draggableCards.forEach(card => {
|
||||||
|
if (!card.dataset.originalContainerId) {
|
||||||
|
card.dataset.originalContainerId = card.parentNode.id;
|
||||||
|
}
|
||||||
|
const header = card.querySelector('.card-header');
|
||||||
|
if (header) {
|
||||||
|
header.classList.add('drag-header');
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let dragTimer = null;
|
||||||
|
let offsetX = 0, offsetY = 0;
|
||||||
|
let initialLeft, initialTop;
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
header.addEventListener('mousedown', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const card = this.closest('.card');
|
||||||
|
// Capture the card's initial bounding rectangle once.
|
||||||
|
const initialRect = card.getBoundingClientRect();
|
||||||
|
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
|
||||||
|
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
|
||||||
|
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||||
|
|
||||||
|
// Store the initial rect so we use it later.
|
||||||
|
dragTimer = setTimeout(() => {
|
||||||
|
isDragging = true;
|
||||||
|
card.classList.add('dragging');
|
||||||
|
card.style.pointerEvents = 'none';
|
||||||
|
addTopZoneHighlight();
|
||||||
|
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.add('active');
|
||||||
|
sidebar.style.display = 'block';
|
||||||
|
sidebar.classList.add('highlight');
|
||||||
|
sidebar.style.height = '800px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the stored initialRect rather than recalculating.
|
||||||
|
initialLeft = initialRect.left + window.pageXOffset;
|
||||||
|
initialTop = initialRect.top + window.pageYOffset;
|
||||||
|
offsetX = e.pageX - initialLeft;
|
||||||
|
offsetY = e.pageY - initialTop;
|
||||||
|
|
||||||
|
// Append card to body and fix its dimensions to prevent shrinking.
|
||||||
|
document.body.appendChild(card);
|
||||||
|
card.style.position = 'absolute';
|
||||||
|
card.style.left = initialLeft + 'px';
|
||||||
|
card.style.top = initialTop + 'px';
|
||||||
|
card.style.width = initialRect.width + 'px';
|
||||||
|
card.style.height = initialRect.height + 'px';
|
||||||
|
card.style.minWidth = initialRect.width + 'px';
|
||||||
|
card.style.flexShrink = '0';
|
||||||
|
card.style.zIndex = '10000';
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
header.addEventListener('mouseup', function () {
|
||||||
|
clearTimeout(dragTimer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', function (e) {
|
||||||
|
if (isDragging) {
|
||||||
|
card.style.left = (e.pageX - offsetX) + 'px';
|
||||||
|
card.style.top = (e.pageY - offsetY) + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', function (e) {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
card.style.pointerEvents = '';
|
||||||
|
card.classList.remove('dragging');
|
||||||
|
removeTopZoneHighlight();
|
||||||
|
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.remove('highlight');
|
||||||
|
sidebar.style.height = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let droppedInSidebar = false;
|
||||||
|
let droppedInTop = false;
|
||||||
|
|
||||||
|
// Check if dropped in sidebar drop zone.
|
||||||
|
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebarElem) {
|
||||||
|
const rect = sidebarElem.getBoundingClientRect();
|
||||||
|
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
||||||
|
if (
|
||||||
|
e.clientX >= rect.left &&
|
||||||
|
e.clientX <= rect.right &&
|
||||||
|
e.clientY >= rect.top &&
|
||||||
|
e.clientY <= dropZoneBottom
|
||||||
|
) {
|
||||||
|
insertCardInSidebar(card, e);
|
||||||
|
droppedInSidebar = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not dropped in sidebar, check the top drop zone.
|
||||||
|
const topRow = document.getElementById('uploadFolderRow');
|
||||||
|
if (!droppedInSidebar && topRow) {
|
||||||
|
const rect = topRow.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
e.clientX >= rect.left &&
|
||||||
|
e.clientX <= rect.right &&
|
||||||
|
e.clientY >= rect.top &&
|
||||||
|
e.clientY <= rect.bottom
|
||||||
|
) {
|
||||||
|
let container;
|
||||||
|
if (card.id === 'uploadCard') {
|
||||||
|
container = document.getElementById('leftCol');
|
||||||
|
} else if (card.id === 'folderManagementCard') {
|
||||||
|
container = document.getElementById('rightCol');
|
||||||
|
}
|
||||||
|
if (container) {
|
||||||
|
ensureTopZonePlaceholder();
|
||||||
|
updateTopZoneLayout();
|
||||||
|
container.appendChild(card);
|
||||||
|
droppedInTop = true;
|
||||||
|
// Use computed style to determine container's width.
|
||||||
|
const containerWidth = parseFloat(window.getComputedStyle(container).width);
|
||||||
|
card.style.width = "363px";
|
||||||
|
// Animate the card sliding in.
|
||||||
|
animateVerticalSlide(card);
|
||||||
|
// After animation completes, clear the inline width.
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.removeProperty('width');
|
||||||
|
}, 210);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If dropped in neither area, return card to its original container.
|
||||||
|
if (!droppedInSidebar && !droppedInTop) {
|
||||||
|
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||||
|
if (orig) {
|
||||||
|
orig.appendChild(card);
|
||||||
|
card.style.removeProperty('width');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear inline styles from dragging.
|
||||||
|
[
|
||||||
|
'position',
|
||||||
|
'left',
|
||||||
|
'top',
|
||||||
|
'z-index',
|
||||||
|
'height',
|
||||||
|
'min-width',
|
||||||
|
'flex-shrink',
|
||||||
|
'transition',
|
||||||
|
'transform',
|
||||||
|
'opacity'
|
||||||
|
].forEach(prop => card.style.removeProperty(prop));
|
||||||
|
|
||||||
|
// For sidebar drops, force width to 100%.
|
||||||
|
if (droppedInSidebar) {
|
||||||
|
card.style.width = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTopZoneLayout();
|
||||||
|
updateSidebarVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', run);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,8 +80,9 @@ $srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMet
|
|||||||
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
|
||||||
$allSuccess = true;
|
$allSuccess = true;
|
||||||
|
$extractedFiles = array(); // Array to collect names of extracted files
|
||||||
|
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||||
|
|
||||||
// ---------- Process Each File ----------
|
// ---------- Process Each File ----------
|
||||||
foreach ($files as $zipFileName) {
|
foreach ($files as $zipFileName) {
|
||||||
@@ -115,6 +116,14 @@ foreach ($files as $zipFileName) {
|
|||||||
$errors[] = "Failed to extract $originalName.";
|
$errors[] = "Failed to extract $originalName.";
|
||||||
$allSuccess = false;
|
$allSuccess = false;
|
||||||
} else {
|
} else {
|
||||||
|
// Collect extracted file names from this zip.
|
||||||
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||||
|
$entryName = $zip->getNameIndex($i);
|
||||||
|
$extractedFileName = basename($entryName);
|
||||||
|
if ($extractedFileName) {
|
||||||
|
$extractedFiles[] = $extractedFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Update metadata for each extracted file if the zip file has metadata.
|
// Update metadata for each extracted file if the zip file has metadata.
|
||||||
if (isset($srcMetadata[$originalName])) {
|
if (isset($srcMetadata[$originalName])) {
|
||||||
$zipMeta = $srcMetadata[$originalName];
|
$zipMeta = $srcMetadata[$originalName];
|
||||||
@@ -138,7 +147,7 @@ if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRIN
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($allSuccess) {
|
if ($allSuccess) {
|
||||||
echo json_encode(["success" => true]);
|
echo json_encode(["success" => true, "extractedFiles" => $extractedFiles]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
|
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
updateRowHighlight,
|
updateRowHighlight,
|
||||||
toggleRowSelection,
|
toggleRowSelection,
|
||||||
attachEnterKeyListener,
|
attachEnterKeyListener
|
||||||
previewFile as originalPreviewFile
|
|
||||||
} from './domUtils.js';
|
} from './domUtils.js';
|
||||||
|
|
||||||
export let fileData = [];
|
export let fileData = [];
|
||||||
@@ -153,7 +152,7 @@ function openShareModal(file, folder) {
|
|||||||
// ==============================================
|
// ==============================================
|
||||||
// FEATURE: Enhanced Preview Modal with Navigation
|
// FEATURE: Enhanced Preview Modal with Navigation
|
||||||
// ==============================================
|
// ==============================================
|
||||||
function enhancedPreviewFile(fileUrl, fileName) {
|
function previewFile(fileUrl, fileName) {
|
||||||
let modal = document.getElementById("filePreviewModal");
|
let modal = document.getElementById("filePreviewModal");
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
modal = document.createElement("div");
|
modal = document.createElement("div");
|
||||||
@@ -178,12 +177,24 @@ function enhancedPreviewFile(fileUrl, fileName) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
document.getElementById("closeFileModal").addEventListener("click", function () {
|
function closeModal() {
|
||||||
|
// Pause and reset any video or audio elements within the modal
|
||||||
|
const mediaElements = modal.querySelectorAll("video, audio");
|
||||||
|
mediaElements.forEach(media => {
|
||||||
|
media.pause();
|
||||||
|
try {
|
||||||
|
media.currentTime = 0;
|
||||||
|
} catch(e) {
|
||||||
|
// Some media types might not support setting currentTime.
|
||||||
|
}
|
||||||
|
});
|
||||||
modal.style.display = "none";
|
modal.style.display = "none";
|
||||||
});
|
}
|
||||||
|
|
||||||
|
document.getElementById("closeFileModal").addEventListener("click", closeModal);
|
||||||
modal.addEventListener("click", function (e) {
|
modal.addEventListener("click", function (e) {
|
||||||
if (e.target === modal) {
|
if (e.target === modal) {
|
||||||
modal.style.display = "none";
|
closeModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -253,6 +264,13 @@ function enhancedPreviewFile(fileUrl, fileName) {
|
|||||||
video.controls = true;
|
video.controls = true;
|
||||||
video.className = "image-modal-img";
|
video.className = "image-modal-img";
|
||||||
container.appendChild(video);
|
container.appendChild(video);
|
||||||
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) {
|
||||||
|
const audio = document.createElement("audio");
|
||||||
|
audio.src = fileUrl;
|
||||||
|
audio.controls = true;
|
||||||
|
audio.className = "audio-modal";
|
||||||
|
audio.style.maxWidth = "80vw";
|
||||||
|
container.appendChild(audio);
|
||||||
} else {
|
} else {
|
||||||
container.textContent = "Preview not available for this file type.";
|
container.textContent = "Preview not available for this file type.";
|
||||||
}
|
}
|
||||||
@@ -260,10 +278,6 @@ function enhancedPreviewFile(fileUrl, fileName) {
|
|||||||
modal.style.display = "flex";
|
modal.style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewFile(fileUrl, fileName) {
|
|
||||||
enhancedPreviewFile(fileUrl, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==============================================
|
// ==============================================
|
||||||
// ORIGINAL FILE MANAGER FUNCTIONS
|
// ORIGINAL FILE MANAGER FUNCTIONS
|
||||||
// ==============================================
|
// ==============================================
|
||||||
@@ -519,6 +533,8 @@ export function renderGalleryView(folder) {
|
|||||||
let thumbnail;
|
let thumbnail;
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
|
||||||
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`;
|
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`;
|
||||||
|
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||||
|
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||||
} else {
|
} else {
|
||||||
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
||||||
}
|
}
|
||||||
@@ -756,7 +772,12 @@ export function handleExtractZipSelected(e) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast("Zip file(s) extracted successfully!");
|
// If the server returned a list of extracted files, join them into a string.
|
||||||
|
let toastMessage = "Zip file(s) extracted successfully!";
|
||||||
|
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||||
|
toastMessage = "Extracted: " + data.extractedFiles.join(", ");
|
||||||
|
}
|
||||||
|
showToast(toastMessage);
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
||||||
@@ -1371,13 +1392,15 @@ document.addEventListener("keydown", function(e) {
|
|||||||
|
|
||||||
// ---------- CONTEXT MENU SUPPORT FOR FILE LIST ----------
|
// ---------- CONTEXT MENU SUPPORT FOR FILE LIST ----------
|
||||||
|
|
||||||
|
// Function to display the context menu with provided items at (x, y)
|
||||||
// Function to display the context menu with provided items at (x, y)
|
// Function to display the context menu with provided items at (x, y)
|
||||||
function showFileContextMenu(x, y, menuItems) {
|
function showFileContextMenu(x, y, menuItems) {
|
||||||
let menu = document.getElementById("fileContextMenu");
|
let menu = document.getElementById("fileContextMenu");
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
menu = document.createElement("div");
|
menu = document.createElement("div");
|
||||||
menu.id = "fileContextMenu";
|
menu.id = "fileContextMenu";
|
||||||
menu.style.position = "absolute";
|
// Use fixed positioning so the menu is relative to the viewport
|
||||||
|
menu.style.position = "fixed";
|
||||||
menu.style.backgroundColor = "#fff";
|
menu.style.backgroundColor = "#fff";
|
||||||
menu.style.border = "1px solid #ccc";
|
menu.style.border = "1px solid #ccc";
|
||||||
menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)";
|
menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)";
|
||||||
@@ -1394,11 +1417,7 @@ function showFileContextMenu(x, y, menuItems) {
|
|||||||
menuItem.style.padding = "5px 15px";
|
menuItem.style.padding = "5px 15px";
|
||||||
menuItem.style.cursor = "pointer";
|
menuItem.style.cursor = "pointer";
|
||||||
menuItem.addEventListener("mouseover", () => {
|
menuItem.addEventListener("mouseover", () => {
|
||||||
if (document.body.classList.contains("dark-mode")) {
|
menuItem.style.backgroundColor = document.body.classList.contains("dark-mode") ? "#444" : "#f0f0f0";
|
||||||
menuItem.style.backgroundColor = "#444"; // darker gray for dark mode
|
|
||||||
} else {
|
|
||||||
menuItem.style.backgroundColor = "#f0f0f0"; // light gray for light mode
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
menuItem.addEventListener("mouseout", () => {
|
menuItem.addEventListener("mouseout", () => {
|
||||||
menuItem.style.backgroundColor = "";
|
menuItem.style.backgroundColor = "";
|
||||||
@@ -1409,9 +1428,20 @@ function showFileContextMenu(x, y, menuItems) {
|
|||||||
});
|
});
|
||||||
menu.appendChild(menuItem);
|
menu.appendChild(menuItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use the event's clientX and clientY coordinates (which are viewport-relative)
|
||||||
menu.style.left = x + "px";
|
menu.style.left = x + "px";
|
||||||
menu.style.top = y + "px";
|
menu.style.top = y + "px";
|
||||||
menu.style.display = "block";
|
menu.style.display = "block";
|
||||||
|
|
||||||
|
// Adjust if the menu would extend past the bottom of the viewport
|
||||||
|
const menuRect = menu.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
if (menuRect.bottom > viewportHeight) {
|
||||||
|
let newTop = viewportHeight - menuRect.height;
|
||||||
|
if (newTop < 0) newTop = 0;
|
||||||
|
menu.style.top = newTop + "px";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideFileContextMenu() {
|
function hideFileContextMenu() {
|
||||||
@@ -1484,7 +1514,7 @@ function fileListContextMenuHandler(e) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showFileContextMenu(e.pageX, e.pageY, menuItems);
|
showFileContextMenu(e.clientX, e.clientY, menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind the context menu to the file list container.
|
// Bind the context menu to the file list container.
|
||||||
|
|||||||
@@ -63,8 +63,6 @@ function getParentFolder(folder) {
|
|||||||
// Breadcrumb Functions
|
// Breadcrumb Functions
|
||||||
// ----------------------
|
// ----------------------
|
||||||
// Render breadcrumb for a normalized folder path.
|
// Render breadcrumb for a normalized folder path.
|
||||||
// For example, if window.currentFolder is "Folder1/Folder1SubFolder2",
|
|
||||||
// this will return: Root / Folder1 / Folder1SubFolder2.
|
|
||||||
function renderBreadcrumb(normalizedFolder) {
|
function renderBreadcrumb(normalizedFolder) {
|
||||||
if (normalizedFolder === "root") {
|
if (normalizedFolder === "root") {
|
||||||
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
||||||
@@ -307,16 +305,10 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = `<div id="rootRow" class="root-row">
|
let html = `<div id="rootRow" class="root-row">
|
||||||
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
||||||
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
if (folders.length === 0) {
|
if (folders.length > 0) {
|
||||||
html += `<ul class="folder-tree expanded">
|
|
||||||
<li class="folder-item">
|
|
||||||
<span class="folder-option" data-folder="root">(Root)</span>
|
|
||||||
</li>
|
|
||||||
</ul>`;
|
|
||||||
} else {
|
|
||||||
const tree = buildFolderTree(folders);
|
const tree = buildFolderTree(folders);
|
||||||
html += renderFolderTree(tree, "", "block");
|
html += renderFolderTree(tree, "", "block");
|
||||||
}
|
}
|
||||||
@@ -730,8 +722,8 @@ document.addEventListener("click", function () {
|
|||||||
hideFolderManagerContextMenu();
|
hideFolderManagerContextMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.addEventListener("keydown", function(e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
||||||
const tag = e.target.tagName.toLowerCase();
|
const tag = e.target.tagName.toLowerCase();
|
||||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||||
|
|||||||
30
getConfig.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
require 'config.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$configFile = USERS_DIR . 'adminConfig.json';
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$encryptedContent = file_get_contents($configFile);
|
||||||
|
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||||
|
if ($decryptedContent === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Failed to decrypt configuration.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo $decryptedContent;
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'oidc' => [
|
||||||
|
'providerUrl' => 'https://your-oidc-provider.com',
|
||||||
|
'clientId' => 'YOUR_CLIENT_ID',
|
||||||
|
'clientSecret' => 'YOUR_CLIENT_SECRET',
|
||||||
|
'redirectUri' => 'https://yourdomain.com/auth.php?oidc=callback'
|
||||||
|
],
|
||||||
|
'loginOptions' => [
|
||||||
|
'disableFormLogin' => false,
|
||||||
|
'disableBasicAuth' => false,
|
||||||
|
'disableOIDCLogin' => false
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
419
index.html
@@ -4,7 +4,7 @@
|
|||||||
<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" />
|
||||||
<title>Multi File Upload Editor</title>
|
<title>FileRise</title>
|
||||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||||
<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="">
|
||||||
@@ -36,9 +36,15 @@
|
|||||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<!-- Drop shadow filter -->
|
<!-- Drop shadow filter with animated attributes for a lifting effect -->
|
||||||
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
|
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2" />
|
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
|
||||||
|
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
|
||||||
|
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||||
|
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
|
||||||
|
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
|
||||||
|
fill="freeze" />
|
||||||
|
</feDropShadow>
|
||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@@ -62,31 +68,32 @@
|
|||||||
fill: #1565C0;
|
fill: #1565C0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
<!-- Group that will animate upward and then back down once -->
|
||||||
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
<g id="cabinetGroup">
|
||||||
<!-- Divider lines for drawers -->
|
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
||||||
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
||||||
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
<!-- Divider lines for drawers -->
|
||||||
<!-- Drawers with Handles -->
|
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
||||||
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
||||||
<circle cx="54" cy="27" r="1.5" class="handle" />
|
<!-- Drawers with Handles -->
|
||||||
|
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||||
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
<circle cx="54" cy="27" r="1.5" class="handle" />
|
||||||
<circle cx="54" cy="39" r="1.5" class="handle" />
|
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||||
|
<circle cx="54" cy="39" r="1.5" class="handle" />
|
||||||
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||||
<circle cx="54" cy="51" r="1.5" class="handle" />
|
<circle cx="54" cy="51" r="1.5" class="handle" />
|
||||||
|
<!-- Additional detail: a small top handle on the cabinet door -->
|
||||||
<!-- Additional detail: a small top handle on the cabinet door -->
|
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
||||||
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
|
||||||
|
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<h1>Multi File Upload Editor</h1>
|
<h1>FileRise</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
<button id="logoutBtn" title="Logout">
|
<button id="logoutBtn" title="Logout">
|
||||||
@@ -95,7 +102,6 @@
|
|||||||
<button id="changePasswordBtn" title="Change Password">
|
<button id="changePasswordBtn" title="Change Password">
|
||||||
<i class="material-icons">vpn_key</i>
|
<i class="material-icons">vpn_key</i>
|
||||||
</button>
|
</button>
|
||||||
<!-- Restore Files Modal (Admin Only) -->
|
|
||||||
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
|
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h4 class="custom-restore-header">
|
<h4 class="custom-restore-header">
|
||||||
@@ -117,10 +123,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="addUserBtn" title="Add User">
|
<button id="addUserBtn" title="Add User" style="display: none;">
|
||||||
<i class="material-icons">person_add</i>
|
<i class="material-icons">person_add</i>
|
||||||
</button>
|
</button>
|
||||||
<button id="removeUserBtn" title="Remove User">
|
<button id="removeUserBtn" title="Remove User" style="display: none;">
|
||||||
<i class="material-icons">person_remove</i>
|
<i class="material-icons">person_remove</i>
|
||||||
</button>
|
</button>
|
||||||
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
|
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
|
||||||
@@ -130,198 +136,201 @@
|
|||||||
|
|
||||||
<!-- Custom Toast Container -->
|
<!-- Custom Toast Container -->
|
||||||
<div id="customToast"></div>
|
<div id="customToast"></div>
|
||||||
<div class="container-fluid">
|
|
||||||
<!-- Login Form -->
|
|
||||||
<div class="row" id="loginForm">
|
|
||||||
<div class="col-12">
|
|
||||||
<form id="authForm" method="post">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="loginUsername">User:</label>
|
|
||||||
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="loginPassword">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">Login</button>
|
|
||||||
<div class="form-group remember-me-container">
|
|
||||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
|
||||||
<label for="rememberMeCheckbox">Remember me</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Operations: Upload and Folder Management -->
|
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
||||||
<div id="mainOperations">
|
<div class="main-wrapper">
|
||||||
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
|
||||||
<div class="row align-items-start" id="uploadFolderRow">
|
<div id="sidebarDropArea" class="drop-target-sidebar"></div>
|
||||||
<!-- Upload Card: 50% width on medium, 58% on large -->
|
<!-- Main Column -->
|
||||||
<div class="col-md-6 col-lg-7 d-flex">
|
<div id="mainColumn" class="main-column">
|
||||||
<div id="uploadCard" class="card flex-fill" style="max-width: 900px; width: 100%;">
|
<div class="container-fluid">
|
||||||
<div class="card-header">Upload Files/Folders</div>
|
<!-- Login Form (unchanged) -->
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="row" id="loginForm">
|
||||||
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column"
|
<div class="col-12">
|
||||||
style="height: 100%;" novalidate>
|
<form id="authForm" method="post">
|
||||||
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
|
<div class="form-group">
|
||||||
<div id="uploadDropArea"
|
<label for="loginUsername">User:</label>
|
||||||
style="border:2px dashed #ccc; padding:20px; cursor:pointer; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
|
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
||||||
<span>Drop files/folders here or click 'Choose Files'</span>
|
|
||||||
<br />
|
|
||||||
<!-- Note: Remove directory attributes so file picker only allows files -->
|
|
||||||
<input type="file" id="file" name="file[]" class="form-control-file" multiple
|
|
||||||
style="opacity:0; position:absolute; width:1px; height:1px;" />
|
|
||||||
<button type="button" id="customChooseBtn">Choose Files</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
|
|
||||||
<div id="uploadProgressContainer"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="loginPassword">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">Login</button>
|
||||||
|
<div class="form-group remember-me-container">
|
||||||
|
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||||
|
<label for="rememberMeCheckbox">Remember me</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<!-- OIDC Login Option -->
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<button id="oidcLoginBtn" class="btn btn-secondary">Login with OIDC</button>
|
||||||
|
</div>
|
||||||
|
<!-- Basic HTTP Login Option -->
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="login_basic.php" class="btn btn-secondary">Use Basic HTTP Login</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Folder Management Card -->
|
<!-- Main Operations: Upload and Folder Management -->
|
||||||
<div class="col-md-6 col-lg-5 d-flex">
|
<div id="mainOperations">
|
||||||
<div id="folderManagementCard" class="card flex-fill"
|
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
||||||
style="max-width: 900px; width: 100%; position: relative;">
|
<!-- Top Zone: Two columns (60% and 40%) -->
|
||||||
<!-- Card header with folder management title and help icon -->
|
<div id="uploadFolderRow" class="row">
|
||||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
<!-- Left Column (60% for Upload Card) -->
|
||||||
<span>Folder Navigation & Management</span>
|
<div id="leftCol" class="col-md-7" style="display: flex; justify-content: center;">
|
||||||
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
|
<div id="uploadCard" class="card" style="width: 100%;">
|
||||||
style="padding: 0; border: none; background: none;">
|
<div class="card-header">Upload Files/Folders</div>
|
||||||
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
|
<div class="card-body d-flex flex-column">
|
||||||
</button>
|
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
|
||||||
|
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
|
||||||
|
<div id="uploadDropArea"
|
||||||
|
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
|
||||||
|
<span>Drop files/folders here or click 'Choose Files'</span>
|
||||||
|
<br />
|
||||||
|
<input type="file" id="file" name="file[]" class="form-control-file" multiple
|
||||||
|
style="opacity:0; position:absolute; width:1px; height:1px;" />
|
||||||
|
<button type="button" id="customChooseBtn">Choose Files</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
|
||||||
|
<div id="uploadProgressContainer"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body custom-folder-card-body">
|
<!-- Right Column (40% for Folder Management Card) -->
|
||||||
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
|
<div id="rightCol" class="col-md-5" style="display: flex; justify-content: center;">
|
||||||
<div id="folderTreeContainer"></div>
|
<div id="folderManagementCard" class="card" style="width: 100%; position: relative;">
|
||||||
</div>
|
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<!-- Folder actions (create, rename, delete) -->
|
<span>Folder Navigation & Management</span>
|
||||||
<div class="folder-actions mt-3">
|
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
|
||||||
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
|
style="padding: 0; border: none; background: none;">
|
||||||
<!-- Create Folder Modal -->
|
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
|
||||||
<div id="createFolderModal" class="modal">
|
</button>
|
||||||
<div class="modal-content">
|
</div>
|
||||||
<h4>Create Folder</h4>
|
<div class="card-body custom-folder-card-body">
|
||||||
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
|
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
|
||||||
style="margin-top:10px;" />
|
<div id="folderTreeContainer"></div>
|
||||||
<div style="margin-top:15px; text-align:right;">
|
</div>
|
||||||
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
|
<div class="folder-actions mt-3">
|
||||||
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
|
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
|
||||||
|
<div id="createFolderModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Create Folder</h4>
|
||||||
|
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
|
||||||
|
style="margin-top:10px;" />
|
||||||
|
<div style="margin-top:15px; text-align:right;">
|
||||||
|
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
|
||||||
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
|
</button>
|
||||||
|
<div id="renameFolderModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Rename Folder</h4>
|
||||||
|
<input type="text" id="newRenameFolderName" class="form-control"
|
||||||
|
placeholder="Enter new folder name" style="margin-top:10px;" />
|
||||||
|
<div style="margin-top:15px; text-align:right;">
|
||||||
|
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
|
||||||
|
<i class="material-icons">delete</i>
|
||||||
|
</button>
|
||||||
|
<div id="deleteFolderModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Delete Folder</h4>
|
||||||
|
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
|
||||||
|
<div style="margin-top:15px; text-align:right;">
|
||||||
|
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id="folderHelpTooltip" class="folder-help-tooltip"
|
||||||
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
|
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
|
||||||
<i class="material-icons">drive_file_rename_outline</i>
|
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
|
||||||
</button>
|
<li>Click on a folder in the tree to view its files.</li>
|
||||||
<!-- Rename Folder Modal -->
|
<li>Use [-] to collapse and [+] to expand folders.</li>
|
||||||
<div id="renameFolderModal" class="modal">
|
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
|
||||||
<div class="modal-content">
|
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
|
||||||
<h4>Rename Folder</h4>
|
</ul>
|
||||||
<input type="text" id="newRenameFolderName" class="form-control"
|
|
||||||
placeholder="Enter new folder name" style="margin-top:10px;" />
|
|
||||||
<div style="margin-top:15px; text-align:right;">
|
|
||||||
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
|
|
||||||
<i class="material-icons">delete</i>
|
|
||||||
</button>
|
|
||||||
<!-- Delete Folder Modal -->
|
|
||||||
<div id="deleteFolderModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h4>Delete Folder</h4>
|
|
||||||
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
|
|
||||||
<div style="margin-top:15px; text-align:right;">
|
|
||||||
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Help Tooltip: Initially hidden -->
|
</div>
|
||||||
<div id="folderHelpTooltip" class="folder-help-tooltip"
|
</div> <!-- end uploadFolderRow -->
|
||||||
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
|
</div> <!-- end container -->
|
||||||
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
|
</div> <!-- end mainOperations -->
|
||||||
<li>Click on a folder in the tree to view its files.</li>
|
|
||||||
<li>Use [-] to collapse and [+] to expand folders.</li>
|
<!-- File List Section -->
|
||||||
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
|
<div id="fileListContainer" style="display: none;">
|
||||||
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
|
<h2 id="fileListTitle">Files in (Root)</h2>
|
||||||
</ul>
|
<div id="fileListActions" class="file-list-actions">
|
||||||
|
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
|
||||||
|
<div id="deleteFilesModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Delete Selected Files</h4>
|
||||||
|
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
|
||||||
|
<div id="copyFilesModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Copy Selected Files</h4>
|
||||||
|
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
|
||||||
|
<select id="copyTargetFolder" class="form-control modal-input"></select>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
|
||||||
|
<div id="moveFilesModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Move Selected Files</h4>
|
||||||
|
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
|
||||||
|
<select id="moveTargetFolder" class="form-control modal-input"></select>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
|
||||||
|
<button id="extractZipBtn" class="btn btn-sm btn-info" title="Extract Zip">Extract Zip</button>
|
||||||
|
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Download Selected Files as Zip</h4>
|
||||||
|
<p>Enter a name for the zip file:</p>
|
||||||
|
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
|
||||||
|
<div class="modal-footer" style="margin-top:15px; text-align:right;">
|
||||||
|
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
|
||||||
|
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="fileList"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> <!-- end container-fluid -->
|
||||||
</div>
|
</div> <!-- end mainColumn -->
|
||||||
<!-- File List Section -->
|
</div> <!-- end main-wrapper -->
|
||||||
<div id="fileListContainer" style="display: none;">
|
|
||||||
<h2 id="fileListTitle">Files in (Root)</h2>
|
|
||||||
<div id="fileListActions" class="file-list-actions">
|
|
||||||
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
|
|
||||||
<!-- Delete Files Modal -->
|
|
||||||
<div id="deleteFilesModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h4>Delete Selected Files</h4>
|
|
||||||
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
|
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||||
<!-- Copy Files Modal -->
|
|
||||||
<div id="copyFilesModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h4>Copy Selected Files</h4>
|
|
||||||
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
|
|
||||||
<select id="copyTargetFolder" class="form-control modal-input"></select>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
|
|
||||||
<!-- Move Files Modal -->
|
|
||||||
<div id="moveFilesModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h4>Move Selected Files</h4>
|
|
||||||
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
|
|
||||||
<select id="moveTargetFolder" class="form-control modal-input"></select>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
|
|
||||||
<button id="extractZipBtn" class="btn btn-sm btn-info" title="Extract Zip">Extract Zip</button>
|
|
||||||
<!-- Download Zip Modal -->
|
|
||||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h4>Download Selected Files as Zip</h4>
|
|
||||||
<p>Enter a name for the zip file:</p>
|
|
||||||
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
|
|
||||||
<div class="modal-footer" style="margin-top:15px; text-align:right;">
|
|
||||||
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
|
|
||||||
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="fileList"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Change Password-->
|
|
||||||
<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" style="cursor:pointer;">×</span>
|
<span id="closeChangePasswordModal" style="cursor:pointer;">×</span>
|
||||||
@@ -333,8 +342,6 @@
|
|||||||
<button id="saveNewPasswordBtn" class="btn btn-primary" style="width:100%;">Save</button>
|
<button id="saveNewPasswordBtn" class="btn btn-primary" style="width:100%;">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add User Modal -->
|
|
||||||
<div id="addUserModal" class="modal">
|
<div id="addUserModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3>Create New User</h3>
|
<h3>Create New User</h3>
|
||||||
@@ -352,8 +359,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remove User Modal -->
|
|
||||||
<div id="removeUserModal" class="modal">
|
<div id="removeUserModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h3>Remove User</h3>
|
<h3>Remove User</h3>
|
||||||
@@ -365,8 +370,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rename File Modal -->
|
|
||||||
<div id="renameFileModal" class="modal">
|
<div id="renameFileModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h4>Rename File</h4>
|
<h4>Rename File</h4>
|
||||||
@@ -378,8 +381,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom Confirm Modal -->
|
|
||||||
<div id="customConfirmModal" class="modal" style="display:none;">
|
<div id="customConfirmModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<p id="confirmMessage"></p>
|
<p id="confirmMessage"></p>
|
||||||
|
|||||||
62
login_basic.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
require 'config.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
|
|
||||||
|
// Reuse the same authentication function
|
||||||
|
function authenticate($username, $password)
|
||||||
|
{
|
||||||
|
global $usersFile;
|
||||||
|
if (!file_exists($usersFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
list($storedUser, $storedPass, $storedRole) = explode(':', trim($line), 3);
|
||||||
|
if ($username === $storedUser && password_verify($password, $storedPass)) {
|
||||||
|
return $storedRole; // Return the user's role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has sent HTTP Basic auth credentials.
|
||||||
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
|
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||||
|
header('HTTP/1.0 401 Unauthorized');
|
||||||
|
echo 'Authorization Required';
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$username = trim($_SERVER['PHP_AUTH_USER']);
|
||||||
|
$password = trim($_SERVER['PHP_AUTH_PW']);
|
||||||
|
|
||||||
|
// Validate username format (optional)
|
||||||
|
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||||
|
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||||
|
header('HTTP/1.0 401 Unauthorized');
|
||||||
|
echo 'Invalid username format';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt authentication
|
||||||
|
$userRole = authenticate($username, $password);
|
||||||
|
if ($userRole !== false) {
|
||||||
|
// Successful login
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION["authenticated"] = true;
|
||||||
|
$_SESSION["username"] = $username;
|
||||||
|
$_SESSION["isAdmin"] = ($userRole === "1"); // Assuming "1" means admin
|
||||||
|
|
||||||
|
// Redirect to the main page
|
||||||
|
header("Location: index.html");
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
// Invalid credentials; prompt again
|
||||||
|
header('WWW-Authenticate: Basic realm="FileRise Login"');
|
||||||
|
header('HTTP/1.0 401 Unauthorized');
|
||||||
|
echo 'Invalid credentials';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
11
main.js
@@ -17,6 +17,7 @@ import { loadFolderTree } from './folderManager.js';
|
|||||||
import { initUpload } from './upload.js';
|
import { initUpload } from './upload.js';
|
||||||
import { initAuth, checkAuthentication } from './auth.js';
|
import { initAuth, checkAuthentication } from './auth.js';
|
||||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||||
|
import { initDragAndDrop, loadSidebarOrder } from './dragAndDrop.js'
|
||||||
|
|
||||||
function loadCsrfToken() {
|
function loadCsrfToken() {
|
||||||
fetch('token.php', { credentials: 'include' })
|
fetch('token.php', { credentials: 'include' })
|
||||||
@@ -64,6 +65,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
// Call initAuth synchronously.
|
// Call initAuth synchronously.
|
||||||
initAuth();
|
initAuth();
|
||||||
|
|
||||||
|
const newPasswordInput = document.getElementById("newPassword");
|
||||||
|
if (newPasswordInput) {
|
||||||
|
newPasswordInput.addEventListener("input", function() {
|
||||||
|
console.log("newPassword input event:", this.value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("newPassword input not found!");
|
||||||
|
}
|
||||||
// --- Dark Mode Persistence ---
|
// --- Dark Mode Persistence ---
|
||||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||||
const storedDarkMode = localStorage.getItem("darkMode");
|
const storedDarkMode = localStorage.getItem("darkMode");
|
||||||
@@ -121,6 +130,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
window.currentFolder = "root";
|
window.currentFolder = "root";
|
||||||
loadFileList(window.currentFolder);
|
loadFileList(window.currentFolder);
|
||||||
|
initDragAndDrop();
|
||||||
|
loadSidebarOrder();
|
||||||
initFileActions();
|
initFileActions();
|
||||||
initUpload();
|
initUpload();
|
||||||
loadFolderTree();
|
loadFolderTree();
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
export function sendRequest(url, method = "GET", data = null) {
|
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
|
||||||
console.log("Sending request to:", url, "with method:", method);
|
|
||||||
const options = {
|
const options = {
|
||||||
method,
|
method,
|
||||||
credentials: 'include', // include cookies in requests
|
credentials: 'include',
|
||||||
headers: {}
|
headers: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Merge custom headers
|
||||||
|
Object.assign(options.headers, customHeaders);
|
||||||
|
|
||||||
// If data is provided and is not FormData, assume JSON.
|
// If data is provided and is not FormData, assume JSON.
|
||||||
if (data && !(data instanceof FormData)) {
|
if (data && !(data instanceof FormData)) {
|
||||||
options.headers["Content-Type"] = "application/json";
|
if (!options.headers["Content-Type"]) {
|
||||||
|
options.headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
options.body = JSON.stringify(data);
|
options.body = JSON.stringify(data);
|
||||||
} else if (data instanceof FormData) {
|
} else if (data instanceof FormData) {
|
||||||
// For FormData, don't set the Content-Type header; the browser will handle it.
|
|
||||||
options.body = data;
|
options.body = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, options)
|
return fetch(url, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log("Response status:", response.status);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return response.text().then(text => {
|
return response.text().then(text => {
|
||||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
throw new Error(`HTTP error ${response.status}: ${text}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Clone the response so we can safely fall back if JSON parsing fails.
|
|
||||||
const clonedResponse = response.clone();
|
const clonedResponse = response.clone();
|
||||||
return response.json().catch(() => {
|
return response.json().catch(() => clonedResponse.text());
|
||||||
console.warn("Response is not JSON, returning as text");
|
|
||||||
return clonedResponse.text();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
BIN
resources/dark-admin-panel.png
Normal file
|
After Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 626 KiB |
BIN
resources/dark-gallery.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
resources/dark-login.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
resources/dark-mode-default.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 400 KiB |
BIN
resources/light-preview.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
resources/light-share.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
resources/light-trash.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 416 KiB |
359
styles.css
@@ -25,22 +25,15 @@ body {
|
|||||||
/* CONTAINER */
|
/* CONTAINER */
|
||||||
.container,
|
.container,
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
margin-top: 20px;
|
margin-top: 10px;
|
||||||
padding-right: 4px !important;
|
padding-right: 4px !important;
|
||||||
padding-left: 4px !important;
|
padding-left: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 1300px) {
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
padding-left: 50px !important;
|
padding-left: 40px !important;
|
||||||
padding-right: 50px !important;
|
padding-right: 40px !important;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.container-fluid {
|
|
||||||
padding-left: 100px !important;
|
|
||||||
padding-right: 100px !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +45,6 @@ body {
|
|||||||
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
||||||
/************************************************************/
|
/************************************************************/
|
||||||
|
|
||||||
|
|
||||||
#uploadCard,
|
|
||||||
#folderManagementCard {
|
|
||||||
min-height: 342px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-login {
|
.btn-login {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
@@ -236,17 +223,19 @@ body.dark-mode .folder-help-tooltip {
|
|||||||
#folderHelpBtn i.material-icons.folder-help-icon {
|
#folderHelpBtn i.material-icons.folder-help-icon {
|
||||||
-webkit-text-fill-color: orange !important;
|
-webkit-text-fill-color: orange !important;
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
||||||
-webkit-text-fill-color: #ffa500 !important;
|
-webkit-text-fill-color: #ffa500 !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************************************/
|
/************************************************************/
|
||||||
/* RESPONSIVE HEADER FIXES */
|
/* RESPONSIVE HEADER FIXES */
|
||||||
/************************************************************/
|
/************************************************************/
|
||||||
|
|
||||||
@media (max-width: 970px) {
|
@media (max-width: 790px) {
|
||||||
.header-container {
|
.header-container {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -286,7 +275,7 @@ body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
margin-top: 10px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,21 +368,6 @@ body.dark-mode #loginForm button:hover {
|
|||||||
CARDS & MODALS
|
CARDS & MODALS
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
max-width: 900px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .card {
|
|
||||||
background-color: #2c2c2c;
|
|
||||||
color: #e0e0e0;
|
|
||||||
border: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
#restoreFilesModal .modal-content {
|
#restoreFilesModal .modal-content {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: 50% !important;
|
top: 50% !important;
|
||||||
@@ -1023,29 +997,6 @@ label {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadFolderRow {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
#uploadFolderRow .col-md-6 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#uploadFolderRow .col-md-6:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body .form-group {
|
|
||||||
margin-bottom: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createFolderBtn {
|
#createFolderBtn {
|
||||||
margin-top: 0px !important;
|
margin-top: 0px !important;
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
@@ -1100,34 +1051,6 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
|||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-option:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-option.selected {
|
|
||||||
background-color: #d0d0d0;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .folder-option.selected {
|
|
||||||
background-color: #444;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .folder-option:hover {
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
padding: 2px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-folder-card-body {
|
|
||||||
padding-top: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#customToast {
|
#customToast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@@ -1175,8 +1098,10 @@ body.dark-mode .folder-option:hover {
|
|||||||
|
|
||||||
#fileListContainer {
|
#fileListContainer {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 10px 5px;
|
padding-bottom: 10px !important;
|
||||||
margin: 20px auto;
|
padding-left: 5px !important;
|
||||||
|
padding-right: 5px !important;
|
||||||
|
margin: 0 auto 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 750px) {
|
@media (max-width: 750px) {
|
||||||
@@ -1190,11 +1115,6 @@ body.dark-mode #fileListContainer {
|
|||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding-top: 10px !important;
|
|
||||||
padding-bottom: 10px !important;
|
|
||||||
padding-left: 5px !important;
|
|
||||||
padding-right: 5px !important;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileListContainer>h2,
|
#fileListContainer>h2,
|
||||||
@@ -1224,6 +1144,7 @@ body.dark-mode #fileListContainer {
|
|||||||
|
|
||||||
#fileListTitle {
|
#fileListTitle {
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
|
margin-top: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1288,16 +1209,13 @@ body.dark-mode #fileListContainer {
|
|||||||
.breadcrumb-link {
|
.breadcrumb-link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
/* Blue color, for example */
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Change color on hover */
|
|
||||||
.breadcrumb-link:hover {
|
.breadcrumb-link:hover {
|
||||||
color: #0056b3;
|
color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for the selected breadcrumb */
|
|
||||||
.breadcrumb-link.selected {
|
.breadcrumb-link.selected {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -1337,15 +1255,38 @@ body.dark-mode #fileListContainer {
|
|||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-option {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#folderTreeContainer {
|
#folderTreeContainer {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.folder-option {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-option:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-option.selected {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .folder-option.selected {
|
||||||
|
background-color: #444;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .folder-option:hover {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
FILE MANAGER INLINE STYLE REMOVAL - New Classes
|
FILE MANAGER INLINE STYLE REMOVAL - New Classes
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
@@ -1353,17 +1294,11 @@ body.dark-mode #fileListContainer {
|
|||||||
.image-modal-header {
|
.image-modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* Vertically center the text within a fixed height */
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/* Center horizontally */
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
/* Prevent wrapping */
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* Hide any overflowing text */
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
/* Truncate with an ellipsis */
|
|
||||||
height: 25px;
|
height: 25px;
|
||||||
/* Fixed height for a single line */
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
@@ -1410,7 +1345,6 @@ body.dark-mode .image-preview-modal-content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.share-btn {
|
.share-btn {
|
||||||
/* Your custom styles here */
|
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -1903,3 +1837,214 @@ body.dark-mode #folderContextMenu {
|
|||||||
border-color: #555;
|
border-color: #555;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-target-sidebar {
|
||||||
|
display: none;
|
||||||
|
width: 50px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 2px dashed #1565C0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.drop-target-sidebar {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drop-target-sidebar.active {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-column {
|
||||||
|
flex: 1;
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadFolderRow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#uploadFolderRow {
|
||||||
|
gap: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#leftCol,
|
||||||
|
#rightCol {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 370px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leftCol {
|
||||||
|
flex: 0 1 60%;
|
||||||
|
}
|
||||||
|
#rightCol {
|
||||||
|
flex: 0 1 40%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.drop-target-sidebar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
#uploadFolderRow {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#leftCol, #rightCol {
|
||||||
|
flex: 0 1 100% !important;
|
||||||
|
}
|
||||||
|
#rightCol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarDropArea.highlight,
|
||||||
|
#uploadFolderRow.highlight {
|
||||||
|
border: 2px dashed #1565C0;
|
||||||
|
background-color: #eef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-header {
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-header::after {
|
||||||
|
content: '⋮⋮';
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1565C0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadCard,
|
||||||
|
#folderManagementCard {
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
min-height: 353px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#uploadFolderRow.highlight {
|
||||||
|
min-height: 353px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarDropArea,
|
||||||
|
#uploadFolderRow {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
#sidebarDropArea {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode #sidebarDropArea,
|
||||||
|
body.dark-mode #uploadFolderRow {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode #sidebarDropArea.highlight,
|
||||||
|
body.dark-mode #uploadFolderRow.highlight {
|
||||||
|
background-color: #333;
|
||||||
|
border: 2px dashed #555;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-target-sidebar.highlight {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-target-sidebar:not(.highlight) {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarDropArea > .card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-folder-card-body {
|
||||||
|
padding-top: 5px !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addUserModal,
|
||||||
|
#removeUserModal {
|
||||||
|
z-index: 5000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#customConfirmModal {
|
||||||
|
z-index: 6000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default (light mode) for admin panel content */
|
||||||
|
.admin-panel-content {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides for admin panel content */
|
||||||
|
body.dark-mode .admin-panel-content {
|
||||||
|
background: #2c2c2c; /* dark background */
|
||||||
|
color: #e0e0e0; /* light text */
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optionally, adjust input, label, etc. for dark mode */
|
||||||
|
body.dark-mode .admin-panel-content input,
|
||||||
|
body.dark-mode .admin-panel-content select,
|
||||||
|
body.dark-mode .admin-panel-content textarea {
|
||||||
|
background: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .admin-panel-content label {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
82
updateConfig.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
require 'config.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Verify that the user is authenticated and is an admin.
|
||||||
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true ||
|
||||||
|
!isset($_SESSION['isAdmin']) || !$_SESSION['isAdmin']) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Unauthorized access.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CSRF token.
|
||||||
|
$receivedToken = '';
|
||||||
|
if (isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
|
||||||
|
$receivedToken = trim($_SERVER['HTTP_X_CSRF_TOKEN']);
|
||||||
|
} else {
|
||||||
|
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||||
|
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize OIDC configuration.
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate login option booleans.
|
||||||
|
$disableFormLogin = isset($data['disableFormLogin']) ? filter_var($data['disableFormLogin'], FILTER_VALIDATE_BOOLEAN) : false;
|
||||||
|
$disableBasicAuth = isset($data['disableBasicAuth']) ? filter_var($data['disableBasicAuth'], FILTER_VALIDATE_BOOLEAN) : false;
|
||||||
|
$disableOIDCLogin = isset($data['disableOIDCLogin']) ? filter_var($data['disableOIDCLogin'], FILTER_VALIDATE_BOOLEAN) : false;
|
||||||
|
|
||||||
|
// Prepare configuration array.
|
||||||
|
$configUpdate = [
|
||||||
|
'oidc' => [
|
||||||
|
'providerUrl' => $oidcProviderUrl,
|
||||||
|
'clientId' => $oidcClientId,
|
||||||
|
'clientSecret' => $oidcClientSecret,
|
||||||
|
'redirectUri' => $oidcRedirectUri,
|
||||||
|
],
|
||||||
|
'loginOptions' => [
|
||||||
|
'disableFormLogin' => $disableFormLogin,
|
||||||
|
'disableBasicAuth' => $disableBasicAuth,
|
||||||
|
'disableOIDCLogin' => $disableOIDCLogin,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Define the configuration file path.
|
||||||
|
$configFile = USERS_DIR . 'adminConfig.json';
|
||||||
|
|
||||||
|
// Convert and encrypt configuration.
|
||||||
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
|
$encryptedContent = encryptData($plainTextConfig, $encryptionKey);
|
||||||
|
if (file_put_contents($configFile, $encryptedContent, LOCK_EX) === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Failed to update configuration.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => 'Configuration updated successfully.']);
|
||||||
|
?>
|
||||||