From cb51418ffe0c714ed8fd0c8137ea6c72bd9a446e Mon Sep 17 00:00:00 2001 From: Brandon Sprague Date: Tue, 10 Oct 2023 18:45:13 -0700 Subject: [PATCH] Overhaul auth management (#35) * Overhaul auth management Update MSAL integration, mirrors changes we did to OPGEE with some PACTA-specific differences * Add logging defaults --- frontend/.eslintrc.json | 4 +- frontend/composables/useAPI.ts | 59 +-- frontend/composables/useMSAL.ts | 392 +++++++++++-------- frontend/composables/usePACTA.ts | 63 +++ frontend/envs/env.dev | 1 + frontend/envs/env.local | 1 + frontend/middleware/admin.ts | 2 +- frontend/nuxt.config.ts | 1 + frontend/package-lock.json | 42 +- frontend/package.json | 2 +- frontend/pages/admin/pacta-version/[id].vue | 2 +- frontend/pages/admin/pacta-version/index.vue | 2 +- frontend/pages/admin/pacta-version/new.vue | 2 +- frontend/plugins/msal.client.ts | 100 ++--- 14 files changed, 382 insertions(+), 291 deletions(-) create mode 100644 frontend/composables/usePACTA.ts diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 69f3fde..c6bb1d8 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,7 +1,8 @@ { "env": { "browser": true, - "es2021": true + "es2021": true, + "node": true }, "ignorePatterns": [ "openapi/generated/**/*.ts" @@ -34,6 +35,7 @@ "commentPattern": "fallthrough" } ], + "no-useless-return": 0, "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/strict-boolean-expressions": 0, "@typescript-eslint/promise-function-async": 0, diff --git a/frontend/composables/useAPI.ts b/frontend/composables/useAPI.ts index d0652ce..8ef78a0 100644 --- a/frontend/composables/useAPI.ts +++ b/frontend/composables/useAPI.ts @@ -1,45 +1,46 @@ -import { UserClient, type DefaultService as UserDefaultService } from '@/openapi/generated/user' -import { PACTAClient, type DefaultService as PACTADefaultService } from '@/openapi/generated/pacta' +import { UserClient } from '@/openapi/generated/user' +import { PACTAClient } from '@/openapi/generated/pacta' -interface API { - userClient: UserDefaultService - pactaClient: PACTADefaultService - userClientWithCustomToken: (tkn: string) => UserDefaultService -} +import type { BaseHttpRequest } from '@/openapi/generated/pacta/core/BaseHttpRequest' +import type { OpenAPIConfig } from '@/openapi/generated/pacta/core/OpenAPI' + +type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest -export const useAPI = (): API => { +// Note: This is a low-level composable intended to be used by other +// composables, like usePACTA or useMSAL, it probably shouldn't be used by end +// clients. +export const useAPI = () => { const { public: { apiServerURL, authServerURL } } = useRuntimeConfig() + const baseCfg = { CREDENTIALS: 'include' as const, // To satisfy typing of 'include' | 'same-origin' | etc WITH_CREDENTIALS: true, } - // If we're on the server, forward our cookie header along to the backend - // API for auth. We don't do this for the UserClient because it uses separate - // auth. - let headers: Record = {} - if (process.server) { - headers = useRequestHeaders(['cookie']) - } - - const userCfg = { - ...baseCfg, - BASE: authServerURL, - } - const userClient = new UserClient(userCfg) - - const pactaClient = new PACTAClient({ + const pactaCfg = { ...baseCfg, BASE: apiServerURL, - HEADERS: headers, - }) + } return { - userClient: userClient.default, - pactaClient: pactaClient.default, - userClientWithCustomToken: (tkn: string) => { + // The three different PACTA clients are for authentication in different + // cases (client/server, cookies/no cookies, etc). + pactaClient: new PACTAClient(pactaCfg).default, + pactaClientWithHttpRequestClass: (req: HttpRequestConstructor) => { + return new PACTAClient(pactaCfg, req).default + }, + pactaClientWithAuth: (tkn: string) => { + return new PACTAClient({ + ...pactaCfg, + TOKEN: tkn, + }).default + }, + // Auth for the user service comes from Azure and needs to be manually + // appended to each UserService request. + userClientWithAuth: (tkn: string) => { const newCfg = { - ...userCfg, + ...baseCfg, + BASE: authServerURL, TOKEN: tkn, } return new UserClient(newCfg).default diff --git a/frontend/composables/useMSAL.ts b/frontend/composables/useMSAL.ts index 05e25cb..8e50bc6 100644 --- a/frontend/composables/useMSAL.ts +++ b/frontend/composables/useMSAL.ts @@ -3,221 +3,295 @@ // complicated than we need. // [1] https://github.com/Azure-Samples/ms-identity-b2c-javascript-spa // [2] https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-browser-samples/vue3-sample-app -import { type AccountInfo, type AuthenticationResult, InteractionRequiredAuthError, type SilentRequest } from '@azure/msal-browser' -import type { ComputedRef } from 'vue' -import { type APIKey } from '~/openapi/generated/user' +import { + type AccountInfo, + type EventMessage, + EventMessageUtils, + EventType, + InteractionRequiredAuthError, + type InteractionStatus, + type SilentRequest, + PublicClientApplication, +} from '@azure/msal-browser' +import { type AuthenticationResult } from '@azure/msal-common' -interface MSAL { - signIn: () => Promise - signOut: () => Promise - createAPIKey: () => Promise - isAuthenticated: ComputedRef -} +import { computed } from 'vue' +import { type APIKey } from '~/openapi/generated/user' -export const useMSAL = async (): Promise => { - const router = useRouter() +export const useMSAL = async () => { const isAuthenticated = useState('useMSAL.isAuthenticated', () => false) + + // Don't initialize the MSAL client if we're not in the browser. if (process.server) { - const sessionCookie = useCookie('jwt') - isAuthenticated.value = sessionCookie.value !== null && sessionCookie.value !== undefined + const jwt = useCookie('jwt') + isAuthenticated.value = !!jwt.value return { - signIn: async () => { await Promise.reject(new Error('cannot call signIn on server')) }, - signOut: async () => { await Promise.reject(new Error('cannot call signOut on server')) }, - createAPIKey: async () => await Promise.reject(new Error('cannot call createAPIKey on server')), - isAuthenticated: computed(() => isAuthenticated.value), + signIn: () => Promise.reject(new Error('cannot call signIn on server')), + signOut: () => Promise.reject(new Error('cannot call signOut on server')), + createAPIKey: () => Promise.reject(new Error('cannot call createAPIKey on server')), + getToken: () => Promise.reject(new Error('cannot call getToken on server')), + isAuthenticated: computed(() => !!jwt.value), } } - const { $msal: { /* inProgress, */ instance, accounts, msalConfig, b2cPolicies } } = useNuxtApp() - const { userClientWithCustomToken } = useAPI() + const router = useRouter() + const { userClientWithAuth } = useAPI() - const clientInitialized = useState('useMSAL.clientInitialized', () => false) + const { $msal: { msalConfig, b2cPolicies } } = useNuxtApp() + const scopes: string[] = ['openid', 'profile', 'offline_access', msalConfig.auth.clientId] - const account = useState('useMSALAuthentication.account') + const accounts = useState('useMSAL.accounts') + const interactionStatus = useState('useMSAL.interactionStatus') + const instance = useState('useMSAL.instance') - const setAccount = (acctInfo: AccountInfo): void => { - account.value = acctInfo - // TODO: maybe welcome new user? - } + const handleResponse = async (response: AuthenticationResult, force = false): Promise => { + if (!instance.value) { + return await Promise.reject(new Error('MSAL instance was not yet initialized')) + } - const selectAccount = async (): Promise => { - if (accounts.value.length < 1) { - // TODO: Figure out how to handle this scenario - } else if (accounts.value.length > 1) { - /** - * Due to the way MSAL caches account objects, the auth response from initiating a user-flow - * is cached as a new account, which results in more than one account in the cache. Here we make - * sure we are selecting the account with homeAccountId that contains the sign-up/sign-in user-flow, - * as this is the default flow the user initially signed-in with. - */ - const filteredAccounts = accounts.value.filter((account): boolean => { - return account.idTokenClaims?.iss !== undefined && - account.idTokenClaims.aud !== undefined && - account.homeAccountId.toUpperCase().includes(b2cPolicies.names.signUpSignIn.toUpperCase()) && - account.idTokenClaims.iss.toUpperCase().includes(b2cPolicies.authorityDomain.toUpperCase()) && - account.idTokenClaims.aud === msalConfig.auth.clientId - }) + if (!response?.account) { + return await Promise.resolve(response) + } - if (filteredAccounts.length > 1) { - // localAccountId identifies the entity for which the token asserts information. - if (filteredAccounts.every((account) => account.localAccountId === filteredAccounts[0].localAccountId)) { - // All filteredAccounts belong to the same user - setAccount(filteredAccounts[0]) - } else { - // Multiple users detected. Logout all to be safe. - console.log('multiple accounts found, logging out', accounts.value) - await signOut() - } - } else if (filteredAccounts.length === 1) { - setAccount(filteredAccounts[0]) - } - } else if (accounts.value.length === 1) { - setAccount(accounts.value[0]) + // If this came from the cache, we don't need to refresh our token. + if (response.fromCache && !force) { + return response } - } - const handleResponse = async (response: AuthenticationResult): Promise => { - setAccount(response.account) - const userClient = userClientWithCustomToken(response.idToken) + accounts.value = [response.account] + instance.value.setActiveAccount(response.account) + const userClient = userClientWithAuth(response.idToken) try { await userClient.login() isAuthenticated.value = true - } catch { + } catch (error) { + console.log('error at log in, signing out user', error) await signOut() } + return response } - const signIn = async (): Promise => { - const req = { scopes: ['openid'] } + const initializeAndAttemptLogin = async () => { + if (instance.value) { + console.log('instance is already initialized, returning') + return + } + + const inst = new PublicClientApplication(msalConfig) + + inst.addEventCallback((message: EventMessage) => { + switch (message.eventType) { + case EventType.ACCOUNT_ADDED: + case EventType.ACCOUNT_REMOVED: + case EventType.LOGIN_SUCCESS: + case EventType.SSO_SILENT_SUCCESS: + case EventType.HANDLE_REDIRECT_END: + case EventType.LOGIN_FAILURE: + case EventType.SSO_SILENT_FAILURE: + case EventType.LOGOUT_END: + case EventType.ACQUIRE_TOKEN_SUCCESS: + case EventType.ACQUIRE_TOKEN_FAILURE: + accounts.value = inst.getAllAccounts() + break + } + + const status = EventMessageUtils.getInteractionStatusFromEvent(message, interactionStatus.value) + if (status !== null) { + interactionStatus.value = status + } + }) + try { - const response = await instance.loginPopup(req) - await handleResponse(response) - } catch (err) { - console.log('useMSAL.loginPopup', err) + console.log('initializing MSAL client') + await inst.initialize() + instance.value = inst + } catch (error) { + console.log('failed to init instance', error) + return + } + + // See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#handling-app-launch-with-0-or-more-available-accounts + const accts = instance.value.getAllAccounts() + if (accts.length === 0) { + try { + const authRes = await instance.value.ssoSilent({}) + await handleResponse(authRes) + isAuthenticated.value = true + } catch (error) { + console.log('failed to init SSO silently', error) + } + } else if (accts.length === 1) { + try { + const authRes = await instance.value.acquireTokenSilent({ + scopes, + account: accts[0], + }) + await handleResponse(authRes) + isAuthenticated.value = true + } catch (error) { + console.log('failed to acquire token silently', error) + } + } else { + // When we handle this, use instance.setActiveAccount + console.log('multiple accounts found, user needs to select one') } } - const getToken = async (): Promise => { - if (account.value === undefined) { - // TODO: Figure out if this is a legitimate usecase. - return await Promise.reject(new Error('tried to get a token, but no account was found')) + const resolvers = useState void>>('useMSAL.resolvers', () => []) + const loadMSAL = (): Promise => { + // We're already initialized + if (instance.value) { + return Promise.resolve() } - const request: SilentRequest = { - scopes: [], - forceRefresh: false, // Set this to "true" to skip a cached token and go to the server to get a new token - account: instance.getAccountByHomeId(account.value.homeAccountId) ?? undefined, + // We're already initializing MSAL, wait with everyone else + if (resolvers.value.length > 0) { + return new Promise((resolve) => { + resolvers.value.push(resolve) + }) } - const response = await instance.acquireTokenSilent(request) - // In case the response from B2C server has an empty accessToken field - // throw an error to initiate token acquisition - if (response.idToken === '') { - throw new InteractionRequiredAuthError() + // We're the first to request initializing MSAL, kick of the request and hop in line at the front of the queue. + return new Promise((resolve, reject) => { + resolvers.value.push(resolve) + initializeAndAttemptLogin() + .then(() => { + // Let everyone else know we've loaded the user and clear the queue. + resolvers.value.forEach((fn) => { fn() }) + resolvers.value = [] + }) + .catch(reject) + }) + } + + await loadMSAL() + + const account = computed(() => { + if (!accounts.value) { + return undefined } - return response + if (accounts.value.length < 1) { + return undefined + } + if (accounts.value.length === 1) { + return accounts.value[0] + } + /** + * Due to the way MSAL caches account objects, the auth response from initiating a user-flow + * is cached as a new account, which results in more than one account in the cache. Here we make + * sure we are selecting the account with homeAccountId that contains the sign-up/sign-in user-flow, + * as this is the default flow the user initially signed-in with. + */ + const filteredAccounts = accounts.value.filter((account) => { + return account.idTokenClaims && account.idTokenClaims.iss && account.idTokenClaims.aud && + account.homeAccountId.toUpperCase().includes(b2cPolicies.names.signUpSignIn.toUpperCase()) && + account.idTokenClaims.iss.toUpperCase().includes(b2cPolicies.authorityDomain.toUpperCase()) && + account.idTokenClaims.aud === msalConfig.auth.clientId + }) + + if (filteredAccounts.length === 0) { + console.log('no accounts left after filtering', accounts.value, filteredAccounts, b2cPolicies) + return undefined + } + + if (filteredAccounts.length === 1) { + return filteredAccounts[0] + } + + // localAccountId identifies the entity for which the token asserts information. + if (filteredAccounts.every((account) => account.localAccountId === filteredAccounts[0].localAccountId)) { + // All filteredAccounts belong to the same user + return filteredAccounts[0] + } + + // Multiple users detected. Currently, we just return the first, but we should handle this more explicitly elsewhere. + console.log('multiple users detected', filteredAccounts) + return filteredAccounts[0] + }) + + const signIn = () => { + if (!instance.value) { + return Promise.reject(new Error('MSAL instance was not yet initialized')) + } + + const req = { scopes } + return instance.value.loginPopup(req) + .then(handleResponse) + .catch((err) => { + console.log('useMSAL.loginPopup', err) + }) } - const getTokenPopup = async (): Promise => { + const getToken = () => { + if (!instance.value) { + return Promise.reject(new Error('MSAL instance was not yet initialized')) + } + const inst = instance.value + if (account.value === undefined) { // TODO: Figure out if this is a legitimate usecase. - throw new Error('tried to get a token, but no account was found') + return Promise.reject(new Error('tried to get a token, but no account was found')) } const request: SilentRequest = { - scopes: [], + scopes, forceRefresh: false, // Set this to "true" to skip a cached token and go to the server to get a new token - account: instance.getAccountByHomeId(account.value.homeAccountId) ?? undefined, + account: inst.getAccount({ homeAccountId: account.value.homeAccountId }) ?? undefined, } - try { - return await getToken() - } catch (error) { - console.log('Silent token acquisition failed. Acquiring token using popup. \n', error) - if (!(error instanceof InteractionRequiredAuthError)) { - throw new Error('unexpected error while getting token', { cause: error }) - } - // Fallback to interaction when silent call fails - try { - const response = await instance.acquireTokenPopup(request) - console.log('acquireTokenPopup', response) + return inst.acquireTokenSilent(request) + .then((response) => { + // In case the response from B2C server has an empty idToken field + // throw an error to initiate token acquisition + if (response.idToken === '') { + throw new InteractionRequiredAuthError() + } return response - } catch (error) { - throw new Error('catch.acquireTokenPopup', { cause: error }) - } - } + }) + .then(handleResponse) } - const createAPIKey = async (): Promise => { - const response = await getTokenPopup() - const userClient = userClientWithCustomToken(response.idToken) - const apiResp = await userClient.createApiKey() - if ('message' in apiResp) { - throw new Error(`error creating a new API key ${apiResp.message}`) - } - return apiResp + const createAPIKey = (): Promise => { + return getToken() + .then((response) => { + const userClient = userClientWithAuth(response.idToken) + return userClient.createApiKey() + }) + .then((resp) => { + if ('message' in resp) { + throw new Error(`error creating a new API key ${resp.message}`) + } + return resp + }) } - const signOut = async (): Promise => { + const signOut = (): Promise => { + if (!instance.value) { + return Promise.reject(new Error('MSAL instance was not yet initialized')) + } + const logoutRequest = { postLogoutRedirectUri: msalConfig.auth.redirectUri, mainWindowRedirectUri: msalConfig.auth.logoutUri, } - const userClient = userClientWithCustomToken('') // Logging out doesn't require auth. - try { - await Promise.all([ - userClient.logout(), - instance.logoutPopup(logoutRequest), - ]) - } catch (e) { - console.log('failed to log out', e) - } - isAuthenticated.value = false - await router.push('/') - } - - // Initialize the MSAL client if we're in the browser. - if (process.client && !clientInitialized.value) { - clientInitialized.value = true - try { - await instance.initialize() - if (accounts.value.length === 0) { - try { - await instance.ssoSilent({}) - } catch (e) { - console.log('failed to init SSO silently', e) - } - } - const response = await instance.handleRedirectPromise() - if (response !== null) { - const claims = response.idTokenClaims - if (!('tfp' in claims) || typeof claims.tfp !== 'string') { - throw new Error('failed to find \'tfp\' claim') - } - const tfp = claims.tfp - /** - * For the purpose of setting an active account for UI update, we want to consider only the auth response resulting - * from SUSI flow. "tfp" claim in the id token tells us the policy (NOTE: legacy policies may use "acr" instead of "tfp"). - * To learn more about B2C tokens, visit https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview - */ - if (tfp.toUpperCase() !== b2cPolicies.names.signUpSignIn.toUpperCase()) { - throw new Error(`unexpected 'tfp' claim '${tfp.toUpperCase()}' does not match '${b2cPolicies.names.signUpSignIn.toUpperCase()}'`) - } - await handleResponse(response) - } else { - console.log('not coming from a redirect') - } - } catch (e) { - console.log(e) - } + const userClient = userClientWithAuth('') // Logging out doesn't require auth. + return Promise.all([ + userClient.logout(), + instance.value.logoutPopup(logoutRequest), + ]) + .catch((e) => { console.log('failed to log out', e) }) + .then(() => { /* cast to void */ }) + .finally(() => { + isAuthenticated.value = false + void router.push('/') + }) } - await selectAccount() - return { signIn, signOut, createAPIKey, + getToken, isAuthenticated: computed(() => isAuthenticated.value), } } diff --git a/frontend/composables/usePACTA.ts b/frontend/composables/usePACTA.ts new file mode 100644 index 0000000..fddebed --- /dev/null +++ b/frontend/composables/usePACTA.ts @@ -0,0 +1,63 @@ +import { type AuthenticationResult } from '@azure/msal-common' +import type { ApiRequestOptions } from '~/openapi/generated/pacta/core/ApiRequestOptions' +import { BaseHttpRequest } from '~/openapi/generated/pacta/core/BaseHttpRequest' +import { CancelablePromise } from '~/openapi/generated/pacta/core/CancelablePromise' +import type { OpenAPIConfig } from '~/openapi/generated/pacta/core/OpenAPI' +import { request as __request } from '~/openapi/generated/pacta/core/request' + +export const usePACTA = async () => { + const { + pactaClient, // On the server, if there's no JWT + pactaClientWithAuth, // On the server, forward the cookie we got from the client + pactaClientWithHttpRequestClass, // On the client, wrap with a check for a fresh cookie. + } = useAPI() + + if (process.server) { + const jwt = useCookie('jwt') + if (jwt.value) { + return pactaClientWithAuth(jwt.value) + } + return pactaClient + } + + // If we're on the client, we can see if Azure has cached credentials and use + // those, or refresh them if not. Our cookies have the same lifetime as our + // access tokens, so we refresh them together. + const { getToken } = await useMSAL() + + // We define this class as a variable so we can override the PACTA client + // with middleware that refreshes our credentials. This matches the + // interface of our auto-generated code, which expects a class that extends + // BaseHttpRequest. + const httpReqClass = class extends BaseHttpRequest { + private readonly getToken: () => Promise + + constructor (config: OpenAPIConfig) { + super(config) + this.getToken = getToken + } + + /** + * Request method + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ + public override request(options: ApiRequestOptions): CancelablePromise { + return new CancelablePromise((resolve, reject, onCancel) => { + this.getToken() + .then(() => { + const cancelablePromise = __request(this.config, options) + onCancel(() => { + cancelablePromise.cancel() + }) + return cancelablePromise + }) + .then(resolve) + .catch(reject) + }) + } + } + + return pactaClientWithHttpRequestClass(httpReqClass) +} diff --git a/frontend/envs/env.dev b/frontend/envs/env.dev index a06c377..4a1ad65 100644 --- a/frontend/envs/env.dev +++ b/frontend/envs/env.dev @@ -8,3 +8,4 @@ MSAL_CLIENT_ID=218f47ee-11b1-459a-b914-b7fa6f107e7b # See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirecturi-considerations MSAL_REDIRECT_URI=https://pacta.dev.rmi.siliconally.dev/blank MSAL_LOGOUT_URI=https://pacta.dev.rmi.siliconally.dev/ +MSAL_MIN_LOG_LEVEL=INFO diff --git a/frontend/envs/env.local b/frontend/envs/env.local index f3f1839..cb46236 100644 --- a/frontend/envs/env.local +++ b/frontend/envs/env.local @@ -8,3 +8,4 @@ MSAL_CLIENT_ID=2d77a4a9-b7be-4451-ad47-c151d8b6c05f # See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirecturi-considerations MSAL_REDIRECT_URI=http://localhost:3000/blank MSAL_LOGOUT_URI=http://localhost:3000/ +MSAL_MIN_LOG_LEVEL=INFO diff --git a/frontend/middleware/admin.ts b/frontend/middleware/admin.ts index b26b3db..188cfab 100644 --- a/frontend/middleware/admin.ts +++ b/frontend/middleware/admin.ts @@ -1,6 +1,6 @@ export default defineNuxtRouteMiddleware(async () => { /* TODO(grady) add Authorization here - const { getMe } = useMSAL() + const { getMe } = await useMSAL() const { isAdmin } = await getMe() if (!isAdmin.value) { return await navigateTo('/denied') diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 3888549..a5395e4 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -25,6 +25,7 @@ export default defineNuxtConfig({ clientID: process.env.MSAL_CLIENT_ID ?? '', redirectURI: process.env.MSAL_REDIRECT_URI ?? '', logoutURI: process.env.MSAL_LOGOUT_URI ?? '', + minLogLevel: process.env.MSAL_MIN_LOG_LEVEL ?? 'VERBOSE', }, }, }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a6cc008..8e97b32 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "nuxt-app", "dependencies": { - "@azure/msal-browser": "^3.0.2", + "@azure/msal-browser": "^3.2.0", "primeflex": "^3.3.1", "primeicons": "^6.0.1", "primevue": "^3.32.0", @@ -345,20 +345,20 @@ } }, "node_modules/@azure/msal-browser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.0.2.tgz", - "integrity": "sha512-a2lIzWGV8EJhxVTHu2Vd5sRdK6DTisueq2HCXt49CQqc1hG/j9pK+ds+hJi+QLlZHPo3FPMwiTCHEA9sNgX3HA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.2.0.tgz", + "integrity": "sha512-le2qutddMiq0i3ErQaLKuwP1DpNgdd9iXPs3fSCsLuBrdGg9B4/j4ArCAHCwgxA82Ydj9BcqtMIL5BSWwU+P5A==", "dependencies": { - "@azure/msal-common": "14.0.2" + "@azure/msal-common": "14.1.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.0.2.tgz", - "integrity": "sha512-yvVP63oXe20JFzvooTVxiX9UorxcL8VFoVwEtKuGlQnEnNGzrOpQZ6lKa1lqwGqxMtwKpW7oAFQhbbskG5RbYA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.1.0.tgz", + "integrity": "sha512-xphmhcfl5VL+uq5//VKMwQn+wfEZLMKNpFCcMi8Ur8ej5UT166g6chBsxgMzc9xo9Y24R9FB3m/tjDiV03xMIA==", "engines": { "node": ">=0.8.0" } @@ -13317,9 +13317,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -18812,17 +18812,17 @@ } }, "@azure/msal-browser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.0.2.tgz", - "integrity": "sha512-a2lIzWGV8EJhxVTHu2Vd5sRdK6DTisueq2HCXt49CQqc1hG/j9pK+ds+hJi+QLlZHPo3FPMwiTCHEA9sNgX3HA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.2.0.tgz", + "integrity": "sha512-le2qutddMiq0i3ErQaLKuwP1DpNgdd9iXPs3fSCsLuBrdGg9B4/j4ArCAHCwgxA82Ydj9BcqtMIL5BSWwU+P5A==", "requires": { - "@azure/msal-common": "14.0.2" + "@azure/msal-common": "14.1.0" } }, "@azure/msal-common": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.0.2.tgz", - "integrity": "sha512-yvVP63oXe20JFzvooTVxiX9UorxcL8VFoVwEtKuGlQnEnNGzrOpQZ6lKa1lqwGqxMtwKpW7oAFQhbbskG5RbYA==" + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.1.0.tgz", + "integrity": "sha512-xphmhcfl5VL+uq5//VKMwQn+wfEZLMKNpFCcMi8Ur8ej5UT166g6chBsxgMzc9xo9Y24R9FB3m/tjDiV03xMIA==" }, "@azure/msal-node": { "version": "1.18.3", @@ -28244,9 +28244,9 @@ } }, "postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", diff --git a/frontend/package.json b/frontend/package.json index 6fe9fee..2b9eed8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,7 @@ "vue-tsc": "^1.8.8" }, "dependencies": { - "@azure/msal-browser": "^3.0.2", + "@azure/msal-browser": "^3.2.0", "primeflex": "^3.3.1", "primeicons": "^6.0.1", "primevue": "^3.32.0", diff --git a/frontend/pages/admin/pacta-version/[id].vue b/frontend/pages/admin/pacta-version/[id].vue index 31b0b99..b2f5371 100644 --- a/frontend/pages/admin/pacta-version/[id].vue +++ b/frontend/pages/admin/pacta-version/[id].vue @@ -2,7 +2,7 @@ import { type PactaVersion, type PactaVersionChanges } from '@/openapi/generated/pacta' const router = useRouter() -const { pactaClient } = useAPI() +const pactaClient = await usePACTA() const { loading: { withLoading } } = useModal() const { fromParams } = useURLParams() diff --git a/frontend/pages/admin/pacta-version/index.vue b/frontend/pages/admin/pacta-version/index.vue index ca92bef..c522f01 100644 --- a/frontend/pages/admin/pacta-version/index.vue +++ b/frontend/pages/admin/pacta-version/index.vue @@ -1,6 +1,6 @@