// SECURE: GraphQL server with comprehensive security
const { ApolloServer, gql } = require('apollo-server-express');
const { NoIntrospection, specifiedRules } = require('graphql');
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity').costAnalysisValidator;
const express = require('express');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const isProduction = process.env.NODE_ENV === 'production';
// Sanitized schema - sensitive fields removed or protected
const typeDefs = gql`
type User {
id: ID!
email: String!
# passwordHash removed from schema
role: Role! # Enum instead of internal levels
createdAt: DateTime!
# Sensitive fields removed
}
enum Role {
USER
ADMIN
}
type Query {
# Public queries only
me: User # Current user only
publicUsers: [User!]! # Limited public data
# Internal admin queries removed from public schema
}
type Mutation {
# Safe operations only
updateProfile(input: ProfileInput!): User
changePassword(currentPassword: String!, newPassword: String!): Boolean
# Dangerous admin operations removed
}
input ProfileInput {
firstName: String
lastName: String
bio: String
}
`;
// Rate limiting for GraphQL
const graphqlLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: isProduction ? 100 : 1000, // Stricter in production
message: {
error: 'Too many GraphQL requests',
retryAfter: 60
},
keyGenerator: (req) => {
// Rate limit by user ID if authenticated, otherwise by IP
return req.user?.id || req.ip;
}
});
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) throw new Error('Authentication required');
return User.findById(user.id).select('-passwordHash -internalNotes');
},
publicUsers: () => {
return User.find({ isPublic: true })
.select('id email role createdAt')
.limit(50);
}
},
Mutation: {
updateProfile: async (_, { input }, { user }) => {
if (!user) throw new Error('Authentication required');
return User.findByIdAndUpdate(
user.id,
{ $set: input },
{ new: true }
).select('-passwordHash');
}
}
};
// Security configuration
const server = new ApolloServer({
typeDefs,
resolvers,
// SECURITY: Disable introspection and playground in production
introspection: !isProduction,
playground: !isProduction,
// Query validation and complexity analysis
validationRules: [
...specifiedRules,
...(isProduction ? [NoIntrospection] : []),
depthLimit(10),
costAnalysis({
maximumCost: 1000,
defaultCost: 1,
createError: (max, actual) => {
throw new Error(`Query cost ${actual} exceeds maximum ${max}`);
}
})
],
// Context with authentication
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? verifyToken(token) : null;
return {
user,
isProduction,
userAgent: req.headers['user-agent'],
ip: req.ip
};
},
// Error formatting - hide sensitive information
formatError: (error) => {
// Log full error server-side
console.error('GraphQL Error:', {
message: error.message,
locations: error.locations,
path: error.path,
source: error.source?.body
});
if (isProduction) {
// Hide internal errors in production
if (error.message.includes('introspection')) {
return new Error('Query not allowed');
}
if (error.originalError?.code === 'INTERNAL_ERROR') {
return new Error('An internal error occurred');
}
}
return error;
},
// Monitoring and security plugins
plugins: [
{
requestDidStart() {
return {
didResolveOperation(requestContext) {
const query = requestContext.request.query;
// Monitor for introspection attempts
if (query?.includes('__schema') || query?.includes('__type')) {
console.warn('Introspection attempt detected:', {
query: query.substring(0, 200),
userAgent: requestContext.request.http?.headers?.get('user-agent'),
ip: requestContext.request.ip,
timestamp: new Date().toISOString()
});
if (isProduction) {
throw new Error('Introspection is disabled');
}
}
// Monitor for complex queries
if (query?.length > 2000) {
console.warn('Large query detected:', {
size: query.length,
userAgent: requestContext.request.http?.headers?.get('user-agent'),
ip: requestContext.request.ip
});
}
}
};
}
}
]
});
const app = express();
// Additional security middleware
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
// Apply rate limiting
app.use('/graphql', graphqlLimiter);
// Apply GraphQL middleware
server.applyMiddleware({
app,
path: '/graphql',
cors: {
origin: isProduction ? process.env.ALLOWED_ORIGINS?.split(',') : true,
credentials: true
}
});
app.listen(4000, () => {
console.log(`GraphQL server running on http://localhost:4000${server.graphqlPath}`);
console.log(`Introspection: ${!isProduction ? 'enabled' : 'disabled'}`);
console.log(`Playground: ${!isProduction ? 'enabled' : 'disabled'}`);
});