mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
fix: category edit team permissions
This commit is contained in:
@@ -406,6 +406,7 @@ class CategoryController extends OCSController {
|
||||
*
|
||||
* @param int $id Category ID
|
||||
* @param list<array{roleId: int, canView: bool, canPost: bool, canReply: bool, canModerate: bool}> $permissions Role permissions array
|
||||
* @param list<array{teamId: string, canView: bool, canPost: bool, canReply: bool, canModerate: bool}> $teamPermissions Team permissions array
|
||||
* @return DataResponse<Http::STATUS_OK, array{success: bool}, array{}>
|
||||
*
|
||||
* 200: Permissions updated
|
||||
@@ -413,13 +414,13 @@ class CategoryController extends OCSController {
|
||||
#[NoAdminRequired]
|
||||
#[RequirePermission('canEditCategories')]
|
||||
#[ApiRoute(verb: 'POST', url: '/api/categories/{id}/permissions')]
|
||||
public function updatePermissions(int $id, array $permissions): DataResponse {
|
||||
public function updatePermissions(int $id, array $permissions, array $teamPermissions = []): DataResponse {
|
||||
try {
|
||||
// Verify category exists
|
||||
$this->categoryMapper->find($id);
|
||||
|
||||
// Delete existing permissions for this category
|
||||
$this->categoryPermMapper->deleteByCategoryId($id);
|
||||
// Delete existing role permissions for this category
|
||||
$this->categoryPermMapper->deleteByCategoryIdAndTargetType($id, CategoryPerm::TARGET_TYPE_ROLE);
|
||||
|
||||
// Filter out Admin role - it has hardcoded full access
|
||||
$filteredPermissions = array_filter($permissions, function ($perm) {
|
||||
@@ -457,6 +458,26 @@ class CategoryController extends OCSController {
|
||||
$this->categoryPermMapper->insert($categoryPerm);
|
||||
}
|
||||
|
||||
// Delete existing team permissions for this category and re-insert
|
||||
$this->categoryPermMapper->deleteByCategoryIdAndTargetType($id, CategoryPerm::TARGET_TYPE_TEAM);
|
||||
|
||||
foreach ($teamPermissions as $perm) {
|
||||
$teamId = $perm['teamId'] ?? null;
|
||||
if ($teamId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$categoryPerm = new CategoryPerm();
|
||||
$categoryPerm->setCategoryId($id);
|
||||
$categoryPerm->setTargetType(CategoryPerm::TARGET_TYPE_TEAM);
|
||||
$categoryPerm->setTargetId((string)$teamId);
|
||||
$categoryPerm->setCanView($perm['canView'] ?? false);
|
||||
$categoryPerm->setCanPost($perm['canPost'] ?? false);
|
||||
$categoryPerm->setCanReply($perm['canReply'] ?? false);
|
||||
$categoryPerm->setCanModerate($perm['canModerate'] ?? false);
|
||||
$this->categoryPermMapper->insert($categoryPerm);
|
||||
}
|
||||
|
||||
return new DataResponse(['success' => true]);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse(['error' => 'Category not found'], Http::STATUS_NOT_FOUND);
|
||||
|
||||
@@ -249,6 +249,21 @@ class CategoryPermMapper extends QBMapper {
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all permissions for a category filtered by target type
|
||||
*/
|
||||
public function deleteByCategoryIdAndTargetType(int $categoryId, string $targetType): void {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete($this->getTableName())
|
||||
->where(
|
||||
$qb->expr()->eq('category_id', $qb->createNamedParameter($categoryId, IQueryBuilder::PARAM_INT))
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->eq('target_type', $qb->createNamedParameter($targetType, IQueryBuilder::PARAM_STR))
|
||||
);
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all permissions for a category
|
||||
*/
|
||||
|
||||
@@ -3520,6 +3520,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"teamPermissions": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"description": "Team permissions array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"teamId",
|
||||
"canView",
|
||||
"canPost",
|
||||
"canReply",
|
||||
"canModerate"
|
||||
],
|
||||
"properties": {
|
||||
"teamId": {
|
||||
"type": "string"
|
||||
},
|
||||
"canView": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canPost": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canReply": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModerate": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
openapi.json
32
openapi.json
@@ -3520,6 +3520,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"teamPermissions": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"description": "Team permissions array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"teamId",
|
||||
"canView",
|
||||
"canPost",
|
||||
"canReply",
|
||||
"canModerate"
|
||||
],
|
||||
"properties": {
|
||||
"teamId": {
|
||||
"type": "string"
|
||||
},
|
||||
"canView": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canPost": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canReply": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModerate": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,9 @@ import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { isAdminRole, isModeratorRole, isDefaultRole, isGuestRole } from '@/constants'
|
||||
import { useCategories } from '@/composables/useCategories'
|
||||
import type { Category, CategoryPerm, CatHeader, Role } from '@/types'
|
||||
import type { Category, CategoryPerm, CatHeader, Role, Team } from '@/types'
|
||||
|
||||
type PermTarget = { id: string; label: string; type: 'role' | 'team' }
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AdminCategoryEdit',
|
||||
@@ -301,10 +303,11 @@ export default defineComponent({
|
||||
headers: [] as CatHeader[],
|
||||
roles: [] as Role[],
|
||||
selectedHeader: null as { id: number; label: string } | null,
|
||||
selectedViewTargets: [] as Array<{ id: number; label: string }>,
|
||||
selectedPostTargets: [] as Array<{ id: number; label: string }>,
|
||||
selectedReplyTargets: [] as Array<{ id: number; label: string }>,
|
||||
selectedModerateTargets: [] as Array<{ id: number; label: string }>,
|
||||
teams: [] as Team[],
|
||||
selectedViewTargets: [] as PermTarget[],
|
||||
selectedPostTargets: [] as PermTarget[],
|
||||
selectedReplyTargets: [] as PermTarget[],
|
||||
selectedModerateTargets: [] as PermTarget[],
|
||||
formData: {
|
||||
headerId: null as number | null,
|
||||
name: '',
|
||||
@@ -355,20 +358,29 @@ export default defineComponent({
|
||||
permissions: t('forum', 'Permissions'),
|
||||
permissionsDescription: t(
|
||||
'forum',
|
||||
'Control which roles can access and moderate this category',
|
||||
'Control which roles and teams can access and moderate this category',
|
||||
),
|
||||
viewRoles: t('forum', 'Can view'),
|
||||
viewRolesHelp: t('forum', 'Select roles that can view this category and its threads'),
|
||||
viewRolesHelp: t(
|
||||
'forum',
|
||||
'Select roles or teams that can view this category and its threads',
|
||||
),
|
||||
postRoles: t('forum', 'Can post'),
|
||||
postRolesHelp: t('forum', 'Select roles that can create new threads in this category'),
|
||||
postRolesHelp: t(
|
||||
'forum',
|
||||
'Select roles or teams that can create new threads in this category',
|
||||
),
|
||||
replyRoles: t('forum', 'Can reply'),
|
||||
replyRolesHelp: t('forum', 'Select roles that can reply to threads in this category'),
|
||||
replyRolesHelp: t(
|
||||
'forum',
|
||||
'Select roles or teams that can reply to threads in this category',
|
||||
),
|
||||
moderateRoles: t('forum', 'Can moderate'),
|
||||
moderateRolesHelp: t(
|
||||
'forum',
|
||||
'Select roles that can moderate (edit/delete) content in this category',
|
||||
'Select roles or teams that can moderate (edit/delete) content in this category',
|
||||
),
|
||||
selectRoles: t('forum', 'Select roles …'),
|
||||
selectRoles: t('forum', 'Select roles or teams …'),
|
||||
design: t('forum', 'Design'),
|
||||
designDesc: t('forum', 'Customize the appearance of this category'),
|
||||
categoryColor: t('forum', 'Category color'),
|
||||
@@ -399,38 +411,53 @@ export default defineComponent({
|
||||
label: header.name,
|
||||
}))
|
||||
},
|
||||
viewTargetOptions(): Array<{ id: number; label: string }> {
|
||||
return this.roles
|
||||
teamOptions(): PermTarget[] {
|
||||
return this.teams.map((team) => ({
|
||||
id: `team:${team.id}`,
|
||||
label: `Team: ${team.displayName}`,
|
||||
type: 'team' as const,
|
||||
}))
|
||||
},
|
||||
viewTargetOptions(): PermTarget[] {
|
||||
const roleOptions: PermTarget[] = this.roles
|
||||
.filter((role) => !isAdminRole(role))
|
||||
.map((role) => ({
|
||||
id: role.id,
|
||||
id: `role:${role.id}`,
|
||||
label: role.name,
|
||||
type: 'role' as const,
|
||||
}))
|
||||
return [...roleOptions, ...this.teamOptions]
|
||||
},
|
||||
postTargetOptions(): Array<{ id: number; label: string }> {
|
||||
return this.roles
|
||||
postTargetOptions(): PermTarget[] {
|
||||
const roleOptions: PermTarget[] = this.roles
|
||||
.filter((role) => !isAdminRole(role))
|
||||
.map((role) => ({
|
||||
id: role.id,
|
||||
id: `role:${role.id}`,
|
||||
label: role.name,
|
||||
type: 'role' as const,
|
||||
}))
|
||||
return [...roleOptions, ...this.teamOptions]
|
||||
},
|
||||
replyTargetOptions(): Array<{ id: number; label: string }> {
|
||||
return this.roles
|
||||
replyTargetOptions(): PermTarget[] {
|
||||
const roleOptions: PermTarget[] = this.roles
|
||||
.filter((role) => !isAdminRole(role))
|
||||
.map((role) => ({
|
||||
id: role.id,
|
||||
id: `role:${role.id}`,
|
||||
label: role.name,
|
||||
type: 'role' as const,
|
||||
}))
|
||||
return [...roleOptions, ...this.teamOptions]
|
||||
},
|
||||
moderateTargetOptions(): Array<{ id: number; label: string }> {
|
||||
moderateTargetOptions(): PermTarget[] {
|
||||
// Filter out Admin, Guest, and Default roles for moderation
|
||||
return this.roles
|
||||
const roleOptions: PermTarget[] = this.roles
|
||||
.filter((role) => !isAdminRole(role) && !isGuestRole(role) && !isDefaultRole(role))
|
||||
.map((role) => ({
|
||||
id: role.id,
|
||||
id: `role:${role.id}`,
|
||||
label: role.name,
|
||||
type: 'role' as const,
|
||||
}))
|
||||
return [...roleOptions, ...this.teamOptions]
|
||||
},
|
||||
categoryColorPresets(): string[] {
|
||||
return [
|
||||
@@ -522,9 +549,13 @@ export default defineComponent({
|
||||
createdAt: header.createdAt,
|
||||
}))
|
||||
|
||||
// Load roles
|
||||
const rolesResponse = await ocs.get<Role[]>('/roles')
|
||||
// Load roles and teams in parallel
|
||||
const [rolesResponse, teamsResponse] = await Promise.all([
|
||||
ocs.get<Role[]>('/roles'),
|
||||
ocs.get<Team[]>('/teams').catch(() => ({ data: [] as Team[] })),
|
||||
])
|
||||
this.roles = rolesResponse.data || []
|
||||
this.teams = teamsResponse.data || []
|
||||
|
||||
// If editing, load category data and permissions
|
||||
if (this.isEditing && this.categoryId) {
|
||||
@@ -535,7 +566,11 @@ export default defineComponent({
|
||||
// View, Post, Reply: Default user role
|
||||
const memberRole = this.roles.find(isDefaultRole)
|
||||
if (memberRole) {
|
||||
const memberOption = { id: memberRole.id, label: memberRole.name }
|
||||
const memberOption: PermTarget = {
|
||||
id: `role:${memberRole.id}`,
|
||||
label: memberRole.name,
|
||||
type: 'role',
|
||||
}
|
||||
this.selectedViewTargets = [memberOption]
|
||||
this.selectedPostTargets = [memberOption]
|
||||
this.selectedReplyTargets = [memberOption]
|
||||
@@ -547,14 +582,16 @@ export default defineComponent({
|
||||
this.selectedModerateTargets = []
|
||||
if (adminRole) {
|
||||
this.selectedModerateTargets.push({
|
||||
id: adminRole.id,
|
||||
id: `role:${adminRole.id}`,
|
||||
label: adminRole.name,
|
||||
type: 'role',
|
||||
})
|
||||
}
|
||||
if (moderatorRole) {
|
||||
this.selectedModerateTargets.push({
|
||||
id: moderatorRole.id,
|
||||
id: `role:${moderatorRole.id}`,
|
||||
label: moderatorRole.name,
|
||||
type: 'role',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -603,41 +640,48 @@ export default defineComponent({
|
||||
|
||||
const perms = permsResponse.data || []
|
||||
|
||||
// Only handle role-type permissions
|
||||
const rolePerms = perms.filter((p) => p.targetType === 'role')
|
||||
const teamPerms = perms.filter((p) => p.targetType === 'team')
|
||||
|
||||
this.selectedViewTargets = rolePerms
|
||||
.filter((p) => p.canView)
|
||||
.map((p) => {
|
||||
const role = this.roles.find((r) => String(r.id) === p.targetId)
|
||||
return role ? { id: role.id, label: role.name } : null
|
||||
})
|
||||
.filter((o): o is { id: number; label: string } => o !== null)
|
||||
// Map role permissions to PermTarget
|
||||
const mapRolePerm = (p: CategoryPerm): PermTarget | null => {
|
||||
const role = this.roles.find((r) => String(r.id) === p.targetId)
|
||||
return role ? { id: `role:${role.id}`, label: role.name, type: 'role' } : null
|
||||
}
|
||||
|
||||
this.selectedPostTargets = rolePerms
|
||||
.filter((p) => p.canPost)
|
||||
.map((p) => {
|
||||
const role = this.roles.find((r) => String(r.id) === p.targetId)
|
||||
return role ? { id: role.id, label: role.name } : null
|
||||
})
|
||||
.filter((o): o is { id: number; label: string } => o !== null)
|
||||
// Map team permissions to PermTarget
|
||||
const mapTeamPerm = (p: CategoryPerm): PermTarget | null => {
|
||||
const team = this.teams.find((t) => t.id === p.targetId)
|
||||
return team
|
||||
? { id: `team:${team.id}`, label: `Team: ${team.displayName}`, type: 'team' }
|
||||
: null
|
||||
}
|
||||
|
||||
this.selectedReplyTargets = rolePerms
|
||||
.filter((p) => p.canReply)
|
||||
.map((p) => {
|
||||
const role = this.roles.find((r) => String(r.id) === p.targetId)
|
||||
return role ? { id: role.id, label: role.name } : null
|
||||
})
|
||||
.filter((o): o is { id: number; label: string } => o !== null)
|
||||
this.selectedViewTargets = [
|
||||
...rolePerms.filter((p) => p.canView).map(mapRolePerm),
|
||||
...teamPerms.filter((p) => p.canView).map(mapTeamPerm),
|
||||
].filter((o): o is PermTarget => o !== null)
|
||||
|
||||
this.selectedModerateTargets = rolePerms
|
||||
.filter((p) => p.canModerate)
|
||||
.map((p) => {
|
||||
const role = this.roles.find((r) => String(r.id) === p.targetId)
|
||||
if (!role || isGuestRole(role) || isDefaultRole(role)) return null
|
||||
return { id: role.id, label: role.name }
|
||||
})
|
||||
.filter((o): o is { id: number; label: string } => o !== null)
|
||||
this.selectedPostTargets = [
|
||||
...rolePerms.filter((p) => p.canPost).map(mapRolePerm),
|
||||
...teamPerms.filter((p) => p.canPost).map(mapTeamPerm),
|
||||
].filter((o): o is PermTarget => o !== null)
|
||||
|
||||
this.selectedReplyTargets = [
|
||||
...rolePerms.filter((p) => p.canReply).map(mapRolePerm),
|
||||
...teamPerms.filter((p) => p.canReply).map(mapTeamPerm),
|
||||
].filter((o): o is PermTarget => o !== null)
|
||||
|
||||
this.selectedModerateTargets = [
|
||||
...rolePerms
|
||||
.filter((p) => p.canModerate)
|
||||
.map((p) => {
|
||||
const role = this.roles.find((r) => String(r.id) === p.targetId)
|
||||
if (!role || isGuestRole(role) || isDefaultRole(role)) return null
|
||||
return { id: `role:${role.id}`, label: role.name, type: 'role' } as PermTarget
|
||||
}),
|
||||
...teamPerms.filter((p) => p.canModerate).map(mapTeamPerm),
|
||||
].filter((o): o is PermTarget => o !== null)
|
||||
} catch (e) {
|
||||
console.error('Failed to load category permissions', e)
|
||||
}
|
||||
@@ -688,12 +732,15 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async updatePermissions(categoryId: number): Promise<void> {
|
||||
const viewRoleIds = new Set(this.selectedViewTargets.map((t) => t.id))
|
||||
const postRoleIds = new Set(this.selectedPostTargets.map((t) => t.id))
|
||||
const replyRoleIds = new Set(this.selectedReplyTargets.map((t) => t.id))
|
||||
const moderateRoleIds = new Set(this.selectedModerateTargets.map((t) => t.id))
|
||||
const filterByType = (targets: PermTarget[], type: 'role' | 'team') =>
|
||||
new Set(targets.filter((t) => t.type === type).map((t) => t.id.split(':')[1]))
|
||||
|
||||
// Role permissions
|
||||
const viewRoleIds = filterByType(this.selectedViewTargets, 'role')
|
||||
const postRoleIds = filterByType(this.selectedPostTargets, 'role')
|
||||
const replyRoleIds = filterByType(this.selectedReplyTargets, 'role')
|
||||
const moderateRoleIds = filterByType(this.selectedModerateTargets, 'role')
|
||||
|
||||
// Collect all unique role IDs
|
||||
const allRoleIds = new Set([
|
||||
...viewRoleIds,
|
||||
...postRoleIds,
|
||||
@@ -711,7 +758,7 @@ export default defineComponent({
|
||||
|
||||
for (const roleId of allRoleIds) {
|
||||
permissions.push({
|
||||
roleId,
|
||||
roleId: parseInt(roleId),
|
||||
canView: viewRoleIds.has(roleId),
|
||||
canPost: postRoleIds.has(roleId),
|
||||
canReply: replyRoleIds.has(roleId),
|
||||
@@ -719,8 +766,40 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
// Team permissions
|
||||
const viewTeamIds = filterByType(this.selectedViewTargets, 'team')
|
||||
const postTeamIds = filterByType(this.selectedPostTargets, 'team')
|
||||
const replyTeamIds = filterByType(this.selectedReplyTargets, 'team')
|
||||
const moderateTeamIds = filterByType(this.selectedModerateTargets, 'team')
|
||||
|
||||
const allTeamIds = new Set([
|
||||
...viewTeamIds,
|
||||
...postTeamIds,
|
||||
...replyTeamIds,
|
||||
...moderateTeamIds,
|
||||
])
|
||||
|
||||
const teamPermissions: Array<{
|
||||
teamId: string
|
||||
canView: boolean
|
||||
canPost: boolean
|
||||
canReply: boolean
|
||||
canModerate: boolean
|
||||
}> = []
|
||||
|
||||
for (const teamId of allTeamIds) {
|
||||
teamPermissions.push({
|
||||
teamId,
|
||||
canView: viewTeamIds.has(teamId),
|
||||
canPost: postTeamIds.has(teamId),
|
||||
canReply: replyTeamIds.has(teamId),
|
||||
canModerate: moderateTeamIds.has(teamId),
|
||||
})
|
||||
}
|
||||
|
||||
await ocs.post(`/categories/${categoryId}/permissions`, {
|
||||
permissions,
|
||||
teamPermissions,
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -712,9 +712,9 @@ class CategoryControllerTest extends TestCase {
|
||||
->with($categoryId)
|
||||
->willReturn($category);
|
||||
|
||||
$this->categoryPermMapper->expects($this->once())
|
||||
->method('deleteByCategoryId')
|
||||
->with($categoryId);
|
||||
$this->categoryPermMapper->expects($this->exactly(2))
|
||||
->method('deleteByCategoryIdAndTargetType')
|
||||
->with($categoryId, $this->anything());
|
||||
|
||||
$this->roleMapper->expects($this->exactly(4))
|
||||
->method('find')
|
||||
@@ -795,9 +795,9 @@ class CategoryControllerTest extends TestCase {
|
||||
[3, $userRole],
|
||||
]);
|
||||
|
||||
$this->categoryPermMapper->expects($this->once())
|
||||
->method('deleteByCategoryId')
|
||||
->with($categoryId);
|
||||
$this->categoryPermMapper->expects($this->exactly(2))
|
||||
->method('deleteByCategoryIdAndTargetType')
|
||||
->with($categoryId, $this->anything());
|
||||
|
||||
// Should only insert 2 permissions (Admin role ID 1 is filtered out)
|
||||
$this->categoryPermMapper->expects($this->exactly(2))
|
||||
@@ -881,9 +881,9 @@ class CategoryControllerTest extends TestCase {
|
||||
->with($categoryId)
|
||||
->willReturn($category);
|
||||
|
||||
$this->categoryPermMapper->expects($this->once())
|
||||
->method('deleteByCategoryId')
|
||||
->with($categoryId);
|
||||
$this->categoryPermMapper->expects($this->exactly(2))
|
||||
->method('deleteByCategoryIdAndTargetType')
|
||||
->with($categoryId, $this->anything());
|
||||
|
||||
// roleMapper->find() is called twice:
|
||||
// - Once during filtering phase
|
||||
@@ -931,9 +931,9 @@ class CategoryControllerTest extends TestCase {
|
||||
->with($categoryId)
|
||||
->willReturn($category);
|
||||
|
||||
$this->categoryPermMapper->expects($this->once())
|
||||
->method('deleteByCategoryId')
|
||||
->with($categoryId);
|
||||
$this->categoryPermMapper->expects($this->exactly(2))
|
||||
->method('deleteByCategoryIdAndTargetType')
|
||||
->with($categoryId, $this->anything());
|
||||
|
||||
// roleMapper->find() is called twice:
|
||||
// - Once during filtering phase
|
||||
@@ -981,9 +981,9 @@ class CategoryControllerTest extends TestCase {
|
||||
->with($categoryId)
|
||||
->willReturn($category);
|
||||
|
||||
$this->categoryPermMapper->expects($this->once())
|
||||
->method('deleteByCategoryId')
|
||||
->with($categoryId);
|
||||
$this->categoryPermMapper->expects($this->exactly(2))
|
||||
->method('deleteByCategoryIdAndTargetType')
|
||||
->with($categoryId, $this->anything());
|
||||
|
||||
// roleMapper->find() is called twice:
|
||||
// - Once during filtering phase
|
||||
|
||||
Reference in New Issue
Block a user