sessions->user($token); } /** The signed-in user, or a 401 answer – every route but `auth/*` goes through here. */ public function require(): string { $user = $this->user(); if ($user === null) { Response::error('Sign in first', 401); } return $user; } /** * The signed-in user if they are an administrator (#180), else 401 or 403. Every route that * may do more than a user goes through here: the editor only hides what it may not offer, the * decision is made on this side. */ public function requireAdmin(): string { $user = $this->require(); if ($this->users->role($user) !== 'admin') { Response::error('Administrators only', 403); } return $user; } /** `admin` or `user`; the session answer carries it so the editor knows what to offer. */ public function role(string $user): string { return $this->users->role($user); } /** Checks the pair, starts a session and sets the cookie. Null when the pair is wrong. */ public function logIn(string $username, string $password): ?string { $name = $this->users->verify($username, $password); if ($name === null) { return null; } $this->setCookie($this->sessions->create($name), time() + SessionStore::LIFETIME_SECONDS); return $name; } public function logOut(): void { $token = $this->token(); if ($token !== '') { $this->sessions->destroy($token); } $this->setCookie('', time() - 3600); } /** * Sets a new password for `$user`, who has to type the old one again: a session alone is not * enough, or somebody else's open tab would be all it takes to take an account over. * * Every other login of the account ends with it – a session that was started with the old * password has no business outliving it. The one asking keeps theirs. * * @return bool False when the current password does not match; the caller answers 401 then. */ public function changePassword(string $user, string $current, string $new): bool { if ($this->users->verify($user, $current) === null) { return false; } $this->users->put($user, $new); $this->sessions->destroyAllFor($user, $this->token()); return true; } /** * An administrator sets somebody's password without knowing the old one (#179). That account's * sessions end with it, except the one asking when it is their own. */ public function resetPassword(string $user, string $new): void { $this->users->put($user, $new); $this->sessions->destroyAllFor($user, $this->token()); } /** Signs an account out everywhere – it was renamed or deleted, and its sessions carry the old name. */ public function endSessionsOf(string $user): void { $this->sessions->destroyAllFor($user); } private function token(): string { return (string) ($_COOKIE[SessionStore::COOKIE] ?? ''); } private function setCookie(string $value, int $expires): void { setcookie(SessionStore::COOKIE, $value, [ 'expires' => $expires, 'path' => self::basePath(), 'httponly' => true, 'samesite' => 'Lax', 'secure' => ($_SERVER['HTTPS'] ?? '') !== '', ]); } /** * The path the editor is served under (`/feuerwerk/`), read off the script rather than * written down: the same checkout answers under a different prefix in CI. */ private static function basePath(): string { $script = (string) ($_SERVER['SCRIPT_NAME'] ?? '/feuerwerk/api/index.php'); $base = dirname(dirname($script)); return $base === '/' || $base === '\\' || $base === '.' ? '/' : $base . '/'; } }