Authentication

This commit is contained in:
MyNameIsBatman 2021-06-21 02:00:59 -03:00
parent b1e780b253
commit 9b4c0e7fe5
10 changed files with 314 additions and 4 deletions

18
package-lock.json generated
View File

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

View File

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

View File

@ -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": [
{ {

View File

@ -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>
); );
} }

View File

@ -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';

View 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
View 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>
);
}

View 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 = ''
)
{}
}

View 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>
</>);
}

View File

@ -0,0 +1,8 @@
import { AuthField } from './../../AuthView.types';
export interface AuthFormViewProps
{
fields: AuthField[];
setFieldValue: (key: string, value: string | number) => void;
isLoading: boolean;
}