mirror of
https://github.com/billsonnn/nitro-react.git
synced 2024-11-22 22:30: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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.5.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
@ -19,6 +19,7 @@
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.3",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-redux": "^7.2.3",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-transition-group": "^4.4.1",
|
||||
|
@ -21,6 +21,53 @@
|
||||
"furni.extras.url": "${asset.url}/images/furniextras/%image%.png",
|
||||
"url.prefix": "",
|
||||
"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.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 { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
|
||||
import { GetConfiguration } from './utils/GetConfiguration';
|
||||
import { AuthView } from './views/auth/AuthView';
|
||||
import { LoadingView } from './views/loading/LoadingView';
|
||||
import { MainView } from './views/main/MainView';
|
||||
|
||||
@ -12,6 +13,7 @@ export function App(): JSX.Element
|
||||
{
|
||||
const [ isReady, setIsReady ] = useState(false);
|
||||
const [ isError, setIsError ] = useState(false);
|
||||
const [ isAuth, setIsAuth ] = useState(false);
|
||||
const [ message, setMessage ] = useState('Getting Ready');
|
||||
|
||||
//@ts-ignore
|
||||
@ -60,8 +62,17 @@ export function App(): JSX.Element
|
||||
case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKING:
|
||||
return;
|
||||
case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKE_FAILED:
|
||||
setIsError(true);
|
||||
setMessage('Handshake Failed');
|
||||
const authEnabled = (GetConfiguration('auth.system.enabled') as boolean);
|
||||
|
||||
if(authEnabled)
|
||||
{
|
||||
setIsAuth(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
setIsError(true);
|
||||
setMessage('Handshake Failed');
|
||||
}
|
||||
return;
|
||||
case NitroCommunicationDemoEvent.CONNECTION_AUTHENTICATED:
|
||||
setMessage('Finishing Up');
|
||||
@ -125,8 +136,9 @@ export function App(): JSX.Element
|
||||
|
||||
return (
|
||||
<div className="nitro-app">
|
||||
{ (!isReady || isError) && <LoadingView isError={ isError } message={ message } /> }
|
||||
{ (isReady && !isError) && <MainView /> }
|
||||
{ (!isReady || isError) && !isAuth && <LoadingView isError={ isError } message={ message } /> }
|
||||
{ (isReady && !isError && !isAuth) && <MainView /> }
|
||||
{ isAuth && <AuthView /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
@import './auth/AuthView';
|
||||
@import './avatar-editor/AvatarEditorView';
|
||||
@import './avatar-image/AvatarImage';
|
||||
@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