Compare commits

...

2 Commits

16 changed files with 287 additions and 182 deletions

View File

@@ -1,3 +1,4 @@
#file: noinspection SpellCheckingInspection
name: BGApp name: BGApp
variables: variables:
- name: BEARER_TOKEN - name: BEARER_TOKEN

View File

@@ -1,3 +1,4 @@
#file: noinspection SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection,SpellCheckingInspection
opencollection: 1.0.0 opencollection: 1.0.0
info: info:

View File

@@ -1,3 +1,81 @@
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- noinspection SpellCheckingInspectionForFile
-- --
-- PostgreSQL database dump -- PostgreSQL database dump
-- --

View File

@@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { brandColours } from '../utilities/helpers'; import { brandColours } from '../utilities/helpers';
import { size } from 'lodash';
interface InviteEmailProperties { interface InviteEmailProperties {
playerName: string; playerName: string;
@@ -21,6 +20,7 @@ export const InviteEmail = (props: InviteEmailProperties) => (
cellSpacing={0} cellSpacing={0}
cellPadding={0} cellPadding={0}
> >
<tbody>
<tr> <tr>
<td align="center"> <td align="center">
<div <div
@@ -72,6 +72,7 @@ export const InviteEmail = (props: InviteEmailProperties) => (
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
); );

View File

@@ -20,7 +20,7 @@ async function login(request: UnwrappedRequest<LoginRequest>): Promise<Response>
const tokenLifeSpanInDays = 30; const tokenLifeSpanInDays = 30;
const token = jwt.sign( const token = jwt.sign(
{ {
u: verify.userId.raw, u: verify.userId,
r: verify.refreshCount, r: verify.refreshCount,
}, },
process.env.JWT_REFRESH_KEY as string, process.env.JWT_REFRESH_KEY as string,
@@ -54,13 +54,13 @@ async function token(request: UnwrappedRequest): Promise<Response> {
r: string; r: string;
} = jwt.verify(refreshCookie, process.env.JWT_REFRESH_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.fromHash(refreshToken.u), refreshToken.r))) {
const response = new UnauthorizedResponse('Invalid refresh token'); const response = new UnauthorizedResponse('Invalid refresh token');
response.headers.set('Clear-Site-Data', '"cookies","cache","storage","executionContexts"'); response.headers.set('Clear-Site-Data', '"cookies","cache","storage","executionContexts"');
return response; return response;
} }
const claims: Claims | null = await orm.claims.getByUserId(refreshToken.u); const claims: Claims | null = await orm.claims.getByUserId(UserId.fromHash(refreshToken.u));
const token = jwt.sign({ ...claims }, process.env.JWT_SECRET_KEY as string, { const token = jwt.sign({ ...claims }, process.env.JWT_SECRET_KEY as string, {
expiresIn: process.env.JWT_LIFESPAN as any, expiresIn: process.env.JWT_LIFESPAN as any,

View File

@@ -3,30 +3,41 @@ import { ClaimDefinition } from '../utilities/claimDefinitions';
import { UserId } from '../utilities/secureIds'; import { UserId } from '../utilities/secureIds';
export class Claims extends ClaimDefinition { export class Claims extends ClaimDefinition {
userId?: UserId; userId: UserId;
claims: string[] = []; claims: string[] = [];
constructor(raw?: { userId?: string; claims?: string[] }) { constructor(raw?: { userId?: string | UserId; claims?: string[] }) {
super(); super();
this.userId = raw?.userId ? UserId.fromHash(raw.userId) : undefined; if(raw?.userId instanceof UserId) {
this.userId = raw.userId
} else {
this.userId = UserId.fromHash(raw?.userId ?? '');
}
this.claims = raw?.claims ?? []; this.claims = raw?.claims ?? [];
} }
public static test(guardClaim: string, userClaims?: Claims): Boolean { test(...guardClaims: string[]): Boolean {
return userClaims === undefined || userClaims.claims.some((x) => x === guardClaim); return Claims.test(this, ...guardClaims);
}
public static test(userClaims?: Claims, ...guardClaims: string[]): Boolean {
return (
userClaims === undefined ||
userClaims.claims.some((x: string): boolean => guardClaims.some((y: string): boolean => x === y))
);
} }
} }
export class ClaimsOrm { export class ClaimsOrm {
async getByUserId(userId: string): Promise<Claims> { async getByUserId(userId: UserId): Promise<Claims> {
const dbResults: any[] = await sql`SELECT c.name const dbResults: any[] = await sql`SELECT c.name
from user_claims as uc from user_claims as uc
JOIN claims as c on uc.claim_id = c.id JOIN claims as c on uc.claim_id = c.id
where uc.user_id = ${userId};`; where uc.user_id = ${userId.raw};`;
const claims = new Claims(); return new Claims({
claims.userId = UserId.fromID(userId); userId: userId,
claims.claims = dbResults.map((x) => x.name); claims: dbResults.map((x) => x.name),
return claims; });
} }
async getDefaultClaims(): Promise<number[]> { async getDefaultClaims(): Promise<number[]> {

View File

@@ -4,16 +4,18 @@ import { first } from 'lodash';
import { NotFoundError, UnauthorizedError } from '../utilities/errors'; import { NotFoundError, UnauthorizedError } from '../utilities/errors';
import { UpdateCollectionRequest } from '../utilities/requestModels'; import { UpdateCollectionRequest } from '../utilities/requestModels';
import { Game } from './games'; import { Game } from './games';
import { CollectionId, GameId } from '../utilities/secureIds'; import { CollectionId, GameId, UserId } from '../utilities/secureIds';
export class Collection { export class Collection {
id: CollectionId; id: CollectionId;
name: string; name: string;
ownerId: UserId;
games: Game[]; games: Game[];
constructor(input: { id: CollectionId; name: string; games?: Game[] }) { constructor(input: { id: CollectionId; name: string; ownerId:UserId; games?: Game[] }) {
this.id = input.id; this.id = input.id;
this.name = input?.name; this.name = input?.name;
this.ownerId = input.ownerId;
this.games = input.games ?? []; this.games = input.games ?? [];
} }
} }
@@ -38,13 +40,13 @@ export class CollectionsOrm {
LEFT JOIN collection_games cg ON cg.collection_id = c.id LEFT JOIN collection_games cg ON cg.collection_id = c.id
LEFT JOIN games g ON g.id = cg.game_id LEFT JOIN games g ON g.id = cg.game_id
WHERE c.id = ${id.raw}`; WHERE c.id = ${id.raw}`;
if (
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.READ, claims))) { claims &&
throw new UnauthorizedError(); !(
} else if ( claims.test(Claims.ADMIN, Claims.COLLECTIONS.UNOWNED.READ) ||
Claims.test(Claims.COLLECTIONS.OWNED.READ, claims) && (Claims.test(claims, Claims.COLLECTIONS.OWNED.READ) &&
claims?.userId && dbResult?.[0]?.user_id === claims?.userId?.raw)
dbResult?.[0]?.user_id !== claims.userId.raw )
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
@@ -56,6 +58,7 @@ export class CollectionsOrm {
return new Collection({ return new Collection({
id: CollectionId.fromID(dbResult[0].collection_id), id: CollectionId.fromID(dbResult[0].collection_id),
name: dbResult[0].collection_name, name: dbResult[0].collection_name,
ownerId: UserId.fromID(dbResult[0].user_id),
games: dbResult games: dbResult
.filter((x: { game_id: string; game_name: string }) => x.game_id) .filter((x: { game_id: string; game_name: string }) => x.game_id)
.map( .map(
@@ -69,25 +72,27 @@ export class CollectionsOrm {
} }
async list(claims?: Claims): Promise<Collection[]> { async list(claims?: Claims): Promise<Collection[]> {
if (!claims || Claims.test(Claims.ADMIN, claims)) { if (!claims || claims?.test(Claims.ADMIN)) {
return (await sql`SELECT * FROM collections`).map( return (await sql`SELECT * FROM collections`).map(
(x: { id: string; name: string }) => (x: { id: string; name: string, user_id: string }) =>
new Collection({ new Collection({
id: CollectionId.fromID(x.id), id: CollectionId.fromID(x.id),
name: x.name, name: x.name,
ownerId: UserId.fromID(x.user_id)
}), }),
); );
} }
if (!Claims.test(Claims.COLLECTIONS.OWNED.LIST, claims)) { if (!claims.test(Claims.COLLECTIONS.OWNED.LIST)) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
return (await sql`SELECT * FROM collections WHERE user_id=${claims.userId?.raw}`).map( return (await sql`SELECT * FROM collections WHERE user_id=${claims.userId?.raw}`).map(
(x: { id: string; name: string }) => (x: { id: string; name: string; user_id: string }) =>
new Collection({ new Collection({
id: CollectionId.fromID(x.id), id: CollectionId.fromID(x.id),
name: x.name, name: x.name,
ownerId: UserId.fromID(x.user_id)
}), }),
); );
} }
@@ -95,15 +100,17 @@ export class CollectionsOrm {
async update(id: CollectionId, patch: UpdateCollectionRequest, claims?: Claims): Promise<Collection> { async update(id: CollectionId, patch: UpdateCollectionRequest, claims?: Claims): Promise<Collection> {
const collection = await this.get(id); const collection = await this.get(id);
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.UPDATE, claims))) { if (
throw new UnauthorizedError(); claims &&
} else if ( !(
Claims.test(Claims.COLLECTIONS.OWNED.UPDATE, claims) && claims.test(Claims.ADMIN, Claims.COLLECTIONS.UNOWNED.UPDATE) ||
claims?.userId && (Claims.test(claims, Claims.COLLECTIONS.OWNED.UPDATE) &&
collection.id !== claims.userId collection.ownerId === claims?.userId)
)
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
collection.name = patch.name ?? collection.name; collection.name = patch.name ?? collection.name;
await sql`UPDATE collections SET name=${collection.name} WHERE id=${id.raw}`; await sql`UPDATE collections SET name=${collection.name} WHERE id=${id.raw}`;
@@ -113,12 +120,14 @@ export class CollectionsOrm {
async drop(id: CollectionId, claims?: Claims): Promise<void> { async drop(id: CollectionId, claims?: Claims): Promise<void> {
const collection = await this.get(id); const collection = await this.get(id);
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.DELETE, claims))) {
throw new UnauthorizedError(); if (
} else if ( claims &&
Claims.test(Claims.COLLECTIONS.OWNED.DELETE, claims) && !(
claims?.userId && claims.test(Claims.ADMIN, Claims.COLLECTIONS.UNOWNED.DELETE) ||
collection.id !== claims.userId (Claims.test(claims, Claims.COLLECTIONS.OWNED.DELETE) &&
collection.ownerId === claims?.userId)
)
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
@@ -130,12 +139,14 @@ export class CollectionsOrm {
async addGame(id: CollectionId, gameId: GameId, claims: Claims): Promise<void> { async addGame(id: CollectionId, gameId: GameId, claims: Claims): Promise<void> {
const collection = await this.get(id); const collection = await this.get(id);
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.GAME.ADD, claims))) {
throw new UnauthorizedError(); if (
} else if ( claims &&
Claims.test(Claims.COLLECTIONS.OWNED.GAME.ADD, claims) && !(
claims?.userId && claims.test(Claims.ADMIN) ||
collection.id !== claims.userId (Claims.test(claims, Claims.COLLECTIONS.OWNED.GAME.ADD) &&
collection.ownerId === claims?.userId)
)
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
@@ -147,12 +158,14 @@ export class CollectionsOrm {
} }
async removeGame(id: CollectionId, gameId: GameId, claims: Claims): Promise<void> { async removeGame(id: CollectionId, gameId: GameId, claims: Claims): Promise<void> {
const collection = await this.get(id); const collection = await this.get(id);
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.COLLECTIONS.UNOWNED.GAME.REMOVE, claims))) {
throw new UnauthorizedError(); if (
} else if ( claims &&
Claims.test(Claims.COLLECTIONS.OWNED.GAME.REMOVE, claims) && !(
claims?.userId && claims.test(Claims.ADMIN) ||
collection.id !== claims.userId (Claims.test(claims, Claims.COLLECTIONS.OWNED.GAME.REMOVE) &&
collection.ownerId === claims?.userId)
)
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }

View File

@@ -23,7 +23,7 @@ export class Game {
export class GamesOrm { export class GamesOrm {
async create(model: CreateGameRequest, claims?: Claims): Promise<Game> { async create(model: CreateGameRequest, claims?: Claims): Promise<Game> {
await sql`INSERT INTO games (name, image_path, bgg_id) await sql`INSERT INTO games (name, image_path, bgg_id)
VALUES (${model.name}, ${Claims.test(Claims.GAMES.MANAGE_IMAGES, claims) ? model.imagePath : null}, ${model.bggId})`; VALUES (${model.name}, ${claims?.test(Claims.GAMES.MANAGE_IMAGES) ? model.imagePath : null}, ${model.bggId})`;
const newGameId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string; const newGameId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
return await this.get(GameId.fromID(newGameId)); return await this.get(GameId.fromID(newGameId));
@@ -54,7 +54,7 @@ export class GamesOrm {
gameToUpdate.name = patch.name ?? gameToUpdate.name; gameToUpdate.name = patch.name ?? gameToUpdate.name;
gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId; gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId;
if (Claims.test(Claims.GAMES.MANAGE_IMAGES, claims)) { if (claims?.test(Claims.GAMES.MANAGE_IMAGES)) {
gameToUpdate.imagePath = patch.imagePath ?? gameToUpdate.imagePath; gameToUpdate.imagePath = patch.imagePath ?? gameToUpdate.imagePath;
} }

View File

@@ -22,7 +22,7 @@ export class InvitesOrm {
}, },
claims?: Claims, claims?: Claims,
): Promise<void> { ): Promise<void> {
if (!Claims.test(Claims.ADMIN, claims)) { if (!claims?.test(Claims.ADMIN)) {
const userInviteCount = ( const userInviteCount = (
first( first(
await sql`SELECT COUNT(*) AS count await sql`SELECT COUNT(*) AS count

View File

@@ -116,10 +116,11 @@ export class MatchOrm {
WHERE m.id = ${id.raw}`; WHERE m.id = ${id.raw}`;
if ( if (
claims &&
!( !(
Claims.test(Claims.ADMIN, claims) || claims.test(Claims.ADMIN) ||
(Claims.test(Claims.MATCHES.OWNED.READ, claims) && dbResult?.[0]?.owner_id === claims?.userId?.raw) || (claims.test(Claims.MATCHES.OWNED.READ) && dbResult?.[0]?.owner_id === claims?.userId?.raw) ||
(Claims.test(Claims.MATCHES.PARTICIPANT.READ, claims) && (claims.test(Claims.MATCHES.PARTICIPANT.READ) &&
dbResult?.some((x: any) => x.player_id === claims?.userId?.raw)) dbResult?.some((x: any) => x.player_id === claims?.userId?.raw))
) )
) { ) {
@@ -157,10 +158,8 @@ export class MatchOrm {
async drop(id: MatchId, claims?: Claims): Promise<void> { async drop(id: MatchId, claims?: Claims): Promise<void> {
const match = await this.get(id); const match = await this.get(id);
if ( if (
!( claims &&
Claims.test(Claims.ADMIN, claims) || !(claims.test(Claims.ADMIN) || (claims.test(Claims.MATCHES.OWNED.DELETE) && match.owner === claims?.userId))
(Claims.test(Claims.MATCHES.OWNED.DELETE, claims) && match.owner === claims?.userId)
)
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
@@ -189,7 +188,7 @@ export class MatchOrm {
const player = await orm.players.get(playerId); const player = await orm.players.get(playerId);
sql.transaction(async (tx) => { await sql.transaction(async (tx) => {
const eloRefund = parseInt( const eloRefund = parseInt(
( (
await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}` await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}`

View File

@@ -38,14 +38,15 @@ export class PlayersOrm {
} }
async get(id: PlayerId, claims?: Claims): Promise<Player> { async get(id: PlayerId, claims?: Claims): Promise<Player> {
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.READ, claims))) { if(claims) {
throw new UnauthorizedError();
} else if (Claims.test(Claims.PLAYERS.SELF.READ, claims) && claims?.userId) {
const user = await orm.users.get(claims.userId); const user = await orm.users.get(claims.userId);
if (id.raw !== user.playerId.raw) { if(!(claims.test(Claims.ADMIN, Claims.PLAYERS.OTHER.READ) ||
claims.test(Claims.PLAYERS.SELF.READ) && id === user.playerId)) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
} }
const dbResult: any = first( const dbResult: any = first(
await sql`SELECT * await sql`SELECT *
FROM players FROM players
@@ -67,7 +68,7 @@ export class PlayersOrm {
} }
async list(claims?: Claims): Promise<Player[]> { async list(claims?: Claims): Promise<Player[]> {
if (!claims || Claims.test(Claims.ADMIN, claims)) { if (!claims || claims.test(Claims.ADMIN)) {
return (await sql`SELECT * FROM players`).map( return (await sql`SELECT * FROM players`).map(
(x: { id: string; name: string; elo: string; is_rating_locked: boolean; can_be_multiple: boolean }) => (x: { id: string; name: string; elo: string; is_rating_locked: boolean; can_be_multiple: boolean }) =>
new Player({ new Player({
@@ -80,7 +81,7 @@ export class PlayersOrm {
); );
} }
if (!Claims.test(Claims.PLAYERS.OTHER.READ, claims)) { if (!claims.test(Claims.PLAYERS.OTHER.READ)) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
@@ -109,11 +110,11 @@ export class PlayersOrm {
} }
async update(id: PlayerId, patch: UpdatePlayerRequest, claims?: Claims): Promise<Player> { async update(id: PlayerId, patch: UpdatePlayerRequest, claims?: Claims): Promise<Player> {
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.UPDATE, claims))) { if(claims) {
throw new UnauthorizedError();
} else if (Claims.test(Claims.PLAYERS.SELF.UPDATE, claims) && claims?.userId) {
const user = await orm.users.get(claims.userId); const user = await orm.users.get(claims.userId);
if (id.raw !== user.playerId.raw) { if(!(claims.test(Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE) ||
(claims.test(Claims.PLAYERS.SELF.UPDATE) && id === user.playerId)
)) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
} }
@@ -133,11 +134,11 @@ export class PlayersOrm {
} }
async drop(id: PlayerId, claims?: Claims): Promise<void> { async drop(id: PlayerId, claims?: Claims): Promise<void> {
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.DELETE, claims))) { if(claims) {
throw new UnauthorizedError();
} else if (Claims.test(Claims.PLAYERS.SELF.DELETE, claims) && claims?.userId) {
const user = await orm.users.get(claims.userId); const user = await orm.users.get(claims.userId);
if (id.raw !== user.playerId.raw) { if(!(claims.test(Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE) ||
(claims.test(Claims.PLAYERS.SELF.DELETE) && id === user.playerId)
)) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
} }

View File

@@ -24,9 +24,15 @@ export class User {
} }
export class UsersOrm { export class UsersOrm {
async create( async create({
{ email, password, playerId }: { email: string; password: string; playerId: PlayerId }, email,
): Promise<User> { password,
playerId,
}: {
email: string;
password: string;
playerId: PlayerId;
}): Promise<User> {
const existingUser: any = first( const existingUser: any = first(
await sql`SELECT id await sql`SELECT id
FROM users FROM users
@@ -54,10 +60,10 @@ export class UsersOrm {
async get(id: UserId, claims?: Claims): Promise<User> { async get(id: UserId, claims?: Claims): Promise<User> {
if ( if (
claims &&
!( !(
Claims.test(Claims.ADMIN, claims) || claims.test(Claims.ADMIN, Claims.USERS.OTHER.READ) ||
Claims.test(Claims.USERS.OTHER.READ, claims) || (claims.test(Claims.USERS.SELF.READ) && id === claims?.userId)
(Claims.test(Claims.USERS.SELF.READ, claims) && id === claims?.userId)
) )
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
@@ -85,17 +91,17 @@ export class UsersOrm {
async update(id: UserId, patch: UpdateUserRequest, claims?: Claims): Promise<User> { async update(id: UserId, patch: UpdateUserRequest, claims?: Claims): Promise<User> {
if ( if (
claims &&
!( !(
Claims.test(Claims.ADMIN, claims) || claims.test(Claims.ADMIN, Claims.USERS.OTHER.UPDATE) ||
Claims.test(Claims.USERS.OTHER.UPDATE, claims) || (claims.test(Claims.USERS.SELF.UPDATE) && id === claims?.userId)
(Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId)
) )
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }
const userToUpdate = await this.get(id); const userToUpdate = await this.get(id);
if (Claims.test(Claims.ADMIN, claims)) { if (!claims || claims.test(Claims.ADMIN)) {
userToUpdate.isActive = patch.isActive ?? userToUpdate.isActive; userToUpdate.isActive = patch.isActive ?? userToUpdate.isActive;
userToUpdate.isAdmin = patch.isAdmin ?? userToUpdate.isAdmin; userToUpdate.isAdmin = patch.isAdmin ?? userToUpdate.isAdmin;
} }
@@ -110,10 +116,10 @@ export class UsersOrm {
async drop(id: UserId, claims?: Claims): Promise<void> { async drop(id: UserId, claims?: Claims): Promise<void> {
if ( if (
claims &&
!( !(
Claims.test(Claims.ADMIN, claims) || claims.test(Claims.ADMIN, Claims.USERS.OTHER.DELETE) ||
Claims.test(Claims.USERS.OTHER.DELETE, claims) || (claims.test(Claims.USERS.SELF.DELETE) && id === claims?.userId)
(Claims.test(Claims.USERS.SELF.DELETE, claims) && id === claims?.userId)
) )
) { ) {
throw new UnauthorizedError(); throw new UnauthorizedError();
@@ -133,10 +139,7 @@ export class UsersOrm {
return; return;
} }
async verifyCredentials( async verifyCredentials(email: string, password: string): Promise<{ userId: UserId; refreshCount: string } | null> {
email: string,
password: string,
): Promise<{ userId: UserId; refreshCount: string } | null> {
const dbResult: any = first( const dbResult: any = first(
await sql`SELECT * await sql`SELECT *
FROM users FROM users
@@ -168,14 +171,9 @@ export class UsersOrm {
return dbResult.refresh_count === refreshCount; return dbResult.refresh_count === refreshCount;
} }
async changePassword( async changePassword(id: UserId, oldPassword: string | null, newPassword: string, claims?: Claims): Promise<void> {
id: UserId, const isAdmin = claims?.test(Claims.ADMIN) ?? true;
oldPassword: string | null, if (!(isAdmin || (claims?.test(Claims.USERS.SELF.UPDATE) && id === claims?.userId))) {
newPassword: string,
claims?: Claims,
): Promise<void> {
const isAdmin = Claims.test(Claims.ADMIN, claims);
if (!(isAdmin || (Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId))) {
throw new UnauthorizedError(); throw new UnauthorizedError();
} }

View File

@@ -11,14 +11,12 @@ export default {
add: { add: {
POST: guard(collections.addGame, [ POST: guard(collections.addGame, [
Claims.ADMIN, Claims.ADMIN,
Claims.COLLECTIONS.UNOWNED.GAME.ADD,
Claims.COLLECTIONS.OWNED.GAME.ADD, Claims.COLLECTIONS.OWNED.GAME.ADD,
]), ]),
}, },
remove: { remove: {
POST: guard(collections.removeGame, [ POST: guard(collections.removeGame, [
Claims.ADMIN, Claims.ADMIN,
Claims.COLLECTIONS.UNOWNED.GAME.REMOVE,
Claims.COLLECTIONS.OWNED.GAME.REMOVE, Claims.COLLECTIONS.OWNED.GAME.REMOVE,
]), ]),
}, },

View File

@@ -29,49 +29,40 @@ export class ClaimDefinition {
}; };
public static readonly CIRCLES = { public static readonly CIRCLES = {
PUBLIC: { PUBLIC: {
READ: 'CIRCLES_OWNED_READ',
CREATE: 'CIRCLES_PUBLIC_CREATE', CREATE: 'CIRCLES_PUBLIC_CREATE',
JOIN: 'CIRCLES_PUBLIC_JOIN', JOIN: 'CIRCLES_PUBLIC_JOIN',
USERS: {
ADD: 'CIRCLES_PUBLIC_USER_ADD',
LIST: 'CIRCLES_PUBLIC_USER_LIST',
INVITE: 'CIRCLES_PUBLIC_USER_INVITE',
},
COMMENTS: { COMMENTS: {
ADD: 'CIRCLES_PUBLIC_COMMENTS_ADD', ADD: 'CIRCLES_PUBLIC_COMMENTS_ADD',
DELETE: 'CIRCLES_PUBLIC_COMMENTS_DELETE', },
USERS: {
INVITE: 'CIRCLES_OWNED_USER_INVITE',
}, },
}, },
PRIVATE: { PRIVATE: {
READ: 'CIRCLES_PRIVATE_READ',
READ_IF_MEMBER: 'CIRCLES_PRIVATE_READ_IF_MEMBER',
CREATE: 'CIRCLES_PRIVATE_CREATE', CREATE: 'CIRCLES_PRIVATE_CREATE',
USERS: { COMMENTS: {
INVITE: 'CIRCLES_PRIVATE_USER_INVITE', ADD: 'CIRCLES_PRIVATE_COMMENTS_ADD',
}, },
}, },
OWNED: { OWNED: {
READ: 'CIRCLES_OWNED_READ', READ: 'CIRCLES_OWNED_READ',
UPDATE: 'CIRCLES_OWNED_UPDATE', UPDATE: 'CIRCLES_OWNED_UPDATE',
DELETE: 'CIRCLES_OWNED_DELETE', DELETE: 'CIRCLES_OWNED_DELETE',
USERS: { PLAYERS: {
ADD: 'CIRCLES_OWNED_USER_ADD', ADD: 'CIRCLES_OWNED_USER_ADD',
LIST: 'CIRCLES_OWNED_USER_LIST', LIST: 'CIRCLES_OWNED_USER_LIST',
},
USERS: { USERS: {
INVITE: 'CIRCLES_OWNED_USER_INVITE', INVITE: 'CIRCLES_OWNED_USER_INVITE',
}, },
},
COMMENTS: { COMMENTS: {
ADD: 'CIRCLES_OWNED_COMMENTS_ADD', ADD: 'CIRCLES_OWNED_COMMENTS_ADD',
DELETE: 'CIRCLES_OWNED_COMMENTS_DELETE', DELETE: 'CIRCLES_OWNED_COMMENTS_DELETE',
}, },
}, },
UNOWNED: {
READ: 'CIRCLES_UNOWNED_READ',
UPDATE: 'CIRCLES_UNOWNED_UPDATE',
DELETE: 'CIRCLES_UNOWNED_DELETE',
COMMENTS: {
ADD: 'CIRCLES_UNOWNED_COMMENTS_ADD',
DELETE: 'CIRCLES_UNOWNED_COMMENTS_DELETE',
},
},
}; };
public static readonly GAMES = { public static readonly GAMES = {
CREATE: 'GAMES_CREATE', CREATE: 'GAMES_CREATE',
@@ -120,10 +111,6 @@ export class ClaimDefinition {
READ: 'COLLECTIONS_UNOWNED_READ', READ: 'COLLECTIONS_UNOWNED_READ',
UPDATE: 'COLLECTIONS_UNOWNED_UPDATE', UPDATE: 'COLLECTIONS_UNOWNED_UPDATE',
DELETE: 'COLLECTIONS_UNOWNED_DELETE', DELETE: 'COLLECTIONS_UNOWNED_DELETE',
GAME: {
ADD: 'COLLECTIONS_UNOWNED_GAME_ADD',
REMOVE: 'COLLECTIONS_UNOWNED_GAME_REMOVE',
},
}, },
}; };
public static readonly COMMENTS = { public static readonly COMMENTS = {

View File

@@ -23,13 +23,18 @@ export function guard(
const authHeader: string | null = const authHeader: string | null =
(request.headers.get('Authorization')?.replace(/^Bearer /, '') as string) ?? null; (request.headers.get('Authorization')?.replace(/^Bearer /, '') as string) ?? null;
try { try {
const userClaims: Claims = new Claims(jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as any); const userClaims: Claims = new Claims(
if (!userClaims.claims.some((x: string): boolean => guardedClaims.includes(x))) { jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as any,
);
if (
!userClaims.userId.raw ||
!userClaims.claims.some((x: string): boolean => guardedClaims.includes(x))
) {
return new UnauthorizedResponse('Unauthorized'); return new UnauthorizedResponse('Unauthorized');
} }
return method(await unwrap(request, userClaims)); return method(await unwrap(request, userClaims));
} catch (error: any) { } catch (error: any) {
console.log(error) console.log(error);
if (error instanceof TokenExpiredError) { if (error instanceof TokenExpiredError) {
return new UnauthorizedResponse(error.message); return new UnauthorizedResponse(error.message);
} }
@@ -66,6 +71,6 @@ export function unwrapMethod<T = {}>(
): (r: Request) => Promise<Response> { ): (r: Request) => Promise<Response> {
return async (request: Request) => { return async (request: Request) => {
const unwrappedRequest = await unwrap<T>(request); const unwrappedRequest = await unwrap<T>(request);
return await methodToUnwrap(unwrappedRequest); return methodToUnwrap(unwrappedRequest);
}; };
} }

View File

@@ -17,7 +17,7 @@ class SecureId {
constructor(id: { public?: string; secure?: string }, hashScheme?: HashIds) { constructor(id: { public?: string; secure?: string }, hashScheme?: HashIds) {
this.#hashScheme = hashScheme ?? (this.constructor as any).hashScheme; this.#hashScheme = hashScheme ?? (this.constructor as any).hashScheme;
if (id.public) { if (id.public !== undefined) {
this.value = id.public; this.value = id.public;
} else if (id.secure) { } else if (id.secure) {
this.raw = id.secure; this.raw = id.secure;
@@ -134,3 +134,15 @@ export class MatchId extends SecureId {
return super.fromID(id, MatchId); return super.fromID(id, MatchId);
} }
} }
export class CircleId extends SecureId {
protected static override hashPrefix: string = 'CircleId';
public static fromHash(hash: string): CircleId {
return super.fromHash(hash, CircleId);
}
public static fromID(id: string): CircleId {
return super.fromID(id, CircleId);
}
}