Fix recaptcha on login forms
This commit is contained in:
parent
f864b72e0a
commit
66410a35f1
10 changed files with 188 additions and 77 deletions
|
@ -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') ?? '',
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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.'));
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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) });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
);
|
));
|
||||||
|
|
|
@ -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);
|
||||||
|
|
25
resources/scripts/state/settings.ts
Normal file
25
resources/scripts/state/settings.ts
Normal 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;
|
|
@ -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
|
||||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue