Overhaul networkUtils and expand auth

This commit is contained in:
Ryan
2025-04-17 01:20:18 -04:00
committed by GitHub
parent 75d3bf5a9b
commit 22cce5a898
7 changed files with 263 additions and 209 deletions

View File

@@ -26,6 +26,13 @@ This refactor improves maintainability, testability, and documentation clarity a
- Switched your share modal code to use a leading slash ("/api/file/share.php") so it generates absolute URLs instead of relative /share.php. - 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 sharedfolder gallery, adjusted the clientside image path to point at /uploads/... instead of /api/folder/uploads/... - In the sharedfolder gallery, adjusted the clientside image path to point at /uploads/... instead of /api/folder/uploads/...
- Updated both AdminModel defaults and the AuthController to use the exact full path - Updated both AdminModel defaults and the AuthController to use the exact full path
- Network Utilities Overhaul swapped out the old fetch wrapper for one that always reads the raw response, tries to JSON.parse it, and then either returns the parsed object on ok or throws it on error.
- Adjusted your submitLogin .catch() to grab the thrown object (or string) and pass that through to showToast, so now “Invalid credentials” actually shows up.
- Pulled the common sessionsetup and “remember me” logic into two new helpers, finalizeLogin() (for AJAX/form/basic/TOTP) and finishBrowserLogin() (for OIDC redirects). That removed tons of duplication and ensures every path calls the same permissionloading code.
- Ensured that after you POST just a totp_code, we pick up pending_login_user/pending_login_secret, verify it, then immediately call finalizeLogin().
- Expanded checkAuth.php Response now returns all three flags—folderOnly, readOnly, and disableUpload so client can handle every permission.
- In auth.jss updateAuthenticatedUI(), write all three flags into localStorage whenever you land on the app (OIDC, basic or form). That guarantees consistent behavior across page loads.
- Made sure the OIDC handler reads the live config via AdminModel::getConfig() and pushes you through the TOTP flow if needed, then back to /index.html.
--- ---

View File

@@ -118,10 +118,10 @@ $cookieParams = [
'samesite' => 'Lax' 'samesite' => 'Lax'
]; ];
// At the very beginning of config.php // At the very beginning of config.php
ini_set('session.save_path', __DIR__ . '/../sessions'); /*ini_set('session.save_path', __DIR__ . '/../sessions');
if (!is_dir(__DIR__ . '/../sessions')) { if (!is_dir(__DIR__ . '/../sessions')) {
mkdir(__DIR__ . '/../sessions', 0777, true); mkdir(__DIR__ . '/../sessions', 0777, true);
} }*/
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params($cookieParams); session_set_cookie_params($cookieParams);
ini_set('session.gc_maxlifetime', 7200); ini_set('session.gc_maxlifetime', 7200);
@@ -164,7 +164,7 @@ define('BASE_URL', 'http://yourwebsite/uploads/');
if (strpos(BASE_URL, 'yourwebsite') !== false) { if (strpos(BASE_URL, 'yourwebsite') !== false) {
$defaultShareUrl = isset($_SERVER['HTTP_HOST']) $defaultShareUrl = isset($_SERVER['HTTP_HOST'])
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php" ? "http://" . $_SERVER['HTTP_HOST'] . "/api/file/share.php"
: "http://localhost/api/file/share.php"; : "http://localhost/api/file/share.php";
} else { } else {
$defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php"; $defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php";

View File

@@ -15,7 +15,7 @@
<link rel="icon" type="image/png" href="/assets/logo.png"> <link rel="icon" type="image/png" href="/assets/logo.png">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg"> <link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="csrf-token" content=""> <meta name="csrf-token" content="">
<meta name="share-url" content="/api/file/share.php"> <meta name="share-url" content="">
<!-- Google Fonts and Material Icons --> <!-- Google Fonts and Material Icons -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" /> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />

View File

@@ -149,11 +149,12 @@ function updateAuthenticatedUI(data) {
if (data.username) { if (data.username) {
localStorage.setItem("username", data.username); localStorage.setItem("username", data.username);
} }
/* if (typeof data.folderOnly !== "undefined") {
if (typeof data.folderOnly !== "undefined") { localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false"); localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
} }
*/
const headerButtons = document.querySelector(".header-buttons"); const headerButtons = document.querySelector(".header-buttons");
const firstButton = headerButtons.firstElementChild; const firstButton = headerButtons.firstElementChild;
@@ -227,6 +228,10 @@ function checkAuthentication(showLoginToast = true) {
} }
window.setupMode = false; window.setupMode = false;
if (data.authenticated) { if (data.authenticated) {
localStorage.setItem("folderOnly", data.folderOnly );
localStorage.setItem("readOnly", data.readOnly );
localStorage.setItem("disableUpload",data.disableUpload);
updateLoginOptionsUIFromStorage();
if (typeof data.totp_enabled !== "undefined") { if (typeof data.totp_enabled !== "undefined") {
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
} }
@@ -249,6 +254,7 @@ function checkAuthentication(showLoginToast = true) {
function submitLogin(data) { function submitLogin(data) {
setLastLoginData(data); setLastLoginData(data);
window.__lastLoginData = data; window.__lastLoginData = data;
sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
.then(response => { .then(response => {
if (response.success || response.status === "ok") { if (response.success || response.status === "ok") {
@@ -263,7 +269,7 @@ function submitLogin(data) {
} }
}) })
.catch(() => { .catch(() => {
// if fetching permissions fails. // ignore permissionfetch errors
}) })
.finally(() => { .finally(() => {
window.location.reload(); window.location.reload();
@@ -272,7 +278,7 @@ function submitLogin(data) {
openTOTPLoginModal(); openTOTPLoginModal();
} else if (response.error && response.error.includes("Too many failed login attempts")) { } else if (response.error && response.error.includes("Too many failed login attempts")) {
showToast(response.error); showToast(response.error);
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']"); const loginButton = document.querySelector("#authForm button[type='submit']");
if (loginButton) { if (loginButton) {
loginButton.disabled = true; loginButton.disabled = true;
setTimeout(() => { setTimeout(() => {
@@ -284,10 +290,18 @@ function submitLogin(data) {
showToast("Login failed: " + (response.error || "Unknown error")); showToast("Login failed: " + (response.error || "Unknown error"));
} }
}) })
.catch(() => { .catch(err => {
showToast("Login failed: Unknown error"); // err may be an Error object or a string
let msg = "Unknown error";
if (err && typeof err === "object") {
msg = err.error || err.message || msg;
} else if (typeof err === "string") {
msg = err;
}
showToast(`Login failed: ${msg}`);
}); });
} }
window.submitLogin = submitLogin; window.submitLogin = submitLogin;
/* ----------------- Other Helpers ----------------- */ /* ----------------- Other Helpers ----------------- */

View File

@@ -1,31 +1,31 @@
// public/js/networkUtils.js
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) { export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
const options = { const options = {
method, method,
credentials: 'include', credentials: 'include',
headers: {} headers: { ...customHeaders }
}; };
// Merge custom headers
Object.assign(options.headers, customHeaders);
// If data is provided and is not FormData, assume JSON.
if (data && !(data instanceof FormData)) { if (data && !(data instanceof FormData)) {
if (!options.headers["Content-Type"]) { options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
options.headers["Content-Type"] = "application/json";
}
options.body = JSON.stringify(data); options.body = JSON.stringify(data);
} else if (data instanceof FormData) { } else if (data instanceof FormData) {
options.body = data; options.body = data;
} }
return fetch(url, options) return fetch(url, options)
.then(response => { .then(async res => {
if (!response.ok) { const text = await res.text();
return response.text().then(text => { let payload;
throw new Error(`HTTP error ${response.status}: ${text}`); try {
}); payload = JSON.parse(text);
} catch {
payload = text;
} }
const clonedResponse = response.clone(); if (!res.ok) {
return response.json().catch(() => clonedResponse.text()); // Reject with the parsed JSON (or raw text) so .catch(error) gets it
throw payload;
}
return payload;
}); });
} }

View File

@@ -58,209 +58,233 @@ class AuthController
* *
* @return void Redirects on success or outputs JSON error. * @return void Redirects on success or outputs JSON error.
*/ */
// in src/controllers/AuthController.php
public function auth(): void public function auth(): void
{ {
// Global exception handler. header('Content-Type: application/json');
set_exception_handler(function ($e) { set_exception_handler(function ($e) {
error_log("Unhandled exception: " . $e->getMessage()); error_log("Unhandled exception: " . $e->getMessage());
http_response_code(500); http_response_code(500);
echo json_encode(["error" => "Internal Server Error"]); echo json_encode(['error' => 'Internal Server Error']);
exit(); exit();
}); });
header('Content-Type: application/json'); // Decode any JSON payload
$data = json_decode(file_get_contents('php://input'), true) ?: [];
$username = trim($data['username'] ?? '');
$password = trim($data['password'] ?? '');
$totpCode = trim($data['totp_code'] ?? '');
$rememberMe = !empty($data['remember_me']);
// If OIDC parameters are present, initiate OIDC flow. //
// 1) TOTPonly step: user already passed credentials and we asked for TOTP,
// now they POST just totp_code.
//
if ($totpCode && isset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'])) {
$username = $_SESSION['pending_login_user'];
$secret = $_SESSION['pending_login_secret'];
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, Algorithm::Sha1);
if (! $tfa->verifyCode($secret, $totpCode)) {
echo json_encode(['error' => 'Invalid TOTP code']);
exit();
}
// clear the pending markers
unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']);
// now finish login
$this->finalizeLogin($username, $rememberMe);
}
//
// 2) OIDC flow
//
$oidcAction = $_GET['oidc'] ?? null; $oidcAction = $_GET['oidc'] ?? null;
if (!$oidcAction && isset($_GET['code'])) { if (! $oidcAction && isset($_GET['code'])) {
$oidcAction = 'callback'; $oidcAction = 'callback';
} }
if ($oidcAction) { if ($oidcAction) {
// new: delegate to AdminModel
$cfg = AdminModel::getConfig(); $cfg = AdminModel::getConfig();
// Optional: log to confirm you loaded the right values $oidc = new OpenIDConnectClient(
error_log("Loaded OIDC config: " . print_r($cfg['oidc'], true)); $cfg['oidc']['providerUrl'],
$cfg['oidc']['clientId'],
$oidc_provider_url = $cfg['oidc']['providerUrl']; $cfg['oidc']['clientSecret']
$oidc_client_id = $cfg['oidc']['clientId']; );
$oidc_client_secret = $cfg['oidc']['clientSecret']; $oidc->setRedirectURL($cfg['oidc']['redirectUri']);
$oidc_redirect_uri = $cfg['oidc']['redirectUri'];
$oidc = new OpenIDConnectClient($oidc_provider_url, $oidc_client_id, $oidc_client_secret);
$oidc->setRedirectURL($oidc_redirect_uri);
if ($oidcAction === 'callback') { if ($oidcAction === 'callback') {
try { try {
$oidc->authenticate(); $oidc->authenticate();
$username = $oidc->requestUserInfo('preferred_username'); $username = $oidc->requestUserInfo('preferred_username');
// Check for TOTP secret. // check if this user has a TOTP secret
$totp_secret = null; $totp_secret = null;
$usersFile = USERS_DIR . USERS_FILE; $usersFile = USERS_DIR . USERS_FILE;
if (file_exists($usersFile)) { if (file_exists($usersFile)) {
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$parts = explode(":", trim($line)); $parts = explode(':', trim($line));
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) { if (count($parts) >= 4 && $parts[0] === $username && $parts[3] !== '') {
$totp_secret = decryptData($parts[3], $encryptionKey); $totp_secret = decryptData($parts[3], $GLOBALS['encryptionKey']);
break; break;
} }
} }
} }
if ($totp_secret) { if ($totp_secret) {
$_SESSION['pending_login_user'] = $username; $_SESSION['pending_login_user'] = $username;
$_SESSION['pending_login_secret'] = $totp_secret; $_SESSION['pending_login_secret'] = $totp_secret;
header("Location: /index.html?totp_required=1"); header('Location: /index.html?totp_required=1');
exit(); exit();
} }
// Finalize login (no TOTP) // no TOTP → finish immediately
session_regenerate_id(true); $this->finishBrowserLogin($username);
$_SESSION["authenticated"] = true; } catch (\Exception $e) {
$_SESSION["username"] = $username; error_log("OIDC auth error: " . $e->getMessage());
$_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1");
$_SESSION["folderOnly"] = loadUserPermissions($username);
header("Location: /index.html");
exit();
} catch (Exception $e) {
error_log("OIDC authentication error: " . $e->getMessage());
http_response_code(401); http_response_code(401);
echo json_encode(["error" => "Authentication failed."]); echo json_encode(['error' => 'Authentication failed.']);
exit(); exit();
} }
} else { } else {
// Initiate OIDC authentication. // initial OIDC redirect
try { try {
$oidc->authenticate(); $oidc->authenticate();
exit(); exit();
} catch (Exception $e) { } catch (\Exception $e) {
error_log("OIDC initiation error: " . $e->getMessage()); error_log("OIDC initiation error: " . $e->getMessage());
http_response_code(401); http_response_code(401);
echo json_encode(["error" => "Authentication initiation failed."]); echo json_encode(['error' => 'Authentication initiation failed.']);
exit(); exit();
} }
} }
} }
// Fallback: Form-based Authentication. //
$data = json_decode(file_get_contents("php://input"), true); // 3) Formbased / AJAX login
$username = trim($data["username"] ?? ""); //
$password = trim($data["password"] ?? ""); if (! $username || ! $password) {
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
if (!$username || !$password) {
http_response_code(400); http_response_code(400);
echo json_encode(["error" => "Username and password are required"]); 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']);
exit(); exit();
} }
if (!preg_match(REGEX_USER, $username)) { // ratelimit
http_response_code(400); $ip = $_SERVER['REMOTE_ADDR'];
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'; $attemptsFile = USERS_DIR . 'failed_logins.json';
$failedAttempts = AuthModel::loadFailedAttempts($attemptsFile); $failed = AuthModel::loadFailedAttempts($attemptsFile);
$maxAttempts = 5; if (
$lockoutTime = 30 * 60; // 30 minutes isset($failed[$ip]) &&
$failed[$ip]['count'] >= 5 &&
if (isset($failedAttempts[$ip])) { time() - $failed[$ip]['last_attempt'] < 30 * 60
$attemptData = $failedAttempts[$ip]; ) {
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) { http_response_code(429);
http_response_code(429); echo json_encode(['error' => 'Too many failed login attempts. Please try again later.']);
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]); exit();
exit();
}
} }
$user = AuthModel::authenticate($username, $password); $user = AuthModel::authenticate($username, $password);
if ($user !== false) { if ($user === false) {
// Handle TOTP if required. // record failure
if (!empty($user['totp_secret'])) { $failed[$ip] = [
if (empty($data['totp_code']) || !preg_match('/^\d{6}$/', $data['totp_code'])) { 'count' => ($failed[$ip]['count'] ?? 0) + 1,
$_SESSION['pending_login_user'] = $username; 'last_attempt' => time()
$_SESSION['pending_login_secret'] = $user['totp_secret']; ];
echo json_encode([ AuthModel::saveFailedAttempts($attemptsFile, $failed);
"totp_required" => true,
"message" => "TOTP code required"
]);
exit();
} else {
$tfa = new \RobThree\Auth\TwoFactorAuth(
new GoogleChartsQrCodeProvider(),
'FileRise',
6,
30,
Algorithm::Sha1
);
$providedCode = trim($data['totp_code']);
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
echo json_encode(["error" => "Invalid TOTP code"]);
exit();
}
}
}
// 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';
$tokenPersistent = bin2hex(random_bytes(32));
$expiry = time() + (30 * 24 * 60 * 60);
$persistentTokens = [];
if (file_exists($persistentTokensFile)) {
$encryptedContent = file_get_contents($persistentTokensFile);
$decryptedContent = decryptData($encryptedContent, $GLOBALS['encryptionKey']);
$persistentTokens = json_decode($decryptedContent, true);
if (!is_array($persistentTokens)) {
$persistentTokens = [];
}
}
$persistentTokens[$tokenPersistent] = [
"username" => $username,
"expiry" => $expiry,
"isAdmin" => ($_SESSION["isAdmin"] === true)
];
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']);
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
setcookie('remember_me_token', $tokenPersistent, $expiry, '/', '', $secure, true);
}
echo json_encode([
"status" => "ok",
"success" => "Login successful",
"isAdmin" => $_SESSION["isAdmin"],
"folderOnly" => $_SESSION["folderOnly"],
"username" => $_SESSION["username"]
]);
} else {
// Record failed login attempt.
if (isset($failedAttempts[$ip])) {
$failedAttempts[$ip]['count']++;
$failedAttempts[$ip]['last_attempt'] = $currentTime;
} else {
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
}
AuthModel::saveFailedAttempts($attemptsFile, $failedAttempts);
$failedLogFile = USERS_DIR . 'failed_login.log';
$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);
http_response_code(401); http_response_code(401);
echo json_encode(["error" => "Invalid credentials"]); echo json_encode(['error' => 'Invalid credentials']);
exit();
} }
// if this account has TOTP, ask for it
if (! empty($user['totp_secret'])) {
$_SESSION['pending_login_user'] = $username;
$_SESSION['pending_login_secret'] = $user['totp_secret'];
echo json_encode(['totp_required' => true]);
exit();
}
// otherwise clear ratelimit & finish
if (isset($failed[$ip])) {
unset($failed[$ip]);
AuthModel::saveFailedAttempts($attemptsFile, $failed);
}
$this->finalizeLogin($username, $rememberMe);
}
/**
* Finalize an AJAXstyle login (form/basic/TOTP) by
* issuing the session, rememberme cookie, and JSON payload.
*/
protected function finalizeLogin(string $username, bool $rememberMe): void
{
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
$_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1');
$perms = loadUserPermissions($username);
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
// rememberme
if ($rememberMe) {
$tokFile = USERS_DIR . 'persistent_tokens.json';
$token = bin2hex(random_bytes(32));
$expiry = time() + 30 * 24 * 60 * 60;
$all = [];
if (file_exists($tokFile)) {
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
$all = json_decode($dec, true) ?: [];
}
$all[$token] = [
'username' => $username,
'expiry' => $expiry,
'isAdmin' => $_SESSION['isAdmin']
];
file_put_contents(
$tokFile,
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
LOCK_EX
);
$secure = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
}
echo json_encode([
'status' => 'ok',
'success' => 'Login successful',
'isAdmin' => $_SESSION['isAdmin'],
'folderOnly' => $_SESSION['folderOnly'],
'readOnly' => $_SESSION['readOnly'],
'disableUpload' => $_SESSION['disableUpload'],
'username' => $username
]);
exit();
}
/**
* A version of finalizeLogin() that ends in a browser redirect
* (used for OIDC nonAJAX flows).
*/
protected function finishBrowserLogin(string $username): void
{
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
$_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1');
$perms = loadUserPermissions($username);
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
header('Location: /index.html');
exit();
} }
/** /**
@@ -296,51 +320,45 @@ class AuthController
* *
* @return void Outputs a JSON response with authentication details. * @return void Outputs a JSON response with authentication details.
*/ */
public function checkAuth(): void public function checkAuth(): void
{ {
header('Content-Type: application/json'); header('Content-Type: application/json');
$usersFile = USERS_DIR . USERS_FILE; $usersFile = USERS_DIR . USERS_FILE;
// If the users file does not exist or is empty, signal setup mode.
// setup mode?
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') { if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
error_log("checkAuth: users file not found or empty; entering setup mode."); error_log("checkAuth: setup mode");
echo json_encode(["setup" => true]); echo json_encode(['setup' => true]);
exit; exit();
}
if (empty($_SESSION['authenticated'])) {
echo json_encode(['authenticated' => false]);
exit();
} }
// If the session is not authenticated, output false. // TOTP enabled?
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { $totp = false;
echo json_encode(["authenticated" => false]); foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
exit; $parts = explode(':', trim($line));
} if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
$totp = true;
// Retrieve the username from the session. break;
$username = $_SESSION['username'] ?? '';
// Determine TOTP enabled by checking the users file.
$totp_enabled = false;
if ($username) {
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$parts = explode(':', trim($line));
if ($parts[0] === $username && isset($parts[3]) && trim($parts[3]) !== "") {
$totp_enabled = true;
break;
}
} }
} }
// Determine admin status using AuthModel::getUserRole() $isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1);
$userRole = AuthModel::getUserRole($username); $resp = [
$isAdmin = ((int)$userRole === 1); 'authenticated' => true,
'isAdmin' => $isAdmin,
$response = [ 'totp_enabled' => $totp,
"authenticated" => true, 'username' => $_SESSION['username'],
"isAdmin" => $isAdmin, 'folderOnly' => $_SESSION['folderOnly'] ?? false,
"totp_enabled" => $totp_enabled, 'readOnly' => $_SESSION['readOnly'] ?? false,
"username" => $username, 'disableUpload' => $_SESSION['disableUpload'] ?? false
"folderOnly" => $_SESSION["folderOnly"] ?? false
]; ];
echo json_encode($response); echo json_encode($resp);
exit; exit();
} }
/** /**
@@ -441,7 +459,11 @@ class AuthController
$_SESSION["authenticated"] = true; $_SESSION["authenticated"] = true;
$_SESSION["username"] = $username; $_SESSION["username"] = $username;
$_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1"); $_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1");
$_SESSION["folderOnly"] = AuthModel::loadFolderPermission($username); // load _all_ the permissions
$userPerms = loadUserPermissions($username);
$_SESSION["folderOnly"] = $userPerms["folderOnly"] ?? false;
$_SESSION["readOnly"] = $userPerms["readOnly"] ?? false;
$_SESSION["disableUpload"] = $userPerms["disableUpload"] ?? false;
header("Location: /index.html"); header("Location: /index.html");
exit; exit;

View File

@@ -1301,7 +1301,18 @@ class FileController {
// Delegate deletion to the model. // Delegate deletion to the model.
$result = FileModel::deleteTrashFiles($filesToDelete); $result = FileModel::deleteTrashFiles($filesToDelete);
echo json_encode($result);
// Build a humanfriendly success or error message
if (!empty($result['deleted'])) {
$count = count($result['deleted']);
$msg = "Trash item" . ($count===1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
echo json_encode(["success" => $msg]);
} elseif (!empty($result['error'])) {
echo json_encode(["error" => $result['error']]);
} else {
echo json_encode(["success" => "No items to delete."]);
}
exit;
} }
/** /**