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: `
+
+ `,
+})
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 @@
-
-
+import Vue from 'vue';
+import {isObject} from 'lodash';
+import {AxiosError, AxiosResponse} from "axios";
+
+export default Vue.component('reset-password', {
+ props: {
+ token: {type: String, required: true},
+ email: {type: String, required: false},
+ },
+ mounted: function () {
+ if (this.$props.email.length > 0) {
+ (this.$refs.email as HTMLElement).setAttribute('value', this.$props.email);
+ (this.$refs.password as HTMLElement).focus();
+ }
+ },
+ data: function () {
+ return {
+ errors: [],
+ showSpinner: false,
+ password: '',
+ passwordConfirmation: '',
+ };
+ },
+ methods: {
+ updateEmailField: function (event: { target: HTMLInputElement }) {
+ this.$data.submitDisabled = event.target.value.length === 0;
+ },
+
+ submitForm: function () {
+ this.$data.showSpinner = true;
+
+ this.$flash.clear();
+ window.axios.post(this.route('auth.reset-password'), {
+ email: this.$props.email,
+ password: this.$data.password,
+ password_confirmation: this.$data.passwordConfirmation,
+ token: this.$props.token,
+ })
+ .then((response: AxiosResponse) => {
+ if (!(response.data instanceof Object)) {
+ throw new Error('An error was encountered while processing this login.');
+ }
+
+ if (response.data.send_to_login) {
+ this.$flash.success('Your password has been reset, please login to continue.');
+ return this.$router.push({ name: 'login' });
+ }
+
+ return window.location = response.data.redirect_to;
+ })
+ .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);
+ });
+ (this.$refs.password as HTMLElement).focus();
+ }
+ });
+ }
+ },
+ template: `
-
-
-
-
+ `,
+})
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: `
+
+ `,
+});
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,