From cbca2e61841cde77c5a057fd5f3c6c1cdf914349 Mon Sep 17 00:00:00 2001 From: Paulo Gustavo Veiga Date: Tue, 6 Feb 2024 21:48:04 -0800 Subject: [PATCH] JWT impl. --- packages/webapp/package.json | 3 +- packages/webapp/public/index.html | 21 ++- packages/webapp/src/classes/client/index.ts | 7 + .../src/classes/client/mock-client/index.ts | 14 ++ .../src/classes/client/rest-client/index.ts | 135 ++++++++++++------ .../src/components/login-page/index.tsx | 53 +++++-- .../components/registration-page/index.tsx | 4 +- packages/webapp/webpack.dev.js | 13 ++ 8 files changed, 192 insertions(+), 58 deletions(-) diff --git a/packages/webapp/package.json b/packages/webapp/package.json index c6ea4d30..bf268545 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -39,7 +39,8 @@ "react-query": "^3.39.1", "react-redux": "^7.2.2", "react-router-dom": "^6.4.3", - "styled-components": "^5.3.6" + "styled-components": "^5.3.6", + "universal-cookie": "^7.0.2" }, "devDependencies": { "@formatjs/cli": "^6.0.4", diff --git a/packages/webapp/public/index.html b/packages/webapp/public/index.html index c01bad73..b346c02d 100644 --- a/packages/webapp/public/index.html +++ b/packages/webapp/public/index.html @@ -7,7 +7,8 @@ - + @@ -26,6 +27,20 @@ + + --> @@ -61,4 +76,4 @@ --> - + \ No newline at end of file diff --git a/packages/webapp/src/classes/client/index.ts b/packages/webapp/src/classes/client/index.ts index 442943df..0f2ac7f3 100644 --- a/packages/webapp/src/classes/client/index.ts +++ b/packages/webapp/src/classes/client/index.ts @@ -1,5 +1,10 @@ import { Locale, LocaleCode } from '../app-i18n'; +export type JwtAuth = { + email: string; + password: string; +}; + export type NewUser = { email: string; firstname: string; @@ -85,6 +90,8 @@ export type ForgotPasswordResult = { }; interface Client { + login(auth: JwtAuth): Promise; + deleteAccount(): Promise; importMap(model: ImportMapInfo): Promise; createMap(map: BasicMapInfo): Promise; diff --git a/packages/webapp/src/classes/client/mock-client/index.ts b/packages/webapp/src/classes/client/mock-client/index.ts index a052e09e..5610e0f3 100644 --- a/packages/webapp/src/classes/client/mock-client/index.ts +++ b/packages/webapp/src/classes/client/mock-client/index.ts @@ -26,8 +26,10 @@ import Client, { Permission, Oauth2CallbackResult, ForgotPasswordResult, + JwtAuth, } from '..'; import { LocaleCode, localeFromStr } from '../../app-i18n'; +import Cookies from 'universal-cookie'; const label1: Label = { id: 1, @@ -127,6 +129,18 @@ class MockClient implements Client { this.labels = [label1, label2, label3]; } + login(auth: JwtAuth): Promise { + const cookies = new Cookies(); + cookies.set('jwt-token-mock', auth.email, { path: '/' }); + return Promise.resolve(); + } + + private _jwtToken(): string | undefined { + // Set cookie on session ... + const cookies = new Cookies(); + return cookies.get('jwt-token-mock'); + } + fetchStarred(id: number): Promise { return Promise.resolve(Boolean(this.maps.find((m) => m.id == id)?.starred)); } diff --git a/packages/webapp/src/classes/client/rest-client/index.ts b/packages/webapp/src/classes/client/rest-client/index.ts index 50e95028..153a80ce 100644 --- a/packages/webapp/src/classes/client/rest-client/index.ts +++ b/packages/webapp/src/classes/client/rest-client/index.ts @@ -11,9 +11,11 @@ import Client, { Permission, Oauth2CallbackResult, ForgotPasswordResult, + JwtAuth, } from '..'; import { getCsrfToken } from '../../../utils'; import { LocaleCode, localeFromStr } from '../../app-i18n'; +import Cookies from 'universal-cookie'; export default class RestClient implements Client { private baseUrl: string; @@ -32,18 +34,64 @@ export default class RestClient implements Client { constructor(baseUrl: string) { this.baseUrl = baseUrl; this.axios = axios.create({ maxRedirects: 0 }); - const csrfToken = getCsrfToken(); - if (csrfToken) { - this.axios.defaults.headers['X-CSRF-TOKEN'] = csrfToken; - } else { - console.warn('csrf token not found in html head'); - } + + // Configure request interceptors ... + this.axios.interceptors.request.use((config) => { + if (config.headers) { + // JWT Token ... + const jwtToken = this._jwtToken(); + if (jwtToken) { + config.headers['Authorization'] = jwtToken; + } + + // Add Csrf token ... + const csrfToken = getCsrfToken(); + if (csrfToken) { + config.headers['X-CSRF-TOKEN'] = csrfToken; + } else { + console.warn('csrf token not found in html head'); + } + } + + return config; + }); + + // Process response globally ... this.axios.interceptors.response.use( - (r) => r, - (r) => this.checkResponseForSessionExpired(r), + (response) => response, + (respoonse) => this.checkResponseForSessionExpired(respoonse), ); } + login(model: JwtAuth): Promise { + const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { + this.axios + .post(`${this.baseUrl}/api/restful/authenticate`, model, { + headers: { 'Content-Type': 'application/json' }, + }) + .then((response) => { + const token = response.data; + // Set jwt token on cookie ... + const cookies = new Cookies(); + cookies.set('jwt-auth-token', token, { path: '/', maxAge: 604800 }); + + success(); + }) + .catch((error) => { + const errorInfo = this.parseResponseOnError(error.response); + reject(errorInfo); + }); + }; + return new Promise(handler); + } + + private _jwtToken(): string | null { + // Set cookie on session ... + const cookies = new Cookies(); + const token = cookies.get('jwt-auth-token'); + return token ? `Bearer ${token}` : null; + } + private _onSessionExpired: () => void; onSessionExpired(callback?: () => void): () => void { if (callback) { @@ -61,9 +109,12 @@ export default class RestClient implements Client { deleteMapPermission(id: number, email: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .delete(`${this.baseUrl}/c/restful/maps/${id}/collabs?email=${encodeURIComponent(email)}`, { - headers: { 'Content-Type': 'text/plain' }, - }) + .delete( + `${this.baseUrl}/api/restful/maps/${id}/collabs?email=${encodeURIComponent(email)}`, + { + headers: { 'Content-Type': 'text/plain' }, + }, + ) .then(() => { success(); }) @@ -78,7 +129,7 @@ export default class RestClient implements Client { fetchStarred(id: number): Promise { const handler = (success: (starred: boolean) => void, reject: (error: ErrorInfo) => void) => { this.axios - .get(`${this.baseUrl}/c/restful/maps/${id}/starred`, { + .get(`${this.baseUrl}/api/restful/maps/${id}/starred`, { headers: { 'Content-Type': 'text/plain' }, }) .then((response) => { @@ -98,7 +149,7 @@ export default class RestClient implements Client { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios .put( - `${this.baseUrl}/c/restful/maps/${id}/collabs/`, + `${this.baseUrl}/api/restful/maps/${id}/collabs/`, { message: message, collaborations: permissions, @@ -123,7 +174,7 @@ export default class RestClient implements Client { reject: (error: ErrorInfo) => void, ) => { this.axios - .get(`${this.baseUrl}/c/restful/maps/${id}/collabs`, { + .get(`${this.baseUrl}/api/restful/maps/${id}/collabs`, { headers: { 'Content-Type': 'text/plain' }, }) .then((response) => { @@ -150,7 +201,7 @@ export default class RestClient implements Client { deleteAccount(): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .delete(`${this.baseUrl}/c/restful/account`, { + .delete(`${this.baseUrl}/api/restful/account`, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { @@ -167,11 +218,11 @@ export default class RestClient implements Client { updateAccountInfo(firstname: string, lastname: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/c/restful/account/firstname`, firstname, { + .put(`${this.baseUrl}/api/restful/account/firstname`, firstname, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { - return this.axios.put(`${this.baseUrl}/c/restful/account/lastname`, lastname, { + return this.axios.put(`${this.baseUrl}/api/restful/account/lastname`, lastname, { headers: { 'Content-Type': 'text/plain' }, }); }) @@ -190,7 +241,7 @@ export default class RestClient implements Client { updateAccountPassword(pasword: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/c/restful/account/password`, pasword, { + .put(`${this.baseUrl}/api/restful/account/password`, pasword, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { @@ -207,7 +258,7 @@ export default class RestClient implements Client { updateAccountLanguage(locale: LocaleCode): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/c/restful/account/locale`, locale, { + .put(`${this.baseUrl}/api/restful/account/locale`, locale, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { @@ -229,7 +280,7 @@ export default class RestClient implements Client { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { this.axios .post( - `${this.baseUrl}/c/restful/maps?title=${encodeURIComponent( + `${this.baseUrl}/api/restful/maps?title=${encodeURIComponent( model.title, )}&description=${encodeURIComponent(model.description ? model.description : '')}`, model.content, @@ -253,7 +304,7 @@ export default class RestClient implements Client { reject: (error: ErrorInfo) => void, ) => { this.axios - .get(`${this.baseUrl}/c/restful/account`, { + .get(`${this.baseUrl}/api/restful/account`, { headers: { 'Content-Type': 'application/json' }, }) .then((response) => { @@ -278,7 +329,7 @@ export default class RestClient implements Client { deleteMaps(ids: number[]): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .delete(`${this.baseUrl}/c/restful/maps/batch?ids=${ids.join()}`, { + .delete(`${this.baseUrl}/api/restful/maps/batch?ids=${ids.join()}`, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { @@ -296,7 +347,7 @@ export default class RestClient implements Client { updateMapToPublic(id: number, isPublic: boolean): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/c/restful/maps/${id}/publish`, isPublic.toString(), { + .put(`${this.baseUrl}/api/restful/maps/${id}/publish`, isPublic.toString(), { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { @@ -313,7 +364,7 @@ export default class RestClient implements Client { revertHistory(id: number, hid: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .post(`${this.baseUrl}/c/restful/maps/${id}/history/${hid}`, null, { + .post(`${this.baseUrl}/api/restful/maps/${id}/history/${hid}`, null, { headers: { 'Content-Type': 'text/pain' }, }) .then(() => { @@ -333,7 +384,7 @@ export default class RestClient implements Client { reject: (error: ErrorInfo) => void, ) => { this.axios - .get(`${this.baseUrl}/c/restful/maps/${id}/history/`, { + .get(`${this.baseUrl}/api/restful/maps/${id}/history/`, { headers: { 'Content-Type': 'application/json' }, }) .then((response) => { @@ -358,12 +409,12 @@ export default class RestClient implements Client { renameMap(id: number, basicInfo: BasicMapInfo): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/c/restful/maps/${id}/title`, basicInfo.title, { + .put(`${this.baseUrl}/api/restful/maps/${id}/title`, basicInfo.title, { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { return this.axios.put( - `${this.baseUrl}/c/restful/maps/${id}/description`, + `${this.baseUrl}/api/restful/maps/${id}/description`, basicInfo.description || ' ', { headers: { 'Content-Type': 'text/plain' } }, ); @@ -385,10 +436,10 @@ export default class RestClient implements Client { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { this.axios .post( - `${this.baseUrl}/c/restful/maps?title=${encodeURIComponent( + `${this.baseUrl}/api/restful/maps?title=${encodeURIComponent( model.title, )}&description=${encodeURIComponent(model.description ? model.description : '')}`, - null, + undefined, { headers: { 'Content-Type': 'application/json' } }, ) .then((response) => { @@ -409,7 +460,7 @@ export default class RestClient implements Client { reject: (error: ErrorInfo) => void, ) => { this.axios - .get(`${this.baseUrl}/c/restful/maps/`, { + .get(`${this.baseUrl}/api/restful/maps/`, { headers: { 'Content-Type': 'application/json' }, }) .then((response) => { @@ -443,7 +494,7 @@ export default class RestClient implements Client { registerNewUser(user: NewUser): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .post(`${this.baseUrl}/service/users/`, JSON.stringify(user), { + .post(`${this.baseUrl}/api/restful/users/`, JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }) .then(() => { @@ -461,7 +512,7 @@ export default class RestClient implements Client { deleteMap(id: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .delete(`${this.baseUrl}/c/restful/maps/${id}`, { + .delete(`${this.baseUrl}/api/restful/maps/${id}`, { headers: { 'Content-Type': 'application/json' }, }) .then(() => { @@ -482,7 +533,7 @@ export default class RestClient implements Client { ) => { this.axios .put( - `${this.baseUrl}/service/users/resetPassword?email=${encodeURIComponent(email)}`, + `${this.baseUrl}/api/restful/users/resetPassword?email=${encodeURIComponent(email)}`, null, { headers: { 'Content-Type': 'application/json' }, @@ -504,7 +555,7 @@ export default class RestClient implements Client { duplicateMap(id: number, basicInfo: BasicMapInfo): Promise { const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => { this.axios - .post(`${this.baseUrl}/c/restful/maps/${id}`, JSON.stringify(basicInfo), { + .post(`${this.baseUrl}/api/restful/maps/${id}`, JSON.stringify(basicInfo), { headers: { 'Content-Type': 'application/json' }, }) .then((response) => { @@ -523,7 +574,7 @@ export default class RestClient implements Client { updateStarred(id: number, starred: boolean): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/c/restful/maps/${id}/starred`, starred.toString(), { + .put(`${this.baseUrl}/api/restful/maps/${id}/starred`, starred.toString(), { headers: { 'Content-Type': 'text/plain' }, }) .then(() => { @@ -541,7 +592,7 @@ export default class RestClient implements Client { fetchLabels(): Promise { const handler = (success: (labels: Label[]) => void, reject: (error: ErrorInfo) => void) => { this.axios - .get(`${this.baseUrl}/c/restful/labels/`, { + .get(`${this.baseUrl}/api/restful/labels/`, { headers: { 'Content-Type': 'application/json' }, }) .then((response) => { @@ -569,7 +620,7 @@ export default class RestClient implements Client { const handler = (success: (labelId: number) => void, reject: (error: ErrorInfo) => void) => { this.axios .post( - `${this.baseUrl}/c/restful/labels`, + `${this.baseUrl}/api/restful/labels`, JSON.stringify({ title, color, iconName: 'smile' }), { headers: { 'Content-Type': 'application/json' }, @@ -590,7 +641,7 @@ export default class RestClient implements Client { deleteLabel(id: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .delete(`${this.baseUrl}/c/restful/labels/${id}`) + .delete(`${this.baseUrl}/api/restful/labels/${id}`) .then(() => { success(); }) @@ -605,7 +656,7 @@ export default class RestClient implements Client { addLabelToMap(labelId: number, mapId: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), { + .post(`${this.baseUrl}/api/restful/maps/${mapId}/labels`, JSON.stringify(labelId), { headers: { 'Content-Type': 'application/json' }, }) .then(() => { @@ -622,7 +673,7 @@ export default class RestClient implements Client { deleteLabelFromMap(labelId: number, mapId: number): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`) + .delete(`${this.baseUrl}/api/restful/maps/${mapId}/labels/${labelId}`) .then(() => { success(); }) @@ -640,7 +691,7 @@ export default class RestClient implements Client { reject: (error: ErrorInfo) => void, ) => { this.axios - .post(`${this.baseUrl}/service/oauth2/googlecallback?code=${code}`, { + .post(`${this.baseUrl}/api/restful/oauth2/googlecallback?code=${code}`, { headers: { 'Content-Type': 'application/json' }, }) .then((response) => { @@ -661,7 +712,7 @@ export default class RestClient implements Client { confirmAccountSync(email: string, code: string): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { this.axios - .put(`${this.baseUrl}/service/oauth2/confirmaccountsync?email=${email}&code=${code}`, { + .put(`${this.baseUrl}/api/restful/oauth2/confirmaccountsync?email=${email}&code=${code}`, { headers: { 'Content-Type': 'application/json' }, }) .then(() => { diff --git a/packages/webapp/src/components/login-page/index.tsx b/packages/webapp/src/components/login-page/index.tsx index 4b0e387f..bc9998e3 100644 --- a/packages/webapp/src/components/login-page/index.tsx +++ b/packages/webapp/src/components/login-page/index.tsx @@ -1,6 +1,6 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink, useNavigate } from 'react-router-dom'; import Header from '../layout/header'; import Footer from '../layout/footer'; import SubmitButton from '../form/submit-button'; @@ -15,6 +15,17 @@ import Separator from '../common/separator'; import GoogleButton from '../common/google-button'; import AppConfig from '../../classes/app-config'; import CSRFInput from '../common/csrf-input'; +import { useMutation } from 'react-query'; +import { useSelector } from 'react-redux'; +import Client, { ErrorInfo } from '../../classes/client'; +import { activeInstance } from '../../redux/clientSlice'; + +export type Model = { + email: string; + password: string; +}; + +const defaultModel: Model = { email: '', password: '' }; const LoginError = () => { // @Todo: This must be reviewed to be based on navigation state. @@ -44,6 +55,9 @@ const LoginError = () => { const LoginPage = (): React.ReactElement => { const intl = useIntl(); + const [model, setModel] = useState(defaultModel); + const client: Client = useSelector(activeInstance); + const navigate = useNavigate(); useEffect(() => { document.title = intl.formatMessage({ @@ -53,6 +67,29 @@ const LoginPage = (): React.ReactElement => { ReactGA.send({ hitType: 'pageview', page: window.location.pathname, title: 'Login' }); }, []); + const mutation = useMutation( + (model: Model) => client.login({ ...model }), + { + onSuccess: () => navigate('/c/maps/'), + onError: (error) => { + console.log(error); + }, + }, + ); + + const handleOnSubmit = (event: React.FormEvent): void => { + mutation.mutate(model); + event.preventDefault(); + }; + + const handleOnChange = (event: React.ChangeEvent): void => { + event.preventDefault(); + + const name = event.target.name; + const value = event.target.value; + setModel({ ...model, [name as keyof Model]: value }); + }; + return (
@@ -69,10 +106,11 @@ const LoginPage = (): React.ReactElement => { -
+ { autoComplete="email" /> { required autoComplete="current-password" /> -
- - -
{ const navigate = useNavigate(); const intl = useIntl(); - const Client: Client = useSelector(activeInstance); + const client: Client = useSelector(activeInstance); const mutation = useMutation( - (model: Model) => Client.registerNewUser({ ...model }), + (model: Model) => client.registerNewUser({ ...model }), { onSuccess: () => navigate('/c/registration-success'), onError: (error) => { diff --git a/packages/webapp/webpack.dev.js b/packages/webapp/webpack.dev.js index e430f77d..1a05b32a 100644 --- a/packages/webapp/webpack.dev.js +++ b/packages/webapp/webpack.dev.js @@ -10,6 +10,18 @@ module.exports = merge(common, { devServer: { port: 3000, hot: true, + proxy: { + '/api': { + target: { + host: "0.0.0.0", + protocol: 'http:', + port: 8080 + }, + pathRewrite: { + '^/api': '' + } + }, + }, historyApiFallback: { rewrites: [{ from: /^\/c\//, to: '/index.html' }], }, @@ -19,6 +31,7 @@ module.exports = merge(common, { template: path.join(__dirname, 'public/index.html'), templateParameters: { PUBLIC_URL: process.env.PUBLIC_URL ? process.env.PUBLIC_URL : 'http://localhost:3000', + CLIENT_TYPE: process.env.CLIENT_TYPE ? process.env.CLIENT_TYPE : 'mock' }, base: process.env.PUBLIC_URL ? process.env.PUBLIC_URL : 'http://localhost:3000', }),