aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'front-end/src/views')
-rw-r--r--front-end/src/views/Account/[comp].vue100
-rw-r--r--front-end/src/views/Account/components/oauth/CreateApp.vue182
-rw-r--r--front-end/src/views/Account/components/oauth/Oauth.vue78
-rw-r--r--front-end/src/views/Account/components/oauth/SingleApplication.vue198
-rw-r--r--front-end/src/views/Account/components/oauth/o2AppValidation.ts43
-rw-r--r--front-end/src/views/Account/components/profile/Profile.vue119
-rw-r--r--front-end/src/views/Account/components/settings/Fido.vue37
-rw-r--r--front-end/src/views/Account/components/settings/PasswordReset.vue117
-rw-r--r--front-end/src/views/Account/components/settings/Pki.vue262
-rw-r--r--front-end/src/views/Account/components/settings/Security.vue53
-rw-r--r--front-end/src/views/Account/components/settings/Settings.vue13
-rw-r--r--front-end/src/views/Account/components/settings/TotpSettings.vue232
-rw-r--r--front-end/src/views/Blog/components/Posts.vue51
-rw-r--r--front-end/src/views/Blog/components/image-preview-dialog.vue54
-rw-r--r--front-end/src/views/Blog/index.vue66
-rw-r--r--front-end/src/views/Login/components/Social.vue36
-rw-r--r--front-end/src/views/Login/components/UserPass.vue7
-rw-r--r--front-end/src/views/Login/index.vue75
-rw-r--r--front-end/src/views/Login/pki/index.vue70
-rw-r--r--front-end/src/views/Login/social/[type].vue90
20 files changed, 1186 insertions, 697 deletions
diff --git a/front-end/src/views/Account/[comp].vue b/front-end/src/views/Account/[comp].vue
index d4f1c4d..713a6fe 100644
--- a/front-end/src/views/Account/[comp].vue
+++ b/front-end/src/views/Account/[comp].vue
@@ -1,3 +1,51 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { get, set } from '@vueuse/core'
+import { useRouteParams } from '@vueuse/router'
+import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
+import { useStore } from '../../store'
+import Settings from './components/settings/Settings.vue'
+import Profile from './components/profile/Profile.vue'
+import OauthApps from './components/oauth/Oauth.vue'
+
+const store = useStore()
+store.setPageTitle('Account')
+
+const oauthEnabled = computed(() => store.oauth2?.apps);
+
+type ComponentType = 'profile' | 'oauth' | 'settings' | ''
+
+const comp = useRouteParams<ComponentType>('comp', '')
+
+const tabId = computed<number>(() => {
+ switch (comp.value) {
+ case 'oauth':
+ //If oauth is not enabled, redirect to profile
+ return get(oauthEnabled) ? 2 : 0
+ case 'settings':
+ return 1
+ case 'profile':
+ default:
+ return 0
+ }
+})
+
+const onTabChange = (tabid: number) => {
+ switch (tabid) {
+ case 1:
+ set(comp, 'settings')
+ break
+ case 2:
+ set(comp, 'oauth')
+ break
+ case 0:
+ default:
+ set(comp, 'profile')
+ break
+ }
+}
+
+</script>
<template>
<div id="account-template" class="app-component-entry">
<TabGroup :selectedIndex="tabId" @change="onTabChange" as="div" class="container h-full m-auto mt-0 mb-10 duration-150 ease-linear text-color-foreground">
@@ -18,6 +66,12 @@
</span>
</tab>
+ <Tab v-if="oauthEnabled" v-slot="{ selected }" >
+ <span class="page-link" :class="{ 'active': selected }">
+ OAuth
+ </span>
+ </tab>
+
</TabList>
</div>
@@ -31,54 +85,16 @@
<Settings />
</TabPanel>
+ <TabPanel v-if="oauthEnabled" :unmount="false">
+ <OauthApps />
+ </TabPanel>
+
</TabPanels>
</TabGroup>
</div>
</template>
-<script setup lang="ts">
-import { computed } from 'vue'
-import { useRouteParams } from '@vueuse/router'
-import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
-import { useStore } from '../../store'
-import Settings from './components/settings/Settings.vue'
-import Profile from './components/profile/Profile.vue'
-
-const { setPageTitle } = useStore()
-setPageTitle('Account')
-
-enum ComponentType{
- Profile = 'profile',
- Settings = 'settings'
-}
-
-const comp = useRouteParams<ComponentType>('comp', '')
-
-const tabId = computed<number>(() => {
- switch (comp.value) {
- case ComponentType.Settings:
- return 1
- case ComponentType.Profile:
- default:
- return 0
- }
-})
-
-const onTabChange = (tabid : number) =>{
- switch (tabid) {
- case 1:
- comp.value = ComponentType.Settings
- break
- case 0:
- default:
- comp.value = ComponentType.Profile
- break
- }
-}
-
-</script>
-
<style lang="scss">
#account-template{
diff --git a/front-end/src/views/Account/components/oauth/CreateApp.vue b/front-end/src/views/Account/components/oauth/CreateApp.vue
new file mode 100644
index 0000000..038c43f
--- /dev/null
+++ b/front-end/src/views/Account/components/oauth/CreateApp.vue
@@ -0,0 +1,182 @@
+<script setup lang="ts">
+import { indexOf, pull } from 'lodash-es'
+import { Ref, ref, toRefs } from 'vue';
+import { Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
+import { set, useClipboard } from '@vueuse/core'
+import { apiCall } from '@vnuge/vnlib.browser'
+import { getAppValidator } from './o2AppValidation'
+import { useStore } from '../../../../store';
+import { OAuth2Application } from '../../../../store/oauthAppsPlugin';
+
+const emit = defineEmits(['close'])
+const props = defineProps<{
+ isOpen: boolean
+}>()
+
+const { isOpen } = toRefs(props);
+
+//Init the oauth2 app api
+const store = useStore()
+
+const { copied, copy } = useClipboard();
+
+const newAppBuffer = ref<Partial<OAuth2Application& { secret: string }>>();
+const newAppPermissions = ref<string[]>([]);
+
+const { v$, validate, reset } = getAppValidator(newAppBuffer as Ref<OAuth2Application>);
+
+const close = () => {
+ set(newAppBuffer, {});
+ reset()
+ emit('close')
+}
+
+const onFormSubmit = async () => {
+
+ // Validate the new app form
+ if (!await validate()) {
+ return
+ }
+
+ // Create the new app
+ await apiCall(async () => {
+
+ const { secret } = await store.oauth2.createApp(newAppBuffer.value as OAuth2Application)
+
+ // Reset the new app buffer and pass the secret value
+ set(newAppBuffer, { secret })
+ })
+
+ // reset the validator
+ v$.value.$reset()
+}
+
+const permissionChanged = (e: any) => {
+ if (e.target.checked) {
+ // Make sure the permission is not already in the list
+ if (indexOf(newAppPermissions.value, e.target.name) > -1) {
+ return
+ }
+ // Add the permission to the list
+ newAppPermissions.value.push(e.target.name as string)
+ } else {
+ // Remove the permission from the list
+ pull(newAppPermissions.value, e.target.name)
+ }
+ // Update the permissions model
+ v$.value.permissions.$model = newAppPermissions.value.join(',')
+}
+
+</script>
+<template>
+ <Dialog :open="isOpen" @close="close" class="relative z-10">
+ <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
+ <div class="fixed inset-0 flex justify-center top-20">
+ <DialogPanel class="new-o2-app-dialog">
+ <DialogTitle>Create app</DialogTitle>
+ <div class="flex">
+ <div class="m-auto mb-3 text-sm">
+ <p class="my-1">
+ Step 1: Enter a name for your app.
+ </p>
+ <p class="my-1">
+ Step 2: Submit the form.
+ </p>
+ <p class="my-1 text-red-500">
+ Step 3: Save your Client ID and Secret somewhere safe.
+ </p>
+ </div>
+ </div>
+ <!-- If secret is set, show the scret window -->
+ <div v-if="newAppBuffer?.secret" class="mt-2">
+ <div class="block mx-1 sm:inline">
+ Secret:
+ </div>
+ <div class="px-1 py-4 my-2 break-all border-2 border-gray-300 rounded-lg">
+ <div class="text-center secret">
+ <span class="block mx-1 sm:inline">
+ {{ newAppBuffer.secret }}
+ </span>
+ </div>
+ </div>
+ <div class="text-sm">
+ <p class="p-2">
+ This secret will only be displayed <strong>once</strong>, and you cannot request it again.
+ If you lose it, you will need to update the secret from the app edit pane.
+ </p>
+ <p class="p-2">
+ Please remember to keep this secret somewhere safe. If an attacker gains
+ access to it, they will be able to access any APIs on your behalf!
+ </p>
+ </div>
+ <div class="flex justify-end">
+ <button v-if="!copied" class="btn primary" @click="copy(newAppBuffer.secret)">
+ Copy
+ </button>
+ <button v-else class="btn primary" @click="close">
+ Done
+ </button>
+ </div>
+ </div>
+ <div v-else>
+ <form id="o2-app-creation" class="" @submit.prevent="onFormSubmit">
+ <fieldset class="flex flex-col gap-4">
+ <div class="input-container">
+ <label>App Name</label>
+ <input
+ class="w-full mt-1 input primary"
+ :class="{'invalid':v$.name.$invalid, 'dirty': v$.name.$dirty}"
+ name="name"
+ type="text"
+ v-model="v$.name.$model"
+ />
+ </div>
+ <div class="input-container">
+ <label>Description</label>
+ <textarea
+ class="w-full mt-1 input primary"
+ :class="{ 'invalid': v$.description.$invalid, 'dirty': v$.name.$dirty }"
+ name="description"
+ v-model="v$.description.$model"
+ rows="3"
+ />
+ </div>
+ <div class="input-container">
+ <label>Permissions</label>
+ <ul class="text-sm">
+ <li v-for="scope in store.oauth2.scopes" :key="scope" class="my-1.5">
+ <label class="flex cursor-pointer">
+ <input class="w-3.5 cursor-pointer" type="checkbox" :name="`02scope-${scope}`" @change="permissionChanged">
+ <span class="my-auto ml-1.5">{{ scope }}</span>
+ </label>
+ </li>
+ </ul>
+ </div>
+ </fieldset>
+ <div class="flex justify-end mt-4">
+ <div class="button-group">
+ <button type="submit" form="o2-app-creation" class="btn primary">Submit</button>
+ <button class="btn" @click.prevent="close">Cancel</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </DialogPanel>
+ </div>
+ </Dialog>
+</template>
+<style lang="scss">
+
+.new-o2-app-dialog{
+ @apply w-full max-w-lg p-8 pt-4 m-auto mt-0 shadow-md sm:rounded border dark:border-dark-500;
+ @apply bg-white dark:bg-dark-700 dark:text-gray-200;
+
+ #o2-app-creation{
+ input.dirty.invalid,
+ textarea.dirty.invalid{
+ @apply border-red-500 focus:border-red-500;
+ }
+ }
+}
+
+</style> \ No newline at end of file
diff --git a/front-end/src/views/Account/components/oauth/Oauth.vue b/front-end/src/views/Account/components/oauth/Oauth.vue
new file mode 100644
index 0000000..d269689
--- /dev/null
+++ b/front-end/src/views/Account/components/oauth/Oauth.vue
@@ -0,0 +1,78 @@
+<script setup lang="ts">
+import { defineAsyncComponent } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useStore } from '../../../../store'
+const CreateApp = defineAsyncComponent(() => import('./CreateApp.vue'))
+
+import SingleApplication from './SingleApplication.vue'
+import { useToggle } from '@vueuse/core';
+
+const store = useStore()
+const { isLocalAccount } = storeToRefs(store)
+
+const [editNew, toggleEdit] = useToggle()
+
+const newAppClose = () => {
+ toggleEdit(false);
+ //Reload apps on close
+ store.oauth2.refresh();
+}
+
+//Load apps
+store.oauth2.refresh();
+
+</script>
+<template>
+ <div id="oauth-apps" class="acnt-content-container">
+ <div class="app-container panel-container">
+ <div class="mb-6 panel-header">
+ <div class="flex ml-0 mr-auto">
+ <div class="my-auto panel-title">
+ <h4>Your applications</h4>
+ </div>
+ </div>
+ <div class="ml-auto mr-0">
+ <div class="button-container">
+ <button class="btn primary sm" :disabled="!isLocalAccount" @click="toggleEdit(true)">
+ Create App
+ </button>
+ </div>
+ </div>
+ </div>
+ <div v-if="store.oauth2.apps.length == 0" class="no-apps-container">
+ <div class="m-auto">
+ You dont have any OAuth2 client applications yet.
+ </div>
+ </div>
+ <div v-else>
+ <div v-for="app in store.oauth2.apps" :key="app.Id" class="panel-content">
+ <SingleApplication :application="app" :allow-edit="isLocalAccount" />
+ </div>
+ </div>
+ </div>
+ <div class="px-2 my-10">
+ <div class="m-auto text-sm">
+ OAuth2 applications allow you grant api access to OAuth2 clients using the Client Credentials grant type.
+ <a class="link" href="https://oauth.net" target="_blank">
+ Learn more
+ </a>
+ </div>
+ <div v-show="!isLocalAccount" class="mt-3 text-center text-red-500">
+ You may not create or edit applications if you are using external authentication.
+ </div>
+ </div>
+ <CreateApp :is-open="editNew" @close="newAppClose" />
+ </div>
+</template>
+
+<style>
+
+#oauth-apps {
+ @apply m-auto max-w-3xl;
+}
+
+#oauth-apps .app-container .no-apps-container {
+ @apply w-full flex h-36 sm:border sm:rounded-md mt-4 mb-20 dark:border-dark-500 border-gray-300;
+}
+
+</style>
diff --git a/front-end/src/views/Account/components/oauth/SingleApplication.vue b/front-end/src/views/Account/components/oauth/SingleApplication.vue
new file mode 100644
index 0000000..60bad68
--- /dev/null
+++ b/front-end/src/views/Account/components/oauth/SingleApplication.vue
@@ -0,0 +1,198 @@
+<script setup lang="ts">
+import { toUpper } from 'lodash-es'
+import { apiCall, useWait, useConfirm, usePassConfirm, useDataBuffer } from '@vnuge/vnlib.browser'
+import { ref, computed, toRefs, watch } from 'vue'
+import { get, set, useClipboard, useTimeAgo, useToggle } from '@vueuse/core'
+import { getAppValidator } from './o2AppValidation'
+import { OAuth2Application } from '../../../../store/oauthAppsPlugin'
+import { useStore } from '../../../../store'
+
+const props = defineProps<{
+ application: OAuth2Application
+ allowEdit: boolean
+}>()
+
+const store = useStore()
+const { application, allowEdit } = toRefs(props)
+
+//Init data buffer around application
+const { data, revert, modified, buffer, update, apply } = useDataBuffer(get(application),
+ async (adb) => {
+ await store.oauth2.updateAppMeta(adb.buffer);
+ store.oauth2.refresh();
+ })
+
+//Watch for store app changes and apply them to the buffer
+watch(application, apply)
+
+const { waiting } = useWait()
+const { reveal } = useConfirm()
+const { elevatedApiCall } = usePassConfirm()
+const { copied, copy } = useClipboard()
+
+const { v$, validate, reset } = getAppValidator(buffer)
+
+const [showEdit, toggleEdit] = useToggle()
+const newSecret = ref<string | null>(null);
+
+const name = computed(() => data.name)
+const clientId = computed(() => toUpper(data.client_id))
+const createdTime = useTimeAgo(data.Created);
+const formId = computed(() => `app-form-${data.client_id}`)
+
+const onCancel = function () {
+ revert()
+ reset()
+ toggleEdit(false)
+}
+
+const onSubmit = async function () {
+ // Validate the new app form
+ if (!await validate()) {
+ return
+ }
+ // Create the new app
+ await apiCall(async ({ toaster }) => {
+ // Update does not return anything, if successful
+ await update()
+
+ toaster.general.success({
+ text: 'Application successfully updated',
+ title: 'Success'
+ })
+ reset()
+ toggleEdit(false)
+ })
+}
+
+const updateSecret = async function () {
+ // Show a confrimation prompt
+ const { isCanceled } = await reveal({
+ title: 'Update Secret?',
+ text: `Are you sure you want to update the secret? Any active sessions will be invalidated, and the old secret will be invalidated.`
+ })
+ if (isCanceled) {
+ return
+ }
+ await elevatedApiCall(async ({ password }) => {
+ // Submit the secret update with the new challenge
+ newSecret.value = await store.oauth2.updateAppSecret(data, password)
+ store.oauth2.refresh()
+ })
+}
+
+const onDelete = async function () {
+ // Show a confirmation prompt
+ const { isCanceled } = await reveal({
+ title: 'Delete?',
+ text: 'You are about to permanently delete this application. This will invalidate any active sessions.',
+ subtext: ''
+ })
+ if (isCanceled) {
+ return
+ }
+ await elevatedApiCall(async ({ password, toaster }) => {
+ await store.oauth2.deleteApp(data, password)
+ toaster.general.success({
+ text: 'Application deleted successfully',
+ title: 'Success'
+ })
+ store.oauth2.refresh()
+ })
+}
+
+const closeNewSecret = () => set(newSecret, null);
+
+</script>
+
+<template>
+ <div :id="data.Id">
+ <div class="flex flex-row">
+ <div class="flex ml-0 mr-auto">
+ <div class="flex w-8 h-8 rounded-full bg-primary-500">
+ <div class="m-auto text-white dark:text-dark-500">
+ <fa-icon icon="key"></fa-icon>
+ </div>
+ </div>
+ <div class="inline my-auto ml-2">
+ <h5 class="m-0">{{ name }}</h5>
+ </div>
+ </div>
+ <div v-if="allowEdit && showEdit" class="button-group">
+ <button class="btn primary xs" :disabled="!modified" @click="onSubmit">Update</button>
+ <button class="btn xs" @click="onCancel">Cancel</button>
+ </div>
+ <div v-else class="">
+ <button class="btn no-border xs" @click="toggleEdit(true)">Edit</button>
+ </div>
+ </div>
+ <div class="px-3 py-1 text-color-background">
+ <div class="my-1">
+ <span> Client ID: </span>
+ <span class="font-mono text-color-foreground">{{ clientId }}</span>
+ </div>
+ <div class="text-sm">
+ <span> Created: </span>
+ <span>{{ createdTime }}</span>
+ </div>
+ <div v-if="!showEdit" class="text-sm">
+ <span>{{ data.description }}</span>
+ </div>
+ </div>
+ <div v-if="newSecret" class="flex">
+ <div class="py-4 mx-auto">
+ <div class="pl-1 mb-2">
+ New secret
+ </div>
+ <div class="p-4 text-sm break-all border-2 rounded dark:border-dark-500 dark:bg-dark-700">
+ {{ newSecret }}
+ </div>
+ <div class="flex justify-end my-3">
+ <button v-if="!copied" class="rounded btn" @click="copy(newSecret)">
+ Copy
+ </button>
+ <button v-else class="rounded btn" @click="closeNewSecret">
+ Done
+ </button>
+ </div>
+ </div>
+ </div>
+ <div v-else-if="showEdit" class="app-form-container">
+ <div class="py-4">
+ <form :id="formId" class="max-w-md mx-auto">
+ <fieldset :disabled="waiting" class="">
+ <div class="input-container">
+ <div class="pl-1 mb-1">
+ App name
+ </div>
+ <input class="w-full input primary" :class="{ 'invalid': v$.name.$invalid }" v-model="v$.name.$model" type="text" name="name" />
+ </div>
+ <div class="mt-3 input-container">
+ <div class="pl-1 mb-1">
+ App description
+ <span class="text-sm">(optional)</span>
+ </div>
+ <textarea class="w-full input primary" :class="{ 'invalid': v$.description.$invalid }" v-model="v$.description.$model" name="description" rows="3" />
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ <div class="mt-3">
+
+ <div class="mx-auto w-fit">
+ <div class="button-group">
+ <button class="btn xs" @click.prevent="updateSecret">
+ <fa-icon icon="sync" />
+ <span class="pl-2">New Secret</span>
+ </button>
+ <button class="btn red xs" @click.prevent="onDelete">
+ <fa-icon icon="minus-circle" />
+ <span class="pl-2">Delete</span>
+ </button>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/front-end/src/views/Account/components/oauth/o2AppValidation.ts b/front-end/src/views/Account/components/oauth/o2AppValidation.ts
new file mode 100644
index 0000000..0596f1e
--- /dev/null
+++ b/front-end/src/views/Account/components/oauth/o2AppValidation.ts
@@ -0,0 +1,43 @@
+import { MaybeRef } from 'vue'
+import { useVuelidate } from '@vuelidate/core'
+import { maxLength, helpers, required } from '@vuelidate/validators'
+import { useVuelidateWrapper } from '@vnuge/vnlib.browser'
+import { OAuth2Application } from '../../../../store/oauthAppsPlugin'
+
+//Custom alpha numeric regex
+const alphaNumRegex = helpers.regex(/^[a-zA-Z0-9_,\s]*$/)
+
+const rules = {
+ name: {
+ alphaNumSpace: helpers.withMessage("Name contains invalid characters", alphaNumRegex),
+ maxLength: helpers.withMessage('App name must be less than 50 characters', maxLength(50)),
+ required: helpers.withMessage('Oauth Application name is required', required)
+ },
+ description: {
+ alphaNumSpace: helpers.withMessage("Description contains invalid characters", alphaNumRegex),
+ maxLength: helpers.withMessage('Description must be less than 50 characters', maxLength(50))
+ },
+ permissions: {
+ alphaNumSpace: helpers.regex(/^[a-zA-Z0-9_,:\s]*$/),
+ maxLength: helpers.withMessage('Permissions must be less than 64 characters', maxLength(64))
+ }
+}
+
+export interface AppValidator {
+ readonly v$: ReturnType<typeof useVuelidate>
+ readonly validate: () => Promise<boolean>
+ readonly reset: () => void
+}
+
+/**
+ * Gets the validator for the given application (or new appication) buffer
+ * @param buffer The app buffer to validate
+ * @returns The validator instance, validate function, and reset function
+ */
+export const getAppValidator = (buffer: MaybeRef<OAuth2Application>) : AppValidator => {
+ //App validator
+ const v$ = useVuelidate(rules, buffer)
+ //validate wrapper function
+ const { validate } = useVuelidateWrapper(v$);
+ return { v$, validate, reset: v$.value.$reset };
+} \ No newline at end of file
diff --git a/front-end/src/views/Account/components/profile/Profile.vue b/front-end/src/views/Account/components/profile/Profile.vue
index 0db7192..106c8b9 100644
--- a/front-end/src/views/Account/components/profile/Profile.vue
+++ b/front-end/src/views/Account/components/profile/Profile.vue
@@ -1,3 +1,62 @@
+
+<script setup lang="ts">
+import { defaultTo } from 'lodash-es'
+import { useVuelidate } from '@vuelidate/core'
+import { ref, computed, watch, type Ref } from 'vue'
+import { Rules, FormSchema } from './profile-schema.ts'
+import { apiCall, useMessage, useWait, useVuelidateWrapper, type VuelidateInstance } from '@vnuge/vnlib.browser'
+import { useStore } from '../../../../store'
+
+const { waiting } = useWait()
+const { onInput, clearMessage } = useMessage()
+
+const store = useStore()
+const editMode = ref(false)
+
+// Create validator based on the profile buffer as a data model
+const v$ = useVuelidate(Rules, store.userProfile.buffer as any, { $lazy: true })
+
+// Setup the validator wrapper
+const { validate } = useVuelidateWrapper(v$ as Ref<VuelidateInstance>);
+
+//const modified = computed(() => profile.value.Modified)
+const createdTime = computed(() => defaultTo(store.userProfile.data.created?.toLocaleString(), ''))
+
+const revertProfile = () => {
+ //Revert the buffer
+ store.userProfile.revert()
+ clearMessage()
+ editMode.value = false
+}
+
+const onSubmit = async () => {
+ if (waiting.value) {
+ return;
+ }
+ // Validate the form
+ if (!await validate()) {
+ return
+ }
+ // Init the api call
+ await apiCall(async ({ toaster }) => {
+ const res = await store.userProfile.update();
+
+ const successm = res.getResultOrThrow();
+
+ //No longer in edit mode
+ editMode.value = false
+
+ //Show success message
+ toaster.general.success({
+ title: 'Update successful',
+ text: successm,
+ })
+ })
+}
+
+watch(editMode, () => v$.value.$reset())
+
+</script>
<template>
<div id="account-profile" class="acnt-content-container panel-container">
<div class="acnt-content profile-container panel-content">
@@ -57,66 +116,6 @@
</div>
</template>
-<script setup lang="ts">
-import { defaultTo } from 'lodash-es'
-import useVuelidate from '@vuelidate/core'
-import { ref, computed, watch } from 'vue'
-import { Rules, FormSchema } from './profile-schema.ts'
-import { apiCall, useMessage, useWait, useVuelidateWrapper, WebMessage } from '@vnuge/vnlib.browser'
-import { useStore } from '../../../../store'
-
-const { waiting } = useWait()
-const { onInput, clearMessage } = useMessage()
-
-const store = useStore()
-
-const editMode = ref(false)
-
-// Create validator based on the profile buffer as a data model
-const v$ = useVuelidate(Rules, store.userProfile.buffer, { $lazy:true })
-
-// Setup the validator wrapper
-const { validate } = useVuelidateWrapper(v$);
-
-//const modified = computed(() => profile.value.Modified)
-const createdTime = computed(() => defaultTo(store.userProfile.data.created?.toLocaleString(), ''))
-
-const revertProfile = () => {
- //Revert the buffer
- store.userProfile.revert()
- clearMessage()
- editMode.value = false
-}
-
-const onSubmit = async () => {
- if(waiting.value){
- return;
- }
- // Validate the form
- if (!await validate()) {
- return
- }
- // Init the api call
- await apiCall(async ({ toaster }) => {
- const res = await store.userProfile.update();
-
- const successm = (res as WebMessage<string>).getResultOrThrow();
-
- //No longer in edit mode
- editMode.value = false
-
- //Show success message
- toaster.general.success({
- title: 'Update successful',
- text: successm,
- })
- })
-}
-
-watch(editMode, () => v$.value.$reset())
-
-
-</script>
<style lang="scss">
diff --git a/front-end/src/views/Account/components/settings/Fido.vue b/front-end/src/views/Account/components/settings/Fido.vue
index d453378..9303541 100644
--- a/front-end/src/views/Account/components/settings/Fido.vue
+++ b/front-end/src/views/Account/components/settings/Fido.vue
@@ -1,3 +1,19 @@
+<script setup lang="ts">
+import { useSession } from '@vnuge/vnlib.browser'
+import { toRefs } from 'vue';
+
+const props = defineProps<{
+ fidoEnabled?: boolean
+}>()
+
+const { fidoEnabled } = toRefs(props)
+const { isLocalAccount } = useSession()
+
+const Disable = () => { }
+const Setup = () => { }
+
+</script>
+
<template>
<div id="account-fido-settings">
<div v-if="!isLocalAccount" class="flex flex-row justify-between">
@@ -30,24 +46,3 @@
</div>
</div>
</template>
-
-<script setup lang="ts">
-import { useSession } from '@vnuge/vnlib.browser'
-import { toRefs } from 'vue';
-
-const props = defineProps<{
- fidoEnabled?: boolean
-}>()
-
-const { fidoEnabled } = toRefs(props)
-
-const { isLocalAccount } = useSession()
-
-const Disable = () => {}
-const Setup = () => {}
-
-</script>
-
-<style>
-
-</style>
diff --git a/front-end/src/views/Account/components/settings/PasswordReset.vue b/front-end/src/views/Account/components/settings/PasswordReset.vue
index 24dced6..61fda7d 100644
--- a/front-end/src/views/Account/components/settings/PasswordReset.vue
+++ b/front-end/src/views/Account/components/settings/PasswordReset.vue
@@ -1,68 +1,9 @@
-<template>
- <div id="pwreset-settings" class="container">
- <div class="panel-content">
-
- <div v-if="!pwResetShow" class="">
- <div class="flex flex-wrap items-center justify-between">
-
- <div class="">
- <h5>Password Reset</h5>
- </div>
-
- <div class="flex justify-end">
- <button class="btn xs" @click="showForm">
- <fa-icon icon="sync" />
- <span class="pl-2">Reset Password</span>
- </button>
- </div>
- </div>
-
- <p class="mt-3 text-sm text-color-background">
- You may only reset your password if you have an internal user account. If you exclusivly use an external
- authentication provider (like GitHub or Discord), you will need to reset your password externally.
- </p>
- </div>
-
- <div v-else class="px-2 my-2">
-
- <p class="my-3 text-center">
- Enter your current password, new password, and confirm the new password.
- </p>
-
- <dynamic-form
- id="password-reset-form"
- class="pwreset-form primary"
- :form="formSchema"
- :disabled="waiting"
- :validator="v$"
- @submit="onSubmit"
- @input="onInput"
- />
-
- <div class="flex flex-row justify-end my-2">
- <div class="button-group">
- <button type="submit" form="password-reset-form" class="btn primary sm" :disabled="waiting">
- <fa-icon v-if="!waiting" icon="check" />
- <fa-icon v-else class="animate-spin" icon="spinner" />
- Update
- </button>
- <button class="btn sm cancel-btn" :disabled="waiting" @click="resetForm">
- Cancel
- </button>
- </div>
- </div>
-
- </div>
- </div>
- </div>
-</template>
-
<script setup lang="ts">
import { isEmpty, toSafeInteger } from 'lodash-es';
import { useVuelidate } from '@vuelidate/core'
import { required, maxLength, minLength, helpers } from '@vuelidate/validators'
-import { useUser, apiCall, useMessage, useWait, useConfirm, useVuelidateWrapper } from '@vnuge/vnlib.browser'
-import { computed, reactive, ref, toRefs, watch } from 'vue'
+import { useUser, apiCall, useMessage, useWait, useConfirm, useVuelidateWrapper, VuelidateInstance } from '@vnuge/vnlib.browser'
+import { MaybeRef, computed, reactive, ref, toRefs, watch } from 'vue'
const props = defineProps<{
totpEnabled: boolean,
@@ -136,7 +77,7 @@ const rules = computed(() =>{
})
const v$ = useVuelidate(rules, vState, { $lazy: true })
-const { validate } = useVuelidateWrapper(v$)
+const { validate } = useVuelidateWrapper(v$ as MaybeRef<VuelidateInstance>)
const showTotpCode = computed(() => totpEnabled.value && !fidoEnabled.value)
@@ -208,6 +149,58 @@ const resetForm = () => {
</script>
+<template>
+ <div id="pwreset-settings" class="container">
+ <div class="panel-content">
+
+ <div v-if="!pwResetShow" class="">
+ <div class="flex flex-wrap items-center justify-between">
+
+ <div class="">
+ <h5>Password Reset</h5>
+ </div>
+
+ <div class="flex justify-end">
+ <button class="btn xs" @click="showForm">
+ <fa-icon icon="sync" />
+ <span class="pl-2">Reset Password</span>
+ </button>
+ </div>
+ </div>
+
+ <p class="mt-3 text-sm text-color-background">
+ You may only reset your password if you have an internal user account. If you exclusivly use an external
+ authentication provider (like GitHub or Discord), you will need to reset your password externally.
+ </p>
+ </div>
+
+ <div v-else class="px-2 my-2">
+
+ <p class="my-3 text-center">
+ Enter your current password, new password, and confirm the new password.
+ </p>
+
+ <dynamic-form id="password-reset-form" class="pwreset-form primary" :form="formSchema" :disabled="waiting"
+ :validator="v$" @submit="onSubmit" @input="onInput" />
+
+ <div class="flex flex-row justify-end my-2">
+ <div class="button-group">
+ <button type="submit" form="password-reset-form" class="btn primary sm" :disabled="waiting">
+ <fa-icon v-if="!waiting" icon="check" />
+ <fa-icon v-else class="animate-spin" icon="spinner" />
+ Update
+ </button>
+ <button class="btn sm cancel-btn" :disabled="waiting" @click="resetForm">
+ Cancel
+ </button>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+</template>
+
<style lang="scss">
#password-reset-form{
diff --git a/front-end/src/views/Account/components/settings/Pki.vue b/front-end/src/views/Account/components/settings/Pki.vue
index afe606f..957a188 100644
--- a/front-end/src/views/Account/components/settings/Pki.vue
+++ b/front-end/src/views/Account/components/settings/Pki.vue
@@ -1,128 +1,35 @@
-<template>
- <div id="pki-settings" class="container">
- <div class="panel-content">
-
- <div class="flex flex-row flex-wrap justify-between">
- <h5>PKI Authentication</h5>
- <div class="">
- <div v-if="pkiEnabled" class="button-group">
- <button class="btn xs" @click.prevent="setIsOpen(true)">
- <fa-icon icon="plus" />
- <span class="pl-2">Add Key</span>
- </button>
- <button class="btn red xs" @click.prevent="onDisable">
- <fa-icon icon="minus-circle" />
- <span class="pl-2">Disable</span>
- </button>
- </div>
- <div v-else class="">
- <button class="btn primary xs" @click.prevent="setIsOpen(true)">
- <fa-icon icon="plus" />
- <span class="pl-2">Add Key</span>
- </button>
- </div>
- </div>
-
- <div v-if="store.pkiPublicKeys && store.pkiPublicKeys.length > 0" class="w-full mt-4">
- <table class="min-w-full text-sm divide-y-2 divide-gray-200 dark:divide-dark-500">
- <thead class="text-left">
- <tr>
- <th class="p-2 font-medium whitespace-nowrap dark:text-white" >
- KeyID
- </th>
- <th class="p-2 font-medium whitespace-nowrap dark:text-white">
- Algorithm
- </th>
- <th class="p-2 font-medium whitespace-nowrap dark:text-white">
- Curve
- </th>
- <th class="p-2"></th>
- </tr>
- </thead>
-
- <tbody class="divide-y divide-gray-200 dark:divide-dark-500">
- <tr v-for="key in store.pkiPublicKeys">
- <td class="p-2 t font-medium truncate max-w-[8rem] whitespace-nowrap dark:text-white">
- {{ key.kid }}
- </td>
- <td class="p-2 text-gray-700 whitespace-nowrap dark:text-gray-200">
- {{ key.alg }}
- </td>
- <td class="p-2 text-gray-700 whitespace-nowrap dark:text-gray-200">
- {{ key.crv }}
- </td>
- <td class="p-2 text-right whitespace-nowrap">
- <button class="rounded btn red xs borderless" @click="onRemoveKey(key)">
- <span class="hidden sm:inline">Remove</span>
- <fa-icon icon="trash-can" class="inline sm:hidden" />
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
-
- <p v-else class="p-1 pt-3 text-sm text-color-background">
- PKI authentication is a method of authenticating your user account with signed messages and a shared public key. This method implementation
- uses client signed Json Web Tokens to authenticate user generated outside this website as a One Time Password (OTP). This allows for you to
- use your favorite hardware or software tools, to generate said OTPs to authenticate your user.
- </p>
- </div>
- </div>
- </div>
- <Dialog :open="isOpen" @close="setIsOpen" class="relative z-30">
- <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
-
- <div class="fixed inset-0 flex justify-center">
- <DialogPanel class="w-full max-w-lg p-4 m-auto mt-24 bg-white rounded dark:bg-dark-700 dark:text-gray-300">
- <h4>Configure your authentication key</h4>
- <p class="mt-2 text-sm">
- Please paste your authenticator's public key as a Json Web Key (JWK) object. Your JWK must include a kid (key id) and a kty (key type) field.
- </p>
- <div class="p-2 mt-3">
- <textarea class="w-full p-1 text-sm border dark:bg-dark-700 ring-0 dark:border-dark-400" rows="10" v-model="keyData" />
- </div>
- <div class="flex justify-end gap-2 mt-4">
- <button class="rounded btn sm primary" @click.prevent="onSubmitKeys">Submit</button>
- <button class="rounded btn sm" @click.prevent="setIsOpen(false)">Cancel</button>
- </div>
- </DialogPanel>
- </div>
- </Dialog>
-</template>
-
<script setup lang="ts">
import { includes, isEmpty } from 'lodash-es'
-import { apiCall, useConfirm, useSession, debugLog, useFormToaster, PkiPublicKey } from '@vnuge/vnlib.browser'
+import { apiCall, useConfirm, useSession, debugLog, useFormToaster, type PkiPublicKey, MfaMethod } from '@vnuge/vnlib.browser'
import { computed, ref, watch } from 'vue'
import { Dialog, DialogPanel } from '@headlessui/vue'
import { useStore } from '../../../../store'
-import { } from 'pinia'
+import { useToggle } from '@vueuse/core'
const store = useStore()
const { reveal } = useConfirm()
const { isLocalAccount } = useSession()
const { error } = useFormToaster()
+const { refresh, pkiConfig } = store.pki!
-const pkiEnabled = computed(() => isLocalAccount.value && includes(store.mfaEndabledMethods, "pki") && window.crypto.subtle)
+const pkiEnabled = computed(() => isLocalAccount.value && includes(store.mfa.enabledMethods, "pki" as MfaMethod) && window.crypto.subtle)
+const pkiPublicKeys = computed(() => store.pki!.publicKeys)
-const isOpen = ref(false)
+const [isOpen, toggleOpen] = useToggle()
const keyData = ref('')
const pemFormat = ref(false)
const explicitCurve = ref("")
-watch(isOpen, () =>{
+watch(isOpen, () => {
keyData.value = ''
pemFormat.value = false
explicitCurve.value = ""
//Reload status
- store.mfaRefreshMethods()
+ refresh()
})
-const setIsOpen = (value : boolean) => isOpen.value = value
-
-const onRemoveKey = async (single: PkiPublicKey) =>{
- const { isCanceled } = await reveal({
+const onRemoveKey = async (single: PkiPublicKey) => {
+ const { isCanceled } = await reveal({
title: 'Are you sure?',
text: `This will remove key ${single.kid} from your account.`
})
@@ -130,11 +37,11 @@ const onRemoveKey = async (single: PkiPublicKey) =>{
return;
}
- //Delete pki
+ //Delete pki
await apiCall(async ({ toaster }) => {
-
+
//TODO: require password or some upgrade to disable
- const { success } = await store.pkiConfig.removeKey(single.kid);
+ const { success } = await pkiConfig.removeKey(single.kid);
if (success) {
toaster.general.success({
@@ -150,33 +57,33 @@ const onRemoveKey = async (single: PkiPublicKey) =>{
}
//Refresh the status
- store.mfaRefreshMethods()
+ refresh()
});
}
const onDisable = async () => {
- const { isCanceled } = await reveal({
+ const { isCanceled } = await reveal({
title: 'Are you sure?',
text: 'This will disable PKI authentication for your account.'
})
if (isCanceled) {
- return;
+ return;
}
//Delete pki
- await apiCall(async ({ toaster }) =>{
+ await apiCall(async ({ toaster }) => {
//Disable pki
//TODO: require password or some upgrade to disable
- const { success } = await store.pkiConfig.disable();
-
- if(success){
+ const { success } = await pkiConfig.disable();
+
+ if (success) {
toaster.general.success({
title: 'Success',
text: 'PKI authentication has been disabled.'
})
}
- else{
+ else {
toaster.general.error({
title: 'Error',
text: 'PKI authentication could not be disabled.'
@@ -184,40 +91,40 @@ const onDisable = async () => {
}
//Refresh the status
- store.mfaRefreshMethods()
+ refresh()
});
}
-const onSubmitKeys = async () =>{
-
- if(window.crypto.subtle == null){
+const onSubmitKeys = async () => {
+
+ if (window.crypto.subtle == null) {
error({ title: "Your browser does not support PKI authentication." })
return;
}
-
+
//Validate key data
- if(isEmpty(keyData.value)){
+ if (isEmpty(keyData.value)) {
error({ title: "Please enter key data" })
return;
}
- let jwk : PkiPublicKey & JsonWebKey;
+ let jwk: PkiPublicKey & JsonWebKey;
try {
//Try to parse as jwk
jwk = JSON.parse(keyData.value)
- if(isEmpty(jwk.use)
- || isEmpty(jwk.kty)
- || isEmpty(jwk.alg)
- || isEmpty(jwk.kid)
- || isEmpty(jwk.x)
- || isEmpty(jwk.y)){
+ if (isEmpty(jwk.use)
+ || isEmpty(jwk.kty)
+ || isEmpty(jwk.alg)
+ || isEmpty(jwk.kid)
+ || isEmpty(jwk.x)
+ || isEmpty(jwk.y)) {
throw new Error("Invalid JWK");
}
}
catch (e) {
//Write error to debug log
debugLog(e)
- error({ title:"The key is not a valid Json Web Key (JWK)"})
+ error({ title: "The key is not a valid Json Web Key (JWK)" })
return;
}
@@ -226,7 +133,7 @@ const onSubmitKeys = async () =>{
//init/update the key
//TODO: require password or some upgrade to disable
- const { getResultOrThrow } = await store.pkiConfig.addOrUpdate(jwk);
+ const { getResultOrThrow } = await pkiConfig.addOrUpdate(jwk);
const result = getResultOrThrow();
@@ -234,12 +141,103 @@ const onSubmitKeys = async () =>{
title: 'Success',
text: result
})
- setIsOpen(false)
+ toggleOpen(false)
})
}
</script>
-<style>
+<template>
+ <div id="pki-settings" class="container">
+ <div class="panel-content">
+
+ <div class="flex flex-row flex-wrap justify-between">
+ <h5>PKI Authentication</h5>
+ <div class="">
+ <div v-if="pkiEnabled" class="button-group">
+ <button class="btn xs" @click.prevent="toggleOpen(true)">
+ <fa-icon icon="plus" />
+ <span class="pl-2">Add Key</span>
+ </button>
+ <button class="btn red xs" @click.prevent="onDisable">
+ <fa-icon icon="minus-circle" />
+ <span class="pl-2">Disable</span>
+ </button>
+ </div>
+ <div v-else class="">
+ <button class="btn primary xs" @click.prevent="toggleOpen(true)">
+ <fa-icon icon="plus" />
+ <span class="pl-2">Add Key</span>
+ </button>
+ </div>
+ </div>
+
+ <div v-if="pkiPublicKeys && pkiPublicKeys.length > 0" class="w-full mt-4">
+ <table class="min-w-full text-sm divide-y-2 divide-gray-200 dark:divide-dark-500">
+ <thead class="text-left">
+ <tr>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white" >
+ KeyID
+ </th>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ Algorithm
+ </th>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ Curve
+ </th>
+ <th class="p-2"></th>
+ </tr>
+ </thead>
+
+ <tbody class="divide-y divide-gray-200 dark:divide-dark-500">
+ <tr v-for="key in pkiPublicKeys">
+ <td class="p-2 t font-medium truncate max-w-[8rem] whitespace-nowrap dark:text-white">
+ {{ key.kid }}
+ </td>
+ <td class="p-2 text-gray-700 whitespace-nowrap dark:text-gray-200">
+ {{ key.alg }}
+ </td>
+ <td class="p-2 text-gray-700 whitespace-nowrap dark:text-gray-200">
+ {{ key.crv }}
+ </td>
+ <td class="p-2 text-right whitespace-nowrap">
+ <button class="rounded btn red xs borderless" @click="onRemoveKey(key)">
+ <span class="hidden sm:inline">Remove</span>
+ <fa-icon icon="trash-can" class="inline sm:hidden" />
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <p v-else class="p-1 pt-3 text-sm text-color-background">
+ PKI authentication is a method of authenticating your user account with signed messages and a shared public key. This method implementation
+ uses client signed Json Web Tokens to authenticate user generated outside this website as a One Time Password (OTP). This allows for you to
+ use your favorite hardware or software tools, to generate said OTPs to authenticate your user.
+ </p>
+ </div>
+ </div>
+ </div>
+ <Dialog :open="isOpen" @close="toggleOpen(false)" class="relative z-30">
+ <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
+
+ <div class="fixed inset-0 flex justify-center">
+ <DialogPanel class="w-full max-w-lg p-4 m-auto mt-24 bg-white rounded dark:bg-dark-700 dark:text-gray-300">
+ <h4>Configure your authentication key</h4>
+ <p class="mt-2 text-sm">
+ Please paste your authenticator's public key as a Json Web Key (JWK) object. Your JWK must include a kid (key id) and a kty (key type) field.
+ </p>
+ <div class="p-2 mt-3">
+ <textarea class="w-full p-1 text-sm border dark:bg-dark-700 ring-0 dark:border-dark-400" rows="10" v-model="keyData" />
+ </div>
+ <div class="flex justify-end gap-2 mt-4">
+ <button class="rounded btn sm primary" @click.prevent="onSubmitKeys">Submit</button>
+ <button class="rounded btn sm" @click.prevent="toggleOpen(false)">Cancel</button>
+ </div>
+ </DialogPanel>
+ </div>
+ </Dialog>
+</template>
+
-</style>
diff --git a/front-end/src/views/Account/components/settings/Security.vue b/front-end/src/views/Account/components/settings/Security.vue
index e6075f9..ae0d143 100644
--- a/front-end/src/views/Account/components/settings/Security.vue
+++ b/front-end/src/views/Account/components/settings/Security.vue
@@ -1,3 +1,25 @@
+<script setup lang="ts">
+import { MfaMethod } from '@vnuge/vnlib.browser'
+import { computed } from 'vue'
+import { Switch } from '@headlessui/vue'
+import { includes, isNil } from 'lodash-es'
+import { useStore } from '../../../../store'
+import Fido from './Fido.vue'
+import Pki from './Pki.vue'
+import TotpSettings from './TotpSettings.vue'
+import PasswordReset from './PasswordReset.vue'
+
+const store = useStore();
+
+//Load mfa methods
+store.mfa.refresh();
+
+const fidoEnabled = computed(() => includes(store.mfa.enabledMethods, 'fido' as MfaMethod))
+const totpEnabled = computed(() => includes(store.mfa.enabledMethods, MfaMethod.TOTP))
+const pkiEnabled = computed(() => !isNil(store.pki))
+
+</script>
+
<template>
<div id="account-security-settings">
<div class="panel-container">
@@ -20,20 +42,20 @@
</div>
</div>
- <Pki />
+ <Pki v-if="pkiEnabled" />
<div id="browser-poll-settings" class="panel-content" >
<div class="flex justify-between">
<h5>Keep me logged in</h5>
<div class="pl-1">
<Switch
- v-model="autoHeartbeat"
- :class="autoHeartbeat ? 'bg-primary-500 dark:bg-primary-600' : 'bg-gray-200 dark:bg-dark-500'"
+ v-model="store.autoHeartbeat"
+ :class="store.autoHeartbeat ? 'bg-primary-500 dark:bg-primary-600' : 'bg-gray-200 dark:bg-dark-500'"
class="relative inline-flex items-center h-6 rounded-full w-11"
>
<span class="sr-only">Enable auto heartbeat</span>
<span
- :class="autoHeartbeat ? 'translate-x-6' : 'translate-x-1'"
+ :class="store.autoHeartbeat ? 'translate-x-6' : 'translate-x-1'"
class="inline-block w-4 h-4 transition transform bg-white rounded-full"
/>
</Switch>
@@ -51,29 +73,6 @@
</div>
</template>
-<script setup lang="ts">
-import { MfaMethod } from '@vnuge/vnlib.browser'
-import { computed } from 'vue'
-import { Switch } from '@headlessui/vue'
-import { includes } from 'lodash-es'
-import { storeToRefs } from 'pinia'
-import { useStore } from '../../../../store'
-import Fido from './Fido.vue'
-import Pki from './Pki.vue'
-import TotpSettings from './TotpSettings.vue'
-import PasswordReset from './PasswordReset.vue'
-
-const store = useStore();
-const { autoHeartbeat } = storeToRefs(store);
-
-//Load mfa methods
-store.mfaRefreshMethods();
-
-const fidoEnabled = computed(() => includes(store.mfaEndabledMethods, 'fido' as MfaMethod))
-const totpEnabled = computed(() => includes(store.mfaEndabledMethods, MfaMethod.TOTP))
-
-</script>
-
<style>
#account-security-settings .modal-body{
diff --git a/front-end/src/views/Account/components/settings/Settings.vue b/front-end/src/views/Account/components/settings/Settings.vue
index fb86951..0580b58 100644
--- a/front-end/src/views/Account/components/settings/Settings.vue
+++ b/front-end/src/views/Account/components/settings/Settings.vue
@@ -1,3 +1,7 @@
+<script setup lang="ts">
+import Security from './Security.vue'
+</script>
+
<template>
<div id="account-settings" class="container">
<div class="acnt-content-container">
@@ -5,12 +9,3 @@
</div>
</div>
</template>
-
-<script setup lang="ts">
-import Security from './Security.vue'
-
-</script>
-
-<style>
-
-</style>
diff --git a/front-end/src/views/Account/components/settings/TotpSettings.vue b/front-end/src/views/Account/components/settings/TotpSettings.vue
index 0fcfe31..04a261b 100644
--- a/front-end/src/views/Account/components/settings/TotpSettings.vue
+++ b/front-end/src/views/Account/components/settings/TotpSettings.vue
@@ -1,99 +1,11 @@
-<template>
- <div id="totp-settings">
-
- <div v-if="!isLocalAccount" class="flex flex-row justify-between">
- <h6 class="block">
- TOTP Authenticator App
- </h6>
- <div class="text-red-500">
- Unavailable for external auth
- </div>
- </div>
-
- <div v-else-if="showTotpCode" class="w-full py-2 text-center">
- <h5 class="text-center" />
- <p class="py-2">
- Scan the QR code with your TOTP authenticator app.
- </p>
-
- <div class="flex">
- <VueQrcode class="m-auto" :value="qrCode" />
- </div>
-
- <p class="py-2">
- Your secret, if your application requires it.
- </p>
-
- <p class="flex flex-row flex-wrap justify-center p-2 bg-gray-200 border border-gray-300 dark:bg-dark-800 dark:border-dark-500">
- <span v-for="code in secretSegments" :key="code" class="px-2 font-mono tracking-wider" >
- {{ code }}
- </span>
- </p>
-
- <p class="py-2 text-color-background">
- Please enter your code from your authenticator app to continue.
- </p>
-
- <div class="m-auto w-min">
- <VOtpInput
- class="otp-input"
- input-type="letter-numeric"
- separator=""
- :is-disabled="showSubmitButton"
- input-classes="primary input rounded"
- :num-inputs="6"
- @on-change="onInput"
- @on-complete="VerifyTotp"
- />
- </div>
-
- <div v-if="showSubmitButton" class="flex flex-row justify-end my-2">
- <button class="btn primary" @click.prevent="CloseQrWindow">
- Complete
- </button>
- </div>
- </div>
-
- <div v-else class="flex flex-row flex-wrap justify-between">
- <h6>TOTP Authenticator App</h6>
-
- <div v-if="totpEnabled" class="button-group">
- <button class="btn xs" @click.prevent="regenTotp">
- <fa-icon icon="sync" />
- <span class="pl-2">Regenerate</span>
- </button>
- <button class="btn red xs" @click.prevent="disable">
- <fa-icon icon="minus-circle" />
- <span class="pl-2">Disable</span>
- </button>
- </div>
-
- <div v-else>
- <button class="btn primary xs" @click.prevent="configTotp">
- <fa-icon icon="plus" />
- <span class="pl-2">Setup</span>
- </button>
- </div>
- <p class="p-1 pt-3 text-sm text-color-background">
- TOTP is a time based one time password. You can use it as a form of Multi Factor Authentication when
- using another device such as a smart phone or TOTP hardware device. You can use TOTP with your smart
- phone
- using apps like Google Authenticator, Authy, or Duo. Read more on
- <a class="link" href="https://en.wikipedia.org/wiki/Time-based_one-time_password" target="_blank">
- Wikipedia.
- </a>
- </p>
- </div>
-
- </div>
-</template>
-
<script setup lang="ts">
import { isNil, chunk, defaultTo, includes, map, join } from 'lodash-es'
import { TOTP } from 'otpauth'
-import { computed, ref, defineAsyncComponent } from 'vue'
import base32Encode from 'base32-encode'
-import {
+import QrCodeVue from 'qrcode.vue'
+import VOtpInput from "vue3-otp-input";
+import { computed, ref } from 'vue'
+import {
useSession,
useMessage,
useConfirm,
@@ -104,26 +16,23 @@ import {
import { useStore } from '../../../../store';
import { storeToRefs } from 'pinia';
-const VueQrcode = defineAsyncComponent(() => import('@chenfengyuan/vue-qrcode'))
-const VOtpInput = defineAsyncComponent(() => import('vue3-otp-input'));
-
-interface TotpConfig{
- secret: string;
- readonly issuer: string;
- readonly algorithm: string;
- readonly digits?: number;
- readonly period?: number;
+interface TotpConfig {
+ secret: string;
+ readonly issuer: string;
+ readonly algorithm: string;
+ readonly digits?: number;
+ readonly period?: number;
}
const store = useStore();
-const { userName, isLocalAccount, mfaEndabledMethods } = storeToRefs(store);
+const { isLocalAccount } = storeToRefs(store);
const { KeyStore } = useSession()
const { reveal } = useConfirm()
const { elevatedApiCall } = usePassConfirm()
const { onInput, setMessage } = useMessage()
-const totpEnabled = computed(() => includes(mfaEndabledMethods.value, MfaMethod.TOTP))
+const totpEnabled = computed(() => includes(store.mfa.enabledMethods, MfaMethod.TOTP))
const totpMessage = ref<TotpConfig>()
const showSubmitButton = ref(false)
@@ -152,7 +61,7 @@ const qrCode = computed(() => {
params.append('algorithm', m.algorithm)
params.append('digits', defaultTo(m.digits, 6).toString())
params.append('period', defaultTo(m.period, 30).toString())
- const url = `otpauth://totp/${m.issuer}:${userName.value}?${params.toString()}`
+ const url = `otpauth://totp/${m.issuer}:${store.userName}?${params.toString()}`
return url
})
@@ -160,7 +69,7 @@ const ProcessAddOrUpdate = async () => {
await elevatedApiCall(async ({ password }) => {
// Init or update the totp method and get the encrypted totp message
- const res = await store.mfaConfig.initOrUpdateMethod<TotpConfig>(MfaMethod.TOTP, password);
+ const res = await store.mfa.initOrUpdateMethod<TotpConfig>(MfaMethod.TOTP, password);
//Get the encrypted totp message
const totp = res.getResultOrThrow()
@@ -181,7 +90,7 @@ const configTotp = async () => {
text: 'Are you sure you understand TOTP multi factor and wish to enable it?',
})
- if(!isCanceled){
+ if (!isCanceled) {
ProcessAddOrUpdate()
}
}
@@ -197,7 +106,7 @@ const regenTotp = async () => {
text: 'If you continue your previous TOTP authenticator and recovery codes will no longer be valid.'
})
- if(!isCanceled){
+ if (!isCanceled) {
ProcessAddOrUpdate()
}
}
@@ -214,16 +123,15 @@ const disable = async () => {
}
await elevatedApiCall(async ({ password }) => {
-
// Disable the totp method
- const res = await store.mfaConfig.disableMethod(MfaMethod.TOTP, password)
+ const res = await store.mfa.disableMethod(MfaMethod.TOTP, password)
res.getResultOrThrow()
-
- store.mfaRefreshMethods()
+
+ store.mfa.refresh()
})
}
-const VerifyTotp = async (code : string) => {
+const VerifyTotp = async (code: string) => {
// Create a new TOTP instance from the current message
const totp = new TOTP(totpMessage.value)
@@ -231,10 +139,11 @@ const VerifyTotp = async (code : string) => {
const valid = totp.validate({ token: code, window: 4 })
if (valid) {
- showSubmitButton.value = true
+ showSubmitButton.value = true;
+
toaster.success({
title: 'Success',
- text: 'Your TOTP code is valid and your account is now verified.'
+ text: 'Your TOTP code is valid and is now enabled'
})
} else {
setMessage('Your TOTP code is not valid.')
@@ -244,12 +153,103 @@ const VerifyTotp = async (code : string) => {
const CloseQrWindow = () => {
showSubmitButton.value = false
totpMessage.value = undefined
-
+
//Fresh methods
- store.mfaRefreshMethods()
+ store.mfa.refresh()
}
</script>
+<template>
+ <div id="totp-settings">
+
+ <div v-if="!isLocalAccount" class="flex flex-row justify-between">
+ <h6 class="block">
+ TOTP Authenticator App
+ </h6>
+ <div class="text-red-500">
+ Unavailable for external auth
+ </div>
+ </div>
+
+ <div v-else-if="showTotpCode" class="w-full py-2 text-center">
+ <h5 class="text-center" />
+ <p class="py-2">
+ Scan the QR code with your TOTP authenticator app.
+ </p>
+
+ <div class="flex">
+ <QrCodeVue class="m-auto" :size="180" render-as="svg" level="Q" :value="qrCode" />
+ </div>
+
+ <p class="py-2">
+ Your secret, if your application requires it.
+ </p>
+
+ <p class="flex flex-row flex-wrap justify-center p-2 bg-gray-200 border border-gray-300 dark:bg-dark-800 dark:border-dark-500">
+ <span v-for="code in secretSegments" :key="code" class="px-2 font-mono tracking-wider" >
+ {{ code }}
+ </span>
+ </p>
+
+ <p class="py-2 text-color-background">
+ Please enter your code from your authenticator app to continue.
+ </p>
+
+ <div class="m-auto w-min">
+ <VOtpInput
+ class="otp-input"
+ input-type="letter-numeric"
+ separator=""
+ value=""
+ :is-disabled="showSubmitButton"
+ input-classes="primary input rounded"
+ :num-inputs="6"
+ @on-change="onInput"
+ @on-complete="VerifyTotp"
+ />
+ </div>
+
+ <div v-if="showSubmitButton" class="flex flex-row justify-end my-2">
+ <button class="btn primary" @click.prevent="CloseQrWindow">
+ Complete
+ </button>
+ </div>
+ </div>
+
+ <div v-else class="flex flex-row flex-wrap justify-between">
+ <h6>TOTP Authenticator App</h6>
+
+ <div v-if="totpEnabled" class="button-group">
+ <button class="btn xs" @click.prevent="regenTotp">
+ <fa-icon icon="sync" />
+ <span class="pl-2">Regenerate</span>
+ </button>
+ <button class="btn red xs" @click.prevent="disable">
+ <fa-icon icon="minus-circle" />
+ <span class="pl-2">Disable</span>
+ </button>
+ </div>
+
+ <div v-else>
+ <button class="btn primary xs" @click.prevent="configTotp">
+ <fa-icon icon="plus" />
+ <span class="pl-2">Setup</span>
+ </button>
+ </div>
+ <p class="p-1 pt-3 text-sm text-color-background">
+ TOTP is a time based one time password. You can use it as a form of Multi Factor Authentication when
+ using another device such as a smart phone or TOTP hardware device. You can use TOTP with your smart
+ phone
+ using apps like Google Authenticator, Authy, or Duo. Read more on
+ <a class="link" href="https://en.wikipedia.org/wiki/Time-based_one-time_password" target="_blank">
+ Wikipedia.
+ </a>
+ </p>
+ </div>
+
+ </div>
+</template>
+
<style>
diff --git a/front-end/src/views/Blog/components/Posts.vue b/front-end/src/views/Blog/components/Posts.vue
index 647e093..b89d263 100644
--- a/front-end/src/views/Blog/components/Posts.vue
+++ b/front-end/src/views/Blog/components/Posts.vue
@@ -1,24 +1,3 @@
-<template>
- <div id="post-editor" class="">
- <EditorTable title="Manage posts" :show-edit="showEdit" :pagination="pagination" @open-new="openNew">
- <template #table>
- <PostTable
- :items="items"
- @open-edit="openEdit"
- @delete="onDelete"
- />
- </template>
- <template #editor>
- <PostEditor
- @submit="onSubmit"
- @close="closeEdit(true)"
- @delete="onDelete"
- />
- </template>
- </EditorTable>
- </div>
-</template>
-
<script setup lang="ts">
import { computed, defineAsyncComponent } from 'vue';
import { isEmpty } from 'lodash-es';
@@ -49,7 +28,7 @@ const closeEdit = (update?: boolean) => {
//reload channels
if (update) {
//must refresh content and posts when a post is updated
- refresh();
+ refresh();
}
//Reset page to top
window.scrollTo(0, 0);
@@ -62,14 +41,14 @@ const openNew = () => {
window.scrollTo(0, 0)
}
-const onSubmit = async ({post, content } : { post: PostMeta, content: string }) => {
+const onSubmit = async ({ post, content }: { post: PostMeta, content: string }) => {
debugLog('submitting', post, content);
//Check for new channel, or updating old channel
if (store.posts.selectedId === 'new') {
//Exec create call
- await apiCall(async ({toaster}) => {
+ await apiCall(async ({ toaster }) => {
//endpoint returns the content
const newMeta = await store.posts.add(post);
@@ -86,9 +65,9 @@ const onSubmit = async ({post, content } : { post: PostMeta, content: string })
}
else if (!isEmpty(store.posts.selectedId)) {
//Exec update call
- await apiCall(async ( {toaster} ) => {
+ await apiCall(async ({ toaster }) => {
await store.posts.update(post);
-
+
//Publish the content
await store.content.updatePostContent(post, content)
@@ -129,6 +108,26 @@ const onDelete = async (post: PostMeta) => {
}
</script>
+<template>
+ <div id="post-editor" class="">
+ <EditorTable title="Manage posts" :show-edit="showEdit" :pagination="pagination" @open-new="openNew">
+ <template #table>
+ <PostTable
+ :items="items"
+ @open-edit="openEdit"
+ @delete="onDelete"
+ />
+ </template>
+ <template #editor>
+ <PostEditor
+ @submit="onSubmit"
+ @close="closeEdit(true)"
+ @delete="onDelete"
+ />
+ </template>
+ </EditorTable>
+ </div>
+</template>
<style lang="scss">
diff --git a/front-end/src/views/Blog/components/image-preview-dialog.vue b/front-end/src/views/Blog/components/image-preview-dialog.vue
index 5cfe552..b134d19 100644
--- a/front-end/src/views/Blog/components/image-preview-dialog.vue
+++ b/front-end/src/views/Blog/components/image-preview-dialog.vue
@@ -1,34 +1,14 @@
-<template>
- <div class="">
- <Dialog :open="isOpen" @close="onClose" class="relative z-50">
- <!-- The backdrop, rendered as a fixed sibling to the panel container -->
- <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
-
- <!-- Full-screen container to center the panel -->
- <div class="fixed inset-0 flex items-center justify-center w-screen p-4">
- <!-- The actual dialog panel -->
- <DialogPanel class="p-2 bg-white rounded dark:bg-dark-700" ref="dialog">
- <DialogDescription>
- <img class="preview-image" :src="imgUrl" alt="preview" />
- </DialogDescription>
- <!-- ... -->
- </DialogPanel>
- </div>
- </Dialog>
- </div>
-</template>
-
<script setup lang="ts">
import { ref, computed, watch, toRefs } from 'vue'
import { Dialog, DialogDescription } from '@headlessui/vue'
-import { onClickOutside } from '@vueuse/core';
+import { onClickOutside, set } from '@vueuse/core';
import { useStore } from '../../../store';
import { ContentMeta } from '../../../../../lib/admin/dist';
import { isNil } from 'lodash-es';
import { apiCall } from '@vnuge/vnlib.browser';
const emit = defineEmits(['close'])
-const props = defineProps<{
+const props = defineProps<{
item: ContentMeta | undefined,
}>()
@@ -52,19 +32,33 @@ const downloadImage = (item: ContentMeta) => {
})
}
-//load the image when open
-watch(item, (item) => {
- if (isNil(item)) {
- imgUrl.value = undefined
- } else {
- downloadImage(item)
- }
-})
+//load the image when open or remove it if the item is undefined
+watch(item, (item) => isNil(item) ? set(imgUrl, undefined) : downloadImage(item))
//Close dialog when clicking outside
onClickOutside(dialog, onClose)
</script>
+
+<template>
+ <div class="">
+ <Dialog :open="isOpen" @close="onClose" class="relative z-50">
+ <!-- The backdrop, rendered as a fixed sibling to the panel container -->
+ <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
+
+ <!-- Full-screen container to center the panel -->
+ <div class="fixed inset-0 flex items-center justify-center w-screen p-4">
+ <!-- The actual dialog panel -->
+ <DialogPanel class="p-2 bg-white rounded dark:bg-dark-700" ref="dialog">
+ <DialogDescription>
+ <img class="preview-image" :src="imgUrl" alt="preview" />
+ </DialogDescription>
+ <!-- ... -->
+ </DialogPanel>
+ </div>
+ </Dialog>
+ </div>
+</template>
<style lang="scss">
.preview-image {
diff --git a/front-end/src/views/Blog/index.vue b/front-end/src/views/Blog/index.vue
index de435ec..af6ab88 100644
--- a/front-end/src/views/Blog/index.vue
+++ b/front-end/src/views/Blog/index.vue
@@ -1,3 +1,35 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useRouteQuery } from '@vueuse/router';
+import { TabGroup, TabList, Tab, TabPanels, TabPanel, Switch } from '@headlessui/vue'
+import { defer, first } from 'lodash-es';
+import { useStore, SortType } from '../../store';
+import Channels from './components/Channels.vue';
+import Posts from './components/Posts.vue';
+import Content from './components/Content.vue';
+
+//Protect page
+const store = useStore()
+store.setPageTitle('Blog Admin')
+
+const firstLetter = computed(() => first(store.userName))
+const tabIdQ = useRouteQuery<string>('tabid', '', { mode: 'push' })
+
+//Map queries to their respective computed values
+const tabId = computed(() => tabIdQ.value ? parseInt(tabIdQ.value) : 0);
+const lastModified = computed({
+ get: () => store.queryState.sort === SortType.ModifiedTime,
+ set: (value: boolean) => {
+ store.queryState.sort = value ? SortType.ModifiedTime : SortType.CreatedTime
+ }
+})
+
+const onTabChange = (id: number) => tabIdQ.value = id.toString(10)
+
+//Load channels on page load
+defer(() => store.channels.refresh());
+
+</script>
<template>
<div class="container mx-auto mt-10 mb-[10rem]">
<div id="blog-admin-template" class="">
@@ -107,40 +139,6 @@
</div>
</template>
-<script setup lang="ts">
-import { computed } from 'vue';
-import { useRouteQuery } from '@vueuse/router';
-import { TabGroup, TabList, Tab, TabPanels, TabPanel, Switch } from '@headlessui/vue'
-import { defer, first } from 'lodash-es';
-import { useStore, SortType } from '../../store';
-import Channels from './components/Channels.vue';
-import Posts from './components/Posts.vue';
-import Content from './components/Content.vue';
-
-
-//Protect page
-const store = useStore()
-store.setPageTitle('Blog Admin')
-
-const firstLetter = computed(() => first(store.userName))
-const tabIdQ = useRouteQuery<string>('tabid', '', { mode: 'push' })
-
-//Map queries to their respective computed values
-const tabId = computed(() => tabIdQ.value ? parseInt(tabIdQ.value) : 0);
-const lastModified = computed({
- get :() => store.queryState.sort === SortType.ModifiedTime,
- set: (value:boolean) => {
- store.queryState.sort = value ? SortType.ModifiedTime : SortType.CreatedTime
- }
-})
-
-const onTabChange = (id:number) => tabIdQ.value = id.toString(10)
-
-//Load channels on page load
-defer(() => store.channels.refresh());
-
-</script>
-
<style lang="scss">
#blog-admin-template{
diff --git a/front-end/src/views/Login/components/Social.vue b/front-end/src/views/Login/components/Social.vue
index 4d22074..7e92d99 100644
--- a/front-end/src/views/Login/components/Social.vue
+++ b/front-end/src/views/Login/components/Social.vue
@@ -1,19 +1,3 @@
-<template>
- <div class="flex flex-col gap-3">
- <div v-for="method in methods" :key="method.Id" class="">
- <button
- type="submit"
- class="btn social-button"
- :disabled="waiting"
- @click.prevent="submitLogin(method)"
- >
- <fa-icon :icon="getIcon(method)" size="xl" />
- Login with {{ capitalize(method.Id) }}
- </button>
- </div>
- </div>
-</template>
-
<script setup lang="ts">
import { shallowRef } from 'vue'
import { apiCall, useWait, type OAuthMethod } from '@vnuge/vnlib.browser'
@@ -43,4 +27,22 @@ const getIcon = (method: OAuthMethod): string[] => {
//Load methods once the fetch completes
store.socialOauth().then(m => methods.value = m.methods);
-</script> \ No newline at end of file
+</script>
+
+<template>
+
+ <div class="flex flex-col gap-3">
+ <div v-for="method in methods" :key="method.Id" class="">
+ <button
+ type="submit"
+ class="btn social-button"
+ :disabled="waiting"
+ @click.prevent="submitLogin(method)"
+ >
+ <fa-icon :icon="getIcon(method)" size="xl" />
+ Login with {{ capitalize(method.Id) }}
+ </button>
+ </div>
+ </div>
+
+</template>
diff --git a/front-end/src/views/Login/components/UserPass.vue b/front-end/src/views/Login/components/UserPass.vue
index 442abb1..bc9d8d1 100644
--- a/front-end/src/views/Login/components/UserPass.vue
+++ b/front-end/src/views/Login/components/UserPass.vue
@@ -55,14 +55,15 @@
</template>
<script setup lang="ts">
-import { ref, shallowRef, reactive, defineAsyncComponent, type Ref } from 'vue'
+import { ref, shallowRef, reactive, defineAsyncComponent, 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 {
useVuelidateWrapper, useMfaLogin, totpMfaProcessor, IMfaFlowContinuiation, MfaMethod,
- apiCall, useMessage, useWait, debugLog, WebMessage
+ apiCall, useMessage, useWait, debugLog, WebMessage,
+ type VuelidateInstance
} from '@vnuge/vnlib.browser'
const Totp = defineAsyncComponent(() => import('./Totp.vue'))
@@ -97,7 +98,7 @@ const rules = {
}
const v$ = useVuelidate(rules, vState)
-const { validate } = useVuelidateWrapper(v$);
+const { validate } = useVuelidateWrapper(v$ as Ref<VuelidateInstance>);
const SubmitLogin = async () => {
diff --git a/front-end/src/views/Login/index.vue b/front-end/src/views/Login/index.vue
index 6a55aeb..476ebf4 100644
--- a/front-end/src/views/Login/index.vue
+++ b/front-end/src/views/Login/index.vue
@@ -1,3 +1,38 @@
+<script setup lang="ts">
+import { computed } 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'
+
+const store = useStore();
+const { loggedIn } = storeToRefs(store)
+const pkiEnabled = computed(() => !isNil(store.pki?.pkiAuth))
+
+store.setPageTitle('Login')
+
+const { waiting } = useWait()
+
+const submitLogout = async () => {
+ //Submit logout request
+ await apiCall(async ({ toaster }) => {
+ const { logout } = await store.socialOauth()
+ // Attempt to logout
+ await logout()
+ // Push a new toast message
+ toaster.general.success({
+ id: 'logout-success',
+ title: 'Success',
+ text: 'You have been logged out',
+ duration: 5000
+ })
+ })
+}
+
+</script>
+
<template>
<div id="login-template" class="app-component-entry">
<div class="login-container">
@@ -25,7 +60,7 @@
<Social />
<!-- pki button, forward to the pki route -->
- <div v-if="pkiEnabled" class="mt-3">
+ <div v-if="pkiEnabled" class="mt-4">
<router-link to="/login/pki">
<button type="submit" class="btn red social-button" :disabled="waiting">
<fa-icon :icon="['fa','certificate']" size="xl" />
@@ -39,44 +74,6 @@
</div>
</template>
-<script setup lang="ts">
-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'
-
-//pki enabled flag from env
-const pkiEnabled = !isNil(import.meta.env.VITE_PKI_ENABLED);
-
-const store = useStore();
-const { loggedIn } = storeToRefs(store)
-
-store.setPageTitle('Login')
-
-const { waiting } = useWait()
-
-const submitLogout = async () => {
- //Submit logout request
- await apiCall(async ({ toaster }) => {
- // Attempt to logout
- const { logout } = await store.socialOauth()
- await logout()
-
- // Push a new toast message
- toaster.general.success({
- id: 'logout-success',
- title: 'Success',
- text: 'You have been logged out',
- duration: 5000
- })
- })
-}
-
-</script>
-
<style lang="scss">
#login-template {
.login-container{
diff --git a/front-end/src/views/Login/pki/index.vue b/front-end/src/views/Login/pki/index.vue
index 585942a..8edd063 100644
--- a/front-end/src/views/Login/pki/index.vue
+++ b/front-end/src/views/Login/pki/index.vue
@@ -1,3 +1,38 @@
+<script setup lang="ts">
+import { isEmpty } from 'lodash-es';
+import { apiCall, debugLog, useMessage } from '@vnuge/vnlib.browser';
+import { ref } from 'vue'
+import { decodeJwt } from 'jose'
+import { useRouter } from 'vue-router';
+import { useStore } from '../../../store';
+
+const { setMessage } = useMessage()
+const { push } = useRouter()
+const store = useStore()
+
+const otp = ref('')
+
+const submit = () => {
+
+ apiCall(async () => {
+ if (isEmpty(otp.value)) {
+ setMessage('Please enter your OTP')
+ return
+ }
+
+ //try to decode the jwt to confirm its form is valid
+ const jwt = decodeJwt(otp.value)
+ debugLog(jwt)
+
+ await store.pki!.pkiAuth.login(otp.value)
+
+ //Go back to login page
+ push({ name: 'Login' })
+ })
+}
+
+</script>
+
<template>
<div id="pki-login-template" class="app-component-entry">
<div class="container max-w-lg mx-auto mt-6 lg:mt-20">
@@ -30,38 +65,3 @@
</div>
</div>
</template>
-
-<script setup lang="ts">
-import { isEmpty } from 'lodash-es';
-import { apiCall, debugLog, useMessage } from '@vnuge/vnlib.browser';
-import { ref } from 'vue'
-import { decodeJwt } from 'jose'
-import { useRouter } from 'vue-router';
-import { useStore } from '../../../store';
-
-const { setMessage } = useMessage()
-const { push } = useRouter()
-const store = useStore()
-
-const otp = ref('')
-
-const submit = () =>{
-
- apiCall(async () =>{
- if(isEmpty(otp.value)){
- setMessage('Please enter your OTP')
- return
- }
-
- //try to decode the jwt to confirm its form is valid
- const jwt = decodeJwt(otp.value)
- debugLog(jwt)
-
- await store.pkiAuth.login(otp.value)
-
- //Go back to login page
- push({ name: 'Login' })
- })
-}
-
-</script> \ No newline at end of file
diff --git a/front-end/src/views/Login/social/[type].vue b/front-end/src/views/Login/social/[type].vue
index 51da94f..f011f9c 100644
--- a/front-end/src/views/Login/social/[type].vue
+++ b/front-end/src/views/Login/social/[type].vue
@@ -1,35 +1,3 @@
-<template>
- <div id="social-login-template" class="app-component-entry">
- <div class="container flex flex-col m-auto my-16">
- <div id="social-final-template" class="flex justify-center">
- <div class="entry-container">
- <h3>Finalizing login</h3>
- <div class="mt-6 mb-4">
- <div v-if="message?.length > 0" class="text-lg text-red-500 dark:text-rose-500">
- <p>{{ message }}</p>
- <div class="flex justify-center mt-5">
- <router-link to="/login">
- <button type="submit" class="btn primary" :disabled="waiting">
- <fa-icon icon="sign-in-alt" />
- Try again
- </button>
- </router-link>
- </div>
- </div>
- <div v-else>
- <div class="flex justify-center">
- <div class="m-auto">
- <fa-icon class="animate-spin" icon="spinner" size="2x"/>
- </div>
- </div>
- <p>Please wait while we log you in.</p>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-</template>
<script setup lang="ts">
import { defer } from 'lodash-es'
@@ -62,19 +30,20 @@ tryOnMounted(() => defer(() => {
//try to complete an oauth login
apiCall(async ({ toaster }) => {
- try{
- //Complete the login
- const { completeLogin } = await store.socialOauth();
- await completeLogin()
-
- toaster.general.success({
- title:'Login Successful',
- text: 'You have successfully logged in.'
- })
-
- router.push({ name: 'Login' })
+ try {
+ const { completeLogin } = await store.socialOauth();
+
+ //Complete the login
+ await completeLogin();
+
+ toaster.general.success({
+ title: 'Login Successful',
+ text: 'You have successfully logged in.'
+ })
+
+ router.push({ name: 'Login' })
}
- catch(err: any){
+ catch (err: any) {
set(message, err.message)
}
})
@@ -82,6 +51,39 @@ tryOnMounted(() => defer(() => {
</script>
+<template>
+ <div id="social-login-template" class="app-component-entry">
+ <div class="container flex flex-col m-auto my-16">
+ <div id="social-final-template" class="flex justify-center">
+ <div class="entry-container">
+ <h3>Finalizing login</h3>
+ <div class="mt-6 mb-4">
+ <div v-if="message?.length > 0" class="text-lg text-red-500 dark:text-rose-500">
+ <p>{{ message }}</p>
+ <div class="flex justify-center mt-5">
+ <router-link to="/login">
+ <button type="submit" class="btn primary" :disabled="waiting">
+ <fa-icon icon="sign-in-alt" />
+ Try again
+ </button>
+ </router-link>
+ </div>
+ </div>
+ <div v-else>
+ <div class="flex justify-center">
+ <div class="m-auto">
+ <fa-icon class="animate-spin" icon="spinner" size="2x"/>
+ </div>
+ </div>
+ <p>Please wait while we log you in.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
<style lang="scss">
#social-login-template{