UNPKG

@godspeedsystems/plugins-express-as-http

Version:

Godspeed event source plugin for express as http server

382 lines 19 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: (jwtConfig === null || jwtConfig === void 0 ? void 0 : jwtConfig.ignoreExpiration) || false, 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, scope: googleConfig.scope || ["email", "profile"], passReqToCallback: true, // Enable this option }, (req, accessToken, refreshToken, params, profile, done) => { var _a; const stateParam = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state; const state = this.parseJsonIfPossible(stateParam ? decodeURIComponent(stateParam.toString()) : null); return done(null, Object.assign(Object.assign({}, profile), { $state: state })); })); 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, (req, ...args) => { var _a; const queryState = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state; const state = queryState ? encodeURIComponent(queryState.toString()) : ""; passport_1.default.authenticate("google", { state })(req, ...args); }); app.get(callbackRoute, passport_1.default.authenticate("google", { failureRedirect: failureRedirectURL, }), (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"], passReqToCallback: true, }, (req, accessToken, refreshToken, profile, done) => { var _a; const stateParam = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state; const state = this.parseJsonIfPossible(stateParam ? decodeURIComponent(stateParam.toString()) : null); return done(null, Object.assign(Object.assign({}, profile), { $state: state })); })); 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, (req, ...args) => { var _a; const queryState = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state; const state = queryState ? encodeURIComponent(queryState.toString()) : ""; passport_1.default.authenticate("linkedin", { state })(req, ...args); }); 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: githubConfig.scope || ["user:email"], passReqToCallback: true, }, (req, accessToken, refreshToken, result, profile, done) => { var _a; const stateParam = (_a = req.query) === null || _a === void 0 ? void 0 : _a.state; const state = this.parseJsonIfPossible(stateParam ? decodeURIComponent(stateParam.toString()) : null); return done(null, Object.assign(Object.assign({}, profile), { $state: state })); })); 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 || (authn === false && req.headers.authorization)) && (((_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 === null || event === void 0 ? void 0 : 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(); } parseJsonIfPossible(value) { if (!value) return null; const decoded = value; try { const parsed = JSON.parse(decoded); // Only return parsed if it's an object, array, number, boolean, or null if (typeof parsed === "object" || typeof parsed === "number" || typeof parsed === "boolean") { return parsed; } // Otherwise, fallback to string return decoded; } catch (_a) { // Not valid JSON, return as plain string return decoded; } } } 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