aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/views/Login
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-12-13 17:58:51 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-12-13 17:58:51 -0500
commit4b8ae76132d2342f40cec703b3d5145ea075c451 (patch)
tree62b942b6181261566cd3245ee35cd15a138aabf2 /front-end/src/views/Login
parentb564708f29cf8a709c3e3d981477b2ec8440673e (diff)
log time coming ui and lib updates
Diffstat (limited to 'front-end/src/views/Login')
-rw-r--r--front-end/src/views/Login/components/Social.vue55
-rw-r--r--front-end/src/views/Login/components/Totp.vue29
-rw-r--r--front-end/src/views/Login/components/UserPass.vue98
-rw-r--r--front-end/src/views/Login/index.vue120
-rw-r--r--front-end/src/views/Login/pki/index.vue13
-rw-r--r--front-end/src/views/Login/social/[type].vue90
6 files changed, 174 insertions, 231 deletions
diff --git a/front-end/src/views/Login/components/Social.vue b/front-end/src/views/Login/components/Social.vue
index 5824226..2cea930 100644
--- a/front-end/src/views/Login/components/Social.vue
+++ b/front-end/src/views/Login/components/Social.vue
@@ -1,47 +1,30 @@
<template>
-
- <form class="w-full" @submit.prevent="SocalLogin('/login/social/github')">
- <button type="submit" class="btn social-button" :disabled="waiting">
- <fa-icon :icon="['fab','github']" size="xl" />
- Login with Github
- </button>
- </form>
- <form class="mt-4" @submit.prevent="SocalLogin('/login/social/discord')">
- <button type="submit" class="btn social-button" :disabled="waiting">
- <fa-icon :icon="['fab','discord']" size="xl" />
- Login with Discord
- </button>
- </form>
+ <div class="flex flex-col gap-3">
+ <div v-for="method in store.socialOauth.methods" :key="method.Id" class="">
+ <button
+ type="submit"
+ class="btn social-button"
+ :disabled="waiting"
+ @click.prevent="submitLogin(method)"
+ >
+ <fa-icon :icon="['fab', method.Id]" size="xl" />
+ Login with {{ capitalize(method.Id) }}
+ </button>
+ </div>
+ </div>
</template>
<script setup lang="ts">
-import { apiCall, useWait, useSessionUtils, WebMessage, useUser } from '@vnuge/vnlib.browser'
+import { apiCall, useWait, type OAuthMethod } from '@vnuge/vnlib.browser'
+import { capitalize } from 'lodash-es';
+import { useStore } from '../../../store';
const { waiting } = useWait()
-const { KeyStore } = useSessionUtils()
-const { prepareLogin } = useUser()
+const store = useStore()
-const SocalLogin = async (url:string) => {
- await apiCall(async ({ axios }) => {
-
- //Prepare the login claim
- const claim = await prepareLogin()
- const { data } = await axios.put<WebMessage<string>>(url, claim)
-
- const encDat = data.getResultOrThrow()
- // Decrypt the result which should be a redirect url
- const result = await KeyStore.decryptDataAsync(encDat)
- // get utf8 text
- const text = new TextDecoder('utf-8').decode(result)
- // Recover url
- const redirect = new URL(text)
- // Force https
- redirect.protocol = 'https:'
- // redirect to the url
- window.location.href = redirect.href
- })
-}
+//Invoke login wrapped in api call
+const submitLogin = (method: OAuthMethod) => apiCall(() => store.socialOauth.beginLoginFlow(method))
</script> \ No newline at end of file
diff --git a/front-end/src/views/Login/components/Totp.vue b/front-end/src/views/Login/components/Totp.vue
index 43c05d8..2ba1314 100644
--- a/front-end/src/views/Login/components/Totp.vue
+++ b/front-end/src/views/Login/components/Totp.vue
@@ -23,25 +23,40 @@
</template>
<script setup lang="ts">
-import { useMessage, useWait } from '@vnuge/vnlib.browser';
+import { toRefs, defineAsyncComponent } from 'vue';
+import { IMfaFlowContinuiation, apiCall, useMessage, useWait } from '@vnuge/vnlib.browser';
import { toSafeInteger } from 'lodash-es';
-import VOtpInput from "vue3-otp-input";
+const VOtpInput = defineAsyncComponent(() => import('vue3-otp-input'))
-const emit = defineEmits(['submit'])
+const emit = defineEmits(['clear'])
+const props = defineProps<{
+ upgrade: IMfaFlowContinuiation
+}>()
+
+const { upgrade } = toRefs(props)
const { waiting } = useWait();
const { onInput } = useMessage();
-const SubimitTotp = async (code : string) => {
+const SubimitTotp = (code : string) => {
//If a request is still pending, do nothing
if (waiting.value) {
return
}
- //Submit a mfa upgrade result
- emit('submit', {
- code: toSafeInteger(code)
+ apiCall(async ({ toaster }) => {
+ //Submit totp code
+ const res = await upgrade.value.submit({ code: toSafeInteger(code) })
+ res.getResultOrThrow()
+
+ emit('clear')
+
+ // Push a new toast message
+ toaster.general.success({
+ title: 'Success',
+ text: 'You have been logged in',
+ })
})
}
diff --git a/front-end/src/views/Login/components/UserPass.vue b/front-end/src/views/Login/components/UserPass.vue
index e218cb8..442abb1 100644
--- a/front-end/src/views/Login/components/UserPass.vue
+++ b/front-end/src/views/Login/components/UserPass.vue
@@ -1,7 +1,12 @@
<template>
<div class="">
<h3>Login</h3>
- <form id="user-pass-submit-form" method="post" action="/login" @submit.prevent="SubmitLogin">
+
+ <div v-if="mfaUpgrade?.type === MfaMethod.TOTP">
+ <Totp @clear="totpClear" :upgrade="mfaUpgrade" />
+ </div>
+
+ <form v-else id="user-pass-submit-form" method="post" action="/login" @submit.prevent="SubmitLogin">
<fieldset class="" :disabled="waiting" >
<div>
<div class="float-label">
@@ -17,7 +22,7 @@
<label for="username">Email</label>
</div>
</div>
- <div class="py-3">
+ <div class="py-3">
<div class="mb-2 float-label">
<input
id="password"
@@ -37,29 +42,45 @@
<fa-icon :class="{'animate-spin':waiting}" :icon="waiting ? 'spinner' : 'sign-in-alt'"/>
Log-in
</button>
+ <div class="flex flex-row justify-between gap-3 pt-3 pb-2 form-links">
+ <router-link to="/pwreset">
+ Forgot password
+ </router-link>
+ <router-link to="/register">
+ Register a new account
+ </router-link>
+ </div>
</form>
- <div class="flex flex-row justify-between gap-3 pt-3 pb-2 form-links">
- <router-link to="/pwreset">
- Forgot password
- </router-link>
- <router-link to="/register">
- Register a new account
- </router-link>
- </div>
</div>
</template>
<script setup lang="ts">
-import { reactive } from 'vue'
-import useVuelidate from '@vuelidate/core'
+import { ref, shallowRef, reactive, defineAsyncComponent, type Ref } from 'vue'
+import { useTimeoutFn, set } from '@vueuse/core'
+import { useVuelidate } from '@vuelidate/core'
+import { isEqual } from 'lodash-es'
import { required, maxLength, minLength, email, helpers } from '@vuelidate/validators'
-import { useMessage, useVuelidateWrapper, useWait } from '@vnuge/vnlib.browser'
-
-const emit = defineEmits(['login'])
+import {
+ useVuelidateWrapper, useMfaLogin, totpMfaProcessor, IMfaFlowContinuiation, MfaMethod,
+ apiCall, useMessage, useWait, debugLog, WebMessage
+} from '@vnuge/vnlib.browser'
+const Totp = defineAsyncComponent(() => import('./Totp.vue'))
-const { onInput } = useMessage();
+const { onInput, setMessage } = useMessage();
const { waiting } = useWait();
+//Setup mfa login with TOTP support
+const { login } = useMfaLogin([ totpMfaProcessor() ])
+
+const mfaUpgrade = shallowRef<IMfaFlowContinuiation>();
+
+const mfaTimeout = ref<number>(600 * 1000);
+const mfaTimer = useTimeoutFn(() => {
+ //Clear upgrade message
+ mfaUpgrade.value = undefined;
+ setMessage('Your TOTP request has expired')
+}, mfaTimeout, { immediate: false })
+
const vState = reactive({ username: '', password: '' })
const rules = {
@@ -84,9 +105,50 @@ const SubmitLogin = async () => {
if (!await validate()) {
return
}
+
+ // Run login in an apicall wrapper
+ await apiCall(async ({ toaster }) => {
+
+ //Attempt to login
+ const response = await login(
+ v$.value.username.$model,
+ v$.value.password.$model
+ );
+
+ debugLog('Mfa-login', response);
+
+ //See if the response is a web message
+ if(response.getResultOrThrow){
+ (response as WebMessage).getResultOrThrow();
+ }
+
+ //Try to get response as a flow continuation
+ const mfa = response as IMfaFlowContinuiation
+
+ // Response is a totp upgrade request
+ if (isEqual(mfa.type, MfaMethod.TOTP)) {
+ //Store the upgrade message
+ set(mfaUpgrade, mfa);
+ //Setup timeout timer
+ set(mfaTimeout, mfa.expires! * 1000);
+ mfaTimer.start();
+ }
+ //If login without mfa was successful
+ else if (response.success) {
+ // Push a new toast message
+ toaster.general.success({
+ title: 'Success',
+ text: 'You have been logged in',
+ })
+ }
+ })
+}
- //Emit login and pass the username and password
- emit('login', { username: v$.value.username.$model, password: v$.value.password.$model });
+const totpClear = () => {
+ //Clear timer
+ mfaTimer.stop();
+ //Clear upgrade message
+ set(mfaUpgrade, undefined);
}
</script> \ No newline at end of file
diff --git a/front-end/src/views/Login/index.vue b/front-end/src/views/Login/index.vue
index fea02d4..5d8f298 100644
--- a/front-end/src/views/Login/index.vue
+++ b/front-end/src/views/Login/index.vue
@@ -2,12 +2,8 @@
<div id="login-template" class="app-component-entry">
<div class="login-container">
- <div v-if="showTotp">
- <Totp @submit="totpSubmit" />
- </div>
-
- <div v-else-if="!loggedIn">
- <UserPass @login="submitLogin" />
+ <div v-if="!loggedIn">
+ <UserPass/>
</div>
<div v-else>
@@ -24,12 +20,12 @@
</div>
</div>
- <div v-if="!(loggedIn || showTotp)" class="w-full mt-6">
-
+ <div v-if="!loggedIn" class="w-full mt-6">
+
<Social />
<!-- pki button, forward to the pki route -->
- <div v-if="pkiEnabled" class="mt-4">
+ <div v-if="pkiEnabled" class="mt-3">
<router-link to="/login/pki">
<button type="submit" class="btn red social-button" :disabled="waiting">
<fa-icon :icon="['fa','certificate']" size="xl" />
@@ -44,50 +40,29 @@
</template>
<script setup lang="ts">
-import { computed, ref } from 'vue'
-import Totp from './components/Totp.vue'
+import { } from 'vue'
+import { apiCall, useWait } from '@vnuge/vnlib.browser'
+import { isNil } from 'lodash-es'
+import { useStore } from '../../store'
+import { storeToRefs } from 'pinia'
import UserPass from './components/UserPass.vue'
import Social from './components/Social.vue'
-import {
- useMfaLogin, totpMfaProcessor, IMfaFlowContinuiation, MfaMethod, apiCall,
- useMessage, useWait, useUser, useSession, useLastPage, useTitle, debugLog
-} from '@vnuge/vnlib.browser'
-import { useTimeoutFn } from '@vueuse/core'
-import { isNil } from 'lodash-es'
-
-useTitle('Login')
//pki enabled flag from env
-const pkiEnabled = !isNil(import.meta.env.VITE_PKI_ENDPOINT);
+const pkiEnabled = !isNil(import.meta.env.VITE_PKI_ENABLED);
-const { waiting } = useWait()
-const { setMessage } = useMessage()
-const { logout } = useUser();
-const { loggedIn } = useSession()
-
-//Setup mfa login
-const { login } = useMfaLogin([ totpMfaProcessor() ])
-
-//If logged in re-route to the last page the user
-//was on but delayed to the session has time to be set
-const { gotoLastPage } = useLastPage()
-useTimeoutFn(() => loggedIn.value ? gotoLastPage() : null, 500)
+const store = useStore();
+const { loggedIn } = storeToRefs(store)
-const mfaUpgrade = ref<IMfaFlowContinuiation>();
-const mfaTimeout = ref<number>(600 * 1000);
-const { start, stop } = useTimeoutFn(() => {
- //Clear upgrade message
- mfaUpgrade.value = undefined;
- setMessage('Your TOTP request has expired')
-}, mfaTimeout, { immediate: false })
+store.setPageTitle('Login')
-const showTotp = computed(() => mfaUpgrade.value?.type === MfaMethod.TOTP)
+const { waiting } = useWait()
const submitLogout = async () => {
//Submit logout request
await apiCall(async ({ toaster }) => {
- // Attempt to login
- await logout()
+ // Attempt to logout
+ await store.socialOauth.logout()
// Push a new toast message
toaster.general.success({
id: 'logout-success',
@@ -98,67 +73,6 @@ const submitLogout = async () => {
})
}
-const submitLogin = async ({username, password} : { username: string, password:string }) => {
- // Run login in an apicall wrapper
- await apiCall(async ({ toaster }) => {
- // Attempt to login
- const response = await login(username, password);
-
- debugLog('Mfa-login', response)
-
- if(response.success == false){
- setMessage(response.result)
- return;
- }
-
- //Try to get response as a flow continuation
- const mfa = response as IMfaFlowContinuiation
-
- // Response is a totp upgrade request
- if (mfa.type === MfaMethod.TOTP) {
-
- //Store the upgrade message
- mfaUpgrade.value = mfa;
- //Setup timeout timer
- mfaTimeout.value = mfa.expires! * 1000;
- start();
- }
- //If login without mfa was successful
- else if (response.success) {
- // Push a new toast message
- toaster.general.success({
- title: 'Success',
- text: 'You have been logged in',
- })
- }
- })
-}
-
-const totpSubmit = ({ code } : {code:number}) =>{
- apiCall(async ({ toaster }) =>{
-
- if (!mfaUpgrade.value){
- return;
- }
-
- //Submit totp code
- const res = await mfaUpgrade.value.submit({ code })
- res.getResultOrThrow()
-
- //Clear timer
- stop();
-
- //Clear upgrade message
- mfaUpgrade.value = undefined;
-
- // Push a new toast message
- toaster.general.success({
- title: 'Success',
- text: 'You have been logged in',
- })
- })
-}
-
</script>
<style lang="scss">
diff --git a/front-end/src/views/Login/pki/index.vue b/front-end/src/views/Login/pki/index.vue
index cd0113e..585942a 100644
--- a/front-end/src/views/Login/pki/index.vue
+++ b/front-end/src/views/Login/pki/index.vue
@@ -12,7 +12,7 @@
<div class="flex justify-between mt-4">
<div class="text-sm">
- <a class="link" target="_blank" href="https://github.com/VnUgE/Plugins.Essentials/tree/master/plugins/VNLib.Plugins.Essentials.Accounts">
+ <a class="link" target="_blank" href="https://www.vaughnnugent.com/resources/software/articles?tags=docs,_VNLib.Plugins.Essentials.Accounts">
Goto OTP spec
<fa-icon icon="arrow-right" class="ml-1" />
</a>
@@ -33,16 +33,17 @@
<script setup lang="ts">
import { isEmpty } from 'lodash-es';
-import { apiCall, debugLog, useMessage, usePkiAuth } from '@vnuge/vnlib.browser';
+import { apiCall, debugLog, useMessage } from '@vnuge/vnlib.browser';
import { ref } from 'vue'
import { decodeJwt } from 'jose'
import { useRouter } from 'vue-router';
-
-const otp = ref('')
+import { useStore } from '../../../store';
const { setMessage } = useMessage()
const { push } = useRouter()
-const { login } = usePkiAuth(import.meta.env.VITE_PKI_ENDPOINT)
+const store = useStore()
+
+const otp = ref('')
const submit = () =>{
@@ -56,7 +57,7 @@ const submit = () =>{
const jwt = decodeJwt(otp.value)
debugLog(jwt)
- await login(otp.value)
+ await store.pkiAuth.login(otp.value)
//Go back to login page
push({ name: 'Login' })
diff --git a/front-end/src/views/Login/social/[type].vue b/front-end/src/views/Login/social/[type].vue
index 217e89c..68e8b77 100644
--- a/front-end/src/views/Login/social/[type].vue
+++ b/front-end/src/views/Login/social/[type].vue
@@ -32,84 +32,52 @@
</template>
<script setup lang="ts">
-import { isEqual } from 'lodash-es'
-import { useRouteParams, useRouteQuery } from '@vueuse/router'
-import { useSession, useWait, useUser, useTitle, configureApiCall } from '@vnuge/vnlib.browser'
+import { defer } from 'lodash-es'
+import { set, tryOnMounted } from '@vueuse/core'
+import { useWait, configureApiCall } from '@vnuge/vnlib.browser'
import { useRouter } from 'vue-router';
import { ref } from 'vue'
-import { ITokenResponse } from '@vnuge/vnlib.browser/dist/session';
+import { storeToRefs } from 'pinia';
+import { useStore } from '../../../store';
-useTitle('Social Login')
-
-const { loggedIn } = useSession()
-const { prepareLogin } = useUser()
+const store = useStore();
+const { loggedIn } = storeToRefs(store)
const { waiting } = useWait()
-const type = useRouteParams('type')
-const result = useRouteQuery('result', '');
-const nonce = useRouteQuery('nonce', '');
const router = useRouter()
-
const message = ref('')
//Override the message handler to capture the error message and display it
const { apiCall } = configureApiCall(m => message.value = m)
-//If logged-in redirect to login page
-if (loggedIn.value) {
- router.push({ name: 'Login' })
-}
-
+//Set the page title
+store.setPageTitle('Social Login')
-const run = async () => {
- if (isEqual(result.value, 'authorized')) {
+tryOnMounted(() => defer(() => {
- let loginUrl : string = ''
+ //If logged-in redirect to login page
+ if (loggedIn.value) {
+ router.push({ name: 'Login' })
+ }
- switch (type.value) {
- case 'github':
- loginUrl = '/login/social/github';
- break;
- case 'discord':
- loginUrl = '/login/social/discord';
- break;
- default:
+ //try to complete an oauth login
+ apiCall(async ({ toaster }) => {
+ try{
+ //Complete the login
+ await store.socialOauth.completeLogin();
+
+ toaster.general.success({
+ title:'Login Successful',
+ text: 'You have successfully logged in.'
+ })
+
router.push({ name: 'Login' })
- break;
}
-
- // If nonce is set, then we can proceed with finalization
- await apiCall(async ({ axios }) => {
- const preppedLogin = await prepareLogin()
- // Send the login request
- const { data } = await axios.post<ITokenResponse>(loginUrl, { nonce: nonce.value })
-
- data.getResultOrThrow()
-
- // Finalize the login
- await preppedLogin.finalize(data)
-
- // If the login was successful, then we can redirect to the login page
- router.push({ name: 'Login' })
- })
-
- } else {
- switch (result.value) {
- case 'invalid':
- message.value = 'The request was invalid, and you could not be logged in. Please try again.'
- break
- case 'expired':
- message.value = 'The request has expired. Please try again.'
- break
- default:
- message.value = 'There was an error processing the request. Please try again.'
- break
+ catch(err: any){
+ set(message, err.message)
}
- }
-}
-
-//Run without awaiting
-run()
+ })
+}))
</script>