Skip to content
Snippets Groups Projects
auth.ts 6.95 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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
    
    export function useHasUserPermission(requiredPermissions: MaybeRefOrGetter<string[] | Authorizer>) {
    
      const authStore = useAuthStore()
      return computed(() => {
    
        if (authStore.steeringUser?.isPrivileged) return true
        const userPermissions = (authStore.steeringUser?.permissions ?? []) as string[]
        const permissionChecker = toValue(requiredPermissions)
        if (typeof permissionChecker === 'function') return permissionChecker(userPermissions)
        else {
          return (
            permissionChecker.length === 0 ||
            permissionChecker.every((perm) => userPermissions.includes(perm))
          )
        }
    
    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,
        }
      }
    
      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
    
      return {
        currentUser: shallowReadonly(currentUser),
        steeringUser,
    
        sessionInitializationState: readonly(sessionInitializationState),
        isInitializingSession: readonly(isInitializingSession),
    
    })
    
    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}`,
          },
        }
      },
    }