Tidied up Bruno files. Implemented builder utility to have more powerful route definitions.

This commit is contained in:
jd
2026-02-21 20:41:40 +00:00
parent ed942060a2
commit 3e9d61b8c6
42 changed files with 159 additions and 98 deletions

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/auth/login" url: "{{BASE_URL}}/{{SECTOR}}/login"
headers: headers:
- name: Content-Type - name: Content-Type
value: application/json value: application/json

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{BASE_URL}}/api/auth/token" url: "{{BASE_URL}}/{{SECTOR}}/token"
headers: headers:
- name: Cookie - name: Cookie
value: "{{REFRESH_COOKIE}}" value: "{{REFRESH_COOKIE}}"

View File

@@ -2,3 +2,8 @@ info:
name: Auth name: Auth
type: folder type: folder
seq: 1 seq: 1
request:
variables:
- name: SECTOR
value: auth

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/collection/{{CollectionID}}/add" url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}/add"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/collection" url: "{{BASE_URL}}/{{SECTOR}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: DELETE method: DELETE
url: "{{BASE_URL}}/api/collection/{{CollectionID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

@@ -5,11 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{BASE_URL}}/api/collection/{{CollectionID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}"
params:
- name: ""
value: ""
type: query
auth: inherit auth: inherit
runtime: runtime:

View File

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

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/collection/{{CollectionID}}/remove" url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}/remove"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: PATCH method: PATCH
url: "{{BASE_URL}}/api/collection/{{CollectionID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -1,7 +1,10 @@
info: info:
name: Collections name: Collections
type: folder type: folder
seq: 6 seq: 1
request: request:
auth: inherit auth: inherit
variables:
- name: SECTOR
value: collections

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/game" url: "{{BASE_URL}}/{{SECTOR}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: DELETE method: DELETE
url: "{{BASE_URL}}/api/game/{{GameID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{GameID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{BASE_URL}}/api/game/{{GameID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{GameID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{BASE_URL}}/api/game/search/{{Query}}/{{PageSize}}/{{Page}}" url: "{{BASE_URL}}/{{SECTOR}}/search/{{Query}}/{{PageSize}}/{{Page}}"
auth: inherit auth: inherit
runtime: runtime:
@@ -13,9 +13,9 @@ runtime:
- name: Query - name: Query
value: test value: test
- name: PageSize - name: PageSize
value: "2" value: "5"
- name: Page - name: Page
value: "2" value: "1"
settings: settings:
encodeUrl: true encodeUrl: true

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: PATCH method: PATCH
url: "{{BASE_URL}}/api/game/{{GameID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{GameID}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -1,7 +1,10 @@
info: info:
name: Game name: Game
type: folder type: folder
seq: 4 seq: 1
request: request:
auth: inherit auth: inherit
variables:
- name: SECTOR
value: games

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/invite/accept" url: "{{BASE_URL}}/{{SECTOR}}/accept"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/invite" url: "{{BASE_URL}}/{{SECTOR}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,3 +5,6 @@ info:
request: request:
auth: inherit auth: inherit
variables:
- name: SECTOR
value: invites

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/player" url: "{{BASE_URL}}/{{SECTOR}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: DELETE method: DELETE
url: "{{BASE_URL}}/api/player/{{PlayerID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{PlayerID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{BASE_URL}}/api/player/{{PlayerID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{PlayerID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

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

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: PATCH method: PATCH
url: "{{BASE_URL}}/api/player/{{PlayerID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{PlayerID}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -1,7 +1,10 @@
info: info:
name: Players name: Players
type: folder type: folder
seq: 2 seq: 1
request: request:
auth: inherit auth: inherit
variables:
- name: SECTOR
value: players

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: POST method: POST
url: "{{BASE_URL}}/api/user" url: "{{BASE_URL}}/{{SECTOR}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: DELETE method: DELETE
url: "{{BASE_URL}}/api/user/{{UserID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: GET method: GET
url: "{{BASE_URL}}/api/user/{{UserID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}"
auth: inherit auth: inherit
runtime: runtime:

View File

@@ -5,7 +5,7 @@ info:
http: http:
method: PATCH method: PATCH
url: "{{BASE_URL}}/api/user/{{UserID}}" url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}"
body: body:
type: json type: json
data: |- data: |-

View File

@@ -1,7 +1,10 @@
info: info:
name: User name: User
type: folder type: folder
seq: 3 seq: 1
request: request:
auth: inherit auth: inherit
variables:
- name: SECTOR
value: users

View File

@@ -5,4 +5,4 @@ variables:
- name: REFRESH_COOKIE - name: REFRESH_COOKIE
value: "" value: ""
- name: BASE_URL - name: BASE_URL
value: http://localhost:3000 value: http://localhost:3000/api

View File

@@ -23,7 +23,7 @@ async function login(request: UnwrappedRequest<LoginRequest>): Promise<Response>
u: verify.userId.raw, u: verify.userId.raw,
r: verify.refreshCount, r: verify.refreshCount,
}, },
process.env.JWT_SECRET_KEY as string, process.env.JWT_REFRESH_KEY as string,
{ expiresIn: `${tokenLifeSpanInDays * 24}h` }, { expiresIn: `${tokenLifeSpanInDays * 24}h` },
); );
const cookies = request?.request?.cookies; const cookies = request?.request?.cookies;
@@ -52,7 +52,7 @@ async function token(request: UnwrappedRequest): Promise<Response> {
const refreshToken: { const refreshToken: {
u: string; u: string;
r: string; r: string;
} = jwt.verify(refreshCookie, process.env.JWT_SECRET_KEY as string) as { u: string; r: string }; } = jwt.verify(refreshCookie, process.env.JWT_REFRESH_KEY as string) as { u: string; r: string };
if (!(await orm.users.verifyRefreshCount(UserId.fromID(refreshToken.u), refreshToken.r))) { if (!(await orm.users.verifyRefreshCount(UserId.fromID(refreshToken.u), refreshToken.r))) {
const response = new UnauthorizedResponse('Invalid refresh token'); const response = new UnauthorizedResponse('Invalid refresh token');

View File

@@ -5,21 +5,24 @@ import players from './routes/players';
import games from './routes/games'; import games from './routes/games';
import invites from './routes/invites'; import invites from './routes/invites';
import collections from './routes/collections'; import collections from './routes/collections';
import { buildRoute } from './utilities/routeBuilder';
const server = Bun.serve({ const server = Bun.serve({
routes: { routes: buildRoute({
...auth, [process.env.API_ROOT_PATH ?? '']:{
...users, auth,
...players, users,
...games, players,
...invites, games,
...collections, invites,
'/test': { collections,
},
'test': {
GET: () => { GET: () => {
return new OkResponse(); return new OkResponse();
}, },
}, },
}, }) as any,
// (optional) fallback for unmatched routes: // (optional) fallback for unmatched routes:
fetch(): Response { fetch(): Response {

View File

@@ -1,6 +1,6 @@
import { sql } from 'bun'; import { sql } from 'bun';
import { ClaimDefinition } from '../utilities/claimDefinitions'; import { ClaimDefinition } from '../utilities/claimDefinitions';
import { SecureId, UserId } from '../utilities/secureIds'; import { UserId } from '../utilities/secureIds';
export class Claims extends ClaimDefinition { export class Claims extends ClaimDefinition {
userId?: UserId; userId?: UserId;

View File

@@ -4,19 +4,18 @@ import { OkResponse } from '../utilities/responseHelper';
import { Claims } from '../orm/claims'; import { Claims } from '../orm/claims';
export default { export default {
'/api/auth/login': { login: {
POST: unwrapMethod(auth.login), POST: unwrapMethod(auth.login),
}, },
'/api/auth/token': { token: {
GET: unwrapMethod(auth.token), GET: unwrapMethod(auth.token),
}, },
'/api/auth/logout': { logout: {
POST: unwrapMethod(auth.logout), POST: unwrapMethod(auth.logout),
}, },
'/api/auth/changePassword/:id': { changePassword: {
PATCH: guard(auth.changePassword, [Claims.ADMIN, Claims.USERS.SELF.UPDATE]), ':id': {
}, PATCH: guard(auth.changePassword, [Claims.ADMIN, Claims.USERS.SELF.UPDATE]),
'/api/auth/test': { },
GET: () => new OkResponse(),
}, },
}; };

View File

@@ -3,29 +3,28 @@ import { Claims } from '../orm/claims';
import collections from '../endpoints/collections'; import collections from '../endpoints/collections';
export default { export default {
'/api/collection': { 'POST': guard(collections.create, [Claims.ADMIN, Claims.COLLECTIONS.CREATE]),
POST: guard(collections.create, [Claims.ADMIN, Claims.COLLECTIONS.CREATE]), ':id': {
},
'/api/collection/:id': {
GET: guard(collections.get, [Claims.ADMIN, Claims.COLLECTIONS.UNOWNED.READ, Claims.COLLECTIONS.OWNED.READ]), GET: guard(collections.get, [Claims.ADMIN, Claims.COLLECTIONS.UNOWNED.READ, Claims.COLLECTIONS.OWNED.READ]),
// PATCH: guard(collections.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]), 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]), DELETE: guard(collections.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
add: {
POST: guard(collections.addGame, [
Claims.ADMIN,
Claims.COLLECTIONS.UNOWNED.GAME.ADD,
Claims.COLLECTIONS.OWNED.GAME.ADD,
]),
},
remove: {
POST: guard(collections.removeGame, [
Claims.ADMIN,
Claims.COLLECTIONS.UNOWNED.GAME.REMOVE,
Claims.COLLECTIONS.OWNED.GAME.REMOVE,
]),
},
}, },
'/api/collection/:id/add': { 'list': {
POST: guard(collections.addGame, [ variants: [':pageSize/:page', ':page'],
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]), GET: guard(collections.list, [Claims.ADMIN, Claims.COLLECTIONS.OWNED.LIST]),
}, },
}; };

View File

@@ -3,15 +3,16 @@ import { Claims } from '../orm/claims';
import games from '../endpoints/games'; import games from '../endpoints/games';
export default { export default {
'/api/game': { 'POST': guard(games.create, [Claims.ADMIN, Claims.GAMES.CREATE]),
POST: guard(games.create, [Claims.ADMIN, Claims.GAMES.CREATE]), ':id': {
},
'/api/game/:id': {
GET: guard(games.get, [Claims.ADMIN, Claims.GAMES.READ]), GET: guard(games.get, [Claims.ADMIN, Claims.GAMES.READ]),
PATCH: guard(games.update, [Claims.ADMIN, Claims.GAMES.UPDATE]), PATCH: guard(games.update, [Claims.ADMIN, Claims.GAMES.UPDATE]),
DELETE: guard(games.drop, [Claims.ADMIN, Claims.GAMES.DELETE]), DELETE: guard(games.drop, [Claims.ADMIN, Claims.GAMES.DELETE]),
}, },
'/api/game/search/:query/:pageSize/:page': { 'search': {
GET: guard(games.query, [Claims.ADMIN, Claims.GAMES.READ]), ':query': {
variants: [':pageSize/:page', ':page'],
GET: guard(games.query, [Claims.ADMIN, Claims.GAMES.READ]),
},
}, },
}; };

View File

@@ -3,10 +3,8 @@ import { Claims } from '../orm/claims';
import invite from '../endpoints/invites'; import invite from '../endpoints/invites';
export default { export default {
'/api/invite': { POST: guard(invite.create, [Claims.ADMIN, Claims.USERS.INVITE]),
POST: guard(invite.create, [Claims.ADMIN, Claims.USERS.INVITE]), accept: {
},
'/api/invite/accept': {
POST: unwrapMethod(invite.accept), POST: unwrapMethod(invite.accept),
}, },
}; };

View File

@@ -3,15 +3,14 @@ import { Claims } from '../orm/claims';
import player from '../endpoints/players'; import player from '../endpoints/players';
export default { export default {
'/api/player': { 'POST': guard(player.create, [Claims.ADMIN, Claims.PLAYERS.CREATE]),
POST: guard(player.create, [Claims.ADMIN, Claims.PLAYERS.CREATE]), ':id': {
},
'/api/player/:id': {
GET: guard(player.get, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ, Claims.PLAYERS.SELF.READ]), GET: guard(player.get, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ, Claims.PLAYERS.SELF.READ]),
PATCH: guard(player.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]), 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]), DELETE: guard(player.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
}, },
'/api/player/list/:pageSize/:page': { 'list': {
variants: [':pageSize/:page', ':page'],
GET: guard(player.list, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ]), GET: guard(player.list, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ]),
}, },
}; };

View File

@@ -3,10 +3,8 @@ import user from '../endpoints/users';
import { Claims } from '../orm/claims'; import { Claims } from '../orm/claims';
export default { export default {
'/api/user': { 'POST': guard(user.create, [Claims.ADMIN, Claims.USERS.CREATE]),
POST: guard(user.create, [Claims.ADMIN, Claims.USERS.CREATE]), ':id': {
},
'/api/user/:id': {
GET: guard(user.get, [Claims.ADMIN, Claims.USERS.OTHER.READ, Claims.USERS.SELF.READ]), GET: guard(user.get, [Claims.ADMIN, Claims.USERS.OTHER.READ, Claims.USERS.SELF.READ]),
PATCH: guard(user.update, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]), PATCH: guard(user.update, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]),
DELETE: guard(user.drop, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]), DELETE: guard(user.drop, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]),

View File

@@ -0,0 +1,41 @@
export function buildRoute(
route: any,
currentPath: string = '',
): {
[x: string]: {
POST?: Function;
GET?: Function;
PUT?: Function;
DELETE?: Function;
};
} {
let returnValue: { [x: string]: any } = {};
const keys = Object.keys(route);
for (let i in keys) {
const key = keys[i];
if (key === 'POST' || key === 'GET' || key === 'PUT' || key === 'DELETE' || key === 'variants') {
continue;
}
returnValue = {
...returnValue,
...buildRoute(route[key], `${currentPath}/${key}`),
};
}
if (route.variants || route.POST || route.GET || route.PUT || route.DELETE) {
const variants: string[] = route.variants ?? [];
const endpointDefinition = {
POST: route.POST,
GET: route.GET,
PUT: route.PUT,
DELETE: route.DELETE,
};
returnValue[currentPath] = endpointDefinition;
for (let key in variants) {
returnValue[`${currentPath}/${variants[key]}`] = endpointDefinition;
}
}
return returnValue;
}