Compare commits
9 Commits
ed942060a2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c4a37f13be | |||
| c23536b3ed | |||
| dee5b2429d | |||
| df60ad4552 | |||
| 872a79663b | |||
| 0fa00e6759 | |||
| 564ffe7c8c | |||
| 3464f32c46 | |||
| 3e9d61b8c6 |
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/auth/login"
|
url: "{{BASE_URL}}/{{SECTOR}}/login"
|
||||||
headers:
|
headers:
|
||||||
- name: Content-Type
|
- name: Content-Type
|
||||||
value: application/json
|
value: application/json
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/auth/token"
|
url: "{{BASE_URL}}/{{SECTOR}}/token"
|
||||||
headers:
|
headers:
|
||||||
- name: Cookie
|
- name: Cookie
|
||||||
value: "{{REFRESH_COOKIE}}"
|
value: "{{REFRESH_COOKIE}}"
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ info:
|
|||||||
name: Auth
|
name: Auth
|
||||||
type: folder
|
type: folder
|
||||||
seq: 1
|
seq: 1
|
||||||
|
|
||||||
|
request:
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: auth
|
||||||
|
|||||||
30
API Tests/BGApp/Circle/Create.yml
Normal file
30
API Tests/BGApp/Circle/Create.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "{{Name}}",
|
||||||
|
"isPublic": {{IsPublic}},
|
||||||
|
"colour": "#FFFFFF"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: Name
|
||||||
|
value: Test Private Circle
|
||||||
|
- name: IsPublic
|
||||||
|
value: "false"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
API Tests/BGApp/Circle/Delete.yml
Normal file
20
API Tests/BGApp/Circle/Delete.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Delete
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CircleID}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: CircleID
|
||||||
|
value: 6Z4214
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
API Tests/BGApp/Circle/Get.yml
Normal file
20
API Tests/BGApp/Circle/Get.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Get
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CircleID}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: CircleID
|
||||||
|
value: P17GOJ
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
24
API Tests/BGApp/Circle/Search.yml
Normal file
24
API Tests/BGApp/Circle/Search.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
info:
|
||||||
|
name: Search
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/search/{{Query}}/{{PageSize}}/{{Page}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: Query
|
||||||
|
value: test
|
||||||
|
- name: PageSize
|
||||||
|
value: "5"
|
||||||
|
- name: Page
|
||||||
|
value: "1"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
28
API Tests/BGApp/Circle/Update.yml
Normal file
28
API Tests/BGApp/Circle/Update.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
info:
|
||||||
|
name: Update
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PATCH
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CircleID}}"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name":"{{Name}}"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: CircleID
|
||||||
|
value: 6Z4214
|
||||||
|
- name: Name
|
||||||
|
value: Test update
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
10
API Tests/BGApp/Circle/folder.yml
Normal file
10
API Tests/BGApp/Circle/folder.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
info:
|
||||||
|
name: Circle
|
||||||
|
type: folder
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: circles
|
||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/collection/{{CollectionID}}/add"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}/add"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/collection"
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
url: "{{BASE_URL}}/api/collection/{{CollectionID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/collection/{{CollectionID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}"
|
||||||
params:
|
|
||||||
- name: ""
|
|
||||||
value: ""
|
|
||||||
type: query
|
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/collection/list/{{PageSize}}/{{Page}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/list/{{PageSize}}/{{Page}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
variables:
|
variables:
|
||||||
- name: PageSize
|
- name: PageSize
|
||||||
value: "1"
|
value: "5"
|
||||||
- name: Page
|
- name: Page
|
||||||
value: "0"
|
value: "1"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/collection/{{CollectionID}}/remove"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}/remove"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: PATCH
|
method: PATCH
|
||||||
url: "{{BASE_URL}}/api/collection/{{CollectionID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{CollectionID}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
info:
|
info:
|
||||||
name: Collections
|
name: Collections
|
||||||
type: folder
|
type: folder
|
||||||
seq: 6
|
seq: 1
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: collections
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/game"
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
url: "{{BASE_URL}}/api/game/{{GameID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{GameID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/game/{{GameID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{GameID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/game/search/{{Query}}/{{PageSize}}/{{Page}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/search/{{Query}}/{{PageSize}}/{{Page}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
@@ -13,9 +13,9 @@ runtime:
|
|||||||
- name: Query
|
- name: Query
|
||||||
value: test
|
value: test
|
||||||
- name: PageSize
|
- name: PageSize
|
||||||
value: "2"
|
value: "5"
|
||||||
- name: Page
|
- name: Page
|
||||||
value: "2"
|
value: "1"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: PATCH
|
method: PATCH
|
||||||
url: "{{BASE_URL}}/api/game/{{GameID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{GameID}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
@@ -17,9 +17,9 @@ http:
|
|||||||
runtime:
|
runtime:
|
||||||
variables:
|
variables:
|
||||||
- name: GameID
|
- name: GameID
|
||||||
value: ""
|
value: DM5GMY
|
||||||
- name: Name
|
- name: Name
|
||||||
value: ""
|
value: Update test
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
info:
|
info:
|
||||||
name: Game
|
name: Game
|
||||||
type: folder
|
type: folder
|
||||||
seq: 4
|
seq: 1
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: games
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/invite/accept"
|
url: "{{BASE_URL}}/{{SECTOR}}/accept"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/invite"
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,3 +5,6 @@ info:
|
|||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: invites
|
||||||
|
|||||||
35
API Tests/BGApp/Matches/Create.yml
Normal file
35
API Tests/BGApp/Matches/Create.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"gameId": "RM089N",
|
||||||
|
"participants": [
|
||||||
|
{"playerId": "Q3M1PR", "standing": 0},
|
||||||
|
{"playerId": "539DPX", "standing": 1},
|
||||||
|
{"playerId": "JZ7939", "standing": 2}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: Email
|
||||||
|
value: ""
|
||||||
|
- name: Password
|
||||||
|
value: ""
|
||||||
|
- name: PlayerID
|
||||||
|
value: ""
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
API Tests/BGApp/Matches/Delete.yml
Normal file
20
API Tests/BGApp/Matches/Delete.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Delete
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/{{MatchID}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: MatchID
|
||||||
|
value: 846M1L
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
API Tests/BGApp/Matches/Get.yml
Normal file
20
API Tests/BGApp/Matches/Get.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Get
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/{{MatchID}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: MatchID
|
||||||
|
value: 848O12
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
23
API Tests/BGApp/Matches/Leave.yml
Normal file
23
API Tests/BGApp/Matches/Leave.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
info:
|
||||||
|
name: Leave
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{BASE_URL}}/{{SECTOR}}/{{MatchID}}/leave"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: ""
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: MatchID
|
||||||
|
value: 848O12
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
10
API Tests/BGApp/Matches/folder.yml
Normal file
10
API Tests/BGApp/Matches/folder.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
info:
|
||||||
|
name: Matches
|
||||||
|
type: folder
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: matches
|
||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/player"
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
url: "{{BASE_URL}}/api/player/{{PlayerID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{PlayerID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/player/{{PlayerID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{PlayerID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
variables:
|
variables:
|
||||||
- name: PlayerID
|
- name: PlayerID
|
||||||
value: ""
|
value: 539DPX
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
|
|||||||
@@ -5,9 +5,16 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/player/list"
|
url: "{{BASE_URL}}/{{SECTOR}}/list/{{PageSize}}/{{Page}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: PageSize
|
||||||
|
value: "100"
|
||||||
|
- name: Page
|
||||||
|
value: "1"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
timeout: 0
|
timeout: 0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: PATCH
|
method: PATCH
|
||||||
url: "{{BASE_URL}}/api/player/{{PlayerID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{PlayerID}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
info:
|
info:
|
||||||
name: Players
|
name: Players
|
||||||
type: folder
|
type: folder
|
||||||
seq: 2
|
seq: 1
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: players
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
url: "{{BASE_URL}}/api/user"
|
url: "{{BASE_URL}}/{{SECTOR}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
url: "{{BASE_URL}}/api/user/{{UserID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{BASE_URL}}/api/user/{{UserID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
runtime:
|
runtime:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: PATCH
|
method: PATCH
|
||||||
url: "{{BASE_URL}}/api/user/{{UserID}}"
|
url: "{{BASE_URL}}/{{SECTOR}}/{{UserID}}"
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
data: |-
|
data: |-
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
info:
|
info:
|
||||||
name: User
|
name: User
|
||||||
type: folder
|
type: folder
|
||||||
seq: 3
|
seq: 1
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
variables:
|
||||||
|
- name: SECTOR
|
||||||
|
value: users
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#file: noinspection SpellCheckingInspection
|
||||||
name: BGApp
|
name: BGApp
|
||||||
variables:
|
variables:
|
||||||
- name: BEARER_TOKEN
|
- name: BEARER_TOKEN
|
||||||
@@ -5,4 +6,4 @@ variables:
|
|||||||
- name: REFRESH_COOKIE
|
- name: REFRESH_COOKIE
|
||||||
value: ""
|
value: ""
|
||||||
- name: BASE_URL
|
- name: BASE_URL
|
||||||
value: http://localhost:3000
|
value: http://localhost:3000/api
|
||||||
|
|||||||
@@ -18,16 +18,6 @@ request:
|
|||||||
auth:
|
auth:
|
||||||
type: bearer
|
type: bearer
|
||||||
token: "{{BEARER_TOKEN}}"
|
token: "{{BEARER_TOKEN}}"
|
||||||
actions:
|
|
||||||
- type: set-variable
|
|
||||||
phase: after-response
|
|
||||||
selector:
|
|
||||||
expression: ${token}
|
|
||||||
method: jsonq
|
|
||||||
variable:
|
|
||||||
name: Token
|
|
||||||
scope: runtime
|
|
||||||
disabled: true
|
|
||||||
bundled: false
|
bundled: false
|
||||||
extensions:
|
extensions:
|
||||||
bruno:
|
bruno:
|
||||||
|
|||||||
@@ -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
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -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,57 +20,59 @@ export const InviteEmail = (props: InviteEmailProperties) => (
|
|||||||
cellSpacing={0}
|
cellSpacing={0}
|
||||||
cellPadding={0}
|
cellPadding={0}
|
||||||
>
|
>
|
||||||
<tr>
|
<tbody>
|
||||||
<td align="center">
|
<tr>
|
||||||
<div
|
<td align="center">
|
||||||
style={{
|
<div
|
||||||
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={{
|
style={{
|
||||||
marginBottom: '40px',
|
padding: '20px',
|
||||||
|
borderRadius: '20px',
|
||||||
|
background: brandColours.white,
|
||||||
|
margin: '50px',
|
||||||
|
color: brandColours.dark,
|
||||||
|
maxWidth: '450px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<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={{
|
style={{
|
||||||
display: 'inline-block',
|
marginBottom: '40px',
|
||||||
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
|
||||||
</a>
|
style={{
|
||||||
</p>
|
display: 'inline-block',
|
||||||
<p
|
padding: '10px 20px',
|
||||||
style={{
|
borderRadius: '5px',
|
||||||
fontSize: '0.8rem',
|
background: brandColours.primary,
|
||||||
opacity: '80%',
|
textDecoration: 'none',
|
||||||
}}
|
color: brandColours.light,
|
||||||
>
|
fontSize: '20px',
|
||||||
If above button does not work, copy the link below into a new browser tab:
|
fontWeight: 'bold',
|
||||||
<br />
|
}}
|
||||||
{`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
|
href={`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
|
||||||
</p>
|
>
|
||||||
</div>
|
Join {process.env.PRODUCT_NAME}
|
||||||
</td>
|
</a>
|
||||||
</tr>
|
</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>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ 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_SECRET_KEY as string,
|
process.env.JWT_REFRESH_KEY as string,
|
||||||
{ expiresIn: `${tokenLifeSpanInDays * 24}h` },
|
{ expiresIn: `${tokenLifeSpanInDays * 24}h` },
|
||||||
);
|
);
|
||||||
const cookies = request?.request?.cookies;
|
const cookies = request?.request?.cookies;
|
||||||
@@ -33,9 +33,9 @@ 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'
|
path: '/api/auth/token',
|
||||||
});
|
});
|
||||||
return new OkResponse();
|
return new OkResponse({ token });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
@@ -52,15 +52,15 @@ async function token(request: UnwrappedRequest): Promise<Response> {
|
|||||||
const refreshToken: {
|
const refreshToken: {
|
||||||
u: string;
|
u: string;
|
||||||
r: string;
|
r: string;
|
||||||
} = jwt.verify(refreshCookie, process.env.JWT_SECRET_KEY as string) as { u: string; r: string };
|
} = jwt.verify(refreshCookie, process.env.JWT_REFRESH_KEY as string) as { u: string; r: string };
|
||||||
|
|
||||||
if (!(await orm.users.verifyRefreshCount(UserId.fromID(refreshToken.u), refreshToken.r))) {
|
if (!(await orm.users.verifyRefreshCount(UserId.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,
|
||||||
|
|||||||
74
src/endpoints/circles.ts
Normal file
74
src/endpoints/circles.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
|
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
|
||||||
|
import { CreateCircleRequest, InviteToCircleRequest, UpdateCircleRequest } from '../utilities/requestModels';
|
||||||
|
import { CircleId, PlayerId, UserId } from '../utilities/secureIds';
|
||||||
|
|
||||||
|
async function create(request: UnwrappedRequest<CreateCircleRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new CreatedResponse(await orm.circles.create(request.body, request.claims));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invite(request: UnwrappedRequest<InviteToCircleRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
let relatedEntityId: UserId | PlayerId | string | undefined;
|
||||||
|
if (request.body.userId) {
|
||||||
|
relatedEntityId = UserId.fromHash(request.body.userId);
|
||||||
|
} else if (request.body.playerId) {
|
||||||
|
relatedEntityId = PlayerId.fromHash(request.body.playerId);
|
||||||
|
} else {
|
||||||
|
relatedEntityId = request.body.email;
|
||||||
|
}
|
||||||
|
return new CreatedResponse(
|
||||||
|
await orm.circles.invite(CircleId.fromHash(request.params.id), relatedEntityId, request.claims),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.circles.get(CircleId.fromHash(request.params.id)));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(request: UnwrappedRequest<UpdateCircleRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(
|
||||||
|
await orm.circles.update(CircleId.fromHash(request.params.id), request.body),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drop(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.circles.drop(CircleId.fromHash(request.params.id)));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function query(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new PagedResponse(request, await orm.circles.query(request.params.query));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
update,
|
||||||
|
drop,
|
||||||
|
query,
|
||||||
|
invite,
|
||||||
|
};
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import { orm } from '../orm/orm';
|
import { orm } from '../orm/orm';
|
||||||
import { UnwrappedRequest } from '../utilities/guard';
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
|
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
|
||||||
import {
|
import { GameToCollectionRequest, CreateCollectionRequest, UpdateCollectionRequest } from '../utilities/requestModels';
|
||||||
GameToCollectionRequest,
|
|
||||||
CreateCollectionRequest,
|
|
||||||
UpdateCollectionRequest,
|
|
||||||
} from '../utilities/requestModels';
|
|
||||||
import { CollectionId, GameId } from '../utilities/secureIds';
|
import { CollectionId, GameId } from '../utilities/secureIds';
|
||||||
|
|
||||||
async function create(request: UnwrappedRequest<CreateCollectionRequest>): Promise<Response> {
|
async function create(request: UnwrappedRequest<CreateCollectionRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const newPlayer = await orm.collections.create(request.body, request.claims);
|
return new CreatedResponse(await orm.collections.create(request.body, request.claims));
|
||||||
return new CreatedResponse(newPlayer);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { orm } from '../orm/orm';
|
import { orm } from '../orm/orm';
|
||||||
import { UnwrappedRequest } from '../utilities/guard';
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
|
import { CreatedResponse, ErrorResponse, OkResponse, PagedResponse } from '../utilities/responseHelper';
|
||||||
import {
|
import { CreateGameRequest, UpdateGameRequest } from '../utilities/requestModels';
|
||||||
CreateGameRequest,
|
|
||||||
UpdateGameRequest,
|
|
||||||
} from '../utilities/requestModels';
|
|
||||||
import { GameId } from '../utilities/secureIds';
|
import { GameId } from '../utilities/secureIds';
|
||||||
|
|
||||||
async function create(request: UnwrappedRequest<CreateGameRequest>): Promise<Response> {
|
async function create(request: UnwrappedRequest<CreateGameRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const newUser = await orm.games.create(
|
return new CreatedResponse(
|
||||||
{
|
await orm.games.create(
|
||||||
name: request.body.name,
|
{
|
||||||
bggId: request.body.bggId,
|
name: request.body.name,
|
||||||
imagePath: request.body.imagePath,
|
bggId: request.body.bggId,
|
||||||
},
|
imagePath: request.body.imagePath,
|
||||||
request.claims,
|
},
|
||||||
|
request.claims,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return new CreatedResponse(newUser);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
@@ -33,13 +31,7 @@ async function get(request: UnwrappedRequest): Promise<Response> {
|
|||||||
|
|
||||||
async function update(request: UnwrappedRequest<UpdateGameRequest>): Promise<Response> {
|
async function update(request: UnwrappedRequest<UpdateGameRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
return new OkResponse(
|
return new OkResponse(await orm.games.update(GameId.fromHash(request.params.id), request.body, request.claims));
|
||||||
await orm.games.update(
|
|
||||||
GameId.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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import { orm } from '../orm/orm';
|
import { orm } from '../orm/orm';
|
||||||
import { UnwrappedRequest } from '../utilities/guard';
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
import { CreatedResponse, ErrorResponse } from '../utilities/responseHelper';
|
import { CreatedResponse, ErrorResponse } from '../utilities/responseHelper';
|
||||||
import {
|
import { AcceptInviteRequest, InviteUserRequest } from '../utilities/requestModels';
|
||||||
AcceptInviteRequest,
|
|
||||||
InviteUserRequest,
|
|
||||||
} from '../utilities/requestModels';
|
|
||||||
import { PlayerId, UserId } from '../utilities/secureIds';
|
import { PlayerId, UserId } from '../utilities/secureIds';
|
||||||
|
|
||||||
async function create(request: UnwrappedRequest<InviteUserRequest>): Promise<Response> {
|
async function create(request: UnwrappedRequest<InviteUserRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const newUser = await orm.invites.create(
|
return new CreatedResponse(
|
||||||
{
|
await orm.invites.create({
|
||||||
...request.body,
|
...request.body,
|
||||||
playerId: PlayerId.fromHash(request.body.playerId),
|
playerId: PlayerId.fromHash(request.body.playerId),
|
||||||
invitedByUserId: request.claims.userId as UserId,
|
invitedByUserId: request.claims.userId as UserId,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
return new CreatedResponse(newUser);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
@@ -24,8 +20,7 @@ async function create(request: UnwrappedRequest<InviteUserRequest>): Promise<Res
|
|||||||
|
|
||||||
async function accept(request: UnwrappedRequest<AcceptInviteRequest>): Promise<Response> {
|
async function accept(request: UnwrappedRequest<AcceptInviteRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const newUser = await orm.invites.accept(request.body);
|
return new CreatedResponse(await orm.invites.accept(request.body));
|
||||||
return new CreatedResponse(newUser);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/endpoints/matches.ts
Normal file
55
src/endpoints/matches.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
|
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
|
||||||
|
import { CreateMatchRequest } from '../utilities/requestModels';
|
||||||
|
import { GameId, MatchId, PlayerId, UserId } from '../utilities/secureIds';
|
||||||
|
import { MatchParticipant } from '../orm/matches';
|
||||||
|
|
||||||
|
async function create(request: UnwrappedRequest<CreateMatchRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new CreatedResponse(
|
||||||
|
await orm.matches.create({
|
||||||
|
gameId: GameId.fromHash(request.body.gameId),
|
||||||
|
ownerId: request.claims.userId as UserId,
|
||||||
|
participants: request.body.participants.map(
|
||||||
|
(x) => new MatchParticipant(PlayerId.fromHash(x.playerId), x.standing),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.matches.get(MatchId.fromHash(request.params.id), request.claims));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drop(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.matches.drop(MatchId.fromHash(request.params.id), request.claims));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leave(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(
|
||||||
|
await orm.matches.removePlayer(MatchId.fromHash(request.params.id), request.claims.userId as UserId),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
drop,
|
||||||
|
leave,
|
||||||
|
};
|
||||||
@@ -6,8 +6,7 @@ import { PlayerId } from '../utilities/secureIds';
|
|||||||
|
|
||||||
async function create(request: UnwrappedRequest<CreatePlayerRequest>): Promise<Response> {
|
async function create(request: UnwrappedRequest<CreatePlayerRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const newPlayer = await orm.players.create(request.body);
|
return new CreatedResponse(await orm.players.create(request.body));
|
||||||
return new CreatedResponse(newPlayer);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import { PlayerId, UserId } from '../utilities/secureIds';
|
|||||||
|
|
||||||
async function create(request: UnwrappedRequest<CreateUserRequest>): Promise<Response> {
|
async function create(request: UnwrappedRequest<CreateUserRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const newUser = await orm.users.create(
|
return new CreatedResponse(
|
||||||
{
|
await orm.users.create({
|
||||||
...request.body,
|
...request.body,
|
||||||
playerId: PlayerId.fromHash(request.body.playerId),
|
playerId: PlayerId.fromHash(request.body.playerId),
|
||||||
}
|
}),
|
||||||
);
|
);
|
||||||
return new CreatedResponse(newUser);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
@@ -28,9 +27,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(UserId.fromHash(request.params.id), request.body, request.claims));
|
||||||
await orm.users.update(UserId.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);
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/index.ts
28
src/index.ts
@@ -5,21 +5,29 @@ import players from './routes/players';
|
|||||||
import games from './routes/games';
|
import games from './routes/games';
|
||||||
import invites from './routes/invites';
|
import invites from './routes/invites';
|
||||||
import collections from './routes/collections';
|
import collections from './routes/collections';
|
||||||
|
import { buildRoute } from './utilities/routeBuilder';
|
||||||
|
import { MatchId } from './utilities/secureIds';
|
||||||
|
import matches from './routes/matches';
|
||||||
|
import circles from './routes/circles';
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
routes: {
|
routes: buildRoute({
|
||||||
...auth,
|
[process.env.API_ROOT_PATH ?? '']: {
|
||||||
...users,
|
auth,
|
||||||
...players,
|
users,
|
||||||
...games,
|
players,
|
||||||
...invites,
|
games,
|
||||||
...collections,
|
invites,
|
||||||
'/test': {
|
collections,
|
||||||
|
matches,
|
||||||
|
circles,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
GET: () => {
|
GET: () => {
|
||||||
return new OkResponse();
|
return new OkResponse(MatchId.fromID('2').value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}) as any,
|
||||||
|
|
||||||
// (optional) fallback for unmatched routes:
|
// (optional) fallback for unmatched routes:
|
||||||
fetch(): Response {
|
fetch(): Response {
|
||||||
|
|||||||
192
src/orm/circles.ts
Normal file
192
src/orm/circles.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { Claims } from './claims';
|
||||||
|
import { sql } from 'bun';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { CreateCircleRequest, UpdateCircleRequest } from '../utilities/requestModels';
|
||||||
|
import { memo } from '../utilities/helpers';
|
||||||
|
import { CircleId, PlayerId, UserId } from '../utilities/secureIds';
|
||||||
|
import { orm } from './orm';
|
||||||
|
import { User } from './user';
|
||||||
|
|
||||||
|
export class Circle {
|
||||||
|
id: CircleId;
|
||||||
|
owningUserId: UserId;
|
||||||
|
name: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
imagePath?: string;
|
||||||
|
colour?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
id,
|
||||||
|
owningUserId,
|
||||||
|
name,
|
||||||
|
isPublic,
|
||||||
|
imagePath,
|
||||||
|
colour,
|
||||||
|
}: {
|
||||||
|
id: CircleId;
|
||||||
|
owningUserId: UserId;
|
||||||
|
name: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
imagePath?: string;
|
||||||
|
colour?: string;
|
||||||
|
}) {
|
||||||
|
this.id = id;
|
||||||
|
this.owningUserId = owningUserId;
|
||||||
|
this.name = name;
|
||||||
|
this.isPublic = isPublic;
|
||||||
|
this.imagePath = imagePath;
|
||||||
|
this.colour = colour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CircleOrm {
|
||||||
|
async create(model: CreateCircleRequest, claims?: Claims): Promise<Circle> {
|
||||||
|
if (model.isPublic && claims && !claims.test(Claims.ADMIN, Claims.CIRCLES.PUBLIC.CREATE)) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`INSERT INTO circles (owning_user_id, name, is_public, colour)
|
||||||
|
VALUES (${claims?.userId.raw},
|
||||||
|
${model.name},
|
||||||
|
${model.isPublic},
|
||||||
|
${model.colour})`;
|
||||||
|
const newRecordId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
|
return await this.get(CircleId.fromID(newRecordId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: CircleId, claims?: Claims): Promise<Circle> {
|
||||||
|
const record: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM circles
|
||||||
|
WHERE id = ${id.raw}
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new NotFoundError('No matching game exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: User;
|
||||||
|
if (
|
||||||
|
claims &&
|
||||||
|
!claims.test(Claims.ADMIN) &&
|
||||||
|
!(claims.test(Claims.CIRCLES.PUBLIC.READ) && record.is_public) &&
|
||||||
|
!(claims.test(Claims.CIRCLES.OWNED.READ) && record.owning_user_id === claims.userId.raw) &&
|
||||||
|
!(
|
||||||
|
claims.test(Claims.CIRCLES.PRIVATE.READ_IF_MEMBER) &&
|
||||||
|
(user = await orm.users.get(claims.userId)) &&
|
||||||
|
(await sql`SELECT * FROM player_circles WHERE circle_id = ${id.raw}`).some(
|
||||||
|
(x: { player_id: string }) => x.player_id === user.playerId.raw,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Circle({
|
||||||
|
id: CircleId.fromID(record.id),
|
||||||
|
owningUserId: UserId.fromID(record.owning_user_id),
|
||||||
|
name: record.name,
|
||||||
|
isPublic: record.is_public,
|
||||||
|
imagePath: record.image_path,
|
||||||
|
colour: record.colour,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: CircleId, patch: UpdateCircleRequest): Promise<Circle> {
|
||||||
|
const circle = await this.get(id);
|
||||||
|
circle.name = patch.name ?? circle.name;
|
||||||
|
circle.colour = patch.colour ?? circle.colour;
|
||||||
|
circle.imagePath = patch.imagePath ?? circle.imagePath;
|
||||||
|
|
||||||
|
await sql`UPDATE circles
|
||||||
|
SET name=${circle.name},
|
||||||
|
colour=${circle.colour},
|
||||||
|
image_path=${circle.imagePath}
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
|
return await this.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async drop(id: CircleId): Promise<void> {
|
||||||
|
// Ensure record exists before attempting to delete
|
||||||
|
await this.get(id);
|
||||||
|
await sql.transaction(async (tx) => {
|
||||||
|
await tx`DELETE
|
||||||
|
FROM player_circles
|
||||||
|
WHERE circle_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM circle_invites
|
||||||
|
WHERE circle_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM circle_comments
|
||||||
|
WHERE circle_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM circles
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async invite(
|
||||||
|
circleId: CircleId,
|
||||||
|
relatedRecord: PlayerId | UserId | string | undefined,
|
||||||
|
claims: Claims,
|
||||||
|
): Promise<void> {
|
||||||
|
if (relatedRecord === undefined) {
|
||||||
|
throw new BadRequestError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const circle = await this.get(circleId, claims);
|
||||||
|
if (
|
||||||
|
claims &&
|
||||||
|
((circle.isPublic && !claims.test(Claims.CIRCLES.PUBLIC.USERS.INVITE)) ||
|
||||||
|
(!circle.isPublic &&
|
||||||
|
!(claims.test(Claims.CIRCLES.OWNED.USERS.INVITE) && circle.owningUserId === claims.userId)))
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
let invitedUserId: UserId | undefined;
|
||||||
|
if (relatedRecord instanceof UserId) {
|
||||||
|
invitedUserId = relatedRecord;
|
||||||
|
} else if (relatedRecord instanceof PlayerId) {
|
||||||
|
invitedUserId = (await orm.users.getByPlayer(relatedRecord))?.id;
|
||||||
|
} else {
|
||||||
|
invitedUserId = (await orm.users.getByEmail(relatedRecord))?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invitedUserId) {
|
||||||
|
throw new BadRequestError();
|
||||||
|
}
|
||||||
|
await sql`INSERT INTO circle_invites(invited_user_id, invited_by_user_id, circle_id)
|
||||||
|
VALUES (${invitedUserId.raw}, ${claims.userId.raw}, ${circleId.raw})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
query: (query: string) => Promise<Circle[]> = memo<(query: string) => Promise<Circle[]>, Circle[]>(this.#query);
|
||||||
|
async #query(query: string): Promise<Circle[]> {
|
||||||
|
const queryResult: any = await sql` SELECT
|
||||||
|
id, name, owning_user_id, is_public
|
||||||
|
FROM (SELECT *, SIMILARITY(${query}, name) as similarity FROM circles WHERE is_public=true)
|
||||||
|
WHERE similarity > 0
|
||||||
|
ORDER BY similarity
|
||||||
|
LIMIT 5;`;
|
||||||
|
|
||||||
|
if (!queryResult) {
|
||||||
|
throw new NotFoundError('No matching circles exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryResult.map(
|
||||||
|
(x: { id: string; name: string; owning_user_id: string; is_public: boolean }) =>
|
||||||
|
new Circle({
|
||||||
|
id: CircleId.fromID(x.id),
|
||||||
|
name: x.name,
|
||||||
|
isPublic: x.is_public,
|
||||||
|
owningUserId: UserId.fromID(x.owning_user_id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,49 @@
|
|||||||
import { sql } from 'bun';
|
import { sql } from 'bun';
|
||||||
import { ClaimDefinition } from '../utilities/claimDefinitions';
|
import { ClaimDefinition } from '../utilities/claimDefinitions';
|
||||||
import { SecureId, UserId } from '../utilities/secureIds';
|
import { UserId } from '../utilities/secureIds';
|
||||||
|
|
||||||
export class Claims extends ClaimDefinition {
|
export class Claims extends ClaimDefinition {
|
||||||
userId?: UserId;
|
userId: UserId;
|
||||||
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 records: 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: records.map((x) => x.name),
|
||||||
return claims;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultClaims(): Promise<number[]> {
|
async getDefaultClaims(): Promise<number[]> {
|
||||||
const dbResults: any[] = await sql`SELECT id
|
const records: any[] = await sql`SELECT id
|
||||||
FROM claims
|
FROM claims
|
||||||
WHERE is_default = true;`;
|
WHERE is_default = true;`;
|
||||||
return dbResults.map((x) => x.id);
|
return records.map((x) => x.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,41 +24,42 @@ export class CollectionsOrm {
|
|||||||
async create(model: { name: string }, claims: Claims): Promise<Collection> {
|
async create(model: { name: string }, claims: Claims): Promise<Collection> {
|
||||||
await sql`INSERT INTO collections (name, user_id)
|
await sql`INSERT INTO collections (name, user_id)
|
||||||
VALUES (${model.name}, ${claims?.userId?.raw})`;
|
VALUES (${model.name}, ${claims?.userId?.raw})`;
|
||||||
const newPCollectionId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
const newRecordId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
return await this.get(CollectionId.fromID(newPCollectionId));
|
return await this.get(CollectionId.fromID(newRecordId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: CollectionId, claims?: Claims): Promise<Collection> {
|
async get(id: CollectionId, claims?: Claims): Promise<Collection> {
|
||||||
const dbResult: any = await sql`SELECT
|
const records: any = await sql`SELECT
|
||||||
c.id AS collection_id,
|
c.id AS collection_id,
|
||||||
c.name AS collection_name,
|
c.name AS collection_name,
|
||||||
c.user_id AS user_id,
|
c.user_id AS user_id,
|
||||||
g.id AS game_id,
|
g.id AS game_id,
|
||||||
g.name AS game_name
|
g.name AS game_name
|
||||||
FROM collections c
|
FROM collections c
|
||||||
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 &&
|
records?.[0]?.user_id === claims?.userId?.raw)
|
||||||
dbResult?.[0]?.user_id !== claims.userId.raw
|
)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dbResult?.length) {
|
if (!records?.length) {
|
||||||
throw new NotFoundError('No matching player exists');
|
throw new NotFoundError('No matching player exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Collection({
|
return new Collection({
|
||||||
id: CollectionId.fromID(dbResult[0].collection_id),
|
id: CollectionId.fromID(records[0].collection_id),
|
||||||
name: dbResult[0].collection_name,
|
name: records[0].collection_name,
|
||||||
games: dbResult
|
ownerId: UserId.fromID(records[0].user_id),
|
||||||
|
games: records
|
||||||
.filter((x: { game_id: string; game_name: string }) => x.game_id)
|
.filter((x: { game_id: string; game_name: string }) => x.game_id)
|
||||||
.map(
|
.map(
|
||||||
(x: { game_id: string; game_name: string }) =>
|
(x: { game_id: string; game_name: string }) =>
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,45 +23,45 @@ 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 newRecordId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
return await this.get(GameId.fromID(newGameId));
|
return await this.get(GameId.fromID(newRecordId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: GameId): Promise<Game> {
|
async get(id: GameId): Promise<Game> {
|
||||||
const dbResult: any = first(
|
const record: any = first(
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM games
|
FROM games
|
||||||
WHERE id = ${id.raw}
|
WHERE id = ${id.raw}
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dbResult) {
|
if (!record) {
|
||||||
throw new NotFoundError('No matching game exists');
|
throw new NotFoundError('No matching game exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Game({
|
return new Game({
|
||||||
id: GameId.fromID(dbResult.id),
|
id: GameId.fromID(record.id),
|
||||||
name: dbResult.name,
|
name: record.name,
|
||||||
bggId: dbResult.bgg_id,
|
bggId: record.bgg_id,
|
||||||
imagePath: dbResult.image_path,
|
imagePath: record.image_path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: GameId, patch: UpdateGameRequest, claims?: Claims): Promise<Game> {
|
async update(id: GameId, patch: UpdateGameRequest, claims?: Claims): Promise<Game> {
|
||||||
const gameToUpdate = await this.get(id);
|
const game = await this.get(id);
|
||||||
gameToUpdate.name = patch.name ?? gameToUpdate.name;
|
game.name = patch.name ?? game.name;
|
||||||
gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId;
|
game.bggId = patch.bggId ?? game.bggId;
|
||||||
|
|
||||||
if (Claims.test(Claims.GAMES.MANAGE_IMAGES, claims)) {
|
if (claims?.test(Claims.GAMES.MANAGE_IMAGES)) {
|
||||||
gameToUpdate.imagePath = patch.imagePath ?? gameToUpdate.imagePath;
|
game.imagePath = patch.imagePath ?? game.imagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql`UPDATE games
|
await sql`UPDATE games
|
||||||
SET name=${gameToUpdate.name},
|
SET name=${game.name},
|
||||||
bgg_id=${gameToUpdate.bggId},
|
bgg_id=${game.bggId},
|
||||||
image_path=${gameToUpdate.imagePath}
|
image_path=${game.imagePath}
|
||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
return await this.get(id);
|
return await this.get(id);
|
||||||
@@ -90,18 +90,18 @@ export class GamesOrm {
|
|||||||
|
|
||||||
query: (query: string) => Promise<Game[]> = memo<(query: string) => Promise<Game[]>, Game[]>(this.#query);
|
query: (query: string) => Promise<Game[]> = memo<(query: string) => Promise<Game[]>, Game[]>(this.#query);
|
||||||
async #query(query: string): Promise<Game[]> {
|
async #query(query: string): Promise<Game[]> {
|
||||||
const dbResult: any = await sql` SELECT
|
const records: any = await sql` SELECT
|
||||||
id, name
|
id, name
|
||||||
FROM (SELECT *, SIMILARITY(${query}, name) as similarity FROM games)
|
FROM (SELECT *, SIMILARITY(${query}, name) as similarity FROM games)
|
||||||
WHERE similarity > 0
|
WHERE similarity > 0
|
||||||
ORDER BY similarity
|
ORDER BY similarity
|
||||||
LIMIT 5;`;
|
LIMIT 5;`;
|
||||||
|
|
||||||
if (!dbResult) {
|
if (!records) {
|
||||||
throw new NotFoundError('No matching game exists');
|
throw new NotFoundError('No matching game exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbResult.map(
|
return records.map(
|
||||||
(x: { id: string; name: string }) =>
|
(x: { id: string; name: string }) =>
|
||||||
new Game({
|
new Game({
|
||||||
id: GameId.fromID(x.id),
|
id: GameId.fromID(x.id),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -35,7 +35,7 @@ export class InvitesOrm {
|
|||||||
throw new UnauthorizedError('Invite allowance reached.');
|
throw new UnauthorizedError('Invite allowance reached.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviteExists = (
|
const doesInviteExist = (
|
||||||
first(
|
first(
|
||||||
await sql`SELECT COUNT(*) > 0 AS exists
|
await sql`SELECT COUNT(*) > 0 AS exists
|
||||||
FROM user_invites
|
FROM user_invites
|
||||||
@@ -45,12 +45,12 @@ export class InvitesOrm {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
}
|
}
|
||||||
)?.exists;
|
)?.exists;
|
||||||
if (inviteExists) {
|
if (doesInviteExist) {
|
||||||
throw new BadRequestError('Player has already been invited.');
|
throw new BadRequestError('Player has already been invited.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerHasUser = (
|
const isPlayerAssociatedWithUser = (
|
||||||
first(
|
first(
|
||||||
await sql`SELECT COUNT(*) > 0 AS exists
|
await sql`SELECT COUNT(*) > 0 AS exists
|
||||||
FROM users
|
FROM users
|
||||||
@@ -60,7 +60,7 @@ export class InvitesOrm {
|
|||||||
exists: boolean;
|
exists: boolean;
|
||||||
}
|
}
|
||||||
)?.exists;
|
)?.exists;
|
||||||
if (playerHasUser) {
|
if (isPlayerAssociatedWithUser) {
|
||||||
throw new BadRequestError('User has already been invited.');
|
throw new BadRequestError('User has already been invited.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export class InvitesOrm {
|
|||||||
const invitationCode = createRandomString(6);
|
const invitationCode = createRandomString(6);
|
||||||
await sql`INSERT INTO user_invites (invite_code, email, player_id, invited_by_user_id)
|
await sql`INSERT INTO user_invites (invite_code, email, player_id, invited_by_user_id)
|
||||||
VALUES (${invitationCode}, ${email}, ${playerId.raw}, ${invitedByUserId.raw})`;
|
VALUES (${invitationCode}, ${email}, ${playerId.raw}, ${invitedByUserId.raw})`;
|
||||||
const newInviteId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
const newRecordId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_KEY);
|
const resend = new Resend(process.env.RESEND_KEY);
|
||||||
const resendResponse = await resend.emails.send({
|
const resendResponse = await resend.emails.send({
|
||||||
@@ -83,51 +83,51 @@ export class InvitesOrm {
|
|||||||
throw new InternalServerError();
|
throw new InternalServerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql`UPDATE user_invites SET was_email_sent = true WHERE id=${newInviteId}`;
|
await sql`UPDATE user_invites SET was_email_sent = true WHERE id=${newRecordId}`;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async accept({ inviteCode, password }: { inviteCode: string; password: string }): Promise<User> {
|
async accept({ inviteCode, password }: { inviteCode: string; password: string }): Promise<User> {
|
||||||
const invite: {
|
const record: {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
player_id: string;
|
player_id: string;
|
||||||
accepted: boolean;
|
accepted: boolean;
|
||||||
} = first(await sql`SELECT * FROM user_invites WHERE invite_code=${inviteCode} LIMIT 1`);
|
} = first(await sql`SELECT * FROM user_invites WHERE invite_code=${inviteCode} LIMIT 1`);
|
||||||
|
|
||||||
if (!invite) {
|
if (!record) {
|
||||||
throw new NotFoundError('Invalid invite code');
|
throw new NotFoundError('Invalid invite code');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invite.accepted) {
|
if (record.accepted) {
|
||||||
throw new UnauthorizedError('Invite already accepted');
|
throw new UnauthorizedError('Invite already accepted');
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerHasUser = (
|
const isPlayerAssociatedWithUser = (
|
||||||
first(
|
first(
|
||||||
await sql`SELECT COUNT(*) > 0 AS exists
|
await sql`SELECT COUNT(*) > 0 AS exists
|
||||||
FROM users
|
FROM users
|
||||||
WHERE player_id = ${invite.player_id}
|
WHERE player_id = ${record.player_id}
|
||||||
OR email = ${invite.email}`,
|
OR email = ${record.email}`,
|
||||||
) as {
|
) as {
|
||||||
exists: boolean;
|
exists: boolean;
|
||||||
}
|
}
|
||||||
)?.exists;
|
)?.exists;
|
||||||
if (playerHasUser) {
|
if (isPlayerAssociatedWithUser) {
|
||||||
throw new BadRequestError('User has already been invited.');
|
throw new BadRequestError('User has already been invited.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdUser = await orm.users.create({
|
const user = await orm.users.create({
|
||||||
email: invite.email,
|
email: record.email,
|
||||||
playerId: PlayerId.fromID(invite.player_id),
|
playerId: PlayerId.fromID(record.player_id),
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sql`UPDATE user_invites
|
await sql`UPDATE user_invites
|
||||||
SET accepted = true
|
SET accepted = true
|
||||||
WHERE id = ${invite.id}`;
|
WHERE id = ${record.id}`;
|
||||||
|
|
||||||
return createdUser;
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
src/orm/matches.ts
Normal file
205
src/orm/matches.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { Claims } from './claims';
|
||||||
|
import { sql } from 'bun';
|
||||||
|
import { first, orderBy } from 'lodash';
|
||||||
|
import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { GameId, MatchId, PlayerId, UserId } from '../utilities/secureIds';
|
||||||
|
import { calculateElos } from '../utilities/elo';
|
||||||
|
import { orm } from './orm';
|
||||||
|
|
||||||
|
export class MatchParticipant {
|
||||||
|
matchId?: MatchId;
|
||||||
|
playerId: PlayerId;
|
||||||
|
gamesPlayed?: number;
|
||||||
|
standing: number;
|
||||||
|
elo: number;
|
||||||
|
eloChange: number;
|
||||||
|
isRatingLocked?: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
playerId: PlayerId,
|
||||||
|
standing: number,
|
||||||
|
eloChange: number = 0,
|
||||||
|
gamesPlayed?: number,
|
||||||
|
elo: number = 1000,
|
||||||
|
matchId?: MatchId,
|
||||||
|
) {
|
||||||
|
this.matchId = matchId;
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.standing = standing;
|
||||||
|
this.elo = elo;
|
||||||
|
this.gamesPlayed = gamesPlayed;
|
||||||
|
this.eloChange = eloChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Match {
|
||||||
|
id: MatchId;
|
||||||
|
gameId: GameId;
|
||||||
|
players: MatchParticipant[];
|
||||||
|
owner: UserId;
|
||||||
|
|
||||||
|
constructor(id: MatchId, gameId: GameId, players: MatchParticipant[], owner: UserId) {
|
||||||
|
this.id = id;
|
||||||
|
this.gameId = gameId;
|
||||||
|
this.players = players;
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatchOrm {
|
||||||
|
async create({
|
||||||
|
gameId,
|
||||||
|
participants,
|
||||||
|
ownerId,
|
||||||
|
}: {
|
||||||
|
gameId: GameId;
|
||||||
|
participants: MatchParticipant[];
|
||||||
|
ownerId: UserId;
|
||||||
|
}): Promise<Match> {
|
||||||
|
await sql`INSERT INTO matches (game_id, owning_user_id) VALUES (${gameId.raw}, ${ownerId.raw})`;
|
||||||
|
const newMatchId = MatchId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
|
||||||
|
|
||||||
|
const players = await sql`
|
||||||
|
SELECT p.id,
|
||||||
|
p.is_rating_locked,
|
||||||
|
(CASE WHEN p.is_rating_locked THEN 1000 ELSE 1000 + COALESCE(sum(mp.elo_change), 0) END) as elo,
|
||||||
|
(CASE WHEN p.is_rating_locked THEN 0 ELSE count(mp.*) END) as games_played
|
||||||
|
FROM players p
|
||||||
|
LEFT JOIN match_players mp ON mp.player_id = p.id
|
||||||
|
WHERE p.id IN ${sql(participants.map((x) => x.playerId.raw))}
|
||||||
|
GROUP BY p.id;`;
|
||||||
|
|
||||||
|
for (let i in participants) {
|
||||||
|
const player = players.find((x: any) => x.id === participants[i].playerId.raw);
|
||||||
|
participants[i].elo = parseInt(player.elo);
|
||||||
|
participants[i].gamesPlayed = parseInt(player.games_played);
|
||||||
|
participants[i].isRatingLocked = player.is_rating_locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amendedPlayers = calculateElos(participants);
|
||||||
|
|
||||||
|
await sql.transaction(async (tx) => {
|
||||||
|
for (let i in amendedPlayers) {
|
||||||
|
await tx`
|
||||||
|
INSERT INTO match_players(match_id, player_id, standing, elo_change)
|
||||||
|
VALUES (${newMatchId.raw},
|
||||||
|
${amendedPlayers[i].playerId.raw},
|
||||||
|
${amendedPlayers[i].standing},
|
||||||
|
${amendedPlayers[i].isRatingLocked ? 0 : amendedPlayers[i].eloChange})`;
|
||||||
|
|
||||||
|
if (amendedPlayers[i].isRatingLocked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx`UPDATE players SET elo=${amendedPlayers[i].elo} WHERE id=${amendedPlayers[i].playerId.raw}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.get(newMatchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: MatchId, claims?: Claims): Promise<Match> {
|
||||||
|
const dbResult = await sql`
|
||||||
|
SELECT m.id as match_id,
|
||||||
|
m.owning_user_id as owner_id,
|
||||||
|
g.id as game_id,
|
||||||
|
g.name as game_name,
|
||||||
|
p.id as player_id,
|
||||||
|
p.name as player_name,
|
||||||
|
p.elo as elo,
|
||||||
|
mp.standing as standing,
|
||||||
|
mp.elo_change as elo_change
|
||||||
|
FROM matches m
|
||||||
|
LEFT JOIN games g ON g.id = m.game_id
|
||||||
|
LEFT JOIN match_players mp ON mp.match_id = m.id
|
||||||
|
LEFT JOIN players p ON p.id = mp.player_id
|
||||||
|
WHERE m.id = ${id.raw}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
claims &&
|
||||||
|
!(
|
||||||
|
claims.test(Claims.ADMIN) ||
|
||||||
|
(claims.test(Claims.MATCHES.OWNED.READ) && dbResult?.[0]?.owner_id === claims?.userId?.raw) ||
|
||||||
|
(claims.test(Claims.MATCHES.PARTICIPANT.READ) &&
|
||||||
|
dbResult?.some((x: any) => x.player_id === claims?.userId?.raw))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchData = dbResult?.find((x: any) => x.match_id);
|
||||||
|
if (!matchData?.match_id) {
|
||||||
|
throw new NotFoundError('No matching match exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Match(
|
||||||
|
MatchId.fromID(matchData?.match_id),
|
||||||
|
GameId.fromID(matchData?.game_id),
|
||||||
|
orderBy(
|
||||||
|
dbResult
|
||||||
|
.filter((x: any) => x.player_id)
|
||||||
|
.map(
|
||||||
|
(x: any) =>
|
||||||
|
new MatchParticipant(
|
||||||
|
PlayerId.fromID(x.player_id),
|
||||||
|
parseInt(x.standing),
|
||||||
|
parseInt(x.elo_change),
|
||||||
|
undefined,
|
||||||
|
parseInt(x.elo),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'standing',
|
||||||
|
'asc',
|
||||||
|
),
|
||||||
|
UserId.fromID(dbResult?.[0]?.owner_id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async drop(id: MatchId, claims?: Claims): Promise<void> {
|
||||||
|
const match = await this.get(id);
|
||||||
|
if (
|
||||||
|
claims &&
|
||||||
|
!(claims.test(Claims.ADMIN) || (claims.test(Claims.MATCHES.OWNED.DELETE) && match.owner === claims?.userId))
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql.transaction(async (tx) => {
|
||||||
|
await tx`DELETE
|
||||||
|
FROM match_players
|
||||||
|
WHERE match_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM matches
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePlayer(matchId: MatchId, participantId: UserId | PlayerId): Promise<void> {
|
||||||
|
let playerId: PlayerId;
|
||||||
|
if (participantId instanceof PlayerId) {
|
||||||
|
playerId = participantId;
|
||||||
|
} else {
|
||||||
|
playerId = (await orm.users.get(participantId))?.playerId;
|
||||||
|
if (!playerId) {
|
||||||
|
throw new BadRequestError('User is not a participant');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = await orm.players.get(playerId);
|
||||||
|
|
||||||
|
await sql.transaction(async (tx) => {
|
||||||
|
const eloRefund = parseInt(
|
||||||
|
(
|
||||||
|
await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}`
|
||||||
|
)?.[0]?.elo_change ?? 0,
|
||||||
|
);
|
||||||
|
await tx`DELETE FROM match_players WHERE match_id=${matchId.raw} AND player_id=${playerId.raw}`;
|
||||||
|
if (!player.isRatingLocked) {
|
||||||
|
await tx`UPDATE players SET elo=${player.elo - eloRefund} WHERE id=${playerId.raw}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { PlayersOrm } from './players';
|
|||||||
import { GamesOrm } from './games';
|
import { GamesOrm } from './games';
|
||||||
import { InvitesOrm } from './invites';
|
import { InvitesOrm } from './invites';
|
||||||
import { CollectionsOrm } from './collections';
|
import { CollectionsOrm } from './collections';
|
||||||
|
import { MatchOrm } from './matches';
|
||||||
|
import { CircleOrm } from './circles';
|
||||||
|
|
||||||
class Orm {
|
class Orm {
|
||||||
readonly claims: ClaimsOrm = new ClaimsOrm();
|
readonly claims: ClaimsOrm = new ClaimsOrm();
|
||||||
@@ -12,6 +14,8 @@ class Orm {
|
|||||||
readonly games: GamesOrm = new GamesOrm();
|
readonly games: GamesOrm = new GamesOrm();
|
||||||
readonly invites: InvitesOrm = new InvitesOrm();
|
readonly invites: InvitesOrm = new InvitesOrm();
|
||||||
readonly collections: CollectionsOrm = new CollectionsOrm();
|
readonly collections: CollectionsOrm = new CollectionsOrm();
|
||||||
|
readonly matches: MatchOrm = new MatchOrm();
|
||||||
|
readonly circles: CircleOrm = new CircleOrm();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const orm = new Orm();
|
export const orm = new Orm();
|
||||||
|
|||||||
@@ -32,42 +32,43 @@ export class PlayersOrm {
|
|||||||
async create(model: { name: string }): Promise<Player> {
|
async create(model: { name: string }): 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 newRecordId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
return await this.get(PlayerId.fromID(newPlayerId));
|
return await this.get(PlayerId.fromID(newRecordId));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 record: any = first(
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM players
|
FROM players
|
||||||
WHERE id = ${id.raw}
|
WHERE id = ${id.raw}
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dbResult) {
|
if (!record) {
|
||||||
throw new NotFoundError('No matching player exists');
|
throw new NotFoundError('No matching player exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Player({
|
return new Player({
|
||||||
id: PlayerId.fromID(dbResult.id),
|
id: PlayerId.fromID(record.id),
|
||||||
name: dbResult.name,
|
name: record.name,
|
||||||
elo: parseInt(dbResult.elo),
|
elo: parseInt(record.elo),
|
||||||
isRatingLocked: dbResult.is_rating_locked,
|
isRatingLocked: record.is_rating_locked,
|
||||||
canBeMultiple: dbResult.can_be_multiple,
|
canBeMultiple: record.can_be_multiple,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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,35 +110,35 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerToUpdate = await this.get(id);
|
const player = await this.get(id);
|
||||||
playerToUpdate.name = patch.name ?? playerToUpdate.name;
|
player.name = patch.name ?? player.name;
|
||||||
playerToUpdate.isRatingLocked = patch.isRatingLocked ?? playerToUpdate.isRatingLocked;
|
player.isRatingLocked = patch.isRatingLocked ?? player.isRatingLocked;
|
||||||
playerToUpdate.canBeMultiple = patch.canBeMultiple ?? playerToUpdate.canBeMultiple;
|
player.canBeMultiple = patch.canBeMultiple ?? player.canBeMultiple;
|
||||||
|
|
||||||
await sql`UPDATE players
|
await sql`UPDATE players
|
||||||
SET name=${playerToUpdate.name},
|
SET name=${player.name},
|
||||||
is_rating_locked=${playerToUpdate.isRatingLocked},
|
is_rating_locked=${player.isRatingLocked},
|
||||||
can_be_multiple=${playerToUpdate.canBeMultiple}
|
can_be_multiple=${player.canBeMultiple}
|
||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
return await this.get(id);
|
return await this.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
src/orm/user.ts
160
src/orm/user.ts
@@ -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
|
||||||
@@ -41,29 +47,29 @@ export class UsersOrm {
|
|||||||
const passwordHash = await argon2.hash(password);
|
const passwordHash = await argon2.hash(password);
|
||||||
await sql`INSERT INTO users (email, pass_hash, player_id)
|
await sql`INSERT INTO users (email, pass_hash, player_id)
|
||||||
VALUES (${email}, ${passwordHash}, ${playerId.raw})`;
|
VALUES (${email}, ${passwordHash}, ${playerId.raw})`;
|
||||||
const newUserId: UserId = UserId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
|
const newRecordId: string = (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) {
|
||||||
await tx`INSERT INTO user_claims (user_id, claim_id)
|
await tx`INSERT INTO user_claims (user_id, claim_id)
|
||||||
VALUES (${newUserId.raw}, ${defaultClaims[i]})`;
|
VALUES (${newRecordId}, ${defaultClaims[i]})`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.get(newUserId);
|
return await this.get(UserId.fromID(newRecordId));
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbResult: any = first(
|
const record: any = first(
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ${id.raw}
|
WHERE id = ${id.raw}
|
||||||
@@ -71,49 +77,111 @@ export class UsersOrm {
|
|||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dbResult) {
|
if (!record) {
|
||||||
throw new NotFoundError('No matching user exists');
|
throw new NotFoundError('No matching user exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new User(
|
return new User(
|
||||||
UserId.fromID(dbResult.id),
|
UserId.fromID(record.id),
|
||||||
PlayerId.fromID(dbResult.player_id),
|
PlayerId.fromID(record.player_id),
|
||||||
dbResult.email,
|
record.email,
|
||||||
dbResult.is_admin,
|
record.is_admin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: UserId, patch: UpdateUserRequest, claims?: Claims): Promise<User> {
|
async getByPlayer(id: PlayerId, claims?: Claims): Promise<User> {
|
||||||
|
const record: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE player_id = ${id.raw}
|
||||||
|
AND is_active = true
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new NotFoundError('No matching user exists');
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
claims &&
|
||||||
!(
|
!(
|
||||||
Claims.test(Claims.ADMIN, claims) ||
|
claims.test(Claims.ADMIN, Claims.USERS.OTHER.READ) ||
|
||||||
Claims.test(Claims.USERS.OTHER.UPDATE, claims) ||
|
(claims.test(Claims.USERS.SELF.READ) && record.id === claims?.userId.raw)
|
||||||
(Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId)
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const userToUpdate = await this.get(id);
|
return new User(
|
||||||
if (Claims.test(Claims.ADMIN, claims)) {
|
UserId.fromID(record.id),
|
||||||
userToUpdate.isActive = patch.isActive ?? userToUpdate.isActive;
|
PlayerId.fromID(record.player_id),
|
||||||
userToUpdate.isAdmin = patch.isAdmin ?? userToUpdate.isAdmin;
|
record.email,
|
||||||
|
record.is_admin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByEmail(email: string, claims?: Claims): Promise<User> {
|
||||||
|
const record: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE email = ${email}
|
||||||
|
AND is_active = true
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new NotFoundError('No matching user exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
claims &&
|
||||||
|
!(
|
||||||
|
claims.test(Claims.ADMIN, Claims.USERS.OTHER.READ) ||
|
||||||
|
(claims.test(Claims.USERS.SELF.READ) && record.id === claims?.userId.raw)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new User(
|
||||||
|
UserId.fromID(record.id),
|
||||||
|
PlayerId.fromID(record.player_id),
|
||||||
|
record.email,
|
||||||
|
record.is_admin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: UserId, patch: UpdateUserRequest, claims?: Claims): Promise<User> {
|
||||||
|
if (
|
||||||
|
claims &&
|
||||||
|
!(
|
||||||
|
claims.test(Claims.ADMIN, Claims.USERS.OTHER.UPDATE) ||
|
||||||
|
(claims.test(Claims.USERS.SELF.UPDATE) && id === claims?.userId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.get(id);
|
||||||
|
if (!claims || claims.test(Claims.ADMIN)) {
|
||||||
|
user.isActive = patch.isActive ?? user.isActive;
|
||||||
|
user.isAdmin = patch.isAdmin ?? user.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql`UPDATE users
|
await sql`UPDATE users
|
||||||
SET is_active=${userToUpdate.isActive},
|
SET is_active=${user.isActive},
|
||||||
is_admin=${userToUpdate.isAdmin}
|
is_admin=${user.isAdmin}
|
||||||
WHERE id = ${id.raw}`;
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
return await this.get(id);
|
return await this.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,49 +201,41 @@ export class UsersOrm {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyCredentials(
|
async verifyCredentials(email: string, password: string): Promise<{ userId: UserId; refreshCount: string } | null> {
|
||||||
email: string,
|
const record: any = first(
|
||||||
password: string,
|
|
||||||
): Promise<{ userId: UserId; refreshCount: string } | null> {
|
|
||||||
const dbResult: any = first(
|
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE email = ${email}
|
WHERE email = ${email}
|
||||||
AND is_active = true
|
AND is_active = true
|
||||||
limit 1`,
|
limit 1`,
|
||||||
);
|
);
|
||||||
if (!dbResult) {
|
if (!record) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await argon2.verify(dbResult.pass_hash, password))) {
|
if (!(await argon2.verify(record.pass_hash, password))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: UserId.fromID(dbResult.id),
|
userId: UserId.fromID(record.id),
|
||||||
refreshCount: dbResult.refresh_count,
|
refreshCount: record.refresh_count,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyRefreshCount(id: UserId, refreshCount: string): Promise<boolean> {
|
async verifyRefreshCount(id: UserId, refreshCount: string): Promise<boolean> {
|
||||||
const dbResult: any = first(
|
const record: any = first(
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ${id.raw}
|
WHERE id = ${id.raw}
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
return dbResult.refresh_count === refreshCount;
|
return record.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,14 +243,14 @@ export class UsersOrm {
|
|||||||
throw new BadRequestError('Password is required');
|
throw new BadRequestError('Password is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbUser: any = first(
|
const record: any = first(
|
||||||
await sql`SELECT *
|
await sql`SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ${id.raw}
|
WHERE id = ${id.raw}
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isAdmin && !(await argon2.verify(dbUser.pass_hash, oldPassword as string))) {
|
if (!isAdmin && !(await argon2.verify(record.pass_hash, oldPassword as string))) {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { guard, unwrapMethod } from '../utilities/guard';
|
import { guard, unwrapMethod } from '../utilities/guard';
|
||||||
import auth from '../endpoints/auth';
|
import auth from '../endpoints/auth';
|
||||||
import { OkResponse } from '../utilities/responseHelper';
|
|
||||||
import { Claims } from '../orm/claims';
|
import { Claims } from '../orm/claims';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/api/auth/login': {
|
login: {
|
||||||
POST: unwrapMethod(auth.login),
|
POST: unwrapMethod(auth.login),
|
||||||
},
|
},
|
||||||
'/api/auth/token': {
|
token: {
|
||||||
GET: unwrapMethod(auth.token),
|
GET: unwrapMethod(auth.token),
|
||||||
},
|
},
|
||||||
'/api/auth/logout': {
|
logout: {
|
||||||
POST: unwrapMethod(auth.logout),
|
POST: unwrapMethod(auth.logout),
|
||||||
},
|
},
|
||||||
'/api/auth/changePassword/:id': {
|
changePassword: {
|
||||||
PATCH: guard(auth.changePassword, [Claims.ADMIN, Claims.USERS.SELF.UPDATE]),
|
':id': {
|
||||||
},
|
PATCH: guard(auth.changePassword, Claims.ADMIN, Claims.USERS.SELF.UPDATE),
|
||||||
'/api/auth/test': {
|
},
|
||||||
GET: () => new OkResponse(),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
32
src/routes/circles.ts
Normal file
32
src/routes/circles.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { guard } from '../utilities/guard';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
import circles from '../endpoints/circles';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'POST': guard(circles.create, Claims.ADMIN, Claims.CIRCLES.PUBLIC.CREATE, Claims.CIRCLES.PRIVATE.CREATE),
|
||||||
|
':id': {
|
||||||
|
GET: guard(
|
||||||
|
circles.get,
|
||||||
|
Claims.ADMIN,
|
||||||
|
Claims.CIRCLES.PUBLIC.READ,
|
||||||
|
Claims.CIRCLES.PRIVATE.READ,
|
||||||
|
Claims.CIRCLES.PRIVATE.READ_IF_MEMBER,
|
||||||
|
),
|
||||||
|
PATCH: guard(circles.update, Claims.ADMIN, Claims.CIRCLES.OWNED.UPDATE),
|
||||||
|
DELETE: guard(circles.drop, Claims.ADMIN, Claims.CIRCLES.OWNED.DELETE),
|
||||||
|
invite: {
|
||||||
|
POST: guard(
|
||||||
|
circles.invite,
|
||||||
|
Claims.ADMIN,
|
||||||
|
Claims.CIRCLES.PUBLIC.USERS.INVITE,
|
||||||
|
Claims.CIRCLES.OWNED.USERS.INVITE,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'search': {
|
||||||
|
':query': {
|
||||||
|
variants: [':pageSize/:page', ':page'],
|
||||||
|
GET: guard(circles.query, Claims.ADMIN, Claims.CIRCLES.PUBLIC.READ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,29 +3,20 @@ import { Claims } from '../orm/claims';
|
|||||||
import collections from '../endpoints/collections';
|
import collections from '../endpoints/collections';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/api/collection': {
|
'POST': guard(collections.create, Claims.ADMIN, Claims.COLLECTIONS.CREATE),
|
||||||
POST: guard(collections.create, [Claims.ADMIN, Claims.COLLECTIONS.CREATE]),
|
':id': {
|
||||||
|
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),
|
||||||
|
add: {
|
||||||
|
POST: guard(collections.addGame, Claims.ADMIN, Claims.COLLECTIONS.OWNED.GAME.ADD),
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
POST: guard(collections.removeGame, Claims.ADMIN, Claims.COLLECTIONS.OWNED.GAME.REMOVE),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'/api/collection/:id': {
|
'list': {
|
||||||
GET: guard(collections.get, [Claims.ADMIN, Claims.COLLECTIONS.UNOWNED.READ, Claims.COLLECTIONS.OWNED.READ]),
|
variants: [':pageSize/:page', ':page'],
|
||||||
// PATCH: guard(collections.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]),
|
GET: guard(collections.list, Claims.ADMIN, Claims.COLLECTIONS.OWNED.LIST),
|
||||||
// DELETE: guard(collections.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
|
|
||||||
},
|
|
||||||
'/api/collection/:id/add': {
|
|
||||||
POST: guard(collections.addGame, [
|
|
||||||
Claims.ADMIN,
|
|
||||||
Claims.COLLECTIONS.UNOWNED.GAME.ADD,
|
|
||||||
Claims.COLLECTIONS.OWNED.GAME.ADD,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
'/api/collection/:id/remove': {
|
|
||||||
POST: guard(collections.removeGame, [
|
|
||||||
Claims.ADMIN,
|
|
||||||
Claims.COLLECTIONS.UNOWNED.GAME.REMOVE,
|
|
||||||
Claims.COLLECTIONS.OWNED.GAME.REMOVE,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
'/api/collection/list/:pageSize/:page': {
|
|
||||||
GET: guard(collections.list, [Claims.ADMIN, Claims.COLLECTIONS.OWNED.LIST]),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import { Claims } from '../orm/claims';
|
|||||||
import games from '../endpoints/games';
|
import games from '../endpoints/games';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/api/game': {
|
'POST': guard(games.create, Claims.ADMIN, Claims.GAMES.CREATE),
|
||||||
POST: guard(games.create, [Claims.ADMIN, Claims.GAMES.CREATE]),
|
':id': {
|
||||||
|
GET: guard(games.get, Claims.ADMIN, Claims.GAMES.READ),
|
||||||
|
PATCH: guard(games.update, Claims.ADMIN, Claims.GAMES.UPDATE),
|
||||||
|
DELETE: guard(games.drop, Claims.ADMIN, Claims.GAMES.DELETE),
|
||||||
},
|
},
|
||||||
'/api/game/:id': {
|
'search': {
|
||||||
GET: guard(games.get, [Claims.ADMIN, Claims.GAMES.READ]),
|
':query': {
|
||||||
PATCH: guard(games.update, [Claims.ADMIN, Claims.GAMES.UPDATE]),
|
variants: [':pageSize/:page', ':page'],
|
||||||
DELETE: guard(games.drop, [Claims.ADMIN, Claims.GAMES.DELETE]),
|
GET: guard(games.query, Claims.ADMIN, Claims.GAMES.READ),
|
||||||
},
|
},
|
||||||
'/api/game/search/:query/:pageSize/:page': {
|
|
||||||
GET: guard(games.query, [Claims.ADMIN, Claims.GAMES.READ]),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import { Claims } from '../orm/claims';
|
|||||||
import invite from '../endpoints/invites';
|
import invite from '../endpoints/invites';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/api/invite': {
|
POST: guard(invite.create, Claims.ADMIN, Claims.USERS.INVITE),
|
||||||
POST: guard(invite.create, [Claims.ADMIN, Claims.USERS.INVITE]),
|
accept: {
|
||||||
},
|
|
||||||
'/api/invite/accept': {
|
|
||||||
POST: unwrapMethod(invite.accept),
|
POST: unwrapMethod(invite.accept),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
14
src/routes/matches.ts
Normal file
14
src/routes/matches.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { guard } from '../utilities/guard';
|
||||||
|
import matches from '../endpoints/matches';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'POST': guard(matches.create, Claims.ADMIN, Claims.MATCHES.CREATE),
|
||||||
|
':id': {
|
||||||
|
GET: guard(matches.get, Claims.ADMIN, Claims.MATCHES.OWNED.READ, Claims.MATCHES.PARTICIPANT.READ),
|
||||||
|
DELETE: guard(matches.drop, Claims.ADMIN, Claims.MATCHES.OWNED.DELETE, Claims.USERS.SELF.UPDATE),
|
||||||
|
leave: {
|
||||||
|
POST: guard(matches.leave, Claims.MATCHES.PARTICIPANT.LEAVE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,15 +3,14 @@ import { Claims } from '../orm/claims';
|
|||||||
import player from '../endpoints/players';
|
import player from '../endpoints/players';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/api/player': {
|
'POST': guard(player.create, Claims.ADMIN, Claims.PLAYERS.CREATE),
|
||||||
POST: guard(player.create, [Claims.ADMIN, Claims.PLAYERS.CREATE]),
|
':id': {
|
||||||
|
GET: guard(player.get, Claims.ADMIN, Claims.PLAYERS.OTHER.READ, Claims.PLAYERS.SELF.READ),
|
||||||
|
PATCH: guard(player.update, Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE),
|
||||||
|
DELETE: guard(player.drop, Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE),
|
||||||
},
|
},
|
||||||
'/api/player/:id': {
|
'list': {
|
||||||
GET: guard(player.get, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ, Claims.PLAYERS.SELF.READ]),
|
variants: [':pageSize/:page', ':page'],
|
||||||
PATCH: guard(player.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]),
|
GET: guard(player.list, Claims.ADMIN, Claims.PLAYERS.OTHER.READ),
|
||||||
DELETE: guard(player.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
|
|
||||||
},
|
|
||||||
'/api/player/list/:pageSize/:page': {
|
|
||||||
GET: guard(player.list, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ]),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import user from '../endpoints/users';
|
|||||||
import { Claims } from '../orm/claims';
|
import { Claims } from '../orm/claims';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/api/user': {
|
'POST': guard(user.create, Claims.ADMIN, Claims.USERS.CREATE),
|
||||||
POST: guard(user.create, [Claims.ADMIN, Claims.USERS.CREATE]),
|
':id': {
|
||||||
},
|
GET: guard(user.get, Claims.ADMIN, Claims.USERS.OTHER.READ, Claims.USERS.SELF.READ),
|
||||||
'/api/user/:id': {
|
PATCH: guard(user.update, Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE),
|
||||||
GET: guard(user.get, [Claims.ADMIN, Claims.USERS.OTHER.READ, Claims.USERS.SELF.READ]),
|
DELETE: guard(user.drop, Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE),
|
||||||
PATCH: guard(user.update, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]),
|
|
||||||
DELETE: guard(user.drop, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,49 +29,41 @@ 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_PUBLIC_USER_INVITE',
|
||||||
|
LIST: 'CIRCLES_PUBLIC_USER_LIST',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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: {
|
},
|
||||||
INVITE: 'CIRCLES_OWNED_USER_INVITE',
|
USERS: {
|
||||||
},
|
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',
|
||||||
@@ -82,24 +74,24 @@ export class ClaimDefinition {
|
|||||||
};
|
};
|
||||||
public static readonly MATCHES = {
|
public static readonly MATCHES = {
|
||||||
CREATE: 'MATCHES_CREATE',
|
CREATE: 'MATCHES_CREATE',
|
||||||
|
COMMENTS: {
|
||||||
|
ADD: 'MATCHES_UNOWNED_COMMENTS_ADD',
|
||||||
|
},
|
||||||
OWNED: {
|
OWNED: {
|
||||||
READ: 'MATCHES_OWNED_READ',
|
READ: 'MATCHES_OWNED_READ',
|
||||||
UPDATE: 'MATCHES_OWNED_UPDATE',
|
|
||||||
DELETE: 'MATCHES_OWNED_DELETE',
|
DELETE: 'MATCHES_OWNED_DELETE',
|
||||||
COMMENTS: {
|
COMMENTS: {
|
||||||
ADD: 'MATCHES_OWNED_COMMENTS_ADD',
|
ADD: 'MATCHES_OWNED_COMMENTS_ADD',
|
||||||
DELETE: 'MATCHES_OWNED_COMMENTS_DELETE',
|
DELETE: 'MATCHES_OWNED_COMMENTS_DELETE',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UNOWNED: {
|
PARTICIPANT: {
|
||||||
READ: 'MATCHES_UNOWNED_READ',
|
READ: 'MATCHES_PARTICIPANT_READ',
|
||||||
UPDATE: 'MATCHES_UNOWNED_UPDATE',
|
LEAVE: 'MATCHES_LEAVE',
|
||||||
DELETE: 'MATCHES_UNOWNED_DELETE',
|
|
||||||
COMMENTS: {
|
COMMENTS: {
|
||||||
ADD: 'MATCHES_UNOWNED_COMMENTS_ADD',
|
ADD: 'MATCHES_PARTICIPANT_COMMENTS_ADD',
|
||||||
DELETE: 'MATCHES_UNOWNED_COMMENTS_DELETE',
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
public static readonly COLLECTIONS = {
|
public static readonly COLLECTIONS = {
|
||||||
CREATE: 'COLLECTIONS_CREATE',
|
CREATE: 'COLLECTIONS_CREATE',
|
||||||
@@ -120,10 +112,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 = {
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
import { orderBy } from 'lodash';
|
import { orderBy } from 'lodash';
|
||||||
|
import { MatchParticipant } from '../orm/matches';
|
||||||
|
|
||||||
interface GamePlayer {
|
export function calculateElos(players: MatchParticipant[], provisionalPeriod: number = 1): MatchParticipant[] {
|
||||||
id: string;
|
const orderedResults = orderBy(players, (x:any) => parseInt(x.standing ?? 0), 'asc');
|
||||||
elo: number;
|
|
||||||
gamesPlayed: number;
|
|
||||||
standing: number;
|
|
||||||
eloChange?: number;
|
|
||||||
}
|
|
||||||
export interface GamePlayed {
|
|
||||||
players: GamePlayer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateElos(game: GamePlayed, provisionalPeriod: number = 1): GamePlayed {
|
|
||||||
const orderedResults = orderBy(game.players, 'standing', 'asc');
|
|
||||||
for (let i = 0; i < orderedResults.length - 1; i++) {
|
for (let i = 0; i < orderedResults.length - 1; i++) {
|
||||||
for (let j = i + 1; j < orderedResults.length; j++) {
|
for (let j = i + 1; j < orderedResults.length; j++) {
|
||||||
const challengerResults = calculateEloChange(orderedResults[i].elo, orderedResults[j].elo);
|
const challengerResults = calculateEloChange(
|
||||||
|
orderedResults[i].elo,
|
||||||
|
orderedResults[j].elo,
|
||||||
|
orderedResults[i].standing === orderedResults[j].standing,
|
||||||
|
);
|
||||||
orderedResults[i].eloChange =
|
orderedResults[i].eloChange =
|
||||||
(orderedResults[i].eloChange ?? 0) +
|
(orderedResults[i].eloChange ?? 0) +
|
||||||
challengerResults.winnerChange * Math.min(1, orderedResults[j].gamesPlayed / provisionalPeriod);
|
challengerResults.winnerChange *
|
||||||
|
Math.min(1, ((orderedResults[j].gamesPlayed as number) + 1) / provisionalPeriod);
|
||||||
orderedResults[j].eloChange =
|
orderedResults[j].eloChange =
|
||||||
(orderedResults[j].eloChange ?? 0) +
|
(orderedResults[j].eloChange ?? 0) +
|
||||||
challengerResults.loserChange * Math.min(1, orderedResults[i].gamesPlayed / provisionalPeriod);
|
challengerResults.loserChange *
|
||||||
|
Math.min(1, ((orderedResults[i].gamesPlayed as number) + 1) / provisionalPeriod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return orderedResults;
|
||||||
players: orderedResults,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EloResult {
|
interface EloResult {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { Claims } from '../orm/claims';
|
|||||||
export function guardRedirect(
|
export function guardRedirect(
|
||||||
method: (request: UnwrappedRequest<any>) => Promise<Response> | Response,
|
method: (request: UnwrappedRequest<any>) => Promise<Response> | Response,
|
||||||
redirectMethod: Function,
|
redirectMethod: Function,
|
||||||
guardedClaims: string[],
|
...guardedClaims: string[]
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return guard(method, guardedClaims);
|
return guard(method, ...guardedClaims);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return redirectMethod();
|
return redirectMethod();
|
||||||
}
|
}
|
||||||
@@ -17,19 +17,21 @@ export function guardRedirect(
|
|||||||
|
|
||||||
export function guard(
|
export function guard(
|
||||||
method: (request: UnwrappedRequest<any>) => Promise<Response> | Response,
|
method: (request: UnwrappedRequest<any>) => Promise<Response> | Response,
|
||||||
guardedClaims: string[],
|
...guardedClaims: string[]
|
||||||
): (r: Request) => Promise<Response> {
|
): (r: Request) => Promise<Response> {
|
||||||
return async (request: Request): Promise<Response> => {
|
return async (request: Request): Promise<Response> => {
|
||||||
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 +68,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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,3 +50,23 @@ export interface UpdateCollectionRequest {
|
|||||||
export interface GameToCollectionRequest {
|
export interface GameToCollectionRequest {
|
||||||
gameId: string;
|
gameId: string;
|
||||||
}
|
}
|
||||||
|
export interface CreateMatchRequest {
|
||||||
|
gameId: string;
|
||||||
|
participants: { playerId: string; standing: number }[];
|
||||||
|
}
|
||||||
|
export interface CreateCircleRequest {
|
||||||
|
name: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
imagePath?: string;
|
||||||
|
colour: string;
|
||||||
|
}
|
||||||
|
export interface UpdateCircleRequest {
|
||||||
|
name: string;
|
||||||
|
imagePath?: string;
|
||||||
|
colour: string;
|
||||||
|
}
|
||||||
|
export interface InviteToCircleRequest {
|
||||||
|
email?:string;
|
||||||
|
userId?:string;
|
||||||
|
playerId?:string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestError, NotFoundError, UnauthorizedError } from './errors';
|
import { BadRequestError, NotFoundError, UnauthorizedError } from './errors';
|
||||||
import { clamp, isArray } from 'lodash';
|
import { clamp, isArray, isObject } from 'lodash';
|
||||||
import { UnwrappedRequest } from './guard';
|
import { UnwrappedRequest } from './guard';
|
||||||
|
|
||||||
export class ErrorResponse extends Response {
|
export class ErrorResponse extends Response {
|
||||||
@@ -52,11 +52,11 @@ export class OkResponse extends Response {
|
|||||||
constructor(body?: any) {
|
constructor(body?: any) {
|
||||||
if (body) {
|
if (body) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
isArray(body)
|
isObject(body) && !isArray(body)
|
||||||
? body
|
? {
|
||||||
: {
|
|
||||||
...body,
|
...body,
|
||||||
},
|
}
|
||||||
|
: body,
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
42
src/utilities/routeBuilder.ts
Normal file
42
src/utilities/routeBuilder.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export function buildRoute(
|
||||||
|
route: any,
|
||||||
|
currentPath: string = '',
|
||||||
|
): {
|
||||||
|
[x: string]: {
|
||||||
|
POST?: Function;
|
||||||
|
GET?: Function;
|
||||||
|
PUT?: Function;
|
||||||
|
DELETE?: Function;
|
||||||
|
};
|
||||||
|
} {
|
||||||
|
let returnValue: { [x: string]: any } = {};
|
||||||
|
|
||||||
|
const keys = Object.keys(route);
|
||||||
|
for (let i in keys) {
|
||||||
|
const key = keys[i];
|
||||||
|
if (key === 'POST' || key === 'GET' || key === 'PUT' || key === 'DELETE' || key === 'variants') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
returnValue = {
|
||||||
|
...returnValue,
|
||||||
|
...buildRoute(route[key], `${currentPath}/${key}`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.variants || route.POST || route.GET || route.PUT || route.DELETE) {
|
||||||
|
const variants: string[] = route.variants ?? [];
|
||||||
|
const endpointDefinition = {
|
||||||
|
POST: route.POST,
|
||||||
|
GET: route.GET,
|
||||||
|
PATCH: route.PATCH,
|
||||||
|
PUT: route.PUT,
|
||||||
|
DELETE: route.DELETE,
|
||||||
|
};
|
||||||
|
returnValue[currentPath] = endpointDefinition;
|
||||||
|
for (let key in variants) {
|
||||||
|
returnValue[`${currentPath}/${variants[key]}`] = endpointDefinition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -66,59 +66,104 @@ class SecureId {
|
|||||||
export class UserId extends SecureId {
|
export class UserId extends SecureId {
|
||||||
protected static override hashPrefix: string = 'UserId';
|
protected static override hashPrefix: string = 'UserId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodUser(){}
|
||||||
|
|
||||||
public static fromHash(hash: string): UserId {
|
public static fromHash(hash: string): UserId {
|
||||||
return super.fromHash(hash, UserId);
|
return super.fromHash(hash, UserId) as UserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromID(id: string): UserId {
|
public static fromID(id: string): UserId {
|
||||||
return super.fromID(id, UserId);
|
return super.fromID(id, UserId) as UserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlayerId extends SecureId {
|
export class PlayerId extends SecureId {
|
||||||
protected static override hashPrefix: string = 'PlayerId';
|
protected static override hashPrefix: string = 'PlayerId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodPlayer(){}
|
||||||
|
|
||||||
public static fromHash(hash: string): PlayerId {
|
public static fromHash(hash: string): PlayerId {
|
||||||
return super.fromHash(hash, PlayerId);
|
return super.fromHash(hash, PlayerId) as PlayerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromID(id: string): PlayerId {
|
public static fromID(id: string): PlayerId {
|
||||||
return super.fromID(id, PlayerId);
|
return super.fromID(id, PlayerId) as PlayerId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InviteId extends SecureId {
|
export class InviteId extends SecureId {
|
||||||
protected static override hashPrefix: string = 'InviteId';
|
protected static override hashPrefix: string = 'InviteId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodInvite(){}
|
||||||
|
|
||||||
public static fromHash(hash: string): InviteId {
|
public static fromHash(hash: string): InviteId {
|
||||||
return super.fromHash(hash, InviteId);
|
return super.fromHash(hash, InviteId) as InviteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromID(id: string): InviteId {
|
public static fromID(id: string): InviteId {
|
||||||
return super.fromID(id, InviteId);
|
return super.fromID(id, InviteId) as InviteId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GameId extends SecureId {
|
export class GameId extends SecureId {
|
||||||
protected static override hashPrefix: string = 'GameId';
|
protected static override hashPrefix: string = 'GameId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodGame(){}
|
||||||
|
|
||||||
public static fromHash(hash: string): GameId {
|
public static fromHash(hash: string): GameId {
|
||||||
return super.fromHash(hash, GameId);
|
return super.fromHash(hash, GameId) as GameId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromID(id: string): GameId {
|
public static fromID(id: string): GameId {
|
||||||
return super.fromID(id, GameId);
|
return super.fromID(id, GameId) as GameId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionId extends SecureId {
|
export class CollectionId extends SecureId {
|
||||||
protected static override hashPrefix: string = 'CollectionId';
|
protected static override hashPrefix: string = 'CollectionId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodCollection(){}
|
||||||
|
|
||||||
public static fromHash(hash: string): CollectionId {
|
public static fromHash(hash: string): CollectionId {
|
||||||
return super.fromHash(hash, CollectionId);
|
return super.fromHash(hash, CollectionId) as CollectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromID(id: string): CollectionId {
|
public static fromID(id: string): CollectionId {
|
||||||
return super.fromID(id, CollectionId);
|
return super.fromID(id, CollectionId) as CollectionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatchId extends SecureId {
|
||||||
|
protected static override hashPrefix: string = 'MatchId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodMatch(){}
|
||||||
|
|
||||||
|
public static fromHash(hash: string): MatchId {
|
||||||
|
return super.fromHash(hash, MatchId) as MatchId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromID(id: string): MatchId {
|
||||||
|
return super.fromID(id, MatchId) as MatchId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CircleId extends SecureId {
|
||||||
|
protected static override hashPrefix: string = 'CircleId';
|
||||||
|
|
||||||
|
// This method exists to force type errors when using an incorrect ID class.
|
||||||
|
#uniqueMethodCircle(){}
|
||||||
|
|
||||||
|
public static fromHash(hash: string): CircleId {
|
||||||
|
return super.fromHash(hash, CircleId) as CircleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromID(id: string): CircleId {
|
||||||
|
return super.fromID(id, CircleId) as CircleId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user