Files
webapp/server/middleware/00-logto.ts

181 lines
5.2 KiB
TypeScript

import LogtoClient, { CookieStorage } from '@logto/node'
import { getRequestURL, getCookie, setCookie, sendRedirect, type H3Event } from 'h3'
import { randomUUID } from 'crypto'
const SESSION_COOKIE_NAME = 'logtoSession'
const LEGACY_COOKIE_NAME = 'logtoCookies'
const SCOPES = [
'openid',
'profile',
'email',
'offline_access',
'urn:logto:scope:organizations',
'urn:logto:scope:organization_token',
'teams:member'
]
const RESOURCES = [
'https://teams.optovia.ru',
'https://orders.optovia.ru',
'https://kyc.optovia.ru',
'https://exchange.optovia.ru',
'https://billing.optovia.ru'
]
const LOCALE_CODES = ['ru', 'en'] as const
function resolveLocalizedPath(pathname: string) {
const trimmed = pathname === '/' ? '/' : pathname.replace(/\/+$/, '') || '/'
const segments = trimmed.split('/').filter(Boolean)
const firstSegment = segments[0]
if (firstSegment && LOCALE_CODES.includes(firstSegment as (typeof LOCALE_CODES)[number])) {
const normalized = `/${segments.slice(1).join('/')}` || '/'
const normalizedPath = normalized === '//' ? '/' : normalized
return {
localePrefix: `/${firstSegment}`,
normalizedPath: normalizedPath || '/',
}
}
return {
localePrefix: '',
normalizedPath: trimmed,
}
}
const createSessionWrapper = (event: H3Event) => {
const storage = useStorage('logto')
let currentSessionId = ''
return {
wrap: async (data: Record<string, unknown>) => {
if (!data || Object.keys(data).length === 0) {
if (currentSessionId) {
await storage.removeItem(`session:${currentSessionId}`)
}
currentSessionId = ''
return ''
}
if (!currentSessionId) {
currentSessionId = randomUUID()
}
await storage.setItem(`session:${currentSessionId}`, data)
return currentSessionId
},
unwrap: async (value: string) => {
currentSessionId = value || ''
if (!currentSessionId) return {}
const stored = await storage.getItem<Record<string, unknown>>(`session:${currentSessionId}`)
if (!stored) {
// Session ID exists in cookie but data not found in storage - clear the stale cookie
setCookie(event, SESSION_COOKIE_NAME, '', { path: '/', maxAge: 0 })
currentSessionId = ''
return {}
}
return stored
}
}
}
export default defineEventHandler(async (event) => {
const endpoint = process.env.NUXT_LOGTO_ENDPOINT || process.env.LOGTO_ENDPOINT || 'https://auth.optovia.ru'
const appId = process.env.NUXT_LOGTO_APP_ID || process.env.LOGTO_APP_ID || process.env.LOGTO_CLIENT_ID || ''
const appSecret = process.env.NUXT_LOGTO_APP_SECRET || process.env.LOGTO_APP_SECRET || process.env.LOGTO_CLIENT_SECRET || ''
const url = getRequestURL(event)
const { localePrefix, normalizedPath } = resolveLocalizedPath(url.pathname)
if (!appId || !appSecret) {
if (normalizedPath === '/sign-in' || normalizedPath === '/sign-out' || normalizedPath === '/callback') {
await sendRedirect(event, localePrefix || '/', 302)
}
return
}
if (getCookie(event, LEGACY_COOKIE_NAME)) {
setCookie(event, LEGACY_COOKIE_NAME, '', { path: '/', maxAge: 0 })
}
// Check for stale session cookie BEFORE initializing CookieStorage
const existingSessionId = getCookie(event, SESSION_COOKIE_NAME)
if (existingSessionId) {
const nitroStorage = useStorage('logto')
const sessionData = await nitroStorage.getItem(`session:${existingSessionId}`)
if (!sessionData) {
// Session cookie exists but no data in storage - clear it
setCookie(event, SESSION_COOKIE_NAME, '', { path: '/', maxAge: 0 })
}
}
const storage = new CookieStorage({
cookieKey: SESSION_COOKIE_NAME,
isSecure: process.env.NODE_ENV === 'production',
sessionWrapper: createSessionWrapper(event),
getCookie: async (name) => getCookie(event, name),
setCookie: async (name, value, options) => {
setCookie(event, name, value, options)
}
})
await storage.init()
const logto = new LogtoClient(
{
endpoint,
appId,
appSecret,
resources: RESOURCES,
scopes: SCOPES
},
{
navigate: async (target) => {
await sendRedirect(event, target, 302)
},
storage
}
)
if (normalizedPath === '/sign-in') {
const callbackPath = `${localePrefix}/callback`
await logto.signIn({
redirectUri: new URL(callbackPath || '/callback', url).href
})
return
}
if (normalizedPath === '/sign-out') {
const homePath = localePrefix || '/'
await logto.signOut(new URL(homePath, url).href)
return
}
if (normalizedPath === '/callback') {
await logto.handleSignInCallback(url.href)
const clientareaPath = `${localePrefix}/clientarea`
await sendRedirect(event, clientareaPath || '/clientarea', 302)
return
}
event.context.logtoClient = logto
if (await logto.isAuthenticated()) {
try {
event.context.logtoUser = await logto.fetchUserInfo()
} catch {
try {
event.context.logtoUser = await logto.getIdTokenClaims()
} catch {
event.context.logtoUser = undefined
}
}
}
const orgId = (event.context.logtoUser as { organizations?: string[] } | undefined)?.organizations?.[0]
if (orgId) {
event.context.logtoOrgId = orgId
}
})