New Admin Panel, OIDC Integration & Card dragDrop adjustments

This commit is contained in:
Ryan
2025-03-29 04:33:10 -04:00
committed by GitHub
parent 89777584cf
commit 051544dc5a
11 changed files with 847 additions and 388 deletions

View File

@@ -137,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:**
![Dark Admin Panel](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-admin-panel.png)
**Light mode:**
![Light Mode](https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-mode.png)

390
auth.js
View File

@@ -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;">&times;</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
View File

@@ -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"]);
}
?>

View File

@@ -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);
?>

View File

@@ -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();
}
}

30
getConfig.php Normal file
View 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
]
]);
}
?>

View File

@@ -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,10 @@
<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>

View File

@@ -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());
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

View File

@@ -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
View 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.']);
?>