Compare commits

...

7 Commits

13 changed files with 307 additions and 196 deletions

View File

@@ -1,5 +1,22 @@
# Changelog
## Changes 4/24/2025 1.2.5
- Enhance README and wiki with expanded installation instructions
- Adjusted Dockerfiles Apache vhost to:
- Alias `/uploads/` to `/var/www/uploads/` with PHP engine disabled and directory indexes off
- Disable HTTP TRACE and tune keep-alive (On, max 100 requests, 5s timeout) and server Timeout (60s)
- Add security headers (`X-Frame-Options`, `X-Content-Type-Options`, `X-XSS-Protection`, `Referrer-Policy`)
- Enable `mod_deflate` compression for HTML, plain text, CSS, JS and JSON
- Configure `mod_expires` caching for images (1 month), CSS (1 week) and JS (3 hour)
- Deny access to hidden files (dot-files)
- Add access control in public/.htaccess for api.html & openapi.json; update Nginx example in wiki
- Remove obsolete folders from repo root
- Embed API documentation (`api.html`) directly in the FileRise UI as a full-screen modal
- Introduced `openApiModalBtn` in the user panel to launch the API modal
- Added `#apiModal` container with a same-origin `<iframe src="api.html">` so session cookies authenticate automatically
- Close control uses the existing `.editor-close-btn` for consistent styling and hover effects
## Changes 4/23/2025 1.2.4
**AuthModel**
@@ -30,6 +47,21 @@
- **start.sh**
- Session directory setup
- Always sends `credentials: 'include'` and `X-CSRF-Token: window.csrfToken` s
- On HTTP 403, automatically fetches a fresh CSRF token (from the response header or `/api/auth/token.php`) and retries the request once
- Always returns the real `Response` object (no more “clone.json” on every 200)
- Now calls `fetchWithCsrf('/api/auth/token.php')` to guarantee a fresh token
- Checks `res.ok`, then parses JSON to extract `csrf_token` and `share_url`
- Updates both `window.csrfToken` and the `<meta name="csrf-token">` & `<meta name="share-url">` tags
- Removed Old CSRF logic that cloned every successful response and parsed its JSON body
- Removed Any “soft-failure” JSON peek on non-403 responses
- Add missing permissions in `UserModel.php` for TOTP login.
- **Prevent XSS in breadcrumbs**
- Replaced `innerHTML` calls in `fileListTitle` with a new `updateBreadcrumbTitle()` helper that uses `textContent` + `DocumentFragment`.
- Introduced `renderBreadcrumbFragment()` to build each breadcrumb segment as a `<span class="breadcrumb-link" data-folder="…">` node.
- Added `setupBreadcrumbDelegation()` to handle clicks via event delegation on the container, eliminating per-element listeners.
- Removed any raw HTML concatenation to satisfy CodeQL and ensure all breadcrumb text is safely escaped.
## Changes 4/22/2025 v1.2.3
- Support for custom PUID/PGID via `PUID`/`PGID` environment variables, replacing the need to run the container with `--user`

View File

@@ -62,19 +62,64 @@ RUN chown -R root:www-data /var/www && \
# Apache site configuration
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
# Global settings
TraceEnable off
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
Timeout 60
ServerAdmin webmaster@localhost
DocumentRoot /var/www/public
# Security headers for all responses
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
</IfModule>
# Cache static assets
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType text/css "access plus 1 week"
ExpiresByType application/javascript "access plus 3 hour"
</IfModule>
# Protect uploads directory
Alias /uploads/ /var/www/uploads/
<Directory "/var/www/uploads/">
Options -Indexes
AllowOverride None
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
<IfModule mod_php.c>
php_flag engine off
</IfModule>
Require all granted
</Directory>
# Public directory
<Directory "/var/www/public">
AllowOverride All
Require all granted
DirectoryIndex index.html
DirectoryIndex index.html index.php
</Directory>
# Deny access to hidden files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
ErrorLog /var/www/metadata/log/error.log
CustomLog /var/www/metadata/log/access.log combined
</VirtualHost>

View File

@@ -1,7 +1,7 @@
# FileRise
**Elevate your File Management** A modern, self-hosted web file manager.
Upload, organize, and share files through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
Upload, organize, and share files or folders through a sleek web interface. **FileRise** is lightweight yet powerful: think of it as your personal cloud drive that you control. With drag-and-drop uploads, in-browser editing, secure user logins (with SSO and 2FA support), and one-click sharing, **FileRise** makes file management on your server a breeze.
**4/3/2025 Video demo:**
@@ -115,7 +115,7 @@ If you prefer to run FileRise on a traditional web server (LAMP stack or similar
git clone https://github.com/error311/FileRise.git
```
Place the files into your web servers directory (e.g., `/var/www/html/filerise`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
Place the files into your web servers directory (e.g., `/var/www/public`). It can be in a subfolder (just adjust the `BASE_URL` in config as below).
- **Composer Dependencies:** If you plan to use OIDC (SSO login), install Composer and run `composer install` in the FileRise directory. (This pulls in a couple of PHP libraries like jumbojett/openid-connect for OAuth support.)

View File

@@ -15,6 +15,10 @@ DirectoryIndex index.html
Require all denied
</FilesMatch>
<FilesMatch "^(api\.html|openapi\.json)$">
Require valid-user
</FilesMatch>
# -----------------------------
# Enforce HTTPS (optional)
# -----------------------------

View File

@@ -52,28 +52,24 @@ const originalFetch = window.fetch;
* @returns {Promise<Response>}
*/
export async function fetchWithCsrf(url, options = {}) {
options = { credentials: 'include', headers: {}, ...options };
options.headers['X-CSRF-Token'] = window.csrfToken;
// 1) Merge in credentials + header
options = {
credentials: 'include',
...options,
};
options.headers = {
...(options.headers || {}),
'X-CSRF-Token': window.csrfToken,
};
// 1) First attempt using the original fetch
// 2) First attempt
let res = await originalFetch(url, options);
// 2) Softfailure JSON check (200 + {csrf_expired})
if (res.ok && res.headers.get('content-type')?.includes('application/json')) {
const clone = res.clone();
const data = await clone.json();
if (data.csrf_expired) {
const newToken = data.csrf_token;
window.csrfToken = newToken;
document.querySelector('meta[name="csrf-token"]').content = newToken;
options.headers['X-CSRF-Token'] = newToken;
return originalFetch(url, options);
}
}
// 3) HTTP 403 fallback
// 3) If we got a 403, try to refresh token & retry
if (res.status === 403) {
// 3a) See if the server gave us a new token header
let newToken = res.headers.get('X-CSRF-Token');
// 3b) Otherwise fall back to the /api/auth/token endpoint
if (!newToken) {
const tokRes = await originalFetch('/api/auth/token.php', { credentials: 'include' });
if (tokRes.ok) {
@@ -82,17 +78,21 @@ export async function fetchWithCsrf(url, options = {}) {
}
}
if (newToken) {
// 3c) Update global + meta
window.csrfToken = newToken;
document.querySelector('meta[name="csrf-token"]').content = newToken;
const meta = document.querySelector('meta[name="csrf-token"]');
if (meta) meta.content = newToken;
// 3d) Retry the original request with the new token
options.headers['X-CSRF-Token'] = newToken;
res = await originalFetch(url, options);
}
}
// 4) Return the real Response—no body peeking here!
return res;
}
// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
function openTOTPLoginModal() {
originalOpenTOTPLoginModal();

View File

@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js';
import { loadAdminConfigFunc } from './auth.js';
const version = "v1.2.4"; // Update this version string as needed
const version = "v1.2.5"; // Update this version string as needed
const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
let lastLoginData = null;
@@ -230,14 +230,36 @@ export function openUserPanel() {
<!-- New API Docs link -->
<div style="margin-bottom: 15px;">
<a href="api.html" target="_blank" class="btn btn-secondary">
${t("api_docs") || "API Docs"}
</a>
<button type="button" id="openApiModalBtn" class="btn btn-secondary">
${t("api_docs") || "API Docs"}
</button>
</div>
</div>
`;
document.body.appendChild(userPanelModal);
const apiModal = document.createElement("div");
apiModal.id = "apiModal";
apiModal.style.cssText = `
position: fixed; top:0; left:0; width:100vw; height:100vh;
background: rgba(0,0,0,0.8); z-index: 4000; display:none;
align-items: center; justify-content: center;
`;
apiModal.innerHTML = `
<div style="position:relative; width:90vw; height:90vh; background:#fff; border-radius:8px; overflow:hidden;">
<div class="editor-close-btn" id="closeApiModal">&times;</div>
<iframe src="api.html" style="width:100%;height:100%;border:none;"></iframe>
</div>
`;
document.body.appendChild(apiModal);
document.getElementById("openApiModalBtn").addEventListener("click", () => {
apiModal.style.display = "flex";
});
document.getElementById("closeApiModal").addEventListener("click", () => {
apiModal.style.display = "none";
});
// Handlers…
document.getElementById("closeUserPanel").addEventListener("click", () => {
userPanelModal.style.display = "none";
@@ -246,6 +268,7 @@ export function openUserPanel() {
document.getElementById("changePasswordModal").style.display = "block";
});
// TOTP checkbox
const totpCheckbox = document.getElementById("userTOTPEnabled");
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";

View File

@@ -104,24 +104,26 @@ export function setupBreadcrumbDelegation() {
// Click handler via delegation
function breadcrumbClickHandler(e) {
// find the nearest .breadcrumb-link
const link = e.target.closest(".breadcrumb-link");
if (!link) return;
e.stopPropagation();
e.preventDefault();
const folder = link.getAttribute("data-folder");
const folder = link.dataset.folder;
window.currentFolder = folder;
localStorage.setItem("lastOpenedFolder", folder);
// Update the container with sanitized breadcrumbs.
const container = document.getElementById("fileListTitle");
const sanitizedBreadcrumb = DOMPurify.sanitize(renderBreadcrumb(folder));
container.innerHTML = t("files_in") + " (" + sanitizedBreadcrumb + ")";
// rebuild the title safely
updateBreadcrumbTitle(folder);
expandTreePath(folder);
document.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
const targetOption = document.querySelector(`.folder-option[data-folder="${folder}"]`);
if (targetOption) targetOption.classList.add("selected");
document.querySelectorAll(".folder-option").forEach(el =>
el.classList.remove("selected")
);
const target = document.querySelector(`.folder-option[data-folder="${folder}"]`);
if (target) target.classList.add("selected");
loadFileList(folder);
}
@@ -335,6 +337,38 @@ function folderDropHandler(event) {
/* ----------------------
Main Folder Tree Rendering and Event Binding
----------------------*/
// --- Helpers for safe breadcrumb rendering ---
function renderBreadcrumbFragment(folderPath) {
const frag = document.createDocumentFragment();
const parts = folderPath.split("/");
let acc = "";
parts.forEach((part, idx) => {
acc = idx === 0 ? part : acc + "/" + part;
const span = document.createElement("span");
span.classList.add("breadcrumb-link");
span.dataset.folder = acc;
span.textContent = part;
frag.appendChild(span);
if (idx < parts.length - 1) {
frag.appendChild(document.createTextNode(" / "));
}
});
return frag;
}
function updateBreadcrumbTitle(folder) {
const titleEl = document.getElementById("fileListTitle");
titleEl.textContent = "";
titleEl.appendChild(document.createTextNode(t("files_in") + " ("));
titleEl.appendChild(renderBreadcrumbFragment(folder));
titleEl.appendChild(document.createTextNode(")"));
setupBreadcrumbDelegation();
}
export async function loadFolderTree(selectedFolder) {
try {
// Check if the user has folder-only permission.
@@ -420,9 +454,8 @@ export async function loadFolderTree(selectedFolder) {
}
localStorage.setItem("lastOpenedFolder", window.currentFolder);
const titleEl = document.getElementById("fileListTitle");
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(window.currentFolder) + ")";
setupBreadcrumbDelegation();
// Initial breadcrumb update
updateBreadcrumbTitle(window.currentFolder);
loadFileList(window.currentFolder);
const folderState = loadFolderTreeState();
@@ -436,6 +469,7 @@ export async function loadFolderTree(selectedFolder) {
selectedEl.classList.add("selected");
}
// Folder-option click: update selection, breadcrumbs, and file list
container.querySelectorAll(".folder-option").forEach(el => {
el.addEventListener("click", function (e) {
e.stopPropagation();
@@ -444,13 +478,14 @@ export async function loadFolderTree(selectedFolder) {
const selected = this.getAttribute("data-folder");
window.currentFolder = selected;
localStorage.setItem("lastOpenedFolder", selected);
const titleEl = document.getElementById("fileListTitle");
titleEl.innerHTML = t("files_in") + " (" + renderBreadcrumb(selected) + ")";
setupBreadcrumbDelegation();
// Safe breadcrumb update
updateBreadcrumbTitle(selected);
loadFileList(selected);
});
});
// Root toggle handler
const rootToggle = container.querySelector("#rootRow .folder-toggle");
if (rootToggle) {
rootToggle.addEventListener("click", function (e) {
@@ -474,6 +509,7 @@ export async function loadFolderTree(selectedFolder) {
});
}
// Other folder-toggle handlers
container.querySelectorAll(".folder-toggle").forEach(toggle => {
toggle.addEventListener("click", function (e) {
e.stopPropagation();
@@ -502,6 +538,7 @@ export async function loadFolderTree(selectedFolder) {
}
}
// For backward compatibility.
export function loadFolderList(selectedFolder) {
loadFolderTree(selectedFolder);

View File

@@ -14,36 +14,20 @@ import { initFileActions, renameFile, openDownloadModal, confirmSingleDownload }
import { editFile, saveFile } from './fileEditor.js';
import { t, applyTranslations, setLocale } from './i18n.js';
// Remove the retry logic version and just use loadCsrfToken directly:
/**
* Fetches the current CSRF token (and share URL), updates window globals
* and <meta> tags, and returns the data.
*
* @returns {Promise<{csrf_token: string, share_url: string}>}
*/
export function loadCsrfToken() {
return fetch('/api/auth/token.php', {
method: 'GET',
credentials: 'include'
return fetchWithCsrf('/api/auth/token.php', {
method: 'GET'
})
.then(response => {
if (!response.ok) {
throw new Error(`Token fetch failed with status: ${response.status}`);
.then(res => {
if (!res.ok) {
throw new Error(`Token fetch failed with status ${res.status}`);
}
// Prefer header if set, otherwise fall back to body
const headerToken = response.headers.get('X-CSRF-Token');
return response.json()
.then(body => ({
csrf_token: headerToken || body.csrf_token,
share_url: body.share_url
}));
return res.json();
})
.then(({ csrf_token, share_url }) => {
// Update globals
// Update global and <meta>
window.csrfToken = csrf_token;
window.SHARE_URL = share_url;
// Sync <meta name="csrf-token">
let meta = document.querySelector('meta[name="csrf-token"]');
if (!meta) {
meta = document.createElement('meta');
@@ -52,7 +36,6 @@ export function loadCsrfToken() {
}
meta.content = csrf_token;
// Sync <meta name="share-url">
let shareMeta = document.querySelector('meta[name="share-url"]');
if (!shareMeta) {
shareMeta = document.createElement('meta');

View File

@@ -867,126 +867,123 @@ class UserController
* )
*/
public function verifyTOTP()
{
header('Content-Type: application/json');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
// Ratelimit
if (!isset($_SESSION['totp_failures'])) {
$_SESSION['totp_failures'] = 0;
}
if ($_SESSION['totp_failures'] >= 5) {
http_response_code(429);
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
exit;
}
// Must be authenticated OR pending login
if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
exit;
}
// CSRF check
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
exit;
}
// Parse and validate input
$inputData = json_decode(file_get_contents("php://input"), true);
$code = trim($inputData['totp_code'] ?? '');
if (!preg_match('/^\d{6}$/', $code)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
exit;
}
// TFA helper
$tfa = new \RobThree\Auth\TwoFactorAuth(
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
'FileRise',
6,
30,
\RobThree\Auth\Algorithm::Sha1
);
// Pendinglogin flow (first password step passed)
if (isset($_SESSION['pending_login_user'])) {
$username = $_SESSION['pending_login_user'];
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
$_SESSION['totp_failures']++;
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
exit;
}
// === Issue “remember me” token if requested ===
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) ?: [];
}
$isAdmin = ((int)userModel::getUserRole($username) === 1);
$all[$token] = [
'username' => $username,
'expiry' => $expiry,
'isAdmin' => $isAdmin
];
file_put_contents(
$tokFile,
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
LOCK_EX
);
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
// Persistent cookie
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
// Reissue PHP session cookie
setcookie(
session_name(),
session_id(),
$expiry,
'/',
'',
$secure,
true
);
}
// Finalize login
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
$_SESSION['isAdmin'] = $isAdmin;
$_SESSION['folderOnly'] = loadUserPermissions($username);
// Clean up
unset(
$_SESSION['pending_login_user'],
$_SESSION['pending_login_secret'],
$_SESSION['pending_login_remember_me'],
$_SESSION['totp_failures']
);
echo json_encode(['status' => 'ok', 'message' => 'Login successful']);
exit;
}
public function verifyTOTP()
{
header('Content-Type: application/json');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
// Rate-limit
if (!isset($_SESSION['totp_failures'])) {
$_SESSION['totp_failures'] = 0;
}
if ($_SESSION['totp_failures'] >= 5) {
http_response_code(429);
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
exit;
}
// Must be authenticated OR pending login
if (empty($_SESSION['authenticated']) && !isset($_SESSION['pending_login_user'])) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
exit;
}
// CSRF check
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
$csrfHeader = $headersArr['x-csrf-token'] ?? '';
if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
exit;
}
// Parse & validate input
$inputData = json_decode(file_get_contents("php://input"), true);
$code = trim($inputData['totp_code'] ?? '');
if (!preg_match('/^\d{6}$/', $code)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
exit;
}
// TFA helper
$tfa = new \RobThree\Auth\TwoFactorAuth(
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
);
// === Pending-login flow (we just came from auth and need to finish login) ===
if (isset($_SESSION['pending_login_user'])) {
$username = $_SESSION['pending_login_user'];
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
$rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
$_SESSION['totp_failures']++;
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
exit;
}
// Issue “remember me” token if requested
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' => ((int)userModel::getUserRole($username) === 1),
'folderOnly' => loadUserPermissions($username)['folderOnly'] ?? false,
'readOnly' => loadUserPermissions($username)['readOnly'] ?? false,
'disableUpload'=> loadUserPermissions($username)['disableUpload']?? false
];
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);
setcookie(session_name(), session_id(), $expiry, '/', '', $secure, true);
}
// === Finalize login into session exactly as finalizeLogin() would ===
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
$_SESSION['isAdmin'] = ((int)userModel::getUserRole($username) === 1);
$perms = loadUserPermissions($username);
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
// Clean up pending markers
unset(
$_SESSION['pending_login_user'],
$_SESSION['pending_login_secret'],
$_SESSION['pending_login_remember_me'],
$_SESSION['totp_failures']
);
// Send back full login payload
echo json_encode([
'status' => 'ok',
'success' => 'Login successful',
'isAdmin' => $_SESSION['isAdmin'],
'folderOnly' => $_SESSION['folderOnly'],
'readOnly' => $_SESSION['readOnly'],
'disableUpload' => $_SESSION['disableUpload'],
'username' => $_SESSION['username']
]);
exit;
}
// Setup/verification flow (not pending)
$username = $_SESSION['username'] ?? '';

View File

View File

@@ -1,7 +0,0 @@
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
<IfModule mod_php.c>
php_flag engine off
</IfModule>
Options -Indexes

View File

View File

@@ -1,3 +0,0 @@
<Files "users.txt">
Require all denied
</Files>