fix(auth): org-scoped team tokens and header search order
This commit is contained in:
@@ -43,16 +43,36 @@ const LANDING_SEARCH_TOP_STOP = 16
|
|||||||
const LANDING_SEARCH_BOTTOM_GAP = 30
|
const LANDING_SEARCH_BOTTOM_GAP = 30
|
||||||
|
|
||||||
const logisticsSearch = reactive({
|
const logisticsSearch = reactive({
|
||||||
from: '',
|
destination: '',
|
||||||
to: '',
|
product: '',
|
||||||
cargo: '',
|
quantity: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
function syncSearchFromRoute() {
|
function syncSearchFromRoute() {
|
||||||
hydrateFromQuery(route.query)
|
hydrateFromQuery(route.query)
|
||||||
logisticsSearch.from = typeof route.query.from === 'string' ? route.query.from : isCalcPage.value ? calcDraft.value.from : ''
|
logisticsSearch.destination = typeof route.query.hubName === 'string'
|
||||||
logisticsSearch.to = typeof route.query.to === 'string' ? route.query.to : isCalcPage.value ? calcDraft.value.to : ''
|
? route.query.hubName
|
||||||
logisticsSearch.cargo = typeof route.query.cargo === 'string' ? route.query.cargo : isCalcPage.value ? calcDraft.value.cargo : ''
|
: typeof route.query.to === 'string'
|
||||||
|
? route.query.to
|
||||||
|
: isCalcPage.value
|
||||||
|
? calcDraft.value.to
|
||||||
|
: ''
|
||||||
|
logisticsSearch.product = typeof route.query.productName === 'string'
|
||||||
|
? route.query.productName
|
||||||
|
: typeof route.query.from === 'string'
|
||||||
|
? route.query.from
|
||||||
|
: isCalcPage.value
|
||||||
|
? calcDraft.value.from
|
||||||
|
: ''
|
||||||
|
logisticsSearch.quantity = typeof route.query.qty === 'string'
|
||||||
|
? route.query.qty
|
||||||
|
: typeof route.query.quantity === 'string'
|
||||||
|
? route.query.quantity
|
||||||
|
: typeof route.query.cargo === 'string'
|
||||||
|
? route.query.cargo
|
||||||
|
: isCalcPage.value
|
||||||
|
? calcDraft.value.cargo
|
||||||
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferCountryIso(value: string, fallback: string) {
|
function inferCountryIso(value: string, fallback: string) {
|
||||||
@@ -69,10 +89,8 @@ function isoToFlag(iso: string) {
|
|||||||
return String.fromCodePoint(...[...normalized].map(char => 127397 + char.charCodeAt(0)))
|
return String.fromCodePoint(...[...normalized].map(char => 127397 + char.charCodeAt(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromIso = computed(() => inferCountryIso(logisticsSearch.from, 'CN'))
|
const destinationIso = computed(() => inferCountryIso(logisticsSearch.destination, 'RU'))
|
||||||
const toIso = computed(() => inferCountryIso(logisticsSearch.to, 'RU'))
|
const destinationFlag = computed(() => isoToFlag(destinationIso.value))
|
||||||
const fromFlag = computed(() => isoToFlag(fromIso.value))
|
|
||||||
const toFlag = computed(() => isoToFlag(toIso.value))
|
|
||||||
const showAdminDock = computed(() => Boolean(showLogistics.value) && !isAuthPage.value)
|
const showAdminDock = computed(() => Boolean(showLogistics.value) && !isAuthPage.value)
|
||||||
// Fullscreen menu
|
// Fullscreen menu
|
||||||
const isMenuOpen = ref(false)
|
const isMenuOpen = ref(false)
|
||||||
@@ -212,53 +230,60 @@ const headerBackdropClass = computed(() => {
|
|||||||
return 'header-glass-backdrop--default'
|
return 'header-glass-backdrop--default'
|
||||||
})
|
})
|
||||||
|
|
||||||
function buildHeaderSearchQuery(from: string, to: string, cargo: string) {
|
function buildHeaderSearchQuery(destination: string, product: string, quantity: string) {
|
||||||
const currentQuery = route.query || {}
|
const currentQuery = route.query || {}
|
||||||
const patch = {
|
const patch = {
|
||||||
from,
|
from: product,
|
||||||
to,
|
to: destination,
|
||||||
cargo,
|
cargo: quantity,
|
||||||
|
}
|
||||||
|
const semanticQuery = {
|
||||||
|
...(product ? { productName: product } : {}),
|
||||||
|
...(destination ? { hubName: destination } : {}),
|
||||||
|
...(quantity ? { qty: quantity, quantity } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCalcPage.value) {
|
if (isCalcPage.value) {
|
||||||
return {
|
return {
|
||||||
...currentQuery,
|
...currentQuery,
|
||||||
...buildCalcQuery(patch),
|
...buildCalcQuery(patch),
|
||||||
|
...semanticQuery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentQuery,
|
...currentQuery,
|
||||||
...(from ? { from } : {}),
|
...(product ? { from: product } : {}),
|
||||||
...(to ? { to } : {}),
|
...(destination ? { to: destination } : {}),
|
||||||
...(cargo ? { cargo } : {}),
|
...(quantity ? { cargo: quantity } : {}),
|
||||||
|
...semanticQuery,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitHeaderSearch() {
|
async function submitHeaderSearch() {
|
||||||
const from = logisticsSearch.from.trim()
|
const destination = logisticsSearch.destination.trim()
|
||||||
const to = logisticsSearch.to.trim()
|
const product = logisticsSearch.product.trim()
|
||||||
const cargo = logisticsSearch.cargo.trim()
|
const quantity = logisticsSearch.quantity.trim()
|
||||||
await navigateToLocalized({
|
await navigateToLocalized({
|
||||||
path: '/catalog',
|
path: '/catalog',
|
||||||
query: buildHeaderSearchQuery(from, to, cargo),
|
query: buildHeaderSearchQuery(destination, product, quantity),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type StepRoute = 'from' | 'to' | 'cargo'
|
type StepRoute = 'destination' | 'product' | 'quantity'
|
||||||
|
|
||||||
async function openStep(step: StepRoute) {
|
async function openStep(step: StepRoute) {
|
||||||
const from = logisticsSearch.from.trim()
|
const destination = logisticsSearch.destination.trim()
|
||||||
const to = logisticsSearch.to.trim()
|
const product = logisticsSearch.product.trim()
|
||||||
const cargo = logisticsSearch.cargo.trim()
|
const quantity = logisticsSearch.quantity.trim()
|
||||||
const stepPath = step === 'from'
|
const stepPath = step === 'destination'
|
||||||
? '/catalog/product'
|
? '/catalog/destination'
|
||||||
: step === 'to'
|
: step === 'product'
|
||||||
? '/catalog/destination'
|
? '/catalog/product'
|
||||||
: '/catalog/quantity'
|
: '/catalog/quantity'
|
||||||
await navigateToLocalized({
|
await navigateToLocalized({
|
||||||
path: stepPath,
|
path: stepPath,
|
||||||
query: buildHeaderSearchQuery(from, to, cargo),
|
query: buildHeaderSearchQuery(destination, product, quantity),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +322,7 @@ async function goToSignIn() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary h-10 min-h-0 w-full rounded-full text-sm font-semibold"
|
class="btn btn-secondary h-10 min-h-0 w-full rounded-full text-sm font-semibold"
|
||||||
@click="openStep('from')"
|
@click="openStep('destination')"
|
||||||
>
|
>
|
||||||
{{ $t('ui.calculate') }}
|
{{ $t('ui.calculate') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -321,43 +346,46 @@ async function goToSignIn() {
|
|||||||
<div :class="searchCapsuleClass">
|
<div :class="searchCapsuleClass">
|
||||||
<form class="flex min-w-0 flex-wrap items-center gap-2 rounded-full" @submit.prevent="submitHeaderSearch">
|
<form class="flex min-w-0 flex-wrap items-center gap-2 rounded-full" @submit.prevent="submitHeaderSearch">
|
||||||
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
||||||
<span class="shrink-0 text-base leading-none">{{ fromFlag }}</span>
|
<span class="shrink-0 text-base leading-none">{{ destinationFlag }}</span>
|
||||||
<input
|
<input
|
||||||
:value="logisticsSearch.from"
|
:value="logisticsSearch.destination"
|
||||||
type="text"
|
|
||||||
class="w-full cursor-pointer"
|
|
||||||
:placeholder="$t('ui.from')"
|
|
||||||
readonly
|
|
||||||
@focus.prevent="openStep('from')"
|
|
||||||
@click.prevent="openStep('from')"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
|
||||||
<span class="shrink-0 text-base leading-none">{{ toFlag }}</span>
|
|
||||||
<input
|
|
||||||
:value="logisticsSearch.to"
|
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full cursor-pointer"
|
class="w-full cursor-pointer"
|
||||||
:placeholder="$t('ui.to')"
|
:placeholder="$t('ui.to')"
|
||||||
readonly
|
readonly
|
||||||
@focus.prevent="openStep('to')"
|
@focus.prevent="openStep('destination')"
|
||||||
@click.prevent="openStep('to')"
|
@click.prevent="openStep('destination')"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="search-arch input flex h-11 min-h-0 min-w-[190px] flex-[1.4] items-center gap-3 rounded-full shadow-none">
|
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
||||||
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4 shrink-0 text-base-content/60" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4 shrink-0 text-base-content/60" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||||
<path d="m3.3 7 8.7 5 8.7-5" />
|
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||||
<path d="M12 22V12" />
|
<path d="M12 22V12" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
:value="logisticsSearch.cargo"
|
:value="logisticsSearch.product"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full cursor-pointer"
|
class="w-full cursor-pointer"
|
||||||
:placeholder="$t('ui.cargo')"
|
:placeholder="$t('ui.product')"
|
||||||
readonly
|
readonly
|
||||||
@focus.prevent="openStep('cargo')"
|
@focus.prevent="openStep('product')"
|
||||||
@click.prevent="openStep('cargo')"
|
@click.prevent="openStep('product')"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="search-arch input flex h-11 min-h-0 min-w-[170px] flex-1 items-center gap-3 rounded-full shadow-none">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4 shrink-0 text-base-content/60" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 13a2 2 0 1 1 0-4h4a2 2 0 1 1 0 4h-4Zm0 0v2m4-2v2" />
|
||||||
|
<path d="M6 5h12M6 19h12" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
:value="logisticsSearch.quantity"
|
||||||
|
type="text"
|
||||||
|
class="w-full cursor-pointer"
|
||||||
|
:placeholder="$t('ui.quantity')"
|
||||||
|
readonly
|
||||||
|
@focus.prevent="openStep('quantity')"
|
||||||
|
@click.prevent="openStep('quantity')"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" class="btn btn-secondary h-11 min-h-0 rounded-full px-5">{{ $t('ui.find') }}</button>
|
<button type="submit" class="btn btn-secondary h-11 min-h-0 rounded-full px-5">{{ $t('ui.find') }}</button>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
export const useActiveTeam = () => {
|
export const useActiveTeam = () => {
|
||||||
const activeTeamId = useState<string | null>('activeTeamId', () => null)
|
const activeTeamId = useState<string | null>('activeTeamId', () => null)
|
||||||
const activeLogtoOrgId = useState<string | null>('activeLogtoOrgId', () => null)
|
const activeLogtoOrgId = useState<string | null>('activeLogtoOrgId', () => null)
|
||||||
|
const logtoOrgState = useState<string | null>('logto-org-id', () => null)
|
||||||
|
|
||||||
const setActiveTeam = (teamId: string | null, logtoOrgId?: string | null) => {
|
const setActiveTeam = (teamId: string | null, logtoOrgId?: string | null) => {
|
||||||
activeTeamId.value = teamId
|
activeTeamId.value = teamId
|
||||||
activeLogtoOrgId.value = logtoOrgId ?? null
|
const nextOrgId = logtoOrgId ?? null
|
||||||
|
activeLogtoOrgId.value = nextOrgId
|
||||||
|
logtoOrgState.value = nextOrgId
|
||||||
}
|
}
|
||||||
|
|
||||||
return { activeTeamId, activeLogtoOrgId, setActiveTeam }
|
return { activeTeamId, activeLogtoOrgId, setActiveTeam }
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ export const useAuth = () => {
|
|||||||
* Get access token for a resource.
|
* Get access token for a resource.
|
||||||
* Tokens are synced from SSR via useState, auto-refreshes if expired.
|
* Tokens are synced from SSR via useState, auto-refreshes if expired.
|
||||||
*/
|
*/
|
||||||
const getAccessToken = async (resource: string, _organizationId?: string): Promise<string> => {
|
const getAccessToken = async (resource: string, organizationId?: string): Promise<string> => {
|
||||||
return getToken(resource as Parameters<typeof getToken>[0])
|
return getToken(resource as Parameters<typeof getToken>[0], organizationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrganizationToken = getAccessToken
|
const getOrganizationToken = getAccessToken
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ const EXPIRY_BUFFER_MS = 60 * 1000
|
|||||||
*/
|
*/
|
||||||
export const useLogtoTokens = () => {
|
export const useLogtoTokens = () => {
|
||||||
const tokens = useState<Partial<Record<ResourceKey, TokenInfo>>>('logto-tokens', () => ({}))
|
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 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)
|
const isRefreshing = ref(false)
|
||||||
let refreshPromise: Promise<void> | null = null
|
let refreshPromise: Promise<void> | null = null
|
||||||
|
let refreshPromiseOrgId: string | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get organization ID from Logto user (first organization)
|
* 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) {
|
if (import.meta.server) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const context = nuxtApp.ssrContext?.event.context as {
|
const context = nuxtApp.ssrContext?.event.context as {
|
||||||
@@ -43,8 +48,7 @@ export const useLogtoTokens = () => {
|
|||||||
} | undefined
|
} | undefined
|
||||||
return context?.logtoOrgId || context?.logtoUser?.organizations?.[0]
|
return context?.logtoOrgId || context?.logtoUser?.organizations?.[0]
|
||||||
}
|
}
|
||||||
const orgId = useState<string | null>('logto-org-id', () => null)
|
return activeOrgId.value || orgState.value || undefined
|
||||||
return orgId.value || undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +58,7 @@ export const useLogtoTokens = () => {
|
|||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const client = nuxtApp.ssrContext?.event.context.logtoClient
|
const client = nuxtApp.ssrContext?.event.context.logtoClient
|
||||||
const organizationId = getOrganizationId()
|
const organizationId = resolveOrganizationId()
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
const results: Partial<Record<ResourceKey, TokenInfo>> = {}
|
const results: Partial<Record<ResourceKey, TokenInfo>> = {}
|
||||||
@@ -78,6 +82,7 @@ export const useLogtoTokens = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
tokens.value = results
|
tokens.value = results
|
||||||
|
tokensOrgId.value = organizationId || null
|
||||||
|
|
||||||
// Also fetch ID token for SSR
|
// Also fetch ID token for SSR
|
||||||
try {
|
try {
|
||||||
@@ -124,24 +129,36 @@ export const useLogtoTokens = () => {
|
|||||||
/**
|
/**
|
||||||
* Refresh all tokens from server
|
* 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
|
// Deduplicate concurrent refresh calls
|
||||||
if (refreshPromise) {
|
if (refreshPromise) {
|
||||||
return refreshPromise
|
if (refreshPromiseOrgId === resolvedOrgId) {
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
await refreshPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
isRefreshing.value = true
|
isRefreshing.value = true
|
||||||
|
refreshPromiseOrgId = resolvedOrgId
|
||||||
|
|
||||||
refreshPromise = (async () => {
|
refreshPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const response = await $fetch<RefreshResponse>('/api/auth/refresh', {
|
const response = await $fetch<RefreshResponse>('/api/auth/refresh', {
|
||||||
method: 'POST'
|
method: 'POST',
|
||||||
|
body: resolvedOrgId ? { organizationId: resolvedOrgId } : undefined
|
||||||
})
|
})
|
||||||
tokens.value = response.tokens
|
tokens.value = response.tokens
|
||||||
|
tokensOrgId.value = resolvedOrgId
|
||||||
|
if (resolvedOrgId) {
|
||||||
|
orgState.value = resolvedOrgId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
refreshPromise = null
|
refreshPromise = null
|
||||||
|
refreshPromiseOrgId = null
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -152,19 +169,21 @@ export const useLogtoTokens = () => {
|
|||||||
* Get access token for a resource URL.
|
* Get access token for a resource URL.
|
||||||
* Auto-refreshes if token is expired.
|
* 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
|
// Find resource key by URL
|
||||||
const entry = Object.entries(RESOURCES).find(([, url]) => url === resourceUrl)
|
const entry = Object.entries(RESOURCES).find(([, url]) => url === resourceUrl)
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
throw new Error(`Unknown resource: ${resourceUrl}`)
|
throw new Error(`Unknown resource: ${resourceUrl}`)
|
||||||
}
|
}
|
||||||
const key = entry[0] as ResourceKey
|
const key = entry[0] as ResourceKey
|
||||||
|
const resolvedOrgId = resolveOrganizationId(organizationId) || null
|
||||||
|
|
||||||
const tokenInfo = tokens.value[key]
|
const tokenInfo = tokens.value[key]
|
||||||
|
const isOrgMismatch = tokensOrgId.value !== resolvedOrgId
|
||||||
|
|
||||||
// If expired, refresh all tokens
|
// If expired (or cached for another org), refresh all tokens
|
||||||
if (isTokenExpired(tokenInfo)) {
|
if (isOrgMismatch || isTokenExpired(tokenInfo)) {
|
||||||
await refreshTokens()
|
await refreshTokens(resolvedOrgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshedToken = tokens.value[key]
|
const refreshedToken = tokens.value[key]
|
||||||
@@ -178,11 +197,13 @@ export const useLogtoTokens = () => {
|
|||||||
/**
|
/**
|
||||||
* Get token by resource key (teams, exchange, orders, kyc)
|
* 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 tokenInfo = tokens.value[key]
|
||||||
|
const resolvedOrgId = resolveOrganizationId(organizationId) || null
|
||||||
|
const isOrgMismatch = tokensOrgId.value !== resolvedOrgId
|
||||||
|
|
||||||
if (isTokenExpired(tokenInfo)) {
|
if (isOrgMismatch || isTokenExpired(tokenInfo)) {
|
||||||
await refreshTokens()
|
await refreshTokens(resolvedOrgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshedToken = tokens.value[key]
|
const refreshedToken = tokens.value[key]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
const originalConsoleError = console.error
|
const originalConsoleError = console.error
|
||||||
|
const originalConsoleWarn = console.warn
|
||||||
|
|
||||||
console.error = (...args: unknown[]) => {
|
const shouldSuppressApolloNoise = (args: unknown[]) => {
|
||||||
const serializedArgs = args
|
const serializedArgs = args
|
||||||
.map((arg) => {
|
.map((arg) => {
|
||||||
if (typeof arg === 'string') return arg
|
if (typeof arg === 'string') return arg
|
||||||
@@ -14,21 +15,34 @@ export default defineNuxtPlugin(() => {
|
|||||||
})
|
})
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
|
||||||
const hasApolloDevtoolsWarning = (
|
return (
|
||||||
(
|
(
|
||||||
serializedArgs.includes('connectToDevTools')
|
serializedArgs.includes('connectToDevTools')
|
||||||
&& serializedArgs.includes('devtools.enabled')
|
&& serializedArgs.includes('devtools.enabled')
|
||||||
)
|
)
|
||||||
|| (
|
|| (
|
||||||
serializedArgs.includes('go.apollo.dev/c/err')
|
serializedArgs.includes('go.apollo.dev/c/err')
|
||||||
&& serializedArgs.includes('"message":104')
|
&& (
|
||||||
|
serializedArgs.includes('"message":104')
|
||||||
|
|| serializedArgs.includes('%22message%22%3A104')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (hasApolloDevtoolsWarning) {
|
console.error = (...args: unknown[]) => {
|
||||||
|
if (shouldSuppressApolloNoise(args)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalConsoleError(...args)
|
originalConsoleError(...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn = (...args: unknown[]) => {
|
||||||
|
if (shouldSuppressApolloNoise(args)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
originalConsoleWarn(...args)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"from": "From",
|
"from": "From",
|
||||||
"to": "To",
|
"to": "To",
|
||||||
"cargo": "Cargo",
|
"cargo": "Cargo",
|
||||||
|
"product": "What",
|
||||||
|
"quantity": "How much",
|
||||||
"find": "Find",
|
"find": "Find",
|
||||||
"manager_navigation": "Manager navigation",
|
"manager_navigation": "Manager navigation",
|
||||||
"orders": "Orders",
|
"orders": "Orders",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"from": "Откуда",
|
"from": "Откуда",
|
||||||
"to": "Куда",
|
"to": "Куда",
|
||||||
"cargo": "Груз",
|
"cargo": "Груз",
|
||||||
|
"product": "Что",
|
||||||
|
"quantity": "Сколько",
|
||||||
"find": "Найти",
|
"find": "Найти",
|
||||||
"manager_navigation": "Навигация менеджера",
|
"manager_navigation": "Навигация менеджера",
|
||||||
"orders": "Заказы",
|
"orders": "Заказы",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineEventHandler, createError } from 'h3'
|
import { defineEventHandler, createError, readBody } from 'h3'
|
||||||
import type LogtoClient from '@logto/node'
|
import type LogtoClient from '@logto/node'
|
||||||
|
|
||||||
const RESOURCES = {
|
const RESOURCES = {
|
||||||
@@ -20,6 +20,10 @@ export interface RefreshResponse {
|
|||||||
tokens: Partial<Record<ResourceKey, TokenInfo>>
|
tokens: Partial<Record<ResourceKey, TokenInfo>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RefreshBody {
|
||||||
|
organizationId?: string
|
||||||
|
}
|
||||||
|
|
||||||
function decodeTokenExpiry(token: string): number {
|
function decodeTokenExpiry(token: string): number {
|
||||||
try {
|
try {
|
||||||
const payload = token.split('.')[1]
|
const payload = token.split('.')[1]
|
||||||
@@ -44,13 +48,20 @@ function decodeTokenExpiry(token: string): number {
|
|||||||
export default defineEventHandler(async (event): Promise<RefreshResponse> => {
|
export default defineEventHandler(async (event): Promise<RefreshResponse> => {
|
||||||
const client = event.context.logtoClient as LogtoClient | undefined
|
const client = event.context.logtoClient as LogtoClient | undefined
|
||||||
const logtoUser = event.context.logtoUser as { organizations?: string[] } | undefined
|
const logtoUser = event.context.logtoUser as { organizations?: string[] } | undefined
|
||||||
|
let body: RefreshBody = {}
|
||||||
|
try {
|
||||||
|
body = (await readBody<RefreshBody>(event)) || {}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
body = {}
|
||||||
|
}
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw createError({ statusCode: 401, message: 'Not authenticated' })
|
throw createError({ statusCode: 401, message: 'Not authenticated' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get first organization from Logto user
|
// Prefer explicit organizationId from client when switching active team.
|
||||||
const organizationId = logtoUser?.organizations?.[0]
|
const organizationId = body.organizationId || logtoUser?.organizations?.[0]
|
||||||
|
|
||||||
const tokens: Partial<Record<ResourceKey, TokenInfo>> = {}
|
const tokens: Partial<Record<ResourceKey, TokenInfo>> = {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user