704 lines
24 KiB
PHP
704 lines
24 KiB
PHP
<?php
|
||
// src/controllers/UserController.php
|
||
|
||
require_once __DIR__ . '/../../config/config.php';
|
||
require_once PROJECT_ROOT . '/src/models/UserModel.php';
|
||
require_once PROJECT_ROOT . '/src/models/AdminModel.php';
|
||
|
||
/**
|
||
* UserController
|
||
* - Hardened CSRF/auth checks (works even when getallheaders() is unavailable)
|
||
* - Consistent method checks without breaking existing clients (accepts POST as fallback for some endpoints)
|
||
* - Stricter validation & safer defaults
|
||
* - Fixed TOTP setup bug for pending-login users
|
||
* - Standardized calls to UserModel (proper case)
|
||
*/
|
||
class UserController
|
||
{
|
||
/* ---------- Small internal helpers to reduce repetition ---------- */
|
||
|
||
/** Get headers in lowercase, robust across SAPIs. */
|
||
private static function headersLower(): array
|
||
{
|
||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||
$out = [];
|
||
foreach ($headers as $k => $v) {
|
||
$out[strtolower($k)] = $v;
|
||
}
|
||
// Fallbacks from $_SERVER if needed
|
||
foreach ($_SERVER as $k => $v) {
|
||
if (strpos($k, 'HTTP_') === 0) {
|
||
$h = strtolower(str_replace('_', '-', substr($k, 5)));
|
||
if (!isset($out[$h])) $out[$h] = $v;
|
||
}
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/** Enforce allowed HTTP method(s); default to 405 if not allowed. */
|
||
private static function requireMethod(array $allowed): void
|
||
{
|
||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||
if (!in_array($method, $allowed, true)) {
|
||
http_response_code(405);
|
||
header('Allow: ' . implode(', ', $allowed));
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => 'Method not allowed']);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
/** Enforce authentication (401). */
|
||
private static function requireAuth(): void
|
||
{
|
||
if (empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||
http_response_code(401);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => 'Unauthorized']);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
/** Enforce admin (401). */
|
||
private static function requireAdmin(): void
|
||
{
|
||
self::requireAuth();
|
||
|
||
// Prefer the session flag
|
||
$isAdmin = (!empty($_SESSION['isAdmin']) && $_SESSION['isAdmin'] === true);
|
||
|
||
// Fallback: check the user’s role in storage (e.g., users.txt/DB)
|
||
if (!$isAdmin) {
|
||
$u = $_SESSION['username'] ?? '';
|
||
if ($u) {
|
||
try {
|
||
// UserModel::getUserRole($u) should return '1' for admins
|
||
$isAdmin = (UserModel::getUserRole($u) === '1');
|
||
if ($isAdmin) {
|
||
// Normalize session so downstream ACL checks see admin
|
||
$_SESSION['isAdmin'] = true;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// ignore and continue to deny
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$isAdmin) {
|
||
http_response_code(403);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => 'Admin privileges required.']);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
/** Enforce CSRF using X-CSRF-Token header (or csrfToken param as fallback). */
|
||
private static function requireCsrf(): void
|
||
{
|
||
$h = self::headersLower();
|
||
$token = trim($h['x-csrf-token'] ?? ($_POST['csrfToken'] ?? ''));
|
||
if (empty($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
|
||
http_response_code(403);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => 'Invalid CSRF token']);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
/** Read JSON body (empty array if not valid). */
|
||
private static function readJson(): array
|
||
{
|
||
$raw = file_get_contents('php://input');
|
||
$data = json_decode($raw, true);
|
||
return is_array($data) ? $data : [];
|
||
}
|
||
|
||
/** Convenience: set JSON content type + no-store. */
|
||
private static function jsonHeaders(): void
|
||
{
|
||
header('Content-Type: application/json');
|
||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||
header('Pragma: no-cache');
|
||
}
|
||
|
||
/* ------------------------- End helpers -------------------------- */
|
||
|
||
public function getUsers()
|
||
{
|
||
self::jsonHeaders();
|
||
self::requireAdmin();
|
||
|
||
// Retrieve users using the model
|
||
$users = UserModel::getAllUsers();
|
||
echo json_encode($users);
|
||
exit;
|
||
}
|
||
|
||
public function addUser()
|
||
{
|
||
self::jsonHeaders();
|
||
self::requireMethod(['POST']);
|
||
|
||
// Initialize CSRF token if missing (useful for initial page load)
|
||
if (empty($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
}
|
||
|
||
// Setup mode detection (first-run bootstrap)
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
$isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
|
||
$setupMode = false;
|
||
if (
|
||
$isSetup && (!file_exists($usersFile)
|
||
|| filesize($usersFile) === 0
|
||
|| trim(@file_get_contents($usersFile)) === ''
|
||
)
|
||
) {
|
||
$setupMode = true;
|
||
} else {
|
||
// Not setup: enforce CSRF + admin auth
|
||
$h = self::headersLower();
|
||
$receivedToken = trim($h['x-csrf-token'] ?? '');
|
||
|
||
// Soft-fail CSRF: on mismatch, regenerate and return new token (preserve your current UX)
|
||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
header('X-CSRF-Token: ' . $_SESSION['csrf_token']);
|
||
echo json_encode([
|
||
'csrf_expired' => true,
|
||
'csrf_token' => $_SESSION['csrf_token']
|
||
]);
|
||
exit;
|
||
}
|
||
|
||
self::requireAdmin();
|
||
}
|
||
|
||
$data = self::readJson();
|
||
$newUsername = trim($data['username'] ?? '');
|
||
$newPassword = trim($data['password'] ?? '');
|
||
|
||
$isAdmin = $setupMode ? '1' : (!empty($data['isAdmin']) ? '1' : '0');
|
||
|
||
if ($newUsername === '' || $newPassword === '') {
|
||
echo json_encode(["error" => "Username and password required"]);
|
||
exit;
|
||
}
|
||
if (!preg_match(REGEX_USER, $newUsername)) {
|
||
echo json_encode([
|
||
"error" => "Invalid username. Only letters, numbers, underscores, dashes, and spaces are allowed."
|
||
]);
|
||
exit;
|
||
}
|
||
// Keep password rules lenient to avoid breaking existing flows; enforce at least 6 chars
|
||
if (strlen($newPassword) < 6) {
|
||
echo json_encode(["error" => "Password must be at least 6 characters."]);
|
||
exit;
|
||
}
|
||
|
||
$result = UserModel::addUser($newUsername, $newPassword, $isAdmin, $setupMode);
|
||
echo json_encode($result);
|
||
exit;
|
||
}
|
||
|
||
public function removeUser()
|
||
{
|
||
self::jsonHeaders();
|
||
// Accept DELETE or POST for broader compatibility
|
||
self::requireMethod(['DELETE', 'POST']);
|
||
self::requireAdmin();
|
||
self::requireCsrf();
|
||
|
||
$data = self::readJson();
|
||
$usernameToRemove = trim($data['username'] ?? '');
|
||
|
||
if ($usernameToRemove === '') {
|
||
echo json_encode(["error" => "Username is required"]);
|
||
exit;
|
||
}
|
||
if (!preg_match(REGEX_USER, $usernameToRemove)) {
|
||
echo json_encode(["error" => "Invalid username format"]);
|
||
exit;
|
||
}
|
||
if (!empty($_SESSION['username']) && $_SESSION['username'] === $usernameToRemove) {
|
||
echo json_encode(["error" => "Cannot remove yourself"]);
|
||
exit;
|
||
}
|
||
|
||
$result = UserModel::removeUser($usernameToRemove);
|
||
echo json_encode($result);
|
||
exit;
|
||
}
|
||
|
||
public function getUserPermissions()
|
||
{
|
||
self::jsonHeaders();
|
||
self::requireAuth();
|
||
|
||
$permissions = UserModel::getUserPermissions();
|
||
echo json_encode($permissions);
|
||
exit;
|
||
}
|
||
|
||
public function updateUserPermissions()
|
||
{
|
||
self::jsonHeaders();
|
||
// Accept PUT or POST for compatibility with clients that can't send PUT
|
||
self::requireMethod(['PUT', 'POST']);
|
||
self::requireAdmin();
|
||
self::requireCsrf();
|
||
|
||
$input = self::readJson();
|
||
if (!isset($input['permissions']) || !is_array($input['permissions'])) {
|
||
echo json_encode(["error" => "Invalid input"]);
|
||
exit;
|
||
}
|
||
$permissions = $input['permissions'];
|
||
|
||
$result = UserModel::updateUserPermissions($permissions);
|
||
echo json_encode($result);
|
||
exit;
|
||
}
|
||
|
||
public function changePassword()
|
||
{
|
||
self::jsonHeaders();
|
||
self::requireMethod(['POST']);
|
||
self::requireAuth();
|
||
self::requireCsrf();
|
||
|
||
$username = $_SESSION['username'] ?? '';
|
||
if ($username === '') {
|
||
echo json_encode(["error" => "No username in session"]);
|
||
exit;
|
||
}
|
||
|
||
$data = self::readJson();
|
||
$oldPassword = trim($data["oldPassword"] ?? "");
|
||
$newPassword = trim($data["newPassword"] ?? "");
|
||
$confirmPassword = trim($data["confirmPassword"] ?? "");
|
||
|
||
if ($oldPassword === '' || $newPassword === '' || $confirmPassword === '') {
|
||
echo json_encode(["error" => "All fields are required."]);
|
||
exit;
|
||
}
|
||
if ($newPassword !== $confirmPassword) {
|
||
echo json_encode(["error" => "New passwords do not match."]);
|
||
exit;
|
||
}
|
||
if (strlen($newPassword) < 6) {
|
||
echo json_encode(["error" => "Password must be at least 6 characters."]);
|
||
exit;
|
||
}
|
||
|
||
$result = UserModel::changePassword($username, $oldPassword, $newPassword);
|
||
echo json_encode($result);
|
||
exit;
|
||
}
|
||
|
||
public function updateUserPanel()
|
||
{
|
||
self::jsonHeaders();
|
||
// Accept PUT or POST for compatibility
|
||
self::requireMethod(['PUT', 'POST']);
|
||
self::requireAuth();
|
||
self::requireCsrf();
|
||
|
||
$data = self::readJson();
|
||
if (!is_array($data)) {
|
||
http_response_code(400);
|
||
echo json_encode(["error" => "Invalid input"]);
|
||
exit;
|
||
}
|
||
|
||
$username = $_SESSION['username'] ?? '';
|
||
if ($username === '') {
|
||
http_response_code(400);
|
||
echo json_encode(["error" => "No username in session"]);
|
||
exit;
|
||
}
|
||
|
||
$totp_enabled = isset($data['totp_enabled']) ? filter_var($data['totp_enabled'], FILTER_VALIDATE_BOOLEAN) : false;
|
||
$result = UserModel::updateUserPanel($username, $totp_enabled);
|
||
echo json_encode($result);
|
||
exit;
|
||
}
|
||
|
||
public function disableTOTP()
|
||
{
|
||
self::jsonHeaders();
|
||
// Accept PUT or POST
|
||
self::requireMethod(['PUT', 'POST']);
|
||
self::requireAuth();
|
||
self::requireCsrf();
|
||
|
||
$username = $_SESSION['username'] ?? '';
|
||
if ($username === '') {
|
||
http_response_code(400);
|
||
echo json_encode(["error" => "Username not found in session"]);
|
||
exit;
|
||
}
|
||
|
||
$result = UserModel::disableTOTPSecret($username);
|
||
if ($result) {
|
||
echo json_encode(["success" => true, "message" => "TOTP disabled successfully."]);
|
||
} else {
|
||
http_response_code(500);
|
||
echo json_encode(["error" => "Failed to disable TOTP."]);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
public function recoverTOTP()
|
||
{
|
||
self::jsonHeaders();
|
||
self::requireMethod(['POST']);
|
||
self::requireCsrf();
|
||
|
||
$userId = $_SESSION['username'] ?? ($_SESSION['pending_login_user'] ?? null);
|
||
if (!$userId) {
|
||
http_response_code(401);
|
||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||
exit;
|
||
}
|
||
if (!preg_match(REGEX_USER, $userId)) {
|
||
http_response_code(400);
|
||
echo json_encode(['status' => 'error', 'message' => 'Invalid user identifier']);
|
||
exit;
|
||
}
|
||
|
||
$inputData = self::readJson();
|
||
$recoveryCode = $inputData['recovery_code'] ?? '';
|
||
|
||
$result = UserModel::recoverTOTP($userId, $recoveryCode);
|
||
|
||
if (($result['status'] ?? '') === 'ok') {
|
||
// Finalize login
|
||
session_regenerate_id(true);
|
||
$_SESSION['authenticated'] = true;
|
||
$_SESSION['username'] = $userId;
|
||
unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']);
|
||
echo json_encode(['status' => 'ok']);
|
||
} else {
|
||
if (($result['message'] ?? '') === 'Too many attempts. Try again later.') {
|
||
http_response_code(429);
|
||
} else {
|
||
http_response_code(400);
|
||
}
|
||
echo json_encode($result);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
public function saveTOTPRecoveryCode()
|
||
{
|
||
self::jsonHeaders();
|
||
self::requireMethod(['POST']);
|
||
self::requireCsrf();
|
||
|
||
if (empty($_SESSION['username'])) {
|
||
http_response_code(401);
|
||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||
exit;
|
||
}
|
||
|
||
$userId = $_SESSION['username'];
|
||
if (!preg_match(REGEX_USER, $userId)) {
|
||
http_response_code(400);
|
||
echo json_encode(['status' => 'error', 'message' => 'Invalid user identifier']);
|
||
exit;
|
||
}
|
||
|
||
$result = UserModel::saveTOTPRecoveryCode($userId);
|
||
if (($result['status'] ?? '') === 'ok') {
|
||
echo json_encode($result);
|
||
} else {
|
||
http_response_code(500);
|
||
echo json_encode($result);
|
||
}
|
||
exit;
|
||
}
|
||
|
||
public function setupTOTP()
|
||
{
|
||
// Allow access if authenticated OR pending TOTP
|
||
if (!( (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']) )) {
|
||
http_response_code(403);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(["error" => "Not authorized to access TOTP setup"]);
|
||
exit;
|
||
}
|
||
|
||
self::requireCsrf();
|
||
|
||
// Fix: if username not present (pending flow), fall back to pending_login_user
|
||
$username = $_SESSION['username'] ?? ($_SESSION['pending_login_user'] ?? '');
|
||
if ($username === '') {
|
||
http_response_code(400);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => 'Username not available for TOTP setup']);
|
||
exit;
|
||
}
|
||
|
||
header("Content-Type: image/png");
|
||
header('X-Content-Type-Options: nosniff');
|
||
|
||
$result = UserModel::setupTOTP($username);
|
||
if (isset($result['error'])) {
|
||
http_response_code(500);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(["error" => $result['error']]);
|
||
exit;
|
||
}
|
||
|
||
echo $result['imageData'];
|
||
exit;
|
||
}
|
||
|
||
public function verifyTOTP()
|
||
{
|
||
header('Content-Type: application/json');
|
||
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
|
||
header('X-Content-Type-Options: nosniff');
|
||
|
||
// 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
|
||
self::requireCsrf();
|
||
|
||
// Parse & validate input
|
||
$inputData = self::readJson();
|
||
$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
|
||
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) ?: [];
|
||
}
|
||
$perms = loadUserPermissions($username);
|
||
$all[$token] = [
|
||
'username' => $username,
|
||
'expiry' => $expiry,
|
||
'isAdmin' => ((int)UserModel::getUserRole($username) === 1),
|
||
'folderOnly' => $perms['folderOnly'] ?? false,
|
||
'readOnly' => $perms['readOnly'] ?? false,
|
||
'disableUpload' => $perms['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
|
||
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;
|
||
|
||
unset(
|
||
$_SESSION['pending_login_user'],
|
||
$_SESSION['pending_login_secret'],
|
||
$_SESSION['pending_login_remember_me'],
|
||
$_SESSION['totp_failures']
|
||
);
|
||
|
||
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'] ?? '';
|
||
if ($username === '') {
|
||
http_response_code(400);
|
||
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']);
|
||
exit;
|
||
}
|
||
|
||
$totpSecret = UserModel::getTOTPSecret($username);
|
||
if (!$totpSecret) {
|
||
http_response_code(500);
|
||
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
|
||
exit;
|
||
}
|
||
|
||
if (!$tfa->verifyCode($totpSecret, $code)) {
|
||
$_SESSION['totp_failures']++;
|
||
http_response_code(400);
|
||
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
|
||
exit;
|
||
}
|
||
|
||
unset($_SESSION['totp_failures']);
|
||
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Upload profile picture (multipart/form-data)
|
||
*/
|
||
public function uploadPicture()
|
||
{
|
||
self::jsonHeaders();
|
||
|
||
// Auth & CSRF
|
||
self::requireAuth();
|
||
self::requireCsrf();
|
||
|
||
if (empty($_FILES['profile_picture']) || $_FILES['profile_picture']['error'] !== UPLOAD_ERR_OK) {
|
||
http_response_code(400);
|
||
echo json_encode(['success' => false, 'error' => 'No file uploaded or error']);
|
||
exit;
|
||
}
|
||
$file = $_FILES['profile_picture'];
|
||
|
||
// Validate MIME & size
|
||
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'];
|
||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||
$mime = finfo_file($finfo, $file['tmp_name']);
|
||
finfo_close($finfo);
|
||
if (!isset($allowed[$mime])) {
|
||
http_response_code(400);
|
||
echo json_encode(['success' => false, 'error' => 'Invalid file type']);
|
||
exit;
|
||
}
|
||
if ($file['size'] > 2 * 1024 * 1024) {
|
||
http_response_code(400);
|
||
echo json_encode(['success' => false, 'error' => 'File too large']);
|
||
exit;
|
||
}
|
||
|
||
// Destination
|
||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . '/profile_pics';
|
||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true)) {
|
||
http_response_code(500);
|
||
echo json_encode(['success' => false, 'error' => 'Cannot create upload folder']);
|
||
exit;
|
||
}
|
||
|
||
$ext = $allowed[$mime];
|
||
$user = preg_replace('/[^a-zA-Z0-9_\-]/', '', $_SESSION['username']);
|
||
$filename = $user . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
|
||
$dest = $uploadDir . '/' . $filename;
|
||
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||
http_response_code(500);
|
||
echo json_encode(['success' => false, 'error' => 'Failed to save file']);
|
||
exit;
|
||
}
|
||
|
||
// Assuming /uploads maps to UPLOAD_DIR publicly
|
||
$url = '/uploads/profile_pics/' . $filename;
|
||
|
||
$result = UserModel::setProfilePicture($_SESSION['username'], $url);
|
||
if (!($result['success'] ?? false)) {
|
||
@unlink($dest);
|
||
http_response_code(500);
|
||
echo json_encode([
|
||
'success' => false,
|
||
'error' => 'Failed to save profile picture setting'
|
||
]);
|
||
exit;
|
||
}
|
||
|
||
echo json_encode(['success' => true, 'url' => $url]);
|
||
exit;
|
||
}
|
||
|
||
public function siteConfig(): void
|
||
{
|
||
header('Content-Type: application/json');
|
||
|
||
$usersDir = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR;
|
||
$publicPath = $usersDir . 'siteConfig.json';
|
||
$adminEncPath = $usersDir . 'adminConfig.json';
|
||
|
||
$publicMtime = is_file($publicPath) ? (int)@filemtime($publicPath) : 0;
|
||
$adminMtime = is_file($adminEncPath) ? (int)@filemtime($adminEncPath) : 0;
|
||
|
||
// If public cache is present and fresh enough, serve it
|
||
if ($publicMtime > 0 && $publicMtime >= $adminMtime) {
|
||
$raw = @file_get_contents($publicPath);
|
||
$data = is_string($raw) ? json_decode($raw, true) : null;
|
||
if (is_array($data)) {
|
||
echo json_encode($data);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Otherwise regenerate from decrypted admin config
|
||
$cfg = AdminModel::getConfig();
|
||
if (isset($cfg['error'])) {
|
||
http_response_code(500);
|
||
echo json_encode(['error' => $cfg['error']]);
|
||
return;
|
||
}
|
||
|
||
$public = AdminModel::buildPublicSubset($cfg);
|
||
$w = AdminModel::writeSiteConfig($public); // best effort
|
||
echo json_encode($public);
|
||
}
|
||
}
|