import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose' import { GraphQLError } from 'graphql' import type { Request } from 'express' const LOGTO_JWKS_URL = process.env.LOGTO_JWKS_URL || 'https://auth.optovia.ru/oidc/jwks' const LOGTO_ISSUER = process.env.LOGTO_ISSUER || 'https://auth.optovia.ru/oidc' const LOGTO_EXCHANGE_AUDIENCE = process.env.LOGTO_EXCHANGE_AUDIENCE || 'https://exchange.optovia.ru' const jwks = createRemoteJWKSet(new URL(LOGTO_JWKS_URL)) export interface AuthContext { userId?: string teamUuid?: string scopes: string[] isM2M?: boolean } function getBearerToken(req: Request): string { const auth = req.headers.authorization || '' if (!auth.startsWith('Bearer ')) { throw new GraphQLError('Missing Bearer token', { extensions: { code: 'UNAUTHENTICATED' } }) } const token = auth.slice(7) if (!token || token === 'undefined') { throw new GraphQLError('Empty Bearer token', { extensions: { code: 'UNAUTHENTICATED' } }) } return token } function scopesFromPayload(payload: JWTPayload): string[] { const scope = payload.scope if (!scope) return [] if (typeof scope === 'string') return scope.split(' ') if (Array.isArray(scope)) return scope as string[] return [] } export async function publicContext(): Promise { return { scopes: [] } } export async function userContext(req: Request): Promise { const token = getBearerToken(req) const { payload } = await jwtVerify(token, jwks, { issuer: LOGTO_ISSUER }) return { userId: payload.sub, scopes: [] } } export async function teamContext(req: Request): Promise { const token = getBearerToken(req) const { payload } = await jwtVerify(token, jwks, { issuer: LOGTO_ISSUER, audience: LOGTO_EXCHANGE_AUDIENCE, }) const teamUuid = (payload as Record).team_uuid as string | undefined const scopes = scopesFromPayload(payload) if (!teamUuid || !scopes.includes('teams:member')) { throw new GraphQLError('Unauthorized', { extensions: { code: 'UNAUTHENTICATED' } }) } return { userId: payload.sub, teamUuid, scopes } } export async function m2mContext(): Promise { return { scopes: [], isM2M: true } } export function requireScopes(ctx: AuthContext, ...required: string[]): void { const missing = required.filter(s => !ctx.scopes.includes(s)) if (missing.length > 0) { throw new GraphQLError(`Missing required scopes: ${missing.join(', ')}`, { extensions: { code: 'FORBIDDEN' }, }) } }