@godspeedsystems/plugins-express-as-http
Version:
Godspeed event source plugin for express as http server
333 lines • 17.1 kB
JavaScript
;
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