UNPKG

@godspeedsystems/plugins-express-as-http

Version:

Godspeed event source plugin for express as http server

333 lines 17.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_CONFIG = exports.CONFIG_FILE_NAME = exports.Type = exports.SourceType = exports.EventSource = void 0; const core_1 = require("@godspeedsystems/core"); const express_1 = __importDefault(require("express")); const body_parser_1 = __importDefault(require("body-parser")); const metrics_1 = __importDefault(require("@godspeedsystems/metrics")); const cors_1 = __importDefault(require("cors")); //@ts-ignore const express_prometheus_middleware_1 = __importDefault(require("@godspeedsystems/express-prometheus-middleware")); const passport_1 = __importDefault(require("passport")); const session = require('express-session'); const express_fileupload_1 = __importDefault(require("express-fileupload")); const passport_jwt_1 = require("passport-jwt"); const passport_github2_1 = require("passport-github2"); const passport_linkedin_oauth2_1 = require("passport-linkedin-oauth2"); const passport_google_oauth2_1 = require("passport-google-oauth2"); class EventSource extends core_1.GSEventSource { initClient() { return __awaiter(this, void 0, void 0, function* () { const app = (0, express_1.default)(); const { port = 3000, docs = { endpoint: '/api-docs' } } = this.config; this.setupMiddleware(app); this.setupAuthentication(app); // Start the Express server app.listen(port, () => { core_1.logger.info(`Server running on port ${port}`); core_1.logger.info(`Try it out at: http://localhost:${port}${docs.endpoint}`); }); if (process.env.OTEL_ENABLED === 'true') { this.setupMetrics(app); } return app; }); } setupMiddleware(app) { var _a; app.use((0, cors_1.default)()); app.use(body_parser_1.default.urlencoded({ extended: true, limit: this.config.request_body_limit || '50mb' })); app.use(body_parser_1.default.json({ limit: this.config.file_size_limit || '50mb' })); app.use((0, express_fileupload_1.default)({ useTempFiles: true, limits: { fileSize: this.config.file_size_limit || '50mb' }, abortOnLimit: true })); app.use(session({ secret: ((_a = this.config.session) === null || _a === void 0 ? void 0 : _a.secret) || 'mysecret', resave: false, saveUninitialized: false })); } setupAuthentication(app) { var _a, _b, _c, _d, _e, _f, _g; const jwtConfig = ((_a = this.config.authn) === null || _a === void 0 ? void 0 : _a.jwt) || this.config.jwt; const githubConfig = (_c = (_b = this.config.authn) === null || _b === void 0 ? void 0 : _b.oauth2) === null || _c === void 0 ? void 0 : _c.github; const googleConfig = (_e = (_d = this.config.authn) === null || _d === void 0 ? void 0 : _d.oauth2) === null || _e === void 0 ? void 0 : _e.google; const linkedinConfig = (_g = (_f = this.config.authn) === null || _f === void 0 ? void 0 : _f.oauth2) === null || _g === void 0 ? void 0 : _g.linkedin; if (jwtConfig) { this.setupJwtAuthentication(app, jwtConfig); } if (googleConfig) { this.setupGoogleAuthentication(app, googleConfig); } if (githubConfig) { this.setupGithubAuthentication(app, githubConfig); } if (linkedinConfig) { this.setupLinkedInAuthentication(app, linkedinConfig); } } setupJwtAuthentication(app, jwtConfig) { if (!jwtConfig.secretOrKey || !jwtConfig.audience || !jwtConfig.issuer) { core_1.logger.fatal('JWT configuration error. Exiting'); process.exit(1); } app.use(passport_1.default.initialize()); app.use(passport_1.default.session()); passport_1.default.use(new passport_jwt_1.Strategy({ jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: jwtConfig.secretOrKey, ignoreExpiration: true, jsonWebTokenOptions: { audience: jwtConfig.audience, issuer: jwtConfig.issuer, }, }, (jwtPayload, done) => done(null, jwtPayload))); } setupGoogleAuthentication(app, googleConfig) { if (!googleConfig.client_id || !googleConfig.client_secret || !googleConfig.callback_url) { core_1.logger.fatal('Google configuration error. Exiting'); process.exit(1); } app.use(passport_1.default.initialize()); app.use(passport_1.default.session()); passport_1.default.use(new passport_google_oauth2_1.Strategy({ clientID: googleConfig.client_id, clientSecret: googleConfig.client_secret, callbackURL: googleConfig.callback_url, passReqToCallback: true // Enable this option }, function (req, accessToken, refreshToken, profile, done) { return __awaiter(this, void 0, void 0, function* () { core_1.logger.info("******", profile); return done(null, profile); }); })); const authRoute = googleConfig.auth_route || '/auth/google'; const callbackRoute = googleConfig.callback_route || '/auth/google/callback'; const failureRedirectURL = googleConfig.failure_redirect || '/error'; const successRedirectURL = googleConfig.success_redirect || '/verify/user'; // ***************Authentication routes ********************* app.get(authRoute, passport_1.default.authenticate('google', { scope: ['email', 'profile'] }), (req, res) => { }); app.get(callbackRoute, passport_1.default.authenticate('google'), (req, res) => __awaiter(this, void 0, void 0, function* () { res.redirect(successRedirectURL); })); passport_1.default.serializeUser(function (user, done) { done(null, user); }); passport_1.default.deserializeUser(function (obj, done) { done(null, obj); }); } setupLinkedInAuthentication(app, linkedinConfig) { if (!linkedinConfig.client_id || !linkedinConfig.client_secret || !linkedinConfig.callback_url) { core_1.logger.fatal('LinkedIn configuration error. Exiting'); process.exit(1); } app.use(passport_1.default.initialize()); app.use(passport_1.default.session()); passport_1.default.use(new passport_linkedin_oauth2_1.Strategy({ clientID: linkedinConfig.client_id, clientSecret: linkedinConfig.client_secret, callbackURL: linkedinConfig.callback_url, scope: linkedinConfig.scope || ['openid', 'email', 'profile'], // state: true }, (accessToken, refreshToken, profile, done) => done(null, profile))); const authRoute = linkedinConfig.auth_route || '/auth/linkedin'; const callbackRoute = linkedinConfig.callback_route || '/auth/linkedin/callback'; const failureRedirectURL = linkedinConfig.failure_redirect || '/error'; const successRedirectURL = linkedinConfig.success_redirect || '/verify/user'; // LinkedIn Authentication Routes app.get(authRoute, passport_1.default.authenticate('linkedin')); app.get(callbackRoute, passport_1.default.authenticate('linkedin', { failureRedirect: failureRedirectURL }), (req, res) => { res.redirect(successRedirectURL); }); passport_1.default.serializeUser((user, done) => done(null, user)); passport_1.default.deserializeUser((obj, done) => done(null, obj)); } setupGithubAuthentication(app, githubConfig) { if (!githubConfig.client_id || !githubConfig.client_secret || !githubConfig.callback_url) { core_1.logger.fatal('Github configuration error. Exiting'); process.exit(1); } app.use(passport_1.default.initialize()); app.use(passport_1.default.session()); passport_1.default.use(new passport_github2_1.Strategy({ clientID: githubConfig.client_id, clientSecret: githubConfig.client_secret, callbackURL: githubConfig.callback_url, scope: ['user:email'] }, (accessToken, refreshToken, profile, done) => { const pro = { "accessToken": accessToken, "refresh": refreshToken, "profile": profile }; done(null, profile); })); const authRoute = githubConfig.auth_route || '/auth/github'; const callbackRoute = githubConfig.callback_route || '/auth/github/callback'; const failureRedirectURL = githubConfig.failure_redirect || '/error'; const successRedirectURL = githubConfig.success_redirect || '/verify/user'; // ************* Authentication routes GITHUB ****************** app.get(authRoute, passport_1.default.authenticate('github', { session: true, scope: ['user:email'] })); app.get(callbackRoute, passport_1.default.authenticate('github', { failureRedirect: failureRedirectURL }), (req, res) => __awaiter(this, void 0, void 0, function* () { res.redirect(successRedirectURL); })); passport_1.default.serializeUser((user, done) => done(null, user)); passport_1.default.deserializeUser((obj, done) => done(null, obj)); } // Setup OpenTelemetry metrics setupMetrics(app) { const { metrics } = this.config; // Validation helper const validateBuckets = (buckets, name) => { if (!buckets) return null; if (!Array.isArray(buckets) || !buckets.every(b => typeof b === 'number' && !isNaN(b))) { throw new Error(`${name} must be an array of numbers`); } return buckets; }; app.use((0, express_prometheus_middleware_1.default)({ metricsPath: (metrics === null || metrics === void 0 ? void 0 : metrics.metricsPath) || '/metrics', collectDefaultMetrics: true, requestDurationBuckets: validateBuckets(metrics === null || metrics === void 0 ? void 0 : metrics.requestDurationBuckets, 'requestDurationBuckets') || metrics_1.default.exponentialBuckets(0.2, 3, 6), requestLengthBuckets: validateBuckets(metrics === null || metrics === void 0 ? void 0 : metrics.requestLengthBuckets, 'requestLengthBuckets') || metrics_1.default.exponentialBuckets(512, 2, 10), responseLengthBuckets: validateBuckets(metrics === null || metrics === void 0 ? void 0 : metrics.responseLengthBuckets, 'responseLengthBuckets') || metrics_1.default.exponentialBuckets(512, 2, 10), // customLabels: ['traceId', 'spanId', 'traceFlags'], // Add custom label // transformLabels: (labels: any, req: any, res: any) => { // const span = trace.getSpan(context.active()); // if (span) { // const ctx = span.spanContext(); // labels.traceId = ctx.traceId; // labels.spanId = ctx.spanId; // labels.traceFlags = ctx.traceFlags.toString(); // usually 1 or 0 // } else { // labels.traceId = 'unknown'; // labels.spanId = 'unknown'; // labels.traceFlags = '0'; // } // } })); } authnHOF(authn) { return (req, res, next) => { var _a, _b, _c, _d; if (authn !== false && (((_a = this.config.authn) === null || _a === void 0 ? void 0 : _a.jwt) || this.config.authn)) { return passport_1.default.authenticate('jwt', { session: false })(req, res, next); } if (authn !== false && (((_b = this.config.authn) === null || _b === void 0 ? void 0 : _b.oauth2) || ((_d = (_c = this.config.authn) === null || _c === void 0 ? void 0 : _c.oauth2) === null || _d === void 0 ? void 0 : _d.google))) { req.user ? next() : res.sendStatus(401); return passport_1.default.authenticate('google', { scope: ['email', 'profile'] }); } else { next(); } }; } ; subscribeToEvent(eventRoute, eventConfig, processEvent, event) { const routeSplit = eventRoute.split('.'); const httpMethod = routeSplit[1]; let endpoint = routeSplit[2].replace(/{(.*?)}/g, ':$1'); let baseUrl = this.config.base_url; let fullUrl; if (baseUrl) { fullUrl = "/" + baseUrl + "/" + endpoint; fullUrl = fullUrl.replace(/\/\//g, '/'); } else { fullUrl = endpoint; } const app = this.client; //@ts-ignore app[httpMethod](fullUrl, this.authnHOF(event.authn), (req, res) => __awaiter(this, void 0, void 0, function* () { var _a; try { const gsEvent = createGSEvent(req, endpoint); const status = yield processEvent(gsEvent, Object.assign({ key: eventRoute }, eventConfig)); if (status.code && ((_a = status.data) === null || _a === void 0 ? void 0 : _a.redirectUrl) && status.code === 302) { res.redirect(302, status.data.redirectUrl); } else { res .status(status.code || 200) // if data is a integer, it takes it as statusCode, so explicitly converting it to string .send(Number.isInteger(status.data) ? String(status.data) : status.data); } } catch (err) { console.error("Unhandled error in API handler:", err); res.status(500).json({ error: "Internal server error during request handling.", details: err instanceof Error ? err.message : err, }); } })); return Promise.resolve(); } } exports.default = EventSource; exports.EventSource = EventSource; // Remove leading and trailing / (slash) if present function trimSlashes(endpoint) { if (endpoint[0] === '/') { endpoint = endpoint.substring(1); } if (endpoint[endpoint.length - 1] === '/') { endpoint = endpoint.substring(0, endpoint.length - 1); } return endpoint; } function createGSEvent(req, endpoint) { const reqProp = omit(req, [ '_readableState', 'socket', 'client', '_parsedUrl', 'res', 'app' ]); const reqHeaders = pick(req, ['headers']); let data = Object.assign(Object.assign({}, reqProp), reqHeaders); const event = new core_1.GSCloudEvent('id', endpoint, new Date(), 'http', '1.0', data, 'REST', new core_1.GSActor('user'), {}); return event; } function pick(o, keys) { let newObj = {}; for (let key of keys) { newObj[key] = o[key]; } return newObj; //return new copy } function omit(o, keys) { o = Object.assign({}, o); //shallow clone for (let key of keys) { delete o[key]; } return o; //return new copy } const SourceType = 'ES'; exports.SourceType = SourceType; const Type = 'express'; // this is the loader file of the plugin, So the final loader file will be `types/${Type.js}` exports.Type = Type; const CONFIG_FILE_NAME = 'http'; // in case of event source, this also works as event identifier, and in case of datasource works as datasource name exports.CONFIG_FILE_NAME = CONFIG_FILE_NAME; const DEFAULT_CONFIG = { port: 3000, docs: { endpoint: '/api-docs' } }; exports.DEFAULT_CONFIG = DEFAULT_CONFIG; //# sourceMappingURL=index.js.map