Source: Router/group.js

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

/**
 * Group Management Router
 *
 * Provides CRUD endpoints for managing hierarchical group structures
 * within an organisation.  Groups form the backbone of the member
 * management system, supporting multi-level hierarchies with optional
 * gender and age-stage restrictions.
 *
 * Key rules:
 * - `hierarchie`, `stage`, and `gender` are immutable once a group is created.
 * - Banner inheritance propagates recursively down the child tree.
 * - Sister-group links (`memberS`) are managed automatically via `?belongsTo`.
 * - Groups with remaining members cannot be deleted.
 *
 * @swagger
 * tags:
 *   - name: Groups
 *     description: |
 *       Hierarchical group management within an organisation.
 *
 *       **Gender values**
 *       | Code | Meaning |
 *       |------|---------|
 *       | `M`  | Male    |
 *       | `F`  | Female  |
 *       | `B`  | Mixed (both) |
 *       | `C`  | Combined (virtual cross-gender view) |
 *
 *       Schemas and examples are defined in ./group/group.swagger.yaml.
 *       Use $ref: './group/group.swagger.yaml#/components/schemas/{SchemaName}' in route blocks.
 */

import express from 'express';
import { checkAdmin, checkObjectAdmin, checkVisible } from '../utils/authChecks.js';
import { requestUpdateLogger, readLogger } from '../utils/requestLogger.js';
import { validateSingleUID, validateUUID } from '../utils/uuidValidation.js';
import * as groupController from './group/controller.js';

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

// ---------------------------------------------------------------------------
// Mutation routes
// ---------------------------------------------------------------------------

/**
 * @swagger
 * /api/kpe20/group/{UIDparent}:
 *   put:
 *     summary: Create or update a group
 *     description: |
 *       Inserts a new group as a direct child of `UIDparent`, or updates the
 *       existing group identified by `body.UID` if it already exists.
 *
 *       **Creation rules**
 *       - `gender` must be compatible with the parent group's gender setting.
 *       - `stage` must match the parent's stage (unless the parent has stage 0).
 *       - Banner is inherited from the parent according to the org's banner rules.
 *
 *       **Update rules (group already exists)**
 *       - `hierarchie`, `stage`, and `gender` cannot be changed after creation.
 *       - Group migration (moving to a different parent) is not yet supported.
 *
 *       Requires **object-admin** rights on the parent group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UIDparent
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the parent group
 *       - in: query
 *         name: timestamp
 *         required: false
 *         schema:
 *           type: string
 *         description: |
 *           Backdate timestamp for the creation (ISO 8601 or Unix ms).
 *           Clamped to the minimum allowed backdate of the parent group.
 *       - in: query
 *         name: belongsTo
 *         required: false
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of a sister group to link via `memberS`.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './group/group.swagger.yaml#/components/schemas/GroupUpsertBody'
 *     responses:
 *       200:
 *         description: Group created or updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './group/group.swagger.yaml#/components/schemas/GroupObject'
 *       300:
 *         description: Validation error (invalid parent UID, gender mismatch, stage mismatch, invalid UID format)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *       403:
 *         description: User does not have admin rights on the parent group
 */
// @ts-ignore
api.put('/:UIDparent', validateUUID(['UIDparent']), requestUpdateLogger, checkObjectAdmin, groupController.putGroup);

/**
 * @swagger
 * /api/kpe20/group/{UID}:
 *   post:
 *     summary: Partially update an existing group
 *     description: |
 *       Merges the request body over the existing group data.
 *       Only the supplied fields are updated — absent fields are kept as-is.
 *
 *       **Immutable fields** — rejected with an error if they differ from stored values:
 *       `hierarchie`, `stage`, `gender`.
 *
 *       If `banner` changes, the new banner is propagated recursively to all
 *       child groups and members that inherit banners.
 *
 *       When the group UID equals the organisation root UID and `extraConfig`
 *       is present, the organisation-level extra config is also updated.
 *
 *       Requires **object-admin** rights on the group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to update
 *       - in: query
 *         name: timestamp
 *         required: false
 *         schema:
 *           type: string
 *         description: Backdate timestamp for the update (ISO 8601 or Unix ms).
 *       - in: query
 *         name: belongsTo
 *         required: false
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of a sister group to link/update via `memberS`.
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: './group/group.swagger.yaml#/components/schemas/GroupUpdateBody'
 *     responses:
 *       201:
 *         description: Group updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './group/group.swagger.yaml#/components/schemas/GroupObject'
 *       300:
 *         description: Group not found or immutable-field change attempted
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 */
// @ts-ignore
api.post('/:UID', validateSingleUID, requestUpdateLogger, checkObjectAdmin, groupController.updateGroup);

/**
 * @swagger
 * /api/kpe20/group/{UID}:
 *   delete:
 *     summary: Delete a group
 *     description: |
 *       Permanently deletes the group identified by `UID` along with all its
 *       Links and Visible records.
 *
 *       **Blocked** — returns 300 with the list of remaining members when the
 *       group still has direct members (`person`, `extern`, `guest`, `ggroup`,
 *       `group`, or `job` objects linked via `member` or `memberA`).
 *
 *       Fires a `/remove/group/group/{parentUID}` event on success.
 *
 *       Requires **object-admin** rights on the group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to delete
 *     responses:
 *       200:
 *         description: Group deleted successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *       300:
 *         description: Group still has members — deletion blocked
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: has still members
 *                 result:
 *                   type: array
 *                   items:
 *                     $ref: './group/group.swagger.yaml#/components/schemas/GroupMemberSummary'
 */
// @ts-ignore
api.delete('/:UID', validateSingleUID, requestUpdateLogger, checkObjectAdmin, groupController.deleteGroup);

// ---------------------------------------------------------------------------
// Read routes
// ---------------------------------------------------------------------------

/**
 * @swagger
 * /api/kpe20/group/tree/{UID}:
 *   get:
 *     summary: Get group tree graph (Kroki/Graphviz)
 *     description: |
 *       Builds a Graphviz diagram of the group hierarchy anchored at `UID`
 *       using `memberA` (parent-child) and `memberS` (sister) links and
 *       renders it through the internal Kroki proxy.
 *
 *       **Rendering modes**
 *       - `redirect=true` (default): fetches the diagram from Kroki and serves
 *         the raw image bytes with the correct `Content-Type`.
 *       - `redirect=false`: returns a JSON object with the public Kroki URL and
 *         the encoded Graphviz payload.
 *
 *       **React integration** — rendered nodes include `href` attributes usable
 *       for click-navigation (see `REACT-KROKI-GROUP-TREE.md`).
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the root group for the diagram
 *       - in: query
 *         name: displayLevels
 *         required: false
 *         schema:
 *           type: integer
 *           default: 6
 *         description: Number of visible hierarchy levels from the top (alias `levels`)
 *       - in: query
 *         name: includeOutOfHierarchy
 *         required: false
 *         schema:
 *           type: boolean
 *           default: false
 *         description: |
 *           Include out-of-hierarchy groups, keeping them visible regardless of
 *           `displayLevels`. Aliases: `includeOutOfHierarchie`, `outOfHierarchy`, `outOfHierarchie`
 *       - in: query
 *         name: type
 *         required: false
 *         schema:
 *           type: string
 *           enum: [svg, png, pdf]
 *           default: svg
 *         description: Kroki output format
 *       - in: query
 *         name: redirect
 *         required: false
 *         schema:
 *           type: boolean
 *           default: false
 *         description: |
 *           When true, serves the rendered image directly.
 *           When false, returns JSON with the Kroki URL.
 *     responses:
 *       200:
 *         description: Diagram rendered or URL returned
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './group/group.swagger.yaml#/components/schemas/GroupTreeResult'
 *           image/svg+xml:
 *             schema:
 *               type: string
 *               format: binary
 *           image/png:
 *             schema:
 *               type: string
 *               format: binary
 */
// @ts-ignore
api.get('/tree/:UID', validateSingleUID, checkVisible, readLogger, groupController.getGroupTreeGraph);

/**
 * @swagger
 * /api/kpe20/group/admin/{UID}:
 *   get:
 *     summary: Check admin rights for a group
 *     description: |
 *       Returns whether the currently authenticated user has object-admin
 *       privileges for the specified group.
 *
 *       Useful for conditionally rendering edit controls in the UI without
 *       sending a full mutation request first.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to check
 *     responses:
 *       200:
 *         description: Admin check result
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   type: boolean
 *                   description: True if the user has admin rights for this group
 *                   example: true
 */
// @ts-ignore
api.get('/admin/:UID', validateSingleUID, checkVisible, groupController.checkGroupAdmin);

/**
 * @swagger
 * /api/kpe20/group/minTimestamp/{UID}:
 *   get:
 *     summary: Get minimum allowed backdate timestamp for a group
 *     description: |
 *       Returns the minimum Unix timestamp (in milliseconds) to which an
 *       operation on this group may be backdated, constrained by the latest
 *       existing history row for the group object.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group
 *       - in: query
 *         name: timestamp
 *         required: false
 *         schema:
 *           type: integer
 *         description: Proposed timestamp (ms) to validate against the constraint
 *     responses:
 *       200:
 *         description: Minimum allowed timestamp in milliseconds
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   type: integer
 *                   example: 1640995200000
 */
// @ts-ignore
api.get('/minTimestamp/:UID', validateSingleUID, checkVisible, groupController.minTimestamp);


/**
 * @swagger
 * /api/kpe20/group/createdTimestamp/{UID}:
 *   get:
 *     summary: Get creation allowed backdate timestamp for a group
 *     description: |
 *       Returns the creation Unix timestamp (in milliseconds)
 *       for a group object, constrained by its latest history row.
 *       This is used to avoid something is linked to before the creation of the target
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group
 *       - in: query
 *         name: timestamp
 *         required: false
 *         schema:
 *           type: integer
 *         description: Proposed timestamp (ms) to validate against the constraint
 *     responses:
 *       200:
 *         description: Minimum allowed timestamp in milliseconds
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   type: integer
 *                   example: 1640995200000
 */
// @ts-ignore
api.get('/createdTimestamp/:UID', validateSingleUID, checkVisible, groupController.createdTimestamp);

/**
 * @swagger
 * /api/kpe20/group/{UID}:
 *   get:
 *     summary: Get group details
 *     description: |
 *       Retrieves the full record for a group including its `Data` JSON blob.
 *
 *       Optional expansions via query parameters:
 *       - `?parent=true` — includes the direct parent group and its sister group (`parentS`).
 *       - `?sibling=true` — includes the sister group (`memberS` link) of this group.
 *
 *       Requires **visible** access to the group.
 *     tags:
 *       - Groups
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: UID
 *         required: true
 *         schema:
 *           type: string
 *           format: uuid
 *         description: UUID of the group to retrieve
 *       - in: query
 *         name: parent
 *         required: false
 *         schema:
 *           type: boolean
 *         description: Set to `true` to include the parent group object
 *       - in: query
 *         name: sibling
 *         required: false
 *         schema:
 *           type: boolean
 *         description: Set to `true` to include the sister group object
 *     responses:
 *       200:
 *         description: Group details
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 result:
 *                   $ref: './group/group.swagger.yaml#/components/schemas/GroupObject'
 *       200-not-found:
 *         description: Group not found (success=false, no HTTP error code)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: "group aefe03ad-… does not exist"
 */
// @ts-ignore
api.get('/:UID', validateSingleUID, checkVisible, readLogger, groupController.getGroup);

export default api;