142 lines
4.8 KiB
PHP
142 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Ekdos\Auth;
|
|
|
|
use Ekdos\Http\Middleware\SessionMiddleware;
|
|
use Ekdos\Support\Json;
|
|
use Ekdos\Users\Permission;
|
|
use Ekdos\Users\UserRepository;
|
|
use Psr\Http\Message\ResponseInterface as Response;
|
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
|
|
final readonly class AuthController
|
|
{
|
|
public function __construct(
|
|
private AuthService $auth,
|
|
private SessionCookie $cookie,
|
|
private UserRepository $users,
|
|
) {}
|
|
|
|
public function login(Request $request, Response $response): Response
|
|
{
|
|
$body = $request->getParsedBody();
|
|
$body = is_array($body) ? $body : Json::decode((string) $request->getBody());
|
|
|
|
$result = $this->auth->login(
|
|
username: (string) ($body['username'] ?? $body['account'] ?? ''),
|
|
password: (string) ($body['password'] ?? ''),
|
|
clientIp: self::clientIp($request),
|
|
);
|
|
|
|
if ($result['ok'] === false) {
|
|
return Json::error($response, $result['error'], $result['status']);
|
|
}
|
|
|
|
return $this->cookie->attach(Json::write($response, ['user' => self::describe($result['session'])]), $result['session']->id);
|
|
}
|
|
|
|
/**
|
|
* Die Auth-Sonde der Oberflaeche.
|
|
*
|
|
* Bewusst ohne RequireAuth: die SPA fragt hier vor jeder Anzeige an und
|
|
* entscheidet anhand von 200 oder 401, ob sie die Anmeldemaske zeigt.
|
|
*/
|
|
public function me(Request $request, Response $response): Response
|
|
{
|
|
$session = SessionMiddleware::of($request);
|
|
|
|
if ($session === null) {
|
|
return Json::write($response, ['user' => null], 401);
|
|
}
|
|
|
|
return Json::write($response, ['user' => self::describe($session)]);
|
|
}
|
|
|
|
/**
|
|
* Haelt die Sitzung am Leben, solange jemand am Bildschirm arbeitet.
|
|
* Die Oberflaeche ruft das alle fuenf Minuten auf.
|
|
*/
|
|
public function refresh(Request $request, Response $response): Response
|
|
{
|
|
$session = SessionMiddleware::of($request);
|
|
|
|
if ($session === null) {
|
|
return Json::write($response, ['user' => null], 401);
|
|
}
|
|
|
|
return Json::write($response, ['user' => self::describe($session)]);
|
|
}
|
|
|
|
public function logout(Request $request, Response $response): Response
|
|
{
|
|
$this->auth->logout($this->cookie->read($request));
|
|
|
|
return $this->cookie->clear(Json::write($response, ['ok' => true]));
|
|
}
|
|
|
|
/** Eigenes Passwort aendern. Erlaubt jedem angemeldeten Benutzer. */
|
|
public function changeOwnPassword(Request $request, Response $response): Response
|
|
{
|
|
$session = SessionMiddleware::of($request);
|
|
|
|
if ($session === null) {
|
|
return Json::error($response, 'Bitte erneut anmelden.', 401);
|
|
}
|
|
|
|
$body = $request->getParsedBody();
|
|
$body = is_array($body) ? $body : Json::decode((string) $request->getBody());
|
|
$current = (string) ($body['currentPassword'] ?? '');
|
|
$next = (string) ($body['newPassword'] ?? '');
|
|
|
|
$user = $this->users->findByUsernameWithHash($session->username);
|
|
|
|
if ($user === null || !password_verify($current, $user->passwordHash)) {
|
|
return Json::error($response, 'Das aktuelle Passwort ist nicht korrekt.', 403);
|
|
}
|
|
|
|
if (($problem = AuthService::rejectWeakPassword($next)) !== null) {
|
|
return Json::error($response, $problem, 400);
|
|
}
|
|
|
|
$this->users->update($user->id, ['password_hash' => AuthService::hash($next)]);
|
|
$this->users->audit($user->id, $user->displayName, 'password.self', $user->id, $user->username);
|
|
|
|
// Alle anderen Sitzungen dieses Benutzers verfallen; die eigene bleibt bestehen.
|
|
return Json::write($response, ['ok' => true]);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private static function describe(Session $session): array
|
|
{
|
|
return [
|
|
'id' => $session->userId,
|
|
'username' => $session->username,
|
|
'name' => $session->displayName,
|
|
'role' => $session->role->value,
|
|
'roleLabel' => $session->role->label(),
|
|
'permissions' => $session->role->permissions(),
|
|
'canManageUsers' => $session->can(Permission::UsersManage),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Hinter nginx steht die echte Adresse in X-Forwarded-For. Es wird nur der
|
|
* erste Eintrag verwendet und nur, wenn der Header ueberhaupt gesetzt ist --
|
|
* der Origin ist ausschliesslich ueber den Proxy erreichbar.
|
|
*/
|
|
private static function clientIp(Request $request): string
|
|
{
|
|
$forwarded = $request->getHeaderLine('X-Forwarded-For');
|
|
|
|
if ($forwarded !== '') {
|
|
return trim(explode(',', $forwarded)[0]);
|
|
}
|
|
|
|
$server = $request->getServerParams();
|
|
|
|
return is_string($server['REMOTE_ADDR'] ?? null) ? $server['REMOTE_ADDR'] : 'unbekannt';
|
|
}
|
|
}
|