Skip to content

Commit

Permalink
Merge pull request #5 from Tietokilta/dev
Browse files Browse the repository at this point in the history
TiK updates
  • Loading branch information
hedgeho authored Apr 7, 2024
2 parents ae82ff2 + cc310ba commit e77ca90
Show file tree
Hide file tree
Showing 15 changed files with 138 additions and 213 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineMigration } from './util';

export default defineMigration({
name: '0005-add-indexes',
async up({ context: { sequelize } }) {
const query = sequelize.getQueryInterface();
await query.addIndex(
'quota',
{
name: 'idx_quota_main',
fields: ['eventId', 'deletedAt'],
},
);
await query.addIndex(
'signup',
{
name: 'idx_signup_main',
fields: ['quotaId', 'deletedAt', 'confirmedAt', 'createdAt'],
},
);
await query.addIndex(
'answer',
{
name: 'idx_answer_main',
fields: ['signupId', 'questionId', 'deletedAt'],
},
);
},
async down({ context: { sequelize } }) {
const query = sequelize.getQueryInterface();
await query.removeIndex('quota', 'idx_quota_main');
await query.removeIndex('signup', 'idx_signup_main');
await query.removeIndex('answer', 'idx_answer_main');
},
});
2 changes: 2 additions & 0 deletions packages/ilmomasiina-backend/src/models/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import _0001_add_audit_logs from './0001-add-audit-logs';
import _0002_add_event_endDate from './0002-add-event-endDate';
import _0003_add_signup_language from './0003-add-signup-language';
import _0004_answers_to_json from './0004-answers-to-json';
import _0005_add_indexes from './0005-add-indexes';

const migrations: RunnableMigration<Sequelize>[] = [
_0000_initial,
_0001_add_audit_logs,
_0002_add_event_endDate,
_0003_add_signup_language,
_0004_answers_to_json,
_0005_add_indexes,
];

export default migrations;
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function adminLogin(session: AdminAuthSession) {

export function renewAdminToken(session: AdminAuthSession) {
return async (
request: FastifyRequest<{ Body: AdminLoginBody }>,
request: FastifyRequest,
reply: FastifyReply,
): Promise<AdminLoginResponse | void> => {
// Verify existing token
Expand Down
15 changes: 13 additions & 2 deletions packages/ilmomasiina-backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -323,7 +323,18 @@ async function setupPublicRoutes(
adminLogin(opts.adminSession),
);

// TODO: Add an API endpoint for session token renewal as variant of adminLoginSchema
server.post(
'/authentication/renew',
{
schema: {
response: {
...errorResponses,
201: schema.adminLoginResponse,
},
},
},
renewAdminToken(opts.adminSession),
);

// Public routes for events

Expand Down
4 changes: 2 additions & 2 deletions packages/ilmomasiina-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
"lodash-es": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"react": "^18.2.0",
"react": "^17 || ^18.2.0",
"react-bootstrap": "^1.6.8",
"react-countdown": "^2.3.5",
"react-dom": "^18.2.0",
"react-dom": "^17 || ^18.2.0",
"react-final-form": "^6.5.9",
"react-i18next": "^14.0.5",
"react-markdown": "^8.0.7",
Expand Down
12 changes: 4 additions & 8 deletions packages/ilmomasiina-components/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
body?: any;
headers?: Record<string, string>;
accessToken?: string;
signal?: AbortSignal;
}

Expand Down Expand Up @@ -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<T = unknown>(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';
}
Expand All @@ -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<T>;
}
1 change: 0 additions & 1 deletion packages/ilmomasiina-components/src/contexts/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createContext } from 'react';

export interface AuthState {
accessToken?: string;
loggedIn: boolean;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/ilmomasiina-frontend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
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';

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.
*/
export default async function adminApiFetch(uri: string, opts: FetchOptions, dispatch: DispatchAction) {
export default async function adminApiFetch<T = unknown>(
uri: string,
opts: AdminApiFetchOptions,
dispatch: DispatchAction,
) {
try {
return await apiFetch(uri, opts);
const { accessToken } = opts;
if (!accessToken) {
throw new ApiError(401, { isUnauthenticated: true });
}
// Renew token asynchronously if it's expiring soon
if (Date.now() > accessToken.expiresAt - RENEW_LOGIN_THRESHOLD) {
dispatch(renewLogin(accessToken.token));
}
return await apiFetch<T>(uri, { ...opts, headers: { ...opts.headers, Authorization: accessToken.token } });
} catch (err) {
if (err instanceof ApiError && err.code === ErrorCode.BAD_SESSION) {
dispatch(loginExpired());
Expand Down
4 changes: 2 additions & 2 deletions packages/ilmomasiina-frontend/src/containers/requireAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export default function requireAuth<P extends {}>(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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export type AdminEventsActions =
| ReturnType<typeof resetState>;

export const getAdminEvents = () => async (dispatch: DispatchAction, getState: GetState) => {
const { accessToken } = getState().auth;
try {
const { accessToken } = getState().auth;
const response = await adminApiFetch('admin/events', { accessToken }, dispatch);
dispatch(eventsLoaded(response as AdminEventListResponse));
} catch (e) {
Expand Down
33 changes: 29 additions & 4 deletions packages/ilmomasiina-frontend/src/modules/auth/actions.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<AdminLoginResponse>('authentication', {
method: 'POST',
body: {
email,
password,
},
}) as AdminLoginResponse;
});
dispatch(loginSucceeded(sessionResponse));
dispatch(push(appPaths.adminEventsList));
loginToast('success', i18n.t('auth.loginSuccess'), 2000);
Expand Down Expand Up @@ -78,3 +78,28 @@ export const loginExpired = () => (dispatch: DispatchAction) => {
loginToast('error', i18n.t('auth.loginExpired'), 10000);
dispatch(redirectToLogin());
};

export const renewLogin = (accessToken: string) => async (dispatch: DispatchAction) => {
try {
if (accessToken) {
const sessionResponse = await apiFetch<AdminLoginResponse>('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;
}
}
};
15 changes: 7 additions & 8 deletions packages/ilmomasiina-frontend/src/modules/auth/reducer.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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:
Expand Down
7 changes: 5 additions & 2 deletions packages/ilmomasiina-frontend/src/modules/auth/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export interface AccessToken {
token: string;
expiresAt: number; // Unix timestamp
}
export interface AuthState {
accessToken?: string;
accessTokenExpires?: string;
accessToken?: AccessToken;
loggedIn: boolean;
}

Expand Down
1 change: 0 additions & 1 deletion packages/ilmomasiina-models/src/schema/login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export const adminLoginBody = Type.Object({
description: 'Plaintext password.',
}),
});

/** Response schema for a successful login. */
export const adminLoginResponse = Type.Object({
accessToken: Type.String({
Expand Down
Loading

0 comments on commit e77ca90

Please sign in to comment.