diff --git a/app/Http/ViewComposers/AssetComposer.php b/app/Http/ViewComposers/AssetComposer.php index 41e5f7e10..7e8f82dbc 100644 --- a/app/Http/ViewComposers/AssetComposer.php +++ b/app/Http/ViewComposers/AssetComposer.php @@ -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') ?? '', + ], + ]); } } diff --git a/package.json b/package.json index b168221ae..e53ac2a13 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/scripts/api/auth/login.ts b/resources/scripts/api/auth/login.ts index 2bbdfe5a0..af2f5faa3 100644 --- a/resources/scripts/api/auth/login.ts +++ b/resources/scripts/api/auth/login.ts @@ -6,9 +6,19 @@ export interface LoginResponse { confirmationToken?: string; } -export default (user: string, password: string): Promise => { +export interface LoginData { + username: string; + password: string; + recaptchaData?: string | null; +} + +export default ({ username, password, recaptchaData }: LoginData): Promise => { 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.')); diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index 98650be64..bb16b7bc8 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -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 ( diff --git a/resources/scripts/components/auth/LoginContainer.tsx b/resources/scripts/components/auth/LoginContainer.tsx index ca988484b..ed543d6d5 100644 --- a/resources/scripts/components/auth/LoginContainer.tsx +++ b/resources/scripts/components/auth/LoginContainer.tsx @@ -1,78 +1,107 @@ -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; + addFlash: ActionCreator; } -const LoginContainer = ({ isSubmitting }: OwnProps & FormikProps) => ( - -

- Login to Continue -

- - - - -
- +const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps) => { + const ref = useRef(null); + const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + + if (ref.current && !values.recaptchaData) { + return ref.current.execute(); + } + + handleSubmit(e); + }; + + console.log(values.recaptchaData); + + return ( + + {ref.current && ref.current.render()} +

+ Login to Continue +

+ + + -
-
- -
-
- - Forgot password? - -
-
-
-); +
+ + +
+
+ +
+ {recaptchaEnabled && + { + ref.current && ref.current.reset(); + setFieldValue('recaptchaData', token); + submitForm(); + }} + onExpired={() => setFieldValue('recaptchaData', null)} + /> + } +
+ + Forgot password? + +
+ + + ); +}; -const EnhancedForm = withFormik({ +const EnhancedForm = withFormik({ displayName: 'LoginContainerForm', mapPropsToValues: (props) => ({ username: '', password: '', + recaptchaData: null, }), validationSchema: () => object().shape({ @@ -80,9 +109,9 @@ const EnhancedForm = withFormik({ 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({ console.error(error); setSubmitting(false); + setFieldValue('recaptchaData', null); props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); }); }, diff --git a/resources/scripts/components/auth/LoginFormContainer.tsx b/resources/scripts/components/auth/LoginFormContainer.tsx index 2caa15784..2144cb56a 100644 --- a/resources/scripts/components/auth/LoginFormContainer.tsx +++ b/resources/scripts/components/auth/LoginFormContainer.tsx @@ -1,8 +1,10 @@ -import * as React from 'react'; -import { Form } from 'formik'; +import React, { forwardRef } from 'react'; -export default ({ className, ...props }: React.DetailedHTMLProps, HTMLFormElement>) => ( -
, HTMLFormElement>; + +export default forwardRef(({ className, ...props }, ref) => ( + {props.children} - -); + +)); diff --git a/resources/scripts/state/index.ts b/resources/scripts/state/index.ts index 0a9f7e6f7..ee66d5478 100644 --- a/resources/scripts/state/index.ts +++ b/resources/scripts/state/index.ts @@ -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); diff --git a/resources/scripts/state/settings.ts b/resources/scripts/state/settings.ts new file mode 100644 index 000000000..20dbbdc6e --- /dev/null +++ b/resources/scripts/state/settings.ts @@ -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; +} + +const settings: SettingsStore = { + data: undefined, + + setSettings: action((state, payload) => { + state.data = payload; + }), +}; + +export default settings; diff --git a/resources/views/templates/wrapper.blade.php b/resources/views/templates/wrapper.blade.php index cbfa5bfc0..81fbb5c08 100644 --- a/resources/views/templates/wrapper.blade.php +++ b/resources/views/templates/wrapper.blade.php @@ -21,7 +21,12 @@ @section('user-data') @if(!is_null(Auth::user())) + @endif + @if(!empty($siteConfiguration)) + @endif @show diff --git a/yarn.lock b/yarn.lock index c9556c9e5..9ba396b2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"