diff --git a/resources/assets/scripts/bootstrap.ts b/resources/assets/scripts/bootstrap.ts index 26222d0e..8bf47327 100644 --- a/resources/assets/scripts/bootstrap.ts +++ b/resources/assets/scripts/bootstrap.ts @@ -1,6 +1,5 @@ -import axios from './helpers/axios'; +import axios from './api/http'; -// @ts-ignore window._ = require('lodash'); /** @@ -10,11 +9,9 @@ window._ = require('lodash'); */ try { - // @ts-ignore window.$ = window.jQuery = require('jquery'); } catch (e) {} -// @ts-ignore window.axios = axios; /** @@ -28,7 +25,6 @@ let token = document.head.querySelector('meta[name="csrf-token"]'); if (token) { // @ts-ignore window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; - // @ts-ignore window.X_CSRF_TOKEN = token.content; } else { diff --git a/resources/assets/scripts/components/Flash.ts b/resources/assets/scripts/components/Flash.ts new file mode 100644 index 00000000..97fe1847 --- /dev/null +++ b/resources/assets/scripts/components/Flash.ts @@ -0,0 +1,89 @@ +import Vue from 'vue'; +import MessageBox from "./MessageBox"; + +export default Vue.component('flash', { + components: { + MessageBox + }, + props: { + container: {type: String, default: ''}, + timeout: {type: Number, default: 0}, + types: { + type: Object, + default: function () { + return { + base: 'alert', + success: 'alert success', + info: 'alert info', + warning: 'alert warning', + error: 'alert error', + } + } + } + }, + + data: function () { + return { + notifications: [], + }; + }, + + /** + * Listen for flash events. + */ + created: function () { + const self = this; + window.events.$on('flash', function (data: any) { + self.flash(data.message, data.title, data.severity); + }); + + window.events.$on('clear-flashes', function () { + self.clear(); + }); + }, + + methods: { + /** + * Flash a message to the screen when a flash event is emitted over + * the global event stream. + */ + flash: function (message: string, title: string, severity: string) { + this.$data.notifications.push({ + message, severity, title, class: this.$props.types[severity] || this.$props.types.base, + }); + + if (this.$props.timeout > 0) { + setTimeout(this.hide, this.$props.timeout); + } + }, + + /** + * Clear all of the flash messages from the screen. + */ + clear: function () { + this.notifications = []; + window.events.$emit('flashes-cleared'); + }, + + /** + * Hide a notification after a given amount of time. + */ + hide: function (item?: number) { + let key = this.$data.notifications.indexOf(item || this.$data.notifications[0]); + this.$data.notifications.splice(key, 1); + }, + }, + template: ` +
+ +
+ +
+
+
+ `, +}) diff --git a/resources/assets/scripts/components/Flash.vue b/resources/assets/scripts/components/Flash.vue deleted file mode 100644 index 8da4dfd5..00000000 --- a/resources/assets/scripts/components/Flash.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - diff --git a/resources/assets/scripts/components/MessageBox.ts b/resources/assets/scripts/components/MessageBox.ts new file mode 100644 index 00000000..07e6237e --- /dev/null +++ b/resources/assets/scripts/components/MessageBox.ts @@ -0,0 +1,14 @@ +import Vue from 'vue'; + +export default Vue.component('message-box', { + props: { + title: {type: String, required: false}, + message: {type: String, required: true} + }, + template: ` + + `, +}) diff --git a/resources/assets/scripts/components/MessageBox.vue b/resources/assets/scripts/components/MessageBox.vue deleted file mode 100644 index b552f774..00000000 --- a/resources/assets/scripts/components/MessageBox.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/resources/assets/scripts/components/auth/ForgotPassword.ts b/resources/assets/scripts/components/auth/ForgotPassword.ts new file mode 100644 index 00000000..8da6da7d --- /dev/null +++ b/resources/assets/scripts/components/auth/ForgotPassword.ts @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import {isObject} from 'lodash'; +import {AxiosError, AxiosResponse} from "axios"; + +export default Vue.component('forgot-password', { + props: { + email: {type: String, required: true}, + }, + mounted: function () { + (this.$refs.email as HTMLElement).focus(); + }, + data: function () { + return { + X_CSRF_TOKEN: window.X_CSRF_TOKEN, + errors: [], + submitDisabled: false, + showSpinner: false, + }; + }, + methods: { + updateEmail: function (event: { target: HTMLInputElement }) { + this.$data.submitDisabled = false; + this.$emit('update-email', event.target.value); + }, + + submitForm: function () { + this.$data.submitDisabled = true; + this.$data.showSpinner = true; + this.$data.errors = []; + this.$flash.clear(); + + window.axios.post(this.route('auth.forgot-password'), { + email: this.$props.email, + }) + .then((response: AxiosResponse) => { + if (!(response.data instanceof Object)) { + throw new Error('An error was encountered while processing this request.'); + } + + this.$data.submitDisabled = false; + this.$data.showSpinner = false; + this.$flash.success(response.data.status); + this.$router.push({name: 'login'}); + }) + .catch((err: AxiosError) => { + this.$data.showSpinner = false; + if (!err.response) { + return console.error(err); + } + + const response = err.response; + if (response.data && isObject(response.data.errors)) { + response.data.errors.forEach((error: any) => { + this.$flash.error(error.detail); + }); + } + }); + } + }, + template: ` +
+
+
+ + +

{{ $t('auth.forgot_password.label_help') }}

+
+
+
+ +
+
+ + {{ $t('auth.go_to_login') }} + +
+
+ `, +}) diff --git a/resources/assets/scripts/components/auth/ForgotPassword.vue b/resources/assets/scripts/components/auth/ForgotPassword.vue deleted file mode 100644 index 6f40f2df..00000000 --- a/resources/assets/scripts/components/auth/ForgotPassword.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - diff --git a/resources/assets/scripts/components/auth/Login.ts b/resources/assets/scripts/components/auth/Login.ts index 4247f2bf..95c652a8 100644 --- a/resources/assets/scripts/components/auth/Login.ts +++ b/resources/assets/scripts/components/auth/Login.ts @@ -1,5 +1,8 @@ import Vue from 'vue'; import LoginForm from "./LoginForm"; +import ForgotPassword from "./ForgotPassword"; +import TwoFactorForm from "./TwoFactorForm"; +import Flash from "../Flash"; export default Vue.component('login', { data: function () { @@ -10,7 +13,10 @@ export default Vue.component('login', { }; }, components: { + Flash, LoginForm, + ForgotPassword, + TwoFactorForm, }, methods: { onUpdateEmail: function (value: string) { @@ -19,18 +25,18 @@ export default Vue.component('login', { }, template: `
- + - - - - - - + +
`, }); diff --git a/resources/assets/scripts/components/auth/ResetPassword.vue b/resources/assets/scripts/components/auth/ResetPassword.ts similarity index 53% rename from resources/assets/scripts/components/auth/ResetPassword.vue rename to resources/assets/scripts/components/auth/ResetPassword.ts index 451b78ac..bf7a49af 100644 --- a/resources/assets/scripts/components/auth/ResetPassword.vue +++ b/resources/assets/scripts/components/auth/ResetPassword.ts @@ -1,5 +1,70 @@ - - - + `, +}) diff --git a/resources/assets/scripts/components/auth/TwoFactorForm.ts b/resources/assets/scripts/components/auth/TwoFactorForm.ts new file mode 100644 index 00000000..42b41c5a --- /dev/null +++ b/resources/assets/scripts/components/auth/TwoFactorForm.ts @@ -0,0 +1,80 @@ +import Vue from 'vue'; +import {AxiosError, AxiosResponse} from "axios"; +import {isObject} from 'lodash'; + +export default Vue.component('two-factor-form', { + data: function () { + return { + code: '', + }; + }, + mounted: function () { + if ((this.$route.query.token || '').length < 1) { + return this.$router.push({ name: 'login' }); + } + + (this.$refs.code as HTMLElement).focus(); + }, + methods: { + submitToken: function () { + this.$flash.clear(); + window.axios.post(this.route('auth.login-checkpoint'), { + confirmation_token: this.$route.query.token, + authentication_code: this.$data.code, + }) + .then((response: AxiosResponse) => { + if (!(response.data instanceof Object)) { + throw new Error('An error was encountered while processing this login.'); + } + + localStorage.setItem('token', response.data.token); + this.$store.dispatch('login'); + + window.location = response.data.intended; + }) + .catch((err: AxiosError) => { + this.$store.dispatch('logout'); + if (!err.response) { + return console.error(err); + } + + const response = err.response; + if (response.data && isObject(response.data.errors)) { + response.data.errors.forEach((error: any) => { + this.$flash.error(error.detail); + }); + this.$router.push({ name: 'login' }); + } + }); + } + }, + template: ` +
+
+
+ + +

{{ $t('auth.two_factor.label_help') }}

+
+
+
+ +
+
+ + Back to Login + +
+
+ `, +}); diff --git a/resources/assets/scripts/components/auth/TwoFactorForm.vue b/resources/assets/scripts/components/auth/TwoFactorForm.vue deleted file mode 100644 index 8c800af2..00000000 --- a/resources/assets/scripts/components/auth/TwoFactorForm.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/resources/assets/scripts/pterodactyl-shims.d.ts b/resources/assets/scripts/pterodactyl-shims.d.ts index 75ffa1bc..5b579383 100644 --- a/resources/assets/scripts/pterodactyl-shims.d.ts +++ b/resources/assets/scripts/pterodactyl-shims.d.ts @@ -1,6 +1,19 @@ import Vue from "vue"; import {Store} from "vuex"; import {FlashInterface} from "./mixins/flash"; +import {AxiosInstance} from "axios"; +import {Vue as VueType} from "vue/types/vue"; + +declare global { + interface Window { + X_CSRF_TOKEN: string, + _: any, + $: any, + jQuery: any, + axios: AxiosInstance, + events: VueType, + } +} declare module 'vue/types/options' { interface ComponentOptions { @@ -16,6 +29,7 @@ declare module 'vue/types/options' { declare module 'vue/types/vue' { interface Vue { $store: Store, - $flash: FlashInterface + $flash: FlashInterface, + route: (name: string, params?: object, absolute?: boolean) => string, } } diff --git a/resources/assets/scripts/router.ts b/resources/assets/scripts/router.ts index cc992d84..9c2973c4 100644 --- a/resources/assets/scripts/router.ts +++ b/resources/assets/scripts/router.ts @@ -7,7 +7,7 @@ const route = require('./../../../vendor/tightenco/ziggy/src/js/route').default; import Login from './components/auth/Login'; import Dashboard from './components/dashboard/Dashboard.vue'; import Account from './components/dashboard/Account.vue'; -import ResetPassword from './components/auth/ResetPassword.vue'; +import ResetPassword from './components/auth/ResetPassword'; import User from './models/user'; import { Server,