Files
alrahma_sunday_school_api/app/Http/Controllers/Api/RolePermissionController.php
T
2026-03-05 12:29:37 -05:00

728 lines
25 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Api;
use App\Models\Configuration;
use App\Models\Permission;
use App\Models\Role;
use App\Models\RolePermission;
use App\Models\Staff;
use App\Models\User;
use App\Models\UserRole;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\Response;
class RolePermissionController extends BaseApiController
{
protected Role $role;
protected Permission $permission;
protected RolePermission $rolePermission;
protected User $user;
protected UserRole $userRole;
protected Staff $staff;
protected Configuration $config;
protected string $schoolYear;
protected string $semester;
public function __construct()
{
parent::__construct();
$this->role = model(Role::class);
$this->permission = model(Permission::class);
$this->rolePermission = model(RolePermission::class);
$this->user = model(User::class);
$this->userRole = model(UserRole::class);
$this->staff = model(Staff::class);
$this->config = model(Configuration::class);
$this->schoolYear = (string) ($this->config->getConfig('school_year') ?? '');
$this->semester = (string) ($this->config->getConfig('semester') ?? '');
}
/**
* GET /api/v1/rolepermission/roles
* List all roles with user counts
*/
public function roles()
{
$rows = DB::table('roles')
->select('roles.id', 'roles.name', 'roles.description', DB::raw('COUNT(DISTINCT user_roles.user_id) AS user_count'))
->leftJoin('user_roles', 'user_roles.role_id', '=', 'roles.id')
->groupBy('roles.id', 'roles.name', 'roles.description')
->orderBy('roles.name', 'ASC')
->get()
->toArray();
$normalized = array_map(static function ($row) {
$row = (array) $row;
if (!is_array($row)) {
return [];
}
return [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'description' => $row['description'] ?? null,
'user_count' => (int) ($row['user_count'] ?? 0),
];
}, $rows ?? []);
return $this->success(['roles' => $normalized], 'Roles retrieved successfully');
}
/**
* GET /api/v1/rolepermission/roles/{id}
* Get a single role
*/
public function showRole($id = null)
{
$role = $this->role->find($id);
if (!$role) {
return $this->error('Role not found', Response::HTTP_NOT_FOUND);
}
return $this->success($role, 'Role retrieved successfully');
}
/**
* POST /api/v1/rolepermission/roles
* Create a new role
*/
public function storeRole()
{
$payload = $this->payloadData();
if (empty($payload)) {
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$rules = [
'name' => 'required|min:3|max:255|unique:roles,name',
'slug' => 'nullable|max:64|unique:roles,slug',
'description' => 'nullable|max:500',
'dashboard_route' => ['required', 'min:1', 'max:255', 'regex:/^[a-z0-9_\/\-]+$/'],
'priority' => 'required|integer|min:0|max:100000',
'is_active' => 'required|in:0,1',
];
$errors = $this->validateRequest($payload, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$rawName = trim((string) ($payload['name'] ?? ''));
$rawSlug = trim((string) ($payload['slug'] ?? ''));
$route = trim((string) ($payload['dashboard_route'] ?? ''));
$desc = trim((string) ($payload['description'] ?? ''));
$priority = (int) ($payload['priority'] ?? 0);
$isActive = (string) ($payload['is_active'] ?? '0') === '1' ? 1 : 0;
$name = strtolower($rawName);
$dashboard_route = strtolower($route);
$description = strtolower($desc);
// Build slug: from input if provided, else from name
$slug = $rawSlug !== '' ? $rawSlug : $name;
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug));
$slug = trim($slug, '_');
$slug = $this->ensureUniqueSlug($slug);
$data = [
'name' => $name,
'slug' => $slug,
'description' => $description,
'dashboard_route' => $dashboard_route,
'priority' => $priority,
'is_active' => $isActive,
'created_at' => utc_now(),
'updated_at' => utc_now(),
];
try {
$role = $this->role->create($data);
return $this->success($role->toArray(), 'Role created successfully', Response::HTTP_CREATED);
} catch (\Throwable $e) {
log_message('error', 'Role creation error: ' . $e->getMessage());
return $this->respondError('Failed to create role', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* PATCH /api/v1/rolepermission/roles/{id}
* Update a role
*/
public function updateRole($id = null)
{
$role = $this->role->find($id);
if (!$role) {
return $this->error('Role not found', Response::HTTP_NOT_FOUND);
}
$payload = $this->payloadData();
if (empty($payload)) {
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$rules = [
'name' => ['required', 'min:3', 'max:255', Rule::unique('roles', 'name')->ignore($id)],
'slug' => ['nullable', 'max:64', Rule::unique('roles', 'slug')->ignore($id)],
'description' => 'nullable|max:500',
'dashboard_route' => ['required', 'min:1', 'max:255', 'regex:/^[a-z0-9_\/\-]+$/'],
'priority' => 'required|integer|min:0|max:100000',
'is_active' => 'required|in:0,1',
];
$errors = $this->validateRequest($payload, $rules);
if (!empty($errors)) {
return $this->respondValidationError($errors);
}
$rawName = trim((string) ($payload['name'] ?? ''));
$rawSlug = trim((string) ($payload['slug'] ?? ''));
$route = trim((string) ($payload['dashboard_route'] ?? ''));
$desc = trim((string) ($payload['description'] ?? ''));
$priority = (int) ($payload['priority'] ?? 0);
$isActive = (string) ($payload['is_active'] ?? '0') === '1' ? 1 : 0;
$name = strtolower($rawName);
$dashboard_route = strtolower($route);
$description = strtolower($desc);
// slug: if blank, derive from name; else normalize
$slug = $rawSlug !== '' ? $rawSlug : $name;
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug));
$slug = trim($slug, '_');
// Ensure unique slug if changed
if ($slug !== $role['slug']) {
$slug = $this->ensureUniqueSlug($slug, (int) $id);
}
$data = [
'name' => $name,
'slug' => $slug,
'description' => $description,
'dashboard_route' => $dashboard_route,
'priority' => $priority,
'is_active' => $isActive,
'updated_at' => utc_now(),
];
try {
$this->role->update($id, $data);
$updatedRole = $this->role->find($id);
return $this->success($updatedRole, 'Role updated successfully');
} catch (\Throwable $e) {
log_message('error', 'Role update error: ' . $e->getMessage());
return $this->respondError('Failed to update role', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* DELETE /api/v1/rolepermission/roles/{id}
* Delete a role
*/
public function deleteRole($id = null)
{
$role = $this->role->find($id);
if (!$role) {
return $this->error('Role not found', Response::HTTP_NOT_FOUND);
}
try {
$this->role->delete($id);
return $this->success(null, 'Role deleted successfully');
} catch (\Throwable $e) {
log_message('error', 'Role deletion error: ' . $e->getMessage());
return $this->respondError('Failed to delete role', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/rolepermission/permissions
* List all permissions
*/
public function permissions()
{
$rows = $this->permission
->select('id, name, description, created_at, updated_at')
->orderBy('id', 'ASC')
->findAll();
$normalized = array_map(static function ($row) {
if (!is_array($row)) {
return [];
}
return [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'description' => $row['description'] ?? null,
'created_at' => $row['created_at'] ?? null,
'updated_at' => $row['updated_at'] ?? null,
];
}, $rows ?? []);
return $this->success(['permissions' => $normalized], 'Permissions retrieved successfully');
}
/**
* POST /api/v1/rolepermission/permissions
* Create a new permission
*/
public function storePermission()
{
$payload = $this->payloadData();
if (empty($payload)) {
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$name = trim((string) ($payload['name'] ?? ''));
if ($name === '') {
return $this->respondError('Permission name is required', Response::HTTP_BAD_REQUEST);
}
// Check existence
$existing = $this->permission->where('name', $name)->first();
if ($existing) {
return $this->success($existing, 'Permission already exists. No changes made.');
}
$data = [
'name' => $name,
'description' => trim((string) ($payload['description'] ?? '')),
];
try {
$permission = $this->permission->create($data);
return $this->success($permission->toArray(), 'Permission created successfully', Response::HTTP_CREATED);
} catch (\Throwable $e) {
log_message('error', 'Permission creation error: ' . $e->getMessage());
return $this->respondError('Failed to create permission', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* PATCH /api/v1/rolepermission/permissions/{id}
* Update a permission
*/
public function updatePermission($id = null)
{
$permission = $this->permission->find($id);
if (!$permission) {
return $this->error('Permission not found', Response::HTTP_NOT_FOUND);
}
$payload = $this->payloadData();
if (empty($payload)) {
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
}
$data = [
'id' => $id,
'name' => trim((string) ($payload['name'] ?? '')),
'description' => trim((string) ($payload['description'] ?? '')),
];
try {
$this->permission->update($id, $data);
$updatedPermission = $this->permission->find($id);
return $this->success($updatedPermission, 'Permission updated successfully');
} catch (\Throwable $e) {
log_message('error', 'Permission update error: ' . $e->getMessage());
return $this->respondError('Failed to update permission', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* DELETE /api/v1/rolepermission/permissions/{id}
* Delete a permission
*/
public function deletePermission($id = null)
{
$permission = $this->permission->find($id);
if (!$permission) {
return $this->error('Permission not found', Response::HTTP_NOT_FOUND);
}
try {
$this->permission->delete($id);
return $this->success(null, 'Permission deleted successfully');
} catch (\Throwable $e) {
log_message('error', 'Permission deletion error: ' . $e->getMessage());
return $this->respondError('Failed to delete permission', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/rolepermission/roles/{roleId}/permissions
* Get permissions for a specific role
*/
public function rolePermissions($roleId = null)
{
$role = $this->role->find($roleId);
if (!$role) {
return $this->error('Role not found', Response::HTTP_NOT_FOUND);
}
$permissions = $this->rolePermission
->where('role_id', $roleId)
->findAll();
// Get all permissions with role permission data
$allPermissions = $this->permission->findAll();
$existingPermissions = [];
foreach ($permissions as $rolePermission) {
$existingPermissions[$rolePermission['permission_id']] = [
'can_create' => $rolePermission['can_create'],
'can_read' => $rolePermission['can_read'],
'can_update' => $rolePermission['can_update'],
'can_delete' => $rolePermission['can_delete'],
];
}
return $this->success([
'role' => $role,
'permissions' => $allPermissions,
'role_permissions' => $existingPermissions,
], 'Role permissions retrieved successfully');
}
/**
* POST /api/v1/rolepermission/roles/{roleId}/permissions
* Save role permissions
*/
public function saveRolePermissions($roleId = null)
{
$role = $this->role->find($roleId);
if (!$role) {
return $this->error('Role not found', Response::HTTP_NOT_FOUND);
}
$payload = $this->payloadData();
$permissions = $payload['permissions'] ?? null;
if (!$permissions || !is_array($permissions)) {
return $this->respondError('Permissions data is required', Response::HTTP_BAD_REQUEST);
}
// Fetch existing permissions for the role
$rolePermissions = $this->rolePermission->where('role_id', $roleId)->findAll();
$existingPermissions = [];
foreach ($rolePermissions as $rolePermission) {
$existingPermissions[$rolePermission['permission_id']] = $rolePermission;
}
try {
DB::beginTransaction();
// Process the new permissions
foreach ($permissions as $permissionId => $crudValues) {
$permissionData = [
'can_create' => isset($crudValues['create']) ? 1 : 0,
'can_read' => isset($crudValues['read']) ? 1 : 0,
'can_update' => isset($crudValues['update']) ? 1 : 0,
'can_delete' => isset($crudValues['delete']) ? 1 : 0,
'updated_at' => utc_now(),
];
if (isset($existingPermissions[$permissionId])) {
// Update existing permission
$this->rolePermission->where('role_id', $roleId)
->where('permission_id', $permissionId)
->update($permissionData);
} else {
// Insert new permission
$permissionData['role_id'] = $roleId;
$permissionData['permission_id'] = $permissionId;
$permissionData['created_at'] = utc_now();
$this->rolePermission->insert($permissionData);
}
}
// Remove permissions that are no longer present
foreach ($existingPermissions as $permissionId => $permissionData) {
if (!isset($permissions[$permissionId])) {
$this->rolePermission->where('role_id', $roleId)
->where('permission_id', $permissionId)
->delete();
}
}
DB::commit();
return $this->success(null, 'Permissions saved successfully');
} catch (\Throwable $e) {
DB::rollBack();
log_message('error', 'Failed to save permissions: ' . $e->getMessage());
return $this->respondError('Failed to save permissions: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/rolepermission/users
* Get users with their roles
*/
public function users()
{
$users = $this->buildUsersWithRolesPayload();
return $this->success($users, 'Users with roles retrieved successfully');
}
/**
* POST /api/v1/rolepermission/users/{userId}/roles
* Assign roles to a user
*/
public function assignRole($userId = null)
{
$user = $this->user->find($userId);
if (!$user) {
return $this->error('User not found', Response::HTTP_NOT_FOUND);
}
$payload = $this->payloadData();
$roleIdsRaw = $payload['role_ids'] ?? [];
$roleIdsArray = is_array($roleIdsRaw) ? $roleIdsRaw : ($roleIdsRaw !== null ? [$roleIdsRaw] : []);
$roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIdsArray), fn($id) => $id > 0)));
$adminId = $this->getCurrentUserId();
if (!$adminId) {
return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED);
}
try {
DB::beginTransaction();
// Remove all existing roles for the user
$this->userRole->where('user_id', $userId)->delete();
// Assign new roles
if (!empty($roleIds)) {
foreach ($roleIds as $roleId) {
$this->userRole->insert([
'user_id' => $userId,
'role_id' => $roleId,
'created_at' => utc_now(),
'updated_by' => $adminId
]);
}
}
// Load role names after saving
$assignedRoles = !empty($roleIds)
? $this->role->whereIn('id', $roleIds)->findAll()
: [];
$roleNames = array_map(fn($r) => strtolower($r['name']), $assignedRoles ?? []);
// Update staff record if needed
$this->updateStaffRecord($userId, $roleNames);
DB::commit();
return $this->success(null, 'Roles updated successfully');
} catch (\Throwable $e) {
DB::rollBack();
log_message('error', 'Error updating roles: ' . $e->getMessage());
return $this->respondError('Failed to update roles: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* GET /api/v1/rolepermission/admins
* Get admin users
*/
public function admins()
{
$admins = $this->user->newQuery()
->select(['users.id', 'users.firstname', 'users.lastname', 'users.email'])
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->where('roles.name', 'admin')
->groupBy('users.id', 'users.firstname', 'users.lastname', 'users.email')
->get()
->toArray();
return $this->success($admins, 'Admin users retrieved successfully');
}
/**
* Helper: Build users with roles payload
*/
private function buildUsersWithRolesPayload(): array
{
$rows = DB::table('users')
->select('users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.account_id', 'roles.name AS role_name')
->leftJoin('user_roles', 'users.id', '=', 'user_roles.user_id')
->leftJoin('roles', 'user_roles.role_id', '=', 'roles.id')
->orderBy('users.id', 'ASC')
->get()
->toArray();
$users = [];
foreach ($rows as $row) {
$row = (array) $row;
if (!is_array($row)) {
continue;
}
$id = (int) ($row['id'] ?? 0);
if ($id <= 0) {
continue;
}
if (!isset($users[$id])) {
$users[$id] = [
'id' => $id,
'firstname' => $row['firstname'] ?? '',
'lastname' => $row['lastname'] ?? '',
'email' => $row['email'] ?? '',
'account_id' => $row['account_id'] ?? '',
'roles' => [],
];
}
$roleName = $row['role_name'] ?? null;
if ($roleName) {
$users[$id]['roles'][] = $roleName;
}
}
$normalized = array_map(static function ($user) {
$roles = array_values(array_unique(array_filter($user['roles'] ?? [], 'strlen')));
return [
'id' => $user['id'],
'firstname' => $user['firstname'] ?? '',
'lastname' => $user['lastname'] ?? '',
'email' => $user['email'] ?? '',
'account_id' => $user['account_id'] ?? '',
'roles' => $roles,
];
}, array_values($users));
return ['users' => $normalized];
}
/**
* Helper: Update staff record based on roles
*/
private function updateStaffRecord(int $userId, array $newRoleNames): void
{
$excludedRoles = ['parent', 'student', 'guest'];
$loweredNewRoles = array_map('strtolower', $newRoleNames);
$user = $this->user->find($userId);
if (!$user) {
log_message('error', "updateStaffRecord: user_id {$userId} not found");
return;
}
// Fetch existing staff record if it exists
$existingStaff = $this->staff->where('user_id', $userId)->first();
// Extract existing roles from DB if any
$existingRoles = [];
if (!empty($existingStaff['role_name'])) {
$existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff['role_name'])));
}
// Merge and deduplicate roles
$allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles));
// Determine staff roles (excluding parent/student/guest)
$staffRoles = array_filter($newRoleNames, fn($role) => !in_array($role, $excludedRoles));
// Determine active role
$activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive';
$email = $this->generateUniqueStaffEmail($user['firstname'], $user['lastname'], $userId);
$now = utc_now();
$row = [
'user_id' => $userId,
'firstname' => $user['firstname'],
'lastname' => $user['lastname'],
'email' => $email,
'phone' => $user['cellphone'] ?? null,
'role_name' => implode(', ', $allRoles),
'active_role' => $activeRole,
'school_year' => $this->schoolYear,
'updated_at' => $now,
];
$this->staff->upsert($row);
}
/**
* Helper: Generate unique staff email
*/
private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string
{
$domain = '@alrahmaisgl.org';
// Sanitize names: lowercase, letters only
$firstname = strtolower(preg_replace('/[^a-z]/i', '', $firstname));
$lastname = strtolower(preg_replace('/[^a-z]/i', '', $lastname));
// Use at least 1 letter from firstname
$min = 1;
$max = strlen($firstname);
$base = '';
$email = '';
for ($i = $min; $i <= $max; $i++) {
$base = substr($firstname, 0, $i) . $lastname;
$email = $base . $domain;
// If email not taken or belongs to same user, use it
$exists = $this->staff->where('email', $email)
->where('user_id !=', $userId)
->first();
if (!$exists) {
return $email;
}
}
// If all combinations used, fallback to adding user ID
return $base . $userId . $domain;
}
/**
* Helper: Ensure slug is unique
*/
private function ensureUniqueSlug(string $baseSlug, ?int $ignoreId = null): string
{
$slug = $baseSlug;
$i = 2;
while (true) {
$builder = $this->role->where('slug', $slug);
if ($ignoreId !== null) {
$builder->where('id !=', $ignoreId);
}
$exists = $builder->countAllResults() > 0;
if (!$exists) {
break;
}
$slug = $baseSlug . '_' . $i;
$i++;
if ($i > 100000) { // safety stop
break;
}
}
return $slug;
}
}