/**
 * Authentication utilities for Baasix
 * New implementation using the adapter-based auth module
 * Provides backward compatibility with existing exports
 */
import jwt from "jsonwebtoken";
import crypto from "crypto";
import env from "./env.js";
import { APIError } from "./errorHandler.js";
import { getSqlClient } from "./db.js";
import { db } from "./db.js";
import { getCache } from "./cache.js";
import { eq, and } from "drizzle-orm";
import { schemaManager } from "./schemaManager.js";
import { permissionService } from '../services/PermissionService.js';
import { validateSecurityPostureAtStartup } from './securityPosture.js';
import fieldUtils from "./fieldUtils.js";
import { setContextAccountability } from './requestContext.js';
/**
 * The only JWT algorithm Baasix issues and accepts. Pinning this on every
 * jwt.verify() prevents algorithm-confusion attacks (e.g. `alg:none`, or forging
 * an HS256 token using a public key if the secret were ever asymmetric).
 */
export const JWT_ALGORITHM = 'HS256';
/** Recommended minimum length for the JWT signing secret. */
export const MIN_SECRET_LENGTH = 32;
/**
 * Get the configured JWT secret.
 *
 * Hard requirement: the secret must be present. A missing/empty secret makes every
 * token trivially forgeable, so we always refuse to sign/verify without one.
 *
 * Secret *strength* (length) is NOT enforced here — that is only a recommendation
 * and is surfaced once at startup via validateAuthSecretAtStartup(). This keeps an
 * existing deployment that runs a shorter key working (rather than crashing on the
 * next restart), while still nudging operators to rotate to a stronger secret.
 */
export function getJwtSecret() {
    const secret = env.get('SECRET_KEY');
    if (!secret) {
        throw new Error('SECRET_KEY is not configured. Refusing to sign/verify JWTs without a secret.');
    }
    return secret;
}
/**
 * One-time startup validation of the JWT secret. Call this once during server
 * boot (NOT per request). Logs a prominent warning when the secret is shorter than
 * the recommended length so operators can plan a rotation, without breaking a
 * running production system.
 */
export function validateAuthSecretAtStartup() {
    // The SECRET_KEY checks (fatal if missing, advisory if short) are now part of the
    // consolidated startup security posture. Delegated here so existing callers/imports
    // keep working; new code can call validateSecurityPostureAtStartup directly.
    validateSecurityPostureAtStartup();
}
/**
 * Lazy getter for ItemsService to avoid circular dependency
 */
let _ItemsService = null;
async function getItemsService() {
    if (!_ItemsService) {
        const module = await import('../services/ItemsService.js');
        _ItemsService = module.default || module.ItemsService;
    }
    return _ItemsService;
}
// Lazy getter for SettingsService
let _SettingsService = null;
async function getSettingsService() {
    if (!_SettingsService) {
        const module = await import('../services/SettingsService.js');
        _SettingsService = module.default;
    }
    return _SettingsService;
}
/**
 * Helper function to get and cache roles and permissions
 * Uses PermissionService for role data (cached in memory) and Redis cache for permissions
 */
export async function getRolesAndPermissions(roleId) {
    const cache = getCache();
    const cacheKey = `auth:role:${roleId}:permissions`;
    // Check cache first
    const cachedRole = await cache.get(cacheKey);
    if (cachedRole) {
        return cachedRole;
    }
    // Get role from PermissionService (hybrid cache), fall back to DB if not cached
    let role = await permissionService.getRoleByIdAsync(roleId);
    if (!role) {
        // Role not in hybrid cache (e.g., freshly created) — query DB directly
        const sqlClient = getSqlClient();
        const roles = await sqlClient `
      SELECT id, name, description, "isTenantSpecific"
      FROM "baasix_Role"
      WHERE id = ${roleId}
      LIMIT 1
    `;
        role = roles[0] || null;
    }
    if (!role) {
        throw new Error(`Role with id ${roleId} not found`);
    }
    // Fetch permissions for the role from database
    const sql = getSqlClient();
    const permissions = await sql `
    SELECT id, collection, action, fields, conditions
    FROM "baasix_Permission"
    WHERE "role_Id" = ${roleId}
  `;
    // Transform permissions to object format
    const permissionsObj = permissions.reduce((acc, perm) => {
        const key = `${perm.collection}_${perm.action}`;
        acc[key] = {
            action: perm.action,
            collection: perm.collection,
            fields: perm.fields,
            conditions: perm.conditions,
        };
        return acc;
    }, {});
    const roleData = {
        id: role.id,
        name: role.name,
        description: role.description,
        isTenantSpecific: role.isTenantSpecific,
        permissions: permissionsObj,
    };
    // Cache role and permissions with infinite TTL
    await cache.set(cacheKey, roleData, -1);
    return roleData;
}
/**
 * Verify JWT token and return decoded payload
 */
export function verifyJWT(token) {
    const secret = getJwtSecret();
    try {
        return jwt.verify(token, secret, { algorithms: [JWT_ALGORITHM] });
    }
    catch (error) {
        throw new Error(`Invalid token: ${error.message}`);
    }
}
/**
 * Get user with roles, permissions, and tenant information
 */
export async function getUserRolesPermissionsAndTenant(userId, tenantId = null) {
    try {
        const sql = getSqlClient();
        // First, get the user's role assignment
        let userRoles;
        if (tenantId) {
            userRoles = await sql `
        SELECT
          ur.id as "userRoleId",
          ur."user_Id",
          ur."role_Id",
          ur."tenant_Id"
        FROM "baasix_UserRole" ur
        WHERE ur."user_Id" = ${userId} AND ur."tenant_Id" = ${tenantId}
        LIMIT 1
      `;
        }
        else {
            userRoles = await sql `
        SELECT
          ur.id as "userRoleId",
          ur."user_Id",
          ur."role_Id",
          ur."tenant_Id"
        FROM "baasix_UserRole" ur
        WHERE ur."user_Id" = ${userId}
        LIMIT 1
      `;
        }
        if (userRoles.length === 0) {
            throw new Error("User role not found");
        }
        const userRole = userRoles[0];
        // Get role from PermissionService hybrid cache
        let role = await permissionService.getRoleByIdAsync(userRole.role_Id);
        // Fallback to database if not in cache
        if (!role) {
            const roles = await sql `
        SELECT id, name, description, "isTenantSpecific"
        FROM "baasix_Role"
        WHERE id = ${userRole.role_Id}
        LIMIT 1
      `;
            if (roles.length > 0) {
                role = {
                    id: roles[0].id,
                    name: roles[0].name,
                    description: roles[0].description,
                    isTenantSpecific: roles[0].isTenantSpecific,
                };
            }
        }
        if (!role) {
            throw new Error(`Role with id ${userRole.role_Id} not found`);
        }
        // Fetch permissions for the role
        const permissions = await sql `
      SELECT
        id,
        collection,
        action,
        fields,
        conditions
      FROM "baasix_Permission"
      WHERE "role_Id" = ${userRole.role_Id}
    `;
        // Transform permissions to object format
        const permissionsObj = permissions.reduce((acc, perm) => {
            const key = `${perm.collection}_${perm.action}`;
            acc[key] = {
                action: perm.action,
                collection: perm.collection,
                fields: perm.fields,
                conditions: perm.conditions,
            };
            return acc;
        }, {});
        // Fetch tenant info if applicable
        let tenant = null;
        if (userRole.tenant_Id) {
            const tenants = await sql `
        SELECT * FROM "baasix_Tenant"
        WHERE id = ${userRole.tenant_Id}
        LIMIT 1
      `;
            tenant = tenants.length > 0 ? tenants[0] : null;
        }
        return {
            role: {
                id: role.id,
                name: role.name,
                description: role.description,
                isTenantSpecific: role.isTenantSpecific,
            },
            permissions: permissionsObj,
            tenant,
        };
    }
    catch (error) {
        throw new Error(`Error fetching user info: ${error.message}`);
    }
}
/**
 * Generate JWT token for user
 */
export function generateJWT(payload, expiresIn = "7d") {
    const secret = getJwtSecret();
    return jwt.sign(payload, secret, { expiresIn, algorithm: JWT_ALGORITHM });
}
/**
 * Extract token from Authorization header
 */
export function extractTokenFromHeader(authHeader) {
    if (!authHeader) {
        return null;
    }
    const parts = authHeader.split(" ");
    if (parts.length === 2 && parts[0] === "Bearer") {
        return parts[1];
    }
    return null;
}
/**
 * Get the public role (from PermissionService hybrid cache)
 * Returns the public role with its ID, or a fallback if not found
 */
export async function getPublicRole() {
    // Get from PermissionService hybrid cache
    const publicRole = await permissionService.getPublicRoleAsync();
    if (publicRole) {
        return { id: publicRole.id, name: publicRole.name };
    }
    // Fallback if roles not loaded yet
    return { id: null, name: 'public' };
}
/**
 * Authentication middleware for Express
 * Verifies JWT token and sets req.accountability
 */
export const authMiddleware = async (req, res, next) => {
    try {
        // Extract token from Authorization header, cookie, query param or body
        let token = req.headers.authorization?.replace("Bearer ", "").trim();
        // Treat empty/whitespace-only tokens as no token
        if (!token || token === "") {
            token = undefined;
        }
        if (!token && req.cookies?.token) {
            token = req.cookies.token?.trim();
            if (!token || token === "")
                token = undefined;
        }
        // Also check accessToken cookie (used by some clients)
        if (!token && req.cookies?.accessToken) {
            token = req.cookies.accessToken?.trim();
            if (!token || token === "")
                token = undefined;
        }
        if (!token && req.query?.access_token) {
            token = req.query.access_token?.trim();
            if (!token || token === "")
                token = undefined;
        }
        if (!token && req.body?.access_token) {
            token = req.body.access_token?.trim();
            if (!token || token === "")
                token = undefined;
        }
        if (!token) {
            // No token provided - treat as public access
            const publicRole = await getPublicRole();
            req.accountability = {
                user: null,
                role: publicRole,
                tenant: null,
                permissions: [],
                ipaddress: req.ip || req.connection?.remoteAddress,
            };
            setContextAccountability(req.accountability);
            return next();
        }
        // Verify JWT token
        const payload = jwt.verify(token, getJwtSecret(), { algorithms: [JWT_ALGORITHM] });
        // Validate session and get user in one query
        const sessionResult = await validateSession(payload.sessionToken, payload.tenant_Id?.toString() || null);
        if (!sessionResult) {
            // Session invalid/expired - fall back to public access instead of returning error
            // This allows public routes to work even with expired cookies
            const publicRole = await getPublicRole();
            req.accountability = {
                user: null,
                role: publicRole,
                tenant: null,
                permissions: [],
                ipaddress: req.ip || req.connection?.remoteAddress,
            };
            setContextAccountability(req.accountability);
            return next();
        }
        const { user } = sessionResult;
        // Get user's role assignment — cached with short TTL to avoid DB hit on every request
        const cache = getCache();
        const tenantKey = payload.tenant_Id ?? 'global';
        const userRoleCacheKey = `auth:userrole:${user.id}:${tenantKey}`;
        let userRole = await cache.get(userRoleCacheKey);
        if (!userRole) {
            const userRoleTable = schemaManager.getTable("baasix_UserRole");
            let userRoles;
            if (payload.tenant_Id !== undefined && payload.tenant_Id !== null) {
                userRoles = await db
                    .select({
                    role_Id: userRoleTable.role_Id,
                    tenant_Id: userRoleTable.tenant_Id,
                })
                    .from(userRoleTable)
                    .where(and(eq(userRoleTable.user_Id, user.id), eq(userRoleTable.tenant_Id, payload.tenant_Id)))
                    .limit(1);
            }
            else {
                userRoles = await db
                    .select({
                    role_Id: userRoleTable.role_Id,
                    tenant_Id: userRoleTable.tenant_Id,
                })
                    .from(userRoleTable)
                    .where(eq(userRoleTable.user_Id, user.id))
                    .limit(1);
            }
            userRole = userRoles?.[0] || null;
            if (userRole) {
                await cache.set(userRoleCacheKey, userRole); // Hybrid key: TTL ignored, invalidated by baasix_UserRole hook
            }
        }
        // Get role and permissions from cache or database
        let role = { id: null, name: "user", isTenantSpecific: false };
        let permissions = [];
        if (userRole?.role_Id) {
            const roleData = await getRolesAndPermissions(userRole.role_Id);
            role = {
                id: roleData.id,
                name: roleData.name,
                isTenantSpecific: roleData.isTenantSpecific,
            };
            permissions = Object.values(roleData.permissions);
        }
        // Calculate isAdmin based on role name
        const isAdmin = role.name === 'administrator';
        // Strip hidden fields (like password) from user object
        const sanitizedUser = fieldUtils.stripHiddenFields('baasix_User', user);
        // For non-tenant-specific roles, don't set tenant context
        // (they operate globally without tenant constraints)
        const tenantContext = role.isTenantSpecific === false ? null : (userRole?.tenant_Id || null);
        // Set accountability object with all user fields
        req.accountability = {
            user: {
                ...sanitizedUser,
                isAdmin: isAdmin,
                role: role.name,
            },
            role: role,
            tenant: tenantContext,
            permissions: permissions || [],
            ipaddress: req.ip || req.connection?.remoteAddress,
        };
        setContextAccountability(req.accountability);
        next();
    }
    catch (error) {
        // Only log unexpected errors, not common cases like malformed/empty tokens
        const isExpectedError = error.message === 'jwt malformed' ||
            error.message === 'jwt must be provided' ||
            error.message?.includes('invalid token');
        if (!isExpectedError) {
            console.error('Auth middleware error:', error.message);
        }
        // Fall back to public access on error
        const publicRole = await getPublicRole();
        req.accountability = {
            user: null,
            role: publicRole,
            tenant: null,
            permissions: [],
            ipaddress: req.ip || req.connection?.remoteAddress,
        };
        setContextAccountability(req.accountability);
        next();
    }
};
/**
 * Helper function to check if user is an administrator
 */
export const isAdmin = (req) => {
    return req.accountability?.role?.name === "administrator" || req.accountability?.user?.isAdmin === true;
};
/**
 * Middleware to check for admin access
 */
export const adminOnly = (req, res, next) => {
    if (!isAdmin(req)) {
        return next(new APIError("Access denied. Administrators only.", 403));
    }
    next();
};
/**
 * Create a session in the database
 */
export async function createSession(user, expiresInSeconds, sessionType = "default", tenantId = null) {
    const ItemsService = await getItemsService();
    const sessionService = new ItemsService('baasix_Sessions');
    const sessionToken = crypto.randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
    await sessionService.createOne({
        token: sessionToken,
        user_Id: user.id,
        type: sessionType,
        expiresAt,
        tenant_Id: tenantId,
    });
    return sessionToken;
}
/**
 * Validate a session token
 */
export async function validateSession(token, expectedTenantId = null) {
    // Check short-TTL session cache to avoid DB query on every request
    const cache = getCache();
    const sessionCacheKey = `auth:session:${token}`;
    const cached = await cache.get(sessionCacheKey);
    if (cached) {
        // Re-check expiry even from cache
        if (new Date() > new Date(cached.session.expiresAt)) {
            await cache.delete(sessionCacheKey);
            const ItemsService = await getItemsService();
            const sessionService = new ItemsService('baasix_Sessions');
            await sessionService.deleteOne(cached.session.id);
            return null;
        }
        return cached;
    }
    const ItemsService = await getItemsService();
    const sessionService = new ItemsService('baasix_Sessions');
    // Fetch session with user relation to get all user fields in one query.
    // includeHidden: this is the trusted session-validation path — the session's
    // `token` field is hidden but needed here for cache/cleanup bookkeeping.
    const sessions = await sessionService.readByQuery({
        filter: { token: { eq: token } },
        fields: ['*', 'user.*'],
        limit: 1,
    }, false, undefined, { includeHidden: true });
    if (!sessions.data || sessions.data.length === 0) {
        return null;
    }
    const session = sessions.data[0];
    // Check if session has expired
    if (new Date() > new Date(session.expiresAt)) {
        await sessionService.deleteOne(session.id);
        return null;
    }
    // Optional tenant validation for enhanced security
    const isMultiTenant = env.get("MULTI_TENANT") === "true";
    if (isMultiTenant && expectedTenantId !== null && expectedTenantId !== undefined && session.tenant_Id !== expectedTenantId) {
        console.warn(`Session tenant mismatch: expected ${expectedTenantId}, got ${session.tenant_Id}`);
    }
    // Get user - either from populated relation or fetch separately
    let user = session.user;
    if (!user || typeof user === 'string') {
        const userService = new ItemsService('baasix_User');
        user = await userService.readOne(session.user_Id);
    }
    if (!user) {
        return null;
    }
    const result = { session, user };
    // Cache for 30 seconds — balances performance with session revocation latency
    await cache.set(sessionCacheKey, result, 30000);
    return result;
}
/**
 * Validate session limits for a user
 */
export async function validateSessionLimits(userId, sessionType, tenantId = null, role = null) {
    if (sessionType === "default") {
        return { isValid: true };
    }
    if (!["mobile", "web"].includes(sessionType)) {
        return { isValid: false, error: "Invalid session type. Must be 'mobile' or 'web'" };
    }
    if (role?.name === "administrator") {
        return { isValid: true };
    }
    try {
        const settingsService = await getSettingsService();
        const ItemsService = await getItemsService();
        const isMultiTenantEnabled = env.get("MULTI_TENANT") === "true";
        let settings;
        if (isMultiTenantEnabled && tenantId) {
            settings = await settingsService.getTenantSettings(tenantId);
        }
        else {
            settings = settingsService.getGlobalSettings();
        }
        if (!settings) {
            return { isValid: true };
        }
        const limitKey = `${sessionType}_session_limit`;
        const sessionLimit = settings[limitKey];
        if (sessionLimit === undefined || sessionLimit === null || sessionLimit === -1) {
            return { isValid: true };
        }
        const sessionLimitRoles = settings.session_limit_roles;
        if (sessionLimitRoles && Array.isArray(sessionLimitRoles) && sessionLimitRoles.length > 0) {
            if (!role?.id || !sessionLimitRoles.includes(role.id)) {
                return { isValid: true };
            }
        }
        if (sessionLimit === 0) {
            return {
                isValid: false,
                error: `${sessionType.charAt(0).toUpperCase() + sessionType.slice(1)} sessions are not allowed`
            };
        }
        const sessionService = new ItemsService('baasix_Sessions');
        const filter = {
            user_Id: userId,
            type: sessionType,
            expiresAt: { gt: new Date().toISOString() }
        };
        if (isMultiTenantEnabled && tenantId) {
            filter.tenant_Id = tenantId;
        }
        const activeSessions = await sessionService.readByQuery({
            filter,
            limit: -1
        }, true);
        if (activeSessions.data.length >= sessionLimit) {
            return {
                isValid: false,
                error: `Maximum ${sessionType} session limit (${sessionLimit}) reached. Please logout from another device.`
            };
        }
        return { isValid: true };
    }
    catch (error) {
        console.error("Error validating session limits:", error);
        return { isValid: true };
    }
}
/**
 * Generate token with session creation
 */
export async function generateToken(user, role, tenant = null, ip = null, sessionType = "default") {
    const expiresIn = parseInt(env.get("ACCESS_TOKEN_EXPIRES_IN") || "3600");
    const sessionToken = await createSession(user, expiresIn, sessionType, tenant?.id);
    const payload = {
        id: user.id,
        role_Id: role.id,
        sessionToken,
    };
    if (tenant) {
        payload.tenant_Id = tenant.id;
    }
    // Add audit log for login
    try {
        const ItemsService = await getItemsService();
        const auditService = new ItemsService('baasix_AuditLog');
        await auditService.createOne({
            type: "auth",
            action: "login",
            entity: "baasix_User",
            entityId: user.id,
            userId: user.id,
            ipaddress: ip,
            tenant_Id: tenant?.id,
        });
    }
    catch (error) {
        console.error("Error creating audit log:", error);
    }
    return generateJWT(payload, `${expiresIn}s`);
}
export default authMiddleware;
//# sourceMappingURL=auth.js.map