mirror of
https://github.com/billsonnn/nitro-react.git
synced 2024-11-26 23:50:52 +01:00
Authentication
This commit is contained in:
parent
b1e780b253
commit
9b4c0e7fe5
18
package-lock.json
generated
18
package-lock.json
generated
@ -14770,6 +14770,15 @@
|
|||||||
"whatwg-fetch": "^3.4.1"
|
"whatwg-fetch": "^3.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-async-script": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
|
||||||
|
"requires": {
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"prop-types": "^15.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-bootstrap": {
|
"react-bootstrap": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz",
|
||||||
@ -14925,6 +14934,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
||||||
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
|
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
|
||||||
},
|
},
|
||||||
|
"react-google-recaptcha": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-K9jr7e0CWFigi8KxC3WPvNqZZ47df2RrMAta6KmRoE4RUi7Ys6NmNjytpXpg4HI/svmQJLKR+PncEPaNJ98DqQ==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.5.0",
|
||||||
|
"react-async-script": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-draggable": "^4.4.3",
|
"react-draggable": "^4.4.3",
|
||||||
|
"react-google-recaptcha": "^2.1.0",
|
||||||
"react-redux": "^7.2.3",
|
"react-redux": "^7.2.3",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
|
@ -21,6 +21,53 @@
|
|||||||
"furni.extras.url": "${asset.url}/images/furniextras/%image%.png",
|
"furni.extras.url": "${asset.url}/images/furniextras/%image%.png",
|
||||||
"url.prefix": "",
|
"url.prefix": "",
|
||||||
"chat.viewer.height.percentage": 0.40,
|
"chat.viewer.height.percentage": 0.40,
|
||||||
|
"auth.system.enabled": true,
|
||||||
|
"auth.system.http.enabled": true,
|
||||||
|
"auth.system.http.endpoint.login": "https://reqres.in/api/posts",
|
||||||
|
"auth.system.http.endpoint.register": "https://reqres.in/api/posts",
|
||||||
|
"auth.system.sso_field_name": "username",
|
||||||
|
"auth.system.recaptcha.public_key": "",
|
||||||
|
"auth.system.recaptcha.field_name": "recaptcha",
|
||||||
|
"auth.system.login.fields": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"label": "Username",
|
||||||
|
"type": "text",
|
||||||
|
"col": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"label": "Password",
|
||||||
|
"type": "password",
|
||||||
|
"col": 12
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth.system.register.fields": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"label": "Username",
|
||||||
|
"type": "text",
|
||||||
|
"col": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"label": "Email Address",
|
||||||
|
"type": "email",
|
||||||
|
"col": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"label": "Password",
|
||||||
|
"type": "password",
|
||||||
|
"col": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "confirm_password",
|
||||||
|
"label": "Confirm Password",
|
||||||
|
"type": "password",
|
||||||
|
"col": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
"navigator.slider.enabled": true,
|
"navigator.slider.enabled": true,
|
||||||
"navigator.slider.content": [
|
"navigator.slider.content": [
|
||||||
{
|
{
|
||||||
|
20
src/App.tsx
20
src/App.tsx
@ -5,6 +5,7 @@ import { useLocalizationEvent } from './hooks/events/nitro/localization/localiza
|
|||||||
import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event';
|
import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event';
|
||||||
import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
|
import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
|
||||||
import { GetConfiguration } from './utils/GetConfiguration';
|
import { GetConfiguration } from './utils/GetConfiguration';
|
||||||
|
import { AuthView } from './views/auth/AuthView';
|
||||||
import { LoadingView } from './views/loading/LoadingView';
|
import { LoadingView } from './views/loading/LoadingView';
|
||||||
import { MainView } from './views/main/MainView';
|
import { MainView } from './views/main/MainView';
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ export function App(): JSX.Element
|
|||||||
{
|
{
|
||||||
const [ isReady, setIsReady ] = useState(false);
|
const [ isReady, setIsReady ] = useState(false);
|
||||||
const [ isError, setIsError ] = useState(false);
|
const [ isError, setIsError ] = useState(false);
|
||||||
|
const [ isAuth, setIsAuth ] = useState(false);
|
||||||
const [ message, setMessage ] = useState('Getting Ready');
|
const [ message, setMessage ] = useState('Getting Ready');
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -60,8 +62,17 @@ export function App(): JSX.Element
|
|||||||
case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKING:
|
case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKING:
|
||||||
return;
|
return;
|
||||||
case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKE_FAILED:
|
case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKE_FAILED:
|
||||||
setIsError(true);
|
const authEnabled = (GetConfiguration('auth.system.enabled') as boolean);
|
||||||
setMessage('Handshake Failed');
|
|
||||||
|
if(authEnabled)
|
||||||
|
{
|
||||||
|
setIsAuth(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setIsError(true);
|
||||||
|
setMessage('Handshake Failed');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case NitroCommunicationDemoEvent.CONNECTION_AUTHENTICATED:
|
case NitroCommunicationDemoEvent.CONNECTION_AUTHENTICATED:
|
||||||
setMessage('Finishing Up');
|
setMessage('Finishing Up');
|
||||||
@ -125,8 +136,9 @@ export function App(): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="nitro-app">
|
<div className="nitro-app">
|
||||||
{ (!isReady || isError) && <LoadingView isError={ isError } message={ message } /> }
|
{ (!isReady || isError) && !isAuth && <LoadingView isError={ isError } message={ message } /> }
|
||||||
{ (isReady && !isError) && <MainView /> }
|
{ (isReady && !isError && !isAuth) && <MainView /> }
|
||||||
|
{ isAuth && <AuthView /> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
@import './auth/AuthView';
|
||||||
@import './avatar-editor/AvatarEditorView';
|
@import './avatar-editor/AvatarEditorView';
|
||||||
@import './avatar-image/AvatarImage';
|
@import './avatar-image/AvatarImage';
|
||||||
@import './badge-image/BadgeImage';
|
@import './badge-image/BadgeImage';
|
||||||
|
11
src/views/auth/AuthView.scss
Normal file
11
src/views/auth/AuthView.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.nitro-auth {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
background: url('../../assets/images/nitro/nitro-light.svg') no-repeat center;
|
||||||
|
width: 45%;
|
||||||
|
height: 45%;
|
||||||
|
}
|
||||||
|
}
|
168
src/views/auth/AuthView.tsx
Normal file
168
src/views/auth/AuthView.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { AuthenticationMessageComposer } from 'nitro-renderer';
|
||||||
|
import { AuthenticationEvent } from 'nitro-renderer/src/nitro/communication/messages/incoming/handshake/AuthenticationEvent';
|
||||||
|
import { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
import ReCAPTCHA from "react-google-recaptcha";
|
||||||
|
import { CreateMessageHook, SendMessageHook } from '../../hooks/messages';
|
||||||
|
import { GetConfiguration } from '../../utils/GetConfiguration';
|
||||||
|
import { AuthField, AuthViewProps } from './AuthView.types';
|
||||||
|
import { AuthFormView } from './views/form/AuthFormView';
|
||||||
|
|
||||||
|
export const AuthView: FC<AuthViewProps> = props =>
|
||||||
|
{
|
||||||
|
const [ showLogin, setShowLogin ] = useState(true);
|
||||||
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
|
const [ fields, setFields ] = useState<AuthField[]>(null);
|
||||||
|
const [ recaptchaPublicKey, setRecaptchaPublicKey ] = useState(null);
|
||||||
|
const [ recaptchaAnswer, setRecaptchaAnswer ] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const configFields = GetConfiguration<AuthField[]>('auth.system.' + (showLogin ? 'login' : 'register') + '.fields');
|
||||||
|
|
||||||
|
if(configFields.length > 0) setFields(configFields);
|
||||||
|
|
||||||
|
const recaptchaKey = GetConfiguration('auth.system.recaptcha.public_key');
|
||||||
|
|
||||||
|
if(recaptchaKey) setRecaptchaPublicKey(recaptchaKey);
|
||||||
|
}, [ showLogin ]);
|
||||||
|
|
||||||
|
const setFieldValue = useCallback((key: string, value: string) =>
|
||||||
|
{
|
||||||
|
const fieldsClone = Array.from(fields);
|
||||||
|
|
||||||
|
const field = fieldsClone.find(field => field.name === key);
|
||||||
|
|
||||||
|
if(!field) return;
|
||||||
|
|
||||||
|
field.value = value;
|
||||||
|
|
||||||
|
setFields(fieldsClone);
|
||||||
|
}, [ fields ]);
|
||||||
|
|
||||||
|
const sendHttpAuthentication = useCallback((body: string) =>
|
||||||
|
{
|
||||||
|
const endpoint = (GetConfiguration('auth.system.http.endpoint.' + (showLogin ? 'login' : 'register')) as string);
|
||||||
|
|
||||||
|
if(!endpoint) return;
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: body
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(endpoint, requestOptions)
|
||||||
|
.then(response => response.json(), error =>
|
||||||
|
{
|
||||||
|
setIsLoading(false);
|
||||||
|
console.log('Login failed', error);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(data => handleAuthentication(data));
|
||||||
|
}, [ showLogin ]);
|
||||||
|
|
||||||
|
const sendPacketAuthentication = useCallback((keys: string[], values: string[]) =>
|
||||||
|
{
|
||||||
|
SendMessageHook(new AuthenticationMessageComposer((showLogin ? 'login' : 'register'), keys, values));
|
||||||
|
}, [ showLogin ]);
|
||||||
|
|
||||||
|
CreateMessageHook(AuthenticationEvent, event => handleAuthentication(Object.entries(event.parser)));
|
||||||
|
|
||||||
|
const handleAuthentication = useCallback((data: any) =>
|
||||||
|
{
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if(!data) return;
|
||||||
|
|
||||||
|
let ssoFieldName = 'sso';
|
||||||
|
|
||||||
|
if(GetConfiguration<boolean>('auth.system.http.enabled')) ssoFieldName = GetConfiguration<string>('auth.system.sso_field_name', 'sso');
|
||||||
|
|
||||||
|
if(!data[ssoFieldName]) return;
|
||||||
|
|
||||||
|
window.location.href = window.location.origin + '/?sso=' + data[ssoFieldName];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendAuthentication = useCallback(() =>
|
||||||
|
{
|
||||||
|
const recaptchaFieldName = GetConfiguration<string>('auth.system.recaptcha.field_name');
|
||||||
|
|
||||||
|
if(recaptchaPublicKey && (!recaptchaAnswer || !recaptchaFieldName)) return;
|
||||||
|
|
||||||
|
let fieldsOk = true;
|
||||||
|
|
||||||
|
const httpEnabled = GetConfiguration<boolean>('auth.system.http.enabled');
|
||||||
|
|
||||||
|
const requestData = {};
|
||||||
|
const requestKeys: string[] = [];
|
||||||
|
const requestValues: string[] = [];
|
||||||
|
|
||||||
|
fields.map(field =>
|
||||||
|
{
|
||||||
|
if(!field.value || field.value.length === 0)
|
||||||
|
{
|
||||||
|
fieldsOk = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(httpEnabled)
|
||||||
|
requestData[field.name] = field.value;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
requestKeys.push(field.name);
|
||||||
|
requestValues.push(field.value.toString());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if(!fieldsOk) return;
|
||||||
|
|
||||||
|
if(recaptchaPublicKey) requestData[recaptchaFieldName] = recaptchaAnswer;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
if(httpEnabled)
|
||||||
|
{
|
||||||
|
sendHttpAuthentication(JSON.stringify(requestData));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sendPacketAuthentication(requestKeys, requestValues);
|
||||||
|
}
|
||||||
|
}, [ fields, recaptchaPublicKey, recaptchaAnswer ]);
|
||||||
|
|
||||||
|
const handleAction = useCallback((action: string, value?: string) =>
|
||||||
|
{
|
||||||
|
if(!action) return;
|
||||||
|
|
||||||
|
switch(action)
|
||||||
|
{
|
||||||
|
case 'toggle_login':
|
||||||
|
setShowLogin(value => !value);
|
||||||
|
return;
|
||||||
|
case 'recaptcha_load':
|
||||||
|
setRecaptchaAnswer(value);
|
||||||
|
return;
|
||||||
|
case 'send':
|
||||||
|
sendAuthentication();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [ fields, recaptchaAnswer ]);
|
||||||
|
|
||||||
|
if(!fields) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="nitro-auth d-flex flex-column justify-content-center align-items-center w-100 h-100">
|
||||||
|
<div className="logo mb-4"></div>
|
||||||
|
<AuthFormView fields={ fields } isLoading={ isLoading } setFieldValue={ setFieldValue } />
|
||||||
|
{ recaptchaPublicKey && <ReCAPTCHA sitekey={ recaptchaPublicKey } onChange={ (event) => handleAction('recaptcha_load', event) } /> }
|
||||||
|
<div className="d-flex justify-content-center mt-3">
|
||||||
|
<button className="btn btn-success btn-lg me-2" disabled={ isLoading } onClick={ () => handleAction('send') }><i className={ 'fas ' + classNames({'fa-paper-plane': !isLoading, 'fa-spinner fa-spin': isLoading })}></i></button>
|
||||||
|
<button className="btn btn-primary btn-lg" disabled={ isLoading } onClick={ () => handleAction('toggle_login') }><i className={'fas ' + classNames({'fa-user-plus': showLogin, 'fa-chevron-left': !showLogin})}></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
14
src/views/auth/AuthView.types.ts
Normal file
14
src/views/auth/AuthView.types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export interface AuthViewProps
|
||||||
|
{}
|
||||||
|
|
||||||
|
export class AuthField
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
public label: string,
|
||||||
|
public type: string,
|
||||||
|
public col: number,
|
||||||
|
public value: string = ''
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
}
|
30
src/views/auth/views/form/AuthFormView.tsx
Normal file
30
src/views/auth/views/form/AuthFormView.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { FC, useCallback } from 'react';
|
||||||
|
import { AuthFormViewProps } from './AuthFormView.types';
|
||||||
|
|
||||||
|
export const AuthFormView: FC<AuthFormViewProps> = props =>
|
||||||
|
{
|
||||||
|
const { fields = null, setFieldValue = null, isLoading = null } = props;
|
||||||
|
|
||||||
|
const getFieldValue = useCallback((key: string) =>
|
||||||
|
{
|
||||||
|
const field = fields.find(field => field.name === key);
|
||||||
|
|
||||||
|
if(!field) return;
|
||||||
|
|
||||||
|
return field.value;
|
||||||
|
}, [ fields ]);
|
||||||
|
|
||||||
|
if(!fields) return null;
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<div className="row mt-3">
|
||||||
|
{ fields.map(field =>
|
||||||
|
{
|
||||||
|
return <div key={ field.name } className={ "mb-3 col-md-" + field.col }>
|
||||||
|
<h5>{ field.label }</h5>
|
||||||
|
<input type={ field.type } disabled={ isLoading } className="form-control" name={ field.name } value={ getFieldValue(field.name) || '' } onChange={(event) => setFieldValue(field.name, event.target.value.toString())} />
|
||||||
|
</div>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
}
|
8
src/views/auth/views/form/AuthFormView.types.ts
Normal file
8
src/views/auth/views/form/AuthFormView.types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { AuthField } from './../../AuthView.types';
|
||||||
|
|
||||||
|
export interface AuthFormViewProps
|
||||||
|
{
|
||||||
|
fields: AuthField[];
|
||||||
|
setFieldValue: (key: string, value: string | number) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user