Compare commits

..

7 Commits

13 changed files with 307 additions and 196 deletions

View File

@@ -1,5 +1,22 @@
# Changelog # 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 ## Changes 4/23/2025 1.2.4
**AuthModel** **AuthModel**
@@ -30,6 +47,21 @@
- **start.sh** - **start.sh**
- Session directory setup - 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 ## 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` - 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 # Apache site configuration
RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80> <VirtualHost *:80>
# Global settings
TraceEnable off
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
Timeout 60
ServerAdmin webmaster@localhost ServerAdmin webmaster@localhost
DocumentRoot /var/www/public 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/ Alias /uploads/ /var/www/uploads/
<Directory "/var/www/uploads/"> <Directory "/var/www/uploads/">
Options -Indexes Options -Indexes
AllowOverride None AllowOverride None
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
<IfModule mod_php.c>
php_flag engine off
</IfModule>
Require all granted Require all granted
</Directory> </Directory>
# Public directory
<Directory "/var/www/public"> <Directory "/var/www/public">
AllowOverride All AllowOverride All
Require all granted Require all granted
DirectoryIndex index.html DirectoryIndex index.html index.php
</Directory> </Directory>
# Deny access to hidden files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
ErrorLog /var/www/metadata/log/error.log ErrorLog /var/www/metadata/log/error.log
CustomLog /var/www/metadata/log/access.log combined CustomLog /var/www/metadata/log/access.log combined
</VirtualHost> </VirtualHost>

View File

@@ -1,7 +1,7 @@
# FileRise # FileRise
**Elevate your File Management** A modern, self-hosted web file manager. **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:** **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 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.) - **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 Require all denied
</FilesMatch> </FilesMatch>
<FilesMatch "^(api\.html|openapi\.json)$">
Require valid-user
</FilesMatch>
# ----------------------------- # -----------------------------
# Enforce HTTPS (optional) # Enforce HTTPS (optional)
# ----------------------------- # -----------------------------

View File

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

View File

@@ -3,7 +3,7 @@ import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js'; import { t, applyTranslations, setLocale } from './i18n.js';
import { loadAdminConfigFunc } from './auth.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>`; const adminTitle = `${t("admin_panel")} <small style="font-size: 12px; color: gray;">${version}</small>`;
let lastLoginData = null; let lastLoginData = null;
@@ -230,14 +230,36 @@ export function openUserPanel() {
<!-- New API Docs link --> <!-- New API Docs link -->
<div style="margin-bottom: 15px;"> <div style="margin-bottom: 15px;">
<a href="api.html" target="_blank" class="btn btn-secondary"> <button type="button" id="openApiModalBtn" class="btn btn-secondary">
${t("api_docs") || "API Docs"} ${t("api_docs") || "API Docs"}
</a> </button>
</div> </div>
</div> </div>
`; `;
document.body.appendChild(userPanelModal); 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… // Handlers…
document.getElementById("closeUserPanel").addEventListener("click", () => { document.getElementById("closeUserPanel").addEventListener("click", () => {
userPanelModal.style.display = "none"; userPanelModal.style.display = "none";
@@ -246,6 +268,7 @@ export function openUserPanel() {
document.getElementById("changePasswordModal").style.display = "block"; document.getElementById("changePasswordModal").style.display = "block";
}); });
// TOTP checkbox // TOTP checkbox
const totpCheckbox = document.getElementById("userTOTPEnabled"); const totpCheckbox = document.getElementById("userTOTPEnabled");
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true"; totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";

View File

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

View File

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

View File

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