Migrate orders backend from Django to Express + Apollo Server
Build Docker Image / build (push) Successful in 1m37s
Build Docker Image / build (push) Successful in 1m37s
Replace Python/Django/Graphene stack with TypeScript/Express/Apollo Server. Same 3 GraphQL endpoints (public/user/team), same JWT auth via Logto JWKS, same Odoo proxy logic. No database needed (pure proxy).
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
||||
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_ORDERS_AUDIENCE = process.env.LOGTO_ORDERS_AUDIENCE || 'https://orders.optovia.ru'
|
||||
|
||||
const jwks = createRemoteJWKSet(new URL(LOGTO_JWKS_URL))
|
||||
|
||||
export interface AuthContext {
|
||||
userId?: string
|
||||
teamUuid?: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
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: scopesFromPayload(payload),
|
||||
}
|
||||
}
|
||||
|
||||
export async function teamContext(req: Request): Promise<AuthContext> {
|
||||
const token = getBearerToken(req)
|
||||
const { payload } = await jwtVerify(token, jwks, {
|
||||
issuer: LOGTO_ISSUER,
|
||||
audience: LOGTO_ORDERS_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 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' },
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user