Files
exchange/src/auth.ts
Ruslan Bakiev 27b86c85b7
All checks were successful
Build Docker Image / build (push) Successful in 1m54s
Migrate exchange backend from Django to Express + Apollo Server + Prisma
Replace Python/Django/Graphene with TypeScript/Express/Apollo Server.
Same 4 endpoints (public/user/team/m2m), same JWT auth.
Prisma replaces Django ORM for Offer/Request/SupplierProfile.
Temporal and Odoo integrations preserved.
2026-03-09 09:20:37 +07:00

74 lines
2.5 KiB
TypeScript

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<AuthContext> {
return { scopes: [] }
}
export async function userContext(req: Request): Promise<AuthContext> {
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<AuthContext> {
const token = getBearerToken(req)
const { payload } = await jwtVerify(token, jwks, {
issuer: LOGTO_ISSUER,
audience: LOGTO_EXCHANGE_AUDIENCE,
})
const teamUuid = (payload as Record<string, unknown>).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<AuthContext> {
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' },
})
}
}