update controllers logic

This commit is contained in:
root
2026-04-23 00:04:35 -04:00
parent 1977a513df
commit ca4ba272fc
353 changed files with 13402 additions and 1301 deletions
@@ -0,0 +1,130 @@
<?php
namespace App\Services\Auth;
use App\Models\IpAttempt;
use App\Models\LoginActivity;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
* Mirrors CodeIgniter `AuthController::apiLogin` security behavior:
* IP lockout (ip_attempts), failed_attempts / suspension after 3 failures, login_activity logging.
*/
class ApiLoginSecurityService
{
public function isIpBlocked(string $ip): bool
{
$row = IpAttempt::query()->where('ip_address', $ip)->first();
if (!$row || !$row->blocked_until) {
return false;
}
return $row->blocked_until->isFuture();
}
public function logIpAttempt(string $ip): void
{
$now = utc_now();
$attempt = IpAttempt::query()->where('ip_address', $ip)->first();
if ($attempt) {
$attempts = ((int) $attempt->attempts) + 1;
$blockedUntil = null;
if ($attempts >= 10) {
$blockedUntil = date('Y-m-d H:i:s', strtotime('+24 hours'));
}
$attempt->update([
'attempts' => $attempts,
'last_attempt_at' => $now,
'blocked_until' => $blockedUntil,
]);
return;
}
IpAttempt::query()->create([
'ip_address' => $ip,
'attempts' => 1,
'last_attempt_at' => $now,
]);
}
public function handleFailedLogin(User $user, string $email, string $ip): void
{
$user->refresh();
$failedAttempts = ((int) ($user->failed_attempts ?? 0)) + 1;
$data = [
'failed_attempts' => $failedAttempts,
'last_failed_at' => utc_now(),
];
if ($failedAttempts >= 3) {
$data['is_suspended'] = true;
// CI sends reset email via View\UserController::sendResetEmail — wire Laravel mail here when parity is needed.
Log::notice('api_login: account suspended after failed attempts', ['email' => $email, 'user_id' => $user->id]);
}
$user->fill($data);
$user->save();
$this->logIpAttempt($ip);
}
public function resetFailedAttempts(User $user): void
{
$user->failed_attempts = 0;
$user->last_failed_at = null;
$user->save();
}
public function logSuccessfulLogin(User $user, Request $request): void
{
LoginActivity::query()->create([
'user_id' => $user->id,
'email' => $user->email,
'login_time' => utc_now(),
'ip_address' => $request->ip(),
'user_agent' => (string) ($request->userAgent() ?? ''),
]);
}
/**
* CI returns roles as an object map {"RoleName": true}.
*
* @param list<string> $roleNames
*/
public function rolesToObjectMap(array $roleNames): object
{
$rolesMap = [];
foreach ($roleNames as $r) {
$rolesMap[$r] = true;
}
return (object) $rolesMap;
}
/**
* Top-level JSON body aligned with the CodeIgniter 4 `AuthController::apiLogin` success payload.
*
* @return array{status: true, token: string, user: array{id: int, name: string, roles: object}}
*/
public function buildLoginResponse(User $user, string $jwtToken): array
{
$roleNames = $user->roleNamesLikeCodeIgniter();
$fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? ''));
return [
'status' => true,
'token' => $jwtToken,
'user' => [
'id' => (int) $user->id,
'name' => $fullName,
'roles' => $this->rolesToObjectMap($roleNames),
],
];
}
}
+206
View File
@@ -0,0 +1,206 @@
<?php
namespace App\Services\Auth;
use App\Models\Configuration;
use App\Services\ApplicationUrlService;
use App\Models\LoginActivity;
use App\Models\Preferences;
use App\Models\Role;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
/**
* CodeIgniter `AuthController` web (session) flow: session keys, dashboards, redirects, preferences.
*/
class AuthSessionService
{
private const FALLBACK_DASHBOARD = '/landing_page/guest_dashboard';
public function __construct(
private ApplicationUrlService $urls,
) {
}
/**
* Same-host relative redirect sanitization as CI `sanitizeRedirectTarget`.
*/
public function sanitizeRedirectTarget(string $redirectTo): ?string
{
$redirectTo = trim($redirectTo);
if ($redirectTo === '') {
return null;
}
if (preg_match('#^https?://#i', $redirectTo)) {
$appHost = (string) parse_url($this->urls->docsHomeUrl(), PHP_URL_HOST);
$targetHost = (string) parse_url($redirectTo, PHP_URL_HOST);
$targetPath = (string) parse_url($redirectTo, PHP_URL_PATH);
$targetQuery = (string) parse_url($redirectTo, PHP_URL_QUERY);
if ($appHost === '' || $targetHost === '' || strcasecmp($appHost, $targetHost) !== 0) {
return null;
}
$redirectTo = $targetPath !== '' ? $targetPath : '/';
if ($targetQuery !== '') {
$redirectTo .= '?'.$targetQuery;
}
}
if (! str_starts_with($redirectTo, '/')) {
$redirectTo = '/'.ltrim($redirectTo, '/');
}
if (str_starts_with($redirectTo, '//')) {
return null;
}
return $redirectTo;
}
/**
* Highest-priority dashboard route matching any of the role names/slugs.
*
* @param list<string> $roleNames
*/
public function dashboardRouteForRoles(array $roleNames): string
{
if ($roleNames === []) {
return self::FALLBACK_DASHBOARD;
}
$rows = Role::findByNamesOrSlugs($roleNames);
if ($rows !== []) {
$route = $rows[0]->dashboard_route ?? null;
if (is_string($route) && $route !== '') {
return $route;
}
}
return self::FALLBACK_DASHBOARD;
}
/**
* Apply CI-style preference keys into session (style/menu colors).
*/
public function applyStylePreferencesToSession(int $userId): void
{
if ($userId <= 0) {
return;
}
$prefs = Preferences::query()->where('user_id', $userId)->first();
if (! $prefs) {
return;
}
$styleColor = (string) ($prefs->style_color ?? '');
if ($styleColor !== '') {
session()->put('style_color', $styleColor);
}
$menuColor = (string) ($prefs->menu_color ?? '');
if ($menuColor === 'custom') {
session()->put('menu_color', 'custom');
session()->put('menu_custom_bg', (string) ($prefs->menu_custom_bg ?? '#0f172a'));
session()->put('menu_custom_text', (string) ($prefs->menu_custom_text ?? '#ffffff'));
session()->put('menu_custom_mode', (string) ($prefs->menu_custom_mode ?? 'dark'));
} elseif ($menuColor !== '') {
session()->put('menu_color', $menuColor);
}
}
/**
* Populate Laravel web auth + CI-compatible session keys.
*
* @param list<string> $roleNames
*/
public function writeCiAuthenticatedSession(User $user, array $roleNames): void
{
Auth::guard('web')->login($user);
$schoolYear = Configuration::getConfig('school_year');
$semester = Configuration::getConfig('semester');
session([
'user_id' => $user->id,
'user_email' => $user->email,
'user_name' => trim(($user->firstname ?? '').' '.($user->lastname ?? '')),
'user_type' => $user->user_type ?? null,
'is_logged_in' => true,
'login_time' => time(),
'last_activity' => time(),
'roles' => $roleNames,
'semester' => $semester,
'school_year' => $schoolYear,
]);
$this->applyStylePreferencesToSession((int) $user->id);
}
/**
* Close open login_activity row(s) without logout_time for this user (CI parity).
*/
public function logLogout(?int $userId, ?string $email): void
{
if (! $userId || ! $email) {
return;
}
LoginActivity::query()
->where('user_id', $userId)
->whereNull('logout_time')
->update([
'logout_time' => utc_now(),
'email' => $email,
]);
}
/**
* Determine next navigation after credentials validated (single vs multi-role).
*
* @return array{kind: string, redirect_url?: string|null, roles?: array<int, string>, reason?: string}
*/
public function resolvePostLoginDestination(User $user, ?string $sanitizedRedirect): array
{
$roleNames = $user->roleNamesLikeCodeIgniter();
if ($roleNames === []) {
Log::notice('session_login: user has no roles', ['user_id' => $user->id]);
return [
'kind' => 'no_roles',
'reason' => 'Role not assigned. Please contact support.',
];
}
$this->writeCiAuthenticatedSession($user, $roleNames);
if (count($roleNames) === 1) {
session()->put('role', $roleNames[0]);
if ($sanitizedRedirect !== null) {
return ['kind' => 'redirect', 'redirect_url' => $sanitizedRedirect];
}
return [
'kind' => 'redirect',
'redirect_url' => $this->dashboardRouteForRoles([$roleNames[0]]),
];
}
if ($sanitizedRedirect !== null) {
session()->put('post_login_redirect', $sanitizedRedirect);
} else {
session()->forget('post_login_redirect');
}
return [
'kind' => 'select_role',
'roles' => array_values($roleNames),
'redirect_url' => $this->urls->webSelectRoleUrl(false),
];
}
}
@@ -0,0 +1,126 @@
<?php
namespace App\Services\Auth;
use App\Models\Configuration;
use App\Models\Role;
use App\Models\User;
use App\Models\UserRole;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
/**
* Matches CodeIgniter 4 `AuthController::apiRegister` (public API register, no captcha).
*/
class CodeIgniterApiRegistrationService
{
public function registerResponse(Request $request): JsonResponse
{
$requestData = $request->all();
if ($request->isJson() && ($requestData === [] || $requestData === null)) {
$decoded = json_decode((string) $request->getContent(), true);
$requestData = is_array($decoded) ? $decoded : [];
}
$validator = Validator::make($requestData, [
'firstname' => ['required', 'string', 'min:2', 'max:30'],
'lastname' => ['required', 'string', 'min:2', 'max:30'],
'email' => ['required', 'email'],
'password' => ['required', 'string', 'min:8'],
'cellphone' => ['required', 'string', 'min:10', 'max:20'],
'gender' => ['nullable', 'string'],
'address_street' => ['nullable', 'string'],
'city' => ['nullable', 'string'],
'state' => ['nullable', 'string'],
'zip' => ['nullable', 'string'],
'role' => ['nullable', 'string'],
]);
if ($validator->fails()) {
return response()->json([
'status' => false,
'message' => 'Validation failed',
'errors' => $validator->errors()->toArray(),
], 422);
}
/** @var array<string, mixed> $data */
$data = $validator->validated();
if (User::query()->where('email', $data['email'])->exists()) {
return response()->json([
'status' => false,
'message' => 'Email already registered',
], 422);
}
$schoolYear = Configuration::getConfig('school_year');
$semester = Configuration::getConfig('semester');
$userData = [
'firstname' => $data['firstname'],
'lastname' => $data['lastname'],
'email' => $data['email'],
'password' => pbkdf2_hash((string) $data['password']),
'cellphone' => $data['cellphone'],
'gender' => $data['gender'] ?? null,
'address_street' => $data['address_street'] ?? '',
'city' => $data['city'] ?? '',
'state' => $data['state'] ?? '',
'zip' => $data['zip'] ?? '',
'school_year' => $schoolYear,
'semester' => $semester ?? '',
'status' => 'active',
'is_verified' => 0,
'accept_school_policy' => 0,
];
try {
$userId = null;
$rolesForResponse = [];
DB::transaction(function () use ($userData, $data, &$userId, &$rolesForResponse): void {
$user = User::query()->create($userData);
$userId = (int) $user->id;
if (($data['role'] ?? '') === 'parent') {
$role = Role::query()->where('name', 'parent')->first();
if ($role) {
UserRole::query()->create([
'user_id' => $userId,
'role_id' => (int) $role->id,
]);
$rolesForResponse = ['parent'];
}
}
});
$user = User::query()->findOrFail($userId);
$token = JWTAuth::fromUser($user);
$fullName = trim((string) $user->firstname.' '.(string) $user->lastname);
return response()->json([
'status' => true,
'message' => 'Registration successful. Please verify your email.',
'token' => $token,
'user' => [
'id' => $userId,
'name' => $fullName,
'email' => $userData['email'],
'roles' => $rolesForResponse,
],
], 201);
} catch (\Throwable $e) {
log_message('error', 'Registration error: '.$e->getMessage());
return response()->json([
'status' => false,
'message' => 'Registration failed. Please try again.',
], 500);
}
}
}
+8 -19
View File
@@ -7,6 +7,7 @@ use App\Models\ParentModel;
use App\Models\Role;
use App\Models\User;
use App\Models\UserRole;
use App\Services\ApplicationUrlService;
use App\Services\EmailService;
use App\Services\SchoolIdService;
use Illuminate\Support\Facades\DB;
@@ -17,7 +18,8 @@ class RegistrationService
private EmailService $emailService,
private SchoolIdService $schoolIdService,
private RegistrationFormatterService $formatter,
private RegistrationCaptchaService $captchaService
private RegistrationCaptchaService $captchaService,
private ApplicationUrlService $urls,
) {
}
@@ -125,26 +127,13 @@ class RegistrationService
}
});
$activationLink = url('/user/confirm/' . $token);
$activationLink = $this->urls->spaRegistrationConfirmUrl($token);
$recipientName = trim((string) ($formatted['firstname'] ?? '') . ' ' . (string) ($formatted['lastname'] ?? ''));
$emailData = [
'recipientName' => $recipientName,
'activationLink' => $activationLink,
'orgName' => 'Al Rahma Sunday School',
'contactInfo' => 'alrahma.isgl@gmail.com',
'logoUrl' => 'https://alrahmaisgl.org/assets/images/alrahma_logo.png',
'subject' => 'Email Confirmation',
];
if (view()->exists($isParent ? 'emails.welcome_parent' : 'emails.welcome_staff')) {
$html = view($isParent ? 'emails.welcome_parent' : 'emails.welcome_staff', $emailData);
} else {
$html = '<p>Hello ' . e($recipientName !== '' ? $recipientName : 'there') . ',</p>'
. '<p>Please confirm your email by clicking the link below:</p>'
. '<p><a href="' . e($activationLink) . '">Activate your account</a></p>'
. '<p>Thank you.</p>';
}
$html = '<p>Hello ' . e($recipientName !== '' ? $recipientName : 'there') . ',</p>'
. '<p>Please confirm your email by clicking the link below:</p>'
. '<p><a href="' . e($activationLink) . '">Activate your account</a></p>'
. '<p>Thank you.</p>';
$sent = $this->emailService->send($email, 'Email Confirmation', $html, 'general');
$this->captchaService->clear();