Files
nextcloud-forum/lib/Service/UserRoleService.php
Chen Asraf d2baf2a813 feat: add optional guest access with role-based permissions
Implement opt-in guest access system allowing unauthenticated users to
view forum content with configurable permissions.

Features Added:
  - Guest access toggle in admin settings for forum-wide control
  - Guest role with configurable category-level permissions
(view/post/reply)
  - Role type system (admin, moderator, default, guest, custom) for
enhanced access control
  - Public page support in routing with automatic redirect to login when
disabled
  - Public settings API endpoint for unauthenticated access to forum
metadata
  - Guest role permissions UI in admin panel with clear capability
restrictions
  - Database migration with automatic role type assignment and guest
role seeding

Security & Permission Improvements:
  - Permission middleware now validates permissions on public pages
instead of skipping checks
  - Admin/moderator roles have full access; guest/default roles
restricted from moderation
  - Guest role cannot be assigned to authenticated users

Breaking Changes:
  - Forum title/subtitle moved from system config to app config
(auto-migrated)
  - Permission middleware behavior changed for PublicPage routes (now
checks permissions)
2025-11-25 02:02:46 +02:00

133 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Forum\Service;
use OCA\Forum\Db\UserRole;
use OCA\Forum\Db\UserRoleMapper;
use Psr\Log\LoggerInterface;
/**
* Service for managing user role assignments
*/
class UserRoleService {
public function __construct(
private UserRoleMapper $userRoleMapper,
private LoggerInterface $logger,
) {
}
/**
* Assign a role to a user
*
* @param string $userId The user ID
* @param int $roleId The role ID to assign
* @param bool $skipIfExists If true, silently skip if user already has the role. If false, log a warning.
* @return UserRole|null The created UserRole, or null if already exists and skipIfExists is true
*/
public function assignRole(string $userId, int $roleId, bool $skipIfExists = true): ?UserRole {
// Check if user already has this role
if ($this->hasRole($userId, $roleId)) {
if ($skipIfExists) {
return null;
}
$this->logger->warning('User {userId} already has role {roleId}', [
'userId' => $userId,
'roleId' => $roleId,
]);
return null;
}
// Create and insert the new user role
$userRole = new UserRole();
$userRole->setUserId($userId);
$userRole->setRoleId($roleId);
$userRole->setCreatedAt(time());
try {
$createdRole = $this->userRoleMapper->insert($userRole);
$this->logger->info('Assigned role {roleId} to user {userId}', [
'userId' => $userId,
'roleId' => $roleId,
]);
return $createdRole;
} catch (\Exception $ex) {
$this->logger->error('Failed to assign role {roleId} to user {userId}: {error}', [
'userId' => $userId,
'roleId' => $roleId,
'error' => $ex->getMessage(),
]);
throw $ex;
}
}
/**
* Check if a user has a specific role
*
* @param string $userId The user ID
* @param int $roleId The role ID to check
* @return bool True if user has the role, false otherwise
*/
public function hasRole(string $userId, int $roleId): bool {
$userRoles = $this->userRoleMapper->findByUserId($userId);
foreach ($userRoles as $userRole) {
if ($userRole->getRoleId() === $roleId) {
return true;
}
}
return false;
}
/**
* Remove a role from a user
*
* @param string $userId The user ID
* @param int $roleId The role ID to remove
* @return bool True if role was removed, false if user didn't have the role
*/
public function removeRole(string $userId, int $roleId): bool {
$userRoles = $this->userRoleMapper->findByUserId($userId);
foreach ($userRoles as $userRole) {
if ($userRole->getRoleId() === $roleId) {
try {
$this->userRoleMapper->delete($userRole);
$this->logger->info('Removed role {roleId} from user {userId}', [
'userId' => $userId,
'roleId' => $roleId,
]);
return true;
} catch (\Exception $ex) {
$this->logger->error('Failed to remove role {roleId} from user {userId}: {error}', [
'userId' => $userId,
'roleId' => $roleId,
'error' => $ex->getMessage(),
]);
throw $ex;
}
}
}
$this->logger->debug('User {userId} does not have role {roleId}, nothing to remove', [
'userId' => $userId,
'roleId' => $roleId,
]);
return false;
}
/**
* Get all role IDs for a user
*
* @param string $userId The user ID
* @return array<int> Array of role IDs
*/
public function getUserRoleIds(string $userId): array {
$userRoles = $this->userRoleMapper->findByUserId($userId);
return array_map(fn ($userRole) => $userRole->getRoleId(), $userRoles);
}
}