update controllers logic
This commit is contained in:
@@ -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),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user