JWT impl.

This commit is contained in:
Paulo Gustavo Veiga 2024-02-06 21:48:04 -08:00
parent 7a4b5212d9
commit cbca2e6184
8 changed files with 192 additions and 58 deletions

View File

@ -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",

View File

@ -7,7 +7,8 @@
<meta charset="utf-8" />
<link rel="icon" href="<%=PUBLIC_URL%>/favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;600&display=swap" rel="stylesheet" rel="preload"/>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;600&display=swap"
rel="stylesheet" rel="preload" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#000000" />
@ -26,6 +27,20 @@
<meta property="og:url" content="https://www.wisemapping.com" />
<meta property="og:site_name" content="WiseMapping" />
<script>
if ('<%=CLIENT_TYPE%>' === 'rest');
{
window.serverconfig = {
apiBaseUrl: 'http://localhost:3000',
analyticsAccount: 'G-RSDEJH16YM',
clientType: 'rest',
recaptcha2Enabled: true,
recaptcha2SiteKey: '6Lcat08kAAAAAIP-HjhzIa-Yq21PHgGa_ADWc-Ro',
googleOauth2Url: 'https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=https://app.wisemapping.com/c/registration-google&prompt=consent&response_type=code&client_id=625682766634-cocbbbbb403iuvps1evecdk6d7phvbkf.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&state=wisemapping&include_granted_scopes=true'
};
}
window.errorMvcView = '';
</script>
<!--
manifest.json provides metadata used when your web app is installed on a
@ -43,7 +58,7 @@
-->
<!--
Dynamic runtime configuration properties (https://dev.to/matt_catalfamo/runtime-configurations-with-react-22dl)
-->
-->
</head>
<body>
@ -61,4 +76,4 @@
-->
</body>
</html>
</html>

View File

@ -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<void>;
deleteAccount(): Promise<void>;
importMap(model: ImportMapInfo): Promise<number>;
createMap(map: BasicMapInfo): Promise<number>;

View File

@ -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<void> {
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<boolean> {
return Promise.resolve(Boolean(this.maps.find((m) => m.id == id)?.starred));
}

View File

@ -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<void> {
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<void> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<number> {
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<void> {
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<Label[]> {
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<void> {
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<void> {
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<void> {
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<void> {
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(() => {

View File

@ -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<Model>(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<void, ErrorInfo, Model>(
(model: Model) => client.login({ ...model }),
{
onSuccess: () => navigate('/c/maps/'),
onError: (error) => {
console.log(error);
},
},
);
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
mutation.mutate(model);
event.preventDefault();
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
event.preventDefault();
const name = event.target.name;
const value = event.target.value;
setModel({ ...model, [name as keyof Model]: value });
};
return (
<div>
<Header type="only-signup" />
@ -69,10 +106,11 @@ const LoginPage = (): React.ReactElement => {
<LoginError />
<FormControl>
<form action="/c/perform-login" method="POST">
<form onSubmit={handleOnSubmit}>
<CSRFInput />
<Input
name="username"
onChange={handleOnChange}
name="email"
type="email"
label={intl.formatMessage({
id: 'login.email',
@ -82,6 +120,7 @@ const LoginPage = (): React.ReactElement => {
autoComplete="email"
/>
<Input
onChange={handleOnChange}
name="password"
type="password"
label={intl.formatMessage({
@ -91,12 +130,6 @@ const LoginPage = (): React.ReactElement => {
required
autoComplete="current-password"
/>
<div>
<input name="remember-me" id="remember-me" type="checkbox" />
<label htmlFor="remember-me">
<FormattedMessage id="login.remberme" defaultMessage="Remember me" />
</label>
</div>
<SubmitButton
value={intl.formatMessage({
id: 'login.signin',

View File

@ -41,9 +41,9 @@ const RegistrationForm = () => {
const navigate = useNavigate();
const intl = useIntl();
const Client: Client = useSelector(activeInstance);
const client: Client = useSelector(activeInstance);
const mutation = useMutation<void, ErrorInfo, Model>(
(model: Model) => Client.registerNewUser({ ...model }),
(model: Model) => client.registerNewUser({ ...model }),
{
onSuccess: () => navigate('/c/registration-success'),
onError: (error) => {

View File

@ -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',
}),