diff --git a/src/endpoints/collections.ts b/src/endpoints/collections.ts index 12c5bf5..75a2eed 100644 --- a/src/endpoints/collections.ts +++ b/src/endpoints/collections.ts @@ -1,8 +1,12 @@ import { orm } from '../orm/orm'; import { UnwrappedRequest } from '../utilities/guard'; import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper'; -import { CreateCollectionRequest, UpdateCollectionRequest } from '../utilities/requestModels'; -import { CollectionId } from '../utilities/secureIds'; +import { + GameToCollectionRequest, + CreateCollectionRequest, + UpdateCollectionRequest, +} from '../utilities/requestModels'; +import { CollectionId, GameId } from '../utilities/secureIds'; async function create(request: UnwrappedRequest): Promise { try { @@ -47,10 +51,40 @@ async function drop(request: UnwrappedRequest): Promise { } } +async function addGame(request: UnwrappedRequest): Promise { + try { + return new OkResponse( + await orm.collections.addGame( + CollectionId.fromHash(request.params.id), + GameId.fromHash(request.body.gameId), + request.claims, + ), + ); + } catch (error: any) { + return new ErrorResponse(error as Error); + } +} + +async function removeGame(request: UnwrappedRequest): Promise { + try { + return new OkResponse( + await orm.collections.removeGame( + CollectionId.fromHash(request.params.id), + GameId.fromHash(request.body.gameId), + request.claims, + ), + ); + } catch (error: any) { + return new ErrorResponse(error as Error); + } +} + export default { create, get, list, update, drop, + addGame, + removeGame, }; diff --git a/src/orm/collections.ts b/src/orm/collections.ts index 0863c77..9bbc7e9 100644 --- a/src/orm/collections.ts +++ b/src/orm/collections.ts @@ -127,4 +127,41 @@ export class CollectionsOrm { return; } + + async addGame(id: CollectionId, gameId: GameId, claims: Claims): Promise { + const collection = await this.get(id); + if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.GAME.ADD, claims))) { + throw new UnauthorizedError(); + } else if ( + Claims.test(Claims.COLLECTIONS.OWNED.GAME.ADD, claims) && + claims?.userId && + collection.id !== claims.userId + ) { + throw new UnauthorizedError(); + } + + await sql`INSERT INTO collection_games (collection_id, game_id) + VALUES (${id.raw}, ${gameId.raw})`; + + return; + } + async removeGame(id: CollectionId, gameId: GameId, claims: Claims): Promise { + const collection = await this.get(id); + if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.GAME.REMOVE, claims))) { + throw new UnauthorizedError(); + } else if ( + Claims.test(Claims.COLLECTIONS.OWNED.GAME.REMOVE, claims) && + claims?.userId && + collection.id !== claims.userId + ) { + throw new UnauthorizedError(); + } + + await sql`DELETE + FROM collection_games + WHERE collection_id = ${id.raw} + AND game_id = ${gameId.raw}`; + + return; + } } diff --git a/src/routes/collections.ts b/src/routes/collections.ts index 67c03ff..191e8fb 100644 --- a/src/routes/collections.ts +++ b/src/routes/collections.ts @@ -11,6 +11,20 @@ export default { // PATCH: guard(collections.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]), // DELETE: guard(collections.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]), }, + '/api/collection/:id/add': { + POST: guard(collections.addGame, [ + Claims.ADMIN, + Claims.COLLECTIONS.UNOWNED.GAME.ADD, + Claims.COLLECTIONS.OWNED.GAME.ADD, + ]), + }, + '/api/collection/:id/remove': { + POST: guard(collections.removeGame, [ + Claims.ADMIN, + Claims.COLLECTIONS.UNOWNED.GAME.REMOVE, + Claims.COLLECTIONS.OWNED.GAME.REMOVE, + ]), + }, '/api/collection/list': { GET: guard(collections.list, [Claims.ADMIN, Claims.COLLECTIONS.OWNED.LIST]), }, diff --git a/src/utilities/claimDefinitions.ts b/src/utilities/claimDefinitions.ts index c78b56f..1f0ce0f 100644 --- a/src/utilities/claimDefinitions.ts +++ b/src/utilities/claimDefinitions.ts @@ -108,6 +108,10 @@ export class ClaimDefinition { UPDATE: 'COLLECTIONS_OWNED_UPDATE', DELETE: 'COLLECTIONS_OWNED_DELETE', LIST: 'COLLECTIONS_OWNED_LIST', + GAME: { + ADD: 'COLLECTIONS_OWNED_GAME_ADD', + REMOVE: 'COLLECTIONS_OWNED_GAME_REMOVE', + }, COMMENTS: { DELETE: 'COLLECTIONS_OWNED_COMMENTS_DELETE', }, @@ -116,6 +120,10 @@ export class ClaimDefinition { READ: 'COLLECTIONS_UNOWNED_READ', UPDATE: 'COLLECTIONS_UNOWNED_UPDATE', DELETE: 'COLLECTIONS_UNOWNED_DELETE', + GAME: { + ADD: 'COLLECTIONS_UNOWNED_GAME_ADD', + REMOVE: 'COLLECTIONS_UNOWNED_GAME_REMOVE', + }, }, }; public static readonly COMMENTS = { diff --git a/src/utilities/requestModels.ts b/src/utilities/requestModels.ts index 324b600..330e64c 100644 --- a/src/utilities/requestModels.ts +++ b/src/utilities/requestModels.ts @@ -47,3 +47,6 @@ export interface CreateCollectionRequest { export interface UpdateCollectionRequest { name?: string; } +export interface GameToCollectionRequest { + gameId: string; +} \ No newline at end of file