Files
feuerwerk/api/lib/Auth.php
T
adminandClaude Opus 5 c830c7a3aa
game-maker / gate (push) Successful in 2m4s
game-maker / suite (push) Successful in 7m18s
Changing the password: the Logout button at the far right of the toolbar became an Account menu that names the signed-in account in an entry of its own that cannot be picked, with Change password... above Logout, and the new dialog (src/editor/auth/PasswordDialog.ts) asks for the current password, the new one and its repetition, all three as bullets, and stays standing when the server refuses so that it can say why, unlike the other form dialogs; the rules for empty, too short, new equal to old and a differing repetition sit as the pure function passwordProblem() next to loginProblem(), the API gained POST ?r=auth/password which answers 204, insists on the current password although a session is there, enforces a minimum length of 8 in UserStore::MIN_PASSWORD_LENGTH and again in the client so the mask can answer without asking the server, and the new SessionStore::destroyAllFor($user, $keep) ends every other session of the account while the tab that changed the password keeps its own; the placeholders admin/admin and user/user stay as they are, which the length check does not touch because it only runs when a password is set through the API; e2e 45 grew by 8 checks for the menu, the dialog, both refusals and signing in with the old and the new password, and it sets the e2e account back at the end - a run dying in between leaves it on <password>-tmp, which a make-user.php call repairs (#182)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 13:46:40 +02:00

98 lines
3.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* Who is asking. The browser sends the session cookie; the check tools log in the same way and
* send the same cookie, so there is exactly one way into the API.
*/
final class Auth
{
public function __construct(
private readonly UserStore $users,
private readonly SessionStore $sessions,
) {}
/** The signed-in user, or null when nobody is. */
public function user(): ?string
{
$token = (string) ($_COOKIE[SessionStore::COOKIE] ?? '');
return $token === '' ? null : $this->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;
}
/** 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;
}
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 (`/game-maker/`), 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'] ?? '/game-maker/api/index.php');
$base = dirname(dirname($script));
return $base === '/' || $base === '\\' || $base === '.' ? '/' : $base . '/';
}
}