Skip to content
Snippets Groups Projects
auth.ts 7.70 KiB
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}`,
      },
    }
  },
}