import expressLib, { Request, Response } from "express"; import path from "path"; import { getDatabase, createOptions } from "@vostro/c2-utils/lib/database"; import setup from "./setup"; // import {promisify} from "util"; import passport from "passport"; import models from "./models"; import merge from "@vostro/c2-utils/lib/merge"; import logger from "@vostro/c2-utils/lib/logger"; import waterfall from "@vostro/c2-utils/lib/waterfall"; import {Strategy as BearerStrategy} from "passport-http-bearer"; import { toGlobalId, fromGlobalId } from "graphql-relay"; import { getCache, initialiseCache } from "@vostro/c2-utils/lib/redis-cache"; import {getContextFromOptions} from "@vostro/c2-utils/lib/auth"; import {C2Engine} from "@vostro/c2-engine/lib/types"; import {C2Utils} from "@vostro/c2-utils/lib/types"; import { Op } from "sequelize"; import { createContext, createServiceContext } from "./context"; import { createMigrator } from "./migrate"; const log = logger("index:", "", module); export const name = "core"; export const version = "1.0.0"; export const dependencies = []; let bearerProviders = ["bearer"]; export function registerBearerProvider(name: string) { bearerProviders = bearerProviders.concat([name]); } const _importDynamic = new Function('modulePath', 'return import(modulePath)'); //** Functions are laid out in order of execution */ let i = 0; export async function configure(settings: C2Engine.ApplicationSettings, appContext: C2Engine.CoreContext, cfg: C2Engine.Config) { const config = cfg.moduleConfig.items || {}; await initialiseCache(config.cache); (settings.database.models as any) = merge(settings.database.models, models); return settings; } let passportInitialize: any; let passportSession: any; let roleProcessor: any; export async function initialise(appContext: C2Engine.CoreContext, settings: C2Engine.ApplicationSettings, config: C2Engine.Config) { if (appContext.engine.workerType === "http") { const {express: app, socketio} = appContext.engine; if(!app) { throw "Express not available for the http thread"; } if (config.socketio?.enabled) { if (!socketio) { throw "SocketIO not available for the http thread"; } if(config.socketio?.jwtSecret) { const {authorize} = await _importDynamic("@thream/socketio-jwt"); socketio.use(authorize({ secret: Buffer.from(config.socketio.jwtSecret) .toString("base64"), })); } } passport.serializeUser(function(user: any, done) { done(null, user.id); }); passport.deserializeUser(async(id: string, done) => { try { const db = await getDatabase(); const {User, Role} = db.models; // const transaction = await db.transaction(); const user = await User.findOne(createOptions({ // transaction }, { where: { id: { [Op.eq]: id } }, include: [{ model: Role, as: "role", }], override: true, })); // await transaction.commit(); return done(undefined, user); } catch (err) { return done(err, undefined); } }); passport.use(new BearerStrategy(async(token, done) => { try { const db = await getDatabase(); const {UserAuth, Role} = db.models; const userAuth = await UserAuth.findOne({ where: { token, type: "token", }, override: true, }); if (userAuth) { const user = await userAuth.getUser({ override: true, include: [{ model: Role, as: "role", }], }); return done(null, user, {scope: "all"}); } return done(null, false); } catch (err) { return done(err); } })); const webcfg = config.moduleConfig?.web || {}; if (webcfg.static) { app.use(expressLib.static(path.resolve(webcfg.appPath, webcfg.static))); } passportInitialize = passport.initialize(); passportSession = passport.session(); roleProcessor = async(req: Request, res: Response, next: () => {}) => { (req as any).loginAsync = (user: any) => { return new Promise((resolve, reject) => { try { return req.logIn(user, async(err) => { if (err) { return reject(err); } await assignUserToRequest(req, user); return resolve(user); // return req.session.save(() => { // }); }); } catch (err) { return reject(err); } }); }; // inject easier to use hook for logging in if (req.user) { const {user} = req; await assignUserToRequest(req, user); } else { i++; // console.log("req.i", i); (req as any).getUser = () => { // console.log("req.i", req.i); return undefined; }; (req as any).getRole = () => "public"; (req as any).role = "public"; } return next(); }; async function bearerAuth(req: Request, res: Response, next: () => {}) { if (req.headers.Authorization || req.headers.authorization) { await waterfall(bearerProviders || ["bearer"], async(bearerProvider: any, success: any) => { try { const response = await authenticateAsync(bearerProvider, req, res) as any; if (response?.user) { await assignUserToRequest(req, response.user); return true; } return success; } catch (err) { log.error(`${bearerProvider} failed`, err); } return false; }); } return next(); } app.use(passportInitialize); app.use(passportSession); app.use(roleProcessor); app.get("/auth.api/logout", (req, res, next) => { req.logout(); res.redirect("/login"); }); app.use(bearerAuth as any); (appContext as any).processAuthRequest = async function processRequest(req: Request, res: any) { await waterfall([passportInitialize, passportSession, roleProcessor, bearerAuth], (f) => { return new Promise((resolve, reject) => { try { return f(req, res, resolve); } catch(err) { return reject(err); } }) }); } if (settings.routes) { Object.keys(settings.routes).forEach((r) => { if(settings.routes) { const routes = settings.routes[r] if (Array.isArray(routes)) { routes.forEach((f: any) => { (app as any)[r].apply(app, [f]); }); } else { Object.keys(routes).forEach((k) => { routes[k].forEach((f: any) => { (app as any)[r].apply(app, [k, f]); }); }); } } }); } } return appContext; } async function assignUserToRequest(req: Request, user: any) { req.user = user; const role = await user.getRole({override: true}); (req as any).role = role; (req as any).getUser = () => { return user; }; (req as any).getRole = () => role; return; } async function createEventLog(action: string) { const db = await getDatabase(); const {EventLog} = db.models; return async(model: any, options: C2Utils.FindOptions) => { const {name} = model.constructor; if (name !== "EventLog" || options.disableEventLog) { const context = getContextFromOptions(options) || {}; if(!context.disableEventLog) { let source = "", userId, changeset = {} as any; if (context.override || options.override) { source = "System"; } if (context?.getUser) { const user = await context.getUser(); if (user) { userId = user.id; source = "User"; } } changeset = { current: model.dataValues, }; if (action === "UPDATE") { changeset.previous = model._previousDataValues } await EventLog.create({ action: action, description: `${action} - ${name}`, source, userId, changeset, model: name, }, createOptions(context, {override: true})); } } return model; }; } export async function beforeDatabaseLoaded(appContext: C2Engine.CoreContext, settings: C2Engine.ApplicationSettings, config: C2Engine.Config) { if (appContext.engine.workerType === "setup") { try { const migrator = await createMigrator(settings, appContext, config, { fake: config.database.sync?.force || config.migrator?.fake, path: config.migrator?.path || path.resolve(process.cwd(), "./migrations"), }); await migrator.up(); } catch(err) { log.err("migrator", err); // debugger; return process.exit(66); } } return appContext; } // database is initialised and loaded export async function databaseLoaded(appContext: C2Engine.CoreContext, settings: C2Engine.ApplicationSettings, config: C2Engine.Config) { const db = await getDatabase(); if (!config.database.disableEventLog) { db.addHook("afterCreate", await createEventLog("CREATE")); db.addHook("afterUpdate", await createEventLog("UPDATE")); db.addHook("beforeDestroy", await createEventLog("DELETE")); } if (appContext.engine.workerType === "setup") { await setup(settings, appContext); } if (appContext.engine.workerType === "http") { return Object.assign(appContext, { "auth": { passportInitialize, passportSession, roleProcessor, }, }); } return appContext; } // http port is now listening export async function finished(appContext: C2Engine.CoreContext, settings: C2Engine.ApplicationSettings, cfg: C2Engine.Config) { const config = cfg.moduleConfig?.items || {}; if (config.enableCache) { await getCache().enableCache(); } if (cfg.socketio?.enabled) { appContext.engine.socketio?.on("connection", async(socket :any) => { let ctx: C2Utils.DataContext | undefined; const db = await getDatabase(); const {User, Role} = db.models; if (config.socketio?.jwtSecret){ if(!(socket as any).decodedToken?.userId) { return; } const userGId = fromGlobalId((socket as any).decodedToken.userId); const user = await User.findOne({ where: { id: { [Op.eq]: userGId.id, }, }, override: true, }); if (user) { ctx = await createContext({ getUser() { return user; }, } as any, {} as any); } } if(!ctx) { const role = await Role.findOne({ where: { name: { [Op.eq]: "public", }, }, override: true, }); ctx = await createServiceContext(role); } socket.context = ctx; if (appContext.engine.execute) { await appContext.engine.execute("oniosocket", true, socket, ctx, appContext, settings, cfg); } // await Promise.all((settings.socketio || []).map((func) => { // if(!ctx) { // throw "Error context is missing?"; // } // return func(socket, ctx); // })); }); } return appContext; } export function authenticateAsync(strategy: string | passport.Strategy | string[], req: Request, res: Response) { return new Promise((resolve, reject) => { try { return passport.authenticate(strategy, {session: true}, (err, user, info) => { if (!err) { return resolve({user, info}); } return reject(err); })(req, res, (err: any) => { if (err) { return reject(err); } return reject(new Error("Unknown Error - next was called")); }); } catch (err) { return reject(err); } }); }