From 01ce5e9d2f5862086bbbd5af89412271ba7656a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahlstr=C3=B6m=20Kalle?= Date: Thu, 25 May 2023 20:32:58 +0300 Subject: [PATCH 1/8] refactor AuthState, remove momentjs --- packages/ilmomasiina-components/src/api.ts | 12 ++++-------- .../ilmomasiina-components/src/contexts/auth.ts | 1 - packages/ilmomasiina-frontend/src/api.ts | 13 +++++++++++-- .../src/containers/requireAuth.tsx | 4 ++-- .../src/modules/adminEvents/actions.ts | 3 +-- .../src/modules/auth/actions.ts | 4 ++-- .../src/modules/auth/reducer.ts | 15 +++++++-------- .../src/modules/auth/types.ts | 7 +++++-- 8 files changed, 32 insertions(+), 27 deletions(-) diff --git a/packages/ilmomasiina-components/src/api.ts b/packages/ilmomasiina-components/src/api.ts index b322278a..52d7337a 100644 --- a/packages/ilmomasiina-components/src/api.ts +++ b/packages/ilmomasiina-components/src/api.ts @@ -4,7 +4,6 @@ export interface FetchOptions { method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; body?: any; headers?: Record; - accessToken?: string; signal?: AbortSignal; } @@ -40,15 +39,12 @@ export function configureApi(url: string) { apiUrl = url; } -export default async function apiFetch(uri: string, { - method = 'GET', body, headers, accessToken, signal, +export default async function apiFetch(uri: string, { + method = 'GET', body, headers, signal, }: FetchOptions = {}) { const allHeaders = { ...headers || {}, }; - if (accessToken) { - allHeaders.Authorization = accessToken; - } if (body !== undefined) { allHeaders['Content-Type'] = 'application/json; charset=utf-8'; } @@ -68,10 +64,10 @@ export default async function apiFetch(uri: string, { } // 204 No Content if (response.status === 204) { - return null; + return null as T; } // just in case, convert JSON parse errors for 2xx responses to ApiError return response.json().catch((err) => { throw new ApiError(0, err); - }); + }) as Promise; } diff --git a/packages/ilmomasiina-components/src/contexts/auth.ts b/packages/ilmomasiina-components/src/contexts/auth.ts index 70862478..05a1a9c5 100644 --- a/packages/ilmomasiina-components/src/contexts/auth.ts +++ b/packages/ilmomasiina-components/src/contexts/auth.ts @@ -1,7 +1,6 @@ import { createContext } from 'react'; export interface AuthState { - accessToken?: string; loggedIn: boolean; } diff --git a/packages/ilmomasiina-frontend/src/api.ts b/packages/ilmomasiina-frontend/src/api.ts index 873fa406..264a788d 100644 --- a/packages/ilmomasiina-frontend/src/api.ts +++ b/packages/ilmomasiina-frontend/src/api.ts @@ -1,14 +1,23 @@ import { ApiError, apiFetch, FetchOptions } from '@tietokilta/ilmomasiina-components'; import { ErrorCode } from '@tietokilta/ilmomasiina-models'; import { loginExpired } from './modules/auth/actions'; +import { AccessToken } from './modules/auth/types'; import type { DispatchAction } from './store/types'; +interface AdminApiFetchOptions extends FetchOptions { + accessToken?: AccessToken; +} + /** Wrapper for apiFetch that checks for Unauthenticated responses and dispatches a loginExpired * action if necessary. */ -export default async function adminApiFetch(uri: string, opts: FetchOptions, dispatch: DispatchAction) { +export default async function adminApiFetch(uri: string, opts: AdminApiFetchOptions, dispatch: DispatchAction) { try { - return await apiFetch(uri, opts); + const { accessToken } = opts; + if (!accessToken) { + throw new ApiError(401, { isUnauthenticated: true }); + } + return await apiFetch(uri, { ...opts, headers: { ...opts.headers, Authorization: accessToken.token } }); } catch (err) { if (err instanceof ApiError && err.code === ErrorCode.BAD_SESSION) { dispatch(loginExpired()); diff --git a/packages/ilmomasiina-frontend/src/containers/requireAuth.tsx b/packages/ilmomasiina-frontend/src/containers/requireAuth.tsx index 05a07d71..3ec0a794 100644 --- a/packages/ilmomasiina-frontend/src/containers/requireAuth.tsx +++ b/packages/ilmomasiina-frontend/src/containers/requireAuth.tsx @@ -9,11 +9,11 @@ export default function requireAuth

(WrappedComponent: ComponentTyp const RequireAuth = (props: P) => { const dispatch = useTypedDispatch(); - const { accessToken, accessTokenExpires } = useTypedSelector( + const { accessToken } = useTypedSelector( (state) => state.auth, ); - const expired = accessTokenExpires && new Date(accessTokenExpires) < new Date(); + const expired = accessToken && accessToken.expiresAt < Date.now(); const needLogin = expired || !accessToken; useEffect(() => { diff --git a/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts b/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts index 9cb5318b..607bfa2d 100644 --- a/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts +++ b/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts @@ -28,9 +28,8 @@ export type AdminEventsActions = | ReturnType; export const getAdminEvents = () => async (dispatch: DispatchAction, getState: GetState) => { - const { accessToken } = getState().auth; try { - const response = await adminApiFetch('admin/events', { accessToken }, dispatch); + const response = await adminApiFetch('admin/events', { accessToken: getState().auth.accessToken }, dispatch); dispatch(eventsLoaded(response as AdminEventListResponse)); } catch (e) { dispatch(eventsLoadFailed(e as ApiError)); diff --git a/packages/ilmomasiina-frontend/src/modules/auth/actions.ts b/packages/ilmomasiina-frontend/src/modules/auth/actions.ts index 72ff4f23..8f4e234e 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/actions.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/actions.ts @@ -36,13 +36,13 @@ const loginToast = (type: 'success' | 'error', text: string, autoClose: number) }; export const login = (email: string, password: string) => async (dispatch: DispatchAction) => { - const sessionResponse = await apiFetch('authentication', { + const sessionResponse = await apiFetch('authentication', { method: 'POST', body: { email, password, }, - }) as AdminLoginResponse; + }); dispatch(loginSucceeded(sessionResponse)); dispatch(push(appPaths.adminEventsList)); loginToast('success', i18n.t('auth.loginSuccess'), 2000); diff --git a/packages/ilmomasiina-frontend/src/modules/auth/reducer.ts b/packages/ilmomasiina-frontend/src/modules/auth/reducer.ts index 9c441341..d169fa77 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/reducer.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/reducer.ts @@ -1,29 +1,26 @@ -import moment, { Moment } from 'moment'; - import { LOGIN_SUCCEEDED, RESET } from './actionTypes'; import type { AuthActions, AuthState } from './types'; const initialState: AuthState = { accessToken: undefined, - accessTokenExpires: undefined, loggedIn: false, }; -function getTokenExpiry(jwt: string): Moment { +function getTokenExpiry(jwt: string): number { const parts = jwt.split('.'); try { const payload = JSON.parse(window.atob(parts[1])); if (payload.exp) { - return moment.unix(payload.exp); + return payload.exp * 1000; } } catch { // eslint-disable-next-line no-console console.error('Invalid jwt token received!'); } - return moment(); + return 0; } export default function reducer( @@ -35,8 +32,10 @@ export default function reducer( return initialState; case LOGIN_SUCCEEDED: return { - accessToken: action.payload.accessToken, - accessTokenExpires: getTokenExpiry(action.payload.accessToken).toISOString(), + accessToken: { + token: action.payload.accessToken, + expiresAt: getTokenExpiry(action.payload.accessToken), + }, loggedIn: true, }; default: diff --git a/packages/ilmomasiina-frontend/src/modules/auth/types.ts b/packages/ilmomasiina-frontend/src/modules/auth/types.ts index edde1e53..cd227f18 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/types.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/types.ts @@ -1,6 +1,9 @@ +export interface AccessToken { + token: string; + expiresAt: number; // Unix timestamp +} export interface AuthState { - accessToken?: string; - accessTokenExpires?: string; + accessToken?: AccessToken; loggedIn: boolean; } From 879f744ea744737e4627bf3fd39310a894408064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahlstr=C3=B6m=20Kalle?= Date: Thu, 25 May 2023 20:51:22 +0300 Subject: [PATCH 2/8] add renew session endpoint --- .../src/routes/authentication/adminLogin.ts | 4 ++-- .../ilmomasiina-backend/src/routes/index.ts | 17 ++++++++++++++-- packages/ilmomasiina-frontend/src/api.ts | 10 +++++++--- .../src/modules/auth/actionTypes.ts | 1 + .../src/modules/auth/actions.ts | 20 +++++++++++++++++++ .../src/schema/login/index.ts | 8 +++++++- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts b/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts index 10b7aced..934c0779 100644 --- a/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts +++ b/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts @@ -1,7 +1,7 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { HttpError, Unauthorized } from 'http-errors'; -import type { AdminLoginBody, AdminLoginResponse } from '@tietokilta/ilmomasiina-models'; +import type { AdminLoginBody, AdminLoginResponse, AdminRenewLoginBody } from '@tietokilta/ilmomasiina-models'; import AdminAuthSession, { AdminTokenData } from '../../authentication/adminAuthSession'; import AdminPasswordAuth from '../../authentication/adminPasswordAuth'; import { User } from '../../models/user'; @@ -34,7 +34,7 @@ export function adminLogin(session: AdminAuthSession) { export function renewAdminToken(session: AdminAuthSession) { return async ( - request: FastifyRequest<{ Body: AdminLoginBody }>, + request: FastifyRequest<{ Body: AdminRenewLoginBody }>, reply: FastifyReply, ): Promise => { // Verify existing token diff --git a/packages/ilmomasiina-backend/src/routes/index.ts b/packages/ilmomasiina-backend/src/routes/index.ts index 234425c1..5561369c 100644 --- a/packages/ilmomasiina-backend/src/routes/index.ts +++ b/packages/ilmomasiina-backend/src/routes/index.ts @@ -17,7 +17,7 @@ import deleteUser from './admin/users/deleteUser'; import inviteUser from './admin/users/inviteUser'; import listUsers from './admin/users/listUsers'; import resetPassword from './admin/users/resetPassword'; -import { adminLogin, requireAdmin } from './authentication/adminLogin'; +import { adminLogin, renewAdminToken, requireAdmin } from './authentication/adminLogin'; import { getEventDetailsForAdmin, getEventDetailsForUser } from './events/getEventDetails'; import { getEventsListForAdmin, getEventsListForUser } from './events/getEventsList'; import { sendICalFeed } from './ical'; @@ -323,8 +323,21 @@ async function setupPublicRoutes( adminLogin(opts.adminSession), ); - // TODO: Add an API endpoint for session token renewal as variant of adminLoginSchema + // an API endpoint for session token renewal as variant of adminLoginSchema + server.post<{ Body: schema.AdminRenewLoginBody }>( + '/authentication/renew', + { + schema: { + body: schema.adminRenewTokenBody, + response: { + ...errorResponses, + 201: schema.adminLoginResponse, + }, + }, + }, + renewAdminToken(opts.adminSession), + ); // Public routes for events server.get<{ Querystring: schema.EventListQuery }>( diff --git a/packages/ilmomasiina-frontend/src/api.ts b/packages/ilmomasiina-frontend/src/api.ts index 264a788d..48735b3f 100644 --- a/packages/ilmomasiina-frontend/src/api.ts +++ b/packages/ilmomasiina-frontend/src/api.ts @@ -1,6 +1,6 @@ import { ApiError, apiFetch, FetchOptions } from '@tietokilta/ilmomasiina-components'; import { ErrorCode } from '@tietokilta/ilmomasiina-models'; -import { loginExpired } from './modules/auth/actions'; +import { loginExpired, renewLogin } from './modules/auth/actions'; import { AccessToken } from './modules/auth/types'; import type { DispatchAction } from './store/types'; @@ -11,13 +11,17 @@ interface AdminApiFetchOptions extends FetchOptions { /** Wrapper for apiFetch that checks for Unauthenticated responses and dispatches a loginExpired * action if necessary. */ -export default async function adminApiFetch(uri: string, opts: AdminApiFetchOptions, dispatch: DispatchAction) { +// eslint-disable-next-line max-len +export default async function adminApiFetch(uri: string, opts: AdminApiFetchOptions, dispatch: DispatchAction) { try { const { accessToken } = opts; if (!accessToken) { throw new ApiError(401, { isUnauthenticated: true }); } - return await apiFetch(uri, { ...opts, headers: { ...opts.headers, Authorization: accessToken.token } }); + if (!(accessToken.expiresAt < Date.now() - 4 * 60 * 1000)) { + await dispatch(renewLogin(accessToken.token)); + } + return await apiFetch(uri, { ...opts, headers: { ...opts.headers, Authorization: accessToken.token } }); } catch (err) { if (err instanceof ApiError && err.code === ErrorCode.BAD_SESSION) { dispatch(loginExpired()); diff --git a/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts b/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts index 298b1f58..a0858a4a 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts @@ -1,2 +1,3 @@ export const LOGIN_SUCCEEDED = 'auth/LOGIN_SUCCEEDED'; export const RESET = 'auth/RESET'; +export const RENEWING_TOKEN = 'auth/RENEWING_TOKEN'; diff --git a/packages/ilmomasiina-frontend/src/modules/auth/actions.ts b/packages/ilmomasiina-frontend/src/modules/auth/actions.ts index 8f4e234e..766ad2ce 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/actions.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/actions.ts @@ -78,3 +78,23 @@ export const loginExpired = () => (dispatch: DispatchAction) => { loginToast('error', i18n.t('auth.loginExpired'), 10000); dispatch(redirectToLogin()); }; + +export const renewLogin = (accessToken: string) => async (dispatch: DispatchAction) => { + if (accessToken) { + const sessionResponse = await apiFetch('authentication/renew', { + method: 'POST', + body: { + accessToken, + }, + headers: { + Authorization: accessToken, + }, + }); + if (sessionResponse) { + dispatch(loginSucceeded(sessionResponse)); + return true; + } + } + dispatch(loginExpired()); + return false; +}; diff --git a/packages/ilmomasiina-models/src/schema/login/index.ts b/packages/ilmomasiina-models/src/schema/login/index.ts index 8b620460..e1bfed62 100644 --- a/packages/ilmomasiina-models/src/schema/login/index.ts +++ b/packages/ilmomasiina-models/src/schema/login/index.ts @@ -9,7 +9,11 @@ export const adminLoginBody = Type.Object({ description: 'Plaintext password.', }), }); - +export const adminRenewTokenBody = Type.Object({ + accessToken: Type.String({ + description: 'JWT access token. Used in `Authorization` header to authorize requests.', + }), +}); /** Response schema for a successful login. */ export const adminLoginResponse = Type.Object({ accessToken: Type.String({ @@ -19,5 +23,7 @@ export const adminLoginResponse = Type.Object({ /** Request body for login. */ export type AdminLoginBody = Static; +/** Request body for renewLogin */ +export type AdminRenewLoginBody = Static; /** Response schema for a successful login. */ export type AdminLoginResponse = Static; From fa1c4a3306eb7c653a99692ac3a0f4faea889db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20Ahlstr=C3=B6m?= <71292737+kqlski@users.noreply.github.com> Date: Sat, 27 May 2023 04:45:27 -0700 Subject: [PATCH 3/8] remove unused code Co-authored-by: PurkkaKoodari <8439661+PurkkaKoodari@users.noreply.github.com> --- .../src/routes/authentication/adminLogin.ts | 4 ++-- packages/ilmomasiina-backend/src/routes/index.ts | 3 +-- .../ilmomasiina-frontend/src/modules/auth/actionTypes.ts | 1 - packages/ilmomasiina-models/src/schema/login/index.ts | 7 ------- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts b/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts index 934c0779..5a1c9522 100644 --- a/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts +++ b/packages/ilmomasiina-backend/src/routes/authentication/adminLogin.ts @@ -1,7 +1,7 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { HttpError, Unauthorized } from 'http-errors'; -import type { AdminLoginBody, AdminLoginResponse, AdminRenewLoginBody } from '@tietokilta/ilmomasiina-models'; +import type { AdminLoginBody, AdminLoginResponse } from '@tietokilta/ilmomasiina-models'; import AdminAuthSession, { AdminTokenData } from '../../authentication/adminAuthSession'; import AdminPasswordAuth from '../../authentication/adminPasswordAuth'; import { User } from '../../models/user'; @@ -34,7 +34,7 @@ export function adminLogin(session: AdminAuthSession) { export function renewAdminToken(session: AdminAuthSession) { return async ( - request: FastifyRequest<{ Body: AdminRenewLoginBody }>, + request: FastifyRequest, reply: FastifyReply, ): Promise => { // Verify existing token diff --git a/packages/ilmomasiina-backend/src/routes/index.ts b/packages/ilmomasiina-backend/src/routes/index.ts index 5561369c..77e68e1b 100644 --- a/packages/ilmomasiina-backend/src/routes/index.ts +++ b/packages/ilmomasiina-backend/src/routes/index.ts @@ -325,11 +325,10 @@ async function setupPublicRoutes( // an API endpoint for session token renewal as variant of adminLoginSchema - server.post<{ Body: schema.AdminRenewLoginBody }>( + server.post( '/authentication/renew', { schema: { - body: schema.adminRenewTokenBody, response: { ...errorResponses, 201: schema.adminLoginResponse, diff --git a/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts b/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts index a0858a4a..298b1f58 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/actionTypes.ts @@ -1,3 +1,2 @@ export const LOGIN_SUCCEEDED = 'auth/LOGIN_SUCCEEDED'; export const RESET = 'auth/RESET'; -export const RENEWING_TOKEN = 'auth/RENEWING_TOKEN'; diff --git a/packages/ilmomasiina-models/src/schema/login/index.ts b/packages/ilmomasiina-models/src/schema/login/index.ts index e1bfed62..23e492d0 100644 --- a/packages/ilmomasiina-models/src/schema/login/index.ts +++ b/packages/ilmomasiina-models/src/schema/login/index.ts @@ -9,11 +9,6 @@ export const adminLoginBody = Type.Object({ description: 'Plaintext password.', }), }); -export const adminRenewTokenBody = Type.Object({ - accessToken: Type.String({ - description: 'JWT access token. Used in `Authorization` header to authorize requests.', - }), -}); /** Response schema for a successful login. */ export const adminLoginResponse = Type.Object({ accessToken: Type.String({ @@ -23,7 +18,5 @@ export const adminLoginResponse = Type.Object({ /** Request body for login. */ export type AdminLoginBody = Static; -/** Request body for renewLogin */ -export type AdminRenewLoginBody = Static; /** Response schema for a successful login. */ export type AdminLoginResponse = Static; From dcd85a733b3b689dcb8d56ddfd7fc2a19084c49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20Ahlstr=C3=B6m?= Date: Sat, 27 May 2023 04:54:37 -0700 Subject: [PATCH 4/8] use middle variable --- .../ilmomasiina-frontend/src/modules/adminEvents/actions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts b/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts index 607bfa2d..b5a34b9c 100644 --- a/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts +++ b/packages/ilmomasiina-frontend/src/modules/adminEvents/actions.ts @@ -29,7 +29,8 @@ export type AdminEventsActions = export const getAdminEvents = () => async (dispatch: DispatchAction, getState: GetState) => { try { - const response = await adminApiFetch('admin/events', { accessToken: getState().auth.accessToken }, dispatch); + const { accessToken } = getState().auth; + const response = await adminApiFetch('admin/events', { accessToken }, dispatch); dispatch(eventsLoaded(response as AdminEventListResponse)); } catch (e) { dispatch(eventsLoadFailed(e as ApiError)); From 99e1e308a0e4223ac6ad00bf065811ef44946427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20Ahlstr=C3=B6m?= Date: Sat, 27 May 2023 04:56:25 -0700 Subject: [PATCH 5/8] move login renew threshold to a variable --- packages/ilmomasiina-frontend/src/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ilmomasiina-frontend/src/api.ts b/packages/ilmomasiina-frontend/src/api.ts index 48735b3f..50b3abf4 100644 --- a/packages/ilmomasiina-frontend/src/api.ts +++ b/packages/ilmomasiina-frontend/src/api.ts @@ -7,7 +7,7 @@ import type { DispatchAction } from './store/types'; interface AdminApiFetchOptions extends FetchOptions { accessToken?: AccessToken; } - +const RENEW_LOGIN_THRESHOLD = 5 * 60 * 1000; /** Wrapper for apiFetch that checks for Unauthenticated responses and dispatches a loginExpired * action if necessary. */ @@ -18,7 +18,7 @@ export default async function adminApiFetch(uri: string, opts: AdminApi if (!accessToken) { throw new ApiError(401, { isUnauthenticated: true }); } - if (!(accessToken.expiresAt < Date.now() - 4 * 60 * 1000)) { + if (!(accessToken.expiresAt < Date.now() - RENEW_LOGIN_THRESHOLD)) { await dispatch(renewLogin(accessToken.token)); } return await apiFetch(uri, { ...opts, headers: { ...opts.headers, Authorization: accessToken.token } }); From ba4ff2969bbbaf6efa7e79b9616f8adef8ec6848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kalle=20Ahlstr=C3=B6m?= Date: Sat, 27 May 2023 04:59:31 -0700 Subject: [PATCH 6/8] remove 'any' default typing --- packages/ilmomasiina-components/src/api.ts | 2 +- packages/ilmomasiina-frontend/src/api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ilmomasiina-components/src/api.ts b/packages/ilmomasiina-components/src/api.ts index 52d7337a..dc76819a 100644 --- a/packages/ilmomasiina-components/src/api.ts +++ b/packages/ilmomasiina-components/src/api.ts @@ -39,7 +39,7 @@ export function configureApi(url: string) { apiUrl = url; } -export default async function apiFetch(uri: string, { +export default async function apiFetch(uri: string, { method = 'GET', body, headers, signal, }: FetchOptions = {}) { const allHeaders = { diff --git a/packages/ilmomasiina-frontend/src/api.ts b/packages/ilmomasiina-frontend/src/api.ts index 50b3abf4..408edb2e 100644 --- a/packages/ilmomasiina-frontend/src/api.ts +++ b/packages/ilmomasiina-frontend/src/api.ts @@ -12,7 +12,7 @@ const RENEW_LOGIN_THRESHOLD = 5 * 60 * 1000; * action if necessary. */ // eslint-disable-next-line max-len -export default async function adminApiFetch(uri: string, opts: AdminApiFetchOptions, dispatch: DispatchAction) { +export default async function adminApiFetch(uri: string, opts: AdminApiFetchOptions, dispatch: DispatchAction) { try { const { accessToken } = opts; if (!accessToken) { From 4763d5f4f0a3ab5b38c18f28798133ffcb01dc73 Mon Sep 17 00:00:00 2001 From: Petrus Asikainen Date: Thu, 7 Mar 2024 19:11:54 +0200 Subject: [PATCH 7/8] Remove comment --- packages/ilmomasiina-backend/src/routes/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ilmomasiina-backend/src/routes/index.ts b/packages/ilmomasiina-backend/src/routes/index.ts index 77e68e1b..a5b2c473 100644 --- a/packages/ilmomasiina-backend/src/routes/index.ts +++ b/packages/ilmomasiina-backend/src/routes/index.ts @@ -323,8 +323,6 @@ async function setupPublicRoutes( adminLogin(opts.adminSession), ); - // an API endpoint for session token renewal as variant of adminLoginSchema - server.post( '/authentication/renew', { @@ -337,6 +335,7 @@ async function setupPublicRoutes( }, renewAdminToken(opts.adminSession), ); + // Public routes for events server.get<{ Querystring: schema.EventListQuery }>( From 32e7f3da554fafa880dbe4aa4f62260b95ecfb30 Mon Sep 17 00:00:00 2001 From: Petrus Asikainen Date: Thu, 7 Mar 2024 19:22:15 +0200 Subject: [PATCH 8/8] Renew token asynchronously --- packages/ilmomasiina-frontend/src/api.ts | 14 +++++-- .../src/modules/auth/actions.ts | 39 +++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/ilmomasiina-frontend/src/api.ts b/packages/ilmomasiina-frontend/src/api.ts index 408edb2e..9761ec19 100644 --- a/packages/ilmomasiina-frontend/src/api.ts +++ b/packages/ilmomasiina-frontend/src/api.ts @@ -7,19 +7,25 @@ import type { DispatchAction } from './store/types'; interface AdminApiFetchOptions extends FetchOptions { accessToken?: AccessToken; } + const RENEW_LOGIN_THRESHOLD = 5 * 60 * 1000; + /** Wrapper for apiFetch that checks for Unauthenticated responses and dispatches a loginExpired * action if necessary. */ -// eslint-disable-next-line max-len -export default async function adminApiFetch(uri: string, opts: AdminApiFetchOptions, dispatch: DispatchAction) { +export default async function adminApiFetch( + uri: string, + opts: AdminApiFetchOptions, + dispatch: DispatchAction, +) { try { const { accessToken } = opts; if (!accessToken) { throw new ApiError(401, { isUnauthenticated: true }); } - if (!(accessToken.expiresAt < Date.now() - RENEW_LOGIN_THRESHOLD)) { - await dispatch(renewLogin(accessToken.token)); + // Renew token asynchronously if it's expiring soon + if (Date.now() > accessToken.expiresAt - RENEW_LOGIN_THRESHOLD) { + dispatch(renewLogin(accessToken.token)); } return await apiFetch(uri, { ...opts, headers: { ...opts.headers, Authorization: accessToken.token } }); } catch (err) { diff --git a/packages/ilmomasiina-frontend/src/modules/auth/actions.ts b/packages/ilmomasiina-frontend/src/modules/auth/actions.ts index 766ad2ce..e9f0fcc9 100644 --- a/packages/ilmomasiina-frontend/src/modules/auth/actions.ts +++ b/packages/ilmomasiina-frontend/src/modules/auth/actions.ts @@ -1,8 +1,8 @@ import { push } from 'connected-react-router'; import { toast } from 'react-toastify'; -import { apiFetch } from '@tietokilta/ilmomasiina-components'; -import type { AdminLoginResponse } from '@tietokilta/ilmomasiina-models'; +import { ApiError, apiFetch } from '@tietokilta/ilmomasiina-components'; +import { AdminLoginResponse, ErrorCode } from '@tietokilta/ilmomasiina-models'; import i18n from '../../i18n'; import appPaths from '../../paths'; import type { DispatchAction } from '../../store/types'; @@ -80,21 +80,26 @@ export const loginExpired = () => (dispatch: DispatchAction) => { }; export const renewLogin = (accessToken: string) => async (dispatch: DispatchAction) => { - if (accessToken) { - const sessionResponse = await apiFetch('authentication/renew', { - method: 'POST', - body: { - accessToken, - }, - headers: { - Authorization: accessToken, - }, - }); - if (sessionResponse) { - dispatch(loginSucceeded(sessionResponse)); - return true; + try { + if (accessToken) { + const sessionResponse = await apiFetch('authentication/renew', { + method: 'POST', + body: { + accessToken, + }, + headers: { + Authorization: accessToken, + }, + }); + if (sessionResponse) { + dispatch(loginSucceeded(sessionResponse)); + } + } + } catch (err) { + if (err instanceof ApiError && err.code === ErrorCode.BAD_SESSION) { + dispatch(loginExpired()); + } else { + throw err; } } - dispatch(loginExpired()); - return false; };