import axios from 'axios' import { defineStore } from 'pinia' import { computed, MaybeRefOrGetter, readonly, ref, shallowReadonly, toValue, watch } from 'vue' import { components as steeringTypes } from '../steering-types' import { createSteeringURL, createTankURL } from '@/api' import { getUser, oidcManager } from '@/oidc' import { APICreate, APIListUnpaginated, APIRemove, APIRetrieve, APIUpdate, createExtendableAPI, } from '@rokoli/bnb/drf' const log = globalThis.console export type CurrentUser = { name: string email: string oidcAccessToken: string tankSessionToken: string } type Session = { allshows?: boolean privileged?: boolean publicshows?: string[] readonly?: boolean shows?: string[] username?: string } type NewSessionResponse = { session?: Session token?: string } type SessionInitializationState = | 'OIDC_AUTH' | 'STEERING_INITIALIZATION' | 'TANK_INITIALIZATION' | undefined export type SteeringUser = Required<steeringTypes['schemas']['User']> export const useUserStore = defineStore('steeringUser', () => { const { api, base } = createExtendableAPI<SteeringUser>(createSteeringURL.prefix('users'), { getRequestDefaults() { const authStore = useAuthStore() return authStore.currentUser ? { headers: { Authorization: `Bearer ${authStore.currentUser.oidcAccessToken}` } } : {} }, }) const listOperations = APIListUnpaginated(api) useOnAuthBehaviour(() => void listOperations.list()) return { ...base, ...listOperations, ...APIRetrieve(api), ...APICreate(api), ...APIUpdate(api), ...APIRemove(api), } }) async function createTankSession(accessToken: string): Promise<NewSessionResponse> { const res = await fetch(`${import.meta.env.VUE_APP_TANK}/auth/session`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ backend: 'oidc', arguments: { access_token: accessToken, token_type: 'Bearer', }, }), }) if (res.ok) { return await res.json() } else { throw new Error('Could not initialize Tank session.') } } export function useOnAuthBehaviour(behaviour: (user?: SteeringUser) => unknown | Promise<unknown>) { const authStore = useAuthStore() return watch( () => authStore.steeringUser, async (user) => { if (user) await behaviour(user) }, { immediate: true }, ) } export type Authorizer = (userPermissions: string[]) => boolean function _hasUserPermission( user: SteeringUser | undefined | null, requiredPermissions: string[] | Authorizer, ) { if (user?.isPrivileged) return true const userPermissions = (user?.permissions ?? []) as string[] const permissionChecker = requiredPermissions if (typeof permissionChecker === 'function') return permissionChecker(userPermissions) else { return ( permissionChecker.length === 0 || permissionChecker.every((perm) => userPermissions.includes(perm)) ) } } export function useHasUserPermission(requiredPermissions: MaybeRefOrGetter<string[] | Authorizer>) { const authStore = useAuthStore() return computed(() => { return _hasUserPermission(authStore.steeringUser, toValue(requiredPermissions)) }) } export const useAuthStore = defineStore('auth', () => { const currentUser = ref<CurrentUser>() const _steeringUser = ref<SteeringUser>() const steeringUser = computed(() => (currentUser.value ? _steeringUser.value : undefined)) const sessionInitializationState = ref<SessionInitializationState>() const isInitializingSession = computed(() => sessionInitializationState.value !== undefined) async function loadUser() { sessionInitializationState.value = 'OIDC_AUTH' const oidcUser = await getUser() sessionInitializationState.value = 'STEERING_INITIALIZATION' const userStore = useUserStore() const users = await userStore.list({ requestInit: { headers: { Authorization: `Bearer ${oidcUser.access_token}` }, }, }) const user = users.find((user: SteeringUser) => user.username === oidcUser.profile?.username) // now that we have a valid token, we can create a session with tank sessionInitializationState.value = 'TANK_INITIALIZATION' let tankSessionToken: string try { tankSessionToken = (await createTankSession(oidcUser.access_token))?.token ?? '' log.debug('Tank session token:', tankSessionToken) } catch (e) { log.error('Could not create tank session.', e) return } if (!tankSessionToken) { log.error('Tank session creation was successful, but no token has been assigned.') return } _steeringUser.value = user currentUser.value = { name: oidcUser.profile.nickname ?? '', email: oidcUser.profile.email ?? '', oidcAccessToken: oidcUser.access_token, tankSessionToken, } if (user?.permissions) { const permissions = user.permissions.map((p) => `\t${p}`).join('\n') console.debug(`User is logged in. Provided permissions are:\n${permissions}`) } } async function init() { log.debug('Initializing oidc client') oidcManager.events.addUserSignedOut(async () => { log.debug('User has signed out. Resetting auth store.') currentUser.value = undefined }) oidcManager.events.addAccessTokenExpiring(async () => { log.debug('User token expiration imminent. Starting silent access token renewal.') try { const user = await oidcManager.signinSilent() if (currentUser.value) { log.debug('Successfully renewed user access token.') currentUser.value.oidcAccessToken = user.access_token } } catch (e) { log.error('Silent OIDC access token renewal has failed.', e) } }) oidcManager.events.addAccessTokenExpired(() => { log.debug('OIDC token has expired. Logging out...') currentUser.value = undefined }) try { await loadUser() } catch (e) { log.debug('Could not load user data.') currentUser.value = undefined _steeringUser.value = undefined } finally { sessionInitializationState.value = undefined } } function hasUserPermission(requiredPermissions: string[] | Authorizer) { return _hasUserPermission(_steeringUser.value, requiredPermissions) } function isOwner(ownedBy: SteeringUser['id'][], privilegedOwnsEverything = true) { const user = steeringUser.value if (!user) return false return ownedBy.includes(user.id) || (privilegedOwnsEverything && user.isPrivileged) } return { currentUser: shallowReadonly(currentUser), steeringUser, sessionInitializationState: readonly(sessionInitializationState), isInitializingSession: readonly(isInitializingSession), hasUserPermission, isOwner, init, } }) axios.interceptors.request.use((config) => { const url = config?.url const authStore = useAuthStore() if (!url || !authStore.currentUser) return config if (url.startsWith(createSteeringURL())) { config.headers.set('Authorization', `Bearer ${authStore.currentUser.oidcAccessToken}`) } if (url.startsWith(createTankURL())) { config.headers.set('Authorization', `Bearer ${authStore.currentUser.tankSessionToken}`) } return config }) export const steeringAuthInit: { getRequestDefaults: () => RequestInit } = { getRequestDefaults() { const authStore = useAuthStore() return { headers: { Authorization: `Bearer ${authStore.currentUser?.oidcAccessToken}`, }, } }, } export const tankAuthInit: { getRequestDefaults: () => RequestInit } = { getRequestDefaults() { const authStore = useAuthStore() return { headers: { Authorization: `Bearer ${authStore.currentUser?.tankSessionToken}`, }, } }, }