-
Konrad Mohrfeldt authoredKonrad Mohrfeldt authored
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}`,
},
}
},
}