2026-02-23 12:58:19 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use MintyPHP\Http\ApiBootstrap;
|
|
|
|
|
use MintyPHP\Http\ApiResponse;
|
2026-03-06 11:28:22 +01:00
|
|
|
use MintyPHP\Http\RequestRuntimeInterface;
|
2026-03-04 15:56:58 +01:00
|
|
|
use MintyPHP\Service\Auth\ApiTokenEndpointService;
|
2026-02-23 12:58:19 +01:00
|
|
|
use MintyPHP\Service\Auth\AuthServicesFactory;
|
|
|
|
|
use MintyPHP\Service\Security\SecurityServicesFactory;
|
|
|
|
|
use MintyPHP\Service\User\UserServicesFactory;
|
|
|
|
|
|
2026-03-06 11:28:22 +01:00
|
|
|
$requestRuntime = app(RequestRuntimeInterface::class);
|
|
|
|
|
|
2026-02-24 08:49:40 +01:00
|
|
|
ApiBootstrap::init(false);
|
2026-02-23 12:58:19 +01:00
|
|
|
ApiResponse::requireMethod('POST');
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$request = requestInput();
|
|
|
|
|
$input = $request->bodyAll();
|
|
|
|
|
$apiTokenEndpointService = app(ApiTokenEndpointService::class);
|
2026-02-23 12:58:19 +01:00
|
|
|
|
|
|
|
|
// ---- Login-specific rate limits (stricter than the general API rate limit in ApiBootstrap) ----
|
2026-03-04 15:56:58 +01:00
|
|
|
$rateLimiter = (app(SecurityServicesFactory::class))->createRateLimiterService();
|
2026-03-06 11:28:22 +01:00
|
|
|
$ip = $requestRuntime->ip();
|
2026-02-23 12:58:19 +01:00
|
|
|
|
|
|
|
|
$ipResult = $rateLimiter->isBlocked('api.auth.login.ip', $ip);
|
|
|
|
|
if (!($ipResult['allowed'] ?? true)) {
|
|
|
|
|
ApiResponse::tooManyRequests((int) ($ipResult['retry_after'] ?? 60));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Input ----
|
|
|
|
|
$email = strtolower(trim((string) ($input['email'] ?? '')));
|
|
|
|
|
$password = (string) ($input['password'] ?? '');
|
|
|
|
|
$tokenName = trim((string) ($input['token_name'] ?? '')) ?: 'Login';
|
|
|
|
|
$tenantUuid = trim((string) ($input['tenant_uuid'] ?? ''));
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$validationErrors = formErrors();
|
2026-02-23 12:58:19 +01:00
|
|
|
if ($email === '') {
|
2026-03-04 15:56:58 +01:00
|
|
|
$validationErrors->add('email', 'required');
|
2026-02-23 12:58:19 +01:00
|
|
|
}
|
|
|
|
|
if ($password === '') {
|
2026-03-04 15:56:58 +01:00
|
|
|
$validationErrors->add('password', 'required');
|
2026-02-23 12:58:19 +01:00
|
|
|
}
|
2026-03-04 15:56:58 +01:00
|
|
|
if ($validationErrors->hasAny()) {
|
|
|
|
|
ApiResponse::validationFromFormErrors($validationErrors);
|
2026-02-23 12:58:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Email+IP rate limit ----
|
|
|
|
|
$emailKey = $email . '|' . $ip;
|
|
|
|
|
$emailResult = $rateLimiter->isBlocked('api.auth.login.email_ip', $emailKey);
|
|
|
|
|
if (!($emailResult['allowed'] ?? true)) {
|
|
|
|
|
ApiResponse::tooManyRequests((int) ($emailResult['retry_after'] ?? 60));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- User lookup ----
|
2026-03-04 15:56:58 +01:00
|
|
|
$userReadRepository = (app(UserServicesFactory::class))->createUserReadRepository();
|
2026-02-23 12:58:19 +01:00
|
|
|
$user = $userReadRepository->findByEmail($email);
|
|
|
|
|
if (!$user) {
|
2026-03-05 11:17:42 +01:00
|
|
|
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
|
|
|
|
|
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
|
2026-02-23 12:58:19 +01:00
|
|
|
ApiResponse::error('invalid_credentials', 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Account checks ----
|
|
|
|
|
if (!(int) ($user['active'] ?? 0)) {
|
2026-03-05 11:17:42 +01:00
|
|
|
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
|
|
|
|
|
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
|
2026-02-23 12:58:19 +01:00
|
|
|
ApiResponse::error('account_inactive', 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty($user['email_verified_at'])) {
|
2026-03-05 11:17:42 +01:00
|
|
|
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
|
|
|
|
|
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
|
2026-02-23 12:58:19 +01:00
|
|
|
ApiResponse::error('invalid_credentials', 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Password verification ----
|
|
|
|
|
if (!password_verify($password, (string) ($user['password'] ?? ''))) {
|
2026-03-05 11:17:42 +01:00
|
|
|
$rateLimiter->registerFailure('api.auth.login.ip', $ip, 10, 600, 300);
|
|
|
|
|
$rateLimiter->registerFailure('api.auth.login.email_ip', $emailKey, 5, 900, 900);
|
2026-02-23 12:58:19 +01:00
|
|
|
ApiResponse::error('invalid_credentials', 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- 2FA stub ----
|
|
|
|
|
// When TOTP is implemented, intercept here if totp_secret is non-empty:
|
|
|
|
|
// return a short-lived challenge token instead of issuing a full API token.
|
|
|
|
|
// Example: ApiResponse::success(['requires_2fa' => true, 'challenge_token' => '...'], 200);
|
|
|
|
|
// $totpSecret = trim((string) ($user['totp_secret'] ?? ''));
|
|
|
|
|
// if ($totpSecret !== '') { /* TODO: 2FA challenge flow */ }
|
|
|
|
|
|
|
|
|
|
// ---- Successful login: reset email+IP rate limit ----
|
|
|
|
|
$rateLimiter->reset('api.auth.login.email_ip', $emailKey);
|
|
|
|
|
|
|
|
|
|
// ---- Optional tenant scope ----
|
|
|
|
|
$tenantId = null;
|
|
|
|
|
if ($tenantUuid !== '') {
|
2026-03-04 15:56:58 +01:00
|
|
|
$tenantResolution = $apiTokenEndpointService->resolveLoginTenant((int) ($user['id'] ?? 0), $tenantUuid);
|
|
|
|
|
if (!($tenantResolution['ok'] ?? false)) {
|
|
|
|
|
ApiResponse::error(
|
|
|
|
|
(string) ($tenantResolution['error'] ?? 'invalid_tenant_uuid'),
|
|
|
|
|
(int) ($tenantResolution['status'] ?? 422)
|
|
|
|
|
);
|
2026-02-23 12:58:19 +01:00
|
|
|
}
|
2026-03-04 15:56:58 +01:00
|
|
|
$tenantId = array_key_exists('tenant_id', $tenantResolution) ? (int) ($tenantResolution['tenant_id'] ?? 0) : null;
|
2026-02-23 12:58:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Create token ----
|
|
|
|
|
$userId = (int) ($user['id'] ?? 0);
|
2026-03-04 15:56:58 +01:00
|
|
|
$result = (app(AuthServicesFactory::class))->createApiTokenService()->create(
|
2026-02-23 12:58:19 +01:00
|
|
|
$userId,
|
|
|
|
|
$tokenName,
|
|
|
|
|
$tenantId,
|
|
|
|
|
null, // expiresAt null → uses getApiTokenDefaultTtlDays() from settings
|
|
|
|
|
$userId // createdBy = self-issued
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!($result['ok'] ?? false)) {
|
|
|
|
|
$error = (string) ($result['error'] ?? 'create_failed');
|
|
|
|
|
if ($error === 'max_tokens_reached') {
|
|
|
|
|
ApiResponse::error($error, 409);
|
|
|
|
|
}
|
|
|
|
|
ApiResponse::error($error, 422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ApiResponse::success([
|
|
|
|
|
'data' => [
|
|
|
|
|
'token' => (string) ($result['token'] ?? ''),
|
|
|
|
|
'token_uuid' => (string) ($result['uuid'] ?? ''),
|
|
|
|
|
'expires_at' => $result['expires_at'] ?? null,
|
|
|
|
|
],
|
|
|
|
|
], 201);
|