Initial
EK-DOS-WEB bauen und ausrollen / frontend (push) Failing after 22s
EK-DOS-WEB bauen und ausrollen / backend (push) Failing after 27s
EK-DOS-WEB bauen und ausrollen / deploy (push) Skipped

This commit is contained in:
Kyle Müller
2026-09-06 13:25:44 +02:00
commit 7d82e807fc
71 changed files with 10601 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
<?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';
}
}