Source: Router/orgaSettings.js

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from '../types.js'
 */

/**
 * OrgaSettings Router
 *
 * Endpoints for managing per-organisation settings stored in Vault.
 * All routes require admin privileges (checkAdmin middleware).
 *
 * Mounted at: /api/kpe20/orgaSettings  (see http-server.js)
 *
 * @swagger
 * tags:
 *   - name: OrgaSettings
 *     description: |
 *       Per-organisation settings stored in Vault —
 *       domain mappings and SMTP configuration.
 *       Schemas and examples are defined in ./orgaSettings/orgaSettings.swagger.yaml.
 *       Use $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/{SchemaName}' in route blocks.
 */

import express from 'express';
import { checkAdmin }               from '../utils/authChecks.js';
import { requestUpdateLogger }      from '../utils/requestLogger.js';
import * as orgaSettingsController  from './orgaSettings/controller.js';

/** @type {express.Express} */
const api = express();

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

/**
 * @swagger
 * /api/kpe20/orgaSettings/domains:
 *   get:
 *     summary: Load domain settings for the current organisation
 *     description: >
 *       Returns the full domain map (domain → type) stored in Vault
 *       for the authenticated user's organisation.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: Domain map retrieved successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/DomainMap'
 *       400:
 *         description: No organisation in session
 *       500:
 *         description: Failed to load domain settings
 */
// @ts-ignore
api.get('/domains', checkAdmin, orgaSettingsController.getDomainsController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/domains:
 *   put:
 *     summary: Save domain settings for the current organisation
 *     description: >
 *       Validates and persists the domain map for the authenticated user's
 *       organisation in Vault.  Each entry maps a domain name to its type
 *       ("internal" or "external").  Validation checks format rules and
 *       conflicts with other organisations.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/DomainMap'
 *           example:
 *             myclub: internal
 *             myclub.de: external
 *     responses:
 *       200:
 *         description: Domains saved successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       400:
 *         description: Validation errors or missing organisation
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 errors:
 *                   type: array
 *                   items:
 *                     $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/DomainValidationError'
 *       500:
 *         description: Failed to save domain settings
 */
// @ts-ignore
api.put('/domains', checkAdmin, requestUpdateLogger, orgaSettingsController.saveDomainsController);

// ── Apps ─────────────────────────────────────────────────────────────────────

/**
 * @swagger
 * /api/kpe20/orgaSettings/apps:
 *   get:
 *     summary: Load app registry for the current organisation
 *     description: >
 *       Returns all configured apps (domain, roles, title, etc.) stored in
 *       Vault for the authenticated user's organisation.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: App registry retrieved successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/AppsMap'
 *       400:
 *         description: No organisation in session
 *       500:
 *         description: Failed to load app settings
 */
// @ts-ignore
api.get('/apps', checkAdmin, orgaSettingsController.getAppsController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/apps:
 *   put:
 *     summary: Save app registry for the current organisation
 *     description: >
 *       Persists the full app registry for the authenticated user's organisation
 *       in Vault.  The entire object is replaced; pass the full updated map.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/AppsMap'
 *     responses:
 *       200:
 *         description: App registry saved
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       400:
 *         description: Invalid request body or missing organisation
 *       500:
 *         description: Failed to save app settings
 */
// @ts-ignore
api.post('/apps', checkAdmin, requestUpdateLogger, orgaSettingsController.saveAppsController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/apps/{appId}:
 *   put:
 *     summary: Save a single app entry
 *     description: >
 *       Persists exactly one app (its identity and assignment) for the
 *       authenticated user's organisation. The edit dialog writes one row, so
 *       the endpoint takes one row — the rest of the map is left untouched.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: appId
 *         required: true
 *         schema:
 *           type: string
 *         description: The app identifier key (e.g. member.app)
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/AppEntry'
 *     responses:
 *       200:
 *         description: App saved
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       400:
 *         description: Invalid app entry or missing organisation
 *       500:
 *         description: Failed to save app settings
 */
// @ts-ignore
api.put('/apps/:appId', checkAdmin, requestUpdateLogger, orgaSettingsController.saveAppController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/apps/{appId}:
 *   delete:
 *     summary: Delete a single app entry
 *     description: >
 *       Removes exactly one app (and its links) for the authenticated user's
 *       organisation.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: appId
 *         required: true
 *         schema:
 *           type: string
 *         description: The app identifier key (e.g. ext-kpe-wiki)
 *     responses:
 *       200:
 *         description: App deleted
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       400:
 *         description: Missing organisation
 *       500:
 *         description: Failed to delete app settings
 */
// @ts-ignore
api.delete('/apps/:appId', checkAdmin, requestUpdateLogger, orgaSettingsController.deleteAppController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/apps/{appId}/icon:
 *   post:
 *     summary: Upload an icon for an app
 *     description: >
 *       Uploads an image file (PNG, JPG, SVG, GIF or WebP) to the
 *       `commtool-public` MinIO bucket under `{orgId}/icons/{appId}.{ext}`
 *       and persists the resulting public URL in Vault under
 *       `apps[appId].icon` for the current organisation.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: appId
 *         required: true
 *         schema:
 *           type: string
 *         description: The app identifier key (e.g. member.app)
 *     requestBody:
 *       required: true
 *       content:
 *         multipart/form-data:
 *           schema:
 *             type: object
 *             properties:
 *               files:
 *                 type: string
 *                 format: binary
 *                 description: Image file (PNG / JPG / SVG / GIF / WebP, max recommended 512 KB)
 *     responses:
 *       200:
 *         description: Icon uploaded and URL stored in Vault
 *         content:
 *           application/json:
 *             schema:
 *               $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/IconUploadResponse'
 *       400:
 *         description: Unsupported file type, missing app or missing organisation
 *       500:
 *         description: Upload or Vault write failed
 */
// @ts-ignore
api.post('/apps/:appId/icon', checkAdmin, requestUpdateLogger, orgaSettingsController.uploadAppIconController);

// ── Deployments ───────────────────────────────────────────────────────────────

/**
 * @swagger
 * /api/kpe20/orgaSettings/deployments:
 *   get:
 *     summary: Catalogue and current choice for this organisation's apps
 *     description: >
 *       Returns, per app, the selectable versions and backend branches **by
 *       name** plus the organisation's current choice. Backend URLs are
 *       deliberately absent: the choice belongs to the customer admin, the
 *       targets are CommTool's. A `version` of `null` means "follow the
 *       pointer" (`AppRelease.Current`).
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: Deployment catalogue keyed by appKey
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/DeploymentCatalog'
 *       400:
 *         description: No organisation in session
 *       500:
 *         description: Failed to load deployments
 */
// @ts-ignore
api.get('/deployments', checkAdmin, orgaSettingsController.getDeploymentsController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/app-offers:
 *   get:
 *     summary: The contract's offer — products and their environments
 *     description: >
 *       What an organisation may **add**: per product (`member`, `admin`,
 *       `portal`) its display defaults and its selectable environments (backend
 *       branches). Source is the imported contract (`AppCatalog` +
 *       `AppBackendBranch`); the branch **targets** (URLs) are stripped — the
 *       customer admin chooses a name, not a host. Adding an app creates the org
 *       app `<produkt>.<umgebung>` (`member.test`) and its deployment.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: Offer keyed by product
 *       400:
 *         description: No organisation in session
 *       500:
 *         description: Failed to load app offers
 */
// @ts-ignore
api.get('/app-offers', checkAdmin, orgaSettingsController.getAppOffersController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/deployments/{appKey}:
 *   put:
 *     summary: Choose version and backend branch for this organisation
 *     description: >
 *       The only write an organisation admin has in the release model. Both
 *       values are validated against the existing catalogue and releases, so a
 *       choice can never point at something that does not exist. The catalogue
 *       itself and the pointer (`AppRelease.Current`) stay CommTool-only — there
 *       is no shared write path, which is what keeps the two authorities apart.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: appKey
 *         required: true
 *         schema:
 *           type: string
 *         example: member.app
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/DeploymentChoice'
 *           example:
 *             branch: prod
 *             version: null
 *     responses:
 *       200:
 *         description: Choice saved
 *       400:
 *         description: Unknown branch, unknown release or missing organisation
 *       500:
 *         description: Failed to save deployment
 */
// @ts-ignore
api.put('/deployments/:appKey', checkAdmin, requestUpdateLogger, orgaSettingsController.saveDeploymentController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/deployments/{appKey}:
 *   delete:
 *     summary: Drop this organisation's choice for an app
 *     description: >
 *       The organisation follows the pointer and the release's backends again.
 *       This is the rollback, and it is a `DELETE` — no deployment is torn down.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: appKey
 *         required: true
 *         schema:
 *           type: string
 *         example: member.app
 *     responses:
 *       200:
 *         description: Choice removed (`removed: false` when there was none)
 *       400:
 *         description: No organisation in session
 *       500:
 *         description: Failed to clear deployment
 */
// @ts-ignore
api.delete('/deployments/:appKey', checkAdmin, requestUpdateLogger, orgaSettingsController.clearDeploymentController);

// ── Mail / SMTP ───────────────────────────────────────────────────────────────

/**
 * @swagger
 * /api/kpe20/orgaSettings/mail:
 *   get:
 *     summary: Load SMTP settings for the current organisation
 *     description: >
 *       Returns the SMTP configuration stored in Vault for the authenticated
 *       user's organisation.  The `password` field is masked before being
 *       returned.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: SMTP settings retrieved (password masked)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/MailSettings'
 *       400:
 *         description: No organisation in session
 *       500:
 *         description: Failed to load mail settings
 */
// @ts-ignore
api.get('/mail', checkAdmin, orgaSettingsController.getMailController);

/**
 * @swagger
 * /api/kpe20/orgaSettings/mail:
 *   post:
 *     summary: Save SMTP settings for the current organisation
 *     description: >
 *       Validates and persists SMTP configuration for the authenticated
 *       user's organisation in Vault.  If `password` is the mask sentinel
 *       (••••••••) the existing password is preserved unchanged.
 *     tags: [OrgaSettings]
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './orgaSettings/orgaSettings.swagger.yaml#/components/schemas/MailSettings'
 *     responses:
 *       200:
 *         description: SMTP settings saved
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       400:
 *         description: Validation error (missing host/user or invalid port)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: 'Field "host" is required.'
 *       500:
 *         description: Failed to save mail settings
 */
// @ts-ignore
api.post('/mail', checkAdmin, requestUpdateLogger, orgaSettingsController.saveMailController);

export default api;