731 lines
24 KiB
PHP
731 lines
24 KiB
PHP
<?php
|
||
// src/models/userModel.php
|
||
|
||
require_once PROJECT_ROOT . '/config/config.php';
|
||
|
||
class userModel
|
||
{
|
||
/**
|
||
* Retrieve all users (username + role).
|
||
*/
|
||
public static function getAllUsers()
|
||
{
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
$users = [];
|
||
if (file_exists($usersFile)) {
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) >= 3 && preg_match(REGEX_USER, $parts[0])) {
|
||
$users[] = [
|
||
"username" => $parts[0],
|
||
"role" => trim($parts[2])
|
||
];
|
||
}
|
||
}
|
||
}
|
||
return $users;
|
||
}
|
||
|
||
/**
|
||
* Add a user.
|
||
*
|
||
* @param string $username
|
||
* @param string $password
|
||
* @param string $isAdmin "1" or "0"
|
||
* @param bool $setupMode overwrite file if true
|
||
*/
|
||
public static function addUser($username, $password, $isAdmin, $setupMode)
|
||
{
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
|
||
// Defense in depth
|
||
if (!preg_match(REGEX_USER, $username)) {
|
||
return ["error" => "Invalid username"];
|
||
}
|
||
if (!is_string($password) || $password === '') {
|
||
return ["error" => "Password required"];
|
||
}
|
||
$isAdmin = $isAdmin === '1' ? '1' : '0';
|
||
|
||
if (!file_exists($usersFile)) {
|
||
@file_put_contents($usersFile, '', LOCK_EX);
|
||
}
|
||
|
||
// Check duplicates
|
||
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
foreach ($existingUsers as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (isset($parts[0]) && $username === $parts[0]) {
|
||
return ["error" => "User already exists"];
|
||
}
|
||
}
|
||
|
||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||
$newUserLine = $username . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
|
||
|
||
if ($setupMode) {
|
||
if (file_put_contents($usersFile, $newUserLine, LOCK_EX) === false) {
|
||
return ["error" => "Failed to write users file"];
|
||
}
|
||
} else {
|
||
if (file_put_contents($usersFile, $newUserLine, FILE_APPEND | LOCK_EX) === false) {
|
||
return ["error" => "Failed to write users file"];
|
||
}
|
||
}
|
||
|
||
return ["success" => "User added successfully"];
|
||
}
|
||
|
||
/**
|
||
* Remove a user and update encrypted userPermissions.json.
|
||
*/
|
||
public static function removeUser($usernameToRemove)
|
||
{
|
||
global $encryptionKey;
|
||
|
||
if (!preg_match(REGEX_USER, $usernameToRemove)) {
|
||
return ["error" => "Invalid username"];
|
||
}
|
||
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return ["error" => "Users file not found"];
|
||
}
|
||
|
||
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
$newUsers = [];
|
||
$userFound = false;
|
||
|
||
foreach ($existingUsers as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) < 3) {
|
||
continue;
|
||
}
|
||
if (strcasecmp($parts[0], $usernameToRemove) === 0) {
|
||
$userFound = true;
|
||
continue; // skip this user
|
||
}
|
||
$newUsers[] = $line;
|
||
}
|
||
|
||
if (!$userFound) {
|
||
return ["error" => "User not found"];
|
||
}
|
||
|
||
$newContent = $newUsers ? (implode(PHP_EOL, $newUsers) . PHP_EOL) : '';
|
||
if (file_put_contents($usersFile, $newContent, LOCK_EX) === false) {
|
||
return ["error" => "Failed to update users file"];
|
||
}
|
||
|
||
// Update encrypted userPermissions.json — remove any key matching case-insensitively
|
||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||
if (file_exists($permissionsFile)) {
|
||
$raw = file_get_contents($permissionsFile);
|
||
$decrypted = decryptData($raw, $encryptionKey);
|
||
$permissionsArray = $decrypted !== false
|
||
? json_decode($decrypted, true)
|
||
: (json_decode($raw, true) ?: []); // tolerate legacy plaintext
|
||
|
||
if (is_array($permissionsArray)) {
|
||
foreach (array_keys($permissionsArray) as $k) {
|
||
if (strcasecmp($k, $usernameToRemove) === 0) {
|
||
unset($permissionsArray[$k]);
|
||
}
|
||
}
|
||
$plain = json_encode($permissionsArray, JSON_PRETTY_PRINT);
|
||
$enc = encryptData($plain, $encryptionKey);
|
||
file_put_contents($permissionsFile, $enc, LOCK_EX);
|
||
}
|
||
}
|
||
|
||
// Purge from ACL (remove from every bucket in every folder)
|
||
require_once PROJECT_ROOT . '/src/lib/ACL.php';
|
||
if (method_exists('ACL', 'purgeUser')) {
|
||
ACL::purgeUser($usernameToRemove);
|
||
} else {
|
||
// Fallback inline purge if you haven't added ACL::purgeUser yet:
|
||
$aclPath = rtrim(META_DIR, '/\\') . DIRECTORY_SEPARATOR . 'folder_acl.json';
|
||
$acl = is_file($aclPath) ? json_decode((string)file_get_contents($aclPath), true) : [];
|
||
if (!is_array($acl)) $acl = ['folders' => [], 'groups' => []];
|
||
$buckets = ['owners','read','write','share','read_own'];
|
||
|
||
$changed = false;
|
||
foreach ($acl['folders'] ?? [] as $f => &$rec) {
|
||
foreach ($buckets as $b) {
|
||
if (!isset($rec[$b]) || !is_array($rec[$b])) { $rec[$b] = []; continue; }
|
||
$before = $rec[$b];
|
||
$rec[$b] = array_values(array_filter($rec[$b], fn($u) => strcasecmp((string)$u, $usernameToRemove) !== 0));
|
||
if ($rec[$b] !== $before) $changed = true;
|
||
}
|
||
}
|
||
unset($rec);
|
||
|
||
if ($changed) {
|
||
@file_put_contents($aclPath, json_encode($acl, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
||
}
|
||
}
|
||
|
||
return ["success" => "User removed successfully"];
|
||
}
|
||
|
||
/**
|
||
* Get permissions for current user (or all, if admin).
|
||
*/
|
||
public static function getUserPermissions()
|
||
{
|
||
global $encryptionKey;
|
||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||
$permissionsArray = [];
|
||
|
||
if (file_exists($permissionsFile)) {
|
||
$content = file_get_contents($permissionsFile);
|
||
$decrypted = decryptData($content, $encryptionKey);
|
||
if ($decrypted === false) {
|
||
// tolerate legacy plaintext
|
||
$permissionsArray = json_decode($content, true);
|
||
} else {
|
||
$permissionsArray = json_decode($decrypted, true);
|
||
}
|
||
if (!is_array($permissionsArray)) {
|
||
$permissionsArray = [];
|
||
}
|
||
}
|
||
|
||
if (!empty($_SESSION['isAdmin'])) {
|
||
return $permissionsArray;
|
||
}
|
||
|
||
$username = $_SESSION['username'] ?? '';
|
||
foreach ($permissionsArray as $storedUsername => $data) {
|
||
if (strcasecmp($storedUsername, $username) === 0) {
|
||
return $data;
|
||
}
|
||
}
|
||
|
||
return new stdClass();
|
||
}
|
||
|
||
/**
|
||
* Update permissions (encrypted on disk). Skips admins.
|
||
*/
|
||
public static function updateUserPermissions($permissions)
|
||
{
|
||
global $encryptionKey;
|
||
$permissionsFile = USERS_DIR . "userPermissions.json";
|
||
$existingPermissions = [];
|
||
|
||
// Load existing (decrypt if needed)
|
||
if (file_exists($permissionsFile)) {
|
||
$encryptedContent = file_get_contents($permissionsFile);
|
||
$json = decryptData($encryptedContent, $encryptionKey);
|
||
if ($json === false) $json = $encryptedContent; // legacy plaintext
|
||
$existingPermissions = json_decode($json, true) ?: [];
|
||
}
|
||
|
||
// Load roles to skip admins
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
$userRoles = [];
|
||
if (file_exists($usersFile)) {
|
||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) >= 3 && preg_match(REGEX_USER, $parts[0])) {
|
||
$userRoles[strtolower($parts[0])] = trim($parts[2]);
|
||
}
|
||
}
|
||
}
|
||
|
||
$knownKeys = [
|
||
'folderOnly','readOnly','disableUpload',
|
||
'bypassOwnership','canShare','canZip','viewOwnOnly'
|
||
];
|
||
|
||
// Build a map of lowercase->actual key to update existing entries case-insensitively
|
||
$lcIndex = [];
|
||
foreach ($existingPermissions as $k => $_) {
|
||
$lcIndex[strtolower($k)] = $k;
|
||
}
|
||
|
||
foreach ($permissions as $perm) {
|
||
if (empty($perm['username'])) continue;
|
||
|
||
$unameOrig = (string)$perm['username']; // preserve original case
|
||
$unameLc = strtolower($unameOrig);
|
||
$role = $userRoles[$unameLc] ?? null;
|
||
if ($role === "1") continue; // skip admins
|
||
|
||
// Find existing key case-insensitively; otherwise use original case as canonical
|
||
$storeKey = $lcIndex[$unameLc] ?? $unameOrig;
|
||
|
||
$current = $existingPermissions[$storeKey] ?? [];
|
||
foreach ($knownKeys as $k) {
|
||
if (array_key_exists($k, $perm)) {
|
||
$current[$k] = (bool)$perm[$k];
|
||
} elseif (!isset($current[$k])) {
|
||
$current[$k] = false;
|
||
}
|
||
}
|
||
|
||
$existingPermissions[$storeKey] = $current;
|
||
$lcIndex[$unameLc] = $storeKey; // keep index up to date
|
||
}
|
||
|
||
$plain = json_encode($existingPermissions, JSON_PRETTY_PRINT);
|
||
$encrypted = encryptData($plain, $encryptionKey);
|
||
if (file_put_contents($permissionsFile, $encrypted) === false) {
|
||
return ["error" => "Failed to save user permissions."];
|
||
}
|
||
return ["success" => "User permissions updated successfully."];
|
||
}
|
||
|
||
/**
|
||
* Change password (preserve TOTP + extra fields).
|
||
*/
|
||
public static function changePassword($username, $oldPassword, $newPassword)
|
||
{
|
||
if (!preg_match(REGEX_USER, $username)) {
|
||
return ["error" => "Invalid username"];
|
||
}
|
||
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return ["error" => "Users file not found"];
|
||
}
|
||
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
$userFound = false;
|
||
$newLines = [];
|
||
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) < 3) {
|
||
$newLines[] = $line;
|
||
continue;
|
||
}
|
||
$storedUser = $parts[0];
|
||
$storedHash = $parts[1];
|
||
|
||
if ($storedUser === $username) {
|
||
$userFound = true;
|
||
if (!password_verify($oldPassword, $storedHash)) {
|
||
return ["error" => "Old password is incorrect."];
|
||
}
|
||
$parts[1] = password_hash($newPassword, PASSWORD_BCRYPT);
|
||
$newLines[] = implode(':', $parts);
|
||
} else {
|
||
$newLines[] = $line;
|
||
}
|
||
}
|
||
|
||
if (!$userFound) {
|
||
return ["error" => "User not found."];
|
||
}
|
||
|
||
$payload = implode(PHP_EOL, $newLines) . PHP_EOL;
|
||
if (file_put_contents($usersFile, $payload, LOCK_EX) === false) {
|
||
return ["error" => "Could not update password."];
|
||
}
|
||
|
||
return ["success" => "Password updated successfully."];
|
||
}
|
||
|
||
/**
|
||
* Update panel: if TOTP disabled, clear secret.
|
||
*/
|
||
public static function updateUserPanel($username, $totp_enabled)
|
||
{
|
||
if (!preg_match(REGEX_USER, $username)) {
|
||
return ["error" => "Invalid username"];
|
||
}
|
||
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return ["error" => "Users file not found"];
|
||
}
|
||
|
||
if (!$totp_enabled) {
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
$newLines = [];
|
||
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) < 3) {
|
||
$newLines[] = $line;
|
||
continue;
|
||
}
|
||
if ($parts[0] === $username) {
|
||
while (count($parts) < 4) {
|
||
$parts[] = "";
|
||
}
|
||
$parts[3] = "";
|
||
$newLines[] = implode(':', $parts);
|
||
} else {
|
||
$newLines[] = $line;
|
||
}
|
||
}
|
||
|
||
if (file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX) === false) {
|
||
return ["error" => "Failed to disable TOTP secret"];
|
||
}
|
||
return ["success" => "User panel updated: TOTP disabled"];
|
||
}
|
||
|
||
return ["success" => "User panel updated: TOTP remains enabled"];
|
||
}
|
||
|
||
/**
|
||
* Clear TOTP secret.
|
||
*/
|
||
public static function disableTOTPSecret($username)
|
||
{
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return false;
|
||
}
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
$modified = false;
|
||
$newLines = [];
|
||
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) < 3) {
|
||
$newLines[] = $line;
|
||
continue;
|
||
}
|
||
if ($parts[0] === $username) {
|
||
while (count($parts) < 4) {
|
||
$parts[] = "";
|
||
}
|
||
$parts[3] = "";
|
||
$modified = true;
|
||
$newLines[] = implode(":", $parts);
|
||
} else {
|
||
$newLines[] = $line;
|
||
}
|
||
}
|
||
|
||
if ($modified) {
|
||
return file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX) !== false;
|
||
}
|
||
return $modified;
|
||
}
|
||
|
||
/**
|
||
* Recover via recovery code.
|
||
*/
|
||
public static function recoverTOTP($userId, $recoveryCode)
|
||
{
|
||
// Rate limit storage
|
||
$attemptsFile = rtrim(USERS_DIR, '/\\') . '/recovery_attempts.json';
|
||
$attempts = is_file($attemptsFile) ? (json_decode(@file_get_contents($attemptsFile), true) ?: []) : [];
|
||
$key = ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0') . '|' . $userId;
|
||
$now = time();
|
||
|
||
if (isset($attempts[$key])) {
|
||
$attempts[$key] = array_values(array_filter($attempts[$key], fn($ts) => $ts > $now - 900));
|
||
}
|
||
if (count($attempts[$key] ?? []) >= 5) {
|
||
return ['status' => 'error', 'message' => 'Too many attempts. Try again later.'];
|
||
}
|
||
|
||
// User JSON file
|
||
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
|
||
if (!file_exists($userFile)) {
|
||
return ['status' => 'error', 'message' => 'User not found'];
|
||
}
|
||
|
||
$fp = fopen($userFile, 'c+');
|
||
if (!$fp || !flock($fp, LOCK_EX)) {
|
||
if ($fp) fclose($fp);
|
||
return ['status' => 'error', 'message' => 'Server error'];
|
||
}
|
||
|
||
$fileContents = stream_get_contents($fp);
|
||
$data = json_decode($fileContents, true) ?: [];
|
||
|
||
if (empty($recoveryCode)) {
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
return ['status' => 'error', 'message' => 'Recovery code required'];
|
||
}
|
||
|
||
$storedHash = $data['totp_recovery_code'] ?? null;
|
||
if (!$storedHash || !password_verify($recoveryCode, $storedHash)) {
|
||
// record failed attempt
|
||
$attempts[$key][] = $now;
|
||
@file_put_contents($attemptsFile, json_encode($attempts, JSON_PRETTY_PRINT), LOCK_EX);
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
return ['status' => 'error', 'message' => 'Invalid recovery code'];
|
||
}
|
||
|
||
// Invalidate code
|
||
$data['totp_recovery_code'] = null;
|
||
rewind($fp);
|
||
ftruncate($fp, 0);
|
||
fwrite($fp, json_encode($data, JSON_PRETTY_PRINT));
|
||
fflush($fp);
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
|
||
return ['status' => 'ok'];
|
||
}
|
||
|
||
/**
|
||
* Generate random recovery code.
|
||
*/
|
||
private static function generateRecoveryCode($length = 12)
|
||
{
|
||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||
$max = strlen($chars) - 1;
|
||
$code = '';
|
||
for ($i = 0; $i < $length; $i++) {
|
||
$code .= $chars[random_int(0, $max)];
|
||
}
|
||
return $code;
|
||
}
|
||
|
||
/**
|
||
* Save new TOTP recovery code (hash on disk) and return plaintext to caller.
|
||
*/
|
||
public static function saveTOTPRecoveryCode($userId)
|
||
{
|
||
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
|
||
|
||
if (!file_exists($userFile)) {
|
||
if (file_put_contents($userFile, json_encode([], JSON_PRETTY_PRINT), LOCK_EX) === false) {
|
||
return ['status' => 'error', 'message' => 'Server error: could not create user file'];
|
||
}
|
||
}
|
||
|
||
$recoveryCode = self::generateRecoveryCode();
|
||
$recoveryHash = password_hash($recoveryCode, PASSWORD_DEFAULT);
|
||
|
||
$fp = fopen($userFile, 'c+');
|
||
if (!$fp || !flock($fp, LOCK_EX)) {
|
||
if ($fp) fclose($fp);
|
||
return ['status' => 'error', 'message' => 'Server error: could not lock user file'];
|
||
}
|
||
|
||
$contents = stream_get_contents($fp);
|
||
$data = json_decode($contents, true) ?: [];
|
||
$data['totp_recovery_code'] = $recoveryHash;
|
||
|
||
rewind($fp);
|
||
ftruncate($fp, 0);
|
||
fwrite($fp, json_encode($data, JSON_PRETTY_PRINT));
|
||
fflush($fp);
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
|
||
return ['status' => 'ok', 'recoveryCode' => $recoveryCode];
|
||
}
|
||
|
||
/**
|
||
* Setup TOTP & build QR PNG.
|
||
*/
|
||
public static function setupTOTP($username)
|
||
{
|
||
global $encryptionKey;
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
|
||
if (!file_exists($usersFile)) {
|
||
return ['error' => 'Users file not found'];
|
||
}
|
||
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
$totpSecret = null;
|
||
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
||
$totpSecret = decryptData($parts[3], $encryptionKey);
|
||
break;
|
||
}
|
||
}
|
||
|
||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
||
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
|
||
'FileRise',
|
||
6,
|
||
30,
|
||
\RobThree\Auth\Algorithm::Sha1
|
||
);
|
||
|
||
if (!$totpSecret) {
|
||
$totpSecret = $tfa->createSecret();
|
||
$encryptedSecret = encryptData($totpSecret, $encryptionKey);
|
||
|
||
$newLines = [];
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||
if (count($parts) >= 4) {
|
||
$parts[3] = $encryptedSecret;
|
||
} else {
|
||
$parts[] = $encryptedSecret;
|
||
}
|
||
$newLines[] = implode(':', $parts);
|
||
} else {
|
||
$newLines[] = $line;
|
||
}
|
||
}
|
||
file_put_contents($usersFile, implode(PHP_EOL, $newLines) . PHP_EOL, LOCK_EX);
|
||
}
|
||
|
||
// Prefer admin-configured otpauth template if present
|
||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||
$globalOtpauthUrl = "";
|
||
if (file_exists($adminConfigFile)) {
|
||
$encryptedContent = file_get_contents($adminConfigFile);
|
||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||
if ($decryptedContent !== false) {
|
||
$config = json_decode($decryptedContent, true);
|
||
if (!empty($config['globalOtpauthUrl'])) {
|
||
$globalOtpauthUrl = $config['globalOtpauthUrl'];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($globalOtpauthUrl)) {
|
||
$label = "FileRise:" . $username;
|
||
$otpauthUrl = str_replace(
|
||
["{label}", "{secret}"],
|
||
[urlencode($label), $totpSecret],
|
||
$globalOtpauthUrl
|
||
);
|
||
} else {
|
||
$label = urlencode("FileRise:" . $username);
|
||
$issuer = urlencode("FileRise");
|
||
$otpauthUrl = "otpauth://totp/{$label}?secret={$totpSecret}&issuer={$issuer}";
|
||
}
|
||
|
||
$result = \Endroid\QrCode\Builder\Builder::create()
|
||
->writer(new \Endroid\QrCode\Writer\PngWriter())
|
||
->data($otpauthUrl)
|
||
->build();
|
||
|
||
return [
|
||
'imageData' => $result->getString(),
|
||
'mimeType' => $result->getMimeType()
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Get decrypted TOTP secret.
|
||
*/
|
||
public static function getTOTPSecret($username)
|
||
{
|
||
global $encryptionKey;
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return null;
|
||
}
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||
foreach ($lines as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
||
return decryptData($parts[3], $encryptionKey);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Get role ('1' admin, '0' user) or null.
|
||
*/
|
||
public static function getUserRole($username)
|
||
{
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return null;
|
||
}
|
||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||
$parts = explode(':', trim($line));
|
||
if (count($parts) >= 3 && $parts[0] === $username) {
|
||
return trim($parts[2]);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Get a single user’s info (admin flag, TOTP status, profile picture).
|
||
*/
|
||
public static function getUser(string $username): array
|
||
{
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return [];
|
||
}
|
||
|
||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||
$parts = explode(':', $line);
|
||
if ($parts[0] !== $username) {
|
||
continue;
|
||
}
|
||
$isAdmin = (isset($parts[2]) && $parts[2] === '1');
|
||
$totpEnabled = !empty($parts[3]);
|
||
$pic = isset($parts[4]) ? $parts[4] : '';
|
||
|
||
// Normalize to a leading slash (UI expects /uploads/…)
|
||
if ($pic !== '' && $pic[0] !== '/') {
|
||
$pic = '/' . $pic;
|
||
}
|
||
|
||
return [
|
||
'username' => $parts[0],
|
||
'isAdmin' => $isAdmin,
|
||
'totp_enabled' => $totpEnabled,
|
||
'profile_picture' => $pic,
|
||
];
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* Persist profile picture URL as 5th field (keeps TOTP secret intact).
|
||
*
|
||
* users.txt: username:hash:isAdmin:totp_secret:profile_picture
|
||
*/
|
||
public static function setProfilePicture(string $username, string $url): array
|
||
{
|
||
$usersFile = USERS_DIR . USERS_FILE;
|
||
if (!file_exists($usersFile)) {
|
||
return ['success' => false, 'error' => 'Users file not found'];
|
||
}
|
||
|
||
// Ensure leading slash (consistent with controller response)
|
||
$url = '/' . ltrim($url, '/');
|
||
|
||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES) ?: [];
|
||
$out = [];
|
||
$found = false;
|
||
|
||
foreach ($lines as $line) {
|
||
if ($line === '') { $out[] = $line; continue; }
|
||
$parts = explode(':', $line);
|
||
if ($parts[0] === $username) {
|
||
$found = true;
|
||
while (count($parts) < 5) {
|
||
$parts[] = '';
|
||
}
|
||
$parts[4] = $url;
|
||
$line = implode(':', $parts);
|
||
}
|
||
$out[] = $line;
|
||
}
|
||
|
||
if (!$found) {
|
||
return ['success' => false, 'error' => 'User not found'];
|
||
}
|
||
|
||
$newContent = implode(PHP_EOL, $out) . PHP_EOL;
|
||
if (file_put_contents($usersFile, $newContent, LOCK_EX) === false) {
|
||
return ['success' => false, 'error' => 'Failed to write users file'];
|
||
}
|
||
|
||
return ['success' => true];
|
||
}
|
||
} |