aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/store
diff options
context:
space:
mode:
authorLibravatar Vaughn Nugent <public@vaughnnugent.com>2024-01-31 14:31:45 -0500
committerLibravatar Vaughn Nugent <public@vaughnnugent.com>2024-01-31 14:31:45 -0500
commite4000144e6236f8afc3e2b7d6d11ff50248be1bd (patch)
tree77853294b3dcced363ea65791a3baeb3fbafb1eb /front-end/src/store
parent2889b3dd10de9a5df42f3f8ab2171b52893ac3f4 (diff)
parentf3661776f679a517f2b4fce0a8f583d1a2b0f854 (diff)
Merges pull request #1 cmnext-cli
Master changes to cli branch
Diffstat (limited to 'front-end/src/store')
-rw-r--r--front-end/src/store/index.ts4
-rw-r--r--front-end/src/store/mfaSettingsPlugin.ts109
-rw-r--r--front-end/src/store/oauthAppsPlugin.ts154
-rw-r--r--front-end/src/store/pageProtectionPlugin.ts10
-rw-r--r--front-end/src/store/socialMfaPlugin.ts31
-rw-r--r--front-end/src/store/userProfile.ts30
6 files changed, 272 insertions, 66 deletions
diff --git a/front-end/src/store/index.ts b/front-end/src/store/index.ts
index 1b2d7ee..936dddf 100644
--- a/front-end/src/store/index.ts
+++ b/front-end/src/store/index.ts
@@ -16,10 +16,12 @@
import { useSession } from "@vnuge/vnlib.browser";
import { set } from "@vueuse/core";
import { defineStore } from "pinia";
-import { computed, shallowRef } from "vue";
+import { computed, shallowRef, type UnwrapNestedRefs } from "vue";
export { SortType, QueryType } from './sharedTypes'
+export const storeExport = <T>(val: T): UnwrapNestedRefs<T> => val as UnwrapNestedRefs<T>;
+
/**
* Loads the main store for the application
*/
diff --git a/front-end/src/store/mfaSettingsPlugin.ts b/front-end/src/store/mfaSettingsPlugin.ts
index dffafce..b801f32 100644
--- a/front-end/src/store/mfaSettingsPlugin.ts
+++ b/front-end/src/store/mfaSettingsPlugin.ts
@@ -1,62 +1,99 @@
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 { MaybeRef, ref, shallowRef, watch } from 'vue';
+import { MfaMethod, PkiPublicKey, apiCall, useMfaConfig, usePkiConfig, usePkiAuth, MfaApi } from '@vnuge/vnlib.browser';
+import { useToggle, get, set } from '@vueuse/core';
import { PiniaPluginContext, PiniaPlugin, storeToRefs } from 'pinia'
import { includes } from 'lodash-es';
+import { storeExport, } from './index';
+
+interface PkiStore {
+ publicKeys: PkiPublicKey[]
+ pkiConfig: ReturnType<typeof usePkiConfig>
+ pkiAuth: ReturnType<typeof usePkiAuth>
+ refresh: () => void
+}
+
+export interface MfaSettingsStore{
+ mfa:{
+ enabledMethods: MfaMethod[]
+ refresh: () => void
+ } & MfaApi
+ pki?: PkiStore
+}
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 interface PiniaCustomProperties extends MfaSettingsStore {
+
}
}
export const mfaSettingsPlugin = (mfaEndpoint: MaybeRef<string>, pkiEndpoint?:MaybeRef<string>): PiniaPlugin => {
- return ({ store }: PiniaPluginContext) => {
+ return ({ store }: PiniaPluginContext): MfaSettingsStore => {
const { loggedIn } = storeToRefs(store)
const mfaConfig = useMfaConfig(mfaEndpoint)
- const pkiConfig = usePkiConfig(pkiEndpoint || '/')
- const pkiAuth = usePkiAuth(pkiEndpoint || '/')
- const [onRefresh, mfaRefreshMethods] = useToggle()
+
+ const [onRefresh, refresh] = useToggle()
+
+ const enabledMethods = ref<MfaMethod[]>([])
+
+ const usePki = () => {
+
+ const publicKeys = shallowRef<PkiPublicKey[]>([])
+
+ const pkiConfig = usePkiConfig(pkiEndpoint || '/')
+ const pkiAuth = usePkiAuth(pkiEndpoint || '/')
+
+ //Watch for changes to mfa methods (refresh) and update the pki keys
+ watch([enabledMethods], ([methods]) => {
+ if (!includes(methods, 'pki' as MfaMethod) || !get(pkiEndpoint)) {
+ set(publicKeys, [])
+ return
+ }
- const mfaEndabledMethods = shallowRef<MfaMethod[]>([])
- const pkiPublicKeys = shallowRef<PkiPublicKey[]>([])
+ //load the pki keys if pki is enabled
+ apiCall(async () => publicKeys.value = await pkiConfig.getAllKeys())
+ })
+
+ return{
+ publicKeys,
+ pkiConfig,
+ pkiAuth,
+ refresh
+ }
+ }
watch([loggedIn, onRefresh], ([ li ]) => {
if(!li){
- mfaEndabledMethods.value = []
+ set(enabledMethods, [])
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())
+ apiCall(async () => enabledMethods.value = await mfaConfig.getMethods())
})
- return{
- mfaRefreshMethods,
- mfaEndabledMethods,
- mfaConfig,
- pkiConfig,
- pkiAuth,
- pkiPublicKeys
+ //Only return the pki store if pki is enabled
+ if(get(pkiEndpoint)){
+ return storeExport({
+ mfa:{
+ enabledMethods,
+ refresh,
+ ...mfaConfig
+ },
+ pki: usePki()
+ })
+ }
+ else{
+ return storeExport({
+ mfa:{
+ enabledMethods,
+ refresh,
+ ...mfaConfig
+ },
+ })
+
}
}
} \ No newline at end of file
diff --git a/front-end/src/store/oauthAppsPlugin.ts b/front-end/src/store/oauthAppsPlugin.ts
new file mode 100644
index 0000000..7a76992
--- /dev/null
+++ b/front-end/src/store/oauthAppsPlugin.ts
@@ -0,0 +1,154 @@
+import 'pinia'
+import { MaybeRef, computed, ref, shallowRef, watch } from 'vue';
+import { apiCall, useAxios } from '@vnuge/vnlib.browser';
+import { get, set, useToggle } from '@vueuse/core';
+import { PiniaPlugin, PiniaPluginContext, storeToRefs } from 'pinia'
+import { map, sortBy, isArray } from 'lodash-es';
+import { storeExport } from '.';
+
+export interface OAuth2Application {
+ readonly Id: string,
+ readonly name: string,
+ readonly description: string,
+ readonly permissions: string[],
+ readonly client_id: string,
+ Created: Date,
+ readonly LastModified: Date,
+}
+
+export interface NewAppResponse {
+ readonly secret: string
+ readonly app: OAuth2Application
+}
+
+export interface Oauth2Store{
+ oauth2: {
+ apps: OAuth2Application[],
+ scopes: string[],
+ getApps(): Promise<OAuth2Application[]>
+ createApp(app: OAuth2Application): Promise<NewAppResponse>
+ updateAppSecret(app: OAuth2Application, password: string): Promise<string>
+ updateAppMeta(app: OAuth2Application): Promise<void>
+ deleteApp(app: OAuth2Application, password: string): Promise<void>
+ refresh(): void
+ }
+}
+
+declare module 'pinia' {
+ export interface PiniaCustomProperties extends Oauth2Store{
+
+ }
+}
+
+export const oauth2AppsPlugin = (o2EndpointUrl: MaybeRef<string>, scopeEndpoint: MaybeRef<string>): PiniaPlugin =>{
+
+ return ({ store }: PiniaPluginContext): Oauth2Store => {
+
+ const axios = useAxios(null);
+ const { loggedIn } = storeToRefs(store)
+
+ const [onRefresh, refresh] = useToggle()
+
+ const _oauth2Apps = shallowRef<OAuth2Application[]>([])
+ const scopes = ref<string[]>([])
+
+ /**
+ * Updates an Oauth2 application's metadata
+ */
+ const updateAppMeta = async (app: OAuth2Application): Promise<void> => {
+ //Update the app metadata
+ await axios.put(get(o2EndpointUrl), app)
+ }
+
+ /**
+ * Gets all of the user's oauth2 applications from the server
+ * @returns The user's oauth2 applications
+ */
+ const getApps = async () => {
+ // Get all apps
+ const { data } = await axios.get<OAuth2Application[]>(get(o2EndpointUrl));
+
+ if(!isArray(data)){
+ throw new Error("Invalid response from server")
+ }
+
+ return map(data, (appData) => {
+ //Store the created time as a date object
+ appData.Created = new Date(appData?.Created ?? 0)
+ //create a new state manager for the user's profile
+ return appData;
+ })
+ }
+
+ /**
+ * Creates a new application from the given data
+ * @param param0 The application server buffer
+ * @returns The newly created application
+ */
+ const createApp = async ({ name, description, permissions }: OAuth2Application): Promise<NewAppResponse> => {
+
+ // make the post request, response is the new app data with a secret
+ const { data } = await axios.post<OAuth2Application & { raw_secret: string }>(`${get(o2EndpointUrl)}?action=create`, { name, description, permissions })
+
+ // Store secret
+ const secret = data.raw_secret
+
+ // remove secre tfrom the response
+ delete (data as any).raw_secret
+
+ return { secret, app: data }
+ }
+
+ /**
+ * Requets a new secret for an application from the server
+ * @param app The app to request a new secret for
+ * @param password The user's password
+ * @returns The new secret
+ */
+ const updateAppSecret = async (app: OAuth2Application, password: string): Promise<string> => {
+ const { data } = await axios.post(`${o2EndpointUrl}?action=secret`, { Id: app.Id, password })
+ return data.raw_secret
+ }
+
+ /**
+ * Deletes an application from the server
+ * @param app The application to delete
+ * @param password The user's password
+ * @returns The response from the server
+ */
+ const deleteApp = async ({ Id }: OAuth2Application, password: string): Promise<void> => {
+ await axios.post(`${o2EndpointUrl}?action=delete`, { password, Id });
+ }
+
+ const apps = computed(() => sortBy(_oauth2Apps.value, a => a.Created))
+
+ watch([loggedIn, onRefresh], async ([li]) => {
+ if (!li){
+ set(_oauth2Apps, [])
+ return;
+ }
+
+ //Load the user's oauth2 apps
+ apiCall(async () => {
+ _oauth2Apps.value = await getApps()
+
+ //Load the oauth2 scopes
+ const { data } = await axios.get<string[]>(get(scopeEndpoint))
+ set(scopes, data)
+ })
+ })
+
+ return storeExport({
+ oauth2:{
+ apps,
+ scopes,
+ getApps,
+ createApp,
+ updateAppMeta,
+ updateAppSecret,
+ deleteApp,
+ refresh
+ }
+ })
+ }
+} \ No newline at end of file
diff --git a/front-end/src/store/pageProtectionPlugin.ts b/front-end/src/store/pageProtectionPlugin.ts
index 9831dad..a747e49 100644
--- a/front-end/src/store/pageProtectionPlugin.ts
+++ b/front-end/src/store/pageProtectionPlugin.ts
@@ -60,14 +60,12 @@ export const pageProtectionPlugin = (router: ReturnType<typeof useRouter>): Pini
return true;
})
- router.afterEach(() => {
- //scroll window back to top
- window.scrollTo(0, 0)
- })
+ //scroll window back to top
+ router.afterEach(() => window.scrollTo(0, 0))
- watch(loggedIn, (loggedIn) => {
+ watch(loggedIn, (li) => {
//If the user gets logged out, redirect to login
- if(loggedIn === false && router.currentRoute.value.name !== 'Login'){
+ if(li === false && router.currentRoute.value.name !== 'Login'){
router.push({ name: 'Login' })
}
})
diff --git a/front-end/src/store/socialMfaPlugin.ts b/front-end/src/store/socialMfaPlugin.ts
index b9bce27..3968cf1 100644
--- a/front-end/src/store/socialMfaPlugin.ts
+++ b/front-end/src/store/socialMfaPlugin.ts
@@ -1,3 +1,4 @@
+
import 'pinia'
import { MaybeRef } from 'vue';
import { useSocialOauthLogin, useUser, SocialOAuthPortal, fromPortals, useAxios } from '@vnuge/vnlib.browser'
@@ -34,30 +35,42 @@ export const socialMfaPlugin = (portalEndpoint?: MaybeRef<string>): PiniaPlugin
}
}
- const _loadPromise = new Promise<SocialMfaPlugin>((resolve, reject) => {
+ const _loadPromise = new Promise<SocialMfaPlugin>((resolve, _) => {
- if(get(portalEndpoint) == null) {
+ if (get(portalEndpoint) == null) {
const socialOauth = useSocialOauthLogin([])
setLogoutMethod(socialOauth)
return resolve(socialOauth)
}
+ /*
+ Try to load social methods from server, if it fails, then we will
+ fall back to default
+ */
+
defer(async () => {
+
+ let portals: SocialOAuthPortal[] = []
+
try {
//Get axios instance
const axios = useAxios(null)
//Get all enabled portals
- const { data } = await axios.get<SocialOAuthPortal[]>(get(portalEndpoint));
- //Setup social providers from server portals
- const socialOauth = useSocialOauthLogin(fromPortals(data));
- setLogoutMethod(socialOauth);
-
- resolve(socialOauth)
+ const { data, headers } = await axios.get<SocialOAuthPortal[]>(get(portalEndpoint)!);
+
+ if(headers['content-type'] === 'application/json') {
+ portals = data
+ }
} catch (error) {
- reject(error)
+ //Let failure fall back to default
}
+
+ //Create social login from available portals
+ const socialOauth = useSocialOauthLogin(fromPortals(portals));
+ setLogoutMethod(socialOauth);
+ resolve(socialOauth)
})
})
diff --git a/front-end/src/store/userProfile.ts b/front-end/src/store/userProfile.ts
index a4ea469..0320ace 100644
--- a/front-end/src/store/userProfile.ts
+++ b/front-end/src/store/userProfile.ts
@@ -3,7 +3,8 @@ 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';
+import { defer, noop } from 'lodash-es';
+import { storeExport } from './index';
export interface OAuth2Application {
readonly Id: string,
@@ -24,17 +25,21 @@ interface ExUserProfile extends UserProfile {
created: string | Date
}
+export interface UserProfileStore{
+ userProfile: ServerDataBuffer<ExUserProfile, WebMessage<string>>
+ userName: string | undefined
+ refreshProfile(): void;
+}
+
declare module 'pinia' {
- export interface PiniaCustomProperties {
- userProfile: ServerDataBuffer<ExUserProfile, WebMessage<string>>
- userName: string | undefined
- refreshProfile(): void;
+ export interface PiniaCustomProperties extends UserProfileStore {
+
}
}
export const profilePlugin = (accountsUrl:MaybeRef<string>) :PiniaPlugin => {
- return ({ store }: PiniaPluginContext) => {
+ return ({ store }: PiniaPluginContext): UserProfileStore => {
const { loggedIn } = storeToRefs(store)
const { getProfile, userName } = useUser()
@@ -64,19 +69,16 @@ export const profilePlugin = (accountsUrl:MaybeRef<string>) :PiniaPlugin => {
userProfile.apply(profile)
}
- watch([loggedIn, onRefresh], ([li]) => {
- //If the user is logged in, load the profile buffer
- if (li) {
- apiCall(loadProfile)
- }
- })
+ //If the user is logged in, load the profile buffer
+ watch([loggedIn, onRefresh], ([li]) => li ? apiCall(loadProfile) : noop())
+ //Defer intiial profile load
defer(refreshProfile);
- return {
+ return storeExport({
userProfile,
refreshProfile,
userName
- }
+ })
}
} \ No newline at end of file