From 4b2b2f53879ca23015570e37a988f6b408b14fd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:08:51 +0000 Subject: [PATCH 1/7] Initial plan From 8cac118a56824ed40508439acf0db2b2ed13d23f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:18:09 +0000 Subject: [PATCH 2/7] Refactor AccessControlService to use unified resource-based methods Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- src/lib/dto/accessControl.ts | 68 +++++ src/lib/services/AccessControlService.ts | 235 ++++++++++-------- .../collaborators/collaborators.remote.ts | 19 +- .../collaborators/collaborators.remote.ts | 19 +- 4 files changed, 242 insertions(+), 99 deletions(-) diff --git a/src/lib/dto/accessControl.ts b/src/lib/dto/accessControl.ts index 49b5e68..1caa618 100644 --- a/src/lib/dto/accessControl.ts +++ b/src/lib/dto/accessControl.ts @@ -1,3 +1,5 @@ +import * as v from 'valibot'; + /** * Permission levels for collaborators on tasks and contests. */ @@ -7,6 +9,29 @@ export enum Permission { Owner = 'owner' } +/** + * Resource types for access control. + */ +export enum ResourceType { + Tasks = 'tasks', + Contests = 'contests' +} + +/** + * Valibot schema for Permission enum. + */ +export const PermissionSchema = v.picklist([Permission.Edit, Permission.Manage, Permission.Owner]); + +/** + * Valibot schema for editable permissions (for add/update operations). + */ +export const EditablePermissionSchema = v.picklist([Permission.Edit, Permission.Manage]); + +/** + * Valibot schema for ResourceType enum. + */ +export const ResourceTypeSchema = v.picklist([ResourceType.Tasks, ResourceType.Contests]); + /** * Represents a collaborator on a task or contest. */ @@ -19,3 +44,46 @@ export interface Collaborator { permission: Permission; addedAt: string; } + +/** + * Valibot schema for Collaborator. + */ +export const CollaboratorSchema = v.object({ + userId: v.number(), + userName: v.string(), + userEmail: v.string(), + firstName: v.string(), + lastName: v.string(), + permission: PermissionSchema, + addedAt: v.string() +}); + +/** + * Request body for adding a collaborator. + */ +export interface AddCollaboratorRequest { + user_id: number; + permission: Permission.Edit | Permission.Manage; +} + +/** + * Valibot schema for AddCollaboratorRequest. + */ +export const AddCollaboratorRequestSchema = v.object({ + user_id: v.pipe(v.number(), v.integer(), v.minValue(1)), + permission: EditablePermissionSchema +}); + +/** + * Request body for updating a collaborator. + */ +export interface UpdateCollaboratorRequest { + permission: Permission.Edit | Permission.Manage; +} + +/** + * Valibot schema for UpdateCollaboratorRequest. + */ +export const UpdateCollaboratorRequestSchema = v.object({ + permission: EditablePermissionSchema +}); diff --git a/src/lib/services/AccessControlService.ts b/src/lib/services/AccessControlService.ts index 2673115..94d8969 100644 --- a/src/lib/services/AccessControlService.ts +++ b/src/lib/services/AccessControlService.ts @@ -1,24 +1,69 @@ import { ApiError, type ApiService } from './ApiService'; -import type { ApiResponse } from '../dto/response'; -import type { Collaborator, Permission } from '../dto/accessControl'; - -export interface AddCollaboratorRequest { - user_id: number; - permission: Permission; -} - -export interface UpdateCollaboratorRequest { - permission: Permission; -} +import type { ApiResponse, PaginatedData } from '../dto/response'; +import type { + Collaborator, + AddCollaboratorRequest, + UpdateCollaboratorRequest, + ResourceType +} from '../dto/accessControl'; +import type { User } from '../dto/user'; export class AccessControlService { constructor(private apiClient: ApiService) {} /** - * Get collaborators for a specific task. + * Get assignable users for a resource. + * Returns users (teachers) who can be granted access to the resource. + * Only users with manage permission can view assignable users. + * Returned users do not currently have any access entry for the resource. + */ + async getAssignableUsers( + resourceType: ResourceType, + resourceId: number, + params?: { limit?: number; offset?: number; sort?: string } + ): Promise<{ + success: boolean; + status: number; + data?: PaginatedData; + error?: string; + }> { + try { + const queryParams = new URLSearchParams(); + if (params?.limit !== undefined) { + queryParams.append('limit', params.limit.toString()); + } + if (params?.offset !== undefined) { + queryParams.append('offset', params.offset.toString()); + } + if (params?.sort) { + queryParams.append('sort', params.sort); + } + + const url = `/access-control/resources/${resourceType}/${resourceId}/assignable${queryParams.toString() ? `?${queryParams.toString()}` : ''}`; + const response = await this.apiClient.get>>({ + url + }); + return { success: true, data: response.data, status: 200 }; + } catch (error) { + if (error instanceof ApiError) { + return { + success: false, + error: error.getApiMessage(), + status: error.getStatus() + }; + } + throw error; + } + } + + /** + * Get collaborators for a resource. * Only users with edit permission or higher can see collaborators. */ - async getTaskCollaborators(taskId: number): Promise<{ + async getCollaborators( + resourceType: ResourceType, + resourceId: number + ): Promise<{ success: boolean; status: number; data?: Collaborator[]; @@ -26,7 +71,7 @@ export class AccessControlService { }> { try { const response = await this.apiClient.get>({ - url: `/access-control/resources/tasks/${taskId}/collaborators` + url: `/access-control/resources/${resourceType}/${resourceId}/collaborators` }); return { success: true, data: response.data, status: 200 }; } catch (error) { @@ -42,11 +87,12 @@ export class AccessControlService { } /** - * Add a collaborator to a specific task. + * Add a collaborator to a resource. * Only users with manage permission can add collaborators. */ - async addTaskCollaborator( - taskId: number, + async addCollaborator( + resourceType: ResourceType, + resourceId: number, data: AddCollaboratorRequest ): Promise<{ success: boolean; @@ -55,7 +101,7 @@ export class AccessControlService { }> { try { await this.apiClient.post>({ - url: `/access-control/resources/tasks/${taskId}/collaborators`, + url: `/access-control/resources/${resourceType}/${resourceId}/collaborators`, body: JSON.stringify(data) }); return { success: true, status: 201 }; @@ -72,11 +118,12 @@ export class AccessControlService { } /** - * Update a collaborator's permission on a specific task. + * Update a collaborator's permission on a resource. * Only users with manage permission can update collaborators. */ - async updateTaskCollaborator( - taskId: number, + async updateCollaborator( + resourceType: ResourceType, + resourceId: number, userId: number, data: UpdateCollaboratorRequest ): Promise<{ @@ -86,7 +133,7 @@ export class AccessControlService { }> { try { await this.apiClient.put>({ - url: `/access-control/resources/tasks/${taskId}/collaborators/${userId}`, + url: `/access-control/resources/${resourceType}/${resourceId}/collaborators/${userId}`, body: JSON.stringify(data) }); return { success: true, status: 200 }; @@ -103,12 +150,13 @@ export class AccessControlService { } /** - * Remove a collaborator from a specific task. + * Remove a collaborator from a resource. * Only users with manage or owner permission can remove collaborators. * Managers can only remove editors and managers, owners can remove everyone except other owners. */ - async deleteTaskCollaborator( - taskId: number, + async deleteCollaborator( + resourceType: ResourceType, + resourceId: number, userId: number ): Promise<{ success: boolean; @@ -117,7 +165,7 @@ export class AccessControlService { }> { try { await this.apiClient.delete>({ - url: `/access-control/resources/tasks/${taskId}/collaborators/${userId}` + url: `/access-control/resources/${resourceType}/${resourceId}/collaborators/${userId}` }); return { success: true, status: 200 }; } catch (error) { @@ -132,9 +180,64 @@ export class AccessControlService { } } + // Legacy compatibility methods - these delegate to the new unified methods /** - * Get collaborators for a specific contest. - * Only users with edit permission or higher can see collaborators. + * @deprecated Use getCollaborators with ResourceType.Tasks instead + */ + async getTaskCollaborators(taskId: number): Promise<{ + success: boolean; + status: number; + data?: Collaborator[]; + error?: string; + }> { + return this.getCollaborators('tasks' as ResourceType, taskId); + } + + /** + * @deprecated Use addCollaborator with ResourceType.Tasks instead + */ + async addTaskCollaborator( + taskId: number, + data: AddCollaboratorRequest + ): Promise<{ + success: boolean; + status: number; + error?: string; + }> { + return this.addCollaborator('tasks' as ResourceType, taskId, data); + } + + /** + * @deprecated Use updateCollaborator with ResourceType.Tasks instead + */ + async updateTaskCollaborator( + taskId: number, + userId: number, + data: UpdateCollaboratorRequest + ): Promise<{ + success: boolean; + status: number; + error?: string; + }> { + return this.updateCollaborator('tasks' as ResourceType, taskId, userId, data); + } + + /** + * @deprecated Use deleteCollaborator with ResourceType.Tasks instead + */ + async deleteTaskCollaborator( + taskId: number, + userId: number + ): Promise<{ + success: boolean; + status: number; + error?: string; + }> { + return this.deleteCollaborator('tasks' as ResourceType, taskId, userId); + } + + /** + * @deprecated Use getCollaborators with ResourceType.Contests instead */ async getContestCollaborators(contestId: number): Promise<{ success: boolean; @@ -142,26 +245,11 @@ export class AccessControlService { data?: Collaborator[]; error?: string; }> { - try { - const response = await this.apiClient.get>({ - url: `/access-control/resources/contests/${contestId}/collaborators` - }); - return { success: true, data: response.data, status: 200 }; - } catch (error) { - if (error instanceof ApiError) { - return { - success: false, - error: error.getApiMessage(), - status: error.getStatus() - }; - } - throw error; - } + return this.getCollaborators('contests' as ResourceType, contestId); } /** - * Add a collaborator to a specific contest. - * Only users with manage permission can add collaborators. + * @deprecated Use addCollaborator with ResourceType.Contests instead */ async addContestCollaborator( contestId: number, @@ -171,27 +259,11 @@ export class AccessControlService { status: number; error?: string; }> { - try { - await this.apiClient.post>({ - url: `/access-control/resources/contests/${contestId}/collaborators`, - body: JSON.stringify(data) - }); - return { success: true, status: 201 }; - } catch (error) { - if (error instanceof ApiError) { - return { - success: false, - error: error.getApiMessage(), - status: error.getStatus() - }; - } - throw error; - } + return this.addCollaborator('contests' as ResourceType, contestId, data); } /** - * Update a collaborator's permission on a specific contest. - * Only users with manage permission can update collaborators. + * @deprecated Use updateCollaborator with ResourceType.Contests instead */ async updateContestCollaborator( contestId: number, @@ -202,28 +274,11 @@ export class AccessControlService { status: number; error?: string; }> { - try { - await this.apiClient.put>({ - url: `/access-control/resources/contests/${contestId}/collaborators/${userId}`, - body: JSON.stringify(data) - }); - return { success: true, status: 200 }; - } catch (error) { - if (error instanceof ApiError) { - return { - success: false, - error: error.getApiMessage(), - status: error.getStatus() - }; - } - throw error; - } + return this.updateCollaborator('contests' as ResourceType, contestId, userId, data); } /** - * Remove a collaborator from a specific contest. - * Only users with manage or owner permission can remove collaborators. - * Managers can only remove editors and managers, owners can remove everyone except other owners. + * @deprecated Use deleteCollaborator with ResourceType.Contests instead */ async deleteContestCollaborator( contestId: number, @@ -233,20 +288,6 @@ export class AccessControlService { status: number; error?: string; }> { - try { - await this.apiClient.delete>({ - url: `/access-control/resources/contests/${contestId}/collaborators/${userId}` - }); - return { success: true, status: 200 }; - } catch (error) { - if (error instanceof ApiError) { - return { - success: false, - error: error.getApiMessage(), - status: error.getStatus() - }; - } - throw error; - } + return this.deleteCollaborator('contests' as ResourceType, contestId, userId); } } diff --git a/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts b/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts index a8a9b05..853a565 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts +++ b/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts @@ -2,7 +2,7 @@ import { query, form, getRequestEvent } from '$app/server'; import { createApiClient } from '$lib/services/ApiService'; import { AccessControlService } from '$lib/services/AccessControlService'; import { UserService } from '$lib/services/UserService'; -import { Permission } from '$lib/dto/accessControl'; +import { Permission, ResourceType } from '$lib/dto/accessControl'; import { error } from '@sveltejs/kit'; import * as v from 'valibot'; @@ -38,6 +38,23 @@ export const getAllUsers = query(async () => { return result.data; }); +export const getAssignableUsers = query(v.number(), async (contestId: number) => { + const { cookies } = getRequestEvent(); + + const apiClient = createApiClient(cookies); + const accessControlService = new AccessControlService(apiClient); + + const result = await accessControlService.getAssignableUsers(ResourceType.Contests, contestId, { + limit: 1000 + }); + + if (!result.success || !result.data) { + error(result.status, { message: result.error || 'Failed to load assignable users' }); + } + + return result.data; +}); + export const addCollaborator = form( v.object({ contestId: v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1)), diff --git a/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts b/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts index 639e2b7..a8ce335 100644 --- a/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts +++ b/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts @@ -2,7 +2,7 @@ import { query, form, getRequestEvent } from '$app/server'; import { createApiClient } from '$lib/services/ApiService'; import { AccessControlService } from '$lib/services/AccessControlService'; import { UserService } from '$lib/services/UserService'; -import { Permission } from '$lib/dto/accessControl'; +import { Permission, ResourceType } from '$lib/dto/accessControl'; import { error } from '@sveltejs/kit'; import * as v from 'valibot'; @@ -38,6 +38,23 @@ export const getAllUsers = query(async () => { return result.data; }); +export const getAssignableUsers = query(v.number(), async (taskId: number) => { + const { cookies } = getRequestEvent(); + + const apiClient = createApiClient(cookies); + const accessControlService = new AccessControlService(apiClient); + + const result = await accessControlService.getAssignableUsers(ResourceType.Tasks, taskId, { + limit: 1000 + }); + + if (!result.success || !result.data) { + error(result.status, { message: result.error || 'Failed to load assignable users' }); + } + + return result.data; +}); + export const addCollaborator = form( v.object({ taskId: v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1)), From 7674f4def29368520e3ae66cc5e51986be47b600 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:21:03 +0000 Subject: [PATCH 3/7] Use proper ResourceType enum values in legacy methods Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- src/lib/services/AccessControlService.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/services/AccessControlService.ts b/src/lib/services/AccessControlService.ts index 94d8969..98b8694 100644 --- a/src/lib/services/AccessControlService.ts +++ b/src/lib/services/AccessControlService.ts @@ -3,9 +3,9 @@ import type { ApiResponse, PaginatedData } from '../dto/response'; import type { Collaborator, AddCollaboratorRequest, - UpdateCollaboratorRequest, - ResourceType + UpdateCollaboratorRequest } from '../dto/accessControl'; +import { ResourceType } from '../dto/accessControl'; import type { User } from '../dto/user'; export class AccessControlService { @@ -190,7 +190,7 @@ export class AccessControlService { data?: Collaborator[]; error?: string; }> { - return this.getCollaborators('tasks' as ResourceType, taskId); + return this.getCollaborators(ResourceType.Tasks, taskId); } /** @@ -204,7 +204,7 @@ export class AccessControlService { status: number; error?: string; }> { - return this.addCollaborator('tasks' as ResourceType, taskId, data); + return this.addCollaborator(ResourceType.Tasks, taskId, data); } /** @@ -219,7 +219,7 @@ export class AccessControlService { status: number; error?: string; }> { - return this.updateCollaborator('tasks' as ResourceType, taskId, userId, data); + return this.updateCollaborator(ResourceType.Tasks, taskId, userId, data); } /** @@ -233,7 +233,7 @@ export class AccessControlService { status: number; error?: string; }> { - return this.deleteCollaborator('tasks' as ResourceType, taskId, userId); + return this.deleteCollaborator(ResourceType.Tasks, taskId, userId); } /** @@ -245,7 +245,7 @@ export class AccessControlService { data?: Collaborator[]; error?: string; }> { - return this.getCollaborators('contests' as ResourceType, contestId); + return this.getCollaborators(ResourceType.Contests, contestId); } /** @@ -259,7 +259,7 @@ export class AccessControlService { status: number; error?: string; }> { - return this.addCollaborator('contests' as ResourceType, contestId, data); + return this.addCollaborator(ResourceType.Contests, contestId, data); } /** @@ -274,7 +274,7 @@ export class AccessControlService { status: number; error?: string; }> { - return this.updateCollaborator('contests' as ResourceType, contestId, userId, data); + return this.updateCollaborator(ResourceType.Contests, contestId, userId, data); } /** @@ -288,6 +288,6 @@ export class AccessControlService { status: number; error?: string; }> { - return this.deleteCollaborator('contests' as ResourceType, contestId, userId); + return this.deleteCollaborator(ResourceType.Contests, contestId, userId); } } From 103614aacf7e15b584e283122974072dee52d884 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:57:47 +0000 Subject: [PATCH 4/7] Remove deprecated methods and unused code per review feedback - Removed getAssignableUsers from remote functions (not used, getAllUsers with frontend filtering is used instead) - Removed all deprecated methods from AccessControlService - Updated all usages to use new unified methods (getCollaborators, addCollaborator, updateCollaborator, deleteCollaborator) - Removed unused Valibot schemas (CollaboratorSchema, AddCollaboratorRequestSchema, etc.) - Updated layout.server.ts files to use ResourceType enum Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- src/lib/dto/accessControl.ts | 45 ------- src/lib/services/AccessControlService.ts | 110 ------------------ .../contests/[contestId]/+layout.server.ts | 3 +- .../collaborators/collaborators.remote.ts | 27 +---- .../teacher/tasks/[taskId]/+layout.server.ts | 3 +- .../collaborators/collaborators.remote.ts | 38 +++--- .../[contestId]/results/+layout.server.ts | 3 +- 7 files changed, 27 insertions(+), 202 deletions(-) diff --git a/src/lib/dto/accessControl.ts b/src/lib/dto/accessControl.ts index 1caa618..c021d10 100644 --- a/src/lib/dto/accessControl.ts +++ b/src/lib/dto/accessControl.ts @@ -1,5 +1,3 @@ -import * as v from 'valibot'; - /** * Permission levels for collaborators on tasks and contests. */ @@ -17,21 +15,6 @@ export enum ResourceType { Contests = 'contests' } -/** - * Valibot schema for Permission enum. - */ -export const PermissionSchema = v.picklist([Permission.Edit, Permission.Manage, Permission.Owner]); - -/** - * Valibot schema for editable permissions (for add/update operations). - */ -export const EditablePermissionSchema = v.picklist([Permission.Edit, Permission.Manage]); - -/** - * Valibot schema for ResourceType enum. - */ -export const ResourceTypeSchema = v.picklist([ResourceType.Tasks, ResourceType.Contests]); - /** * Represents a collaborator on a task or contest. */ @@ -45,19 +28,6 @@ export interface Collaborator { addedAt: string; } -/** - * Valibot schema for Collaborator. - */ -export const CollaboratorSchema = v.object({ - userId: v.number(), - userName: v.string(), - userEmail: v.string(), - firstName: v.string(), - lastName: v.string(), - permission: PermissionSchema, - addedAt: v.string() -}); - /** * Request body for adding a collaborator. */ @@ -66,24 +36,9 @@ export interface AddCollaboratorRequest { permission: Permission.Edit | Permission.Manage; } -/** - * Valibot schema for AddCollaboratorRequest. - */ -export const AddCollaboratorRequestSchema = v.object({ - user_id: v.pipe(v.number(), v.integer(), v.minValue(1)), - permission: EditablePermissionSchema -}); - /** * Request body for updating a collaborator. */ export interface UpdateCollaboratorRequest { permission: Permission.Edit | Permission.Manage; } - -/** - * Valibot schema for UpdateCollaboratorRequest. - */ -export const UpdateCollaboratorRequestSchema = v.object({ - permission: EditablePermissionSchema -}); diff --git a/src/lib/services/AccessControlService.ts b/src/lib/services/AccessControlService.ts index 98b8694..e3d9194 100644 --- a/src/lib/services/AccessControlService.ts +++ b/src/lib/services/AccessControlService.ts @@ -180,114 +180,4 @@ export class AccessControlService { } } - // Legacy compatibility methods - these delegate to the new unified methods - /** - * @deprecated Use getCollaborators with ResourceType.Tasks instead - */ - async getTaskCollaborators(taskId: number): Promise<{ - success: boolean; - status: number; - data?: Collaborator[]; - error?: string; - }> { - return this.getCollaborators(ResourceType.Tasks, taskId); - } - - /** - * @deprecated Use addCollaborator with ResourceType.Tasks instead - */ - async addTaskCollaborator( - taskId: number, - data: AddCollaboratorRequest - ): Promise<{ - success: boolean; - status: number; - error?: string; - }> { - return this.addCollaborator(ResourceType.Tasks, taskId, data); - } - - /** - * @deprecated Use updateCollaborator with ResourceType.Tasks instead - */ - async updateTaskCollaborator( - taskId: number, - userId: number, - data: UpdateCollaboratorRequest - ): Promise<{ - success: boolean; - status: number; - error?: string; - }> { - return this.updateCollaborator(ResourceType.Tasks, taskId, userId, data); - } - - /** - * @deprecated Use deleteCollaborator with ResourceType.Tasks instead - */ - async deleteTaskCollaborator( - taskId: number, - userId: number - ): Promise<{ - success: boolean; - status: number; - error?: string; - }> { - return this.deleteCollaborator(ResourceType.Tasks, taskId, userId); - } - - /** - * @deprecated Use getCollaborators with ResourceType.Contests instead - */ - async getContestCollaborators(contestId: number): Promise<{ - success: boolean; - status: number; - data?: Collaborator[]; - error?: string; - }> { - return this.getCollaborators(ResourceType.Contests, contestId); - } - - /** - * @deprecated Use addCollaborator with ResourceType.Contests instead - */ - async addContestCollaborator( - contestId: number, - data: AddCollaboratorRequest - ): Promise<{ - success: boolean; - status: number; - error?: string; - }> { - return this.addCollaborator(ResourceType.Contests, contestId, data); - } - - /** - * @deprecated Use updateCollaborator with ResourceType.Contests instead - */ - async updateContestCollaborator( - contestId: number, - userId: number, - data: UpdateCollaboratorRequest - ): Promise<{ - success: boolean; - status: number; - error?: string; - }> { - return this.updateCollaborator(ResourceType.Contests, contestId, userId, data); - } - - /** - * @deprecated Use deleteCollaborator with ResourceType.Contests instead - */ - async deleteContestCollaborator( - contestId: number, - userId: number - ): Promise<{ - success: boolean; - status: number; - error?: string; - }> { - return this.deleteCollaborator(ResourceType.Contests, contestId, userId); - } } diff --git a/src/routes/dashboard/teacher/contests/[contestId]/+layout.server.ts b/src/routes/dashboard/teacher/contests/[contestId]/+layout.server.ts index 10196a9..25aa502 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/+layout.server.ts +++ b/src/routes/dashboard/teacher/contests/[contestId]/+layout.server.ts @@ -1,6 +1,7 @@ import { error } from '@sveltejs/kit'; import { createApiClient } from '$lib/services/ApiService'; import { AccessControlService } from '$lib/services/AccessControlService'; +import { ResourceType } from '$lib/dto/accessControl'; export const load = async ({ params, @@ -22,7 +23,7 @@ export const load = async ({ // Verify contest exists by checking if we can access collaborators const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.getContestCollaborators(contestId); + const result = await accessControlService.getCollaborators(ResourceType.Contests, contestId); if (!result.success) { if (result.status === 404) { diff --git a/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts b/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts index 853a565..eeaf00e 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts +++ b/src/routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote.ts @@ -12,7 +12,7 @@ export const getContestCollaborators = query(v.number(), async (contestId: numbe const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.getContestCollaborators(contestId); + const result = await accessControlService.getCollaborators(ResourceType.Contests, contestId); if (!result.success || !result.data) { error(result.status, { message: result.error || 'Failed to load collaborators' }); @@ -38,23 +38,6 @@ export const getAllUsers = query(async () => { return result.data; }); -export const getAssignableUsers = query(v.number(), async (contestId: number) => { - const { cookies } = getRequestEvent(); - - const apiClient = createApiClient(cookies); - const accessControlService = new AccessControlService(apiClient); - - const result = await accessControlService.getAssignableUsers(ResourceType.Contests, contestId, { - limit: 1000 - }); - - if (!result.success || !result.data) { - error(result.status, { message: result.error || 'Failed to load assignable users' }); - } - - return result.data; -}); - export const addCollaborator = form( v.object({ contestId: v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1)), @@ -67,7 +50,7 @@ export const addCollaborator = form( const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.addContestCollaborator(data.contestId, { + const result = await accessControlService.addCollaborator(ResourceType.Contests, data.contestId, { user_id: data.userId, permission: data.permission }); @@ -97,7 +80,8 @@ export const updateCollaborator = form( const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.updateContestCollaborator( + const result = await accessControlService.updateCollaborator( + ResourceType.Contests, data.contestId, data.userId, { @@ -129,7 +113,8 @@ export const removeCollaborator = form( const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.deleteContestCollaborator( + const result = await accessControlService.deleteCollaborator( + ResourceType.Contests, data.contestId, data.userId ); diff --git a/src/routes/dashboard/teacher/tasks/[taskId]/+layout.server.ts b/src/routes/dashboard/teacher/tasks/[taskId]/+layout.server.ts index 7e1f779..db5d1df 100644 --- a/src/routes/dashboard/teacher/tasks/[taskId]/+layout.server.ts +++ b/src/routes/dashboard/teacher/tasks/[taskId]/+layout.server.ts @@ -1,6 +1,7 @@ import { error } from '@sveltejs/kit'; import { createApiClient } from '$lib/services/ApiService'; import { AccessControlService } from '$lib/services/AccessControlService'; +import { ResourceType } from '$lib/dto/accessControl'; export const load = async ({ params, @@ -22,7 +23,7 @@ export const load = async ({ // Verify task exists by checking if we can access collaborators const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.getTaskCollaborators(taskId); + const result = await accessControlService.getCollaborators(ResourceType.Tasks, taskId); if (!result.success) { if (result.status === 404) { diff --git a/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts b/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts index a8ce335..da83255 100644 --- a/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts +++ b/src/routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote.ts @@ -12,7 +12,7 @@ export const getTaskCollaborators = query(v.number(), async (taskId: number) => const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.getTaskCollaborators(taskId); + const result = await accessControlService.getCollaborators(ResourceType.Tasks, taskId); if (!result.success || !result.data) { error(result.status, { message: result.error || 'Failed to load collaborators' }); @@ -38,23 +38,6 @@ export const getAllUsers = query(async () => { return result.data; }); -export const getAssignableUsers = query(v.number(), async (taskId: number) => { - const { cookies } = getRequestEvent(); - - const apiClient = createApiClient(cookies); - const accessControlService = new AccessControlService(apiClient); - - const result = await accessControlService.getAssignableUsers(ResourceType.Tasks, taskId, { - limit: 1000 - }); - - if (!result.success || !result.data) { - error(result.status, { message: result.error || 'Failed to load assignable users' }); - } - - return result.data; -}); - export const addCollaborator = form( v.object({ taskId: v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1)), @@ -67,7 +50,7 @@ export const addCollaborator = form( const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.addTaskCollaborator(data.taskId, { + const result = await accessControlService.addCollaborator(ResourceType.Tasks, data.taskId, { user_id: data.userId, permission: data.permission }); @@ -97,9 +80,14 @@ export const updateCollaborator = form( const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.updateTaskCollaborator(data.taskId, data.userId, { - permission: data.permission - }); + const result = await accessControlService.updateCollaborator( + ResourceType.Tasks, + data.taskId, + data.userId, + { + permission: data.permission + } + ); if (!result.success) { error(result.status, { message: result.error || 'Failed to update collaborator' }); @@ -125,7 +113,11 @@ export const removeCollaborator = form( const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.deleteTaskCollaborator(data.taskId, data.userId); + const result = await accessControlService.deleteCollaborator( + ResourceType.Tasks, + data.taskId, + data.userId + ); if (!result.success) { error(result.status, { message: result.error || 'Failed to remove collaborator' }); diff --git a/src/routes/dashboard/user/contests/[contestId]/results/+layout.server.ts b/src/routes/dashboard/user/contests/[contestId]/results/+layout.server.ts index b10e728..8b2ed10 100644 --- a/src/routes/dashboard/user/contests/[contestId]/results/+layout.server.ts +++ b/src/routes/dashboard/user/contests/[contestId]/results/+layout.server.ts @@ -2,6 +2,7 @@ import { error } from '@sveltejs/kit'; import { createContestService } from '$lib/services/ContestService'; import { createApiClient } from '$lib/services/ApiService'; import { AccessControlService } from '$lib/services/AccessControlService'; +import { ResourceType } from '$lib/dto/accessControl'; import { UserRole } from '$lib/dto/jwt'; import { ContestStatus } from '$lib/dto/contest'; import * as m from '$lib/paraglide/messages'; @@ -43,7 +44,7 @@ export const load = async ({ if (isTeacher) { const apiClient = createApiClient(cookies); const accessControlService = new AccessControlService(apiClient); - const result = await accessControlService.getContestCollaborators(contestId); + const result = await accessControlService.getCollaborators(ResourceType.Contests, contestId); if (!result.success) { throw error(403, m.contest_results_access_denied()); From af7e961ee58d11fafba2566cab0e18c88a467d53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:05:25 +0000 Subject: [PATCH 5/7] Use getAssignableUsers endpoint instead of client-side filtering - Added getAssignableUsers query to both task and contest collaborators remote files - Updated Svelte pages to call getAssignableUsers with taskId/contestId - Backend now handles filtering of students and existing collaborators - Removed client-side filtering logic from AddCollaboratorButton components - Removed existingCollaborators prop (no longer needed) - Removed UserService import from remote files Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- .../AddContestCollaboratorButton.svelte | 22 +++---------------- .../admin/tasks/AddCollaboratorButton.svelte | 16 +++----------- .../[contestId]/collaborators/+page.svelte | 11 +++++----- .../collaborators/collaborators.remote.ts | 13 +++++------ .../tasks/[taskId]/collaborators/+page.svelte | 11 +++++----- .../collaborators/collaborators.remote.ts | 13 +++++------ 6 files changed, 28 insertions(+), 58 deletions(-) diff --git a/src/lib/components/dashboard/admin/contests/AddContestCollaboratorButton.svelte b/src/lib/components/dashboard/admin/contests/AddContestCollaboratorButton.svelte index 1c1f4f7..fe97ec3 100644 --- a/src/lib/components/dashboard/admin/contests/AddContestCollaboratorButton.svelte +++ b/src/lib/components/dashboard/admin/contests/AddContestCollaboratorButton.svelte @@ -11,10 +11,8 @@ import * as m from '$lib/paraglide/messages'; import { Permission } from '$lib/dto/accessControl'; import type { User } from '$lib/dto/user'; - import type { Collaborator } from '$lib/dto/accessControl'; import type { AddCollaboratorForm } from '$routes/dashboard/teacher/contests/[contestId]/collaborators/collaborators.remote'; import { LoadingSpinner } from '$lib/components/common'; - import { UserRole } from '$lib/dto/jwt'; import type { PaginatedData } from '$lib/dto/response'; interface Props { @@ -23,31 +21,17 @@ users: PaginatedData | undefined; usersLoading: boolean; usersError: Error | null; - existingCollaborators: Collaborator[] | undefined; } - let { - contestId, - addCollaborator, - users, - usersLoading, - usersError, - existingCollaborators - }: Props = $props(); + let { contestId, addCollaborator, users, usersLoading, usersError }: Props = $props(); let dialogOpen = $state(false); let searchQuery = $state(''); let selectedUserId = $state(null); let selectedPermission = $state(null); - // Filter out users who are already collaborators and users with student role - let availableUsers = $derived.by(() => { - if (!users) return []; - const collaboratorIds = new Set(existingCollaborators?.map((c) => c.userId) ?? []); - return users.items.filter( - (user) => !collaboratorIds.has(user.id) && user.role !== UserRole.Student - ); - }); + // Backend returns only assignable users (teachers who aren't already collaborators) + let availableUsers = $derived(users?.items ?? []); // Filter users by search query let filteredUsers = $derived.by(() => { diff --git a/src/lib/components/dashboard/admin/tasks/AddCollaboratorButton.svelte b/src/lib/components/dashboard/admin/tasks/AddCollaboratorButton.svelte index ca8c357..6aa88a2 100644 --- a/src/lib/components/dashboard/admin/tasks/AddCollaboratorButton.svelte +++ b/src/lib/components/dashboard/admin/tasks/AddCollaboratorButton.svelte @@ -11,10 +11,8 @@ import * as m from '$lib/paraglide/messages'; import { Permission } from '$lib/dto/accessControl'; import type { User } from '$lib/dto/user'; - import type { Collaborator } from '$lib/dto/accessControl'; import type { AddCollaboratorForm } from '$routes/dashboard/teacher/tasks/[taskId]/collaborators/collaborators.remote'; import { LoadingSpinner } from '$lib/components/common'; - import { UserRole } from '$lib/dto/jwt'; import type { PaginatedData } from '$lib/dto/response'; interface Props { @@ -23,25 +21,17 @@ users: PaginatedData | undefined; usersLoading: boolean; usersError: Error | null; - existingCollaborators: Collaborator[] | undefined; } - let { taskId, addCollaborator, users, usersLoading, usersError, existingCollaborators }: Props = - $props(); + let { taskId, addCollaborator, users, usersLoading, usersError }: Props = $props(); let dialogOpen = $state(false); let searchQuery = $state(''); let selectedUserId = $state(null); let selectedPermission = $state(null); - // Filter out users who are already collaborators and users with student role - let availableUsers = $derived.by(() => { - if (!users) return []; - const collaboratorIds = new Set(existingCollaborators?.map((c) => c.userId) ?? []); - return users.items.filter( - (user) => !collaboratorIds.has(user.id) && user.role !== UserRole.Student - ); - }); + // Backend returns only assignable users (teachers who aren't already collaborators) + let availableUsers = $derived(users?.items ?? []); // Filter users by search query let filteredUsers = $derived.by(() => { diff --git a/src/routes/dashboard/teacher/contests/[contestId]/collaborators/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/collaborators/+page.svelte index db2ea35..cfe1040 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/collaborators/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/collaborators/+page.svelte @@ -1,7 +1,7 @@ + +
+ {#key checkboxKey} + + {/key} + +
+ + + + + {m.admin_tasks_visibility_confirm_title()} + + {pendingVisibility + ? m.admin_tasks_visibility_confirm_enable({ taskTitle }) + : m.admin_tasks_visibility_confirm_disable({ taskTitle })} + + + + + + + + diff --git a/src/lib/components/dashboard/admin/tasks/TasksUploadDialog.svelte b/src/lib/components/dashboard/admin/tasks/TasksUploadDialog.svelte index 2435f3a..4a2ffdb 100644 --- a/src/lib/components/dashboard/admin/tasks/TasksUploadDialog.svelte +++ b/src/lib/components/dashboard/admin/tasks/TasksUploadDialog.svelte @@ -3,6 +3,7 @@ import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; + import { Checkbox } from '$lib/components/ui/checkbox'; import * as Dialog from '$lib/components/ui/dialog'; import { toast } from 'svelte-sonner'; import { isHttpError } from '@sveltejs/kit'; @@ -123,6 +124,21 @@ {/if} +
+ +
+ +

+ {m.admin_tasks_form_visible_description()} +

+
+
+