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