getBody()->getContents(), true); $email = $body['email'] ?? ''; $password = $body['password'] ?? ''; if (empty($email) || empty($password)) { return $this->json($response, ['message' => 'Email and password required'], 400); } $user = User::where('email', $email)->first(); if (!$user || !password_verify($password, $user->password_hash)) { return $this->json($response, ['message' => 'Invalid credentials'], 401); } if (!$user->is_active) { return $this->json($response, ['message' => 'Account disabled'], 403); } // Generate JWT $jwt = $this->generateJwt($user); // Generate refresh token $refreshToken = $this->generateRefreshToken($user->id); $userData = $user->toArray(); unset($userData['password_hash']); return $this->json($response, [ 'token' => $jwt, 'refresh_token' => $refreshToken, 'expires_in' => self::JWT_EXPIRY, 'user' => $userData, ]); } public function refresh(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = json_decode($request->getBody()->getContents(), true); $refreshToken = $body['refresh_token'] ?? ''; if (empty($refreshToken)) { return $this->json($response, ['message' => 'Refresh token required'], 400); } // Find and validate refresh token $tokenRecord = RefreshToken::findValid($refreshToken); if (!$tokenRecord) { return $this->json($response, ['message' => 'Invalid or expired refresh token'], 401); } // Load user $user = User::find($tokenRecord->user_id); if (!$user || !$user->is_active) { return $this->json($response, ['message' => 'User not found or disabled'], 401); } // Generate new JWT $jwt = $this->generateJwt($user); // Optionally rotate refresh token (revoke old, create new) $tokenRecord->revoke(); $newRefreshToken = $this->generateRefreshToken($user->id); $userData = $user->toArray(); unset($userData['password_hash']); return $this->json($response, [ 'token' => $jwt, 'refresh_token' => $newRefreshToken, 'expires_in' => self::JWT_EXPIRY, 'user' => $userData, ]); } public function logout(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $body = json_decode($request->getBody()->getContents(), true); $refreshToken = $body['refresh_token'] ?? null; if ($refreshToken) { $tokenRecord = RefreshToken::findValid($refreshToken); if ($tokenRecord) { $tokenRecord->revoke(); } } return $this->json($response, ['message' => 'Logged out']); } private function generateJwt(User $user): string { $now = time(); $payload = [ 'iss' => 'api.fahrschultermin.de', 'sub' => $user->id, 'tenant_id' => $user->tenant_id, 'role' => $user->role, 'iat' => $now, 'exp' => $now + self::JWT_EXPIRY, ]; $secret = $_ENV['JWT_SECRET'] ?? null; if (!$secret) { throw new \RuntimeException('JWT_SECRET environment variable not set'); } return JWT::encode($payload, $secret, 'HS256'); } private function generateRefreshToken(int $userId): string { $token = bin2hex(random_bytes(32)); // 64-char hex token $hash = hash('sha256', $token); RefreshToken::create([ 'user_id' => $userId, 'token_hash' => $hash, 'expires_at' => \Carbon\Carbon::now()->addDays(self::REFRESH_EXPIRY_DAYS), ]); return $token; } private function json(ResponseInterface $response, array $data, int $status = 200): ResponseInterface { $response->getBody()->write(json_encode($data)); return $response ->withStatus($status) ->withHeader('Content-Type', 'application/json'); } }