UNPKG

@ideal-photography/shared

Version:

Shared GraphQL (Apollo Server v5) and Mongoose logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, and audit logs.

334 lines (307 loc) 11.2 kB
import { mergeTypeDefs } from '@graphql-tools/merge'; import { mergeResolvers } from '@graphql-tools/merge'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@as-integrations/express5'; import jwt from 'jsonwebtoken'; import { models } from '../mongoDB/index.js'; // Import all typeDefs import userTypeDefs from './typeDefs/user.type.js'; import adminTypeDefs from './typeDefs/admin.type.js'; import productTypeDefs from './typeDefs/product.type.js'; import bookingTypeDefs from './typeDefs/booking.type.js'; import serviceTypeDefs from './typeDefs/service.type.js'; import galleryTypeDefs from './typeDefs/gallery.type.js'; import reviewTypeDefs from './typeDefs/review.type.js'; import orderTypeDefs from './typeDefs/order.type.js'; import campaignTypeDefs from './typeDefs/campaign.type.js'; import notificationTypeDefs from './typeDefs/notification.type.js'; import settingsTypeDefs from './typeDefs/settings.type.js'; import auditLogTypeDefs from './typeDefs/auditLog.type.js'; import mediaTypeDefs from './typeDefs/media.type.js'; import emailTemplateTypeDefs from './typeDefs/emailTemplate.type.js'; import commonTypes from './typeDefs/common.types.js'; // Import all resolvers import userResolvers from './resolvers/user.resolver.js'; import adminResolvers from './resolvers/admin.resolver.js'; import productResolvers from './resolvers/product.resolver.js'; import bookingResolvers from './resolvers/booking.resolver.js'; import serviceResolvers from './resolvers/service.resolver.js'; import galleryResolvers from './resolvers/gallery.resolver.js'; import reviewResolvers from './resolvers/review.resolver.js'; import orderResolvers from './resolvers/order.resolver.js'; import campaignResolvers from './resolvers/campaign.resolver.js'; import notificationResolvers from './resolvers/notification.resolver.js'; import settingsResolvers from './resolvers/settings.resolver.js'; import auditLogResolvers from './resolvers/auditLog.resolver.js'; import mediaResolvers from './resolvers/media.resolver.js'; import emailTemplateResolvers from './resolvers/emailTemplate.resolver.js'; import scalarResolvers from './resolvers/scalars.js'; // Base GraphQL schema for common scalars and directives const baseTypeDefs = ` scalar JSON scalar Upload scalar Date scalar DateTime scalar Time directive @auth(requires: [String!]) on FIELD_DEFINITION directive @admin(requires: [String!]) on FIELD_DEFINITION directive @owner on FIELD_DEFINITION directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION # Common shared types type Query { _empty: String } type Mutation { _empty: String } type Subscription { _empty: String } `; // Merge all typeDefs and resolvers const typeDefs = mergeTypeDefs([ baseTypeDefs, commonTypes, userTypeDefs, adminTypeDefs, productTypeDefs, bookingTypeDefs, serviceTypeDefs, galleryTypeDefs, reviewTypeDefs, orderTypeDefs, campaignTypeDefs, notificationTypeDefs, settingsTypeDefs, auditLogTypeDefs, mediaTypeDefs, emailTemplateTypeDefs, ]); const resolvers = mergeResolvers([ scalarResolvers, userResolvers, adminResolvers, productResolvers, bookingResolvers, serviceResolvers, galleryResolvers, reviewResolvers, orderResolvers, campaignResolvers, notificationResolvers, settingsResolvers, auditLogResolvers, mediaResolvers, emailTemplateResolvers, ]); // Authentication middleware const authMiddleware = (req) => { const authHeader = req.headers.authorization; const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.substring(7) : null; if (!token) { req.user = null; return { token: null, user: null }; } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); // Attach minimal claims in case DB lookup fails req.tokenPayload = decoded; return { token, user: null, }; } catch (err) { // Invalid token; proceed unauthenticated req.user = null; return { token: null, user: null }; } }; // Audit logging middleware const auditMiddleware = async (req, resolverName, args, result, error) => { // TODO: Implement audit logging for mutations if (resolverName.includes('Mutation') && req.user) { try { const { utils: mongoUtils } = await import('../mongoDB/index.js'); await mongoUtils.createAuditLog( resolverName, { user: req.user._id, userInfo: { name: req.user.name, email: req.user.email, role: req.user.role } }, { resourceType: 'graphql', resourceName: resolverName }, { status: error ? 'failure' : 'success', message: error?.message }, { request: { method: 'POST', url: '/graphql' } } ); } catch (auditError) { console.error('Audit logging failed:', auditError); } } }; // Enhanced context function const createContext = async ({ req, res }) => { const auth = authMiddleware(req); let user = null; // If an upstream middleware (like admin server) already populated req.user, trust it if (req.user) { user = req.user; } else if (auth.token) { try { if (req.tokenPayload?.userId) { const foundUser = await models.User.findById(req.tokenPayload.userId).select('-password'); if (foundUser && foundUser.isActive && !foundUser.isLocked) { user = foundUser; req.user = foundUser; } } else if (req.tokenPayload?.adminId) { const foundAdmin = await models.Admin.findById(req.tokenPayload.adminId).select('-password'); if (foundAdmin && foundAdmin.isActive) { user = foundAdmin; // expose as user in context for authorization helpers req.user = foundAdmin; } } } catch (_) { req.user = null; } } return { req, res, user, token: auth.token, isAuthenticated: !!user, isAdmin: user?.role === 'admin' || user?.role === 'super_admin' || user?.role === 'manager', isSuperAdmin: user?.role === 'super_admin', // Helper functions for common operations requireAuth: () => { if (!user) throw new Error('Authentication required'); }, requireAdmin: () => { if (!user || !['admin', 'super_admin', 'manager'].includes(user.role)) { throw new Error('Admin access required'); } }, requireSuperAdmin: () => { if (!user || user.role !== 'super_admin') { throw new Error('Super admin access required'); } }, checkPermission: (permission) => { if (!user?.permissions?.includes(permission)) { throw new Error(`Permission ${permission} required`); } }, // Audit logging helper audit: (action, target, result, details = {}) => { return auditMiddleware(req, action, {}, result, null); } }; }; // Plugin for authentication and authorization const authPlugin = { requestDidStart() { return { didResolveOperation(requestContext) { // TODO: Check rate limiting here }, willSendResponse(requestContext) { // TODO: Log response metrics } }; } }; // Plugin for audit logging const auditPlugin = { requestDidStart() { return { didEncounterErrors(requestContext) { // Log GraphQL errors console.error('GraphQL Error:', requestContext.errors); } }; } }; // Helper function to create Apollo Server v5 instance const createApolloServer = (options = {}) => { return new ApolloServer({ typeDefs, resolvers, plugins: [ authPlugin, auditPlugin, ...(options.plugins || []) ], formatError: (error) => { // Log errors but don't expose internal details in production console.error('GraphQL Error:', error); if (process.env.NODE_ENV === 'production') { // Hide internal error details in production if (error.message.includes('ValidationError') || error.message.includes('CastError') || error.message.includes('MongoError')) { return new Error('An internal error occurred'); } } return error; }, introspection: process.env.NODE_ENV !== 'production', ...options, }); }; // Helper function to apply Apollo Server middleware to Express app const applyApolloMiddleware = async (app, server, options = {}) => { await server.start(); const defaultOptions = { context: createContext, cors: { origin: process.env.CLIENT_URL || 'http://localhost:4001', credentials: true } }; app.use( '/graphql', expressMiddleware(server, { ...defaultOptions, ...options, context: async ({ req, res }) => { const baseContext = await createContext({ req, res }); const customContext = options.context ? await options.context({ req, res }) : {}; return { ...baseContext, ...customContext }; } }) ); }; // Helper function for subscription server setup const createSubscriptionServer = (httpServer, options = {}) => { // TODO: Implement WebSocket subscriptions using graphql-ws return { start: () => console.log('Subscription server started'), stop: () => console.log('Subscription server stopped') }; }; export { typeDefs, resolvers, createApolloServer, applyApolloMiddleware, createSubscriptionServer, createContext, authMiddleware, auditMiddleware, // Legacy exports for backward compatibility ApolloServer, expressMiddleware }; // Additional utilities export const utils = { formatError: (error) => { console.error('GraphQL Error:', error); return error; }, authenticate: authMiddleware, authorize: (roles = []) => (user) => { if (!user) throw new Error('Authentication required'); if (roles.length > 0 && !roles.includes(user.role)) { throw new Error('Insufficient permissions'); } return true; } };