Newer
Older
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}`)
}
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
}
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,
})
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}`,
},
}
},
}