Compare commits
5 Commits
0f6452fdd4
...
f81220f837
| Author | SHA1 | Date | |
|---|---|---|---|
| f81220f837 | |||
| 335f1821cd | |||
| 5742214115 | |||
| 98315db912 | |||
| 4a63f0507d |
5
.env.dev
5
.env.dev
@@ -1,2 +1,3 @@
|
|||||||
DATABASE_URL=postgres://ApiUser:2<KtJ=*5`;19@192.168.1.166:5432/bgApp
|
DATABASE_URL=
|
||||||
JWT_SECRET_KEY=MySecret
|
JWT_SECRET_KEY=
|
||||||
|
RESEND_KEY=
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
DATABASE_URL=postgres://admin:iiyama12@192.168.1.166:5432/bgAppTest
|
DATABASE_URL=
|
||||||
JWT_SECRET_KEY=MySecret
|
JWT_SECRET_KEY=
|
||||||
|
RESEND_KEY=
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,4 +3,6 @@ node_modules/
|
|||||||
bun.lock
|
bun.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.dockerignore
|
.dockerignore
|
||||||
bgapp
|
bgapp
|
||||||
|
.env.dev
|
||||||
|
.env.test
|
||||||
25
API Tests/BGApp/Auth/Login.yml
Normal file
25
API Tests/BGApp/Auth/Login.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
info:
|
||||||
|
name: Login
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: http://localhost:3000/api/auth/login
|
||||||
|
headers:
|
||||||
|
- name: Content-Type
|
||||||
|
value: application/json
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email":"james@dardry.com",
|
||||||
|
"password":"Foobar"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
18
API Tests/BGApp/Auth/Token.yml
Normal file
18
API Tests/BGApp/Auth/Token.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
info:
|
||||||
|
name: Token
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: http://localhost:3000/api/auth/token
|
||||||
|
headers:
|
||||||
|
- name: Cookie
|
||||||
|
value: refresh=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiMSIsInIiOiIxIiwiaWF0IjoxNzcxNTk3NjQ2LCJleHAiOjE3NzQxODk2NDZ9.07ViS5Nie3Bi2OgnlHyybDNZ9bdXPRRiqO-RFLhjoKo; Path=/; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
4
API Tests/BGApp/Auth/folder.yml
Normal file
4
API Tests/BGApp/Auth/folder.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
info:
|
||||||
|
name: Auth
|
||||||
|
type: folder
|
||||||
|
seq: 1
|
||||||
21
API Tests/BGApp/Game/Create.yml
Normal file
21
API Tests/BGApp/Game/Create.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: http://localhost:3000/api/game
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "Test Game3"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/Game/Delete.yml
Normal file
19
API Tests/BGApp/Game/Delete.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Delete
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: http://localhost:3000/api/game/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: bk5e
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/Game/Get.yml
Normal file
19
API Tests/BGApp/Game/Get.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Get
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: http://localhost:3000/api/game/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: bk5e
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/Game/Search.yml
Normal file
19
API Tests/BGApp/Game/Search.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Search
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: http://localhost:3000/api/game/search/:query
|
||||||
|
params:
|
||||||
|
- name: query
|
||||||
|
value: game
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
25
API Tests/BGApp/Game/Update.yml
Normal file
25
API Tests/BGApp/Game/Update.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
info:
|
||||||
|
name: Update
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PATCH
|
||||||
|
url: http://localhost:3000/api/game/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: el5a
|
||||||
|
type: path
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name":"Updated game"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
7
API Tests/BGApp/Game/folder.yml
Normal file
7
API Tests/BGApp/Game/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
info:
|
||||||
|
name: Game
|
||||||
|
type: folder
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
22
API Tests/BGApp/Invites/Accept.yml
Normal file
22
API Tests/BGApp/Invites/Accept.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
info:
|
||||||
|
name: Accept
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: http://localhost:3000/api/invite/accept
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"inviteCode": "3ST6N8",
|
||||||
|
"password": "test123"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
22
API Tests/BGApp/Invites/Create.yml
Normal file
22
API Tests/BGApp/Invites/Create.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: http://localhost:3000/api/invite
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "james+test2@dardry.com",
|
||||||
|
"playerId": "boja"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
7
API Tests/BGApp/Invites/folder.yml
Normal file
7
API Tests/BGApp/Invites/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
info:
|
||||||
|
name: Invites
|
||||||
|
type: folder
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
21
API Tests/BGApp/Players/Create.yml
Normal file
21
API Tests/BGApp/Players/Create.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: http://localhost:3000/api/player
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "Invited player2"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/Players/Delete.yml
Normal file
19
API Tests/BGApp/Players/Delete.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Delete
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: http://localhost:3000/api/player/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: bmOe
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/Players/Get.yml
Normal file
19
API Tests/BGApp/Players/Get.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Get
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: http://localhost:3000/api/player/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: ejRe
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
15
API Tests/BGApp/Players/List.yml
Normal file
15
API Tests/BGApp/Players/List.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
info:
|
||||||
|
name: List
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: http://localhost:3000/api/player/list
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
27
API Tests/BGApp/Players/Update.yml
Normal file
27
API Tests/BGApp/Players/Update.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
info:
|
||||||
|
name: Update
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PATCH
|
||||||
|
url: http://localhost:3000/api/player/: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
|
||||||
7
API Tests/BGApp/Players/folder.yml
Normal file
7
API Tests/BGApp/Players/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
info:
|
||||||
|
name: Players
|
||||||
|
type: folder
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
23
API Tests/BGApp/User/Create.yml
Normal file
23
API Tests/BGApp/User/Create.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: http://localhost:3000/api/user
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "Test User",
|
||||||
|
"password": "Test123",
|
||||||
|
"playerId": "enRe"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/User/Delete.yml
Normal file
19
API Tests/BGApp/User/Delete.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Delete
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: http://localhost:3000/api/user/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: ""
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
19
API Tests/BGApp/User/Get.yml
Normal file
19
API Tests/BGApp/User/Get.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: Get
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: http://localhost:3000/api/user/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: ejRe
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
26
API Tests/BGApp/User/Update.yml
Normal file
26
API Tests/BGApp/User/Update.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
info:
|
||||||
|
name: Update
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PATCH
|
||||||
|
url: http://localhost:3000/api/user/:id
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: ""
|
||||||
|
type: path
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"isActive": true,
|
||||||
|
"isAdmin": false
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
7
API Tests/BGApp/User/folder.yml
Normal file
7
API Tests/BGApp/User/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
info:
|
||||||
|
name: User
|
||||||
|
type: folder
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
40
API Tests/BGApp/opencollection.yml
Normal file
40
API Tests/BGApp/opencollection.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
opencollection: 1.0.0
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: BGApp
|
||||||
|
config:
|
||||||
|
proxy:
|
||||||
|
inherit: true
|
||||||
|
config:
|
||||||
|
protocol: http
|
||||||
|
hostname: ""
|
||||||
|
port: ""
|
||||||
|
auth:
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
bypassProxy: ""
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth:
|
||||||
|
type: bearer
|
||||||
|
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJlalJlIiwiY2xhaW1zIjpbIkFETUlOIiwiVVNFUlNfQ1JFQVRFIiwiVVNFUlNfU0VMRl9SRUFEIiwiVVNFUlNfU0VMRl9VUERBVEUiLCJVU0VSU19TRUxGX0RFTEVURSIsIlVTRVJTX09USEVSX1JFQUQiLCJVU0VSU19PVEhFUl9VUERBVEUiXSwiaWF0IjoxNzcxNjE4NTQzLCJleHAiOjE4MDMxNTQ1NDN9.R-3Qb5CEcLJBSt7DnsO9b0IGRVYDIZuFfH1m9TikVXU
|
||||||
|
actions:
|
||||||
|
- type: set-variable
|
||||||
|
phase: after-response
|
||||||
|
selector:
|
||||||
|
expression: ${token}
|
||||||
|
method: jsonq
|
||||||
|
variable:
|
||||||
|
name: Token
|
||||||
|
scope: runtime
|
||||||
|
disabled: true
|
||||||
|
bundled: false
|
||||||
|
extensions:
|
||||||
|
bruno:
|
||||||
|
ignore:
|
||||||
|
- node_modules
|
||||||
|
- .git
|
||||||
|
presets:
|
||||||
|
request:
|
||||||
|
type: http
|
||||||
|
url: http://localhost:3000/api/
|
||||||
@@ -24,8 +24,6 @@ COPY ./package.json ./package.json
|
|||||||
|
|
||||||
# copy production dependencies and source code into final image
|
# copy production dependencies and source code into final image
|
||||||
FROM base AS release
|
FROM base AS release
|
||||||
ENV DATABASE_URL=postgres://admin:iiyama12@192.168.1.166:5432/bgApp
|
|
||||||
ENV JWT_SECRET_KEY=MySecret
|
|
||||||
COPY --from=install /temp/prod/node_modules node_modules
|
COPY --from=install /temp/prod/node_modules node_modules
|
||||||
COPY --from=prerelease /usr/src/app/index.ts .
|
COPY --from=prerelease /usr/src/app/index.ts .
|
||||||
COPY --from=prerelease /usr/src/app/utilities/ ./utilities
|
COPY --from=prerelease /usr/src/app/utilities/ ./utilities
|
||||||
|
|||||||
@@ -10,13 +10,17 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-email/render": "^2.0.4",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/lodash": "^4.17.23",
|
"@types/lodash": "^4.17.23",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"hashids": "^2.3.0",
|
"hashids": "^2.3.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"reflect-metadata": "^0.2.2"
|
"react": "^19.2.4",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"resend": "^6.9.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.9"
|
"@types/bun": "^1.3.9"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
77
src/emails/invite.tsx
Normal file
77
src/emails/invite.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { brandColours } from '../utilities/helpers';
|
||||||
|
import { size } from 'lodash';
|
||||||
|
|
||||||
|
interface InviteEmailProperties {
|
||||||
|
playerName: string;
|
||||||
|
inviteCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InviteEmail = (props: InviteEmailProperties) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
background: brandColours.light,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
width="100%"
|
||||||
|
border={0}
|
||||||
|
cellSpacing={0}
|
||||||
|
cellPadding={0}
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '20px',
|
||||||
|
background: brandColours.white,
|
||||||
|
margin: '50px',
|
||||||
|
color: brandColours.dark,
|
||||||
|
maxWidth: '450px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>You're in, {props.playerName}!</h1>
|
||||||
|
<p>
|
||||||
|
You've been invited to join {process.env.PRODUCT_NAME}, please click the button below to
|
||||||
|
finish signing up.
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginBottom: '40px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
background: brandColours.primary,
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: brandColours.light,
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
href={`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
|
||||||
|
>
|
||||||
|
Join {process.env.PRODUCT_NAME}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
opacity: '80%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
If above button does not work, copy the link below into a new browser tab:
|
||||||
|
<br />
|
||||||
|
{`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -10,7 +10,7 @@ async function login(request: UnwrappedRequest<LoginRequest>): Promise<Response>
|
|||||||
const verify: {
|
const verify: {
|
||||||
userId: SecureId;
|
userId: SecureId;
|
||||||
refreshCount: string;
|
refreshCount: string;
|
||||||
} | null = await orm.users.verifyCredentials(request.body.username, request.body.password);
|
} | null = await orm.users.verifyCredentials(request.body.email, request.body.password);
|
||||||
if (!verify) {
|
if (!verify) {
|
||||||
return new UnauthorizedResponse('Invalid credentials');
|
return new UnauthorizedResponse('Invalid credentials');
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ async function login(request: UnwrappedRequest<LoginRequest>): Promise<Response>
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
maxAge: tokenLifeSpanInDays * 24 * 60 * 60,
|
maxAge: tokenLifeSpanInDays * 24 * 60 * 60,
|
||||||
|
path: '/api/auth/token'
|
||||||
});
|
});
|
||||||
return new OkResponse();
|
return new OkResponse();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -60,7 +61,9 @@ async function token(request: UnwrappedRequest): Promise<Response> {
|
|||||||
|
|
||||||
const claims: Claims | null = await orm.claims.getByUserId(refreshToken.u);
|
const claims: Claims | null = await orm.claims.getByUserId(refreshToken.u);
|
||||||
|
|
||||||
const token = jwt.sign({ ...claims }, process.env.JWT_SECRET_KEY as string, { expiresIn: '1h' });
|
const token = jwt.sign({ ...claims }, process.env.JWT_SECRET_KEY as string, {
|
||||||
|
expiresIn: process.env.JWT_LIFESPAN as any,
|
||||||
|
});
|
||||||
return new OkResponse({ token });
|
return new OkResponse({ token });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
|
|||||||
37
src/endpoints/invite.ts
Normal file
37
src/endpoints/invite.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
|
import { CreatedResponse, ErrorResponse } from '../utilities/responseHelper';
|
||||||
|
import {
|
||||||
|
AcceptInviteRequest,
|
||||||
|
InviteUserRequest,
|
||||||
|
SecureId,
|
||||||
|
} from '../utilities/requestModels';
|
||||||
|
|
||||||
|
async function create(request: UnwrappedRequest<InviteUserRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const newUser = await orm.invites.create(
|
||||||
|
{
|
||||||
|
...request.body,
|
||||||
|
playerId: SecureId.fromHash(request.body.playerId),
|
||||||
|
invitedByUserId: request.claims.userId as SecureId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return new CreatedResponse(newUser);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function accept(request: UnwrappedRequest<AcceptInviteRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const newUser = await orm.invites.accept(request.body);
|
||||||
|
return new CreatedResponse(newUser);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
create,
|
||||||
|
accept,
|
||||||
|
};
|
||||||
@@ -20,6 +20,14 @@ async function get(request: UnwrappedRequest): Promise<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function list(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.players.list(request.claims));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function update(request: UnwrappedRequest<UpdatePlayerRequest>): Promise<Response> {
|
async function update(request: UnwrappedRequest<UpdatePlayerRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
return new OkResponse(
|
return new OkResponse(
|
||||||
@@ -41,6 +49,7 @@ async function drop(request: UnwrappedRequest): Promise<Response> {
|
|||||||
export default {
|
export default {
|
||||||
create,
|
create,
|
||||||
get,
|
get,
|
||||||
|
list,
|
||||||
update,
|
update,
|
||||||
drop,
|
drop,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ async function get(request: UnwrappedRequest): Promise<Response> {
|
|||||||
async function update(request: UnwrappedRequest<UpdateUserRequest>): Promise<Response> {
|
async function update(request: UnwrappedRequest<UpdateUserRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
return new OkResponse(
|
return new OkResponse(
|
||||||
await orm.users.update(
|
await orm.users.update(SecureId.fromHash(request.params.id), request.body, request.claims),
|
||||||
SecureId.fromHash(request.params.id),
|
|
||||||
request.body,
|
|
||||||
request.claims,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import user from './routes/user';
|
|||||||
import player from './routes/player';
|
import player from './routes/player';
|
||||||
import game from './routes/game';
|
import game from './routes/game';
|
||||||
import { OkResponse } from './utilities/responseHelper';
|
import { OkResponse } from './utilities/responseHelper';
|
||||||
|
import invite from './routes/invite';
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
routes: {
|
routes: {
|
||||||
@@ -10,6 +11,7 @@ const server = Bun.serve({
|
|||||||
...user,
|
...user,
|
||||||
...player,
|
...player,
|
||||||
...game,
|
...game,
|
||||||
|
...invite,
|
||||||
'/test': {
|
'/test': {
|
||||||
GET: () => {
|
GET: () => {
|
||||||
return new OkResponse();
|
return new OkResponse();
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { sql } from 'bun';
|
import { sql } from 'bun';
|
||||||
import { ClaimDefinition } from '../utilities/claimDefinitions';
|
import { ClaimDefinition } from '../utilities/claimDefinitions';
|
||||||
|
import { SecureId } from '../utilities/requestModels';
|
||||||
|
|
||||||
export class Claims extends ClaimDefinition {
|
export class Claims extends ClaimDefinition {
|
||||||
userId?: string;
|
userId?: SecureId;
|
||||||
claims: string[] = [];
|
claims: string[] = [];
|
||||||
|
|
||||||
|
constructor(raw?:{userId?:string, claims?: string[]}) {
|
||||||
|
super();
|
||||||
|
this.userId = raw?.userId ? SecureId.fromHash(raw.userId) : undefined;
|
||||||
|
this.claims = raw?.claims ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
public static test(guardClaim: string, userClaims?: Claims): Boolean {
|
public static test(guardClaim: string, userClaims?: Claims): Boolean {
|
||||||
return userClaims === undefined || userClaims.claims.some((x) => x === guardClaim);
|
return userClaims === undefined || userClaims.claims.some((x) => x === guardClaim);
|
||||||
}
|
}
|
||||||
@@ -17,7 +24,7 @@ export class ClaimsOrm {
|
|||||||
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};`;
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.userId = userId;
|
claims.userId = SecureId.fromID(userId);
|
||||||
claims.claims = dbResults.map((x) => x.name);
|
claims.claims = dbResults.map((x) => x.name);
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,19 +20,12 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GamesOrm {
|
export class GamesOrm {
|
||||||
async create(model: CreateGameRequest, claims?: Claims): Promise<Game | null> {
|
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, claims) ? 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;
|
||||||
|
|
||||||
try {
|
return await this.get(SecureId.fromID(newGameId));
|
||||||
return await this.get(SecureId.fromID(newGameId));
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UnauthorizedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: SecureId): Promise<Game> {
|
async get(id: SecureId): Promise<Game> {
|
||||||
@@ -55,7 +48,7 @@ export class GamesOrm {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: SecureId, patch: UpdateGameRequest, claims?: Claims): Promise<Game | null> {
|
async update(id: SecureId, patch: UpdateGameRequest, claims?: Claims): Promise<Game> {
|
||||||
const gameToUpdate = await this.get(id);
|
const gameToUpdate = await this.get(id);
|
||||||
gameToUpdate.name = patch.name ?? gameToUpdate.name;
|
gameToUpdate.name = patch.name ?? gameToUpdate.name;
|
||||||
gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId;
|
gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId;
|
||||||
@@ -70,17 +63,10 @@ export class GamesOrm {
|
|||||||
image_path=${gameToUpdate.imagePath}
|
image_path=${gameToUpdate.imagePath}
|
||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
try {
|
return await this.get(id);
|
||||||
return await this.get(id);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UnauthorizedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async drop(id: SecureId): Promise<undefined> {
|
async drop(id: SecureId): Promise<void> {
|
||||||
// Ensure player exists before attempting to delete
|
// Ensure player exists before attempting to delete
|
||||||
await this.get(id);
|
await this.get(id);
|
||||||
await sql.transaction(async (tx) => {
|
await sql.transaction(async (tx) => {
|
||||||
|
|||||||
133
src/orm/invites.ts
Normal file
133
src/orm/invites.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { sql } from 'bun';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
import { BadRequestError, InternalServerError, NotFoundError, UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { SecureId } from '../utilities/requestModels';
|
||||||
|
import { createRandomString } from '../utilities/helpers';
|
||||||
|
import { Resend } from 'resend';
|
||||||
|
import { orm } from './orm';
|
||||||
|
import { InviteEmail } from '../emails/invite';
|
||||||
|
import { User } from './user';
|
||||||
|
import { Claims } from './claims';
|
||||||
|
|
||||||
|
export class InvitesOrm {
|
||||||
|
async create(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
playerId,
|
||||||
|
invitedByUserId,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
playerId: SecureId;
|
||||||
|
invitedByUserId: SecureId;
|
||||||
|
},
|
||||||
|
claims?: Claims,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!Claims.test(Claims.ADMIN, claims)) {
|
||||||
|
const userInviteCount = (
|
||||||
|
first(
|
||||||
|
await sql`SELECT COUNT(*) AS count
|
||||||
|
FROM user_invites
|
||||||
|
WHERE invited_by_user_id = ${invitedByUserId.raw}`,
|
||||||
|
) as { count: number }
|
||||||
|
)?.count;
|
||||||
|
|
||||||
|
if (process.env.MAX_INVITE_ALLOWANCE && userInviteCount >= parseInt(process.env.MAX_INVITE_ALLOWANCE)) {
|
||||||
|
throw new UnauthorizedError('Invite allowance reached.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviteExists = (
|
||||||
|
first(
|
||||||
|
await sql`SELECT COUNT(*) > 0 AS exists
|
||||||
|
FROM user_invites
|
||||||
|
WHERE player_id = ${playerId.raw}
|
||||||
|
OR email = ${email}`,
|
||||||
|
) as {
|
||||||
|
exists: boolean;
|
||||||
|
}
|
||||||
|
)?.exists;
|
||||||
|
if (inviteExists) {
|
||||||
|
throw new BadRequestError('Player has already been invited.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerHasUser = (
|
||||||
|
first(
|
||||||
|
await sql`SELECT COUNT(*) > 0 AS exists
|
||||||
|
FROM users
|
||||||
|
WHERE player_id = ${playerId.raw}
|
||||||
|
OR email = ${email}`,
|
||||||
|
) as {
|
||||||
|
exists: boolean;
|
||||||
|
}
|
||||||
|
)?.exists;
|
||||||
|
if (playerHasUser) {
|
||||||
|
throw new BadRequestError('User has already been invited.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = await orm.players.get(playerId);
|
||||||
|
|
||||||
|
const invitationCode = createRandomString(6);
|
||||||
|
await sql`INSERT INTO user_invites (invite_code, email, player_id, invited_by_user_id)
|
||||||
|
VALUES (${invitationCode}, ${email}, ${playerId.raw}, ${invitedByUserId.raw})`;
|
||||||
|
const newInviteId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
|
const resend = new Resend(process.env.RESEND_KEY);
|
||||||
|
const resendResponse = await resend.emails.send({
|
||||||
|
from: `${process.env.PRODUCT_NAME} <noreply@mail.jdar.uk>`,
|
||||||
|
to: [email],
|
||||||
|
subject: "You've been invited!",
|
||||||
|
react: InviteEmail({ playerName: player.name, inviteCode: invitationCode }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resendResponse.error) {
|
||||||
|
throw new InternalServerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`UPDATE user_invites SET was_email_sent = true WHERE id=${newInviteId}`;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async accept({ inviteCode, password }: { inviteCode: string; password: string }): Promise<User> {
|
||||||
|
const invite: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
player_id: string;
|
||||||
|
accepted: boolean;
|
||||||
|
} = first(await sql`SELECT * FROM user_invites WHERE invite_code=${inviteCode} LIMIT 1`);
|
||||||
|
|
||||||
|
if (!invite) {
|
||||||
|
throw new NotFoundError('Invalid invite code');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite.accepted) {
|
||||||
|
throw new UnauthorizedError('Invite already accepted');
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerHasUser = (
|
||||||
|
first(
|
||||||
|
await sql`SELECT COUNT(*) > 0 AS exists
|
||||||
|
FROM users
|
||||||
|
WHERE player_id = ${invite.player_id}
|
||||||
|
OR email = ${invite.email}`,
|
||||||
|
) as {
|
||||||
|
exists: boolean;
|
||||||
|
}
|
||||||
|
)?.exists;
|
||||||
|
if (playerHasUser) {
|
||||||
|
throw new BadRequestError('User has already been invited.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdUser = await orm.users.create({
|
||||||
|
email: invite.email,
|
||||||
|
playerId: SecureId.fromID(invite.player_id),
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sql`UPDATE user_invites
|
||||||
|
SET accepted = true
|
||||||
|
WHERE id = ${invite.id}`;
|
||||||
|
|
||||||
|
return createdUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ import { ClaimsOrm } from './claims';
|
|||||||
import { UsersOrm } from './user';
|
import { UsersOrm } from './user';
|
||||||
import { PlayersOrm } from './players';
|
import { PlayersOrm } from './players';
|
||||||
import { GamesOrm } from './games';
|
import { GamesOrm } from './games';
|
||||||
|
import { InvitesOrm } from './invites';
|
||||||
|
|
||||||
class Orm {
|
class Orm {
|
||||||
readonly claims: ClaimsOrm = new ClaimsOrm();
|
readonly claims: ClaimsOrm = new ClaimsOrm();
|
||||||
readonly users: UsersOrm = new UsersOrm();
|
readonly users: UsersOrm = new UsersOrm();
|
||||||
readonly players: PlayersOrm = new PlayersOrm();
|
readonly players: PlayersOrm = new PlayersOrm();
|
||||||
readonly games: GamesOrm = new GamesOrm();
|
readonly games: GamesOrm = new GamesOrm();
|
||||||
|
readonly invites: InvitesOrm = new InvitesOrm();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const orm = new Orm();
|
export const orm = new Orm();
|
||||||
|
|||||||
@@ -28,26 +28,19 @@ export class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PlayersOrm {
|
export class PlayersOrm {
|
||||||
async create(model: { name: string }, claims?: Claims): Promise<Player | null> {
|
async create(model: { name: string }, claims?: Claims): Promise<Player> {
|
||||||
await sql`INSERT INTO players (name)
|
await sql`INSERT INTO players (name)
|
||||||
VALUES (${model.name})`;
|
VALUES (${model.name})`;
|
||||||
const newPlayerId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
const newPlayerId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
try {
|
return await this.get(SecureId.fromID(newPlayerId));
|
||||||
return await this.get(SecureId.fromID(newPlayerId), claims);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UnauthorizedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: SecureId, claims?: Claims): Promise<Player> {
|
async get(id: SecureId, claims?: Claims): Promise<Player> {
|
||||||
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.READ, claims))) {
|
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.READ, claims))) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
} else if (Claims.test(Claims.PLAYERS.SELF.READ, claims) && claims?.userId) {
|
} else if (Claims.test(Claims.PLAYERS.SELF.READ, claims) && claims?.userId) {
|
||||||
const user = await orm.users.get(SecureId.fromHash(claims.userId));
|
const user = await orm.users.get(claims.userId);
|
||||||
if (id.raw !== user.playerId.raw) {
|
if (id.raw !== user.playerId.raw) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
@@ -66,21 +59,59 @@ export class PlayersOrm {
|
|||||||
return new Player({
|
return new Player({
|
||||||
id: SecureId.fromID(dbResult.id),
|
id: SecureId.fromID(dbResult.id),
|
||||||
name: dbResult.name,
|
name: dbResult.name,
|
||||||
elo: dbResult.elo,
|
elo: parseInt(dbResult.elo),
|
||||||
isRatingLocked: dbResult.is_rating_locked,
|
isRatingLocked: dbResult.is_rating_locked,
|
||||||
canBeMultiple: dbResult.can_be_multiple,
|
canBeMultiple: dbResult.can_be_multiple,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async list(claims?: Claims): Promise<Player[]> {
|
||||||
id: SecureId,
|
if (!claims || Claims.test(Claims.ADMIN, claims)) {
|
||||||
patch: UpdatePlayerRequest,
|
return (await sql`SELECT * FROM players`).map(
|
||||||
claims?: Claims,
|
(x: { id: string; name: string; elo: string; is_rating_locked: boolean; can_be_multiple: boolean }) =>
|
||||||
): Promise<Player | null> {
|
new Player({
|
||||||
|
id: SecureId.fromID(x.id),
|
||||||
|
name: x.name,
|
||||||
|
elo: parseInt(x.elo),
|
||||||
|
isRatingLocked: x.is_rating_locked,
|
||||||
|
canBeMultiple: x.can_be_multiple,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Claims.test(Claims.PLAYERS.OTHER.READ, claims)) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
await sql`SELECT p.*
|
||||||
|
FROM
|
||||||
|
users u
|
||||||
|
JOIN player_circles upc on upc.player_id = u.id
|
||||||
|
JOIN circles c ON c.id = upc.circle_id
|
||||||
|
JOIN player_circles pc ON pc.circle_id = c.id
|
||||||
|
JOIN players p ON p.id = pc.player_id
|
||||||
|
WHERE
|
||||||
|
u.player_id = ${claims.userId?.raw}
|
||||||
|
AND
|
||||||
|
pc.player_id <> u.player_id`
|
||||||
|
).map(
|
||||||
|
(x: { id: string; name: string; elo: string; is_rating_locked: boolean; can_be_multiple: boolean }) =>
|
||||||
|
new Player({
|
||||||
|
id: SecureId.fromID(x.id),
|
||||||
|
name: x.name,
|
||||||
|
elo: parseInt(x.elo),
|
||||||
|
isRatingLocked: x.is_rating_locked,
|
||||||
|
canBeMultiple: x.can_be_multiple,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: SecureId, patch: UpdatePlayerRequest, claims?: Claims): Promise<Player> {
|
||||||
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.UPDATE, claims))) {
|
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.UPDATE, claims))) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
} else if (Claims.test(Claims.PLAYERS.SELF.UPDATE, claims) && claims?.userId) {
|
} else if (Claims.test(Claims.PLAYERS.SELF.UPDATE, claims) && claims?.userId) {
|
||||||
const user = await orm.users.get(SecureId.fromHash(claims.userId));
|
const user = await orm.users.get(claims.userId);
|
||||||
if (id.raw !== user.playerId.raw) {
|
if (id.raw !== user.playerId.raw) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
@@ -97,21 +128,14 @@ export class PlayersOrm {
|
|||||||
can_be_multiple=${playerToUpdate.canBeMultiple}
|
can_be_multiple=${playerToUpdate.canBeMultiple}
|
||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
try {
|
return await this.get(id);
|
||||||
return await this.get(id, claims);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UnauthorizedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async drop(id: SecureId, claims?: Claims): Promise<undefined> {
|
async drop(id: SecureId, claims?: Claims): Promise<void> {
|
||||||
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.DELETE, claims))) {
|
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.DELETE, claims))) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
} else if (Claims.test(Claims.PLAYERS.SELF.DELETE, claims) && claims?.userId) {
|
} else if (Claims.test(Claims.PLAYERS.SELF.DELETE, claims) && claims?.userId) {
|
||||||
const user = await orm.users.get(SecureId.fromHash(claims.userId));
|
const user = await orm.users.get(claims.userId);
|
||||||
if (id.raw !== user.playerId.raw) {
|
if (id.raw !== user.playerId.raw) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import { orm } from './orm';
|
|||||||
export class User {
|
export class User {
|
||||||
id: SecureId;
|
id: SecureId;
|
||||||
playerId: SecureId;
|
playerId: SecureId;
|
||||||
name: string;
|
email: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
constructor(id: SecureId, playerId: SecureId, name: string, isAdmin: boolean = false, isActive: boolean = true) {
|
constructor(id: SecureId, playerId: SecureId, email: string, isAdmin: boolean = false, isActive: boolean = true) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.playerId = playerId;
|
this.playerId = playerId;
|
||||||
this.name = name;
|
this.email = email;
|
||||||
this.isAdmin = isAdmin;
|
this.isAdmin = isAdmin;
|
||||||
this.isActive = isActive;
|
this.isActive = isActive;
|
||||||
}
|
}
|
||||||
@@ -24,23 +24,23 @@ export class User {
|
|||||||
|
|
||||||
export class UsersOrm {
|
export class UsersOrm {
|
||||||
async create(
|
async create(
|
||||||
model: { username: string; password: string; playerId: SecureId },
|
{ email, password, playerId }: { email: string; password: string; playerId: SecureId },
|
||||||
claims?: Claims,
|
claims?: Claims,
|
||||||
): Promise<User | null> {
|
): Promise<User> {
|
||||||
const existingUser: any = first(
|
const existingUser: any = first(
|
||||||
await sql`SELECT id
|
await sql`SELECT id
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = ${model.username}
|
WHERE email = ${email} OR player_id = ${playerId.raw}
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new BadRequestError(`User ${model.username} already exists`);
|
throw new BadRequestError(`User or player already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultClaims: number[] = await orm.claims.getDefaultClaims();
|
const defaultClaims: number[] = await orm.claims.getDefaultClaims();
|
||||||
const passwordHash = await argon2.hash(model.password);
|
const passwordHash = await argon2.hash(password);
|
||||||
await sql`INSERT INTO users (username, pass_hash, player_id)
|
await sql`INSERT INTO users (email, pass_hash, player_id)
|
||||||
VALUES (${model.username}, ${passwordHash}, ${model.playerId.raw})`;
|
VALUES (${email}, ${passwordHash}, ${playerId.raw})`;
|
||||||
const newUserId: SecureId = SecureId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
|
const newUserId: SecureId = SecureId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
|
||||||
await sql.transaction(async (tx) => {
|
await sql.transaction(async (tx) => {
|
||||||
for (let i in defaultClaims) {
|
for (let i in defaultClaims) {
|
||||||
@@ -49,14 +49,7 @@ export class UsersOrm {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
return await this.get(newUserId);
|
||||||
return await this.get(newUserId, claims);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UnauthorizedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: SecureId, claims?: Claims): Promise<User> {
|
async get(id: SecureId, claims?: Claims): Promise<User> {
|
||||||
@@ -64,7 +57,7 @@ export class UsersOrm {
|
|||||||
!(
|
!(
|
||||||
Claims.test(Claims.ADMIN, claims) ||
|
Claims.test(Claims.ADMIN, claims) ||
|
||||||
Claims.test(Claims.USERS.OTHER.READ, claims) ||
|
Claims.test(Claims.USERS.OTHER.READ, claims) ||
|
||||||
(Claims.test(Claims.USERS.SELF.READ, claims) && id.raw === claims?.userId)
|
(Claims.test(Claims.USERS.SELF.READ, claims) && id === claims?.userId)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
@@ -85,21 +78,17 @@ export class UsersOrm {
|
|||||||
return new User(
|
return new User(
|
||||||
SecureId.fromID(dbResult.id),
|
SecureId.fromID(dbResult.id),
|
||||||
SecureId.fromID(dbResult.player_id),
|
SecureId.fromID(dbResult.player_id),
|
||||||
dbResult.username,
|
dbResult.email,
|
||||||
dbResult.is_admin,
|
dbResult.is_admin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(id: SecureId, patch: UpdateUserRequest, claims?: Claims): Promise<User> {
|
||||||
id: SecureId,
|
|
||||||
patch: UpdateUserRequest,
|
|
||||||
claims?: Claims,
|
|
||||||
): Promise<User | null> {
|
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
Claims.test(Claims.ADMIN, claims) ||
|
Claims.test(Claims.ADMIN, claims) ||
|
||||||
Claims.test(Claims.USERS.OTHER.UPDATE, claims) ||
|
Claims.test(Claims.USERS.OTHER.UPDATE, claims) ||
|
||||||
(Claims.test(Claims.USERS.SELF.UPDATE, claims) && id.raw === claims?.userId)
|
(Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
@@ -116,22 +105,15 @@ export class UsersOrm {
|
|||||||
is_admin=${userToUpdate.isAdmin}
|
is_admin=${userToUpdate.isAdmin}
|
||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
try {
|
return await this.get(id);
|
||||||
return await this.get(id, claims);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UnauthorizedError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async drop(id: SecureId, claims?: Claims): Promise<User | null> {
|
async drop(id: SecureId, claims?: Claims): Promise<void> {
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
Claims.test(Claims.ADMIN, claims) ||
|
Claims.test(Claims.ADMIN, claims) ||
|
||||||
Claims.test(Claims.USERS.OTHER.DELETE, claims) ||
|
Claims.test(Claims.USERS.OTHER.DELETE, claims) ||
|
||||||
(Claims.test(Claims.USERS.SELF.DELETE, claims) && id.raw === claims?.userId)
|
(Claims.test(Claims.USERS.SELF.DELETE, claims) && id === claims?.userId)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
@@ -148,17 +130,17 @@ export class UsersOrm {
|
|||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyCredentials(
|
async verifyCredentials(
|
||||||
username: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<{ userId: SecureId; refreshCount: string } | null> {
|
): Promise<{ userId: SecureId; refreshCount: string } | null> {
|
||||||
const dbResult: any = first(
|
const dbResult: any = first(
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = ${username}
|
WHERE email = ${email}
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
limit 1`,
|
limit 1`,
|
||||||
);
|
);
|
||||||
@@ -183,7 +165,6 @@ export class UsersOrm {
|
|||||||
WHERE id = ${id.raw}
|
WHERE id = ${id.raw}
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
console.log(dbResult.refresh_count, refreshCount);
|
|
||||||
return dbResult.refresh_count === refreshCount;
|
return dbResult.refresh_count === refreshCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,9 +173,9 @@ export class UsersOrm {
|
|||||||
oldPassword: string | null,
|
oldPassword: string | null,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
claims?: Claims,
|
claims?: Claims,
|
||||||
): Promise<undefined> {
|
): Promise<void> {
|
||||||
const isAdmin = Claims.test(Claims.ADMIN, claims);
|
const isAdmin = Claims.test(Claims.ADMIN, claims);
|
||||||
if (!(isAdmin || (Claims.test(Claims.USERS.SELF.UPDATE, claims) && id.raw === claims?.userId))) {
|
if (!(isAdmin || (Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId))) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/routes/invite.ts
Normal file
12
src/routes/invite.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { guard, unwrapMethod } from '../utilities/guard';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
import invite from '../endpoints/invite';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'/api/invite': {
|
||||||
|
POST: guard(invite.create, [Claims.ADMIN, Claims.USERS.INVITE]),
|
||||||
|
},
|
||||||
|
'/api/invite/accept': {
|
||||||
|
POST: unwrapMethod(invite.accept),
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -11,4 +11,7 @@ export default {
|
|||||||
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': {
|
||||||
|
GET: guard(player.list, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ]),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { guard } from '../utilities/guard';
|
import { guard, unwrap, unwrapMethod } from '../utilities/guard';
|
||||||
import user from '../endpoints/user';
|
import user from '../endpoints/user';
|
||||||
import { Claims } from '../orm/claims';
|
import { Claims } from '../orm/claims';
|
||||||
|
|
||||||
@@ -6,6 +6,12 @@ export default {
|
|||||||
'/api/user': {
|
'/api/user': {
|
||||||
POST: guard(user.create, [Claims.ADMIN, Claims.USERS.CREATE]),
|
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': {
|
'/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]),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export class ClaimDefinition {
|
|||||||
public static readonly ADMIN = 'ADMIN';
|
public static readonly ADMIN = 'ADMIN';
|
||||||
public static readonly USERS = {
|
public static readonly USERS = {
|
||||||
CREATE: 'USERS_CREATE',
|
CREATE: 'USERS_CREATE',
|
||||||
|
INVITE: 'USERS_INVITE',
|
||||||
SELF: {
|
SELF: {
|
||||||
READ: 'USERS_SELF_READ',
|
READ: 'USERS_SELF_READ',
|
||||||
UPDATE: 'USERS_SELF_UPDATE',
|
UPDATE: 'USERS_SELF_UPDATE',
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ export class BadRequestError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InternalServerError extends Error {
|
||||||
|
constructor(message?: string | undefined) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
export class UnauthorizedError extends Error {
|
||||||
constructor(message?: string | undefined) {
|
constructor(message?: string | undefined) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ 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 = jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as Claims;
|
const userClaims: Claims = new Claims(jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as any);
|
||||||
if (!userClaims.claims.some((x: string): boolean => guardedClaims.includes(x))) {
|
if (!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)
|
||||||
if (error instanceof TokenExpiredError) {
|
if (error instanceof TokenExpiredError) {
|
||||||
return new UnauthorizedResponse(error.message);
|
return new UnauthorizedResponse(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,3 +18,20 @@ export function memo<T extends (...args: any[]) => {}, S>(
|
|||||||
return cache[key].value;
|
return cache[key].value;
|
||||||
}) as unknown as T;
|
}) as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createRandomString(length:number = 6):string {
|
||||||
|
const maxRandStringVal = parseInt(''.padEnd(length, 'z'), 36);
|
||||||
|
return Math.floor(Math.random() * maxRandStringVal).toString(36).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const brandColours = {
|
||||||
|
dark: '#14111C',
|
||||||
|
mid: '#CBCACB',
|
||||||
|
light: '#FBF8FC',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
black: '#000000',
|
||||||
|
primary: '#CA00E7',
|
||||||
|
secondary: '#FFB527',
|
||||||
|
tertiary: '#6ED500',
|
||||||
|
danger: '#CA3211',
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { hashIds } from './guard';
|
import { hashIds } from './guard';
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
username: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
export interface ChangePasswordRequest {
|
export interface ChangePasswordRequest {
|
||||||
@@ -9,7 +9,7 @@ export interface ChangePasswordRequest {
|
|||||||
newPassword: string;
|
newPassword: string;
|
||||||
}
|
}
|
||||||
export interface CreateUserRequest {
|
export interface CreateUserRequest {
|
||||||
username: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
playerId: string;
|
playerId: string;
|
||||||
}
|
}
|
||||||
@@ -17,13 +17,21 @@ export interface UpdateUserRequest {
|
|||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
export interface InviteUserRequest {
|
||||||
|
email: string;
|
||||||
|
playerId: string;
|
||||||
|
}
|
||||||
|
export interface AcceptInviteRequest {
|
||||||
|
inviteCode: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
export interface CreatePlayerRequest {
|
export interface CreatePlayerRequest {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
export interface UpdatePlayerRequest {
|
export interface UpdatePlayerRequest {
|
||||||
name?: string;
|
name?: string;
|
||||||
isRatingLocked?:boolean;
|
isRatingLocked?: boolean;
|
||||||
canBeMultiple?:boolean;
|
canBeMultiple?: boolean;
|
||||||
}
|
}
|
||||||
export interface CreateGameRequest {
|
export interface CreateGameRequest {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -65,6 +73,10 @@ export class SecureId {
|
|||||||
return this.#hashedValue;
|
return this.#hashedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
valueOf(): string | undefined {
|
||||||
|
return this.#secureValue;
|
||||||
|
}
|
||||||
|
|
||||||
public static fromHash(hash: string) {
|
public static fromHash(hash: string) {
|
||||||
return new SecureId({ public: hash });
|
return new SecureId({ public: hash });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from './errors';
|
import { BadRequestError, NotFoundError, UnauthorizedError } from './errors';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
|
||||||
export class ErrorResponse extends Response {
|
export class ErrorResponse extends Response {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -41,9 +42,11 @@ export class OkResponse extends Response {
|
|||||||
constructor(body?: Model | null) {
|
constructor(body?: Model | null) {
|
||||||
if (body) {
|
if (body) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
isArray(body)
|
||||||
...body,
|
? body
|
||||||
},
|
: {
|
||||||
|
...body,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user