add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+868
View File
@@ -0,0 +1,868 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\IpAttempt;
use App\Models\LoginActivity;
use App\Models\PasswordReset;
use App\Models\PasswordResetRequest;
use App\Models\Role;
use App\Models\User;
use App\Models\UserRole;
use App\Services\EmailService;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Symfony\Component\HttpFoundation\Response;
class UserController extends BaseApiController
{
protected User $user;
protected UserRole $userRole;
protected Role $role;
protected PasswordReset $passwordReset;
protected PasswordResetRequest $resetRequest;
protected LoginActivity $loginActivity;
protected IpAttempt $ipAttempt;
protected EmailService $emailService;
public function __construct()
{
parent::__construct();
$this->user = model(User::class);
$this->userRole = model(UserRole::class);
$this->role = model(Role::class);
$this->passwordReset = model(PasswordReset::class);
$this->resetRequest = model(PasswordResetRequest::class);
$this->loginActivity = model(LoginActivity::class);
$this->ipAttempt = model(IpAttempt::class);
$this->emailService = app(EmailService::class);
}
public function index(): JsonResponse
{
$sort = (string) $this->laravelRequest->query('sort', 'id');
$order = strtolower((string) $this->laravelRequest->query('order', 'asc')) === 'desc' ? 'desc' : 'asc';
$allowedFields = ['id', 'firstname', 'lastname', 'email', 'role'];
$builder = DB::table('users')
->select(
'users.id',
'users.firstname',
'users.lastname',
'users.email',
'user_roles.role_id',
'roles.name as role',
'users.status',
'users.updated_at'
)
->leftJoin('user_roles', 'user_roles.user_id', '=', 'users.id')
->leftJoin('roles', 'roles.id', '=', 'user_roles.role_id');
if (in_array($sort, $allowedFields, true)) {
$column = $sort === 'role' ? 'roles.name' : 'users.' . $sort;
$builder->orderBy($column, $order);
} else {
$builder->orderBy('users.id', 'asc');
}
$users = $builder->get()->map(fn($row) => (array) $row)->toArray();
$roles = $this->role->newQuery()->orderBy('name')->get()->toArray();
return $this->respondSuccess([
'users' => $users,
'roles' => $roles,
], 'Users retrieved successfully');
}
public function listWithRoles(): JsonResponse
{
return $this->respondSuccess([
'users' => $this->buildUsersWithRoles(),
], 'Users with roles retrieved');
}
public function store(): JsonResponse
{
$data = $this->payloadData();
$rules = [
'firstname' => 'required|min:2|max:100',
'lastname' => 'required|min:2|max:100',
'password' => 'required|min:8',
'cellphone' => 'required|min:7|max:50',
'email' => 'required|email|unique:users,email',
'address_street' => 'required|min:3|max:150',
'city' => 'required|min:2|max:100',
'state' => 'required|min:2|max:10',
'zip' => 'required|min:3|max:20',
'accept_school_policy' => 'required',
'role_id' => 'required|integer|exists:roles,id',
];
$errors = $this->validateRequest($data, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$userData = [
'lastname' => ucfirst(strtolower((string) $data['lastname'])),
'firstname' => ucfirst(strtolower((string) $data['firstname'])),
'cellphone' => trim((string) $data['cellphone']),
'email' => strtolower(trim((string) $data['email'])),
'address_street' => trim((string) $data['address_street']),
'city' => ucfirst(strtolower((string) $data['city'])),
'state' => strtoupper(trim((string) $data['state'])),
'zip' => trim((string) $data['zip']),
'accept_school_policy' => in_array($data['accept_school_policy'], [true, '1', 1, 'on'], true) ? 1 : 0,
'password' => $this->hashPassword((string) $data['password']),
'status' => 'Inactive',
];
DB::beginTransaction();
try {
$user = $this->user->newQuery()->create($userData);
$this->userRole->newQuery()->create([
'user_id' => $user->id,
'role_id' => (int) $data['role_id'],
'created_at' => utc_now(),
]);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
log_message('error', 'Failed to create user: ' . $e->getMessage());
return $this->respondError('Failed to create user', Response::HTTP_INTERNAL_SERVER_ERROR);
}
$created = $this->user->find($user->id);
unset($created['password'], $created['token']);
return $this->success(['user' => $created], 'User created successfully', Response::HTTP_CREATED);
}
public function destroy(int $id): JsonResponse
{
$user = $this->user->find($id);
if (!$user) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
DB::beginTransaction();
try {
$this->userRole->newQuery()->where('user_id', $id)->delete();
$this->user->delete($id);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
log_message('error', 'Failed to delete user: ' . $e->getMessage());
return $this->respondError('Failed to delete user', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->respondSuccess(null, 'User deleted successfully');
}
public function updateUser(int $id): JsonResponse
{
$payload = $this->payloadData();
$user = $this->user->find($id);
if (!$user) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
$validationData = array_merge($user, $payload);
$rules = [
'firstname' => 'required|min:2|max:100',
'lastname' => 'required|min:2|max:100',
'email' => 'required|email|unique:users,email,' . $id,
'state' => 'nullable|max:10',
'zip' => 'nullable|max:20',
'cellphone' => 'nullable|max:50',
'failed_attempts' => 'nullable|integer',
'gender' => 'nullable|in:male,female,MALE,FEMALE',
];
$errors = $this->validateRequest($validationData, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$bool = static function ($value, $default = 0) {
if ($value === null) {
return $default;
}
return in_array($value, [true, 1, '1', 'true', 'on'], true) ? 1 : 0;
};
$parseDateTime = static function ($value) {
if ($value === null || $value === '') {
return null;
}
$timestamp = strtotime((string) $value);
return $timestamp ? date('Y-m-d H:i:s', $timestamp) : null;
};
$data = [
'id' => $id,
'account_id' => trim((string) ($payload['account_id'] ?? $user['account_id'] ?? '')) ?: null,
'lastname' => trim((string) ($payload['lastname'] ?? $user['lastname'] ?? '')),
'firstname' => trim((string) ($payload['firstname'] ?? $user['firstname'] ?? '')),
'gender' => trim((string) ($payload['gender'] ?? $user['gender'] ?? '')) ?: null,
'cellphone' => trim((string) ($payload['cellphone'] ?? $user['cellphone'] ?? '')) ?: null,
'email' => strtolower(trim((string) ($payload['email'] ?? $user['email'] ?? ''))),
'address_street' => trim((string) ($payload['address_street'] ?? $user['address_street'] ?? '')) ?: null,
'apt' => trim((string) ($payload['apt'] ?? $user['apt'] ?? '')) ?: null,
'city' => trim((string) ($payload['city'] ?? $user['city'] ?? '')) ?: null,
'state' => strtoupper(trim((string) ($payload['state'] ?? $user['state'] ?? ''))) ?: null,
'zip' => trim((string) ($payload['zip'] ?? $user['zip'] ?? '')) ?: null,
'accept_school_policy' => $bool($payload['accept_school_policy'] ?? $user['accept_school_policy'] ?? 0, (int) ($user['accept_school_policy'] ?? 0)),
'user_type' => trim((string) ($payload['user_type'] ?? $user['user_type'] ?? '')) ?: null,
'rfid_tag' => trim((string) ($payload['rfid_tag'] ?? $user['rfid_tag'] ?? '')) ?: null,
'school_id' => trim((string) ($payload['school_id'] ?? $user['school_id'] ?? '')) ?: null,
'failed_attempts' => ($payload['failed_attempts'] ?? '') === '' ? null : (int) $payload['failed_attempts'],
'last_failed_at' => $parseDateTime($payload['last_failed_at'] ?? $user['last_failed_at'] ?? null),
'semester' => trim((string) ($payload['semester'] ?? $user['semester'] ?? '')) ?: null,
'school_year' => trim((string) ($payload['school_year'] ?? $user['school_year'] ?? '')) ?: null,
'status' => trim((string) ($payload['status'] ?? $user['status'] ?? '')) ?: null,
'is_suspended' => $bool($payload['is_suspended'] ?? $user['is_suspended'] ?? 0, (int) ($user['is_suspended'] ?? 0)),
'is_verified' => $bool($payload['is_verified'] ?? $user['is_verified'] ?? 0, (int) ($user['is_verified'] ?? 0)),
'token' => trim((string) ($payload['token'] ?? $user['token'] ?? '')) ?: null,
'updated_at' => $parseDateTime($payload['updated_at'] ?? null) ?? $user['updated_at'],
'created_at' => $parseDateTime($payload['created_at'] ?? null) ?? $user['created_at'],
];
if (!$this->user->save($data)) {
return $this->respondError('Failed to update user', Response::HTTP_INTERNAL_SERVER_ERROR);
}
$updated = $this->user->find($id);
unset($updated['password'], $updated['token']);
return $this->respondSuccess($updated, 'User updated successfully');
}
public function profile(): JsonResponse
{
$user = $this->getCurrentUser();
if (!$user) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
$userData = $this->user->find($user->id);
if (!$userData) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
$roles = $this->fetchUserRoles($user->id);
unset($userData['password'], $userData['token']);
$userData['roles'] = $roles;
return $this->success($userData, 'Profile retrieved successfully');
}
public function profileAuth(): JsonResponse
{
$data = $this->payloadData();
if (empty($data['email']) || empty($data['password'])) {
return $this->respondError('Email and password are required', Response::HTTP_BAD_REQUEST);
}
$user = $this->user->where('email', $data['email'])->first();
if (!$user || !$this->verifyPassword($data['password'], $user['password'] ?? '')) {
return $this->respondError('Invalid credentials', Response::HTTP_UNAUTHORIZED);
}
$roles = $this->fetchUserRoles($user['id']);
$tokenPayload = $this->buildLoginPayload($user, $roles);
Session::put([
'user_id' => (int) $user['id'],
'roles' => $roles,
]);
return $this->success($tokenPayload, 'Authenticated successfully');
}
public function updateProfile(): JsonResponse
{
$user = $this->getCurrentUser();
if (!$user) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
$data = $this->payloadData();
if (empty($data)) {
return $this->respondError('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$allowedFields = [
'firstname',
'lastname',
'cellphone',
'address_street',
'apt',
'city',
'state',
'zip',
'gender',
];
$updateData = [];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $data)) {
$updateData[$field] = $data[$field];
}
}
$userRecord = $this->user->find($user->id);
if (!$userRecord) {
return $this->respondError('User not found', Response::HTTP_NOT_FOUND);
}
$emailChanging = isset($data['email']) && $data['email'] !== $userRecord['email'];
$passwordChanging = isset($data['password']) && trim((string) $data['password']) !== '';
if ($emailChanging) {
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
return $this->respondError('Invalid email format', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$existing = $this->user->newQuery()
->where('email', $data['email'])
->where('id', '!=', $user->id)
->first();
if ($existing) {
return $this->respondError('Email already in use', Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
if ($emailChanging || $passwordChanging) {
$current = $data['current_password'] ?? '';
if ($current === '' || !$this->verifyPassword($current, $userRecord['password'] ?? '')) {
return $this->respondError('Current password is required to change email or password', Response::HTTP_UNAUTHORIZED);
}
}
if ($emailChanging) {
$updateData['email'] = $data['email'];
}
if ($passwordChanging) {
$updateData['password'] = $this->hashPassword($data['password']);
}
if (empty($updateData)) {
return $this->respondError('No valid fields to update', Response::HTTP_BAD_REQUEST);
}
try {
$this->user->where('id', $user->id)->update($updateData);
$updated = $this->user->find($user->id);
unset($updated['password'], $updated['token']);
return $this->success($updated, 'Profile updated successfully');
} catch (\Throwable $e) {
log_message('error', 'Profile update error: ' . $e->getMessage());
return $this->respondError('Failed to update profile', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
public function requestPasswordReset(): JsonResponse
{
$data = $this->payloadData();
$email = strtolower(trim((string) ($data['email'] ?? '')));
if ($email === '') {
return $this->respondValidationError(['email' => ['Email is required.']]);
}
$ip = $this->laravelRequest->ip();
$now = CarbonImmutable::now();
$recentAttempts = $this->resetRequest->newQuery()
->where('ip_address', $ip)
->where('requested_at', '>=', $now->subDay()->toDateTimeString())
->count();
if ($recentAttempts >= 3) {
log_message('warning', 'Password reset blocked for IP: ' . $ip);
return $this->respondError('Too many reset attempts. Try again later.', Response::HTTP_TOO_MANY_REQUESTS);
}
$this->resetRequest->newQuery()->create([
'ip_address' => $ip,
'requested_at' => $now->toDateTimeString(),
]);
$user = $this->user->where('email', $email)->first();
if (!$user) {
return $this->respondSuccess(null, 'If the email is registered, a reset link will be sent.');
}
if ((int) ($user['is_verified'] ?? 0) === 0) {
return $this->respondError('Please activate your account before resetting the password.', Response::HTTP_BAD_REQUEST);
}
$token = bin2hex(random_bytes(48));
$expiresAt = $now->addHour();
$this->passwordReset->newQuery()->create([
'email' => $email,
'token' => $token,
'created_at' => $now->toDateTimeString(),
'expires_at' => $expiresAt->toDateTimeString(),
]);
$this->sendResetEmail($email, $token);
return $this->respondSuccess(null, 'Password reset link sent if the email exists.');
}
public function validateResetToken(string $token): JsonResponse
{
$now = CarbonImmutable::now()->toDateTimeString();
$entry = $this->passwordReset->newQuery()
->where('token', $token)
->where('expires_at', '>=', $now)
->first();
if (!$entry) {
return $this->respondError('Invalid or expired reset token.', Response::HTTP_BAD_REQUEST);
}
return $this->respondSuccess([
'email' => $entry['email'],
], 'Token valid');
}
public function completePasswordReset(): JsonResponse
{
$data = $this->payloadData();
$rules = [
'token' => 'required',
'password' => 'required|min:8|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?]).{8,}$/',
'password_confirmation' => 'required|same:password',
];
$errors = $this->validateRequest($data, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$now = CarbonImmutable::now()->toDateTimeString();
$resetEntry = $this->passwordReset->newQuery()
->where('token', $data['token'])
->where('expires_at', '>=', $now)
->first();
if (!$resetEntry) {
return $this->respondError('Invalid or expired reset link.', Response::HTTP_BAD_REQUEST);
}
$user = $this->user->where('email', $resetEntry['email'])->first();
if (!$user) {
return $this->respondError('User not found.', Response::HTTP_NOT_FOUND);
}
$hashedPassword = $this->hashPassword($data['password']);
$this->user->update($user['id'], [
'failed_attempts' => 0,
'is_suspended' => 0,
'last_failed_at' => null,
'password' => $hashedPassword,
'status' => 'Active',
]);
$this->passwordReset->newQuery()->where('token', $data['token'])->delete();
$ip = $this->laravelRequest->ip();
$attempt = $this->ipAttempt->newQuery()->where('ip_address', $ip)->first();
if ($attempt) {
$this->ipAttempt->newQuery()->where('ip_address', $ip)->delete();
}
return $this->respondSuccess(null, 'Password has been reset successfully.');
}
public function confirm(string $token): JsonResponse
{
$user = $this->user->where('token', $token)->first();
if (!$user) {
return $this->respondError('Invalid or expired confirmation token.', Response::HTTP_BAD_REQUEST);
}
if ((int) ($user['is_verified'] ?? 0) === 1) {
return $this->respondSuccess([
'user_id' => $user['id'],
'email' => $user['email'],
], 'Account already verified.');
}
return $this->respondSuccess([
'user_id' => $user['id'],
'email' => $user['email'],
], 'Token valid. Please set your password.');
}
public function setPassword(): JsonResponse
{
$data = $this->payloadData();
$rules = [
'user_id' => 'required|integer|exists:users,id',
'token' => 'required',
'password' => 'required|min:8|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\-=\+*#$%&!?]).{8,}$/',
'password_confirm' => 'required|same:password',
];
$errors = $this->validateRequest($data, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$user = $this->user->newQuery()
->where('id', $data['user_id'])
->where('token', $data['token'])
->first();
if (!$user) {
return $this->respondError('Invalid token or user.', Response::HTTP_BAD_REQUEST);
}
$accountId = $user['account_id'] ?? null;
if (!$accountId) {
$accountId = 'ACC' . str_pad((string) $user['id'], 8, '0', STR_PAD_LEFT);
}
$this->user->update($user['id'], [
'password' => $this->hashPassword($data['password']),
'status' => 'Active',
'is_verified' => 1,
'token' => null,
'account_id' => $accountId,
]);
return $this->respondSuccess(null, 'Password set successfully. You may now log in.');
}
public function loginActivity(): JsonResponse
{
$perPage = (int) ($this->laravelRequest->query('per_page', 25));
$page = (int) ($this->laravelRequest->query('page', 1));
return $this->respondSuccess(
$this->buildLoginActivityPayload($perPage, $page),
'Login activity retrieved'
);
}
protected function buildUsersWithRoles(): array
{
$users = $this->user->newQuery()->get()->map(fn($user) => (array) $user)->toArray();
if (empty($users)) {
return [];
}
$guardianMap = [];
try {
$rows = DB::table('family_guardians')
->select('user_id', DB::raw('MAX(is_primary) as max_primary'), DB::raw('COUNT(*) as cnt'))
->groupBy('user_id')
->get();
foreach ($rows as $row) {
$uid = (int) ($row->user_id ?? 0);
if ($uid <= 0) {
continue;
}
$guardianMap[$uid] = ((int) ($row->max_primary ?? 0)) > 0 ? 'Primary' : 'Secondary';
}
} catch (\Throwable $e) {
// legacy table missing; ignore
}
$secondByUserId = [];
$secondByEmail = [];
try {
$uids = DB::table('parents')
->select('secondparent_user_id as uid')
->where('secondparent_user_id', '!=', 0)
->groupBy('secondparent_user_id')
->get();
foreach ($uids as $row) {
$u = (int) ($row->uid ?? 0);
if ($u > 0) {
$secondByUserId[$u] = true;
}
}
$emails = DB::table('parents')
->select(DB::raw('LOWER(TRIM(secondparent_email)) as email'))
->where('secondparent_email', '!=', '')
->groupBy('secondparent_email')
->get();
foreach ($emails as $row) {
$em = trim(strtolower((string) ($row->email ?? '')));
if ($em !== '') {
$secondByEmail[$em] = true;
}
}
} catch (\Throwable $e) {
// ignore legacy table issues
}
$result = [];
foreach ($users as $user) {
if (!is_array($user)) {
continue;
}
$roles = $this->userRole->where('user_id', $user['id'] ?? 0)->findAll() ?? [];
$roleNames = [];
$roleIds = [];
foreach ($roles as $userRole) {
if (!is_array($userRole) || empty($userRole['role_id'])) {
continue;
}
$role = $this->role->find($userRole['role_id']);
if ($role) {
$roleIds[] = (int) $role['id'];
$roleNames[] = $role['name'];
}
}
$email = trim(strtolower((string) ($user['email'] ?? '')));
$uid = (int) ($user['id'] ?? 0);
$isSecond = ($uid > 0 && !empty($secondByUserId[$uid])) || ($email !== '' && !empty($secondByEmail[$email]));
$result[] = [
'user' => $user,
'roles' => $roleNames,
'role_ids' => $roleIds,
'parent_type' => $guardianMap[$uid] ?? '',
'second_from_parents' => $isSecond ? 'Yes' : '',
];
}
return $result;
}
protected function sendResetEmail(string $email, string $token): void
{
$resetLink = url('/reset-password?token=' . urlencode($token));
try {
$html = view('emails/reset_password', ['resetLink' => $resetLink])->render();
} catch (\Throwable $e) {
$html = '<p>Click the link below to reset your password:</p><p><a href="' . $resetLink . '">' . $resetLink . '</a></p>';
}
$this->emailService->send($email, 'Password Reset Request', $html, 'general');
}
protected function fetchUserRoles(int $userId): array
{
return $this->userRole->newQuery()
->select('roles.name')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->where('user_roles.user_id', $userId)
->pluck('name')
->toArray();
}
protected function buildLoginPayload(array $user, array $roles): array
{
$now = time();
$exp = $now + 60 * 60 * 24;
$payload = [
'sub' => (int) $user['id'],
'name' => trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')),
'roles' => $roles,
'iat' => $now,
'exp' => $exp,
];
$token = $this->encodeJwt($payload);
$rolesMap = [];
foreach ($roles as $role) {
$rolesMap[$role] = true;
}
return [
'token' => $token,
'user' => [
'id' => (int) $user['id'],
'name' => $payload['name'],
'email' => $user['email'],
'roles' => (object) $rolesMap,
],
];
}
protected function encodeJwt(array $payload): string
{
$header = ['alg' => 'HS256', 'typ' => 'JWT'];
$secret = env('JWT_SECRET', 'change-me-in-env');
$segments = [
$this->base64UrlEncode(json_encode($header)),
$this->base64UrlEncode(json_encode($payload)),
];
$signature = hash_hmac('sha256', implode('.', $segments), $secret, true);
$segments[] = $this->base64UrlEncode($signature);
return implode('.', $segments);
}
protected function base64UrlEncode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
protected function verifyPassword(string $password, string $storedHash): bool
{
$delimiter = str_contains($storedHash, '$') ? '$' : ':';
$parts = explode($delimiter, $storedHash);
if (count($parts) !== 4) {
return false;
}
[$algo, $iterations, $salt, $hash] = $parts;
$calc = hash_pbkdf2($algo, $password, $salt, (int) $iterations, strlen($hash));
return hash_equals($hash, $calc);
}
protected function hashPassword(string $password): string
{
$algo = 'sha256';
$iterations = 200000;
$salt = bin2hex(random_bytes(16));
$hash = hash_pbkdf2($algo, $password, $salt, $iterations, 64);
return implode(':', [$algo, $iterations, $salt, $hash]);
}
protected function buildLoginActivityPayload(int $perPage, int $page): array
{
$perPage = max(1, min($perPage, 200));
$page = max(1, $page);
$query = $this->loginActivity->newQuery()->orderBy('login_time', 'DESC');
$total = (int) $query->count();
$activities = $query->forPage($page, $perPage)->get()->map(fn($row) => (array) $row)->toArray();
$normalized = array_map(static function ($activity) {
return [
'user_id' => $activity['user_id'] ?? null,
'email' => $activity['email'] ?? null,
'login_time' => $activity['login_time'] ?? null,
'logout_time' => $activity['logout_time'] ?? null,
'ip_address' => $activity['ip_address'] ?? null,
'user_agent' => $activity['user_agent'] ?? null,
];
}, $activities);
$pageCount = (int) max(1, ceil($total / $perPage));
return [
'activities' => $normalized,
'pagination' => [
'total' => $total,
'perPage' => $perPage,
'currentPage' => $page,
'pageCount' => $pageCount,
'hasNext' => $page < $pageCount,
'hasPrevious' => $page > 1,
],
];
}
/**
* POST /api/v1/users/select-role
* Select and update user's role in database
*/
public function selectRole(): JsonResponse
{
$user = $this->getCurrentUser();
if (!$user) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
$data = $this->payloadData();
$roleKey = (string) ($data['role'] ?? '');
if ($roleKey === '') {
return $this->respondError('Role is required', Response::HTTP_BAD_REQUEST);
}
log_message('info', 'Processing setRole form submission for user: ' . $user->id);
log_message('info', 'Role selected: ' . $roleKey);
$route = $this->role->getRouteByNameOrSlug($roleKey);
if ($route === null) {
log_message('error', 'Invalid or inactive role selected: ' . $roleKey);
return $this->respondError('Invalid role selected.', Response::HTTP_BAD_REQUEST);
}
try {
// Update the user's role field in the database
$this->user->update($user->id, ['role' => $roleKey]);
log_message('info', 'Role updated in database for user: ' . $user->id);
log_message('info', 'Redirecting to role dashboard: ' . $route);
return $this->success([
'role' => $roleKey,
'route' => $route,
], 'Role selected successfully');
} catch (\Throwable $e) {
log_message('error', 'Role selection error: ' . $e->getMessage());
return $this->respondError('Failed to select role', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* DELETE /api/v1/users/roles/{roleId}
* Delete a role and reassign users to Guest role
*/
public function deleteRole(int $roleId): JsonResponse
{
$role = $this->role->find($roleId);
if (!$role) {
return $this->respondError('Role not found', Response::HTTP_NOT_FOUND);
}
// Fetch the "Guest" role
$guestRole = $this->role->where('name', 'Guest')->first();
if (!$guestRole) {
return $this->respondError('Guest role not found', Response::HTTP_NOT_FOUND);
}
DB::beginTransaction();
try {
// Update users with the deleted role to have the "Guest" role
$this->user->newQuery()
->where('role_id', $roleId)
->update([
'role_id' => $guestRole['id'],
'role' => 'Guest',
'status' => 'Inactive',
]);
// Also update user_roles table
$this->userRole->newQuery()
->where('role_id', $roleId)
->update(['role_id' => $guestRole['id']]);
// Delete the role
$this->role->delete($roleId);
DB::commit();
log_message('info', 'Role deleted: ' . $roleId . ', users reassigned to Guest role');
return $this->respondSuccess(null, 'Role deleted successfully. Users with this role have been assigned the Guest role.');
} catch (\Throwable $e) {
DB::rollBack();
log_message('error', 'Failed to delete role: ' . $e->getMessage());
return $this->respondError('Failed to delete role', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}