From 75d3bf5a9bf3e57869459452ed145ec97d0fde1c Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 16 Apr 2025 17:15:59 -0400 Subject: [PATCH] Refactor fixes and adjustments --- CHANGELOG.md | 9 +++ config/config.php | 4 +- public/index.html | 4 +- public/js/auth.js | 8 +- public/js/authModals.js | 4 +- public/js/fileActions.js | 2 +- public/js/fileListView.js | 2 +- public/js/filePreview.js | 4 +- public/js/folderManager.js | 2 +- public/js/upload.js | 2 +- src/controllers/authController.php | 105 ++++++++++++++------------- src/controllers/fileController.php | 2 +- src/controllers/folderController.php | 18 +++-- src/models/AdminModel.php | 2 +- src/models/FolderModel.php | 2 +- 15 files changed, 93 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecc85c..ed62b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,15 @@ This refactor improves maintainability, testability, and documentation clarity across all API endpoints. +### Refactor fixes and adjustments + +- Added fallback checks for disableFormLogin / disableBasicAuth / disableOIDCLogin when coming in either at the top level or under loginOptions. +- Updated auth.js to read and store the nested loginOptions booleans correctly in localStorage, then show/hide the Basic‑Auth and OIDC buttons as configured. +- Changed the logout controller to header("Location: /index.html?logout=1") so after /api/auth/logout.php it lands on the root index.html, not under /api/auth/. +- Switched your share modal code to use a leading slash ("/api/file/share.php") so it generates absolute URLs instead of relative /share.php. +- In the shared‑folder gallery, adjusted the client‑side image path to point at /uploads/... instead of /api/folder/uploads/... +- Updated both AdminModel defaults and the AuthController to use the exact full path + --- ## Changes 4/15/2025 diff --git a/config/config.php b/config/config.php index dda052f..3a94c9d 100644 --- a/config/config.php +++ b/config/config.php @@ -165,8 +165,8 @@ define('BASE_URL', 'http://yourwebsite/uploads/'); if (strpos(BASE_URL, 'yourwebsite') !== false) { $defaultShareUrl = isset($_SERVER['HTTP_HOST']) ? "http://" . $_SERVER['HTTP_HOST'] . "/share.php" - : "http://localhost/public/api/file/share.php"; + : "http://localhost/api/file/share.php"; } else { - $defaultShareUrl = rtrim(BASE_URL, '/') . "api/file/share.php"; + $defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php"; } define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl); diff --git a/public/index.html b/public/index.html index 70f6da1..3fba26a 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,7 @@ - + @@ -200,7 +200,7 @@
- Use Basic HTTP + Use Basic HTTP Login
diff --git a/public/js/auth.js b/public/js/auth.js index dd99bf2..0dc3183 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -25,7 +25,7 @@ const currentOIDCConfig = { providerUrl: "https://your-oidc-provider.com", clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", - redirectUri: "https://yourdomain.com/auth.php?oidc=callback", + redirectUri: "https://yourdomain.com/api/auth/auth.php?oidc=callback", globalOtpauthUrl: "" }; window.currentOIDCConfig = currentOIDCConfig; @@ -51,7 +51,7 @@ function openTOTPLoginModal() { const isFormLogin = Boolean(window.__lastLoginData); if (!isFormLogin) { // disable Basic‑Auth link - const basicLink = document.querySelector("a[href='api/auth/login_basic.php']"); + const basicLink = document.querySelector("a[href='/api/auth/login_basic.php']"); if (basicLink) { basicLink.style.pointerEvents = 'none'; basicLink.style.opacity = '0.5'; @@ -80,7 +80,7 @@ function updateLoginOptionsUI({ disableFormLogin, disableBasicAuth, disableOIDCL const authForm = document.getElementById("authForm"); if (authForm) authForm.style.display = disableFormLogin ? "none" : "block"; - const basicAuthLink = document.querySelector("a[href='api/auth/login_basic.php']"); + const basicAuthLink = document.querySelector("a[href='/api/auth/login_basic.php']"); if (basicAuthLink) basicAuthLink.style.display = disableBasicAuth ? "none" : "inline-block"; const oidcLoginBtn = document.getElementById("oidcLoginBtn"); if (oidcLoginBtn) oidcLoginBtn.style.display = disableOIDCLogin ? "none" : "inline-block"; @@ -480,7 +480,7 @@ document.addEventListener("DOMContentLoaded", function () { const oidcLoginBtn = document.getElementById("oidcLoginBtn"); if (oidcLoginBtn) { oidcLoginBtn.addEventListener("click", () => { - window.location.href = "api/auth/auth.php?oidc=initiate"; + window.location.href = "/api/auth/auth.php?oidc=initiate"; }); } diff --git a/public/js/authModals.js b/public/js/authModals.js index 968a07e..4677f12 100644 --- a/public/js/authModals.js +++ b/public/js/authModals.js @@ -97,7 +97,7 @@ export function openTOTPLoginModal() { .then(json => { if (json.status === "ok") { // recovery succeeded → finalize login - window.location.href = "index.html"; + window.location.href = "/index.html"; } else { showToast(json.message || t("recovery_code_verification_failed")); } @@ -125,7 +125,7 @@ export function openTOTPLoginModal() { .then(res => res.json()) .then(json => { if (json.status === "ok") { - window.location.href = "index.html"; + window.location.href = "/index.html"; } else { showToast(json.message || t("totp_verification_failed")); this.value = ""; diff --git a/public/js/fileActions.js b/public/js/fileActions.js index 5a9116a..8e994f5 100644 --- a/public/js/fileActions.js +++ b/public/js/fileActions.js @@ -111,7 +111,7 @@ export function confirmSingleDownload() { // Build the URL for download.php using GET parameters. const folder = window.currentFolder || "root"; - const downloadURL = "api/file/download.php?folder=" + encodeURIComponent(folder) + + const downloadURL = "/api/file/download.php?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(window.singleFileToDownload); fetch(downloadURL, { diff --git a/public/js/fileListView.js b/public/js/fileListView.js index 7de2fac..326bb3e 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -197,7 +197,7 @@ export function loadFileList(folderParam) { .then(response => { if (response.status === 401) { showToast("Session expired. Please log in again."); - window.location.href = "logout.php"; + window.location.href = "/api/auth/logout.php"; throw new Error("Unauthorized"); } return response.json(); diff --git a/public/js/filePreview.js b/public/js/filePreview.js index 123e3b8..66a97d2 100644 --- a/public/js/filePreview.js +++ b/public/js/filePreview.js @@ -65,9 +65,7 @@ export function openShareModal(file, folder) { .then(response => response.json()) .then(data => { if (data.token) { - let shareEndpoint = document.querySelector('meta[name="share-url"]') - ? document.querySelector('meta[name="share-url"]').getAttribute('content') - : (window.SHARE_URL || "api/file/share.php"); + const shareEndpoint = `${window.location.origin}/api/file/share.php`; const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`; const displayDiv = document.getElementById("shareLinkDisplay"); const inputField = document.getElementById("shareLinkInput"); diff --git a/public/js/folderManager.js b/public/js/folderManager.js index aca2b9a..7c27d18 100644 --- a/public/js/folderManager.js +++ b/public/js/folderManager.js @@ -364,7 +364,7 @@ export async function loadFolderTree(selectedFolder) { if (response.status === 401) { console.error("Unauthorized: Please log in to view folders."); showToast("Session expired. Please log in again."); - window.location.href = "logout.php"; + window.location.href = "/api/auth/logout.php"; return; } let folderData = await response.json(); diff --git a/public/js/upload.js b/public/js/upload.js index c3d72fe..ba43a76 100644 --- a/public/js/upload.js +++ b/public/js/upload.js @@ -405,7 +405,7 @@ const useResumable = true; // Enable resumable for file picker uploads let resumableInstance; function initResumableUpload() { resumableInstance = new Resumable({ - target: "api/upload/upload.php", + target: "/api/upload/upload.php", query: { folder: window.currentFolder || "root", upload_token: window.csrfToken }, chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks simultaneousUploads: 3, diff --git a/src/controllers/authController.php b/src/controllers/authController.php index ae01c52..dfb93d2 100644 --- a/src/controllers/authController.php +++ b/src/controllers/authController.php @@ -4,12 +4,14 @@ require_once __DIR__ . '/../../config/config.php'; require_once PROJECT_ROOT . '/src/models/AuthModel.php'; require_once PROJECT_ROOT . '/vendor/autoload.php'; +require_once PROJECT_ROOT . '/src/models/AdminModel.php'; use RobThree\Auth\Algorithm; use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider; use Jumbojett\OpenIDConnectClient; -class AuthController { +class AuthController +{ /** * @OA\Post( @@ -56,7 +58,8 @@ class AuthController { * * @return void Redirects on success or outputs JSON error. */ - public function auth(): void { + public function auth(): void + { // Global exception handler. set_exception_handler(function ($e) { error_log("Unhandled exception: " . $e->getMessage()); @@ -64,7 +67,7 @@ class AuthController { echo json_encode(["error" => "Internal Server Error"]); exit(); }); - + header('Content-Type: application/json'); // If OIDC parameters are present, initiate OIDC flow. @@ -73,20 +76,15 @@ class AuthController { $oidcAction = 'callback'; } if ($oidcAction) { - // Load admin configuration for OIDC. - $adminConfigFile = USERS_DIR . 'adminConfig.json'; - if (file_exists($adminConfigFile)) { - $enc = file_get_contents($adminConfigFile); - $dec = decryptData($enc, $encryptionKey); - $cfg = ($dec !== false) ? json_decode($dec, true) : []; - } else { - $cfg = []; - } - $oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com'; - $oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID'; - $oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET'; - $oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/api/auth/auth.php?oidc=callback'; + // new: delegate to AdminModel + $cfg = AdminModel::getConfig(); + // Optional: log to confirm you loaded the right values + error_log("Loaded OIDC config: " . print_r($cfg['oidc'], true)); + $oidc_provider_url = $cfg['oidc']['providerUrl']; + $oidc_client_id = $cfg['oidc']['clientId']; + $oidc_client_secret = $cfg['oidc']['clientSecret']; + $oidc_redirect_uri = $cfg['oidc']['redirectUri']; $oidc = new OpenIDConnectClient($oidc_provider_url, $oidc_client_id, $oidc_client_secret); $oidc->setRedirectURL($oidc_redirect_uri); @@ -94,7 +92,7 @@ class AuthController { try { $oidc->authenticate(); $username = $oidc->requestUserInfo('preferred_username'); - + // Check for TOTP secret. $totp_secret = null; $usersFile = USERS_DIR . USERS_FILE; @@ -110,17 +108,17 @@ class AuthController { if ($totp_secret) { $_SESSION['pending_login_user'] = $username; $_SESSION['pending_login_secret'] = $totp_secret; - header("Location: index.html?totp_required=1"); + header("Location: /index.html?totp_required=1"); exit(); } - + // Finalize login (no TOTP) session_regenerate_id(true); $_SESSION["authenticated"] = true; $_SESSION["username"] = $username; $_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1"); $_SESSION["folderOnly"] = loadUserPermissions($username); - header("Location: index.html"); + header("Location: /index.html"); exit(); } catch (Exception $e) { error_log("OIDC authentication error: " . $e->getMessage()); @@ -141,32 +139,32 @@ class AuthController { } } } - + // Fallback: Form-based Authentication. $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; - + if (!$username || !$password) { http_response_code(400); echo json_encode(["error" => "Username and password are required"]); exit(); } - + if (!preg_match(REGEX_USER, $username)) { http_response_code(400); echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]); exit(); } - + $ip = $_SERVER['REMOTE_ADDR']; $currentTime = time(); $attemptsFile = USERS_DIR . 'failed_logins.json'; $failedAttempts = AuthModel::loadFailedAttempts($attemptsFile); $maxAttempts = 5; $lockoutTime = 30 * 60; // 30 minutes - + if (isset($failedAttempts[$ip])) { $attemptData = $failedAttempts[$ip]; if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) { @@ -175,7 +173,7 @@ class AuthController { exit(); } } - + $user = AuthModel::authenticate($username, $password); if ($user !== false) { // Handle TOTP if required. @@ -203,19 +201,19 @@ class AuthController { } } } - + // Clear failed attempts. if (isset($failedAttempts[$ip])) { unset($failedAttempts[$ip]); AuthModel::saveFailedAttempts($attemptsFile, $failedAttempts); } - + session_regenerate_id(true); $_SESSION["authenticated"] = true; $_SESSION["username"] = $username; $_SESSION["isAdmin"] = ($user['role'] === "1"); $_SESSION["folderOnly"] = loadUserPermissions($username); - + // Handle "remember me" if ($rememberMe) { $persistentTokensFile = USERS_DIR . 'persistent_tokens.json'; @@ -240,7 +238,7 @@ class AuthController { $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); setcookie('remember_me_token', $tokenPersistent, $expiry, '/', '', $secure, true); } - + echo json_encode([ "status" => "ok", "success" => "Login successful", @@ -298,7 +296,8 @@ class AuthController { * * @return void Outputs a JSON response with authentication details. */ - public function checkAuth(): void { + public function checkAuth(): void + { header('Content-Type: application/json'); $usersFile = USERS_DIR . USERS_FILE; @@ -328,7 +327,7 @@ class AuthController { } } } - + // Determine admin status using AuthModel::getUserRole() $userRole = AuthModel::getUserRole($username); $isAdmin = ((int)$userRole === 1); @@ -344,7 +343,7 @@ class AuthController { exit; } - /** + /** * @OA\Get( * path="/api/auth/token.php", * summary="Retrieve CSRF token and share URL", @@ -366,7 +365,8 @@ class AuthController { * * @return void Outputs the JSON response. */ - public function getToken(): void { + public function getToken(): void + { header('Content-Type: application/json'); echo json_encode([ "csrf_token" => $_SESSION['csrf_token'], @@ -400,7 +400,8 @@ class AuthController { * * @return void Redirects on success or sends a 401 header. */ - public function loginBasic(): void { + public function loginBasic(): void + { // Set header for plain-text or JSON as needed. header('Content-Type: application/json'); @@ -432,7 +433,7 @@ class AuthController { // If TOTP is required, store pending values and redirect to prompt for TOTP. $_SESSION['pending_login_user'] = $username; $_SESSION['pending_login_secret'] = $secret; - header("Location: index.html?totp_required=1"); + header("Location: /index.html?totp_required=1"); exit; } // Finalize login. @@ -442,7 +443,7 @@ class AuthController { $_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1"); $_SESSION["folderOnly"] = AuthModel::loadFolderPermission($username); - header("Location: index.html"); + header("Location: /index.html"); exit; } // Invalid credentials; prompt again. @@ -473,16 +474,17 @@ class AuthController { * * @return void Redirects to index.html with a logout flag. */ - public function logout(): void { + public function logout(): void + { // Retrieve headers and check CSRF token. $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); $receivedToken = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; - + // Log mismatch but do not prevent logout. if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) { error_log("CSRF token mismatch on logout. Proceeding with logout."); } - + // Remove the "remember_me_token" from persistent tokens. if (isset($_COOKIE['remember_me_token'])) { $token = $_COOKIE['remember_me_token']; @@ -501,24 +503,29 @@ class AuthController { $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true); } - + // Clear session data. $_SESSION = []; - + // Clear the session cookie. if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); - setcookie(session_name(), '', time() - 42000, - $params["path"], $params["domain"], - $params["secure"], $params["httponly"] + setcookie( + session_name(), + '', + time() - 42000, + $params["path"], + $params["domain"], + $params["secure"], + $params["httponly"] ); } - + // Destroy the session. session_destroy(); - + // Redirect the user to the login page (or index) with a logout flag. - header("Location: index.html?logout=1"); + header("Location: /index.html?logout=1"); exit; } -} \ No newline at end of file +} diff --git a/src/controllers/fileController.php b/src/controllers/fileController.php index d278384..649cc12 100644 --- a/src/controllers/fileController.php +++ b/src/controllers/fileController.php @@ -935,7 +935,7 @@ class FileController {

This file is protected by a password.

-
+ diff --git a/src/controllers/folderController.php b/src/controllers/folderController.php index 0f7d2bb..a63d916 100644 --- a/src/controllers/folderController.php +++ b/src/controllers/folderController.php @@ -437,7 +437,7 @@ class FolderController {

Folder Protected

This folder is protected by a password. Please enter the password to view its contents.

- + @@ -534,7 +534,7 @@ class FolderController { foreach ($files as $file): $filePath = $data['realFolderPath'] . DIRECTORY_SEPARATOR . $file; $fileSize = file_exists($filePath) ? formatBytes(filesize($filePath)) : "Unknown"; - $downloadLink = "api/folder/downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file); + $downloadLink = "/api/folder/downloadSharedFile.php?token=" . urlencode($token) . "&file=" . urlencode($file); ?> @@ -557,7 +557,7 @@ class FolderController {