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:
@@ -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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user