Initial commit from monorepo
This commit is contained in:
152
server/middleware/00-logto.ts
Normal file
152
server/middleware/00-logto.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
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 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 || 'https://auth.optovia.ru'
|
||||
const appId = process.env.NUXT_LOGTO_APP_ID || ''
|
||||
const appSecret = process.env.NUXT_LOGTO_APP_SECRET || ''
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = getRequestURL(event)
|
||||
|
||||
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 (url.pathname === '/sign-in') {
|
||||
await logto.signIn({
|
||||
redirectUri: new URL('/callback', url).href
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === '/sign-out') {
|
||||
await logto.signOut(new URL('/', url).href)
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === '/callback') {
|
||||
await logto.handleSignInCallback(url.href)
|
||||
await sendRedirect(event, '/', 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
|
||||
}
|
||||
})
|
||||
74
server/middleware/me.ts
Normal file
74
server/middleware/me.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type LogtoClient from '@logto/node'
|
||||
import { print } from 'graphql'
|
||||
|
||||
type SelectedLocation = {
|
||||
type: string
|
||||
uuid: string
|
||||
name: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
} | null
|
||||
|
||||
type MePayload = {
|
||||
id?: string | null
|
||||
firstName?: string | null
|
||||
lastName?: string | null
|
||||
phone?: string | null
|
||||
avatarId?: string | null
|
||||
activeTeamId?: string | null
|
||||
activeTeam?: {
|
||||
name?: string | null
|
||||
teamType?: string | null
|
||||
logtoOrgId?: string | null
|
||||
selectedLocation?: SelectedLocation
|
||||
} | null
|
||||
teams?: Array<{ id?: string | null; name?: string | null; logtoOrgId?: string | null; teamType?: string | null } | null> | null
|
||||
} | null
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (event.context.meLoaded) return
|
||||
event.context.meLoaded = true
|
||||
|
||||
const client = event.context.logtoClient as LogtoClient | undefined
|
||||
if (!client) return
|
||||
|
||||
let idToken: string | null = null
|
||||
try {
|
||||
idToken = await client.getIdToken()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (!idToken) return
|
||||
|
||||
try {
|
||||
const { GetMeDocument, GetMeProfileDocument } = await import('~/composables/graphql/user/teams-generated')
|
||||
const endpoint = process.env.NUXT_PUBLIC_TEAMS_GRAPHQL_USER || 'https://teams.optovia.ru/graphql/user/'
|
||||
|
||||
const [meResponse, profileResponse] = await Promise.all([
|
||||
$fetch<{ data?: { me?: MePayload } }>(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${idToken}` },
|
||||
body: { query: print(GetMeDocument) }
|
||||
}),
|
||||
$fetch<{ data?: { me?: MePayload } }>(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${idToken}` },
|
||||
body: { query: print(GetMeProfileDocument) }
|
||||
})
|
||||
])
|
||||
|
||||
const baseMe = meResponse?.data?.me ?? null
|
||||
const profileMe = profileResponse?.data?.me ?? null
|
||||
if (baseMe || profileMe) {
|
||||
event.context.me = {
|
||||
...(baseMe || {}),
|
||||
...(profileMe || {}),
|
||||
activeTeam: baseMe?.activeTeam || profileMe?.activeTeam || null,
|
||||
teams: baseMe?.teams || null
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore if user context can't be fetched
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user