From d9f30294ded67ef57b1ec2cf519f1bbac3e8fe18 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 9 Jun 2019 19:26:20 -0700 Subject: [PATCH] Migrate the existing login form to use React --- package.json | 5 + .../assets/styles/components/animations.css | 71 ++--------- resources/scripts/api/auth/login.ts | 25 ++++ resources/scripts/api/http.ts | 42 +++++++ resources/scripts/components/App.tsx | 9 +- resources/scripts/components/MessageBox.tsx | 14 +++ .../components/auth/LoginContainer.tsx | 111 ++++++++++++++++++ .../components/forms/OpenInputField.tsx | 30 +++++ .../scripts/routers/AuthenticationRouter.tsx | 26 ++++ .../pterodactyl/templates/auth/core.blade.php | 5 +- .../pterodactyl/templates/base/core.blade.php | 2 +- .../pterodactyl/templates/wrapper.blade.php | 4 +- routes/base.php | 4 +- tsconfig.json | 1 + yarn.lock | 45 ++++++- 15 files changed, 322 insertions(+), 72 deletions(-) create mode 100644 resources/scripts/api/auth/login.ts create mode 100644 resources/scripts/api/http.ts create mode 100644 resources/scripts/components/MessageBox.tsx create mode 100644 resources/scripts/components/auth/LoginContainer.tsx create mode 100644 resources/scripts/components/forms/OpenInputField.tsx create mode 100644 resources/scripts/routers/AuthenticationRouter.tsx diff --git a/package.json b/package.json index c5c9a3570..d182b39b0 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "@hot-loader/react-dom": "^16.8.6", "axios": "^0.18.0", "brace": "^0.11.1", + "classnames": "^2.2.6", "date-fns": "^1.29.0", "feather-icons": "^4.10.0", "jquery": "^3.3.1", @@ -12,6 +13,7 @@ "react-dom": "^16.8.6", "react-hot-loader": "^4.9.0", "react-router-dom": "^5.0.1", + "react-transition-group": "^4.1.0", "redux": "^4.0.1", "socket.io-client": "^2.2.0", "ws-wrapper": "^2.0.0", @@ -22,10 +24,13 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/preset-env": "^7.3.1", "@babel/preset-react": "^7.0.0", + "@types/classnames": "^2.2.8", "@types/feather-icons": "^4.7.0", "@types/lodash": "^4.14.119", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.4", + "@types/react-router-dom": "^4.3.3", + "@types/react-transition-group": "^2.9.2", "@types/webpack-env": "^1.13.6", "@typescript-eslint/eslint-plugin": "^1.10.1", "@typescript-eslint/parser": "^1.10.1", diff --git a/resources/assets/styles/components/animations.css b/resources/assets/styles/components/animations.css index 25b3985a4..d76279725 100644 --- a/resources/assets/styles/components/animations.css +++ b/resources/assets/styles/components/animations.css @@ -1,68 +1,19 @@ -.animate { - &.fadein { - animation: fadein 500ms; - } -} - -.animated-fade-in { - animation: fadein 500ms; +/*! purgecss start ignore */ +.fade-enter { + @apply .opacity-0; } .fade-enter-active { - animation: fadein 500ms; + @apply .opacity-100; + transition: opacity 150ms; } -.fade-leave-active { - animation: fadein 500ms reverse; +.fade-exit { + @apply .opacity-100; } -@keyframes fadein { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes onlineblink { - 0% { - @apply .bg-green-500; - } - 100% { - @apply .bg-green-600; - } -} - -@keyframes offlineblink { - 0% { - @apply .bg-red-500; - } - 100% { - @apply .bg-red-600; - } -} - -/* - * transition="modal" - */ -.modal-enter, .modal-leave-active { - opacity: 0; -} - -.modal-enter .modal-container, -.modal-leave-active .modal-container { - animation: opacity 250ms linear; -} - -/** - * name="slide-fade" mode="out-in" - */ -.slide-fade-enter-active { - transition: all 250ms ease; -} - -.slide-fade-leave-active { - transition: all 250ms cubic-bezier(1.0, 0.5, 0.8, 1.0); -} - -.slide-fade-enter, .slide-fade-leave-to { - transform: translateX(10px); - opacity: 0; +.fade-exit-active { + @apply .opacity-0; + transition: opacity 150ms; } +/*! purgecss end ignore */ diff --git a/resources/scripts/api/auth/login.ts b/resources/scripts/api/auth/login.ts new file mode 100644 index 000000000..742c0b17c --- /dev/null +++ b/resources/scripts/api/auth/login.ts @@ -0,0 +1,25 @@ +import http from '@/api/http'; + +interface LoginResponse { + complete: boolean; + intended?: string; + token?: string; +} + +export default (user: string, password: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/auth/login', { user, password }) + .then(response => { + if (!(response.data instanceof Object)) { + return reject(new Error('An error occurred while processing the login request.')); + } + + return resolve({ + complete: response.data.complete, + intended: response.data.intended || undefined, + token: response.data.token || undefined, + }); + }) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/http.ts b/resources/scripts/api/http.ts new file mode 100644 index 000000000..1c8359d97 --- /dev/null +++ b/resources/scripts/api/http.ts @@ -0,0 +1,42 @@ +import axios, { AxiosInstance } from 'axios'; + +// This token is set in the bootstrap.js file at the beginning of the request +// and is carried through from there. +// const token: string = ''; + +const http: AxiosInstance = axios.create({ + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, +}); + +// If we have a phpdebugbar instance registered at this point in time go +// ahead and route the response data through to it so things show up. +// @ts-ignore +if (typeof window.phpdebugbar !== 'undefined') { + http.interceptors.response.use(response => { + // @ts-ignore + window.phpdebugbar.ajaxHandler.handle(response.request); + + return response; + }); +} + +export default http; + +/** + * Converts an error into a human readable response. Mostly just a generic helper to + * make sure we display the message from the server back to the user if we can. + */ +export function httpErrorToHuman (error: any): string { + if (error.response && error.response.data) { + const { data } = error.response; + if (data.errors && data.errors[0] && data.errors[0].detail) { + return data.errors[0].detail; + } + } + + return error.message; +} diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index a0b85c996..69f482e7d 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -1,10 +1,17 @@ import * as React from 'react'; import { hot } from 'react-hot-loader/root'; +import { BrowserRouter as Router, Route } from 'react-router-dom'; +import AuthenticationRouter from '@/routers/AuthenticationRouter'; class App extends React.PureComponent { render () { return ( -

Hello

+ +
+ + +
+
); } } diff --git a/resources/scripts/components/MessageBox.tsx b/resources/scripts/components/MessageBox.tsx new file mode 100644 index 000000000..8ebf11553 --- /dev/null +++ b/resources/scripts/components/MessageBox.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +interface Props { + title?: string; + message: string; + type?: 'success' | 'info' | 'warning' | 'error'; +} + +export default ({ title, message, type }: Props) => ( +
+ {title && {title}} + {message} +
+); diff --git a/resources/scripts/components/auth/LoginContainer.tsx b/resources/scripts/components/auth/LoginContainer.tsx new file mode 100644 index 000000000..db0b947fa --- /dev/null +++ b/resources/scripts/components/auth/LoginContainer.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import OpenInputField from '@/components/forms/OpenInputField'; +import { Link } from 'react-router-dom'; +import login from '@/api/auth/login'; +import { httpErrorToHuman } from '@/api/http'; +import MessageBox from '@/components/MessageBox'; + +type State = Readonly<{ + errorMessage?: string; + isLoading: boolean; + username?: string; + password?: string; +}>; + +export default class LoginContainer extends React.PureComponent<{}, State> { + username = React.createRef(); + + state: State = { + isLoading: false, + }; + + submit = (e: React.FormEvent) => { + e.preventDefault(); + + const { username, password } = this.state; + + this.setState({ isLoading: true }, () => { + login(username!, password!) + .then(response => { + + }) + .catch(error => this.setState({ + isLoading: false, + errorMessage: httpErrorToHuman(error), + }, () => console.error(error))); + }); + }; + + canSubmit () { + if (!this.state.username || !this.state.password) { + return false; + } + + return this.state.username.length > 0 && this.state.password.length > 0; + } + + // @ts-ignore + handleFieldUpdate = (e: React.ChangeEvent) => this.setState({ + [e.target.id]: e.target.value, + }); + + render () { + return ( + + {this.state.errorMessage && +
+ +
+ } +
+
+ +
+
+ +
+
+ +
+
+ + Forgot password? + +
+
+
+ ); + } +} diff --git a/resources/scripts/components/forms/OpenInputField.tsx b/resources/scripts/components/forms/OpenInputField.tsx new file mode 100644 index 000000000..0f7e2603b --- /dev/null +++ b/resources/scripts/components/forms/OpenInputField.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import classNames from 'classnames'; + +type Props = React.InputHTMLAttributes & { + label: string; +}; + +export default ({ className, onChange, label, ...props }: Props) => { + const [ value, setValue ] = React.useState(''); + + const classes = classNames('input open-label', { + 'has-content': value && value.length > 0, + }); + + return ( +
+ { + setValue(e.target.value); + if (onChange) { + onChange(e); + } + }} + {...props} + /> + +
+ ); +}; diff --git a/resources/scripts/routers/AuthenticationRouter.tsx b/resources/scripts/routers/AuthenticationRouter.tsx new file mode 100644 index 000000000..45de3165c --- /dev/null +++ b/resources/scripts/routers/AuthenticationRouter.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { BrowserRouter, Route, Switch } from 'react-router-dom'; +import LoginContainer from '@/components/auth/LoginContainer'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +export default class AuthenticationRouter extends React.PureComponent { + render () { + return ( + + ( + + + + + + + + + + )} + /> + + ); + } +} diff --git a/resources/themes/pterodactyl/templates/auth/core.blade.php b/resources/themes/pterodactyl/templates/auth/core.blade.php index 988509c3d..da27fcc95 100644 --- a/resources/themes/pterodactyl/templates/auth/core.blade.php +++ b/resources/themes/pterodactyl/templates/auth/core.blade.php @@ -4,10 +4,7 @@ @section('container')
- - +

{!! trans('strings.copyright', ['year' => date('Y')]) !!}

diff --git a/resources/themes/pterodactyl/templates/base/core.blade.php b/resources/themes/pterodactyl/templates/base/core.blade.php index bf3b37c1a..6fdb68687 100644 --- a/resources/themes/pterodactyl/templates/base/core.blade.php +++ b/resources/themes/pterodactyl/templates/base/core.blade.php @@ -1,7 +1,7 @@ @extends('templates/wrapper') @section('container') - +
@endsection @section('below-container') diff --git a/resources/themes/pterodactyl/templates/wrapper.blade.php b/resources/themes/pterodactyl/templates/wrapper.blade.php index 4424038d2..cbfa5bfc0 100644 --- a/resources/themes/pterodactyl/templates/wrapper.blade.php +++ b/resources/themes/pterodactyl/templates/wrapper.blade.php @@ -35,9 +35,7 @@ @section('content') @yield('above-container') -
- @yield('container') -
+ @yield('container') @yield('below-container') @show @section('scripts') diff --git a/routes/base.php b/routes/base.php index dbdad9712..e90fa7ef8 100644 --- a/routes/base.php +++ b/routes/base.php @@ -32,5 +32,5 @@ Route::group(['prefix' => 'account/two_factor'], function () { Route::post('/totp/disable', 'SecurityController@delete')->name('account.two_factor.disable'); }); -Route::get('/{vue}', 'IndexController@index') - ->where('vue', '^(?!(\/)?(api|admin|daemon)).+'); +Route::get('/{react}', 'IndexController@index') + ->where('react', '^(?!(\/)?(api|auth|admin|daemon)).+'); diff --git a/tsconfig.json b/tsconfig.json index 5336d7ffc..454ab76ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "moduleResolution": "node", "sourceMap": true, "baseUrl": ".", + "lib": ["es2015", "dom"], "paths": { "@/*": [ "./resources/scripts/*" diff --git a/yarn.lock b/yarn.lock index 2827bb211..3718ced67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -735,6 +735,10 @@ prop-types "^15.6.2" scheduler "^0.13.6" +"@types/classnames@^2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.8.tgz#17139e1e1104203572caa4368f6796f6225b70b4" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -743,6 +747,10 @@ version "4.7.0" resolved "https://registry.yarnpkg.com/@types/feather-icons/-/feather-icons-4.7.0.tgz#ec66bc046bcd1513835f87541ecef54b50c57ec9" +"@types/history@*": + version "4.7.2" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" + "@types/lodash@^4.14.119": version "4.14.119" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39" @@ -757,6 +765,27 @@ dependencies: "@types/react" "*" +"@types/react-router-dom@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.3.tgz#7837e3e9fefbc84a8f6c8a51dca004f4e83e94e3" + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.0.1.tgz#9f4548c75755c55b0cffdd743080e5afa87da6dd" + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-transition-group@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.9.2.tgz#c48cf2a11977c8b4ff539a1c91d259eaa627028d" + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.8.19": version "16.8.19" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.19.tgz#629154ef05e2e1985cdde94477deefd823ad9be3" @@ -1841,7 +1870,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: +classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -2551,6 +2580,12 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + dependencies: + "@babel/runtime" "^7.1.2" + dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -6112,6 +6147,14 @@ react-router@5.0.1: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-transition-group@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.1.0.tgz#7b50c0a93a6c127336187252c3c1a70eff3304ce" + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"