diff --git a/.gitignore b/.gitignore
index e96b170..52ac7f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,4 @@ package-lock.json
.env.test
.env*
.DS_Store
-Thumbs.db
-.DS_Store
Thumbs.db
\ No newline at end of file
diff --git a/.idea_old/.gitignore b/.idea_old/.gitignore
new file mode 100644
index 0000000..ab1f416
--- /dev/null
+++ b/.idea_old/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea_old/codeStyles/Project.xml b/.idea_old/codeStyles/Project.xml
new file mode 100644
index 0000000..4c8ee25
--- /dev/null
+++ b/.idea_old/codeStyles/Project.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea_old/codeStyles/codeStyleConfig.xml b/.idea_old/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea_old/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea_old/vcs.xml b/.idea_old/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea_old/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/API Tests/BGApp/Collections/Create.yml b/API Tests/BGApp/Collections/Create.yml
new file mode 100644
index 0000000..4b6ad67
--- /dev/null
+++ b/API Tests/BGApp/Collections/Create.yml
@@ -0,0 +1,21 @@
+info:
+ name: Create
+ type: http
+ seq: 1
+
+http:
+ method: POST
+ url: http://localhost:3000/api/collection
+ body:
+ type: json
+ data: |-
+ {
+ "name": "Invited player2"
+ }
+ auth: inherit
+
+settings:
+ encodeUrl: true
+ timeout: 0
+ followRedirects: true
+ maxRedirects: 5
diff --git a/API Tests/BGApp/Collections/Delete.yml b/API Tests/BGApp/Collections/Delete.yml
new file mode 100644
index 0000000..0dac8f7
--- /dev/null
+++ b/API Tests/BGApp/Collections/Delete.yml
@@ -0,0 +1,19 @@
+info:
+ name: Delete
+ type: http
+ seq: 4
+
+http:
+ method: DELETE
+ url: http://localhost:3000/api/collection/:id
+ params:
+ - name: id
+ value: bmOe
+ type: path
+ auth: inherit
+
+settings:
+ encodeUrl: true
+ timeout: 0
+ followRedirects: true
+ maxRedirects: 5
diff --git a/API Tests/BGApp/Collections/Get.yml b/API Tests/BGApp/Collections/Get.yml
new file mode 100644
index 0000000..bacde05
--- /dev/null
+++ b/API Tests/BGApp/Collections/Get.yml
@@ -0,0 +1,19 @@
+info:
+ name: Get
+ type: http
+ seq: 2
+
+http:
+ method: GET
+ url: http://localhost:3000/api/collection/:id
+ params:
+ - name: id
+ value: ejRe
+ type: path
+ auth: inherit
+
+settings:
+ encodeUrl: true
+ timeout: 0
+ followRedirects: true
+ maxRedirects: 5
diff --git a/API Tests/BGApp/Collections/List.yml b/API Tests/BGApp/Collections/List.yml
new file mode 100644
index 0000000..554f35c
--- /dev/null
+++ b/API Tests/BGApp/Collections/List.yml
@@ -0,0 +1,15 @@
+info:
+ name: List
+ type: http
+ seq: 5
+
+http:
+ method: GET
+ url: http://localhost:3000/api/collection/list
+ auth: inherit
+
+settings:
+ encodeUrl: true
+ timeout: 0
+ followRedirects: true
+ maxRedirects: 5
diff --git a/API Tests/BGApp/Collections/Update.yml b/API Tests/BGApp/Collections/Update.yml
new file mode 100644
index 0000000..dbb4fe0
--- /dev/null
+++ b/API Tests/BGApp/Collections/Update.yml
@@ -0,0 +1,27 @@
+info:
+ name: Update
+ type: http
+ seq: 3
+
+http:
+ method: PATCH
+ url: http://localhost:3000/api/collection/:id
+ params:
+ - name: id
+ value: bmOe
+ type: path
+ body:
+ type: json
+ data: |-
+ {
+ "name": "Test Player",
+ "isRatingLocked": true,
+ "canBeMultiple": false
+ }
+ auth: inherit
+
+settings:
+ encodeUrl: true
+ timeout: 0
+ followRedirects: true
+ maxRedirects: 5
diff --git a/API Tests/BGApp/Collections/folder.yml b/API Tests/BGApp/Collections/folder.yml
new file mode 100644
index 0000000..de23efa
--- /dev/null
+++ b/API Tests/BGApp/Collections/folder.yml
@@ -0,0 +1,7 @@
+info:
+ name: Collections
+ type: folder
+ seq: 6
+
+request:
+ auth: inherit
diff --git a/API Tests/BGApp/opencollection.yml b/API Tests/BGApp/opencollection.yml
index ea6e588..7b744d0 100644
--- a/API Tests/BGApp/opencollection.yml
+++ b/API Tests/BGApp/opencollection.yml
@@ -17,7 +17,7 @@ config:
request:
auth:
type: bearer
- token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJlalJlIiwiY2xhaW1zIjpbIkFETUlOIiwiVVNFUlNfQ1JFQVRFIiwiVVNFUlNfU0VMRl9SRUFEIiwiVVNFUlNfU0VMRl9VUERBVEUiLCJVU0VSU19TRUxGX0RFTEVURSIsIlVTRVJTX09USEVSX1JFQUQiLCJVU0VSU19PVEhFUl9VUERBVEUiXSwiaWF0IjoxNzcxNjE4NTQzLCJleHAiOjE4MDMxNTQ1NDN9.R-3Qb5CEcLJBSt7DnsO9b0IGRVYDIZuFfH1m9TikVXU
+ token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJNUFJLTFIiLCJjbGFpbXMiOlsiQURNSU4iLCJVU0VSU19DUkVBVEUiLCJVU0VSU19TRUxGX1JFQUQiLCJVU0VSU19TRUxGX1VQREFURSIsIlVTRVJTX1NFTEZfREVMRVRFIiwiVVNFUlNfT1RIRVJfUkVBRCIsIlVTRVJTX09USEVSX1VQREFURSJdLCJpYXQiOjE3NzE2ODcxNzAsImV4cCI6MTgwMzIyMzE3MH0.inf1q3LTMuTkzLI-lEezYduPCpidJDaqsWZNNIY_doE
actions:
- type: set-variable
phase: after-response
diff --git a/src/endpoints/auth.ts b/src/endpoints/auth.ts
index 73e67f6..49dab6e 100644
--- a/src/endpoints/auth.ts
+++ b/src/endpoints/auth.ts
@@ -3,12 +3,13 @@ import jwt from 'jsonwebtoken';
import { UnwrappedRequest } from '../utilities/guard';
import { ErrorResponse, OkResponse, UnauthorizedResponse } from '../utilities/responseHelper';
import { Claims } from '../orm/claims';
-import { ChangePasswordRequest, LoginRequest, SecureId } from '../utilities/requestModels';
+import { ChangePasswordRequest, LoginRequest } from '../utilities/requestModels';
+import { UserId } from '../utilities/secureIds';
async function login(request: UnwrappedRequest): Promise {
try {
const verify: {
- userId: SecureId;
+ userId: UserId;
refreshCount: string;
} | null = await orm.users.verifyCredentials(request.body.email, request.body.password);
if (!verify) {
@@ -53,7 +54,7 @@ async function token(request: UnwrappedRequest): Promise {
r: string;
} = jwt.verify(refreshCookie, process.env.JWT_SECRET_KEY as string) as { u: string; r: string };
- if (!(await orm.users.verifyRefreshCount(SecureId.fromID(refreshToken.u), refreshToken.r))) {
+ if (!(await orm.users.verifyRefreshCount(UserId.fromID(refreshToken.u), refreshToken.r))) {
const response = new UnauthorizedResponse('Invalid refresh token');
response.headers.set('Clear-Site-Data', '"cookies","cache","storage","executionContexts"');
return response;
@@ -84,7 +85,7 @@ async function changePassword(request: UnwrappedRequest):
try {
return new OkResponse(
await orm.users.changePassword(
- SecureId.fromHash(request.params.id),
+ UserId.fromHash(request.params.id),
request.body.oldPassword,
request.body.newPassword,
request.claims,
diff --git a/src/endpoints/collections.ts b/src/endpoints/collections.ts
new file mode 100644
index 0000000..12c5bf5
--- /dev/null
+++ b/src/endpoints/collections.ts
@@ -0,0 +1,56 @@
+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';
+
+async function create(request: UnwrappedRequest): Promise {
+ try {
+ const newPlayer = await orm.collections.create(request.body, request.claims);
+ return new CreatedResponse(newPlayer);
+ } catch (error: any) {
+ return new ErrorResponse(error as Error);
+ }
+}
+
+async function get(request: UnwrappedRequest): Promise {
+ try {
+ return new OkResponse(await orm.collections.get(CollectionId.fromHash(request.params.id), request.claims));
+ } catch (error: any) {
+ return new ErrorResponse(error as Error);
+ }
+}
+
+async function list(request: UnwrappedRequest): Promise {
+ try {
+ return new OkResponse(await orm.collections.list(request.claims));
+ } catch (error: any) {
+ return new ErrorResponse(error as Error);
+ }
+}
+
+async function update(request: UnwrappedRequest): Promise {
+ try {
+ return new OkResponse(
+ await orm.collections.update(CollectionId.fromHash(request.params.id), request.body, request.claims),
+ );
+ } catch (error: any) {
+ return new ErrorResponse(error as Error);
+ }
+}
+
+async function drop(request: UnwrappedRequest): Promise {
+ try {
+ return new OkResponse(await orm.collections.drop(CollectionId.fromHash(request.params.id), request.claims));
+ } catch (error: any) {
+ return new ErrorResponse(error as Error);
+ }
+}
+
+export default {
+ create,
+ get,
+ list,
+ update,
+ drop,
+};
diff --git a/src/endpoints/games.ts b/src/endpoints/games.ts
index 68d0f16..a8bd145 100644
--- a/src/endpoints/games.ts
+++ b/src/endpoints/games.ts
@@ -3,9 +3,9 @@ import { UnwrappedRequest } from '../utilities/guard';
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
import {
CreateGameRequest,
- SecureId,
UpdateGameRequest,
} from '../utilities/requestModels';
+import { GameId } from '../utilities/secureIds';
async function create(request: UnwrappedRequest): Promise {
try {
@@ -25,7 +25,7 @@ async function create(request: UnwrappedRequest): Promise {
try {
- return new OkResponse(await orm.games.get(SecureId.fromHash(request.params.id)));
+ return new OkResponse(await orm.games.get(GameId.fromHash(request.params.id)));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
@@ -35,7 +35,7 @@ async function update(request: UnwrappedRequest): Promise): Promise {
try {
- return new OkResponse(await orm.games.drop(SecureId.fromHash(request.params.id)));
+ return new OkResponse(await orm.games.drop(GameId.fromHash(request.params.id)));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
diff --git a/src/endpoints/invite.ts b/src/endpoints/invites.ts
similarity index 83%
rename from src/endpoints/invite.ts
rename to src/endpoints/invites.ts
index 824bec8..feadfae 100644
--- a/src/endpoints/invite.ts
+++ b/src/endpoints/invites.ts
@@ -4,16 +4,16 @@ import { CreatedResponse, ErrorResponse } from '../utilities/responseHelper';
import {
AcceptInviteRequest,
InviteUserRequest,
- SecureId,
} from '../utilities/requestModels';
+import { PlayerId, UserId } from '../utilities/secureIds';
async function create(request: UnwrappedRequest): Promise {
try {
const newUser = await orm.invites.create(
{
...request.body,
- playerId: SecureId.fromHash(request.body.playerId),
- invitedByUserId: request.claims.userId as SecureId,
+ playerId: PlayerId.fromHash(request.body.playerId),
+ invitedByUserId: request.claims.userId as UserId,
},
);
return new CreatedResponse(newUser);
diff --git a/src/endpoints/player.ts b/src/endpoints/players.ts
similarity index 78%
rename from src/endpoints/player.ts
rename to src/endpoints/players.ts
index d97fc87..5551291 100644
--- a/src/endpoints/player.ts
+++ b/src/endpoints/players.ts
@@ -1,11 +1,12 @@
import { orm } from '../orm/orm';
import { UnwrappedRequest } from '../utilities/guard';
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
-import { CreatePlayerRequest, SecureId, UpdatePlayerRequest } from '../utilities/requestModels';
+import { CreatePlayerRequest, UpdatePlayerRequest } from '../utilities/requestModels';
+import { PlayerId } from '../utilities/secureIds';
async function create(request: UnwrappedRequest): Promise {
try {
- const newPlayer = await orm.players.create(request.body, request.claims);
+ const newPlayer = await orm.players.create(request.body);
return new CreatedResponse(newPlayer);
} catch (error: any) {
return new ErrorResponse(error as Error);
@@ -14,7 +15,7 @@ async function create(request: UnwrappedRequest): Promise {
try {
- return new OkResponse(await orm.players.get(SecureId.fromHash(request.params.id), request.claims));
+ return new OkResponse(await orm.players.get(PlayerId.fromHash(request.params.id), request.claims));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
@@ -31,7 +32,7 @@ async function list(request: UnwrappedRequest): Promise {
async function update(request: UnwrappedRequest): Promise {
try {
return new OkResponse(
- await orm.players.update(SecureId.fromHash(request.params.id), request.body, request.claims),
+ await orm.players.update(PlayerId.fromHash(request.params.id), request.body, request.claims),
);
} catch (error: any) {
return new ErrorResponse(error as Error);
@@ -40,7 +41,7 @@ async function update(request: UnwrappedRequest): Promise {
try {
- return new OkResponse(await orm.players.drop(SecureId.fromHash(request.params.id), request.claims));
+ return new OkResponse(await orm.players.drop(PlayerId.fromHash(request.params.id), request.claims));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
diff --git a/src/endpoints/user.ts b/src/endpoints/users.ts
similarity index 68%
rename from src/endpoints/user.ts
rename to src/endpoints/users.ts
index bc355b4..55a2ae8 100644
--- a/src/endpoints/user.ts
+++ b/src/endpoints/users.ts
@@ -1,16 +1,16 @@
import { orm } from '../orm/orm';
import { UnwrappedRequest } from '../utilities/guard';
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
-import { CreateUserRequest, SecureId, UpdateUserRequest } from '../utilities/requestModels';
+import { CreateUserRequest, UpdateUserRequest } from '../utilities/requestModels';
+import { PlayerId, UserId } from '../utilities/secureIds';
async function create(request: UnwrappedRequest): Promise {
try {
const newUser = await orm.users.create(
{
...request.body,
- playerId: SecureId.fromHash(request.body.playerId),
- },
- request.claims,
+ playerId: PlayerId.fromHash(request.body.playerId),
+ }
);
return new CreatedResponse(newUser);
} catch (error: any) {
@@ -20,7 +20,7 @@ async function create(request: UnwrappedRequest): Promise {
try {
- return new OkResponse(await orm.users.get(SecureId.fromHash(request.params.id), request.claims));
+ return new OkResponse(await orm.users.get(UserId.fromHash(request.params.id), request.claims));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
@@ -29,7 +29,7 @@ async function get(request: UnwrappedRequest): Promise {
async function update(request: UnwrappedRequest): Promise {
try {
return new OkResponse(
- await orm.users.update(SecureId.fromHash(request.params.id), request.body, request.claims),
+ await orm.users.update(UserId.fromHash(request.params.id), request.body, request.claims),
);
} catch (error: any) {
return new ErrorResponse(error as Error);
@@ -38,7 +38,7 @@ async function update(request: UnwrappedRequest): Promise {
try {
- return new OkResponse(await orm.users.drop(SecureId.fromHash(request.params.id), request.claims));
+ return new OkResponse(await orm.users.drop(UserId.fromHash(request.params.id), request.claims));
} catch (error: any) {
return new ErrorResponse(error as Error);
}
diff --git a/src/index.ts b/src/index.ts
index ec78d99..1733b1a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,17 +1,19 @@
-import auth from './routes/auth';
-import user from './routes/user';
-import player from './routes/player';
-import game from './routes/game';
import { OkResponse } from './utilities/responseHelper';
-import invite from './routes/invite';
+import auth from './routes/auth';
+import users from './routes/users';
+import players from './routes/players';
+import games from './routes/games';
+import invites from './routes/invites';
+import collections from './routes/collections';
const server = Bun.serve({
routes: {
...auth,
- ...user,
- ...player,
- ...game,
- ...invite,
+ ...users,
+ ...players,
+ ...games,
+ ...invites,
+ ...collections,
'/test': {
GET: () => {
return new OkResponse();
diff --git a/src/orm/claims.ts b/src/orm/claims.ts
index c732823..cb788d9 100644
--- a/src/orm/claims.ts
+++ b/src/orm/claims.ts
@@ -1,14 +1,14 @@
import { sql } from 'bun';
import { ClaimDefinition } from '../utilities/claimDefinitions';
-import { SecureId } from '../utilities/requestModels';
+import { SecureId, UserId } from '../utilities/secureIds';
export class Claims extends ClaimDefinition {
- userId?: SecureId;
+ userId?: UserId;
claims: string[] = [];
- constructor(raw?:{userId?:string, claims?: string[]}) {
+ constructor(raw?: { userId?: string; claims?: string[] }) {
super();
- this.userId = raw?.userId ? SecureId.fromHash(raw.userId) : undefined;
+ this.userId = raw?.userId ? UserId.fromHash(raw.userId) : undefined;
this.claims = raw?.claims ?? [];
}
@@ -24,7 +24,7 @@ export class ClaimsOrm {
JOIN claims as c on uc.claim_id = c.id
where uc.user_id = ${userId};`;
const claims = new Claims();
- claims.userId = SecureId.fromID(userId);
+ claims.userId = UserId.fromID(userId);
claims.claims = dbResults.map((x) => x.name);
return claims;
}
diff --git a/src/orm/collections.ts b/src/orm/collections.ts
new file mode 100644
index 0000000..0863c77
--- /dev/null
+++ b/src/orm/collections.ts
@@ -0,0 +1,130 @@
+import { Claims } from './claims';
+import { sql } from 'bun';
+import { first } from 'lodash';
+import { NotFoundError, UnauthorizedError } from '../utilities/errors';
+import { UpdateCollectionRequest } from '../utilities/requestModels';
+import { Game } from './games';
+import { CollectionId, GameId } from '../utilities/secureIds';
+
+export class Collection {
+ id: CollectionId;
+ name: string;
+ games: Game[];
+
+ constructor(input: { id: CollectionId; name: string; games?: Game[] }) {
+ this.id = input.id;
+ this.name = input?.name;
+ this.games = input.games ?? [];
+ }
+}
+
+export class CollectionsOrm {
+ async create(model: { name: string }, claims: Claims): Promise {
+ await sql`INSERT INTO collections (name, user_id)
+ VALUES (${model.name}, ${claims?.userId?.raw})`;
+ const newPCollectionId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
+
+ return await this.get(CollectionId.fromID(newPCollectionId));
+ }
+
+ async get(id: CollectionId, claims?: Claims): Promise {
+ const dbResult: any = await sql`SELECT
+ c.id AS collection_id,
+ c.name AS collection_name,
+ c.user_id AS user_id,
+ g.id AS game_id,
+ g.name AS game_name
+ FROM collections c
+ LEFT JOIN collection_games cg ON cg.collection_id = c.id
+ LEFT JOIN games g ON g.id = cg.game_id
+ WHERE c.id = ${id.raw}`;
+
+ if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.READ, claims))) {
+ throw new UnauthorizedError();
+ } else if (
+ Claims.test(Claims.COLLECTIONS.OWNED.READ, claims) &&
+ claims?.userId &&
+ dbResult?.[0]?.user_id !== claims.userId.raw
+ ) {
+ throw new UnauthorizedError();
+ }
+
+ if (!dbResult?.length) {
+ throw new NotFoundError('No matching player exists');
+ }
+
+ return new Collection({
+ id: CollectionId.fromID(dbResult[0].collection_id),
+ name: dbResult[0].collection_name,
+ games: dbResult
+ .filter((x: { game_id: string; game_name: string }) => x.game_id)
+ .map(
+ (x: { game_id: string; game_name: string }) =>
+ new Game({
+ id: GameId.fromID(x.game_id),
+ name: x.game_name,
+ }),
+ ),
+ });
+ }
+
+ async list(claims?: Claims): Promise {
+ if (!claims || Claims.test(Claims.ADMIN, claims)) {
+ return (await sql`SELECT * FROM collections`).map(
+ (x: { id: string; name: string }) =>
+ new Collection({
+ id: CollectionId.fromID(x.id),
+ name: x.name,
+ }),
+ );
+ }
+
+ if (!Claims.test(Claims.COLLECTIONS.OWNED.LIST, claims)) {
+ throw new UnauthorizedError();
+ }
+
+ return (await sql`SELECT * FROM collections WHERE user_id=${claims.userId?.raw}`).map(
+ (x: { id: string; name: string }) =>
+ new Collection({
+ id: CollectionId.fromID(x.id),
+ name: x.name,
+ }),
+ );
+ }
+
+ async update(id: CollectionId, patch: UpdateCollectionRequest, claims?: Claims): Promise {
+ const collection = await this.get(id);
+
+ if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.UPDATE, claims))) {
+ throw new UnauthorizedError();
+ } else if (
+ Claims.test(Claims.COLLECTIONS.OWNED.UPDATE, claims) &&
+ claims?.userId &&
+ collection.id !== claims.userId
+ ) {
+ throw new UnauthorizedError();
+ }
+ collection.name = patch.name ?? collection.name;
+
+ await sql`UPDATE collections SET name=${collection.name} WHERE id=${id.raw}`;
+
+ return await this.get(id);
+ }
+
+ async drop(id: CollectionId, claims?: Claims): Promise {
+ const collection = await this.get(id);
+ if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.DELETE, claims))) {
+ throw new UnauthorizedError();
+ } else if (
+ Claims.test(Claims.COLLECTIONS.OWNED.DELETE, claims) &&
+ claims?.userId &&
+ collection.id !== claims.userId
+ ) {
+ throw new UnauthorizedError();
+ }
+
+ await sql`DELETE FROM collections WHERE id=${id.raw}`;
+
+ return;
+ }
+}
diff --git a/src/orm/games.ts b/src/orm/games.ts
index 930d50f..baf1dd1 100644
--- a/src/orm/games.ts
+++ b/src/orm/games.ts
@@ -1,17 +1,18 @@
import { Claims } from './claims';
import { sql } from 'bun';
import { first } from 'lodash';
-import { NotFoundError, UnauthorizedError } from '../utilities/errors';
-import { CreateGameRequest, SecureId, UpdateGameRequest } from '../utilities/requestModels';
+import { NotFoundError } from '../utilities/errors';
+import { CreateGameRequest, UpdateGameRequest } from '../utilities/requestModels';
import { memo } from '../utilities/helpers';
+import { GameId } from '../utilities/secureIds';
export class Game {
- id: SecureId;
+ id: GameId;
name: string;
imagePath?: string;
bggId?: string;
- constructor(input: { id: SecureId; name: string; imagePath?: string; bggId?: string }) {
+ constructor(input: { id: GameId; name: string; imagePath?: string; bggId?: string }) {
this.id = input.id;
this.name = input?.name;
this.imagePath = input?.imagePath;
@@ -25,10 +26,10 @@ export class GamesOrm {
VALUES (${model.name}, ${Claims.test(Claims.GAMES.MANAGE_IMAGES, claims) ? model.imagePath : null}, ${model.bggId})`;
const newGameId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
- return await this.get(SecureId.fromID(newGameId));
+ return await this.get(GameId.fromID(newGameId));
}
- async get(id: SecureId): Promise {
+ async get(id: GameId): Promise {
const dbResult: any = first(
await sql`SELECT *
FROM games
@@ -41,14 +42,14 @@ export class GamesOrm {
}
return new Game({
- id: SecureId.fromID(dbResult.id),
+ id: GameId.fromID(dbResult.id),
name: dbResult.name,
bggId: dbResult.bgg_id,
imagePath: dbResult.image_path,
});
}
- async update(id: SecureId, patch: UpdateGameRequest, claims?: Claims): Promise {
+ async update(id: GameId, patch: UpdateGameRequest, claims?: Claims): Promise {
const gameToUpdate = await this.get(id);
gameToUpdate.name = patch.name ?? gameToUpdate.name;
gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId;
@@ -66,7 +67,7 @@ export class GamesOrm {
return await this.get(id);
}
- async drop(id: SecureId): Promise {
+ async drop(id: GameId): Promise {
// Ensure player exists before attempting to delete
await this.get(id);
await sql.transaction(async (tx) => {
@@ -87,7 +88,7 @@ export class GamesOrm {
return;
}
- query: (query: string) => Promise = memo<(query: string) => Promise,Game[]>(this.#query);
+ query: (query: string) => Promise = memo<(query: string) => Promise, Game[]>(this.#query);
async #query(query: string): Promise {
const dbResult: any = await sql` SELECT
id, name
@@ -103,7 +104,7 @@ export class GamesOrm {
return dbResult.map(
(x: { id: string; name: string }) =>
new Game({
- id: SecureId.fromID(x.id),
+ id: GameId.fromID(x.id),
name: x.name,
}),
);
diff --git a/src/orm/invites.ts b/src/orm/invites.ts
index eb5debd..7c0160e 100644
--- a/src/orm/invites.ts
+++ b/src/orm/invites.ts
@@ -1,7 +1,7 @@
import { sql } from 'bun';
import { first } from 'lodash';
import { BadRequestError, InternalServerError, NotFoundError, UnauthorizedError } from '../utilities/errors';
-import { SecureId } from '../utilities/requestModels';
+import { PlayerId, UserId } from '../utilities/secureIds';
import { createRandomString } from '../utilities/helpers';
import { Resend } from 'resend';
import { orm } from './orm';
@@ -17,8 +17,8 @@ export class InvitesOrm {
invitedByUserId,
}: {
email: string;
- playerId: SecureId;
- invitedByUserId: SecureId;
+ playerId: PlayerId;
+ invitedByUserId: UserId;
},
claims?: Claims,
): Promise {
@@ -120,7 +120,7 @@ export class InvitesOrm {
const createdUser = await orm.users.create({
email: invite.email,
- playerId: SecureId.fromID(invite.player_id),
+ playerId: PlayerId.fromID(invite.player_id),
password,
});
diff --git a/src/orm/orm.ts b/src/orm/orm.ts
index 9bcd7ac..1f154c7 100644
--- a/src/orm/orm.ts
+++ b/src/orm/orm.ts
@@ -3,6 +3,7 @@ import { UsersOrm } from './user';
import { PlayersOrm } from './players';
import { GamesOrm } from './games';
import { InvitesOrm } from './invites';
+import { CollectionsOrm } from './collections';
class Orm {
readonly claims: ClaimsOrm = new ClaimsOrm();
@@ -10,6 +11,7 @@ class Orm {
readonly players: PlayersOrm = new PlayersOrm();
readonly games: GamesOrm = new GamesOrm();
readonly invites: InvitesOrm = new InvitesOrm();
+ readonly collections: CollectionsOrm = new CollectionsOrm();
}
export const orm = new Orm();
diff --git a/src/orm/players.ts b/src/orm/players.ts
index 7e366a0..59e0a38 100644
--- a/src/orm/players.ts
+++ b/src/orm/players.ts
@@ -3,17 +3,18 @@ import { sql } from 'bun';
import { first } from 'lodash';
import { NotFoundError, UnauthorizedError } from '../utilities/errors';
import { orm } from './orm';
-import { SecureId, UpdatePlayerRequest } from '../utilities/requestModels';
+import { UpdatePlayerRequest } from '../utilities/requestModels';
+import { PlayerId } from '../utilities/secureIds';
export class Player {
- id: SecureId;
+ id: PlayerId;
name: string;
elo: number;
isRatingLocked: boolean;
canBeMultiple: boolean;
constructor(input: {
- id: SecureId;
+ id: PlayerId;
name: string;
elo?: number;
isRatingLocked?: boolean;
@@ -28,15 +29,15 @@ export class Player {
}
export class PlayersOrm {
- async create(model: { name: string }, claims?: Claims): Promise {
+ async create(model: { name: string }): Promise {
await sql`INSERT INTO players (name)
VALUES (${model.name})`;
const newPlayerId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
- return await this.get(SecureId.fromID(newPlayerId));
+ return await this.get(PlayerId.fromID(newPlayerId));
}
- async get(id: SecureId, claims?: Claims): Promise {
+ async get(id: PlayerId, claims?: Claims): Promise {
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.READ, claims))) {
throw new UnauthorizedError();
} else if (Claims.test(Claims.PLAYERS.SELF.READ, claims) && claims?.userId) {
@@ -57,7 +58,7 @@ export class PlayersOrm {
}
return new Player({
- id: SecureId.fromID(dbResult.id),
+ id: PlayerId.fromID(dbResult.id),
name: dbResult.name,
elo: parseInt(dbResult.elo),
isRatingLocked: dbResult.is_rating_locked,
@@ -70,7 +71,7 @@ export class PlayersOrm {
return (await sql`SELECT * FROM players`).map(
(x: { id: string; name: string; elo: string; is_rating_locked: boolean; can_be_multiple: boolean }) =>
new Player({
- id: SecureId.fromID(x.id),
+ id: PlayerId.fromID(x.id),
name: x.name,
elo: parseInt(x.elo),
isRatingLocked: x.is_rating_locked,
@@ -98,7 +99,7 @@ export class PlayersOrm {
).map(
(x: { id: string; name: string; elo: string; is_rating_locked: boolean; can_be_multiple: boolean }) =>
new Player({
- id: SecureId.fromID(x.id),
+ id: PlayerId.fromID(x.id),
name: x.name,
elo: parseInt(x.elo),
isRatingLocked: x.is_rating_locked,
@@ -107,7 +108,7 @@ export class PlayersOrm {
);
}
- async update(id: SecureId, patch: UpdatePlayerRequest, claims?: Claims): Promise {
+ async update(id: PlayerId, patch: UpdatePlayerRequest, claims?: Claims): Promise {
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.UPDATE, claims))) {
throw new UnauthorizedError();
} else if (Claims.test(Claims.PLAYERS.SELF.UPDATE, claims) && claims?.userId) {
@@ -131,7 +132,7 @@ export class PlayersOrm {
return await this.get(id);
}
- async drop(id: SecureId, claims?: Claims): Promise {
+ async drop(id: PlayerId, claims?: Claims): Promise {
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.DELETE, claims))) {
throw new UnauthorizedError();
} else if (Claims.test(Claims.PLAYERS.SELF.DELETE, claims) && claims?.userId) {
diff --git a/src/orm/user.ts b/src/orm/user.ts
index 28a70de..7e388cd 100644
--- a/src/orm/user.ts
+++ b/src/orm/user.ts
@@ -3,17 +3,18 @@ import { sql } from 'bun';
import { first } from 'lodash';
import argon2 from 'argon2';
import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors';
-import { SecureId, UpdateUserRequest } from '../utilities/requestModels';
+import { UpdateUserRequest } from '../utilities/requestModels';
import { orm } from './orm';
+import { PlayerId, UserId } from '../utilities/secureIds';
export class User {
- id: SecureId;
- playerId: SecureId;
+ id: UserId;
+ playerId: PlayerId;
email: string;
isAdmin: boolean;
isActive: boolean;
- constructor(id: SecureId, playerId: SecureId, email: string, isAdmin: boolean = false, isActive: boolean = true) {
+ constructor(id: UserId, playerId: PlayerId, email: string, isAdmin: boolean = false, isActive: boolean = true) {
this.id = id;
this.playerId = playerId;
this.email = email;
@@ -24,8 +25,7 @@ export class User {
export class UsersOrm {
async create(
- { email, password, playerId }: { email: string; password: string; playerId: SecureId },
- claims?: Claims,
+ { email, password, playerId }: { email: string; password: string; playerId: PlayerId },
): Promise {
const existingUser: any = first(
await sql`SELECT id
@@ -41,7 +41,7 @@ export class UsersOrm {
const passwordHash = await argon2.hash(password);
await sql`INSERT INTO users (email, pass_hash, player_id)
VALUES (${email}, ${passwordHash}, ${playerId.raw})`;
- const newUserId: SecureId = SecureId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
+ const newUserId: UserId = UserId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
await sql.transaction(async (tx) => {
for (let i in defaultClaims) {
await tx`INSERT INTO user_claims (user_id, claim_id)
@@ -52,7 +52,7 @@ export class UsersOrm {
return await this.get(newUserId);
}
- async get(id: SecureId, claims?: Claims): Promise {
+ async get(id: UserId, claims?: Claims): Promise {
if (
!(
Claims.test(Claims.ADMIN, claims) ||
@@ -76,14 +76,14 @@ export class UsersOrm {
}
return new User(
- SecureId.fromID(dbResult.id),
- SecureId.fromID(dbResult.player_id),
+ UserId.fromID(dbResult.id),
+ PlayerId.fromID(dbResult.player_id),
dbResult.email,
dbResult.is_admin,
);
}
- async update(id: SecureId, patch: UpdateUserRequest, claims?: Claims): Promise {
+ async update(id: UserId, patch: UpdateUserRequest, claims?: Claims): Promise {
if (
!(
Claims.test(Claims.ADMIN, claims) ||
@@ -108,7 +108,7 @@ export class UsersOrm {
return await this.get(id);
}
- async drop(id: SecureId, claims?: Claims): Promise {
+ async drop(id: UserId, claims?: Claims): Promise {
if (
!(
Claims.test(Claims.ADMIN, claims) ||
@@ -136,7 +136,7 @@ export class UsersOrm {
async verifyCredentials(
email: string,
password: string,
- ): Promise<{ userId: SecureId; refreshCount: string } | null> {
+ ): Promise<{ userId: UserId; refreshCount: string } | null> {
const dbResult: any = first(
await sql`SELECT *
FROM users
@@ -153,12 +153,12 @@ export class UsersOrm {
}
return {
- userId: SecureId.fromID(dbResult.id),
+ userId: UserId.fromID(dbResult.id),
refreshCount: dbResult.refresh_count,
};
}
- async verifyRefreshCount(id: SecureId, refreshCount: string): Promise {
+ async verifyRefreshCount(id: UserId, refreshCount: string): Promise {
const dbResult: any = first(
await sql`SELECT *
FROM users
@@ -169,7 +169,7 @@ export class UsersOrm {
}
async changePassword(
- id: SecureId,
+ id: UserId,
oldPassword: string | null,
newPassword: string,
claims?: Claims,
diff --git a/src/routes/collections.ts b/src/routes/collections.ts
new file mode 100644
index 0000000..67c03ff
--- /dev/null
+++ b/src/routes/collections.ts
@@ -0,0 +1,17 @@
+import { guard } from '../utilities/guard';
+import { Claims } from '../orm/claims';
+import collections from '../endpoints/collections';
+
+export default {
+ '/api/collection': {
+ POST: guard(collections.create, [Claims.ADMIN, Claims.COLLECTIONS.CREATE]),
+ },
+ '/api/collection/:id': {
+ 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]),
+ // DELETE: guard(collections.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
+ },
+ '/api/collection/list': {
+ GET: guard(collections.list, [Claims.ADMIN, Claims.COLLECTIONS.OWNED.LIST]),
+ },
+};
diff --git a/src/routes/game.ts b/src/routes/games.ts
similarity index 100%
rename from src/routes/game.ts
rename to src/routes/games.ts
diff --git a/src/routes/invite.ts b/src/routes/invites.ts
similarity index 87%
rename from src/routes/invite.ts
rename to src/routes/invites.ts
index c6c3128..e9f17ec 100644
--- a/src/routes/invite.ts
+++ b/src/routes/invites.ts
@@ -1,6 +1,6 @@
import { guard, unwrapMethod } from '../utilities/guard';
import { Claims } from '../orm/claims';
-import invite from '../endpoints/invite';
+import invite from '../endpoints/invites';
export default {
'/api/invite': {
diff --git a/src/routes/player.ts b/src/routes/players.ts
similarity index 93%
rename from src/routes/player.ts
rename to src/routes/players.ts
index 13a6c31..7b2219d 100644
--- a/src/routes/player.ts
+++ b/src/routes/players.ts
@@ -1,6 +1,6 @@
import { guard } from '../utilities/guard';
import { Claims } from '../orm/claims';
-import player from '../endpoints/player';
+import player from '../endpoints/players';
export default {
'/api/player': {
diff --git a/src/routes/user.ts b/src/routes/users.ts
similarity index 62%
rename from src/routes/user.ts
rename to src/routes/users.ts
index 12ac068..55325a9 100644
--- a/src/routes/user.ts
+++ b/src/routes/users.ts
@@ -1,17 +1,11 @@
-import { guard, unwrap, unwrapMethod } from '../utilities/guard';
-import user from '../endpoints/user';
+import { guard } from '../utilities/guard';
+import user from '../endpoints/users';
import { Claims } from '../orm/claims';
export default {
'/api/user': {
POST: guard(user.create, [Claims.ADMIN, Claims.USERS.CREATE]),
},
- '/api/user/invite': {
- POST: guard(user.invite, [Claims.ADMIN, Claims.USERS.INVITE]),
- },
- '/api/user/invite/accept': {
- POST: unwrapMethod(user.acceptInvite),
- },
'/api/user/:id': {
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]),
diff --git a/src/tests/user.test.ts b/src/tests/user.test.ts
index 1cb8424..bcc75d5 100644
--- a/src/tests/user.test.ts
+++ b/src/tests/user.test.ts
@@ -1,5 +1,5 @@
import { expect, test } from 'bun:test';
-import user from '../endpoints/user';
+import user from '../endpoints/users';
import { UnwrappedRequest } from '../utilities/guard';
import { Claims } from '../orm/claims';
import { orm } from '../orm/orm';
diff --git a/src/utilities/claimDefinitions.ts b/src/utilities/claimDefinitions.ts
index 41512c2..c78b56f 100644
--- a/src/utilities/claimDefinitions.ts
+++ b/src/utilities/claimDefinitions.ts
@@ -107,6 +107,7 @@ export class ClaimDefinition {
READ: 'COLLECTIONS_OWNED_READ',
UPDATE: 'COLLECTIONS_OWNED_UPDATE',
DELETE: 'COLLECTIONS_OWNED_DELETE',
+ LIST: 'COLLECTIONS_OWNED_LIST',
COMMENTS: {
DELETE: 'COLLECTIONS_OWNED_COMMENTS_DELETE',
},
diff --git a/src/utilities/errors.ts b/src/utilities/errors.ts
index 6ec87fe..0b4982d 100644
--- a/src/utilities/errors.ts
+++ b/src/utilities/errors.ts
@@ -21,3 +21,8 @@ export class NotFoundError extends Error {
super(message);
}
}
+export class NotImplementedError extends Error {
+ constructor(message?: string | undefined) {
+ super(message);
+ }
+}
diff --git a/src/utilities/guard.ts b/src/utilities/guard.ts
index eeac0f3..1a65fe7 100644
--- a/src/utilities/guard.ts
+++ b/src/utilities/guard.ts
@@ -2,9 +2,6 @@ import { BunRequest as Request } from 'bun';
import jwt, { TokenExpiredError } from 'jsonwebtoken';
import { ErrorResponse, UnauthorizedResponse } from './responseHelper';
import { Claims } from '../orm/claims';
-import HashIds from 'hashids';
-
-export const hashIds = new HashIds(process.env.JWT_SECRET, 4);
export function guardRedirect(
method: (request: UnwrappedRequest) => Promise | Response,
diff --git a/src/utilities/requestModels.ts b/src/utilities/requestModels.ts
index 4459fc4..324b600 100644
--- a/src/utilities/requestModels.ts
+++ b/src/utilities/requestModels.ts
@@ -1,5 +1,3 @@
-import { hashIds } from './guard';
-
export interface LoginRequest {
email: string;
password: string;
@@ -43,45 +41,9 @@ export interface UpdateGameRequest {
imagePath?: string;
bggId?: string;
}
-
-export class SecureId {
- #hashedValue?: string;
- #secureValue?: string;
- get value(): string | undefined {
- return this.#hashedValue;
- }
- set value(value: string) {
- this.#hashedValue = value;
- this.#secureValue = hashIds.decode(value)?.toString();
- }
- get raw(): string | undefined {
- return this.#secureValue;
- }
- set raw(value: string) {
- this.#hashedValue = hashIds.encode(value);
- this.#secureValue = value;
- }
- constructor(id: { public?: string; secure?: string }) {
- if (id.public) {
- this.value = id.public;
- } else if (id.secure) {
- this.raw = id.secure;
- }
- }
-
- toJSON(): string | undefined {
- return this.#hashedValue;
- }
-
- valueOf(): string | undefined {
- return this.#secureValue;
- }
-
- public static fromHash(hash: string) {
- return new SecureId({ public: hash });
- }
-
- public static fromID(id: string) {
- return new SecureId({ secure: id });
- }
+export interface CreateCollectionRequest {
+ name: string;
+}
+export interface UpdateCollectionRequest {
+ name?: string;
}
diff --git a/src/utilities/secureIds.ts b/src/utilities/secureIds.ts
new file mode 100644
index 0000000..5ba74e5
--- /dev/null
+++ b/src/utilities/secureIds.ts
@@ -0,0 +1,124 @@
+import HashIds from 'hashids';
+
+class SecureId {
+ protected static hashPrefix: string = '';
+ protected static get hashScheme(): HashIds {
+ return new HashIds(
+ `${this.hashPrefix}_${process.env.HASHID_SALT_BASE}`,
+ parseInt(process.env.HASHID_LENGTH ?? '6'),
+ process.env.HASHID_ALPHABET,
+ );
+ }
+
+ #hashedValue?: string;
+ #secureValue?: string;
+ #hashScheme: HashIds;
+
+ constructor(id: { public?: string; secure?: string }, hashScheme?: HashIds) {
+ this.#hashScheme = hashScheme ?? (this.constructor as any).hashScheme;
+
+ if (id.public) {
+ this.value = id.public;
+ } else if (id.secure) {
+ this.raw = id.secure;
+ }
+ }
+ get value(): string | undefined {
+ return this.#hashedValue;
+ }
+ set value(value: string) {
+ this.#hashedValue = value;
+ this.#secureValue = this.#hashScheme.decode(value)?.toString();
+ }
+ get raw(): string | undefined {
+ return this.#secureValue;
+ }
+ set raw(value: string) {
+ this.#hashedValue = this.#hashScheme.encode(value);
+ this.#secureValue = value;
+ }
+
+ toJSON(): string | undefined {
+ return this.#hashedValue;
+ }
+
+ valueOf(): string | undefined {
+ return this.#secureValue;
+ }
+
+ public static fromHash(
+ hash: string,
+ type?: { new (id: { public?: string; secure?: string }): T },
+ ): SecureId {
+ const t = type ?? SecureId;
+ return new t({ public: hash });
+ }
+
+ public static fromID(
+ id: string,
+ type?: { new (id: { public?: string; secure?: string }): T },
+ ): SecureId {
+ const t = type ?? SecureId;
+ return new t({ secure: id });
+ }
+}
+
+export class UserId extends SecureId {
+ protected static override hashPrefix: string = 'UserId';
+
+ public static fromHash(hash: string): UserId {
+ return super.fromHash(hash, UserId);
+ }
+
+ public static fromID(id: string): UserId {
+ return super.fromID(id, UserId);
+ }
+}
+
+export class PlayerId extends SecureId {
+ protected static override hashPrefix: string = 'PlayerId';
+
+ public static fromHash(hash: string): PlayerId {
+ return super.fromHash(hash, PlayerId);
+ }
+
+ public static fromID(id: string): PlayerId {
+ return super.fromID(id, PlayerId);
+ }
+}
+
+export class InviteId extends SecureId {
+ protected static override hashPrefix: string = 'InviteId';
+
+ public static fromHash(hash: string): InviteId {
+ return super.fromHash(hash, InviteId);
+ }
+
+ public static fromID(id: string): InviteId {
+ return super.fromID(id, InviteId);
+ }
+}
+
+export class GameId extends SecureId {
+ protected static override hashPrefix: string = 'GameId';
+
+ public static fromHash(hash: string): GameId {
+ return super.fromHash(hash, GameId);
+ }
+
+ public static fromID(id: string): GameId {
+ return super.fromID(id, GameId);
+ }
+}
+
+export class CollectionId extends SecureId {
+ protected static override hashPrefix: string = 'CollectionId';
+
+ public static fromHash(hash: string): CollectionId {
+ return super.fromHash(hash, CollectionId);
+ }
+
+ public static fromID(id: string): CollectionId {
+ return super.fromID(id, CollectionId);
+ }
+}