Skip to content

Commit 66410a3

Browse files
committed
Fix recaptcha on login forms
1 parent f864b72 commit 66410a3

File tree

10 files changed

+188
-77
lines changed

10 files changed

+188
-77
lines changed

app/Http/ViewComposers/AssetComposer.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,13 @@ public function __construct(AssetHashService $assetHashService)
3030
public function compose(View $view)
3131
{
3232
$view->with('asset', $this->assetHashService);
33+
$view->with('siteConfiguration', [
34+
'name' => config('app.name') ?? 'Pterodactyl',
35+
'locale' => config('app.locale') ?? 'en',
36+
'recaptcha' => [
37+
'enabled' => config('recaptcha.enabled', false),
38+
'siteKey' => config('recaptcha.website_key') ?? '',
39+
],
40+
]);
3341
}
3442
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"@fortawesome/fontawesome-svg-core": "^1.2.19",
55
"@fortawesome/free-solid-svg-icons": "^5.9.0",
66
"@fortawesome/react-fontawesome": "^0.1.4",
7+
"@types/react-google-recaptcha": "^1.1.1",
78
"axios": "^0.19.0",
89
"ayu-ace": "^2.0.4",
910
"brace": "^0.11.1",
@@ -23,6 +24,7 @@
2324
"query-string": "^6.7.0",
2425
"react": "^16.12.0",
2526
"react-dom": "npm:@hot-loader/react-dom",
27+
"react-google-recaptcha": "^2.0.1",
2628
"react-hot-loader": "^4.12.18",
2729
"react-i18next": "^11.2.1",
2830
"react-redux": "^7.1.0",

resources/scripts/api/auth/login.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@ export interface LoginResponse {
66
confirmationToken?: string;
77
}
88

9-
export default (user: string, password: string): Promise<LoginResponse> => {
9+
export interface LoginData {
10+
username: string;
11+
password: string;
12+
recaptchaData?: string | null;
13+
}
14+
15+
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
1016
return new Promise((resolve, reject) => {
11-
http.post('/auth/login', { user, password })
17+
http.post('/auth/login', {
18+
user: username,
19+
password,
20+
'g-recaptcha-response': recaptchaData,
21+
})
1222
.then(response => {
1323
if (!(response.data instanceof Object)) {
1424
return reject(new Error('An error occurred while processing the login request.'));

resources/scripts/components/App.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import DashboardRouter from '@/routers/DashboardRouter';
77
import ServerRouter from '@/routers/ServerRouter';
88
import AuthenticationRouter from '@/routers/AuthenticationRouter';
99
import { Provider } from 'react-redux';
10+
import { SiteSettings } from '@/state/settings';
1011

11-
interface WindowWithUser extends Window {
12+
interface ExtendedWindow extends Window {
13+
SiteConfiguration?: SiteSettings;
1214
PterodactylUser?: {
1315
uuid: string;
1416
username: string;
@@ -22,20 +24,24 @@ interface WindowWithUser extends Window {
2224
}
2325

2426
const App = () => {
25-
const data = (window as WindowWithUser).PterodactylUser;
26-
if (data && !store.getState().user.data) {
27+
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
28+
if (PterodactylUser && !store.getState().user.data) {
2729
store.getActions().user.setUserData({
28-
uuid: data.uuid,
29-
username: data.username,
30-
email: data.email,
31-
language: data.language,
32-
rootAdmin: data.root_admin,
33-
useTotp: data.use_totp,
34-
createdAt: new Date(data.created_at),
35-
updatedAt: new Date(data.updated_at),
30+
uuid: PterodactylUser.uuid,
31+
username: PterodactylUser.username,
32+
email: PterodactylUser.email,
33+
language: PterodactylUser.language,
34+
rootAdmin: PterodactylUser.root_admin,
35+
useTotp: PterodactylUser.use_totp,
36+
createdAt: new Date(PterodactylUser.created_at),
37+
updatedAt: new Date(PterodactylUser.updated_at),
3638
});
3739
}
3840

41+
if (!store.getState().settings.data) {
42+
store.getActions().settings.setSettings(SiteConfiguration!);
43+
}
44+
3945
return (
4046
<StoreProvider store={store}>
4147
<Provider store={store}>

resources/scripts/components/auth/LoginContainer.tsx

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,117 @@
1-
import React from 'react';
1+
import React, { useRef } from 'react';
22
import { Link, RouteComponentProps } from 'react-router-dom';
3-
import login from '@/api/auth/login';
3+
import login, { LoginData } from '@/api/auth/login';
44
import LoginFormContainer from '@/components/auth/LoginFormContainer';
55
import FlashMessageRender from '@/components/FlashMessageRender';
6-
import { Actions, useStoreActions } from 'easy-peasy';
6+
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
77
import { ApplicationStore } from '@/state';
88
import { FormikProps, withFormik } from 'formik';
99
import { object, string } from 'yup';
1010
import Field from '@/components/elements/Field';
1111
import { httpErrorToHuman } from '@/api/http';
12-
13-
interface Values {
14-
username: string;
15-
password: string;
16-
}
12+
import { FlashMessage } from '@/state/flashes';
13+
import ReCAPTCHA from 'react-google-recaptcha';
1714

1815
type OwnProps = RouteComponentProps & {
19-
clearFlashes: any;
20-
addFlash: any;
16+
clearFlashes: ActionCreator<void>;
17+
addFlash: ActionCreator<FlashMessage>;
2118
}
2219

23-
const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps<Values>) => (
24-
<React.Fragment>
25-
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
26-
Login to Continue
27-
</h2>
28-
<FlashMessageRender className={'mb-2'}/>
29-
<LoginFormContainer>
30-
<label htmlFor={'username'}>Username or Email</label>
31-
<Field
32-
type={'text'}
33-
id={'username'}
34-
name={'username'}
35-
className={'input'}
36-
/>
37-
<div className={'mt-6'}>
38-
<label htmlFor={'password'}>Password</label>
20+
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
21+
const ref = useRef<ReCAPTCHA | null>(null);
22+
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
23+
24+
const submit = (e: React.FormEvent<HTMLFormElement>) => {
25+
e.preventDefault();
26+
27+
if (ref.current && !values.recaptchaData) {
28+
return ref.current.execute();
29+
}
30+
31+
handleSubmit(e);
32+
};
33+
34+
console.log(values.recaptchaData);
35+
36+
return (
37+
<React.Fragment>
38+
{ref.current && ref.current.render()}
39+
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
40+
Login to Continue
41+
</h2>
42+
<FlashMessageRender className={'mb-2'}/>
43+
<LoginFormContainer onSubmit={submit}>
44+
<label htmlFor={'username'}>Username or Email</label>
3945
<Field
40-
type={'password'}
41-
id={'password'}
42-
name={'password'}
46+
type={'text'}
47+
id={'username'}
48+
name={'username'}
4349
className={'input'}
4450
/>
45-
</div>
46-
<div className={'mt-6'}>
47-
<button
48-
type={'submit'}
49-
className={'btn btn-primary btn-jumbo'}
50-
>
51-
{isSubmitting ?
52-
<span className={'spinner white'}>&nbsp;</span>
53-
:
54-
'Login'
55-
}
56-
</button>
57-
</div>
58-
<div className={'mt-6 text-center'}>
59-
<Link
60-
to={'/auth/password'}
61-
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
62-
>
63-
Forgot password?
64-
</Link>
65-
</div>
66-
</LoginFormContainer>
67-
</React.Fragment>
68-
);
51+
<div className={'mt-6'}>
52+
<label htmlFor={'password'}>Password</label>
53+
<Field
54+
type={'password'}
55+
id={'password'}
56+
name={'password'}
57+
className={'input'}
58+
/>
59+
</div>
60+
<div className={'mt-6'}>
61+
<button
62+
type={'submit'}
63+
className={'btn btn-primary btn-jumbo'}
64+
>
65+
{isSubmitting ?
66+
<span className={'spinner white'}>&nbsp;</span>
67+
:
68+
'Login'
69+
}
70+
</button>
71+
</div>
72+
{recaptchaEnabled &&
73+
<ReCAPTCHA
74+
ref={ref}
75+
size={'invisible'}
76+
sitekey={siteKey || '_invalid_key'}
77+
onChange={token => {
78+
ref.current && ref.current.reset();
79+
setFieldValue('recaptchaData', token);
80+
submitForm();
81+
}}
82+
onExpired={() => setFieldValue('recaptchaData', null)}
83+
/>
84+
}
85+
<div className={'mt-6 text-center'}>
86+
<Link
87+
to={'/auth/password'}
88+
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
89+
>
90+
Forgot password?
91+
</Link>
92+
</div>
93+
</LoginFormContainer>
94+
</React.Fragment>
95+
);
96+
};
6997

70-
const EnhancedForm = withFormik<OwnProps, Values>({
98+
const EnhancedForm = withFormik<OwnProps, LoginData>({
7199
displayName: 'LoginContainerForm',
72100

73101
mapPropsToValues: (props) => ({
74102
username: '',
75103
password: '',
104+
recaptchaData: null,
76105
}),
77106

78107
validationSchema: () => object().shape({
79108
username: string().required('A username or email must be provided.'),
80109
password: string().required('Please enter your account password.'),
81110
}),
82111

83-
handleSubmit: ({ username, password }, { props, setSubmitting }) => {
112+
handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
84113
props.clearFlashes();
85-
login(username, password)
114+
login(values)
86115
.then(response => {
87116
if (response.complete) {
88117
// @ts-ignore
@@ -96,6 +125,7 @@ const EnhancedForm = withFormik<OwnProps, Values>({
96125
console.error(error);
97126

98127
setSubmitting(false);
128+
setFieldValue('recaptchaData', null);
99129
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
100130
});
101131
},
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import * as React from 'react';
2-
import { Form } from 'formik';
1+
import React, { forwardRef } from 'react';
32

4-
export default ({ className, ...props }: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>) => (
5-
<Form
3+
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>;
4+
5+
export default forwardRef<any, Props>(({ className, ...props }, ref) => (
6+
<form
7+
ref={ref}
68
className={'flex items-center justify-center login-box'}
79
{...props}
810
style={{
@@ -15,5 +17,5 @@ export default ({ className, ...props }: React.DetailedHTMLProps<React.FormHTMLA
1517
<div className={'flex-1'}>
1618
{props.children}
1719
</div>
18-
</Form>
19-
);
20+
</form>
21+
));

resources/scripts/state/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import { createStore } from 'easy-peasy';
22
import flashes, { FlashStore } from '@/state/flashes';
33
import user, { UserStore } from '@/state/user';
44
import permissions, { GloablPermissionsStore } from '@/state/permissions';
5+
import settings, { SettingsStore } from '@/state/settings';
56

67
export interface ApplicationStore {
78
permissions: GloablPermissionsStore;
89
flashes: FlashStore;
910
user: UserStore;
11+
settings: SettingsStore;
1012
}
1113

1214
const state: ApplicationStore = {
1315
permissions,
1416
flashes,
1517
user,
18+
settings,
1619
};
1720

1821
export const store = createStore(state);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { action, Action } from 'easy-peasy';
2+
3+
export interface SiteSettings {
4+
name: string;
5+
locale: string;
6+
recaptcha: {
7+
enabled: boolean;
8+
siteKey: string;
9+
};
10+
}
11+
12+
export interface SettingsStore {
13+
data?: SiteSettings;
14+
setSettings: Action<SettingsStore, SiteSettings>;
15+
}
16+
17+
const settings: SettingsStore = {
18+
data: undefined,
19+
20+
setSettings: action((state, payload) => {
21+
state.data = payload;
22+
}),
23+
};
24+
25+
export default settings;

resources/views/templates/wrapper.blade.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
@section('user-data')
2222
@if(!is_null(Auth::user()))
2323
<script>
24-
window.PterodactylUser = {!! json_encode(Auth::user()->toVueObject()) !!}
24+
window.PterodactylUser = {!! json_encode(Auth::user()->toVueObject()) !!};
25+
</script>
26+
@endif
27+
@if(!empty($siteConfiguration))
28+
<script>
29+
window.SiteConfiguration = {!! json_encode($siteConfiguration) !!};
2530
</script>
2631
@endif
2732
@show

0 commit comments

Comments
 (0)