// @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;