// @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) |
*
* components:
* schemas:
* GroupData:
* type: object
* description: The JSON `Data` blob stored in the `Member` table for a group.
* properties:
* name:
* type: string
* description: Human-readable group name
* example: "Junior Team A"
* gender:
* type: string
* enum: [M, F, B, C]
* description: Gender restriction for membership
* example: M
* stage:
* type: string
* description: Age-stage restriction (org-specific values, e.g. "Junioren")
* example: Junioren
* hierarchie:
* type: string
* description: Organisation hierarchy level (org-specific, e.g. "Verein", "Abteilung")
* example: Abteilung
* location:
* type: string
* description: Geographic location label
* example: Sporthalle West
* banner:
* type: string
* description: URL or path of the group banner image
* example: "https://cdn.example.com/banners/jugend.jpg"
* fees:
* type: array
* description: Fee configuration for this group
* items:
* type: object
* accounts:
* type: array
* description: Account configuration (protected for non-account-managers)
* items:
* type: object
*
* GroupObject:
* type: object
* description: Full group record as returned by GET /group/:UID
* properties:
* UID:
* type: string
* format: uuid
* example: "aefe03ad-3d74-11f0-b612-b2b0f54f4439"
* UIDBelongsTo:
* type: string
* format: uuid
* description: UID of the root/organisation object this group belongs to
* Type:
* type: string
* enum: [group, ggroup]
* Title:
* type: string
* description: Rendered display title
* Display:
* type: string
* description: Short display name from the Member table
* gender:
* type: string
* enum: [M, F, B, C]
* hierarchie:
* type: string
* stage:
* type: string
* dindex:
* type: integer
* description: Sort index within its parent
* validFrom:
* type: number
* description: Unix timestamp (seconds) when the group became active
* Data:
* $ref: '#/components/schemas/GroupData'
* parent:
* $ref: '#/components/schemas/GroupObject'
* description: Present only when ?parent=true is requested
* parentS:
* $ref: '#/components/schemas/GroupObject'
* description: Sister group of the parent; present only when ?parent=true
* sibling:
* $ref: '#/components/schemas/GroupObject'
* description: Present only when ?sibling=true is requested
*
* GroupUpsertBody:
* type: object
* required:
* - UID
* - name
* - gender
* - stage
* - hierarchie
* properties:
* UID:
* type: string
* format: uuid
* description: |
* Client-generated UID for the group.
* If this UID already exists the request becomes an update.
* example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
* name:
* type: string
* example: "Junior Team A"
* gender:
* type: string
* enum: [M, F, B, C]
* example: M
* stage:
* type: string
* example: Junioren
* hierarchie:
* type: string
* example: Abteilung
* location:
* type: string
* example: Sporthalle West
* banner:
* type: string
* example: "https://cdn.example.com/banners/jugend.jpg"
* fees:
* type: array
* items:
* type: object
* accounts:
* type: array
* items:
* type: object
*
* GroupUpdateBody:
* type: object
* description: |
* Partial update payload — only supplied fields are changed.
* `hierarchie`, `stage`, and `gender` are immutable and will be rejected
* if they differ from the stored values.
* properties:
* name:
* type: string
* location:
* type: string
* banner:
* type: string
* fees:
* type: array
* items:
* type: object
* accounts:
* type: array
* items:
* type: object
* extraConfig:
* type: object
* description: Extra organisation-level config (only applied when UID === root UID)
*
* GroupMemberSummary:
* type: object
* description: Compact member info returned when a delete is blocked by remaining members
* properties:
* UID:
* type: string
* format: uuid
* Display:
* type: string
* Title:
* type: string
*
* GroupTreeResult:
* type: object
* properties:
* krokiUrl:
* type: string
* description: Complete Kroki render URL for the diagram
* encoded:
* type: string
* description: Base64-encoded Graphviz payload
* renderedContent:
* type: string
* description: Raw rendered diagram bytes (when redirect=true)
* nodeCount:
* type: integer
* description: Number of group nodes in the diagram
* edgeCount:
* type: integer
* description: Number of edges (links) in the diagram
*/
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: '#/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: '#/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: '#/components/schemas/GroupUpdateBody'
* responses:
* 201:
* description: Group updated successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* result:
* $ref: '#/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: '#/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: '#/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/{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: '#/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;