Phase 1: API + Frontend Skelett
This commit is contained in:
317
www/api.fahrschuldesk.de/app/Http/Controllers/AuthController.php
Normal file
317
www/api.fahrschuldesk.de/app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth Controller
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Support\Database;
|
||||
use App\Support\Response;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
private array $config;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
*/
|
||||
public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$email = trim($body['email'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (empty($email) || empty($password)) {
|
||||
return Response::validationError($response, [
|
||||
'email' => 'Email is required',
|
||||
'password' => 'Password is required',
|
||||
]);
|
||||
}
|
||||
|
||||
// Check if it's a platform admin login
|
||||
$admin = Database::fetch(
|
||||
'SELECT * FROM platform_admins WHERE email = $1',
|
||||
[$email]
|
||||
);
|
||||
|
||||
if ($admin && password_verify($password, $admin['password_hash'])) {
|
||||
return $this->createTokenResponse($response, $admin, 'platform_admin');
|
||||
}
|
||||
|
||||
// Check if it's a tenant user login
|
||||
$user = Database::fetch(
|
||||
'SELECT u.*, t.status as tenant_status
|
||||
FROM users u
|
||||
JOIN tenants t ON t.id = u.tenant_id
|
||||
WHERE u.email = $1',
|
||||
[$email]
|
||||
);
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
if ($user['tenant_status'] !== 'active') {
|
||||
return Response::error($response, 'Account is not active', 403);
|
||||
}
|
||||
if ($user['status'] !== 'active') {
|
||||
return Response::error($response, 'User is inactive', 403);
|
||||
}
|
||||
|
||||
// Update last login
|
||||
Database::update('users', ['last_login_at' => 'NOW()'], 'id = $1', [$user['id']]);
|
||||
|
||||
return $this->createTokenResponse($response, $user, 'user');
|
||||
}
|
||||
|
||||
return Response::error($response, 'Invalid credentials', 401);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
*/
|
||||
public function register(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$name = trim($body['name'] ?? '');
|
||||
$email = trim($body['email'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
$phone = trim($body['phone'] ?? '');
|
||||
$address = trim($body['address'] ?? '');
|
||||
|
||||
// Validation
|
||||
$errors = [];
|
||||
if (empty($name)) $errors['name'] = 'Name is required';
|
||||
if (empty($email)) $errors['email'] = 'Email is required';
|
||||
elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = 'Invalid email format';
|
||||
if (empty($password)) $errors['password'] = 'Password is required';
|
||||
elseif (strlen($password) < 8) $errors['password'] = 'Password must be at least 8 characters';
|
||||
|
||||
if (!empty($errors)) {
|
||||
return Response::validationError($response, $errors);
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
$exists = Database::fetch(
|
||||
'SELECT id FROM tenants WHERE email = $1',
|
||||
[$email]
|
||||
);
|
||||
|
||||
if ($exists) {
|
||||
return Response::error($response, 'Email already registered', 409);
|
||||
}
|
||||
|
||||
// Generate slug from name
|
||||
$slug = strtolower(preg_replace('/[^a-z0-9]+/', '-', $name));
|
||||
$originalSlug = $slug;
|
||||
$counter = 1;
|
||||
while (Database::fetch('SELECT id FROM tenants WHERE slug = $1', [$slug])) {
|
||||
$slug = $originalSlug . '-' . $counter++;
|
||||
}
|
||||
|
||||
// Create tenant
|
||||
$tenantId = Database::insert('tenants', [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'email' => $email,
|
||||
'phone' => $phone,
|
||||
'address' => $address,
|
||||
'status' => 'pending_approval',
|
||||
]);
|
||||
|
||||
// Create first user (admin of the tenant)
|
||||
$userId = Database::insert('users', [
|
||||
'tenant_id' => $tenantId,
|
||||
'email' => $email,
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'first_name' => $name,
|
||||
'last_name' => '',
|
||||
'user_type' => 'admin',
|
||||
'status' => 'pending', // Needs email verification first
|
||||
]);
|
||||
|
||||
// Enable default modules (office, calendar, messenger)
|
||||
$defaultModules = Database::fetchAll('SELECT id FROM modules WHERE key IN (\'office\', \'calendar\', \'messenger\')');
|
||||
foreach ($defaultModules as $module) {
|
||||
Database::insert('tenant_modules', [
|
||||
'tenant_id' => $tenantId,
|
||||
'module_id' => $module['id'],
|
||||
'status' => 'enabled',
|
||||
]);
|
||||
}
|
||||
|
||||
return Response::success($response, [
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $userId,
|
||||
'message' => 'Registration successful. Pending admin approval.',
|
||||
], 'Registration successful', 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/verify-email
|
||||
*/
|
||||
public function verifyEmail(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
$token = $body['token'] ?? '';
|
||||
|
||||
if (empty($token)) {
|
||||
return Response::validationError($response, ['token' => 'Token is required']);
|
||||
}
|
||||
|
||||
// TODO: Verify token and activate user
|
||||
|
||||
return Response::success($response, null, 'Email verified successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
*/
|
||||
public function me(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$auth = $request->getAttribute('auth');
|
||||
|
||||
if ($auth['type'] === 'platform_admin') {
|
||||
$data = Database::fetch(
|
||||
'SELECT id, email, name, role, created_at, last_login_at FROM platform_admins WHERE id = $1',
|
||||
[$auth['id']]
|
||||
);
|
||||
$data['type'] = 'platform_admin';
|
||||
} else {
|
||||
$data = Database::fetch(
|
||||
'SELECT u.id, u.email, u.first_name, u.last_name, u.user_type, u.status, u.avatar_url, u.last_login_at,
|
||||
t.id as tenant_id, t.name as tenant_name, t.slug as tenant_slug, t.status as tenant_status
|
||||
FROM users u
|
||||
JOIN tenants t ON t.id = u.tenant_id
|
||||
WHERE u.id = $1',
|
||||
[$auth['id']]
|
||||
);
|
||||
$data['type'] = 'user';
|
||||
}
|
||||
|
||||
return Response::success($response, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/refresh
|
||||
*/
|
||||
public function refresh(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
$refreshToken = $body['refresh_token'] ?? '';
|
||||
|
||||
if (empty($refreshToken)) {
|
||||
return Response::validationError($response, ['refresh_token' => 'Refresh token is required']);
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = JWT::decode($refreshToken, new Key($this->config['jwt']['secret'], 'HS256'));
|
||||
|
||||
if ($decoded->type !== 'refresh') {
|
||||
return Response::error($response, 'Invalid token type', 401);
|
||||
}
|
||||
|
||||
// Get user/admin
|
||||
if ($decoded->user_type === 'platform_admin') {
|
||||
$user = Database::fetch('SELECT * FROM platform_admins WHERE id = $1', [$decoded->sub]);
|
||||
} else {
|
||||
$user = Database::fetch('SELECT * FROM users WHERE id = $1', [$decoded->sub]);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
return Response::error($response, 'User not found', 401);
|
||||
}
|
||||
|
||||
return $this->createTokenResponse($response, $user, $decoded->user_type);
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($response, 'Invalid refresh token', 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
*/
|
||||
public function logout(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
// JWT is stateless, so logout is handled client-side
|
||||
// Here we could blacklist the token if needed
|
||||
return Response::success($response, null, 'Logged out successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/forgot-password
|
||||
*/
|
||||
public function forgotPassword(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
$email = trim($body['email'] ?? '');
|
||||
|
||||
if (empty($email)) {
|
||||
return Response::validationError($response, ['email' => 'Email is required']);
|
||||
}
|
||||
|
||||
// Check if email exists
|
||||
$admin = Database::fetch('SELECT id FROM platform_admins WHERE email = $1', [$email]);
|
||||
$user = Database::fetch('SELECT id FROM users WHERE email = $1', [$email]);
|
||||
|
||||
// Always return success to prevent email enumeration
|
||||
if (!$admin && !$user) {
|
||||
return Response::success($response, null, 'If the email exists, a reset link has been sent');
|
||||
}
|
||||
|
||||
// TODO: Generate reset token, send email
|
||||
|
||||
return Response::success($response, null, 'If the email exists, a reset link has been sent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JWT token response
|
||||
*/
|
||||
private function createTokenResponse(ResponseInterface $response, array $user, string $type): ResponseInterface
|
||||
{
|
||||
$now = time();
|
||||
$expiry = $this->config['jwt']['expiry'];
|
||||
$refreshExpiry = $this->config['jwt']['refresh_expiry'];
|
||||
|
||||
$payload = [
|
||||
'iss' => 'fahrschuldesk',
|
||||
'sub' => $user['id'],
|
||||
'iat' => $now,
|
||||
'exp' => $now + $expiry,
|
||||
'type' => 'access',
|
||||
'user_type' => $type,
|
||||
];
|
||||
|
||||
$refreshPayload = [
|
||||
'iss' => 'fahrschuldesk',
|
||||
'sub' => $user['id'],
|
||||
'iat' => $now,
|
||||
'exp' => $now + $refreshExpiry,
|
||||
'type' => 'refresh',
|
||||
'user_type' => $type,
|
||||
];
|
||||
|
||||
$accessToken = JWT::encode($payload, $this->config['jwt']['secret'], 'HS256');
|
||||
$refreshToken = JWT::encode($refreshPayload, $this->config['jwt']['secret'], 'HS256');
|
||||
|
||||
return Response::json($response, [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'access_token' => $accessToken,
|
||||
'refresh_token' => $refreshToken,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => $expiry,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user