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) public function compose(View $view)
{ {
$view->with('asset', $this->assetHashService); $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/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "^0.1.4", "@fortawesome/react-fontawesome": "^0.1.4",
"@types/react-google-recaptcha": "^1.1.1",
"axios": "^0.19.0", "axios": "^0.19.0",
"ayu-ace": "^2.0.4", "ayu-ace": "^2.0.4",
"brace": "^0.11.1", "brace": "^0.11.1",
@ -23,6 +24,7 @@
"query-string": "^6.7.0", "query-string": "^6.7.0",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "npm:@hot-loader/react-dom", "react-dom": "npm:@hot-loader/react-dom",
"react-google-recaptcha": "^2.0.1",
"react-hot-loader": "^4.12.18", "react-hot-loader": "^4.12.18",
"react-i18next": "^11.2.1", "react-i18next": "^11.2.1",
"react-redux": "^7.1.0", "react-redux": "^7.1.0",

View file

@ -6,9 +6,19 @@ export interface LoginResponse {
confirmationToken?: string; 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) => { return new Promise((resolve, reject) => {
http.post('/auth/login', { user, password }) http.post('/auth/login', {
user: username,
password,
'g-recaptcha-response': recaptchaData,
})
.then(response => { .then(response => {
if (!(response.data instanceof Object)) { if (!(response.data instanceof Object)) {
return reject(new Error('An error occurred while processing the login request.')); 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 ServerRouter from '@/routers/ServerRouter';
import AuthenticationRouter from '@/routers/AuthenticationRouter'; import AuthenticationRouter from '@/routers/AuthenticationRouter';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { SiteSettings } from '@/state/settings';
interface WindowWithUser extends Window { interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings;
PterodactylUser?: { PterodactylUser?: {
uuid: string; uuid: string;
username: string; username: string;
@ -22,20 +24,24 @@ interface WindowWithUser extends Window {
} }
const App = () => { const App = () => {
const data = (window as WindowWithUser).PterodactylUser; const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
if (data && !store.getState().user.data) { if (PterodactylUser && !store.getState().user.data) {
store.getActions().user.setUserData({ store.getActions().user.setUserData({
uuid: data.uuid, uuid: PterodactylUser.uuid,
username: data.username, username: PterodactylUser.username,
email: data.email, email: PterodactylUser.email,
language: data.language, language: PterodactylUser.language,
rootAdmin: data.root_admin, rootAdmin: PterodactylUser.root_admin,
useTotp: data.use_totp, useTotp: PterodactylUser.use_totp,
createdAt: new Date(data.created_at), createdAt: new Date(PterodactylUser.created_at),
updatedAt: new Date(data.updated_at), updatedAt: new Date(PterodactylUser.updated_at),
}); });
} }
if (!store.getState().settings.data) {
store.getActions().settings.setSettings(SiteConfiguration!);
}
return ( return (
<StoreProvider store={store}> <StoreProvider store={store}>
<Provider 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 { 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 LoginFormContainer from '@/components/auth/LoginFormContainer';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy'; import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { FormikProps, withFormik } from 'formik'; import { FormikProps, withFormik } from 'formik';
import { object, string } from 'yup'; import { object, string } from 'yup';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import { FlashMessage } from '@/state/flashes';
interface Values { import ReCAPTCHA from 'react-google-recaptcha';
username: string;
password: string;
}
type OwnProps = RouteComponentProps & { type OwnProps = RouteComponentProps & {
clearFlashes: any; clearFlashes: ActionCreator<void>;
addFlash: any; 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> <React.Fragment>
{ref.current && ref.current.render()}
<h2 className={'text-center text-neutral-100 font-medium py-4'}> <h2 className={'text-center text-neutral-100 font-medium py-4'}>
Login to Continue Login to Continue
</h2> </h2>
<FlashMessageRender className={'mb-2'}/> <FlashMessageRender className={'mb-2'}/>
<LoginFormContainer> <LoginFormContainer onSubmit={submit}>
<label htmlFor={'username'}>Username or Email</label> <label htmlFor={'username'}>Username or Email</label>
<Field <Field
type={'text'} type={'text'}
@ -55,6 +69,19 @@ const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps<Values>) => (
} }
</button> </button>
</div> </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'}> <div className={'mt-6 text-center'}>
<Link <Link
to={'/auth/password'} to={'/auth/password'}
@ -66,13 +93,15 @@ const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps<Values>) => (
</LoginFormContainer> </LoginFormContainer>
</React.Fragment> </React.Fragment>
); );
};
const EnhancedForm = withFormik<OwnProps, Values>({ const EnhancedForm = withFormik<OwnProps, LoginData>({
displayName: 'LoginContainerForm', displayName: 'LoginContainerForm',
mapPropsToValues: (props) => ({ mapPropsToValues: (props) => ({
username: '', username: '',
password: '', password: '',
recaptchaData: null,
}), }),
validationSchema: () => object().shape({ validationSchema: () => object().shape({
@ -80,9 +109,9 @@ const EnhancedForm = withFormik<OwnProps, Values>({
password: string().required('Please enter your account password.'), password: string().required('Please enter your account password.'),
}), }),
handleSubmit: ({ username, password }, { props, setSubmitting }) => { handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
props.clearFlashes(); props.clearFlashes();
login(username, password) login(values)
.then(response => { .then(response => {
if (response.complete) { if (response.complete) {
// @ts-ignore // @ts-ignore
@ -96,6 +125,7 @@ const EnhancedForm = withFormik<OwnProps, Values>({
console.error(error); console.error(error);
setSubmitting(false); setSubmitting(false);
setFieldValue('recaptchaData', null);
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
}); });
}, },

View file

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

View file

@ -2,17 +2,20 @@ import { createStore } from 'easy-peasy';
import flashes, { FlashStore } from '@/state/flashes'; import flashes, { FlashStore } from '@/state/flashes';
import user, { UserStore } from '@/state/user'; import user, { UserStore } from '@/state/user';
import permissions, { GloablPermissionsStore } from '@/state/permissions'; import permissions, { GloablPermissionsStore } from '@/state/permissions';
import settings, { SettingsStore } from '@/state/settings';
export interface ApplicationStore { export interface ApplicationStore {
permissions: GloablPermissionsStore; permissions: GloablPermissionsStore;
flashes: FlashStore; flashes: FlashStore;
user: UserStore; user: UserStore;
settings: SettingsStore;
} }
const state: ApplicationStore = { const state: ApplicationStore = {
permissions, permissions,
flashes, flashes,
user, user,
settings,
}; };
export const store = createStore(state); 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') @section('user-data')
@if(!is_null(Auth::user())) @if(!is_null(Auth::user()))
<script> <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> </script>
@endif @endif
@show @show

View file

@ -800,6 +800,12 @@
dependencies: dependencies:
"@types/react" "*" "@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@*": "@types/react-native@*":
version "0.60.2" version "0.60.2"
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.2.tgz#2dca78481a904419c2a5907288dd97d1090c6e3c" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.2.tgz#2dca78481a904419c2a5907288dd97d1090c6e3c"
@ -5803,7 +5809,7 @@ promise@^7.1.1:
dependencies: dependencies:
asap "~2.0.3" 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" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
dependencies: dependencies:
@ -5956,6 +5962,13 @@ rc@^1.1.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" 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": "react-dom@npm:@hot-loader/react-dom":
version "16.11.0" version "16.11.0"
resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd" 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" version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" 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: react-hot-loader@^4.12.18:
version "4.12.18" version "4.12.18"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.18.tgz#a9029e34af2690d76208f9a35189d73c2dfea6a7" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.18.tgz#a9029e34af2690d76208f9a35189d73c2dfea6a7"