diff --git a/src/auth.ts b/src/auth.ts index 5f2edfc..79e5c70 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -5,6 +5,7 @@ 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 MANAGER_JWT_ISSUER = 'optovia:teams' const jwks = createRemoteJWKSet(new URL(LOGTO_JWKS_URL)) @@ -68,6 +69,27 @@ export async function teamContext(req: Request): Promise { } } +function managerJwtSecret(): Uint8Array { + const secret = process.env.MANAGER_JWT_SECRET + if (!secret) throw new GraphQLError('MANAGER_JWT_SECRET is required', { extensions: { code: 'INTERNAL_SERVER_ERROR' } }) + return new TextEncoder().encode(secret) +} + +export async function managerContext(req: Request): Promise { + const token = getBearerToken(req) + const { payload } = await jwtVerify(token, managerJwtSecret(), { + issuer: MANAGER_JWT_ISSUER, + audience: LOGTO_ORDERS_AUDIENCE, + }) + const scopes = scopesFromPayload(payload) + const role = (payload as Record).role + const teamUuid = (payload as Record).team_uuid as string | undefined + if (!payload.sub || role !== 'manager' || !scopes.includes('manager') || !teamUuid) { + throw new GraphQLError('Unauthorized', { extensions: { code: 'UNAUTHENTICATED' } }) + } + return { userId: payload.sub, teamUuid, scopes: ['teams:member', 'manager'] } +} + export function requireScopes(ctx: AuthContext, ...required: string[]): void { const missing = required.filter(s => !ctx.scopes.includes(s)) if (missing.length > 0) { diff --git a/src/index.ts b/src/index.ts index 5d4026d..652a73f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import * as Sentry from '@sentry/node' import { publicTypeDefs, publicResolvers } from './schemas/public.js' import { userTypeDefs, userResolvers } from './schemas/user.js' import { teamTypeDefs, teamResolvers } from './schemas/team.js' -import { publicContext, userContext, teamContext, type AuthContext } from './auth.js' +import { publicContext, userContext, teamContext, managerContext, type AuthContext } from './auth.js' const PORT = parseInt(process.env.PORT || '8000', 10) const SENTRY_DSN = process.env.SENTRY_DSN || '' @@ -41,8 +41,13 @@ const teamServer = new ApolloServer({ resolvers: teamResolvers, introspection: true, }) +const managerServer = new ApolloServer({ + typeDefs: teamTypeDefs, + resolvers: teamResolvers, + introspection: true, +}) -await Promise.all([publicServer.start(), userServer.start(), teamServer.start()]) +await Promise.all([publicServer.start(), userServer.start(), teamServer.start(), managerServer.start()]) app.use( '/graphql/public', @@ -74,6 +79,14 @@ app.use( }) as unknown as express.RequestHandler, ) +app.use( + '/graphql/manager', + express.json(), + expressMiddleware(managerServer, { + context: async ({ req }) => managerContext(req as unknown as import('express').Request), + }) as unknown as express.RequestHandler, +) + app.get('/health', (_, res) => { res.json({ status: 'ok' }) }) @@ -83,4 +96,5 @@ app.listen(PORT, '0.0.0.0', () => { console.log(` /graphql/public - public`) console.log(` /graphql/user - id token auth`) console.log(` /graphql/team - team access token auth`) + console.log(` /graphql/manager - manager JWT auth`) })