Source: utils/organizationUtils.js

/**
 * Organization utilities for handling organization selection and root setting
 *
 * Key Concept: Link Propagation
 * =============================
 * Most objects (persons, groups, externs, jobs, guests) have their member links
 * propagated up to their organization. However, LISTS are different:
 *
 * - Propagated objects: Person/Group/Extern/Job/Guest → member link → Organization
 * - Non-propagated objects: List → memberA link → Group → member link → Organization
 *
 * This is why getOrganizationForList() needs a more complex query than getOrganizationForObject().
 * 
 *  @import {ExpressRequestAuthorized, ExpressResponse} from './../types.js'
 */
import { query, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { getConfig } from '../utils/compileTemplates.js';
import { getRedisClient } from '@commtool/shared-auth';
import { getSuperAdmin, getAllOrganizations } from '../Router/orga/service.js';
import { isAdmin, isAdminOrga, isBaseAdmin } from '../utils/authChecks.js';
import { wsFakeUser } from '../server.ws.js';
import { getUser, getFakeUser } from './userUtils.js';

/**
 * Get the required database roles based on NODE_ENV
 * Production: db-user, db-admin
 * Development: db-dev, db-admin
 * Demonstration: db-demo, db-admin
 * @returns {string[]} Array of valid roles for current environment
 */
const getRequiredDbRoles = () => {
    if (process.env.NODE_ENV === 'development') {
        return ['db-dev', 'db-admin'];
    }
    if (process.env.NODE_ENV === 'demonstration') {
        return ['db-demo', 'db-admin'];
    }
    // Production (default)
    return ['db-user', 'db-admin'];
};




/**
 * Get the organization UID for any PROPAGATED object UID (group, person, extern, job, guest)
 * Works because these object links are propagated to their organization via member links.
 *
 * Link Propagation Flow:
 * Propagated Object → member link → ... → Organization (Data.root=true)
 *
 * NOTE: Does NOT work for lists - use getOrganizationForList() instead
 *
 * @param {Buffer} objectUID - The object UID (Buffer or hex string)
 * @returns {Promise<string|null>} - The organization UID or null if not found
 */
export const getOrganizationForObject = async (objectUID) => {
    try {
        const result = await query(`
            SELECT org.UID AS UIDOrga
            FROM ObjectBase AS obj
            INNER JOIN Links AS objOrgaLink ON (objOrgaLink.UID = obj.UID AND objOrgaLink.Type IN ('member','memberSys', 'memberA','memberS','memberG'))
            INNER JOIN ObjectBase AS org ON (org.UID = objOrgaLink.UIDTarget AND JSON_VALUE(org.Data, '$.root'))
            WHERE obj.UID = ?
            LIMIT 1
        `, [objectUID], { cast: ['UUID'] });

        return result.length > 0 ? result[0].UIDOrga : null;
    } catch (error) {
        console.error('Error getting organization for object:', error);
        return null;
    }
};

/**
 * Get the organization UID for a given group UID
 * @deprecated Use getOrganizationForObject instead (works for all object types due to link propagation)
 * @param {Buffer} groupUID - The group UID (Buffer or hex string)
 *  @returns {Promise<string|null>} - The organization UID or null if not found
 */
export const getOrganizationForGroup = async (groupUID) => {
    return getOrganizationForObject(groupUID);
};

/**
 * Get the organization UID for a given list UID
 * Lists are NOT propagated, so we need: List → memberA → Group → member → Organization
 * Uses simplified organization detection with Data.root=true
 * @param {Buffer} listUID - The list UID (Buffer or hex string)
 *  @returns {Promise<string|null>} - The organization UID or null if not found
 */
export const getOrganizationForList = async (listUID) => {
    try {
        const result = await query(`
            SELECT (
                SELECT org.UID
                FROM Links AS groupOrgaLink
                INNER JOIN ObjectBase AS org ON (org.UID = groupOrgaLink.UIDTarget AND JSON_VALUE(org.Data, '$.root'))
                WHERE groupOrgaLink.UID = listGroupLink.UID
                AND groupOrgaLink.Type IN ('member','memberSys', 'memberA','memberS','memberG')
                LIMIT 1
            ) AS UIDOrga
            FROM ObjectBase AS list
            INNER JOIN Links AS listGroupLink ON (listGroupLink.UID = list.UID AND listGroupLink.Type = 'memberA')
            WHERE list.UID = ?
            LIMIT 1
        `, [listUID]);

        return result.length > 0 ? result[0].UIDOrga : null;
    } catch (error) {
        console.error('Error getting organization for list:', error);
        return null;
    }
};

/** this code is obsoleted, as we always get an organisation from keycloak login */
/**
 * Alternative root finding logic when no specific organization is provided
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 * @returns {Promise<Object>} - {user, config} object
 */
export const alternativeRoot = async (req, res) => {
    // Get authenticated user from session (set by sessionEnhancer)
    const authUser = req.session.authUser;
    
    // Try to find the orga the user has in his keycloak data
    const orgas = await query(
        `SELECT UID FROM Objects WHERE Objects.Type='group' AND Objects.UID=Objects.UIDBelongsTo AND JSON_VALUE(Objects.Data,'$.keycloakOrga')=?`,
        [`UUID-${authUser.organization}`],
        { cast: ['UUID'] }
    );

    if (orgas.length > 0) {
        return setOrgaRoot(orgas[0].UID, req);
    }

    // Try to find the first orga the user is member of
    const orgas2 = await query(
        `SELECT Links.UIDTarget AS UID FROM Links
        INNER JOIN Links AS MLink ON (Links.UIDTarget=MLink.UID AND MLink.Type IN ('member','memberA','memberSys'))
        WHERE Links.UID=? AND Links.Type='identifyer'
        ORDER BY MLink.Type DESC LIMIT 1`,
        [UUID2hex(`UUID-${authUser.id}`)],
        { cast: ['UUID'] }
    );

    if (orgas2.length > 0) {
        return setOrgaRoot(orgas2[0].UID, req);
    }

    // If employee, try to find an orga which has no admin list
    if (authUser.groups && authUser.groups.includes('employees')) {
        const orgas3 = await query(
            `SELECT Objects.UID FROM Objects
            WHERE Objects.Type='group' AND Objects.UID=Objects.UIDBelongsTo
            AND NOT EXISTS (SELECT 1 FROM Links WHERE Links.UIDTarget=Objects.UID AND Links.Type='admin')
            ORDER BY Objects.UID LIMIT 1`,
            [],
            { cast: ['UUID'] }
        );

        if (orgas3.length > 0) {
            // Make the session an admin session
            req.session.admin = true;
            return setOrgaRoot(orgas3[0].UID, req);
        }
    }

    return { user: null, config: null };
};


/**
 * Set the organization root for the current session
 * @param {string} UIDroot - Organization UUID
 * @param {Object} req - Express request object
 * @returns {Promise<Object>} - {user, config} object
 */
export const setOrgaRoot = async (UIDroot, req) => {
    // Set both properties at once
    Object.assign(req.session, {
        root: UIDroot,
        UIDroot: UIDroot,
        app: req.params.app ? (req.params.app === 'root' ? 'db' : req.params.app) : (req.session.app ? req.session.app : 'db')
    });

    /**
     * SECURITY-CRITICAL: Distinguish between service/bot and regular user
     * Service accounts in Keycloak are assigned to 'app-bot' group
     */
    const authUser = req.session.authUser;
    // Handle groups as array or string (Keycloak can return either format)
    const groups = Array.isArray(authUser?.groups) ? authUser.groups : (authUser?.groups ? [authUser.groups] : []);
    const isServiceClient = groups.includes('app-bot') || req.session.superadmin;

    let result;
    if (isServiceClient) {
        // Handle service client/bot requests - assign cached super admin user
        try {
            // Simplified super admin caching - auth library handles bot user identity
            const superAdminKey = `superadmin-${UIDroot}`;
            let superAdmin = await getRedisClient().get(superAdminKey);

            if (!superAdmin) {
                // Cache miss - fetch from database
                superAdmin = await getSuperAdmin(UIDroot);

                if (superAdmin) {
                    // Cache for 1 hour (3600 seconds)
                    await getRedisClient().setEx(superAdminKey, 3600, superAdmin);
                }
            }

            if (superAdmin) {
                // Assign super admin as bot user (auth library handles the detailed user object)
                req.session.user = superAdmin;
                req.session.botUser = !req.session.superadmin; // Keep for logging purposes only
                req.session.baseUser = superAdmin;
                let userOrgas=[]
                if(req.session.superadmin)
                {
                    // get all orgas for superadmin
                    const orgasResult = await query(`
                        SELECT orga.UID FROM ObjectBase as orga
                        INNER JOIN Links AS mslink ON (mslink.UID=orga.UID AND mslink.Type ='memberSys' AND mslink.UID=mslink.UIDTarget)
                        WHERE orga.Type='group'
                    `,
                    [UUID2hex(superAdmin)],
                    { cast: ['UUID'] }
                    );
                    userOrgas=orgasResult.map(o=>o.UID)
                }
                const config = await getConfig(req.session.app, req);
                const user= { UID: superAdmin, isAdmin: true, Data: { firstName: 'super', lastName: 'admin',userOrgas } };
                const mergedConfig = { ...config, sysUserUID: superAdmin, orgaUser: user };

                result = { user: user, config: mergedConfig };
            } else {
                console.warn(`[setOrgaRoot] No super admin found for org: ${UIDroot}`);
                result = { user: null, config: null };
            }
        } catch (error) {
            console.error(`[setOrgaRoot] Error assigning bot user: ${error.message}`);
            result = { user: null, config: null };
        }
    } else {
        // Handle regular user requests
        const user = await getUser(req);
        let fakeUser = null;
        
        // Check for user impersonation via X-Fake-User header (admin only)
        const fakeUserHeader = req.headers['x-fake-user'];
        if (fakeUserHeader) {
            // First check if requester is admin before allowing impersonation
            req.session.user = user.UID;
            req.session.baseUser = user.UID;
            const isUserAdmin = await isAdmin(req.session);
            
            if (isUserAdmin) {
                fakeUser = await getFakeUser(fakeUserHeader, UIDroot);
            } else {
                console.warn(`[setOrgaRoot] Non-admin user ${user.UID} attempted to impersonate ${fakeUserHeader}`);
            }
        }        const app = req.session.app === 'root' ? 'db' : req.session.app;
        if (fakeUser) {
            // Fake a user for admins (impersonation for testing/support)
            req.session.user = fakeUser.UID;
            req.session.fakeLogin = fakeUserHeader;
            req.session.baseUser = user.UID;
            const config = await getConfig(req.session.app, req);
            const mergedConfig = { ...config, sysUserUID: req.session.sysUser, orgaUser: fakeUser };
            req.session.admin = false; // Admin rights not transferred to fake user
            req.session.sysUser = mergedConfig.sysUser;
            req.session.app = app;
            req.session.sysUser = mergedConfig.sysUser         
            result = { user: fakeUser, config: mergedConfig };
        } else {
            // Standard user settings
            req.session.user = user.UID;
            req.session.baseUser = user.UID;
            // Get org roles from authenticated user (set by sessionEnhancer)
            const authUser = req.session.authUser;
            const config = await getConfig(req.session.app, req);
            const mergedConfig = { ...config, sysUserUID: req.session.sysUser, orgaUser: user };
            req.session.admin = await isAdmin(req.session);
            req.session.app = app;
            req.session.sysUser = mergedConfig.sysUser;
            result = { user, config: mergedConfig };
        }
    }


    return result;
};

/**
 * Check and set the organization root for the current request
 * Uses Keycloak organization as single source of truth
 * 
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {Object} res - Express response object
 * @param {Function} next - Express next function
 */

export const checkRoot = async (req, res, next) => {
    try {
        // Determine the organization from either:
        // 1. X-Organization header (for bots and API calls)
        // 2. Keycloak token organization claim (for logged-in users)
        const orgaHeader = req.headers['x-organization'] || req.headers['x-orga'];
        /**   @type {any} */ (req.session).superadmin = req.headers['x-superadmin-uid'] === 'true';
        let targetOrg=orgaHeader
    

          
        // Validate organization format
        if (!targetOrg.match(/^UUID-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) {
            console.warn(`[checkRoot] Invalid organization format: ${targetOrg}`);
            return res.status(400).json({ 
                success: false, 
                missingOrga: true, 
                message: 'Invalid organization format' 
            });
        }
        
        
        /** @type {any} */ req.session.root=targetOrg;
        
                    
        // Set the new organization root
        const { user, config } = await setOrgaRoot(targetOrg, req);
        
        // Handle optional user override header (for testing/bots)
        const userHeader = req.headers['x-user-uid'];
        if (userHeader && userHeader.match(/^UUID-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) {
            const { validateUserForOrganization } = await import('./userUtils.js');
            const userValidation = await validateUserForOrganization(userHeader, targetOrg);
            
            if (userValidation.valid) {
                req.session.user = userHeader;
                /** @type {any} */ (req.session).baseUser = userHeader;
            } else {
                console.warn(`[checkRoot] ❌ User ${userHeader} not valid for org ${targetOrg}`);
                return res.status(403).json({ 
                    success: false, 
                    message: 'User not authorized for organization' 
                });
            }
        }
        
        // Handle super admin override header (for bots)
        const superAdminHeader = req.headers['x-superadmin-uid'];
        if (superAdminHeader && (!user || user.UID === null)) {
            req.session.user = superAdminHeader;
            /** @type {any} */ (req.session).botUser = true;
            /** @type {any} */ (req.session).baseUser = superAdminHeader;
            
            // Cache super admin UID
            const superAdminKey = `superadmin-${targetOrg}`;
            await getRedisClient().setEx(superAdminKey, 3600, superAdminHeader);
        }
        
        if (!config) {
            console.error(`[checkRoot] Failed to load config for organization: ${targetOrg}`);
            return res.status(500).json({ 
                success: false, 
                message: 'Failed to load organization configuration' 
            });
        }
        // Note: With Bearer-only auth (v7.0), session data is stored in the virtual session object
        // attached to req.session by authFactory. No need to persist to Redis.
        
        
        next();
        
    } catch (error) {
        console.error(`[checkRoot] Error: ${error.message}`);
        return res.status(500).json({ 
            success: false, 
            message: 'Error processing organization context' 
        });
    }
};