Initial
This commit is contained in:
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user