Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
201
app/Services/Auth.php
Normal file
201
app/Services/Auth.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Auth
|
||||
{
|
||||
private static ?array $user = null;
|
||||
public static function start(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
return;
|
||||
}
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
session_name('globinours_session');
|
||||
session_set_cookie_params([
|
||||
'httponly' => true,
|
||||
'secure' => self::isSecureRequest(),
|
||||
'samesite' => 'Strict',
|
||||
'path' => '/',
|
||||
]);
|
||||
session_start();
|
||||
$now = time();
|
||||
$idle = max(300, (int) (getenv('GLOBINOURS_SESSION_IDLE') ?: 1800));
|
||||
$absolute = max($idle, (int) (getenv('GLOBINOURS_SESSION_ABSOLUTE') ?: 43200));
|
||||
if (
|
||||
!empty($_SESSION['user_id']) &&
|
||||
((isset($_SESSION['last_activity']) && $now - (int) $_SESSION['last_activity'] > $idle) ||
|
||||
(isset($_SESSION['authenticated_at']) && $now - (int) $_SESSION['authenticated_at'] > $absolute))
|
||||
) {
|
||||
self::logout(false);
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
if (
|
||||
empty($_SESSION['user_id']) &&
|
||||
class_exists(TrustedDeviceService::class) &&
|
||||
($id = TrustedDeviceService::authenticate())
|
||||
) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $id;
|
||||
$_SESSION['authenticated_at'] = $now;
|
||||
}
|
||||
if (!empty($_SESSION['user_id'])) {
|
||||
$_SESSION['last_activity'] = $now;
|
||||
}
|
||||
}
|
||||
public static function countUsers(): int
|
||||
{
|
||||
return (int) DB::pdo()->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
||||
}
|
||||
public static function user(): ?array
|
||||
{
|
||||
if (self::$user !== null) {
|
||||
return self::$user;
|
||||
}
|
||||
$id = (int) ($_SESSION['user_id'] ?? 0);
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
$s = DB::pdo()->prepare(
|
||||
'SELECT id,username,display_name,role,active,last_login_at,animal_list_view,agenda_view FROM users WHERE id=:id AND active=1',
|
||||
);
|
||||
$s->execute([':id' => $id]);
|
||||
$u = $s->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$u) {
|
||||
self::logout(false);
|
||||
return null;
|
||||
}
|
||||
return self::$user = $u;
|
||||
}
|
||||
public static function id(): ?int
|
||||
{
|
||||
return ($u = self::user()) ? (int) $u['id'] : null;
|
||||
}
|
||||
public static function login(
|
||||
string $username,
|
||||
string $password,
|
||||
bool $remember = false,
|
||||
string $deviceName = '',
|
||||
): bool {
|
||||
$db = DB::pdo();
|
||||
$username = trim($username);
|
||||
$ip = mb_substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 64);
|
||||
$db->exec("DELETE FROM login_attempts WHERE attempted_at<datetime('now','-7 days')");
|
||||
$rate = $db->prepare(
|
||||
"SELECT COUNT(*) FROM login_attempts WHERE succeeded=0 AND ip_address=? AND attempted_at>=datetime('now','-15 minutes')",
|
||||
);
|
||||
$rate->execute([$ip ?: null]);
|
||||
if ((int) $rate->fetchColumn() >= 20) {
|
||||
self::recordAttempt($username, $ip, false);
|
||||
return false;
|
||||
}
|
||||
$s = $db->prepare('SELECT * FROM users WHERE lower(username)=lower(:u) LIMIT 1');
|
||||
$s->execute([':u' => $username]);
|
||||
$u = $s->fetch(PDO::FETCH_ASSOC);
|
||||
$valid =
|
||||
$u &&
|
||||
(int) $u['active'] === 1 &&
|
||||
(empty($u['locked_until']) || strtotime($u['locked_until']) <= time()) &&
|
||||
password_verify($password, (string) $u['password_hash']);
|
||||
if (!$u) {
|
||||
password_verify($password, '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.');
|
||||
}
|
||||
if (!$valid) {
|
||||
if ($u) {
|
||||
$failed = (int) $u['failed_attempts'] + 1;
|
||||
$lock = $failed >= 5 ? date('Y-m-d H:i:s', time() + 900) : null;
|
||||
$db->prepare('UPDATE users SET failed_attempts=:f,locked_until=:l WHERE id=:id')->execute([
|
||||
':f' => $failed,
|
||||
':l' => $lock,
|
||||
':id' => $u['id'],
|
||||
]);
|
||||
}
|
||||
self::recordAttempt($username, $ip, false);
|
||||
return false;
|
||||
}
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = (int) $u['id'];
|
||||
$_SESSION['authenticated_at'] = time();
|
||||
$_SESSION['last_activity'] = time();
|
||||
self::$user = null;
|
||||
$db->prepare(
|
||||
"UPDATE users SET failed_attempts=0,locked_until=NULL,last_login_at=datetime('now') WHERE id=:id",
|
||||
)->execute([':id' => $u['id']]);
|
||||
self::recordAttempt($username, $ip, true);
|
||||
if ($remember && class_exists(TrustedDeviceService::class)) {
|
||||
TrustedDeviceService::create((int) $u['id'], $deviceName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public static function logout(bool $destroy = true): void
|
||||
{
|
||||
if ($destroy && class_exists(TrustedDeviceService::class)) {
|
||||
TrustedDeviceService::revokeCurrent();
|
||||
}
|
||||
self::$user = null;
|
||||
unset($_SESSION['user_id']);
|
||||
if ($destroy) {
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$p['path'],
|
||||
$p['domain'] ?? '',
|
||||
(bool) $p['secure'],
|
||||
(bool) $p['httponly'],
|
||||
);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
}
|
||||
public static function csrf(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf'])) {
|
||||
$_SESSION['csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return (string) $_SESSION['csrf'];
|
||||
}
|
||||
public static function validCsrf(?string $token): bool
|
||||
{
|
||||
return is_string($token) && hash_equals(self::csrf(), $token);
|
||||
}
|
||||
public static function is(string $role): bool
|
||||
{
|
||||
return (self::user()['role'] ?? '') === $role;
|
||||
}
|
||||
public static function canWrite(string $path): bool
|
||||
{
|
||||
$module = PermissionService::moduleForPath($path);
|
||||
return $module === null || PermissionService::can($module, 'edit');
|
||||
}
|
||||
public static function isSecureRequest(): bool
|
||||
{
|
||||
if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
|
||||
return true;
|
||||
}
|
||||
if (str_starts_with(strtolower((string) getenv('APP_URL')), 'https://')) {
|
||||
return true;
|
||||
}
|
||||
$remote = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$trusted = array_filter(array_map('trim', explode(',', (string) getenv('GLOBINOURS_TRUSTED_PROXIES'))));
|
||||
if (in_array($remote, ['127.0.0.1', '::1'], true) || in_array($remote, $trusted, true)) {
|
||||
return strtolower(trim(explode(',', (string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))[0] ?? '')) ===
|
||||
'https';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static function recordAttempt(string $username, string $ip, bool $success): void
|
||||
{
|
||||
try {
|
||||
DB::pdo()
|
||||
->prepare('INSERT INTO login_attempts(username_hash,ip_address,succeeded) VALUES(?,?,?)')
|
||||
->execute([hash('sha256', mb_strtolower($username)), $ip ?: null, $success ? 1 : 0]);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue