Fix recaptcha on login forms

This commit is contained in:
Dane Everitt 2019-12-15 18:05:44 -08:00
parent f864b72e0a
commit 66410a35f1
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 188 additions and 77 deletions

View file

@ -30,5 +30,13 @@ class AssetComposer
public function compose(View $view)
{
$view->with('asset', $this->assetHashService);
$view->with('siteConfiguration', [
'name' => config('app.name') ?? 'Pterodactyl',
'locale' => config('app.locale') ?? 'en',
'recaptcha' => [
'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '',
],
]);
}
}

View file

@ -4,6 +4,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "^0.1.4",
"@types/react-google-recaptcha": "^1.1.1",
"axios": "^0.19.0",
"ayu-ace": "^2.0.4",
"brace": "^0.11.1",
@ -23,6 +24,7 @@
"query-string": "^6.7.0",
"react": "^16.12.0",
"react-dom": "npm:@hot-loader/react-dom",
"react-google-recaptcha": "^2.0.1",
"react-hot-loader": "^4.12.18",
"react-i18next": "^11.2.1",
"react-redux": "^7.1.0",

View file

@ -6,9 +6,19 @@ export interface LoginResponse {
confirmationToken?: string;
}
export default (user: string, password: string): Promise<LoginResponse> => {
export interface LoginData {
username: string;
password: string;
recaptchaData?: string | null;
}
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
return new Promise((resolve, reject) => {
http.post('/auth/login', { user, password })
http.post('/auth/login', {
user: username,
password,
'g-recaptcha-response': recaptchaData,
})
.then(response => {
if (!(response.data instanceof Object)) {
return reject(new Error('An error occurred while processing the login request.'));

View file

@ -7,8 +7,10 @@ import DashboardRouter from '@/routers/DashboardRouter';
import ServerRouter from '@/routers/ServerRouter';
import AuthenticationRouter from '@/routers/AuthenticationRouter';
import { Provider } from 'react-redux';
import { SiteSettings } from '@/state/settings';
interface WindowWithUser extends Window {
interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings;
PterodactylUser?: {
uuid: string;
username: string;
@ -22,20 +24,24 @@ interface WindowWithUser extends Window {
}
const App = () => {
const data = (window as WindowWithUser).PterodactylUser;
if (data && !store.getState().user.data) {
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
if (PterodactylUser && !store.getState().user.data) {
store.getActions().user.setUserData({
uuid: data.uuid,
username: data.username,
email: data.email,
language: data.language,
rootAdmin: data.root_admin,
useTotp: data.use_totp,
createdAt: new Date(data.created_at),
updatedAt: new Date(data.updated_at),
uuid: PterodactylUser.uuid,
username: PterodactylUser.username,
email: PterodactylUser.email,
language: PterodactylUser.language,
rootAdmin: PterodactylUser.root_admin,
useTotp: PterodactylUser.use_totp,
createdAt: new Date(PterodactylUser.created_at),
updatedAt: new Date(PterodactylUser.updated_at),
});
}
if (!store.getState().settings.data) {
store.getActions().settings.setSettings(SiteConfiguration!);
}
return (
<StoreProvider store={store}>
<Provider store={store}>

View file

@ -1,32 +1,46 @@
import React from 'react';
import React, { useRef } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import login from '@/api/auth/login';
import login, { LoginData } from '@/api/auth/login';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { FormikProps, withFormik } from 'formik';
import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import { httpErrorToHuman } from '@/api/http';
interface Values {
username: string;
password: string;
}
import { FlashMessage } from '@/state/flashes';
import ReCAPTCHA from 'react-google-recaptcha';
type OwnProps = RouteComponentProps & {
clearFlashes: any;
addFlash: any;
clearFlashes: ActionCreator<void>;
addFlash: ActionCreator<FlashMessage>;
}
const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps<Values>) => (
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
const ref = useRef<ReCAPTCHA | null>(null);
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (ref.current && !values.recaptchaData) {
return ref.current.execute();
}
handleSubmit(e);
};
console.log(values.recaptchaData);
return (
<React.Fragment>
{ref.current && ref.current.render()}
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
Login to Continue
</h2>
<FlashMessageRender className={'mb-2'}/>
<LoginFormContainer>
<LoginFormContainer onSubmit={submit}>
<label htmlFor={'username'}>Username or Email</label>
<Field
type={'text'}
@ -55,6 +69,19 @@ const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps<Values>) => (
}
</button>
</div>
{recaptchaEnabled &&
<ReCAPTCHA
ref={ref}
size={'invisible'}
sitekey={siteKey || '_invalid_key'}
onChange={token => {
ref.current && ref.current.reset();
setFieldValue('recaptchaData', token);
submitForm();
}}
onExpired={() => setFieldValue('recaptchaData', null)}
/>
}
<div className={'mt-6 text-center'}>
<Link
to={'/auth/password'}
@ -66,13 +93,15 @@ const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps<Values>) => (
</LoginFormContainer>
</React.Fragment>
);
};
const EnhancedForm = withFormik<OwnProps, Values>({
const EnhancedForm = withFormik<OwnProps, LoginData>({
displayName: 'LoginContainerForm',
mapPropsToValues: (props) => ({
username: '',
password: '',
recaptchaData: null,
}),
validationSchema: () => object().shape({
@ -80,9 +109,9 @@ const EnhancedForm = withFormik<OwnProps, Values>({
password: string().required('Please enter your account password.'),
}),
handleSubmit: ({ username, password }, { props, setSubmitting }) => {
handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
props.clearFlashes();
login(username, password)
login(values)
.then(response => {
if (response.complete) {
// @ts-ignore
@ -96,6 +125,7 @@ const EnhancedForm = withFormik<OwnProps, Values>({
console.error(error);
setSubmitting(false);
setFieldValue('recaptchaData', null);
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
});
},

View file

@ -1,8 +1,10 @@
import * as React from 'react';
import { Form } from 'formik';
import React, { forwardRef } from 'react';
export default ({ className, ...props }: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>) => (
<Form
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>;
export default forwardRef<any, Props>(({ className, ...props }, ref) => (
<form
ref={ref}
className={'flex items-center justify-center login-box'}
{...props}
style={{
@ -15,5 +17,5 @@ export default ({ className, ...props }: React.DetailedHTMLProps<React.FormHTMLA
<div className={'flex-1'}>
{props.children}
</div>
</Form>
);
</form>
));

View file

@ -2,17 +2,20 @@ import { createStore } from 'easy-peasy';
import flashes, { FlashStore } from '@/state/flashes';
import user, { UserStore } from '@/state/user';
import permissions, { GloablPermissionsStore } from '@/state/permissions';
import settings, { SettingsStore } from '@/state/settings';
export interface ApplicationStore {
permissions: GloablPermissionsStore;
flashes: FlashStore;
user: UserStore;
settings: SettingsStore;
}
const state: ApplicationStore = {
permissions,
flashes,
user,
settings,
};
export const store = createStore(state);

View file

@ -0,0 +1,25 @@
import { action, Action } from 'easy-peasy';
export interface SiteSettings {
name: string;
locale: string;
recaptcha: {
enabled: boolean;
siteKey: string;
};
}
export interface SettingsStore {
data?: SiteSettings;
setSettings: Action<SettingsStore, SiteSettings>;
}
const settings: SettingsStore = {
data: undefined,
setSettings: action((state, payload) => {
state.data = payload;
}),
};
export default settings;

View file

@ -21,7 +21,12 @@
@section('user-data')
@if(!is_null(Auth::user()))
<script>
window.PterodactylUser = {!! json_encode(Auth::user()->toVueObject()) !!}
window.PterodactylUser = {!! json_encode(Auth::user()->toVueObject()) !!};
</script>
@endif
@if(!empty($siteConfiguration))
<script>
window.SiteConfiguration = {!! json_encode($siteConfiguration) !!};
</script>
@endif
@show

View file

@ -800,6 +800,12 @@
dependencies:
"@types/react" "*"
"@types/react-google-recaptcha@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea"
dependencies:
"@types/react" "*"
"@types/react-native@*":
version "0.60.2"
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.2.tgz#2dca78481a904419c2a5907288dd97d1090c6e3c"
@ -5803,7 +5809,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
dependencies:
@ -5956,6 +5962,13 @@ rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-async-script@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf"
dependencies:
hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0"
"react-dom@npm:@hot-loader/react-dom":
version "16.11.0"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd"
@ -5969,6 +5982,13 @@ react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
react-google-recaptcha@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8"
dependencies:
prop-types "^15.5.0"
react-async-script "^1.1.1"
react-hot-loader@^4.12.18:
version "4.12.18"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.18.tgz#a9029e34af2690d76208f9a35189d73c2dfea6a7"