UNPKG

@accounter/server

Version:
173 lines (146 loc) 5.53 kB
import { defaultFieldResolver, GraphQLError, GraphQLSchema } from 'graphql'; import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'; import { AuthContextProvider } from '../providers/auth-context.provider.js'; type RoleDirectiveArgs = { role: string; }; type AnyRoleDirectiveArgs = { roles: string[]; }; /** * IMPORTANT MAINTENANCE NOTE: * * This file intentionally uses graphql-modules internals to resolve an operation injector * in schema-transformed field resolvers: * - resolver symbol metadata (to infer moduleId) * - context.ɵinjector * - context.ɵgetModuleContext(...) * * Rationale: * When resolvers are wrapped by `mapSchema`, `context.injector` is not always available at * this point in the execution flow. Without these fallbacks, auth directives may fail with * missing injector errors even for valid authenticated requests. * * Risk: * These are private implementation details and may break on graphql-modules upgrades. * If auth starts failing after dependency updates, inspect this code first. * * Preferred long-term direction: * Replace this with a stable/public graphql-modules API for module context/injector access * as soon as one is available. */ type MaybeContext = { injector?: { get: (token: unknown) => unknown }; ɵinjector?: { get: (token: unknown) => unknown }; ɵgetModuleContext?: ( moduleId: string, context: unknown, ) => { injector?: { get: (token: unknown) => unknown } }; }; function extractModuleIdFromResolver(resolve: unknown): string | undefined { if (typeof resolve !== 'function') { return undefined; } const resolverWithSymbols = resolve as unknown as Record<symbol, unknown>; for (const symbol of Object.getOwnPropertySymbols(resolve)) { const metadata = resolverWithSymbols[symbol]; if ( metadata && typeof metadata === 'object' && 'moduleId' in metadata && typeof (metadata as { moduleId?: unknown }).moduleId === 'string' ) { return (metadata as { moduleId: string }).moduleId; } } return undefined; } function resolveInjector(context: MaybeContext, moduleId: string | undefined) { if (context?.injector) { return context.injector; } if (context?.ɵinjector) { return context.ɵinjector; } if (typeof context?.ɵgetModuleContext !== 'function') { return undefined; } const moduleCandidates = [moduleId, 'auth'].filter( (candidate): candidate is string => !!candidate, ); for (const candidate of moduleCandidates) { try { const moduleContext = context.ɵgetModuleContext(candidate, context); if (moduleContext?.injector) { return moduleContext.injector; } } catch { // Ignore invalid module candidate and continue with the next fallback. } } return undefined; } function throwAuthInfrastructureError(reason: string): never { console.error(`[auth-directives] ${reason}`); throw new GraphQLError('Authentication subsystem unavailable', { extensions: { code: 'INTERNAL_SERVER_ERROR' }, }); } export function authDirectiveTransformer(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: fieldConfig => { const requiresAuthDirective = getDirective(schema, fieldConfig, 'requiresAuth')?.[0]; const requiresRoleDirective = getDirective(schema, fieldConfig, 'requiresRole')?.[0] as | RoleDirectiveArgs | undefined; const requiresAnyRoleDirective = getDirective(schema, fieldConfig, 'requiresAnyRole')?.[0] as | AnyRoleDirectiveArgs | undefined; if (!requiresAuthDirective && !requiresRoleDirective && !requiresAnyRoleDirective) { return fieldConfig; } const { resolve = defaultFieldResolver } = fieldConfig; const moduleId = extractModuleIdFromResolver(resolve); return { ...fieldConfig, resolve: async (source, args, context: MaybeContext, info) => { const injector = resolveInjector(context, moduleId); if (!injector) { throwAuthInfrastructureError( `Failed to resolve injector for auth directives (moduleId: ${moduleId ?? 'unknown'})`, ); } const authProvider = injector.get(AuthContextProvider) as AuthContextProvider | undefined; if (!authProvider) { throwAuthInfrastructureError( `AuthContextProvider is not available in injector (moduleId: ${moduleId ?? 'unknown'})`, ); } const authContext = await authProvider.getAuthContext(); if (!authContext?.user) { throw new GraphQLError('Authentication required', { extensions: { code: 'UNAUTHENTICATED' }, }); } if (requiresRoleDirective) { const role = requiresRoleDirective.role; if (authContext.user.roleId !== role) { throw new GraphQLError(`Requires role: ${role}`, { extensions: { code: 'FORBIDDEN' }, }); } } if (requiresAnyRoleDirective) { const roles = requiresAnyRoleDirective.roles; if (!roles.includes(authContext.user.roleId)) { throw new GraphQLError(`Requires one of roles: ${roles.join(', ')}`, { extensions: { code: 'FORBIDDEN' }, }); } } return resolve(source, args, context, info); }, }; }, }); }