Source: Router/orgaSettings/controller.js

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
 */
/**
 * OrgaSettings Controller
 *
 * HTTP layer for domain and mail/SMTP settings endpoints.
 * All endpoints require the user to be an organisation admin (checkAdmin middleware).
 */

import Busboy from 'busboy';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import * as service from './service.js';

const ALLOWED_ICON_MIME = new Set(['image/png', 'image/jpeg', 'image/svg+xml', 'image/gif', 'image/webp']);

// ── Domains ───────────────────────────────────────────────────────────────────

/**
 * GET /kpe20/orgaSettings/domains
 * Returns the domain settings for the current organisation.
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const getDomainsController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const domains = await service.getDomains(orgId);
        res.json({ success: true, result: domains });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Failed to load domain settings.' });
    }
};

/**
 * PUT /kpe20/orgaSettings/domains
 * Validates and saves the domain settings for the current organisation.
 *
 * Request body: { [domain: string]: "internal" | "external" }
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const saveDomainsController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const domains = req.body;
        if (!domains || typeof domains !== 'object' || Array.isArray(domains))
            return res.status(400).json({ success: false, message: 'Request body must be an object.' });

        const errors = await service.validateDomains(domains, orgId);
        if (errors.length > 0)
            return res.status(400).json({ success: false, errors });

        await service.saveDomains(orgId, domains);
        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Failed to save domain settings.' });
    }
};

// ── Mail settings ─────────────────────────────────────────────────────────────

/**
 * GET /kpe20/orgaSettings/mail
 * Returns the SMTP settings for the current organisation (password masked).
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const getMailController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const settings = await service.getMailSettings(orgId);
        res.json({ success: true, result: settings });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Failed to load mail settings.' });
    }
};

/**
 * PUT /kpe20/orgaSettings/mail
 * Saves the SMTP settings for the current organisation.
 *
 * Request body: { host, port, user, password, userName?, from?, secure? }
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
// ── Apps ─────────────────────────────────────────────────────────────────────

/**
 * GET /kpe20/orgaSettings/apps
 * Returns the app registry for the current organisation.
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const getAppsController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const apps = await service.getApps(orgId);
        res.json({ success: true, result: apps });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Failed to load app settings.' });
    }
};

/**
 * PUT /kpe20/orgaSettings/apps
 * Saves the app registry for the current organisation.
 *
 * Request body: { [appId]: { domain, roles, title, description?, port? } }
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const saveAppsController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const apps = req.body;
        if (!apps || typeof apps !== 'object' || Array.isArray(apps))
            return res.status(400).json({ success: false, message: 'Request body must be an object.' });

        await service.saveApps(orgId, apps);
        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Failed to save app settings.' });
    }
};


/**
 * POST /kpe20/orgaSettings/apps/:appId/icon
 * Upload an icon image for a specific app to the public bucket.
 * Stores the resulting public URL in Vault under apps[appId].icon.
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const uploadAppIconController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const { appId } = req.params;

        const iconUrl = await new Promise((resolve, reject) => {
            const busboy = Busboy({ headers: req.headers });
            /** @type {Promise<string>|null} */
            let uploadPromise = null;

            busboy.on('file', (fieldname, stream, info) => {
                const { mimeType } = info;
                if (!ALLOWED_ICON_MIME.has(mimeType)) {
                    stream.resume();
                    reject(new Error(`File type "${mimeType}" is not allowed. Use PNG, JPG, SVG, GIF or WebP.`));
                    return;
                }
                // Store the promise — busboy may emit 'finish' before the upload completes
                uploadPromise = service.uploadAppIcon(orgId, appId, stream, mimeType);
            });

            busboy.on('finish', () => {
                if (!uploadPromise) { reject(new Error('No file received.')); return; }
                uploadPromise.then(resolve).catch(reject);
            });
            busboy.on('error', reject);
            // @ts-ignore — req is a Node.js IncomingMessage at runtime; TypeScript typedef is narrower
            req.pipe(busboy);
        });

        res.json({ success: true, iconUrl });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: e.message ?? 'Failed to upload icon.' });
    }
};

export const saveMailController = async (req, res) => {
    try {
        const orgId = req.session.root;
        if (!orgId)
            return res.status(400).json({ success: false, message: 'No organisation in session.' });

        const settings = req.body;

        // Basic input validation
        const portNum = parseInt(settings?.port, 10);
        if (!settings?.host || typeof settings.host !== 'string' || !settings.host.trim())
            return res.status(400).json({ success: false, message: 'Field "host" is required.' });
        if (isNaN(portNum) || portNum < 1 || portNum > 65535)
            return res.status(400).json({ success: false, message: 'Field "port" must be a number between 1 and 65535.' });
        if (!settings?.user || typeof settings.user !== 'string' || !settings.user.trim())
            return res.status(400).json({ success: false, message: 'Field "user" is required.' });

        // Sanitise numeric field
        settings.port = portNum;

        await service.saveMailSettings(orgId, settings);
        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: `Failed to save mail settings: ${e.message}` });
    }
};