diff options
author | vnugent <public@vaughnnugent.com> | 2023-12-13 17:58:51 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-12-13 17:58:51 -0500 |
commit | 4b8ae76132d2342f40cec703b3d5145ea075c451 (patch) | |
tree | 62b942b6181261566cd3245ee35cd15a138aabf2 /front-end/src/views/Login | |
parent | b564708f29cf8a709c3e3d981477b2ec8440673e (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.vue | 55 | ||||
-rw-r--r-- | front-end/src/views/Login/components/Totp.vue | 29 | ||||
-rw-r--r-- | front-end/src/views/Login/components/UserPass.vue | 98 | ||||
-rw-r--r-- | front-end/src/views/Login/index.vue | 120 | ||||
-rw-r--r-- | front-end/src/views/Login/pki/index.vue | 13 | ||||
-rw-r--r-- | front-end/src/views/Login/social/[type].vue | 90 |
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> |