feat: public projects, project deletion and sagas

Public projects:
- Project.isPublic (create/update, owners and admins). Anyone can read a
  public project without a token: project, board, tickets, comments,
  history, releases, activity, tracking and statistics, over GraphQL and
  REST. Writes stay for members (401 anonymous, 403 outsiders).
- @OptionalAuth() / @ApiOptionalPireonAuth(): anonymous callers allowed,
  invalid tokens still rejected. ReadAccess lets anonymous viewers read
  public projects only; linked tickets are filtered by visibility.
- publicProjects (GraphQL) and GET /api/explore/projects (REST).
- Web: «Explorar» page, project pages open without a session (read-only
  notice, sign-in hints), public badge, visibility switch in settings,
  no realtime subscriptions without a session.

Project deletion:
- Owner only, confirming the key (deleteProject / DELETE
  /api/projects/{project}?confirm=KEY). The key is freed; the read model
  drops the project and members are notified (web goes home).

Sagas (run by the EventsBridge after projections, durable and ordered):
- ProjectDeletionSaga: project deleted → delete its tickets and releases;
  ticket deleted → purge its comments and drop links from other tickets.
- ReleaseArchivedSaga: unfinished tickets leave an archived release.
- UserDeactivatedSaga: the Pireon webhook records UserDeactivatedEvent in
  the event store (EventRecorder); the tracker unassigns that user's open
  tickets as the system actor.
- Internal idempotent commands; domain refusals are logged and skipped,
  concurrency conflicts retried. Affected streams come from the event store
  (TrackerStreamsRepository).

Tests: domain lifecycle specs, saga specs run like the bridge, facade
tests, e2e for public access, cascade deletion, archived releases and
deactivation webhook. Docs, READMEs, schema.gql/openapi.json and Word/PDF
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 07:04:20 +02:00
parent 0e301ad545
commit e656222b54
164 changed files with 3862 additions and 414 deletions

View File

@@ -632,7 +632,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "A project with its members and settings",
"tags": [
@@ -743,7 +744,117 @@
"pireon": []
}
],
"summary": "Rename or describe a project (owner/admin)",
"summary": "Rename, describe or make public/private a project (owner/admin)",
"tags": [
"Projects"
]
},
"delete": {
"description": "Deletes the project and, right after (asynchronously, through a saga), its tickets, comments and releases; links from tickets of other projects are removed and the key becomes free. Cannot be undone from the API. `confirm` must be the project key.",
"operationId": "ProjectsController_remove",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
},
{
"name": "confirm",
"required": true,
"in": "query",
"description": "The project key, to confirm the deletion",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResultDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Delete a project (owner only)",
"tags": [
"Projects"
]
@@ -981,7 +1092,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Kanban board: one column per status",
"tags": [
@@ -1062,7 +1174,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Tracking dashboard: counts by status, type, person…",
"tags": [
@@ -1159,7 +1272,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Statistics of the last `days` days",
"tags": [
@@ -1278,7 +1392,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Activity feed (newest first)",
"tags": [
@@ -1970,7 +2085,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Search the tickets of a project",
"tags": [
@@ -2161,7 +2277,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "A ticket",
"tags": [
@@ -2944,7 +3061,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Comments of the ticket (oldest first)",
"tags": [
@@ -3166,7 +3284,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "History of the ticket (newest first)",
"tags": [
@@ -3545,7 +3664,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Releases of a project",
"tags": [
@@ -3733,7 +3853,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "A release",
"tags": [
@@ -3919,7 +4040,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Finished tickets out of the total",
"tags": [
@@ -3998,7 +4120,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Burnup: tickets in the release and finished ones, day by day",
"tags": [
@@ -4106,7 +4229,8 @@
},
{
"pireon": []
}
},
{}
],
"summary": "Tickets included in the release",
"tags": [
@@ -4322,6 +4446,33 @@
"Releases"
]
}
},
"/api/explore/projects": {
"get": {
"description": "Their board, tickets, comments, releases, activity and statistics can be read without a token too.",
"operationId": "ExploreController_publicProjects",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProjectDto"
}
}
}
}
}
},
"summary": "Public projects (no token needed)",
"tags": [
"Explore"
],
"security": []
}
}
},
"info": {
@@ -4875,6 +5026,10 @@
"type": "string",
"nullable": true
},
"isPublic": {
"type": "boolean",
"description": "Readable by anyone, also without a token"
},
"owner": {
"$ref": "#/components/schemas/UserRefDto"
},
@@ -4905,6 +5060,7 @@
"key",
"name",
"description",
"isPublic",
"owner",
"members",
"settings",
@@ -4930,6 +5086,11 @@
"type": "string",
"nullable": true,
"maxLength": 5000
},
"isPublic": {
"type": "boolean",
"default": false,
"description": "Anyone can read it without signing in (board, tickets, comments, releases, activity, statistics)"
}
},
"required": [
@@ -4971,6 +5132,11 @@
"type": "string",
"nullable": true,
"maxLength": 5000
},
"isPublic": {
"type": "boolean",
"default": false,
"description": "Anyone can read it without signing in (board, tickets, comments, releases, activity, statistics)"
}
}
},