"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cms = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const csurf_1 = __importDefault(require("csurf"));
const crypto_1 = __importDefault(require("crypto"));
const mustache_1 = __importDefault(require("mustache"));
const passport_1 = __importDefault(require("passport"));
const bcryptjs_1 = __importDefault(require("bcryptjs"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const passport_local_1 = __importDefault(require("passport-local"));
const express_async_handler_1 = __importDefault(require("express-async-handler"));
const express_1 = require("express");
const express_response_formatter_1 = require("express-response-formatter");
const express_session_1 = __importDefault(require("express-session"));
const express_session_mikro_orm_1 = __importStar(require("express-session-mikro-orm"));
const common_1 = require("@tensei/common");
const routes_1 = __importDefault(require("./routes"));
const setup_1 = require("./setup");
const change_password_1 = require("./routes/change-password");
const update_profile_1 = require("./routes/update-profile");
const indexFileContent = fs_1.default.readFileSync(path_1.default.resolve(__dirname, 'template', 'index.mustache')).toString();
const baseScripts = ['main.js', 'vendor.js', 'manifest.js'];
class CmsPlugin {
    constructor() {
        this.scripts = [
            {
                name: 'manifest.js',
                path: path_1.default.resolve(__dirname, 'public', 'manifest.js')
            },
            {
                name: 'vendor.js',
                path: path_1.default.resolve(__dirname, 'public', 'vendor.js')
            },
            {
                name: 'main.js',
                path: path_1.default.resolve(__dirname, 'public', 'main.js')
            }
        ];
        this.config = {
            path: 'cms',
            apiPath: 'cms/api',
            setup: () => { },
            cookieOptions: {},
            dashboards: [],
            userResource: 'Admin User',
            permissionResource: 'Admin Permission',
            roleResource: 'Admin Role',
            tokenResource: 'Admin Token'
        };
        this.router = (0, express_1.Router)();
        this.styles = [];
        this.getApiPath = (path) => {
            return `/api/${path}`;
        };
        this.resources = {
            user: this.userResource(),
            role: this.roleResource(),
            token: this.tokenResource(),
            permission: this.permissionResource()
        };
        this.routes = () => [
            (0, common_1.route)('Get CMS Csrf Token')
                .get()
                .path(this.getApiPath('csrf'))
                .handle((request, response) => {
                response.cookie('x-csrf-token', request.csrfToken());
                return response.status(204).json();
            }),
            change_password_1.changePasswordRoute.path(this.getApiPath('auth/change-password')),
            update_profile_1.updateProfileRoute.path(this.getApiPath('auth/update-profile')),
            (0, common_1.route)('Logout')
                .path(this.getApiPath('auth/logout'))
                .id('logout')
                .post()
                .handle(async (request, response) => {
                request.session.destroy(error => {
                    if (error) {
                        return response.status(204).json();
                    }
                    response.clearCookie('connect.sid');
                    return response.status(204).json();
                });
            })
        ];
        this.loginPassport = async (request, done) => {
            const { config, manager, body } = request;
            const { indicative } = config;
            try {
                const { email, password } = await indicative.validator.validate(body, {
                    email: 'required|email'
                });
                let user = await manager.findOne(this.resources.user.data.pascalCaseName, {
                    email,
                    active: true
                });
                if (!user) {
                    return done([
                        {
                            field: 'email',
                            message: 'These credentials do not match our records.'
                        }
                    ], null);
                }
                if (!bcryptjs_1.default.compareSync(password, user.password)) {
                    return done([
                        {
                            field: 'email',
                            message: 'These credentials do not match our records.'
                        }
                    ], null);
                }
                return done(null, user);
            }
            catch (errors) {
                return done(errors, null);
            }
        };
        this.registerPassport = async (request, done) => {
            var _a, _b;
            const { config, manager, body, resources, repositories } = request;
            const { emitter } = config;
            let invitedUser = null;
            if (body.inviteCode) {
                invitedUser = await manager.findOne(this.resources.user.data.pascalCaseName, {
                    inviteCode: body.inviteCode
                });
            }
            if (invitedUser) {
                manager.assign(invitedUser, {
                    active: true,
                    inviteCode: null,
                    password: body.password,
                    firstName: (_a = body.firstName) !== null && _a !== void 0 ? _a : invitedUser.firstName,
                    lastName: (_b = body.lastName) !== null && _b !== void 0 ? _b : invitedUser.lastName
                });
                await manager.persistAndFlush(invitedUser);
                emitter.emit('ADMIN_REGISTERED', invitedUser);
                return done(null, invitedUser);
            }
            const adminCount = await manager.count(this.resources.user.data.pascalCaseName, {});
            if (adminCount !== 0) {
                return done([
                    {
                        message: 'An administrator already exists. Please join the team by requesting an invitation.',
                        field: 'email'
                    }
                ], null);
            }
            const validator = common_1.Utils.validator(this.userResource(), manager, resources);
            const [success, payload] = await validator.validate(body);
            if (!success) {
                return done(payload, null);
            }
            let superAdminRole = await repositories.adminRoles().findOne({
                slug: 'super-admin'
            });
            if (!superAdminRole) {
                return done([
                    {
                        message: 'The Super Admin Role must exist before you create an administrator account.',
                        field: 'role'
                    }
                ], null);
            }
            let createUserPayload = Object.assign(Object.assign({}, payload), { active: true, adminRoles: [superAdminRole] });
            const admin = manager.create(this.resources.user.data.pascalCaseName, createUserPayload);
            await manager.persistAndFlush(admin);
            emitter.emit('ADMIN_REGISTERED', admin);
            return done(null, admin);
        };
        this.sessionMikroOrmOptions = {
            entityName: `${this.resources.user.data.pascalCaseName}Session`,
            tableName: `${this.resources.user.data.camelCaseNamePlural}_sessions`,
            collection: `${this.resources.user.data.camelCaseNamePlural}_sessions`
        };
        this.authorizeResolver = async (ctx, query) => {
            const authorized = await Promise.all(query.config.authorize.map(fn => fn(ctx, ctx.entity)));
            if (authorized.filter(result => result).length !==
                query.config.authorize.length) {
                throw ctx.forbiddenError('Unauthorized.');
            }
        };
    }
    cookies(cookieOptions) {
        this.config.cookieOptions = cookieOptions;
        return this;
    }
    path(path) {
        this.config.path = path;
        this.config.apiPath = `/${path}/api`;
        return this;
    }
    generateRandomToken(length = 32) {
        return crypto_1.default.randomBytes(length).toString('hex');
    }
    permissionResource() {
        return (0, common_1.resource)(this.config.permissionResource)
            .fields([
            (0, common_1.text)('Name').searchable().rules('required'),
            (0, common_1.text)('Slug').rules('required').unique().searchable().rules('required'),
            (0, common_1.belongsToMany)(this.config.roleResource)
        ])
            .displayField('Name')
            .hideOnApi()
            .hideFromNavigation();
    }
    tokenResource() {
        return (0, common_1.resource)(this.config.tokenResource)
            .fields([
            (0, common_1.select)('Type').options([
                {
                    label: 'Invite',
                    value: 'INVITATION'
                }
            ]),
            (0, common_1.text)('Token'),
            (0, common_1.dateTime)('Expires At').nullable(),
            (0, common_1.belongsTo)(this.config.userResource)
        ])
            .hideFromNavigation()
            .hideOnApi();
    }
    roleResource() {
        return (0, common_1.resource)(this.config.roleResource)
            .fields([
            (0, common_1.text)('Name')
                .rules('required')
                .unique()
                .searchable()
                .sortable()
                .rules('required'),
            (0, common_1.text)('Slug')
                .rules('required', 'unique:slug')
                .unique()
                .sortable()
                .searchable()
                .rules('required'),
            (0, common_1.text)('Description').nullable().rules('max:255'),
            (0, common_1.belongsToMany)(this.config.userResource),
            (0, common_1.belongsToMany)(this.config.permissionResource).owner()
        ])
            .hideOnApi()
            .displayField('Name')
            .hideFromNavigation();
    }
    userResource() {
        return (0, common_1.resource)(this.config.userResource)
            .fields([
            (0, common_1.text)('First name')
                .searchable()
                .nullable()
                .sortable()
                .creationRules('required'),
            (0, common_1.text)('Last name')
                .searchable()
                .nullable()
                .sortable()
                .creationRules('required'),
            (0, common_1.text)('Password')
                .rules('min:12')
                .requiredOnCreate()
                .nullable()
                .hideOnFetchApi(),
            (0, common_1.text)('Email')
                .unique()
                .searchable()
                .sortable()
                .notNullable()
                .creationRules('required', 'email', 'unique:email')
                .updateRules('unique:email'),
            (0, common_1.boolean)('Active')
                .nullable()
                .sortable()
                .defaultFormValue(true)
                .default(true)
                .rules('boolean'),
            (0, common_1.text)('Invite code').nullable(),
            (0, common_1.belongsToMany)(this.config.roleResource).rules('array')
        ])
            .displayField('First name')
            .secondaryDisplayField('Email')
            .hideOnApi()
            .hideFromNavigation()
            .beforeCreate(({ entity, em }) => {
            const payload = {
                password: entity.password
                    ? bcryptjs_1.default.hashSync(entity.password)
                    : undefined
            };
            em.assign(entity, payload);
        })
            .beforeUpdate(async ({ entity, em, changeSet }) => {
            if (changeSet === null || changeSet === void 0 ? void 0 : changeSet.payload.password) {
                em.assign(entity, {
                    password: bcryptjs_1.default.hashSync(changeSet.payload.password)
                });
            }
        });
    }
    dashboards(dashboards) {
        this.config.dashboards = [...this.config.dashboards, ...dashboards];
        return this;
    }
    plugin() {
        return (0, common_1.plugin)('CMS')
            .id('cms')
            .extra({
            path: this.config.path
        })
            .register(({ script, style, extendResources, databaseConfig }) => {
            this.scripts.forEach(s => script(s.name, s.path, s.chunk));
            this.styles.forEach(s => style(s.name, s.path));
            databaseConfig.entities = [
                ...(databaseConfig.entities || []),
                (0, express_session_mikro_orm_1.generateSessionEntity)(this.sessionMikroOrmOptions)
            ];
            extendResources([
                this.resources.user,
                this.resources.role,
                this.resources.token,
                this.resources.permission
            ]);
        })
            .boot(async (config) => {
            const { app, orm, resources, currentCtx } = config;
            const Store = (0, express_session_mikro_orm_1.default)(express_session_1.default, this.sessionMikroOrmOptions);
            await (0, setup_1.setupCms)(config, [this.resources.role, this.resources.permission]);
            this.router.use((0, cookie_parser_1.default)());
            this.router.use((0, express_session_1.default)({
                resave: false,
                saveUninitialized: false,
                store: new Store({ orm }),
                cookie: this.config.cookieOptions,
                secret: process.env.SESSION_SECRET || '__sessions__secret__'
            }));
            this.router.use(passport_1.default.initialize());
            this.router.use(passport_1.default.session());
            const self = this;
            passport_1.default.use('local-register', new passport_local_1.default.Strategy({
                usernameField: 'email',
                passwordField: 'password',
                passReqToCallback: true
            }, async (request, email, password, done) => {
                await self.registerPassport(request, done);
            }));
            passport_1.default.use('local-login', new passport_local_1.default.Strategy({
                usernameField: 'email',
                passwordField: 'password',
                passReqToCallback: true
            }, async (request, email, password, done) => {
                await this.loginPassport(request, done);
            }));
            passport_1.default.serializeUser((user, done) => {
                done(null, {
                    id: user.id
                });
            });
            passport_1.default.deserializeUser(async (request, id, done) => {
                const user = await request.manager.findOne(this.resources.user.data.pascalCaseName, {
                    id: id.id
                }, {
                    populate: ['adminRoles.adminPermissions']
                });
                done(null, user);
            });
            this.router.post(`${this.getApiPath('auth/register')}`, (request, response, next) => {
                passport_1.default.authenticate('local-register', {
                    successRedirect: `${this.getApiPath('')}`,
                    failureRedirect: `${this.getApiPath('auth/register')}`
                }, async (error, user) => {
                    if (user === false && !error) {
                        // This is a unique case, where the user did not provide the email or password.
                        // Passport is weird, so they do not even send the request to the controller when this happens.
                        // In this scenario, we'll perform validation here on the data, and send the correct validation errors back to the frontend.
                        const validator = common_1.Utils.validator(self.userResource(), request.manager, request.resources);
                        const [, payload] = await validator.validate(request.body);
                        return response.status(422).json({
                            errors: payload
                        });
                    }
                    if (error) {
                        return response.status(422).json({
                            errors: error
                        });
                    }
                    request.logIn(user, error => {
                        if (error) {
                            return next(error);
                        }
                        return response.status(204).json([]);
                    });
                })(request, response, next);
            });
            this.router.post(`${this.getApiPath('auth/login')}`, (request, response, next) => {
                passport_1.default.authenticate('local-login', {}, (error, user, info) => {
                    if (user === false) {
                        return response.status(422).json({
                            errors: [
                                {
                                    message: 'Please provide your email.',
                                    field: 'email'
                                },
                                {
                                    message: 'Please provide your password.',
                                    field: 'password'
                                }
                            ]
                        });
                    }
                    if (error || !user) {
                        return response.status(400).json({
                            errors: error
                        });
                    }
                    request.logIn(user, error => {
                        if (error) {
                            return next(error);
                        }
                        return response.status(204).json([]);
                    });
                })(request, response, next);
            });
            this.router.use((0, express_response_formatter_1.responseEnhancer)());
            this.router.use((request, response, next) => {
                // set filter parameters
                resources.forEach(resource => {
                    resource.data.filters.forEach(filter => {
                        var _a;
                        const filterFromBody = (_a = request.body.filters) === null || _a === void 0 ? void 0 : _a.find((bodyFitler) => bodyFitler.name === filter.config.shortName);
                        request.manager.setFilterParams(filter.config.shortName, (filterFromBody === null || filterFromBody === void 0 ? void 0 : filterFromBody.args) || {});
                    });
                });
                next();
            });
            this.router.use((0, csurf_1.default)());
            [...(0, routes_1.default)(config, this.config), ...this.routes()].forEach(route => {
                const path = route.config.path.startsWith('/')
                    ? route.config.path
                    : `/${route.config.path}`;
                this.router[route.config.type.toLowerCase()](path, ...route.config.middleware.map(fn => (0, express_async_handler_1.default)(fn)), (0, express_async_handler_1.default)(async (request, response, next) => {
                    await this.authorizeResolver(request, route);
                    return next();
                }), (0, express_async_handler_1.default)(async (request, response) => route.config.handler(request, response)));
            });
            app.use(`/${this.config.path}`, this.router);
            app.get(`/${this.config.path}(/*)?`, async (request, response) => {
                const sortedScripts = request.scripts
                    .filter(script => !script.chunk) // Only non-chunk scripts will be mounted. The chunked scripts will be fetched by webpack during load time.
                    .sort(script => (baseScripts.includes(script.name) ? -1 : 0));
                response.send(mustache_1.default.render(indexFileContent, {
                    styles: request.styles,
                    scripts: sortedScripts,
                    // @ts-ignore
                    user: request.user
                        ? JSON.stringify(Object.assign({}, request.user))
                        : null,
                    resources: JSON.stringify(request.config.resources.map(r => r.serialize())),
                    ctx: JSON.stringify({
                        name: request.config.name,
                        dashboardPath: this.config.path,
                        apiPath: `/${this.config.path}/api`,
                        serverUrl: request.config.serverUrl,
                        pluginsConfig: currentCtx().pluginsConfig
                    }),
                    shouldShowRegistrationScreen: (await request.manager.count(this.resources.user.data.pascalCaseName)) === 0
                }));
            });
            app.use('/tensei-assets', (0, express_1.static)(path_1.default.resolve(__dirname, '..', 'default-assets')));
        });
    }
}
const cms = () => new CmsPlugin();
exports.cms = cms;
