fix(auth): org-scoped team tokens and header search order

This commit is contained in:
Ruslan Bakiev
2026-04-21 14:09:51 +07:00
parent e1e6993f35
commit 39712613ae
8 changed files with 157 additions and 76 deletions

View File

@@ -27,14 +27,19 @@ const EXPIRY_BUFFER_MS = 60 * 1000
*/
export const useLogtoTokens = () => {
const tokens = useState<Partial<Record<ResourceKey, TokenInfo>>>('logto-tokens', () => ({}))
const tokensOrgId = useState<string | null>('logto-tokens-org-id', () => null)
const idToken = useState<string | null>('logto-id-token', () => null)
const activeOrgId = useState<string | null>('activeLogtoOrgId', () => null)
const orgState = useState<string | null>('logto-org-id', () => null)
const isRefreshing = ref(false)
let refreshPromise: Promise<void> | null = null
let refreshPromiseOrgId: string | null = null
/**
* Get organization ID from Logto user (first organization)
*/
const getOrganizationId = (): string | undefined => {
const resolveOrganizationId = (preferred?: string | null): string | undefined => {
if (preferred) return preferred
if (import.meta.server) {
const nuxtApp = useNuxtApp()
const context = nuxtApp.ssrContext?.event.context as {
@@ -43,8 +48,7 @@ export const useLogtoTokens = () => {
} | undefined
return context?.logtoOrgId || context?.logtoUser?.organizations?.[0]
}
const orgId = useState<string | null>('logto-org-id', () => null)
return orgId.value || undefined
return activeOrgId.value || orgState.value || undefined
}
/**
@@ -54,7 +58,7 @@ export const useLogtoTokens = () => {
if (import.meta.server) {
const nuxtApp = useNuxtApp()
const client = nuxtApp.ssrContext?.event.context.logtoClient
const organizationId = getOrganizationId()
const organizationId = resolveOrganizationId()
if (client) {
const results: Partial<Record<ResourceKey, TokenInfo>> = {}
@@ -78,6 +82,7 @@ export const useLogtoTokens = () => {
)
tokens.value = results
tokensOrgId.value = organizationId || null
// Also fetch ID token for SSR
try {
@@ -124,24 +129,36 @@ export const useLogtoTokens = () => {
/**
* Refresh all tokens from server
*/
const refreshTokens = async (): Promise<void> => {
const refreshTokens = async (organizationId?: string | null): Promise<void> => {
const resolvedOrgId = resolveOrganizationId(organizationId) || null
// Deduplicate concurrent refresh calls
if (refreshPromise) {
return refreshPromise
if (refreshPromiseOrgId === resolvedOrgId) {
return refreshPromise
}
await refreshPromise
}
isRefreshing.value = true
refreshPromiseOrgId = resolvedOrgId
refreshPromise = (async () => {
try {
const response = await $fetch<RefreshResponse>('/api/auth/refresh', {
method: 'POST'
method: 'POST',
body: resolvedOrgId ? { organizationId: resolvedOrgId } : undefined
})
tokens.value = response.tokens
tokensOrgId.value = resolvedOrgId
if (resolvedOrgId) {
orgState.value = resolvedOrgId
}
}
finally {
isRefreshing.value = false
refreshPromise = null
refreshPromiseOrgId = null
}
})()
@@ -152,19 +169,21 @@ export const useLogtoTokens = () => {
* Get access token for a resource URL.
* Auto-refreshes if token is expired.
*/
const getToken = async (resourceUrl: ResourceUrl): Promise<string> => {
const getToken = async (resourceUrl: ResourceUrl, organizationId?: string | null): Promise<string> => {
// Find resource key by URL
const entry = Object.entries(RESOURCES).find(([, url]) => url === resourceUrl)
if (!entry) {
throw new Error(`Unknown resource: ${resourceUrl}`)
}
const key = entry[0] as ResourceKey
const resolvedOrgId = resolveOrganizationId(organizationId) || null
const tokenInfo = tokens.value[key]
const isOrgMismatch = tokensOrgId.value !== resolvedOrgId
// If expired, refresh all tokens
if (isTokenExpired(tokenInfo)) {
await refreshTokens()
// If expired (or cached for another org), refresh all tokens
if (isOrgMismatch || isTokenExpired(tokenInfo)) {
await refreshTokens(resolvedOrgId)
}
const refreshedToken = tokens.value[key]
@@ -178,11 +197,13 @@ export const useLogtoTokens = () => {
/**
* Get token by resource key (teams, exchange, orders, kyc)
*/
const getTokenByKey = async (key: ResourceKey): Promise<string> => {
const getTokenByKey = async (key: ResourceKey, organizationId?: string | null): Promise<string> => {
const tokenInfo = tokens.value[key]
const resolvedOrgId = resolveOrganizationId(organizationId) || null
const isOrgMismatch = tokensOrgId.value !== resolvedOrgId
if (isTokenExpired(tokenInfo)) {
await refreshTokens()
if (isOrgMismatch || isTokenExpired(tokenInfo)) {
await refreshTokens(resolvedOrgId)
}
const refreshedToken = tokens.value[key]