Compare commits

..

3 Commits

Author SHA1 Message Date
jd
ed942060a2 Tidied up Bruno files. 2026-02-21 17:56:28 +00:00
jd
407043c5be Implemented PagedResponse 2026-02-21 17:51:17 +00:00
jd
afa1c13e13 Implemented the ability to add/remove games from collections. 2026-02-21 17:27:46 +00:00
38 changed files with 375 additions and 108 deletions

View File

@@ -5,7 +5,7 @@ info:
http:
method: POST
url: http://localhost:3000/api/auth/login
url: "{{BASE_URL}}/api/auth/login"
headers:
- name: Content-Type
value: application/json
@@ -18,6 +18,16 @@ http:
}
auth: inherit
runtime:
scripts:
- type: after-response
code: |-
function onResponse(res) {
console.log(res.getHeader('set-cookie'));
bru.setEnvVar("REFRESH_COOKIE", res.getHeader('set-cookie'));
}
onResponse(res);
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,12 +5,22 @@ info:
http:
method: GET
url: http://localhost:3000/api/auth/token
url: "{{BASE_URL}}/api/auth/token"
headers:
- name: Cookie
value: refresh=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiMSIsInIiOiIxIiwiaWF0IjoxNzcxNTk3NjQ2LCJleHAiOjE3NzQxODk2NDZ9.07ViS5Nie3Bi2OgnlHyybDNZ9bdXPRRiqO-RFLhjoKo; Path=/; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax
value: "{{REFRESH_COOKIE}}"
auth: inherit
runtime:
scripts:
- type: after-response
code: |-
function onResponse(res) {
let data = res.getBody();
bru.setEnvVar("BEARER_TOKEN", data.token);
}
onResponse(res);
settings:
encodeUrl: true
timeout: 0

View File

@@ -0,0 +1,28 @@
info:
name: Add game
type: http
seq: 6
http:
method: POST
url: "{{BASE_URL}}/api/collection/{{CollectionID}}/add"
body:
type: json
data: |-
{
"gameId": "{{GameID}}"
}
auth: inherit
runtime:
variables:
- name: CollectionID
value: ""
- name: GameID
value: ""
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,15 +5,20 @@ info:
http:
method: POST
url: http://localhost:3000/api/collection
url: "{{BASE_URL}}/api/collection"
body:
type: json
data: |-
{
"name": "Invited player2"
"name": "{{Name}}"
}
auth: inherit
runtime:
variables:
- name: Name
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: DELETE
url: http://localhost:3000/api/collection/:id
params:
- name: id
value: bmOe
type: path
url: "{{BASE_URL}}/api/collection/{{CollectionID}}"
auth: inherit
runtime:
variables:
- name: CollectionID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,18 @@ info:
http:
method: GET
url: http://localhost:3000/api/collection/:id
url: "{{BASE_URL}}/api/collection/{{CollectionID}}"
params:
- name: id
value: ejRe
type: path
- name: ""
value: ""
type: query
auth: inherit
runtime:
variables:
- name: CollectionID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,9 +5,16 @@ info:
http:
method: GET
url: http://localhost:3000/api/collection/list
url: "{{BASE_URL}}/api/collection/list/{{PageSize}}/{{Page}}"
auth: inherit
runtime:
variables:
- name: PageSize
value: "1"
- name: Page
value: "0"
settings:
encodeUrl: true
timeout: 0

View File

@@ -0,0 +1,28 @@
info:
name: Remove game
type: http
seq: 7
http:
method: POST
url: "{{BASE_URL}}/api/collection/{{CollectionID}}/remove"
body:
type: json
data: |-
{
"gameId": "{{GameID}}"
}
auth: inherit
runtime:
variables:
- name: CollectionID
value: ""
- name: GameID
value: ""
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -5,21 +5,22 @@ info:
http:
method: PATCH
url: http://localhost:3000/api/collection/:id
params:
- name: id
value: bmOe
type: path
url: "{{BASE_URL}}/api/collection/{{CollectionID}}"
body:
type: json
data: |-
{
"name": "Test Player",
"isRatingLocked": true,
"canBeMultiple": false
"name": "{{Name}}"
}
auth: inherit
runtime:
variables:
- name: CollectionID
value: ""
- name: Name
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,15 +5,20 @@ info:
http:
method: POST
url: http://localhost:3000/api/game
url: "{{BASE_URL}}/api/game"
body:
type: json
data: |-
{
"name": "Test Game3"
"name": "{{Name}}"
}
auth: inherit
runtime:
variables:
- name: Name
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: DELETE
url: http://localhost:3000/api/game/:id
params:
- name: id
value: bk5e
type: path
url: "{{BASE_URL}}/api/game/{{GameID}}"
auth: inherit
runtime:
variables:
- name: GameID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: GET
url: http://localhost:3000/api/game/:id
params:
- name: id
value: bk5e
type: path
url: "{{BASE_URL}}/api/game/{{GameID}}"
auth: inherit
runtime:
variables:
- name: GameID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,18 @@ info:
http:
method: GET
url: http://localhost:3000/api/game/search/:query
params:
- name: query
value: game
type: path
url: "{{BASE_URL}}/api/game/search/{{Query}}/{{PageSize}}/{{Page}}"
auth: inherit
runtime:
variables:
- name: Query
value: test
- name: PageSize
value: "2"
- name: Page
value: "2"
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,19 +5,22 @@ info:
http:
method: PATCH
url: http://localhost:3000/api/game/:id
params:
- name: id
value: el5a
type: path
url: "{{BASE_URL}}/api/game/{{GameID}}"
body:
type: json
data: |-
{
"name":"Updated game"
"name":"{{Name}}"
}
auth: inherit
runtime:
variables:
- name: GameID
value: ""
- name: Name
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,16 +5,23 @@ info:
http:
method: POST
url: http://localhost:3000/api/invite/accept
url: "{{BASE_URL}}/api/invite/accept"
body:
type: json
data: |-
{
"inviteCode": "3ST6N8",
"password": "test123"
"inviteCode": "{{InviteCode}}",
"password": "{{Password}}
}
auth: inherit
runtime:
variables:
- name: InviteCode
value: ""
- name: Password
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,16 +5,23 @@ info:
http:
method: POST
url: http://localhost:3000/api/invite
url: "{{BASE_URL}}/api/invite"
body:
type: json
data: |-
{
"email": "james+test2@dardry.com",
"playerId": "boja"
"email": "{{Email}}",
"playerId": "{{PlayerID}}"
}
auth: inherit
runtime:
variables:
- name: Email
value: ""
- name: PlayerID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -1,7 +1,7 @@
info:
name: Invites
type: folder
seq: 5
seq: 1
request:
auth: inherit

View File

@@ -5,15 +5,20 @@ info:
http:
method: POST
url: http://localhost:3000/api/player
url: "{{BASE_URL}}/api/player"
body:
type: json
data: |-
{
"name": "Invited player2"
"name": "{{Name}}"
}
auth: inherit
runtime:
variables:
- name: Name
value: Test
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: DELETE
url: http://localhost:3000/api/player/:id
params:
- name: id
value: bmOe
type: path
url: "{{BASE_URL}}/api/player/{{PlayerID}}"
auth: inherit
runtime:
variables:
- name: PlayerID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: GET
url: http://localhost:3000/api/player/:id
params:
- name: id
value: ejRe
type: path
url: "{{BASE_URL}}/api/player/{{PlayerID}}"
auth: inherit
runtime:
variables:
- name: PlayerID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,7 +5,7 @@ info:
http:
method: GET
url: http://localhost:3000/api/player/list
url: "{{BASE_URL}}/api/player/list"
auth: inherit
settings:

View File

@@ -5,21 +5,24 @@ info:
http:
method: PATCH
url: http://localhost:3000/api/player/:id
params:
- name: id
value: bmOe
type: path
url: "{{BASE_URL}}/api/player/{{PlayerID}}"
body:
type: json
data: |-
{
"name": "Test Player",
"isRatingLocked": true,
"name": "{{Name}}",
"isRatingLocked": false,
"canBeMultiple": false
}
auth: inherit
runtime:
variables:
- name: Name
value: Foo
- name: PlayerID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,17 +5,26 @@ info:
http:
method: POST
url: http://localhost:3000/api/user
url: "{{BASE_URL}}/api/user"
body:
type: json
data: |-
{
"email": "Test User",
"password": "Test123",
"playerId": "enRe"
"email": "{{Email}}",
"password": "{{Password}}",
"playerId": "{{PlayerID}}"
}
auth: inherit
runtime:
variables:
- name: Email
value: ""
- name: Password
value: ""
- name: PlayerID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: DELETE
url: http://localhost:3000/api/user/:id
params:
- name: id
value: ""
type: path
url: "{{BASE_URL}}/api/user/{{UserID}}"
auth: inherit
runtime:
variables:
- name: UserID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,13 +5,14 @@ info:
http:
method: GET
url: http://localhost:3000/api/user/:id
params:
- name: id
value: ejRe
type: path
url: "{{BASE_URL}}/api/user/{{UserID}}"
auth: inherit
runtime:
variables:
- name: UserID
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -5,20 +5,28 @@ info:
http:
method: PATCH
url: http://localhost:3000/api/user/:id
params:
- name: id
value: ""
type: path
url: "{{BASE_URL}}/api/user/{{UserID}}"
body:
type: json
data: |-
{
"isActive": true,
"isAdmin": false
"isActive": {{IsActive}},
"isAdmin": {{IsAdmin}},
"email": "{{Email}}"
}
auth: inherit
runtime:
variables:
- name: UserID
value: ""
- name: IsActive
value: ""
- name: IsAdmin
value: ""
- name: Email
value: ""
settings:
encodeUrl: true
timeout: 0

View File

@@ -0,0 +1,8 @@
name: BGApp
variables:
- name: BEARER_TOKEN
value: ""
- name: REFRESH_COOKIE
value: ""
- name: BASE_URL
value: http://localhost:3000

View File

@@ -17,7 +17,7 @@ config:
request:
auth:
type: bearer
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJNUFJLTFIiLCJjbGFpbXMiOlsiQURNSU4iLCJVU0VSU19DUkVBVEUiLCJVU0VSU19TRUxGX1JFQUQiLCJVU0VSU19TRUxGX1VQREFURSIsIlVTRVJTX1NFTEZfREVMRVRFIiwiVVNFUlNfT1RIRVJfUkVBRCIsIlVTRVJTX09USEVSX1VQREFURSJdLCJpYXQiOjE3NzE2ODcxNzAsImV4cCI6MTgwMzIyMzE3MH0.inf1q3LTMuTkzLI-lEezYduPCpidJDaqsWZNNIY_doE
token: "{{BEARER_TOKEN}}"
actions:
- type: set-variable
phase: after-response

View File

@@ -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 { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
import {
GameToCollectionRequest,
CreateCollectionRequest,
UpdateCollectionRequest,
} from '../utilities/requestModels';
import { CollectionId, GameId } from '../utilities/secureIds';
async function create(request: UnwrappedRequest<CreateCollectionRequest>): Promise<Response> {
try {
@@ -23,7 +27,7 @@ async function get(request: UnwrappedRequest): Promise<Response> {
async function list(request: UnwrappedRequest): Promise<Response> {
try {
return new OkResponse(await orm.collections.list(request.claims));
return new PagedResponse(request, await orm.collections.list(request.claims));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
@@ -47,10 +51,40 @@ async function drop(request: UnwrappedRequest): Promise<Response> {
}
}
async function addGame(request: UnwrappedRequest<GameToCollectionRequest>): Promise<Response> {
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<GameToCollectionRequest>): Promise<Response> {
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,
};

View File

@@ -1,6 +1,6 @@
import { orm } from '../orm/orm';
import { UnwrappedRequest } from '../utilities/guard';
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
import {
CreateGameRequest,
UpdateGameRequest,
@@ -55,7 +55,7 @@ async function drop(request: UnwrappedRequest): Promise<Response> {
async function query(request: UnwrappedRequest): Promise<Response> {
try {
return new OkResponse(await orm.games.query(request.params.query));
return new PagedResponse(request, await orm.games.query(request.params.query));
} catch (error: any) {
return new ErrorResponse(error as Error);
}

View File

@@ -1,6 +1,6 @@
import { orm } from '../orm/orm';
import { UnwrappedRequest } from '../utilities/guard';
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
import { CreatePlayerRequest, UpdatePlayerRequest } from '../utilities/requestModels';
import { PlayerId } from '../utilities/secureIds';
@@ -23,7 +23,7 @@ async function get(request: UnwrappedRequest): Promise<Response> {
async function list(request: UnwrappedRequest): Promise<Response> {
try {
return new OkResponse(await orm.players.list(request.claims));
return new PagedResponse(request, await orm.players.list(request.claims));
} catch (error: any) {
return new ErrorResponse(error as Error);
}

View File

@@ -127,4 +127,41 @@ export class CollectionsOrm {
return;
}
async addGame(id: CollectionId, gameId: GameId, claims: Claims): Promise<void> {
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<void> {
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;
}
}

View File

@@ -11,7 +11,21 @@ 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/list': {
'/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/:pageSize/:page': {
GET: guard(collections.list, [Claims.ADMIN, Claims.COLLECTIONS.OWNED.LIST]),
},
};

View File

@@ -11,7 +11,7 @@ export default {
PATCH: guard(games.update, [Claims.ADMIN, Claims.GAMES.UPDATE]),
DELETE: guard(games.drop, [Claims.ADMIN, Claims.GAMES.DELETE]),
},
'/api/game/search/:query': {
'/api/game/search/:query/:pageSize/:page': {
GET: guard(games.query, [Claims.ADMIN, Claims.GAMES.READ]),
},
};

View File

@@ -11,7 +11,7 @@ export default {
PATCH: guard(player.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]),
DELETE: guard(player.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
},
'/api/player/list': {
'/api/player/list/:pageSize/:page': {
GET: guard(player.list, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ]),
},
};

View File

@@ -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 = {

View File

@@ -47,3 +47,6 @@ export interface CreateCollectionRequest {
export interface UpdateCollectionRequest {
name?: string;
}
export interface GameToCollectionRequest {
gameId: string;
}

View File

@@ -1,5 +1,6 @@
import { BadRequestError, NotFoundError, UnauthorizedError } from './errors';
import { isArray } from 'lodash';
import { clamp, isArray } from 'lodash';
import { UnwrappedRequest } from './guard';
export class ErrorResponse extends Response {
//@ts-ignore
@@ -37,9 +38,18 @@ export class NotFoundResponse extends Response {
}
}
export class PagedResponse extends Response {
//@ts-ignore
constructor(request: UnwrappedRequest, body: any[]) {
const pageSize = clamp(parseInt(request.params.pageSize ?? 100), 1, 100);
const page = Math.max(0, parseInt(request.params.page ?? 1) - 1);
return new OkResponse(body.slice(page * pageSize, page * pageSize + pageSize));
}
}
export class OkResponse extends Response {
// @ts-ignore
constructor(body?: Model | null) {
constructor(body?: any) {
if (body) {
return Response.json(
isArray(body)