Skip to content

Commit 5246fd9

Browse files
committed
Extend login session on activity. fixes #244
1 parent fd03cd8 commit 5246fd9

File tree

16 files changed

+115
-94
lines changed

16 files changed

+115
-94
lines changed

api/api_wrappers.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
from datetime import datetime, timedelta
12
from functools import wraps
2-
33
from flask import current_app, jsonify, request
4+
import json
45
import jwt
56

67
from models.core_models import Player
@@ -34,6 +35,29 @@ def _verify(*args, **kwargs):
3435
player = Player.query.filter_by(user_id=data["sub"]).first()
3536
if not player:
3637
raise RuntimeError("User not found")
37-
return f(player, *args, **kwargs)
38+
39+
response = f(player, *args, **kwargs)
40+
41+
# Extend the expiration date of the JWT since the user is active
42+
# Newly generated token needs to be sent back to frontent
43+
if not isinstance(response, tuple):
44+
return response
45+
46+
extended_token = jwt.encode({
47+
"sub": data["sub"],
48+
"iat": data["iat"],
49+
"exp": datetime.utcnow() + timedelta(days=1)},
50+
current_app.config["SECRET_KEY"])
51+
try:
52+
response_content = json.loads(response[0])
53+
except json.JSONDecodeError:
54+
response_content = {"message": response[0]}
55+
56+
return (
57+
json.dumps({
58+
**response_content,
59+
"token": extended_token,
60+
}),
61+
response[1])
3862

3963
return _verify

global-scoreboard/src/Components/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import type { ServerConfigs } from 'src/Models/Configs'
1212
import Configs from 'src/Models/Configs'
1313
import type Player from 'src/Models/Player'
1414

15-
const getCurrentUser = () => apiGet('users/current').then<{ user: Player | undefined }>(response => response.json())
16-
const getConfigs = () => apiGet('configs').then<ServerConfigs>(response => response.json())
15+
const getCurrentUser = () => apiGet<{ user: Player | undefined }>('users/current')
16+
const getConfigs = () => apiGet<ServerConfigs>('configs')
1717

1818
const logout = (setCurrentUser: (user: null) => void) => {
1919
setCurrentUser(null)

global-scoreboard/src/Components/Dashboard/Dashboard.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ type DashboardProps = {
2020

2121
const MOBILE_SIZE = 767
2222

23-
const getFriends = () => apiGet('players/current/friends').then<Player[]>(response => response.json())
24-
const getAllPlayers = () => apiGet('players')
25-
.then<Player[]>(response => response.json())
23+
const getFriends = () => apiGet<Player[]>('players/current/friends')
24+
const getAllPlayers = () => apiGet<Player[]>('players')
2625
.then(players =>
2726
players.map(player => ({
2827
...player,
@@ -128,8 +127,7 @@ const Dashboard = (props: DashboardProps) => {
128127
return
129128
}
130129
setUpdateStartTime(Date.now())
131-
apiPost(`players/${runnerNameOrId}/update`)
132-
.then<UpdateRunnerResult>(response => response.json())
130+
apiPost<UpdateRunnerResult>(`players/${runnerNameOrId}/update`)
133131
.then(playerResult => {
134132
playerResult.lastUpdate = new Date(playerResult.lastUpdate)
135133
return playerResult

global-scoreboard/src/Components/Dashboard/TableElements/PlayerScoreCell.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ const PlayerScoreCell = (props: PlayerScoreCellProps) => {
1919
const handleClose = () => setShow(false)
2020
const handleShow = () =>
2121
props.player.scoreDetails === undefined
22-
? apiGet(`players/${props.player.userId}/score-details`)
23-
.then(response =>
24-
response.json().then((scoreDetails: RunResult[][]) => {
25-
props.player.scoreDetails = scoreDetails
26-
setShow(true)
27-
}))
22+
? apiGet<RunResult[][]>(`players/${props.player.userId}/score-details`)
23+
.then(scoreDetails => {
24+
props.player.scoreDetails = scoreDetails
25+
setShow(true)
26+
})
2827
: setShow(true)
2928

3029
// Date is last known update date for a user with details

global-scoreboard/src/Components/GameSearch/GameCategorySearchBar.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ const GameCategorySearchBar = (props: GameCategorySearchProps) => {
3838
(searchText: string) =>
3939
!searchText
4040
? props.onClear?.()
41-
: apiGet('https://www.speedrun.com/api/v1/games', { name: searchText, max: MAX_PAGINATION }, false)
42-
.then<DataArray<SpeedruncomGame>>(response => response.json())
41+
: apiGet<DataArray<SpeedruncomGame>>('https://www.speedrun.com/api/v1/games', { name: searchText, max: MAX_PAGINATION }, false)
4342
.then(response => response.data)
4443
.then<IdToNameMap>(games => Object.fromEntries(games.map(game => [game.id, game.names.international])))
4544
.then(games => {

global-scoreboard/src/Components/GameSearch/ScoreDropCalculator.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,30 @@ const filterSubCatVariables = (variables: SpeedruncomRun['data']['values'], subC
3131
}
3232

3333
const getRunDetails = (runId: string) =>
34-
apiGet(
34+
apiGet<SpeedruncomRun>(
3535
`https://www.speedrun.com/api/v1/runs/${runId}`,
3636
{},
3737
false
3838
)
39-
.then<SpeedruncomRun>(response => response.json())
4039
.then(response => response.data)
4140

4241
const getGameSubCategories = (gameId: string) =>
43-
apiGet(
42+
apiGet<DataArray<SpeedruncomVariable>>(
4443
`https://www.speedrun.com/api/v1/games/${gameId}/variables`,
4544
{},
4645
false
4746
)
48-
.then<DataArray<SpeedruncomVariable>>(response => response.json())
4947
.then(response => response
5048
.data
5149
.filter(variable => variable['is-subcategory'])
5250
.map(variable => variable.id))
5351

5452
const getLeaderboardRuns = (gameId: string, categoryId: string, subCategories: SpeedruncomRun['data']['values']) =>
55-
apiGet(
53+
apiGet<SpeedruncomLeaderboard>(
5654
`https://www.speedrun.com/api/v1/leaderboards/${gameId}/category/${categoryId}`,
5755
{ 'video-only': true, ...addVarToValuesKeys(subCategories) },
5856
false
5957
)
60-
.then<SpeedruncomLeaderboard>(response => response.json())
6158
.then(response => response.data.runs.map(run => run.run))
6259

6360
const ScoreDropCalculator = () => {

global-scoreboard/src/Components/NavBar/LoginModal.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,8 @@ const LoginModal = (props: LoginModalProps) => {
3030
}
3131
setLoading(true)
3232
setLoginErrorMessage('')
33-
apiPost('login', { speedruncomApiKey: speedruncomApiKeyInput })
34-
.then(response => response.json())
35-
.then((response: { token: string, user: Player }) => {
36-
if (!response.token) return
37-
localStorage.setItem('jwtToken', response.token)
38-
props.onLogin(response.user)
39-
})
33+
apiPost<{ user: Player }>('login', { speedruncomApiKey: speedruncomApiKeyInput })
34+
.then(response => props.onLogin(response.user))
4035
.catch((error: Response) => {
4136
if (error.status === StatusCodes.UNAUTHORIZED) {
4237
void error.json()

global-scoreboard/src/Models/GameSearch.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -29,31 +29,35 @@ export type PlatformSelectOption = {
2929
}
3030

3131
export const getAllGameValues = () =>
32-
apiGet('game-values')
33-
.then<GameValue[]>(response => response.json())
32+
apiGet<GameValue[]>('game-values')
3433
.then<GameValueRow[]>(gameValues => gameValues.map(gameValue => ({
3534
...gameValue,
3635
wrPointsPerSecond: gameValue.wrPoints / gameValue.wrTime,
3736
meanPointsPerSecond: gameValue.wrPoints / gameValue.meanTime,
3837
})))
3938

40-
export const getAllPlatforms = () => apiGet('https://www.speedrun.com/api/v1/platforms', { max: MAX_PAGINATION }, false)
41-
.then<{ data: PlatformDto[] }>(response => response.json())
42-
.then<PlatformDto[]>(response => response.data)
43-
.then<IdToNameMap>(platforms => Object.fromEntries(platforms.map(platform => [platform.id, platform.name])))
39+
export const getAllPlatforms = () =>
40+
apiGet<{ data: PlatformDto[] }>(
41+
'https://www.speedrun.com/api/v1/platforms',
42+
{ max: MAX_PAGINATION },
43+
false
44+
)
45+
.then<PlatformDto[]>(response => response.data)
46+
.then<IdToNameMap>(platforms => Object.fromEntries(platforms.map(platform => [platform.id, platform.name])))
4447

4548
const requestsStartedForRun = new Map<string, boolean>()
4649

4750
export const fetchValueNamesForRun = async (runId: string) => {
4851
if (requestsStartedForRun.get(runId)) return
4952
requestsStartedForRun.set(runId, true)
50-
return apiGet(`https://www.speedrun.com/api/v1/runs/${runId}?embed=game,category`, undefined, false)
51-
.then<EmbeddedSpeedruncomRun>(response => response.json())
53+
return apiGet<EmbeddedSpeedruncomRun>(
54+
`https://www.speedrun.com/api/v1/runs/${runId}?embed=game,category`,
55+
undefined,
56+
false
57+
)
5258
.then<[IdToNameMap, IdToNameMap]>(response => [
5359
{ [response.data.game.data.id]: response.data.game.data.names.international },
5460
{ [response.data.category.data.id]: response.data.category.data.name },
5561
])
56-
.catch(() => {
57-
requestsStartedForRun.delete(runId)
58-
})
62+
.catch(() => requestsStartedForRun.delete(runId))
5963
}

global-scoreboard/src/fetchers/api.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const makeUrl = (location: string, queryParams?: QueryParams) => {
1818
: targetUrl
1919
}
2020

21-
const apiFetch = (method: RequestInit['method'], url: string, body?: RequestInit['body'], customHeaders = true) =>
21+
const apiFetch = <R>(method: RequestInit['method'], url: string, body?: RequestInit['body'], customHeaders = true) =>
2222
fetch(url, {
2323
method,
2424
headers: customHeaders
@@ -33,15 +33,25 @@ const apiFetch = (method: RequestInit['method'], url: string, body?: RequestInit
3333
.then(response => response.status >= FIRST_HTTP_CODE && response.status <= LAST_HTTP_CODE
3434
? Promise.reject(response)
3535
: response)
36+
.then<R & { token?: string }>(response => response.json())
37+
.then(response => {
38+
// If a token is sent back as part of any response, set it.
39+
// This could be a first login, or a login session extension.
40+
if (response.token) {
41+
localStorage.setItem('jwtToken', response.token)
42+
}
43+
44+
return response as R
45+
})
3646

37-
export const apiGet = (location: string, queryParams?: QueryParams, customHeaders = true) =>
38-
apiFetch('GET', makeUrl(location, queryParams), undefined, customHeaders)
47+
export const apiGet = <R>(location: string, queryParams?: QueryParams, customHeaders = true) =>
48+
apiFetch<R>('GET', makeUrl(location, queryParams), undefined, customHeaders)
3949

40-
export const apiPost = (location: string, body?: Record<string, unknown>) =>
41-
apiFetch('POST', makeUrl(location), JSON.stringify(body))
50+
export const apiPost = <R>(location: string, body?: Record<string, unknown>) =>
51+
apiFetch<R>('POST', makeUrl(location), JSON.stringify(body))
4252

43-
export const apiPut = (location: string, body?: Record<string, unknown>) =>
44-
apiFetch('PUT', makeUrl(location), JSON.stringify(body))
53+
export const apiPut = <R>(location: string, body?: Record<string, unknown>) =>
54+
apiFetch<R>('PUT', makeUrl(location), JSON.stringify(body))
4555

46-
export const apiDelete = (location: string) =>
47-
apiFetch('DELETE', makeUrl(location))
56+
export const apiDelete = <R>(location: string) =>
57+
apiFetch<R>('DELETE', makeUrl(location))

tournament-scheduler/src/Components/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ if (embedded) {
2525
(darkTheme.components?.MuiCssBaseline?.styleOverrides as unknown as StylesOverrides).body.background = 'transparent'
2626
}
2727

28-
const getCurrentUser = () => apiGet('users/current').then(response => response.json())
28+
const getCurrentUser = () => apiGet<{ user: User | undefined }>('users/current')
2929

3030
const logout = (setCurrentUser: (user: null) => void) => {
3131
setCurrentUser(null)
@@ -50,7 +50,7 @@ const App = () => {
5050

5151
useEffect(() => {
5252
getCurrentUser()
53-
.then((response: { user: User | undefined }) => response.user)
53+
.then(response => response.user)
5454
.then(setCurrentUser)
5555
.catch((error: Response) => {
5656
if (error.status === StatusCodes.UNAUTHORIZED) {

tournament-scheduler/src/Components/LoginForm/LoginForm.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,8 @@ type loginFormProps = {
1111
}
1212

1313
const login = (speedruncomApiKey: string, onLoginCallback: (currentUser: User) => void) =>
14-
apiPost('login', { speedruncomApiKey })
15-
.then(response => response.json())
16-
.then((response: { token: string, user: User }) => {
17-
if (!response.token) return
18-
localStorage.setItem('jwtToken', response.token)
19-
onLoginCallback(response.user)
20-
})
14+
apiPost<{ user: User }>('login', { speedruncomApiKey })
15+
.then(response => onLoginCallback(response.user))
2116
.catch(console.error)
2217

2318
const LoginForm = (props: loginFormProps) => {

tournament-scheduler/src/Components/ScheduleManagement/ScheduleManagement.tsx

+6-12
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@ import type User from 'src/Models/User'
1414
import { arrayMove } from 'src/utils/objectUtils'
1515

1616
const getSchedules = () =>
17-
apiGet('schedules')
18-
.then(response =>
19-
response.json().then((scheduleDtos: ScheduleDto[] | undefined) =>
20-
scheduleDtos?.map(scheduleDto => new Schedule(scheduleDto)) ?? []))
17+
apiGet<ScheduleDto[] | undefined>('schedules')
18+
.then(scheduleDtos => scheduleDtos?.map(scheduleDto => new Schedule(scheduleDto)) ?? [])
2119

2220
const postSchedules = (schedule: ScheduleDto) =>
23-
apiPost('schedules', schedule)
24-
.then<number>(response => response.json())
21+
apiPost<number>('schedules', schedule)
2522

2623
const putSchedule = (schedule: ScheduleDto) =>
2724
apiPut(`schedules/${schedule.id}`, schedule)
@@ -36,14 +33,11 @@ const putScheduleOrder = (scheduleOrderDtos: ScheduleOrderDto[]) =>
3633
apiPut('schedules/order', scheduleOrderDtos)
3734

3835
const getGroups = () =>
39-
apiGet('schedule_groups')
40-
.then(response =>
41-
response.json().then((scheduleGroupDtos: ScheduleGroupDto[] | undefined) =>
42-
scheduleGroupDtos?.map(scheduleGroupDto => new ScheduleGroup(scheduleGroupDto)) ?? []))
36+
apiGet<ScheduleGroupDto[] | undefined>('schedule_groups')
37+
.then(scheduleGroupDtos => scheduleGroupDtos?.map(scheduleGroupDto => new ScheduleGroup(scheduleGroupDto)) ?? [])
4338

4439
const postGroups = (scheduleGroup: ScheduleGroupDto) =>
45-
apiPost('schedule_groups', scheduleGroup)
46-
.then<number>(response => response.json())
40+
apiPost<number>('schedule_groups', scheduleGroup)
4741

4842
const putGroup = (scheduleGroup: ScheduleGroupDto) =>
4943
apiPut(`schedule_groups/${scheduleGroup.id}`, scheduleGroup)

tournament-scheduler/src/Components/ScheduleRegistration.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ const entriesLeftText = (timeSlot: TimeSlot) => {
2626
}
2727

2828
const getSchedule = (id: number, registrationKey: string) =>
29-
apiGet(`schedules/${id}`, { registrationKey })
30-
.then(response => response.json().then((scheduleDto: ScheduleDto) => new Schedule(scheduleDto)))
29+
apiGet<ScheduleDto>(`schedules/${id}`, { registrationKey })
30+
.then(scheduleDto => new Schedule(scheduleDto))
3131

3232
const postRegistration = (timeSlotId: number, participants: string[], registrationKey: string) =>
3333
apiPost(`time-slots/${timeSlotId}/registrations`, { participants, registrationKey })
34-
.then(response => response.json())
3534

3635
const ScheduleRegistration = () => {
3736
const [scheduleState, setScheduleState] = useState<Schedule | null | undefined>()

tournament-scheduler/src/Components/ScheduleViewer/ScheduleGroupViewer.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ import type { ScheduleDto, ScheduleGroupDto } from 'src/Models/Schedule'
99
import { Schedule } from 'src/Models/Schedule'
1010

1111
const getSchedules = (id: number) =>
12-
apiGet(`schedule_groups/${id}/schedules`)
13-
.then<ScheduleDto[]>(response => response.json())
12+
apiGet<ScheduleDto[]>(`schedule_groups/${id}/schedules`)
1413

1514
const getGroup = (id: number) =>
16-
apiGet(`schedule_groups/${id}`)
17-
.then<ScheduleGroupDto>(response => response.json())
15+
apiGet<ScheduleGroupDto>(`schedule_groups/${id}`)
1816

1917
const ScheduleGroupViewer = () => {
2018
const [schedules, setSchedules] = useState<ScheduleDto[]>([])

tournament-scheduler/src/Components/ScheduleViewer/ScheduleViewer.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ type ScheduleViewerProps = {
2525
}
2626

2727
const getSchedule = (id: number) =>
28-
apiGet(`schedules/${id}`)
29-
.then(response =>
30-
response.json().then((scheduleDto: ScheduleDto) => {
31-
const newSchedule = new Schedule(scheduleDto)
32-
newSchedule.timeSlots.sort(TimeSlot.compareFn)
33-
return newSchedule
34-
}))
28+
apiGet<ScheduleDto>(`schedules/${id}`)
29+
.then(scheduleDto => {
30+
const newSchedule = new Schedule(scheduleDto)
31+
newSchedule.timeSlots.sort(TimeSlot.compareFn)
32+
return newSchedule
33+
})
3534

3635
export const TimeZoneMessage = <Typography>All dates and times are given in your local timezone.</Typography>
3736

0 commit comments

Comments
 (0)