mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2025-10-04 16:26:18 +00:00
Sign In and TOTP is done
This commit is contained in:
@@ -16,21 +16,10 @@ import NotificationList from "@/components/notification/notificationList.vue";
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<NotificationList></NotificationList>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-enter-active,
|
||||
.app-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 1);
|
||||
}
|
||||
.app-enter-from,
|
||||
.app-leave-to{
|
||||
opacity: 0;
|
||||
filter: blur(5px);
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -72,4 +72,15 @@
|
||||
}
|
||||
.slide-right-leave-to{
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.app-enter-active,
|
||||
.app-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 1);
|
||||
}
|
||||
.app-enter-from,
|
||||
.app-leave-to{
|
||||
opacity: 0;
|
||||
filter: blur(5px);
|
||||
transform: scale(0.97);
|
||||
}
|
112
src/static/client/src/components/SignIn/signInForm.vue
Normal file
112
src/static/client/src/components/SignIn/signInForm.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import {clientStore} from "@/stores/clientStore.js";
|
||||
import axios from "axios";
|
||||
import {requestURl} from "@/utilities/request.js";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
const loading = ref(false)
|
||||
const formData = reactive({
|
||||
Email: "",
|
||||
Password: ""
|
||||
});
|
||||
const emits = defineEmits(['totpToken'])
|
||||
|
||||
const totpToken = ref("")
|
||||
const store = clientStore()
|
||||
const signIn = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!formFilled){
|
||||
store.newNotification("Please fill in all fields", "warning")
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
await axios.post(requestURl("/client/api/signin"), formData).then(res => {
|
||||
let data = res.data;
|
||||
if (!data.status){
|
||||
store.newNotification(data.message, "danger")
|
||||
loading.value = false;
|
||||
}else{
|
||||
emits("totpToken", data.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formFilled = computed(() => {
|
||||
return Object.values(formData).find(x => !x) === undefined
|
||||
})
|
||||
|
||||
// const router = useRouter()
|
||||
const route = useRoute()
|
||||
if (route.query.Email){
|
||||
formData.Email = route.query.Email
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Sign In</h1>
|
||||
<p>to your WGDashboard Client account</p>
|
||||
<form class="mt-4 d-flex flex-column gap-3" @submit="e => signIn(e)">
|
||||
<div class="form-floating">
|
||||
<input type="text"
|
||||
required
|
||||
:disabled="loading"
|
||||
v-model="formData.Email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
class="form-control rounded-3" id="email" placeholder="email">
|
||||
<label for="email" class="d-flex">
|
||||
<i class="bi bi-person-circle me-2"></i>
|
||||
Email
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input type="password"
|
||||
required
|
||||
:disabled="loading"
|
||||
v-model="formData.Password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
class="form-control rounded-3" id="password" placeholder="Password">
|
||||
<label for="password" class="d-flex">
|
||||
<i class="bi bi-key me-2"></i>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="text-body text-decoration-none ms-0">
|
||||
Forgot Password?
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
:disabled="!formFilled || loading"
|
||||
class="btn btn-primary rounded-3 btn-brand px-3 py-2">
|
||||
<Transition name="slide-right" mode="out-in">
|
||||
<span v-if="!loading" class="d-block">
|
||||
Continue <i class="ms-2 bi bi-arrow-right"></i>
|
||||
</span>
|
||||
<span v-else class="d-block">
|
||||
Loading...
|
||||
<i class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
</Transition>
|
||||
</button>
|
||||
</form>
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted">
|
||||
Don't have an account yet?
|
||||
</span>
|
||||
<RouterLink to="/signup" class="text-body text-decoration-none ms-auto fw-bold">
|
||||
Sign Up
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
136
src/static/client/src/components/SignIn/totpForm.vue
Normal file
136
src/static/client/src/components/SignIn/totpForm.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup async>
|
||||
import {computed, onMounted, reactive, ref} from "vue";
|
||||
import axios from "axios";
|
||||
import {requestURl} from "@/utilities/request.js";
|
||||
import {useRouter} from "vue-router";
|
||||
import {clientStore} from "@/stores/clientStore.js";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
const props = defineProps([
|
||||
'totpToken'
|
||||
])
|
||||
const totpKey = ref("")
|
||||
const formData = reactive({
|
||||
TOTP: ""
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const replace = () => {
|
||||
formData.TOTP = formData.TOTP.replace(/\D/i, "")
|
||||
}
|
||||
const formFilled = computed(() => {
|
||||
return /^[0-9]{6}$/.test(formData.TOTP)
|
||||
})
|
||||
|
||||
const store = clientStore()
|
||||
const router = useRouter()
|
||||
|
||||
await axios.get(requestURl('/client/api/signin/totp'), {
|
||||
params: {
|
||||
Token: props.totpToken
|
||||
}
|
||||
}).then(res => {
|
||||
let data = res.data
|
||||
if (data.status){
|
||||
if (data.message){
|
||||
totpKey.value = data.message
|
||||
}
|
||||
}else{
|
||||
store.newNotification(data.message, "danger")
|
||||
router.push('/signin')
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['clearToken'])
|
||||
|
||||
onMounted(() => {
|
||||
if (totpKey.value){
|
||||
QRCode.toCanvas(document.getElementById('qrcode'), totpKey.value, function (error) {})
|
||||
}
|
||||
})
|
||||
|
||||
const verify = async (e) => {
|
||||
e.preventDefault()
|
||||
if (formFilled){
|
||||
loading.value = true
|
||||
await axios.post(requestURl('/client/api/signin/totp'), {
|
||||
Token: props.totpToken,
|
||||
UserProvidedTOTP: formData.TOTP
|
||||
}).then(res => {
|
||||
loading.value = false
|
||||
let data = res.data
|
||||
if (data.status){
|
||||
router.push('/')
|
||||
}else{
|
||||
store.newNotification(data.message, "danger")
|
||||
}
|
||||
}).catch(() => {
|
||||
store.newNotification("Sign in status is invalid", "danger")
|
||||
emits('clearToken')
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="d-flex flex-column gap-3" @submit="e => verify(e)">
|
||||
<div>
|
||||
<a role="button" @click="emits('clearToken')">
|
||||
<i class="me-2 bi bi-chevron-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
<div class="">
|
||||
<h1 class="mb-3">Multi-Factor Authentication (MFA)</h1>
|
||||
<div class="card rounded-3" v-if="totpKey">
|
||||
<div class="card-body d-flex gap-3 flex-column">
|
||||
<h2 class="mb-0">Initial Setup</h2>
|
||||
<p class="mb-0">Please scan the following QR Code to generate TOTP with your choice of authenticator</p>
|
||||
<canvas id="qrcode" class="rounded-3 shadow "></canvas>
|
||||
<p class="mb-0">Or you can click the link below:</p>
|
||||
<div class="card rounded-3 ">
|
||||
<div class="card-body">
|
||||
<a :href="totpKey">
|
||||
{{ totpKey }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning mb-0">
|
||||
<strong>
|
||||
Please note: You won't be able to see this QR Code again, so please save it somewhere safe in case you need to recover your TOTP key
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr v-if="totpKey">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<label for="totp">Enter the TOTP generated by your authenticator to verify</label>
|
||||
<input class="form-control form-control-lg rounded-3 text-center"
|
||||
id="totp"
|
||||
:disabled="loading"
|
||||
autofocus
|
||||
@keyup="replace()"
|
||||
maxlength="6" type="text" inputmode="numeric"
|
||||
placeholder="- - - - - -"
|
||||
autocomplete="one-time-code" v-model="formData.TOTP">
|
||||
|
||||
<button
|
||||
:disabled="!formFilled || loading"
|
||||
class="btn btn-primary rounded-3 btn-brand px-3 py-2">
|
||||
<Transition name="slide-right" mode="out-in">
|
||||
<span v-if="!loading" class="d-block">
|
||||
Continue <i class="ms-2 bi bi-arrow-right"></i>
|
||||
</span>
|
||||
<span v-else class="d-block">
|
||||
<i class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -2,7 +2,9 @@ import {createWebHashHistory, createRouter} from "vue-router";
|
||||
import Index from "@/views/index.vue";
|
||||
import SignIn from "@/views/signin.vue";
|
||||
import SignUp from "@/views/signup.vue";
|
||||
import Totp from "@/views/totp.vue";
|
||||
import axios from "axios";
|
||||
import {requestURl} from "@/utilities/request.js";
|
||||
import {clientStore} from "@/stores/clientStore.js";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
@@ -10,6 +12,9 @@ const router = createRouter({
|
||||
{
|
||||
path: '/',
|
||||
component: Index,
|
||||
meta: {
|
||||
auth: true
|
||||
},
|
||||
name: "Home"
|
||||
},
|
||||
{
|
||||
@@ -21,15 +26,24 @@ const router = createRouter({
|
||||
path: '/signup',
|
||||
component: SignUp,
|
||||
name: "Sign Up"
|
||||
},
|
||||
{
|
||||
path: '/totp',
|
||||
component: Totp,
|
||||
name: "Verify TOTP"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (to.meta.auth){
|
||||
await axios.get(requestURl('/client/api/validateAuthentication')).then(res => {
|
||||
next()
|
||||
}).catch(() => {
|
||||
const store = clientStore()
|
||||
store.newNotification("Sign in session ended, please sign in again", "warning")
|
||||
next('/signin')
|
||||
})
|
||||
}else{
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to, from, next) => {
|
||||
document.title = to.name + ' | WGDashboard Client'
|
||||
})
|
||||
|
@@ -1,107 +1,23 @@
|
||||
<script setup>
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import {clientStore} from "@/stores/clientStore.js";
|
||||
import axios from "axios";
|
||||
import {requestURl} from "@/utilities/request.js";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
const loading = ref(false)
|
||||
const formData = reactive({
|
||||
Email: "",
|
||||
Password: ""
|
||||
});
|
||||
const store = clientStore()
|
||||
const signIn = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!formFilled){
|
||||
store.newNotification("Please fill in all fields", "warning")
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
await axios.post(requestURl("/client/api/signin"), formData).then(res => {
|
||||
let data = res.data;
|
||||
if (!data.status){
|
||||
store.newNotification(data.message, "danger")
|
||||
loading.value = false;
|
||||
}else{
|
||||
import SignInForm from "@/components/SignIn/signInForm.vue";
|
||||
import {ref} from "vue";
|
||||
import TotpForm from "@/components/SignIn/totpForm.vue";
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formFilled = computed(() => {
|
||||
return Object.values(formData).find(x => !x) === undefined
|
||||
})
|
||||
|
||||
// const router = useRouter()
|
||||
const route = useRoute()
|
||||
if (route.query.Email){
|
||||
formData.Email = route.query.Email
|
||||
}
|
||||
const checkTotp = ref("")
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Sign In</h1>
|
||||
<p>to your WGDashboard Client account</p>
|
||||
<form class="mt-4 d-flex flex-column gap-3" @submit="e => signIn(e)">
|
||||
<div class="form-floating">
|
||||
<input type="text"
|
||||
required
|
||||
:disabled="loading"
|
||||
v-model="formData.Email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
class="form-control rounded-3" id="email" placeholder="email">
|
||||
<label for="email" class="d-flex">
|
||||
<i class="bi bi-person-circle me-2"></i>
|
||||
Email
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input type="password"
|
||||
required
|
||||
:disabled="loading"
|
||||
v-model="formData.Password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
class="form-control rounded-3" id="password" placeholder="Password">
|
||||
<label for="password" class="d-flex">
|
||||
<i class="bi bi-key me-2"></i>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" class="text-body text-decoration-none ms-0">
|
||||
Forgot Password?
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
:disabled="!formFilled"
|
||||
class="btn btn-primary rounded-3 btn-brand px-3 py-2">
|
||||
<Transition name="slide-right" mode="out-in">
|
||||
<span v-if="!loading" class="d-block">
|
||||
Continue <i class="ms-2 bi bi-arrow-right"></i>
|
||||
</span>
|
||||
<span v-else class="d-block">
|
||||
<i class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
</Transition>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<hr class="my-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted">
|
||||
Don't have an account yet?
|
||||
</span>
|
||||
<RouterLink to="/signup" class="text-body text-decoration-none ms-auto fw-bold">
|
||||
Sign Up
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<Transition name="app" mode="out-in">
|
||||
<SignInForm
|
||||
@totpToken="token => { checkTotp = token }"
|
||||
v-if="!checkTotp"></SignInForm>
|
||||
<TotpForm
|
||||
@clearToken="checkTotp = ''"
|
||||
:totp-token="checkTotp"
|
||||
v-else></TotpForm>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -123,12 +123,13 @@ onMounted(() => {
|
||||
:disabled="!formFilled || !validatePassword || loading"
|
||||
class=" btn btn-primary rounded-3 btn-brand px-3 py-2">
|
||||
<Transition name="slide-right" mode="out-in">
|
||||
<span v-if="!loading" class="d-block">
|
||||
Continue <i class="ms-2 bi bi-arrow-right"></i>
|
||||
</span>
|
||||
<span v-if="!loading" class="d-block">
|
||||
Continue <i class="ms-2 bi bi-arrow-right"></i>
|
||||
</span>
|
||||
<span v-else class="d-block">
|
||||
<i class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
Loading...
|
||||
<i class="spinner-border spinner-border-sm"></i>
|
||||
</span>
|
||||
</Transition>
|
||||
</button>
|
||||
</form>
|
||||
|
@@ -1,11 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
Reference in New Issue
Block a user