// SPDX-License-Identifier: AGPL-3.0-or-later namespace OCA\Forum\Controller; use OCA\Forum\Attribute\RequirePermission; use OCA\Forum\Db\CategoryPerm; use OCA\Forum\Db\CategoryPermMapper; use OCA\Forum\Db\Role; use OCA\Forum\Db\RoleMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; use Psr\Log\LoggerInterface; class RoleController extends OCSController { public function __construct( string $appName, IRequest $request, private RoleMapper $roleMapper, private CategoryPermMapper $categoryPermMapper, private LoggerInterface $logger, ) { parent::__construct($appName, $request); } /** * Get all roles * * @param int<1, 100> $limit Maximum number of roles to return * @param int<0, max> $offset Offset for pagination * @return DataResponse>, array{}> * * 200: Roles returned */ #[NoAdminRequired] #[RequirePermission('canManageUsers', orGroup: 'access')] #[RequirePermission('canEditRoles', orGroup: 'access')] #[ApiRoute(verb: 'GET', url: '/api/roles')] public function index(int $limit = 100, int $offset = 0): DataResponse { try { $roles = array_slice($this->roleMapper->findAll(), $offset, $limit); return new DataResponse(array_map(fn ($role) => $role->jsonSerialize(), $roles)); } catch (\Exception $e) { $this->logger->error('Error fetching roles: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to fetch roles'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Get a single role * * @param int $id Role ID * @return DataResponse, array{}> * * 200: Role returned */ #[NoAdminRequired] #[RequirePermission('canManageUsers', orGroup: 'access')] #[RequirePermission('canEditRoles', orGroup: 'access')] #[ApiRoute(verb: 'GET', url: '/api/roles/{id}')] public function show(int $id): DataResponse { try { $role = $this->roleMapper->find($id); return new DataResponse($role->jsonSerialize()); } catch (DoesNotExistException $e) { return new DataResponse(['error' => 'Role not found'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { $this->logger->error('Error fetching role: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to fetch role'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Create a new role * * @param string $name Role name * @param string|null $description Role description * @param string|null $colorLight Light mode color * @param string|null $colorDark Dark mode color * @param bool $canAccessAdminTools Can access admin tools * @param bool $canManageUsers Can manage users * @param bool $canEditRoles Can edit roles * @param bool $canEditCategories Can edit categories * @param bool $canEditBbcodes Can edit BBCodes * @param bool $canAccessModeration Can access moderation tools * @return DataResponse, array{}> * * 201: Role created */ #[NoAdminRequired] #[RequirePermission('canEditRoles')] #[ApiRoute(verb: 'POST', url: '/api/roles')] public function create( string $name, ?string $description = null, ?string $colorLight = null, ?string $colorDark = null, bool $canAccessAdminTools = false, bool $canManageUsers = false, bool $canEditRoles = false, bool $canEditCategories = false, bool $canEditBbcodes = false, bool $canAccessModeration = false, ): DataResponse { try { $role = new \OCA\Forum\Db\Role(); $role->setName($name); $role->setDescription($description); $role->setColorLight($colorLight); $role->setColorDark($colorDark); $role->setCanAccessAdminTools($canAccessAdminTools); $role->setCanManageUsers($canManageUsers); $role->setCanEditRoles($canEditRoles); $role->setCanEditCategories($canEditCategories); $role->setCanEditBbcodes($canEditBbcodes); $role->setCanAccessModeration($canAccessModeration); $role->setCreatedAt(time()); /** @var \OCA\Forum\Db\Role */ $createdRole = $this->roleMapper->insert($role); return new DataResponse($createdRole->jsonSerialize(), Http::STATUS_CREATED); } catch (\Exception $e) { $this->logger->error('Error creating role: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to create role'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Update a role * * @param int $id Role ID * @param string|null $name Role name * @param string|null $description Role description * @param string|null $colorLight Light mode color * @param string|null $colorDark Dark mode color * @param bool|null $canAccessAdminTools Can access admin tools * @param bool|null $canManageUsers Can manage users * @param bool|null $canEditRoles Can edit roles * @param bool|null $canEditCategories Can edit categories * @param bool|null $canEditBbcodes Can edit BBCodes * @param bool|null $canAccessModeration Can access moderation tools * @return DataResponse, array{}> * * 200: Role updated */ #[NoAdminRequired] #[RequirePermission('canEditRoles')] #[ApiRoute(verb: 'PUT', url: '/api/roles/{id}')] public function update( int $id, ?string $name = null, ?string $description = null, ?string $colorLight = null, ?string $colorDark = null, ?bool $canAccessAdminTools = null, ?bool $canManageUsers = null, ?bool $canEditRoles = null, ?bool $canEditCategories = null, ?bool $canEditBbcodes = null, ?bool $canAccessModeration = null, ): DataResponse { try { $role = $this->roleMapper->find($id); if ($name !== null) { $role->setName($name); } if ($description !== null) { $role->setDescription($description); } if ($colorLight !== null) { $role->setColorLight($colorLight); } if ($colorDark !== null) { $role->setColorDark($colorDark); } // Coerced permission values per role type (overrides any input) $coercedPermissions = match ($role->getRoleType()) { Role::ROLE_TYPE_ADMIN => true, // Admin: all permissions forced to true Role::ROLE_TYPE_GUEST, Role::ROLE_TYPE_DEFAULT => false, // Guest/Default: all permissions forced to false default => null, // Others: use provided values }; $permissionFields = [ 'canAccessAdminTools' => $canAccessAdminTools, 'canManageUsers' => $canManageUsers, 'canEditRoles' => $canEditRoles, 'canEditCategories' => $canEditCategories, 'canEditBbcodes' => $canEditBbcodes, 'canAccessModeration' => $canAccessModeration, ]; foreach ($permissionFields as $field => $value) { $setter = 'set' . ucfirst($field); if ($coercedPermissions !== null) { $role->$setter($coercedPermissions); } elseif ($value !== null) { $role->$setter($value); } } /** @var \OCA\Forum\Db\Role */ $updatedRole = $this->roleMapper->update($role); return new DataResponse($updatedRole->jsonSerialize()); } catch (DoesNotExistException $e) { return new DataResponse(['error' => 'Role not found'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { $this->logger->error('Error updating role: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to update role'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Delete a role * * @param int $id Role ID * @return DataResponse|DataResponse * * 200: Role deleted * 403: Cannot delete system roles */ #[NoAdminRequired] #[RequirePermission('canEditRoles')] #[ApiRoute(verb: 'DELETE', url: '/api/roles/{id}')] public function destroy(int $id): DataResponse { try { $role = $this->roleMapper->find($id); // Prevent deleting system roles (Admin, Moderator, User) if ($role->getIsSystemRole()) { return new DataResponse(['error' => 'System roles cannot be deleted'], Http::STATUS_FORBIDDEN); } // Delete associated permissions $this->categoryPermMapper->deleteByRoleId($id); $this->roleMapper->delete($role); return new DataResponse(['success' => true]); } catch (DoesNotExistException $e) { return new DataResponse(['error' => 'Role not found'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { $this->logger->error('Error deleting role: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to delete role'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Get permissions for a role * * @param int $id Role ID * @param int<1, 100> $limit Maximum number of permissions to return * @param int<0, max> $offset Offset for pagination * @return DataResponse>, array{}> * * 200: Permissions returned */ #[NoAdminRequired] #[RequirePermission('canEditRoles')] #[ApiRoute(verb: 'GET', url: '/api/roles/{id}/permissions')] public function getPermissions(int $id, int $limit = 100, int $offset = 0): DataResponse { try { $permissions = array_slice($this->categoryPermMapper->findByRoleId($id), $offset, $limit); return new DataResponse(array_map(fn ($perm) => $perm->jsonSerialize(), $permissions)); } catch (\Exception $e) { $this->logger->error('Error fetching role permissions: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to fetch permissions'], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Update permissions for a role * * @param int $id Role ID * @param list $permissions Permissions array * @return DataResponse * * 200: Permissions updated */ #[NoAdminRequired] #[RequirePermission('canEditRoles')] #[ApiRoute(verb: 'POST', url: '/api/roles/{id}/permissions')] public function updatePermissions(int $id, array $permissions): DataResponse { try { // Verify role exists and get role type $role = $this->roleMapper->find($id); // Delete existing permissions for this role $this->categoryPermMapper->deleteByRoleId($id); // Insert new permissions foreach ($permissions as $perm) { $categoryPerm = new CategoryPerm(); $categoryPerm->setCategoryId($perm['categoryId']); $categoryPerm->setTargetType(CategoryPerm::TARGET_TYPE_ROLE); $categoryPerm->setTargetId((string)$id); $categoryPerm->setCanView($perm['canView'] ?? false); $categoryPerm->setCanPost($perm['canPost'] ?? $perm['canView'] ?? false); $categoryPerm->setCanReply($perm['canReply'] ?? $perm['canPost'] ?? $perm['canView'] ?? false); // Guest and Default roles never have moderate permission $categoryPerm->setCanModerate($role->isModeratorRestricted() ? false : ($perm['canModerate'] ?? false)); $this->categoryPermMapper->insert($categoryPerm); } return new DataResponse(['success' => true]); } catch (DoesNotExistException $e) { return new DataResponse(['error' => 'Role not found'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { $this->logger->error('Error updating role permissions: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to update permissions'], Http::STATUS_INTERNAL_SERVER_ERROR); } } }