Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078db33458 | ||
|
|
04f5cbe31f | ||
|
|
b5a7d8d559 | ||
|
|
58f8485b02 | ||
|
|
3e1da9c335 | ||
|
|
6bf6206e1c | ||
|
|
f9c60951c9 |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 4/24/2025 1.2.5
|
||||
|
||||
- Enhance README and wiki with expanded installation instructions
|
||||
- Adjusted Dockerfile’s 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`
|
||||
|
||||
47
Dockerfile
47
Dockerfile
@@ -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>
|
||||
|
||||
@@ -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 server’s 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 server’s 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.)
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ DirectoryIndex index.html
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "^(api\.html|openapi\.json)$">
|
||||
Require valid-user
|
||||
</FilesMatch>
|
||||
|
||||
# -----------------------------
|
||||
# Enforce HTTPS (optional)
|
||||
# -----------------------------
|
||||
|
||||
@@ -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) Soft‐failure 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();
|
||||
|
||||
@@ -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">×</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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';");
|
||||
|
||||
// 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 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
|
||||
);
|
||||
|
||||
// Pending‑login 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);
|
||||
|
||||
// Re‑issue 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'] ?? '';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<IfModule mod_php7.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
<IfModule mod_php.c>
|
||||
php_flag engine off
|
||||
</IfModule>
|
||||
Options -Indexes
|
||||
@@ -1,3 +0,0 @@
|
||||
<Files "users.txt">
|
||||
Require all denied
|
||||
</Files>
|
||||
Reference in New Issue
Block a user