"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.security = void 0;
const types_1 = require("../framework/types");
const defaultSecurityHandler = (req, scopes, schema) => true;
function security(apiDoc, securityHandlers) {
    return async (req, res, next) => {
        var _d, _e, _f;
        // TODO move the following 3 check conditions to a dedicated upstream middleware
        if (!req.openapi) {
            // this path was not found in open api and
            // this path is not defined under an openapi base path
            // skip it
            return next();
        }
        const openapi = req.openapi;
        // use the local security object or fallback to api doc's security or undefined
        const securities = (_d = openapi.schema.security) !== null && _d !== void 0 ? _d : apiDoc.security;
        const path = openapi.openApiRoute;
        if (!path || !Array.isArray(securities) || securities.length === 0) {
            return next();
        }
        const securitySchemes = (_e = apiDoc.components) === null || _e === void 0 ? void 0 : _e.securitySchemes;
        if (!securitySchemes) {
            const message = `security referenced at path ${path}, but not defined in 'components.securitySchemes'`;
            return next(new types_1.InternalServerError({ path: path, message: message }));
        }
        try {
            const results = await new SecuritySchemes(securitySchemes, securityHandlers, securities).executeHandlers(req);
            // TODO handle AND'd and OR'd security
            // This assumes OR only! i.e. at least one security passed authentication
            let firstError = null;
            let success = false;
            function checkErrorWithOr(res) {
                return res.forEach((r) => {
                    if (r.success) {
                        success = true;
                    }
                    else if (!firstError) {
                        firstError = r;
                    }
                });
            }
            function checkErrorsWithAnd(res) {
                let allSuccess = false;
                res.forEach((r) => {
                    if (!r.success) {
                        allSuccess = false;
                        if (!firstError) {
                            firstError = r;
                        }
                    }
                    else if (!firstError) {
                        allSuccess = true;
                    }
                });
                if (allSuccess) {
                    success = true;
                }
            }
            results.forEach((result) => {
                if (Array.isArray(result) && result.length > 1) {
                    checkErrorsWithAnd(result);
                }
                else {
                    checkErrorWithOr(result);
                }
            });
            if (success) {
                next();
            }
            else {
                throw firstError;
            }
        }
        catch (e) {
            const message = ((_f = e === null || e === void 0 ? void 0 : e.error) === null || _f === void 0 ? void 0 : _f.message) || 'unauthorized';
            const err = types_1.HttpError.create({
                status: e.status,
                path: path,
                message: message,
            });
            /*const err =
              e.status == 500
                ? new InternalServerError({ path: path, message: message })
                : e.status == 403
                ? new Forbidden({ path: path, message: message })
                : new Unauthorized({ path: path, message: message });*/
            next(err);
        }
    };
}
exports.security = security;
class SecuritySchemes {
    constructor(securitySchemes, securityHandlers, securities) {
        this.securitySchemes = securitySchemes;
        this.securityHandlers = securityHandlers;
        this.securities = securities;
    }
    async executeHandlers(req) {
        // use a fallback handler if security handlers is not specified
        // This means if security handlers is specified, the user must define
        // all security handlers
        const fallbackHandler = !this.securityHandlers
            ? defaultSecurityHandler
            : null;
        const promises = this.securities.map(async (s) => {
            if (Util.isEmptyObject(s)) {
                // anonymous security
                return [{ success: true }];
            }
            return Promise.all(Object.keys(s).map(async (securityKey) => {
                var _a, _b, _c;
                try {
                    const scheme = this.securitySchemes[securityKey];
                    const handler = (_b =
                        (_a = this.securityHandlers) === null || _a === void 0
                            ? void 0
                            : _a[securityKey]) !== null && _b !== void 0
                        ? _b
                        : fallbackHandler;
                    const scopesTmp = s[securityKey];
                    const scopes = Array.isArray(scopesTmp) ? scopesTmp : [];
                    if (!scheme) {
                        const message = `components.securitySchemes.${securityKey} does not exist`;
                        throw new types_1.InternalServerError({ message });
                    }
                    if (!scheme.hasOwnProperty('type')) {
                        const message = `components.securitySchemes.${securityKey} must have property 'type'`;
                        throw new types_1.InternalServerError({ message });
                    }
                    if (!handler) {
                        const message = `a security handler for '${securityKey}' does not exist`;
                        throw new types_1.InternalServerError({ message });
                    }
                    new AuthValidator(req, scheme, scopes).validate();
                    // expected handler results are:
                    // - throw exception,
                    // - return true,
                    // - return Promise<true>,
                    // - return false,
                    // - return Promise<false>
                    // everything else should be treated as false
                    const securityScheme = scheme;
                    const success = await handler(req, scopes, securityScheme);
                    if (success === true) {
                        return { success };
                    }
                    else {
                        throw Error();
                    }
                }
                catch (e) {
                    return {
                        success: false,
                        status: (_c = e.status) !== null && _c !== void 0 ? _c : 401,
                        error: e,
                    };
                }
            }));
        });
        return Promise.all(promises);
    }
}
class AuthValidator {
    constructor(req, scheme, scopes = []) {
        const openapi = req.openapi;
        this.req = req;
        this.scheme = scheme;
        this.path = openapi.openApiRoute;
        this.scopes = scopes;
    }
    validate() {
        this.validateApiKey();
        this.validateHttp();
        this.validateOauth2();
        this.validateOpenID();
    }
    validateOauth2() {
        const { req, scheme, path } = this;
        if (['oauth2'].includes(scheme.type.toLowerCase())) {
            // TODO oauth2 validation
        }
    }
    validateOpenID() {
        const { req, scheme, path } = this;
        if (['openIdConnect'].includes(scheme.type.toLowerCase())) {
            // TODO openidconnect validation
        }
    }
    validateHttp() {
        const { req, scheme, path } = this;
        if (['http'].includes(scheme.type.toLowerCase())) {
            const authHeader = req.headers['authorization'] &&
                req.headers['authorization'].toLowerCase();
            if (!authHeader) {
                throw Error(`Authorization header required`);
            }
            const type = scheme.scheme && scheme.scheme.toLowerCase();
            if (type === 'bearer' && !authHeader.includes('bearer')) {
                throw Error(`Authorization header with scheme 'Bearer' required`);
            }
            if (type === 'basic' && !authHeader.includes('basic')) {
                throw Error(`Authorization header with scheme 'Basic' required`);
            }
        }
    }
    validateApiKey() {
        var _d;
        const { req, scheme, path } = this;
        if (scheme.type === 'apiKey') {
            if (scheme.in === 'header') {
                if (!req.headers[scheme.name.toLowerCase()]) {
                    throw Error(`'${scheme.name}' header required`);
                }
            }
            else if (scheme.in === 'query') {
                if (!req.query[scheme.name]) {
                    throw Error(`query parameter '${scheme.name}' required`);
                }
            }
            else if (scheme.in === 'cookie') {
                if (!req.cookies[scheme.name] && !((_d = req.signedCookies) === null || _d === void 0 ? void 0 : _d[scheme.name])) {
                    throw Error(`cookie '${scheme.name}' required`);
                }
            }
        }
    }
}
class Util {
    static isEmptyObject(o) {
        return (typeof o === 'object' &&
            Object.entries(o).length === 0 &&
            o.constructor === Object);
    }
}
