diff options
Diffstat (limited to 'front-end/src/store')
-rw-r--r-- | front-end/src/store/globalState.ts | 30 | ||||
-rw-r--r-- | front-end/src/store/index.ts | 49 | ||||
-rw-r--r-- | front-end/src/store/mfaSettingsPlugin.ts | 62 | ||||
-rw-r--r-- | front-end/src/store/pageProtectionPlugin.ts | 79 | ||||
-rw-r--r-- | front-end/src/store/socialMfaPlugin.ts | 42 | ||||
-rw-r--r-- | front-end/src/store/userProfile.ts | 82 |
6 files changed, 344 insertions, 0 deletions
diff --git a/front-end/src/store/globalState.ts b/front-end/src/store/globalState.ts new file mode 100644 index 0000000..9e700eb --- /dev/null +++ b/front-end/src/store/globalState.ts @@ -0,0 +1,30 @@ +import 'pinia' +import { shallowRef } from 'vue'; +import { useAutoHeartbeat } from '@vnuge/vnlib.browser'; +import { toRefs, useLocalStorage } from '@vueuse/core'; +import { PiniaPluginContext, PiniaPlugin } from 'pinia' +import { defaults } from 'lodash-es'; + +declare module 'pinia' { + export interface PiniaCustomProperties { + autoHeartbeat: boolean; + } +} + +export const globalStatePlugin: PiniaPlugin = ({ store }: PiniaPluginContext) =>{ + + //Get shared global state storage + const mainState = useLocalStorage("vn-state", { ahEnabled: false }); + + //Set defaults to avoid undefined errors from toRefs + defaults(mainState.value, { ahEnabled: false }) + + const { ahEnabled } = toRefs(mainState) + + //Setup heartbeat for 5 minutes + useAutoHeartbeat(shallowRef(5 * 60 * 1000), ahEnabled) + + return{ + autoHeartbeat: ahEnabled, + } +}
\ No newline at end of file diff --git a/front-end/src/store/index.ts b/front-end/src/store/index.ts new file mode 100644 index 0000000..924bf1b --- /dev/null +++ b/front-end/src/store/index.ts @@ -0,0 +1,49 @@ +import { useSession } from "@vnuge/vnlib.browser"; +import { set } from "@vueuse/core"; +import { defineStore } from "pinia"; +import { computed, shallowRef } from "vue"; + +/** + * Loads the main store for the application + */ +export const useStore = defineStore('main', () => { + + const { loggedIn, isLocalAccount } = useSession(); + + //MANAGED STATE + const headerRoutes = shallowRef(Array<string>()); + const authRoutes = shallowRef(Array<string>()); + const siteTitle = shallowRef(""); + const pageTitle = shallowRef(""); + const showCookieWarning = shallowRef(!navigator.cookieEnabled); //Default to current cookie status + + /** + * The current routes to display in the header depending on the + * user's login status + */ + const currentRoutes = computed(() => loggedIn.value ? authRoutes.value : headerRoutes.value); + + const setHeaderRouteNames = (routeNames: string[], authRouteNames: string[]) => { + set(headerRoutes, [...routeNames]); + set(authRoutes, [...authRouteNames]); + } + + const setSiteTitle = (title: string) => set(siteTitle, title); + const setPageTitle = (title: string) => set(pageTitle, title); + const setCookieWarning = (show: boolean) => set(showCookieWarning, show); + + return{ + loggedIn, + isLocalAccount, + headerRoutes, + authRoutes, + siteTitle, + pageTitle, + showCookieWarning, + setCookieWarning, + setPageTitle, + currentRoutes, + setHeaderRouteNames, + setSiteTitle + } +}) diff --git a/front-end/src/store/mfaSettingsPlugin.ts b/front-end/src/store/mfaSettingsPlugin.ts new file mode 100644 index 0000000..dffafce --- /dev/null +++ b/front-end/src/store/mfaSettingsPlugin.ts @@ -0,0 +1,62 @@ +import 'pinia' +import { MaybeRef, shallowRef, watch } from 'vue'; +import { MfaMethod, PkiPublicKey, apiCall, useMfaConfig, usePkiConfig, usePkiAuth } from '@vnuge/vnlib.browser'; +import { useToggle, get } from '@vueuse/core'; +import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' +import { includes } from 'lodash-es'; + +declare module 'pinia' { + export interface PiniaCustomProperties { + mfaEndabledMethods: MfaMethod[] + mfaConfig: ReturnType<typeof useMfaConfig> + pkiConfig: ReturnType<typeof usePkiConfig> + pkiAuth: ReturnType<typeof usePkiAuth> + pkiPublicKeys: PkiPublicKey[] + mfaRefreshMethods: () => void + } +} + +export const mfaSettingsPlugin = (mfaEndpoint: MaybeRef<string>, pkiEndpoint?:MaybeRef<string>): PiniaPlugin => { + + return ({ store }: PiniaPluginContext) => { + + const { loggedIn } = storeToRefs(store) + const mfaConfig = useMfaConfig(mfaEndpoint) + const pkiConfig = usePkiConfig(pkiEndpoint || '/') + const pkiAuth = usePkiAuth(pkiEndpoint || '/') + const [onRefresh, mfaRefreshMethods] = useToggle() + + const mfaEndabledMethods = shallowRef<MfaMethod[]>([]) + const pkiPublicKeys = shallowRef<PkiPublicKey[]>([]) + + watch([loggedIn, onRefresh], ([ li ]) => { + if(!li){ + mfaEndabledMethods.value = [] + return + } + + //load the mfa methods if the user is logged in + apiCall(async () => mfaEndabledMethods.value = await mfaConfig.getMethods()) + }) + + //Watch for changes to mfa methods (refresh) and update the pki keys + watch([mfaEndabledMethods], ([ methods ]) => { + if(!includes(methods, 'pki' as MfaMethod) || !get(pkiEndpoint)){ + pkiPublicKeys.value = [] + return + } + + //load the pki keys if pki is enabled + apiCall(async () => pkiPublicKeys.value = await pkiConfig.getAllKeys()) + }) + + return{ + mfaRefreshMethods, + mfaEndabledMethods, + mfaConfig, + pkiConfig, + pkiAuth, + pkiPublicKeys + } + } +}
\ No newline at end of file diff --git a/front-end/src/store/pageProtectionPlugin.ts b/front-end/src/store/pageProtectionPlugin.ts new file mode 100644 index 0000000..9831dad --- /dev/null +++ b/front-end/src/store/pageProtectionPlugin.ts @@ -0,0 +1,79 @@ +import 'pinia' +import { watch } from 'vue'; +import { } from '@vnuge/vnlib.browser'; +import { useSessionStorage, get, set } from '@vueuse/core'; +import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' +import { includes, startsWith } from 'lodash-es'; +import { useRouter } from 'vue-router'; + +declare module 'pinia' { + export interface PiniaCustomProperties { + + } +} + +export const pageProtectionPlugin = (router: ReturnType<typeof useRouter>): PiniaPlugin => { + + return ({ store }: PiniaPluginContext) => { + + const { loggedIn, headerRoutes } = storeToRefs(store) + + const lastPageStore = useSessionStorage('lastPageStore', undefined) + + //Setup nav guards + router.beforeEach((to, from) => { + if(!to.name){ + return true; + } + + //see if to route is login route + if(startsWith(to.name as string, 'Login')){ + + //If the user is logged-in, redirect to the last page + if(get(loggedIn) === true){ + const lastPath = get(lastPageStore); + set(lastPageStore, undefined) //Clear the last page + + return lastPath ? { path: lastPath } : true; + } + else{ + //Set last page as from page + set(lastPageStore, from.fullPath) + } + return true; + } + + //See if the to route is not in allowed routes + if (!includes(get(headerRoutes), to.name as string)){ + //Check if the user is logged in + if(get(loggedIn) === false){ + + //Save the last page + set(lastPageStore, to.fullPath) + + //Redirect to login + return { name: 'Login' } + } + } + + //Allow + return true; + }) + + router.afterEach(() => { + //scroll window back to top + window.scrollTo(0, 0) + }) + + watch(loggedIn, (loggedIn) => { + //If the user gets logged out, redirect to login + if(loggedIn === false && router.currentRoute.value.name !== 'Login'){ + router.push({ name: 'Login' }) + } + }) + + return { + + } + } +}
\ No newline at end of file diff --git a/front-end/src/store/socialMfaPlugin.ts b/front-end/src/store/socialMfaPlugin.ts new file mode 100644 index 0000000..d328399 --- /dev/null +++ b/front-end/src/store/socialMfaPlugin.ts @@ -0,0 +1,42 @@ + +import 'pinia' +import { } from 'vue'; +import { useSocialOauthLogin, createSocialMethod, useUser } from '@vnuge/vnlib.browser' +import { } from '@vueuse/core'; +import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia' +import { } from 'lodash-es'; + +declare module 'pinia' { + export interface PiniaCustomProperties { + socialOauth: ReturnType<typeof useSocialOauthLogin> + } +} + +export const socialMfaPlugin: PiniaPlugin = ({ store }: PiniaPluginContext) => { + + const { } = storeToRefs(store) + const { logout } = useUser() + + //Setup social providers + const socialOauth = useSocialOauthLogin([ + //createSocialMethod('github', '/login/social/github'), + //createSocialMethod('discord', '/login/social/discord'), + ]) + + /** + * Override the logout function to default to a social logout, + * if the social logout fails, then we will logout the user + */ + + const logoutFunc = socialOauth.logout; + + (socialOauth as any).logout = async () => { + if (await logoutFunc() === false) { + await logout() + } + } + + return { + socialOauth + } +}
\ No newline at end of file diff --git a/front-end/src/store/userProfile.ts b/front-end/src/store/userProfile.ts new file mode 100644 index 0000000..a4ea469 --- /dev/null +++ b/front-end/src/store/userProfile.ts @@ -0,0 +1,82 @@ +import 'pinia' +import { MaybeRef, watch } from 'vue'; +import { ServerDataBuffer, ServerObjectBuffer, UserProfile, WebMessage, apiCall, useAxios, useDataBuffer, useUser } from '@vnuge/vnlib.browser'; +import { get, useToggle } from '@vueuse/core'; +import { PiniaPlugin, PiniaPluginContext, storeToRefs } from 'pinia' +import { defer } from 'lodash-es'; + +export interface OAuth2Application { + readonly Id: string, + readonly name: string, + readonly description: string, + readonly permissions: string[], + readonly client_id: string, + readonly Created: Date, + readonly LastModified: Date, +} + +export interface NewAppResponse { + readonly secret: string + readonly app: ServerDataBuffer<OAuth2Application, WebMessage<string>> +} + +interface ExUserProfile extends UserProfile { + created: string | Date +} + +declare module 'pinia' { + export interface PiniaCustomProperties { + userProfile: ServerDataBuffer<ExUserProfile, WebMessage<string>> + userName: string | undefined + refreshProfile(): void; + } +} + +export const profilePlugin = (accountsUrl:MaybeRef<string>) :PiniaPlugin => { + + return ({ store }: PiniaPluginContext) => { + + const { loggedIn } = storeToRefs(store) + const { getProfile, userName } = useUser() + const axios = useAxios(null) + + const [onRefresh, refreshProfile] = useToggle() + + const updateUserProfile = async (profile: ServerObjectBuffer<ExUserProfile>) => { + // Apply the buffer to the profile + const { data } = await axios.post<WebMessage<string>>(get(accountsUrl), profile.buffer) + + //Get the new profile from the server + const newProfile = await getProfile() as ExUserProfile + + //Apply the new profile to the buffer + profile.apply(newProfile) + + return data; + } + + const userProfile = useDataBuffer({} as any, updateUserProfile) + + const loadProfile = async () => { + //Get the user profile + const profile = await getProfile() as ExUserProfile + //Apply the profile to the buffer + userProfile.apply(profile) + } + + watch([loggedIn, onRefresh], ([li]) => { + //If the user is logged in, load the profile buffer + if (li) { + apiCall(loadProfile) + } + }) + + defer(refreshProfile); + + return { + userProfile, + refreshProfile, + userName + } + } +}
\ No newline at end of file |