Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27de0a9a48 | ||
|
|
051544dc5a | ||
|
|
89777584cf | ||
|
|
ed47e3c3bc | ||
|
|
edd9094218 | ||
|
|
3b0083516b | ||
|
|
fee3b544dd | ||
|
|
99ed05d3de | ||
|
|
32469778dc | ||
|
|
ecb4ac2c75 | ||
|
|
4ae509acd2 | ||
|
|
b1cd4b7bdc |
92
CONTRIBUTING.md
Normal file
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!
|
||||
75
README.md
75
README.md
@@ -34,7 +34,7 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
- **Enhanced File Editing Check:** Files with a Content-Length of 0 KB are now allowed to be edited.
|
||||
|
||||
- **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.
|
||||
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
|
||||
|
||||
@@ -84,6 +84,7 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
- Passwords are hashed using PHP’s `password_hash()` for security.
|
||||
- All state-changing endpoints include CSRF token validation.
|
||||
- Change password supported for all users.
|
||||
- Basic Auth supported for login.
|
||||
- **Persistent Login (Remember Me) with Encrypted Tokens:**
|
||||
- Users can remain logged in across sessions securely.
|
||||
- Persistent tokens are encrypted using AES‑256‑CBC before being stored in a JSON file.
|
||||
@@ -136,10 +137,28 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
- **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
|
||||
|
||||
**Light mode:**
|
||||

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

|
||||
|
||||
@@ -185,9 +204,19 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
|
||||
2. **Deploy to Your Web Server:**
|
||||
- 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:
|
||||
- `uploads/` – for file storage.
|
||||
- `users/` – to store `users.txt` (user authentication data).
|
||||
@@ -199,11 +228,11 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
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).
|
||||
- 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:
|
||||
- `BASE_URL`: Set this to your web app’s base URL.
|
||||
- `UPLOAD_DIR`: Adjust the directory path for uploads.
|
||||
@@ -220,7 +249,9 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
|
||||
## 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
|
||||
|
||||
@@ -258,19 +289,19 @@ For users who prefer containerization, a Docker image is available
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
web:
|
||||
image: error311/filerise-docker:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
TIMEZONE: "America/New_York"
|
||||
TOTAL_UPLOAD_SIZE: "5G"
|
||||
SECURE: "false"
|
||||
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
||||
volumes:
|
||||
- /path/to/your/uploads:/var/www/uploads
|
||||
- /path/to/your/users:/var/www/users
|
||||
- /path/to/your/metadata:/var/www/metadata
|
||||
web:
|
||||
image: error311/filerise-docker:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
TIMEZONE: "America/New_York"
|
||||
TOTAL_UPLOAD_SIZE: "5G"
|
||||
SECURE: "false"
|
||||
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
||||
volumes:
|
||||
- /path/to/your/uploads:/var/www/uploads
|
||||
- /path/to/your/users:/var/www/users
|
||||
- /path/to/your/metadata:/var/www/metadata
|
||||
```
|
||||
|
||||
**Then start the container with:**
|
||||
@@ -313,4 +344,8 @@ The `config.php` file contains several key constants that may need adjustment fo
|
||||
- **Logging & Troubleshooting:**
|
||||
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/FileRise).
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please check out our [Contributing Guidelines](CONTRIBUTING.md) before getting started.
|
||||
|
||||
390
auth.js
390
auth.js
@@ -3,9 +3,14 @@ import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmM
|
||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
|
||||
/**
|
||||
* Updates the select element to reflect the stored items-per-page value.
|
||||
*/
|
||||
// Default OIDC configuration (can be overridden via API in production)
|
||||
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() {
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
@@ -14,11 +19,45 @@ function updateItemsPerPageSelect() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI for an authenticated user.
|
||||
* This includes showing the main UI panels, attaching key listeners, updating header buttons,
|
||||
* and displaying admin-only buttons if applicable.
|
||||
*/
|
||||
function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCLogin }) {
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) {
|
||||
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) {
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
@@ -29,45 +68,54 @@ function updateAuthenticatedUI(data) {
|
||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
|
||||
// If admin, show admin-only buttons; otherwise hide them.
|
||||
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");
|
||||
if (!restoreBtn) {
|
||||
restoreBtn = document.createElement("button");
|
||||
restoreBtn.id = "restoreFilesBtn";
|
||||
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>';
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
if (headerButtons) {
|
||||
if (headerButtons.children.length >= 5) {
|
||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
|
||||
if (headerButtons.children.length >= 3) {
|
||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[3]);
|
||||
} else {
|
||||
headerButtons.appendChild(restoreBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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");
|
||||
if (restoreBtn) restoreBtn.style.display = "none";
|
||||
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
||||
}
|
||||
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) {
|
||||
return sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
@@ -78,7 +126,7 @@ function checkAuthentication(showLoginToast = true) {
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById('newUsername').focus();
|
||||
document.getElementById("newUsername").focus();
|
||||
return false;
|
||||
}
|
||||
window.setupMode = false;
|
||||
@@ -95,22 +143,13 @@ function checkAuthentication(showLoginToast = true) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error checking authentication:", error);
|
||||
return false;
|
||||
});
|
||||
.catch(() => 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() {
|
||||
checkAuthentication(false).catch(error => {
|
||||
console.error("Error checking authentication:", error);
|
||||
});
|
||||
checkAuthentication(false);
|
||||
loadAdminConfigFunc();
|
||||
|
||||
// Attach login event listener.
|
||||
const authForm = document.getElementById("authForm");
|
||||
if (authForm) {
|
||||
authForm.addEventListener("submit", function (event) {
|
||||
@@ -126,7 +165,6 @@ function initAuth() {
|
||||
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log("✅ Login successful. Reloading page.");
|
||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
|
||||
window.location.reload();
|
||||
} 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 () {
|
||||
fetch("logout.php", {
|
||||
method: "POST",
|
||||
@@ -157,14 +194,20 @@ function initAuth() {
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
})
|
||||
.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 () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById('newUsername').focus();
|
||||
document.getElementById("newUsername").focus();
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
@@ -192,24 +235,19 @@ function initAuth() {
|
||||
if (data.success) {
|
||||
showToast("User added successfully!");
|
||||
closeAddUserModal();
|
||||
// Re-check auth state to update the UI after adding a user.
|
||||
checkAuthentication(false);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error adding user:", error));
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", function () {
|
||||
closeAddUserModal();
|
||||
.catch(() => {});
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", closeAddUserModal);
|
||||
|
||||
// Remove User functionality.
|
||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||
loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
const usernameToRemove = selectElem.value;
|
||||
@@ -218,9 +256,7 @@ function initAuth() {
|
||||
return;
|
||||
}
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
if (!confirmed) return;
|
||||
fetch("removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@@ -240,22 +276,17 @@ function initAuth() {
|
||||
showToast("Error: " + (data.error || "Could not remove user"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error removing user:", error));
|
||||
});
|
||||
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
|
||||
closeRemoveUserModal();
|
||||
.catch(() => {});
|
||||
});
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
document.getElementById("oldPassword").focus();
|
||||
});
|
||||
|
||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
});
|
||||
|
||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim();
|
||||
@@ -290,13 +321,240 @@ function initAuth() {
|
||||
showToast("Error: " + (result.error || "Could not change password."));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error changing password:", error);
|
||||
.catch(() => {
|
||||
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) {
|
||||
localStorage.setItem("itemsPerPage", value);
|
||||
const folder = window.currentFolder || "root";
|
||||
@@ -307,6 +565,10 @@ window.changeItemsPerPage = function (value) {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
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() {
|
||||
@@ -342,7 +604,7 @@ function loadUserList() {
|
||||
closeRemoveUserModal();
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading user list:", error));
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
export { initAuth, checkAuthentication };
|
||||
119
auth.php
119
auth.php
@@ -1,19 +1,83 @@
|
||||
<?php
|
||||
require_once 'vendor/autoload.php';
|
||||
require 'config.php';
|
||||
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;
|
||||
|
||||
// --- Brute Force Protection Settings ---
|
||||
$maxAttempts = 5;
|
||||
$lockoutTime = 30 * 60; // 30 minutes in seconds
|
||||
$attemptsFile = USERS_DIR . 'failed_logins.json'; // JSON file for tracking failed login attempts
|
||||
$failedLogFile = USERS_DIR . 'failed_login.log'; // Plain text log for fail2ban
|
||||
|
||||
// Persistent tokens file for "Remember me"
|
||||
$lockoutTime = 30 * 60;
|
||||
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
||||
$failedLogFile = USERS_DIR . 'failed_login.log';
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
|
||||
// Load failed attempts data from file.
|
||||
function loadFailedAttempts($file) {
|
||||
if (file_exists($file)) {
|
||||
$data = json_decode(file_get_contents($file), true);
|
||||
@@ -24,30 +88,23 @@ function loadFailedAttempts($file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Save failed attempts data to file.
|
||||
function saveFailedAttempts($file, $data) {
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
// Get current IP address.
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$currentTime = time();
|
||||
|
||||
// Load failed attempts.
|
||||
$failedAttempts = loadFailedAttempts($attemptsFile);
|
||||
|
||||
// Check if this IP is currently locked out.
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$attemptData = $failedAttempts[$ip];
|
||||
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
||||
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;
|
||||
if (!file_exists($usersFile)) {
|
||||
return false;
|
||||
@@ -56,51 +113,41 @@ function authenticate($username, $password)
|
||||
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 $storedRole;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get JSON input.
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$username = trim($data["username"] ?? "");
|
||||
$password = trim($data["password"] ?? "");
|
||||
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
||||
|
||||
// Validate input: ensure both fields are provided.
|
||||
if (!$username || !$password) {
|
||||
echo json_encode(["error" => "Username and password are required"]);
|
||||
exit;
|
||||
exit();
|
||||
}
|
||||
|
||||
// Validate username format.
|
||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
||||
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);
|
||||
if ($userRole !== false) {
|
||||
// On successful login, reset failed attempts for this IP.
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
unset($failedAttempts[$ip]);
|
||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||
}
|
||||
// Regenerate session ID to mitigate session fixation attacks.
|
||||
session_regenerate_id(true);
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["isAdmin"] = ($userRole === "1"); // "1" indicates admin
|
||||
|
||||
// If "Remember me" is checked, generate a persistent login token.
|
||||
$_SESSION["isAdmin"] = ($userRole === "1");
|
||||
|
||||
if ($rememberMe) {
|
||||
// Generate a secure random token.
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + (30 * 24 * 60 * 60); // 30 days
|
||||
|
||||
// Load existing persistent tokens.
|
||||
$expiry = time() + (30 * 24 * 60 * 60);
|
||||
$persistentTokens = [];
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
@@ -110,7 +157,6 @@ if ($userRole !== false) {
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
// Save token along with username, expiry, and admin status.
|
||||
$persistentTokens[$token] = [
|
||||
"username" => $username,
|
||||
"expiry" => $expiry,
|
||||
@@ -118,13 +164,11 @@ if ($userRole !== false) {
|
||||
];
|
||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
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);
|
||||
}
|
||||
|
||||
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
|
||||
} else {
|
||||
// On failed login, update failed attempts.
|
||||
if (isset($failedAttempts[$ip])) {
|
||||
$failedAttempts[$ip]['count']++;
|
||||
$failedAttempts[$ip]['last_attempt'] = $currentTime;
|
||||
@@ -132,11 +176,8 @@ if ($userRole !== false) {
|
||||
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
|
||||
}
|
||||
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;
|
||||
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
|
||||
|
||||
echo json_encode(["error" => "Invalid credentials"]);
|
||||
}
|
||||
?>
|
||||
35
config.php
35
config.php
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
// config.php
|
||||
|
||||
// Define constants first.
|
||||
// Define constants.
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('USERS_DIR', '/var/www/users/');
|
||||
define('USERS_FILE', 'users.txt');
|
||||
@@ -12,15 +12,14 @@ define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||
|
||||
// Set the default timezone.
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
|
||||
/**
|
||||
* Encrypts data using AES-256-CBC.
|
||||
*
|
||||
* @param string $data The plaintext data.
|
||||
* @param string $encryptionKey The secret encryption key.
|
||||
* @return string Base64-encoded string containing IV and ciphertext.
|
||||
* @param string $data The plaintext.
|
||||
* @param string $encryptionKey The encryption key.
|
||||
* @return string Base64-encoded string containing IV and ciphertext.
|
||||
*/
|
||||
function encryptData($data, $encryptionKey) {
|
||||
$cipher = 'AES-256-CBC';
|
||||
@@ -33,9 +32,9 @@ function encryptData($data, $encryptionKey) {
|
||||
/**
|
||||
* Decrypts data encrypted with AES-256-CBC.
|
||||
*
|
||||
* @param string $encryptedData The Base64-encoded data containing IV and ciphertext.
|
||||
* @param string $encryptionKey The secret encryption key.
|
||||
* @return string|false The decrypted plaintext or false on failure.
|
||||
* @param string $encryptedData Base64-encoded data containing IV and ciphertext.
|
||||
* @param string $encryptionKey The encryption key.
|
||||
* @return string|false The decrypted plaintext or false on failure.
|
||||
*/
|
||||
function decryptData($encryptedData, $encryptionKey) {
|
||||
$cipher = 'AES-256-CBC';
|
||||
@@ -46,16 +45,15 @@ function decryptData($encryptedData, $encryptionKey) {
|
||||
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';
|
||||
if (!$encryptionKey) {
|
||||
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');
|
||||
if ($envSecure !== false) {
|
||||
// Convert the environment variable value to a boolean.
|
||||
$secure = filter_var($envSecure, FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||
@@ -64,22 +62,20 @@ if ($envSecure !== false) {
|
||||
$cookieParams = [
|
||||
'lifetime' => 7200,
|
||||
'path' => '/',
|
||||
'domain' => '', // Specify your domain if needed
|
||||
'domain' => '', // Set your domain as needed.
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
session_set_cookie_params($cookieParams);
|
||||
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
|
||||
// Generate CSRF token if not already set.
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_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'])) {
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$persistentTokens = [];
|
||||
@@ -91,15 +87,12 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||
if (isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
||||
if ($tokenData['expiry'] >= time()) {
|
||||
// Token is valid; auto-authenticate the user.
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $tokenData["username"];
|
||||
$_SESSION["isAdmin"] = $tokenData["isAdmin"]; // Restore admin status from the token
|
||||
} else {
|
||||
// Token expired; remove it and clear the cookie.
|
||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
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/');
|
||||
|
||||
// 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) {
|
||||
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
||||
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
|
||||
@@ -120,6 +110,5 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
} else {
|
||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/share.php";
|
||||
}
|
||||
|
||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||
?>
|
||||
90
domUtils.js
90
domUtils.js
@@ -136,18 +136,20 @@ export function buildFileTableRow(file, folderPath) {
|
||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||
|
||||
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 = "";
|
||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
||||
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>`;
|
||||
} else if (/\.pdf$/i.test(file.name)) {
|
||||
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}')">
|
||||
${previewIcon}
|
||||
</button>`;
|
||||
${previewIcon}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
return `
|
||||
@@ -231,86 +233,6 @@ export function toggleRowSelection(event, fileName) {
|
||||
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) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
|
||||
498
dragAndDrop.js
498
dragAndDrop.js
@@ -6,23 +6,23 @@ export function loadSidebarOrder() {
|
||||
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';
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
@@ -31,16 +31,16 @@ export function loadSidebarOrder() {
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,21 +48,21 @@ function updateSidebarVisibility() {
|
||||
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';
|
||||
leftCol.style.display = 'none';
|
||||
rightCol.style.margin = '0 auto';
|
||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||
rightCol.style.display = 'none';
|
||||
leftCol.style.margin = '0 auto';
|
||||
rightCol.style.display = 'none';
|
||||
leftCol.style.margin = '0 auto';
|
||||
} else {
|
||||
leftCol.style.display = '';
|
||||
rightCol.style.display = '';
|
||||
leftCol.style.margin = '';
|
||||
rightCol.style.margin = '';
|
||||
leftCol.style.display = '';
|
||||
rightCol.style.display = '';
|
||||
leftCol.style.margin = '';
|
||||
rightCol.style.margin = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@ function updateTopZoneLayout() {
|
||||
function addTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.add('highlight');
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
topZone.style.minHeight = '375px';
|
||||
}
|
||||
topZone.classList.add('highlight');
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
topZone.style.minHeight = '375px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ function addTopZoneHighlight() {
|
||||
function removeTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.remove('highlight');
|
||||
topZone.style.minHeight = '';
|
||||
topZone.classList.remove('highlight');
|
||||
topZone.style.minHeight = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,14 +93,14 @@ function animateVerticalSlide(card) {
|
||||
// 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';
|
||||
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 = '';
|
||||
card.style.transition = '';
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
}, 310);
|
||||
}
|
||||
|
||||
@@ -111,16 +111,16 @@ function insertCardInSidebar(card, event) {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
sidebar.appendChild(card);
|
||||
}
|
||||
// Ensure card fills the sidebar.
|
||||
card.style.width = '100%';
|
||||
@@ -131,34 +131,34 @@ function insertCardInSidebar(card, event) {
|
||||
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));
|
||||
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();
|
||||
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();
|
||||
moveSidebarCardsToTop();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,198 +167,198 @@ 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);
|
||||
}
|
||||
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();
|
||||
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');
|
||||
const rect = card.getBoundingClientRect();
|
||||
const originX = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const originY = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||
dragTimer = setTimeout(() => {
|
||||
isDragging = true;
|
||||
card.classList.add('dragging');
|
||||
// Disable pointer events on the card so it doesn't block drop detection.
|
||||
card.style.pointerEvents = 'none';
|
||||
addTopZoneHighlight();
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
// Force the sidebar to have a tall drop zone while dragging.
|
||||
sidebar.style.height = '800px';
|
||||
}
|
||||
const rect = card.getBoundingClientRect();
|
||||
initialLeft = rect.left + window.pageXOffset;
|
||||
initialTop = rect.top + window.pageYOffset;
|
||||
offsetX = e.pageX - initialLeft;
|
||||
offsetY = e.pageY - initialTop;
|
||||
document.body.appendChild(card);
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = initialLeft + 'px';
|
||||
card.style.top = initialTop + 'px';
|
||||
card.style.width = rect.width + 'px';
|
||||
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;
|
||||
// Re-enable pointer events on the card.
|
||||
card.style.pointerEvents = '';
|
||||
card.classList.remove('dragging');
|
||||
removeTopZoneHighlight();
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
// Remove the forced height once the drag ends.
|
||||
sidebar.style.height = '';
|
||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
draggableCards.forEach(card => {
|
||||
if (!card.dataset.originalContainerId) {
|
||||
card.dataset.originalContainerId = card.parentNode.id;
|
||||
}
|
||||
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
let droppedInSidebar = false;
|
||||
let droppedInTop = false;
|
||||
|
||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||
if (sidebarElem) {
|
||||
// Instead of using elementsFromPoint, use a virtual drop zone.
|
||||
const rect = sidebarElem.getBoundingClientRect();
|
||||
// Define a drop zone from the top of the sidebar to 1000px below its top.
|
||||
const dropZoneBottom = rect.top + 800;
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= dropZoneBottom
|
||||
) {
|
||||
insertCardInSidebar(card, e);
|
||||
droppedInSidebar = true;
|
||||
sidebarElem.blur();
|
||||
}
|
||||
const header = card.querySelector('.card-header');
|
||||
if (header) {
|
||||
header.classList.add('drag-header');
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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';
|
||||
}
|
||||
if (container) {
|
||||
ensureTopZonePlaceholder();
|
||||
container.appendChild(card);
|
||||
droppedInTop = true;
|
||||
|
||||
container.style.position = 'relative';
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = '0px';
|
||||
|
||||
// Animate vertical slide/fade.
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.opacity = '0';
|
||||
|
||||
card.offsetWidth; // Force reflow.
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.opacity = '1';
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
card.style.position = '';
|
||||
container.style.position = '';
|
||||
card.style.transition = '';
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
card.style.width = '';
|
||||
}, 310);
|
||||
});
|
||||
|
||||
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 (droppedInSidebar || droppedInTop) {
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
} else {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
card.style.width = '';
|
||||
}
|
||||
}
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
run();
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
showToast,
|
||||
updateRowHighlight,
|
||||
toggleRowSelection,
|
||||
attachEnterKeyListener,
|
||||
previewFile as originalPreviewFile
|
||||
attachEnterKeyListener
|
||||
} from './domUtils.js';
|
||||
|
||||
export let fileData = [];
|
||||
@@ -153,7 +152,7 @@ function openShareModal(file, folder) {
|
||||
// ==============================================
|
||||
// FEATURE: Enhanced Preview Modal with Navigation
|
||||
// ==============================================
|
||||
function enhancedPreviewFile(fileUrl, fileName) {
|
||||
function previewFile(fileUrl, fileName) {
|
||||
let modal = document.getElementById("filePreviewModal");
|
||||
if (!modal) {
|
||||
modal = document.createElement("div");
|
||||
@@ -178,12 +177,24 @@ function enhancedPreviewFile(fileUrl, fileName) {
|
||||
</div>`;
|
||||
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";
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("closeFileModal").addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", function (e) {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = "none";
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -253,6 +264,13 @@ function enhancedPreviewFile(fileUrl, fileName) {
|
||||
video.controls = true;
|
||||
video.className = "image-modal-img";
|
||||
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 {
|
||||
container.textContent = "Preview not available for this file type.";
|
||||
}
|
||||
@@ -260,10 +278,6 @@ function enhancedPreviewFile(fileUrl, fileName) {
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
export function previewFile(fileUrl, fileName) {
|
||||
enhancedPreviewFile(fileUrl, fileName);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ORIGINAL FILE MANAGER FUNCTIONS
|
||||
// ==============================================
|
||||
@@ -519,6 +533,8 @@ export function renderGalleryView(folder) {
|
||||
let thumbnail;
|
||||
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;">`;
|
||||
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
||||
thumbnail = `<span class="material-icons gallery-icon">audiotrack</span>`;
|
||||
} else {
|
||||
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
|
||||
}
|
||||
|
||||
30
getConfig.php
Normal file
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
|
||||
]
|
||||
]);
|
||||
}
|
||||
?>
|
||||
12
index.html
12
index.html
@@ -123,10 +123,10 @@
|
||||
</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>
|
||||
</button>
|
||||
<button id="removeUserBtn" title="Remove User">
|
||||
<button id="removeUserBtn" title="Remove User" style="display: none;">
|
||||
<i class="material-icons">person_remove</i>
|
||||
</button>
|
||||
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
|
||||
@@ -162,6 +162,14 @@
|
||||
<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>
|
||||
|
||||
|
||||
62
login_basic.php
Normal file
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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -1,33 +1,31 @@
|
||||
export function sendRequest(url, method = "GET", data = null) {
|
||||
console.log("Sending request to:", url, "with method:", method);
|
||||
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
|
||||
const options = {
|
||||
method,
|
||||
credentials: 'include', // include cookies in requests
|
||||
credentials: 'include',
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// Merge custom headers
|
||||
Object.assign(options.headers, customHeaders);
|
||||
|
||||
// If data is provided and is not FormData, assume JSON.
|
||||
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);
|
||||
} else if (data instanceof FormData) {
|
||||
// For FormData, don't set the Content-Type header; the browser will handle it.
|
||||
options.body = data;
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
.then(response => {
|
||||
console.log("Response status:", response.status);
|
||||
if (!response.ok) {
|
||||
return response.text().then(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();
|
||||
return response.json().catch(() => {
|
||||
console.warn("Response is not JSON, returning as text");
|
||||
return clonedResponse.text();
|
||||
});
|
||||
return response.json().catch(() => clonedResponse.text());
|
||||
});
|
||||
}
|
||||
BIN
resources/dark-admin-panel.png
Normal file
BIN
resources/dark-admin-panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 347 KiB |
35
styles.css
35
styles.css
@@ -2012,4 +2012,39 @@ body.dark-mode .card {
|
||||
.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
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.']);
|
||||
?>
|
||||
Reference in New Issue
Block a user