var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import AccessTokenService from "../Services/AccessTokenService";
import ProjectService from "../Services/ProjectService";
import TeamMemberService from "../Services/TeamMemberService";
import UserService from "../Services/UserService";
import QueryHelper from "../Types/Database/QueryHelper";
import CookieUtil from "../Utils/Cookie";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import JSONWebToken from "../Utils/JsonWebToken";
import logger, { getLogAttributesFromRequest } from "../Utils/Logger";
import Response from "../Utils/Response";
import ProjectMiddleware from "./ProjectAuthorization";
import SpanUtil from "../Utils/Telemetry/SpanUtil";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
import SsoAuthorizationException from "../../Types/Exception/SsoAuthorizationException";
import TenantNotFoundException from "../../Types/Exception/TenantNotFoundException";
import HashedString from "../../Types/HashedString";
import JSONFunctions from "../../Types/JSONFunctions";
import ObjectID from "../../Types/ObjectID";
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
import { PermissionHelper, } from "../../Types/Permission";
import UserType from "../../Types/UserType";
import UserPermissionUtil from "../Utils/UserPermission/UserPermission";
export default class UserMiddleware {
    /*
     * Description: Checking if user is authorized to access the page and decode jwt to get user data.
     * Params:
     * Param 1: req.headers-> {token}
     * Returns: 401: User is unauthorized since unauthorized token was present.
     */
    static getAccessTokenFromCookie(req) {
        let accessToken = undefined;
        if (CookieUtil.getCookieFromExpressRequest(req, CookieUtil.getUserTokenKey())) {
            accessToken = CookieUtil.getCookieFromExpressRequest(req, CookieUtil.getUserTokenKey());
        }
        return accessToken;
    }
    static getAccessTokenFromExpressRequest(req) {
        // 1. Try cookie (existing web dashboard flow)
        const cookieToken = CookieUtil.getCookieFromExpressRequest(req, CookieUtil.getUserTokenKey());
        if (cookieToken) {
            return cookieToken;
        }
        // 2. Fallback: Check Authorization: Bearer <token> header (mobile app flow)
        const authHeader = req.headers["authorization"];
        if (authHeader && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return undefined;
    }
    static getSsoTokens(req) {
        var _a, _b;
        const ssoTokens = {};
        // get sso tokens from cookies.
        const cookies = CookieUtil.getAllCookies(req);
        for (const key of Object.keys(cookies)) {
            if (key.startsWith(CookieUtil.getSSOKey())) {
                const value = cookies[key];
                let projectId = undefined;
                try {
                    projectId = (_a = JSONWebToken.decode(value).projectId) === null || _a === void 0 ? void 0 : _a.toString();
                }
                catch (err) {
                    logger.error(err, getLogAttributesFromRequest(req));
                    continue;
                }
                if (projectId &&
                    value &&
                    typeof value === "string" &&
                    typeof projectId === "string") {
                    ssoTokens[projectId] = cookies[key];
                }
            }
        }
        /*
         * Also check x-sso-tokens header (mobile app flow).
         * The header value is a JSON-encoded map of { projectId: ssoToken }.
         */
        const ssoTokensHeader = req.headers["x-sso-tokens"];
        if (ssoTokensHeader) {
            try {
                const headerTokens = JSON.parse(ssoTokensHeader);
                for (const projectId of Object.keys(headerTokens)) {
                    const token = headerTokens[projectId];
                    if (!token || typeof token !== "string") {
                        continue;
                    }
                    try {
                        const decoded = JSONWebToken.decode(token);
                        if (((_b = decoded.projectId) === null || _b === void 0 ? void 0 : _b.toString()) === projectId) {
                            ssoTokens[projectId] = token;
                        }
                    }
                    catch (err) {
                        logger.error(err, getLogAttributesFromRequest(req));
                        continue;
                    }
                }
            }
            catch (err) {
                logger.error(err, getLogAttributesFromRequest(req));
            }
        }
        return ssoTokens;
    }
    static doesSsoTokenForProjectExist(req, projectId, userId) {
        var _a;
        const ssoTokens = this.getSsoTokens(req);
        if (ssoTokens && ssoTokens[projectId.toString()]) {
            const decodedData = JSONWebToken.decode(ssoTokens[projectId.toString()]);
            if (((_a = decodedData.projectId) === null || _a === void 0 ? void 0 : _a.toString()) === projectId.toString() &&
                decodedData.userId.toString() === userId.toString()) {
                return true;
            }
        }
        return false;
    }
    static async getUserMiddleware(req, res, next) {
        const tenantId = ProjectMiddleware.getProjectId(req);
        const oneuptimeRequest = req;
        if (tenantId) {
            oneuptimeRequest.tenantId = tenantId;
            /*
             * Fire-and-forget: lastActive write is debounced inside the service
             * (60s in-process cache) and we don't need the result before
             * continuing.
             */
            void ProjectService.updateLastActive(tenantId);
        }
        if (ProjectMiddleware.hasApiKey(req)) {
            return await ProjectMiddleware.isValidProjectIdAndApiKeyMiddleware(req, res, next);
        }
        const accessToken = UserMiddleware.getAccessTokenFromExpressRequest(req);
        if (!accessToken) {
            oneuptimeRequest.userType = UserType.Public;
            return next();
        }
        try {
            oneuptimeRequest.userAuthorization = JSONWebToken.decode(accessToken);
        }
        catch (err) {
            // if the token is invalid or expired, return 401 so clients can refresh the token.
            logger.error(err, getLogAttributesFromRequest(oneuptimeRequest));
            return Response.sendErrorResponse(req, res, new NotAuthenticatedException("AccessToken is invalid or expired. Please refresh your token."));
        }
        if (oneuptimeRequest.userAuthorization.isMasterAdmin) {
            oneuptimeRequest.userType = UserType.MasterAdmin;
        }
        else {
            oneuptimeRequest.userType = UserType.User;
        }
        const userId = oneuptimeRequest.userAuthorization.userId.toString();
        // Tag the current span with user and project context for observability
        SpanUtil.addAttributesToCurrentSpan(Object.assign(Object.assign({ userId: userId, userType: oneuptimeRequest.userType }, (tenantId ? { projectId: tenantId.toString() } : {})), (oneuptimeRequest.requestId
            ? { requestId: oneuptimeRequest.requestId }
            : {})));
        /*
         * Fire-and-forget: lastActive write is debounced inside the service
         * (60s in-process cache) and we don't need the result before continuing.
         */
        void UserService.updateLastActive(new ObjectID(userId));
        /*
         * Resolve global permission, tenant permission, and team membership in
         * parallel. These were previously sequential awaits — each added an
         * extra round-trip latency to every authenticated request. The original
         * code wrapped only the tenant-side calls in try/catch (to convert
         * SsoAuthorizationException etc. into an HTTP error response); we
         * preserve that semantic by routing the rejection through the same
         * catch only when tenantId is present.
         */
        const userGlobalAccessPermissionPromise = AccessTokenService.getUserGlobalAccessPermission(oneuptimeRequest.userAuthorization.userId);
        let userGlobalAccessPermission = null;
        if (tenantId) {
            try {
                const [globalPermission, userTenantAccessPermission, userTeamIds] = await Promise.all([
                    userGlobalAccessPermissionPromise,
                    UserMiddleware.getUserTenantAccessPermissionWithTenantId({
                        req,
                        tenantId,
                        userId: new ObjectID(userId),
                    }),
                    TeamMemberService.getTeamIdsForUser(new ObjectID(userId), tenantId),
                ]);
                userGlobalAccessPermission = globalPermission;
                if (userGlobalAccessPermission) {
                    oneuptimeRequest.userGlobalAccessPermission =
                        userGlobalAccessPermission;
                }
                if (userTenantAccessPermission) {
                    oneuptimeRequest.userTenantAccessPermission = {};
                    oneuptimeRequest.userTenantAccessPermission[tenantId.toString()] =
                        userTenantAccessPermission;
                }
                /*
                 * Load the user's team membership for this tenant so that the
                 * `Owned` permission scope can evaluate team-based ownership without
                 * an extra DB roundtrip on every permission check. Absent for non-user
                 * callers (API keys, Probes); `Owned` then evaluates as `All`.
                 */
                oneuptimeRequest.userTeamIds = userTeamIds;
            }
            catch (error) {
                return Response.sendErrorResponse(req, res, error);
            }
        }
        else {
            userGlobalAccessPermission = await userGlobalAccessPermissionPromise;
            if (userGlobalAccessPermission) {
                oneuptimeRequest.userGlobalAccessPermission =
                    userGlobalAccessPermission;
            }
        }
        if (req.headers["is-multi-tenant-query"]) {
            if (userGlobalAccessPermission &&
                userGlobalAccessPermission.projectIds &&
                userGlobalAccessPermission.projectIds.length > 0) {
                const userTenantAccessPermission = await UserMiddleware.getUserTenantAccessPermissionForMultiTenant(req, new ObjectID(userId), userGlobalAccessPermission.projectIds);
                if (userTenantAccessPermission) {
                    oneuptimeRequest.userTenantAccessPermission =
                        userTenantAccessPermission;
                }
            }
        }
        // set permission hash.
        if (oneuptimeRequest.userGlobalAccessPermission) {
            const serializedValue = JSONFunctions.serialize(oneuptimeRequest.userGlobalAccessPermission);
            const globalValue = JSON.stringify(serializedValue);
            const globalPermissionsHash = await HashedString.hashValue(globalValue, null);
            res.set("global-permissions", globalValue);
            res.set("global-permissions-hash", globalPermissionsHash);
        }
        // set project permissions hash.
        if (oneuptimeRequest.userTenantAccessPermission &&
            tenantId &&
            oneuptimeRequest.userTenantAccessPermission[tenantId.toString()]) {
            const projectValue = JSON.stringify(JSONFunctions.serialize(oneuptimeRequest.userTenantAccessPermission[tenantId.toString()]));
            const projectPermissionsHash = await HashedString.hashValue(projectValue, null);
            if (!(req.headers &&
                req.headers["project-permissions-hash"] &&
                req.headers["project-permissions-hash"] === projectPermissionsHash)) {
                res.set("project-permissions", projectValue);
                res.set("project-permissions-hash", projectPermissionsHash);
            }
        }
        return next();
    }
    static async requireUserAuthentication(req, res, next) {
        const oneuptimeRequest = req;
        if (!oneuptimeRequest.userType ||
            oneuptimeRequest.userType === UserType.Public) {
            return Response.sendErrorResponse(req, res, new NotAuthenticatedException("Authentication required. Please log in to access this resource."));
        }
        return next();
    }
    static requirePermission(data) {
        return async (req, res, next) => {
            var _a;
            const oneuptimeRequest = req;
            // Master admins bypass permission checks
            if (oneuptimeRequest.userType === UserType.MasterAdmin) {
                return next();
            }
            const tenantId = oneuptimeRequest.tenantId;
            if (!tenantId) {
                return Response.sendErrorResponse(req, res, new NotAuthorizedException("Project ID is required to access this resource."));
            }
            const userTenantPermission = (_a = oneuptimeRequest.userTenantAccessPermission) === null || _a === void 0 ? void 0 : _a[tenantId.toString()];
            if (!userTenantPermission) {
                return Response.sendErrorResponse(req, res, new NotAuthorizedException("You do not have permission to access this project."));
            }
            const userPermissions = userTenantPermission.permissions.map((p) => {
                return p.permission;
            });
            if (!PermissionHelper.doesPermissionsIntersect(userPermissions, data.permissions)) {
                return Response.sendErrorResponse(req, res, new NotAuthorizedException("You do not have the required permission to perform this action."));
            }
            return next();
        };
    }
    static async getUserTenantAccessPermissionWithTenantId(data) {
        const { req, tenantId, userId } = data;
        /*
         * Resolve the SSO requirement and the tenant permission in parallel.
         * `getRequireSsoForLogin` is cached in-process for 60s, so this is
         * usually free; the tenant permission lookup is the expensive call.
         */
        const [requireSsoForLogin, tenantPermission] = await Promise.all([
            ProjectService.getRequireSsoForLogin(tenantId).catch((err) => {
                /*
                 * Preserve the original behavior of throwing a TenantNotFoundException
                 * for an unknown project. Any other error re-throws.
                 */
                if (err.message === "Project not found") {
                    throw new TenantNotFoundException("Invalid tenantId");
                }
                throw err;
            }),
            AccessTokenService.getUserTenantAccessPermission(userId, tenantId),
        ]);
        if (requireSsoForLogin &&
            !UserMiddleware.doesSsoTokenForProjectExist(req, tenantId, userId)) {
            throw new SsoAuthorizationException();
        }
        return tenantPermission;
    }
    static async getUserTenantAccessPermissionForMultiTenant(req, userId, projectIds) {
        if (!projectIds.length) {
            return null;
        }
        const projects = await ProjectService.findBy({
            query: {
                _id: QueryHelper.any(projectIds.map((i) => {
                    return i.toString();
                }) || []),
            },
            select: {
                requireSsoForLogin: true,
            },
            limit: LIMIT_PER_PROJECT,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        /*
         * Resolve permissions for every project in parallel. With the previous
         * for-await loop this scaled linearly with project count, adding one
         * round-trip per project even on cache hits.
         */
        const resolved = await Promise.all(projectIds.map(async (projectId) => {
            if (projects.find((p) => {
                return p._id === projectId.toString() && p.requireSsoForLogin;
            }) &&
                !UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId)) {
                return {
                    projectId,
                    permission: UserPermissionUtil.getDefaultUserTenantAccessPermission(projectId),
                };
            }
            return {
                projectId,
                permission: await AccessTokenService.getUserTenantAccessPermission(userId, projectId),
            };
        }));
        let result = null;
        for (const { projectId, permission } of resolved) {
            if (permission) {
                if (!result) {
                    result = {};
                }
                result[projectId.toString()] = permission;
            }
        }
        return result;
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Object)
], UserMiddleware, "getAccessTokenFromCookie", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Object)
], UserMiddleware, "getAccessTokenFromExpressRequest", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Object)
], UserMiddleware, "getSsoTokens", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, ObjectID,
        ObjectID]),
    __metadata("design:returntype", Boolean)
], UserMiddleware, "doesSsoTokenForProjectExist", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object, Function]),
    __metadata("design:returntype", Promise)
], UserMiddleware, "getUserMiddleware", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object, Function]),
    __metadata("design:returntype", Promise)
], UserMiddleware, "requireUserAuthentication", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], UserMiddleware, "getUserTenantAccessPermissionWithTenantId", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, ObjectID, Array]),
    __metadata("design:returntype", Promise)
], UserMiddleware, "getUserTenantAccessPermissionForMultiTenant", null);
//# sourceMappingURL=UserAuthorization.js.map