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:
@@ -2,21 +2,23 @@
|
||||
|
||||
## Qué resuelve
|
||||
|
||||
| Necesidad | Cómo se resuelve |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🐛 Bugs | Tickets de tipo `BUG` con prioridad, área y etiquetas |
|
||||
| 🧩 Configuración | Cada proyecto tiene sus estados (con categoría, color y motivo obligatorio), transiciones, tipos de ticket y áreas; lo usado se archiva, no se borra |
|
||||
| 🎨 Cambios de UI | Tipo `UI_CHANGE` con enlace al frame de Figma (validado contra `figma.com`) |
|
||||
| ⚙️ Features | Tipo `FEATURE` siguiendo el workflow completo |
|
||||
| 🧪 QA | Por defecto, `READY_FOR_QA → IN_QA → DONE / QA_FAILED`; fallar QA exige un motivo |
|
||||
| 🚀 Releases | Versiones a las que se asignan tickets; no se publican con tickets sin terminar (salvo forzando) |
|
||||
| 👥 Asignaciones | Solo a miembros del proyecto; panel de carga por persona |
|
||||
| 💬 Comentarios | Decisiones y explicaciones dentro del ticket; el autor edita, el admin modera |
|
||||
| 🔗 Relaciones | `BLOCKS`, `DEPENDS_ON`, `TESTS`, `DUPLICATES`, `RELATES_TO`, también entre proyectos (FE ↔ BE ↔ QA) |
|
||||
| 🏠 Inicio | «Mi trabajo»: mis tickets en todos los proyectos, por categoría (QA fallidos primero), lo que espera QA, próximas releases e «Ir a…» por clave |
|
||||
| 📊 Seguimiento | Panel con contadores, flujo por estado, el equipo con sus tickets abiertos (enlaza a su tablero), feed de actividad e historial por ticket |
|
||||
| 📈 Estadísticas | Creados vs. resueltos, flujo acumulado, burnup de releases, tiempos de ciclo y entrega, tasa de fallo en QA y resueltos por persona |
|
||||
| ⚡ Tiempo real | Tablero, comentarios, actividad, releases y membresías se actualizan solos (GraphQL subscriptions) |
|
||||
| Necesidad | Cómo se resuelve |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🐛 Bugs | Tickets de tipo `BUG` con prioridad, área y etiquetas |
|
||||
| 🌍 Públicos | Un proyecto público se puede leer sin iniciar sesión (web, GraphQL y REST): tablero, tickets, comentarios, releases, actividad y estadísticas; «Explorar» los lista |
|
||||
| 🗑️ Borrar | El propietario borra un proyecto confirmando su clave; tickets, comentarios, releases y enlaces desde otros proyectos se borran en cascada (sagas) y la clave queda libre |
|
||||
| 🧩 Configuración | Cada proyecto tiene sus estados (con categoría, color y motivo obligatorio), transiciones, tipos de ticket y áreas; lo usado se archiva, no se borra |
|
||||
| 🎨 Cambios de UI | Tipo `UI_CHANGE` con enlace al frame de Figma (validado contra `figma.com`) |
|
||||
| ⚙️ Features | Tipo `FEATURE` siguiendo el workflow completo |
|
||||
| 🧪 QA | Por defecto, `READY_FOR_QA → IN_QA → DONE / QA_FAILED`; fallar QA exige un motivo |
|
||||
| 🚀 Releases | Versiones a las que se asignan tickets; no se publican con tickets sin terminar (salvo forzando) |
|
||||
| 👥 Asignaciones | Solo a miembros del proyecto; panel de carga por persona |
|
||||
| 💬 Comentarios | Decisiones y explicaciones dentro del ticket; el autor edita, el admin modera |
|
||||
| 🔗 Relaciones | `BLOCKS`, `DEPENDS_ON`, `TESTS`, `DUPLICATES`, `RELATES_TO`, también entre proyectos (FE ↔ BE ↔ QA) |
|
||||
| 🏠 Inicio | «Mi trabajo»: mis tickets en todos los proyectos, por categoría (QA fallidos primero), lo que espera QA, próximas releases e «Ir a…» por clave |
|
||||
| 📊 Seguimiento | Panel con contadores, flujo por estado, el equipo con sus tickets abiertos (enlaza a su tablero), feed de actividad e historial por ticket |
|
||||
| 📈 Estadísticas | Creados vs. resueltos, flujo acumulado, burnup de releases, tiempos de ciclo y entrega, tasa de fallo en QA y resueltos por persona |
|
||||
| ⚡ Tiempo real | Tablero, comentarios, actividad, releases y membresías se actualizan solos (GraphQL subscriptions) |
|
||||
|
||||
Los **usuarios no se gestionan aquí**: vienen del servidor de identidad **Pireon**
|
||||
([Swagger](https://oauth.pireon.dev/api/documentation)). TaskStream gestiona proyectos, membresías y roles por
|
||||
|
||||
@@ -112,6 +112,7 @@ sequenceDiagram
|
||||
`@AutowiredEvent`) y lo entrega a los `@EventsHandler` registrados para su clase. A diferencia de
|
||||
`eventBus.subject$.next()`, **espera** a cada handler y procesa los eventos en orden; solo hace `XACK` cuando todos
|
||||
terminan. Si uno falla, el evento queda pendiente y se reintenta (y `XAUTOCLAIM` recupera los de instancias caídas).
|
||||
- Después de los handlers, el bridge ejecuta también las **sagas** (ver abajo).
|
||||
|
||||
### Lectura: MongoDB
|
||||
|
||||
@@ -128,6 +129,34 @@ Tras actualizar Mongo, el handler publica en Redis pub/sub (`ChangeFeed`). Las s
|
||||
canales (uno por proyecto, ticket o usuario), así que funcionan con varias instancias de la API. Como se publica
|
||||
**después** de proyectar, quien recibe el aviso siempre lee el estado nuevo.
|
||||
|
||||
### Sagas: reacciones entre agregados
|
||||
|
||||
Cuando un evento de un agregado debe cambiar otros agregados, lo hace una saga (`application/sagas`), con el `@Saga()`
|
||||
estándar de `@nestjs/cqrs`: un observable que filtra eventos (`ofType`) y emite comandos. Como el `EventBus` en memoria
|
||||
nunca ve los eventos guardados (su publisher es el event store), las sagas las ejecuta el **`EventsBridge`**: a cada
|
||||
evento, tras las proyecciones, pasa ese evento solo (`of(event)`) por cada saga y ejecuta los comandos que emite, uno a
|
||||
uno y esperando. Así heredan las garantías del bridge: orden, reintento y nada se pierde si la API se cae a mitad.
|
||||
|
||||
| Saga | Evento | Comandos |
|
||||
| --------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `ProjectDeletionSaga` | `ProjectDeletedEvent` | `DeleteIssueCommand` por cada ticket y `DeleteReleaseCommand` por cada release del proyecto |
|
||||
| | `IssueDeletedEvent` | `PurgeCommentCommand` por cada comentario y `DropLinksToIssueCommand` en los tickets que lo enlazaban |
|
||||
| `ReleaseArchivedSaga` | `ReleaseArchivedEvent` | `DetachIssueFromReleaseCommand` para sus tickets sin terminar (los terminados se quedan) |
|
||||
| `UserDeactivatedSaga` | `UserDeactivatedEvent` (contexto identity) | `UnassignIssueCommand` para sus tickets abiertos en todos los proyectos, como actor `system` («TaskStream») |
|
||||
|
||||
Reglas que cumplen:
|
||||
|
||||
- **A quién afecta sale del event store**, no del read model (`TrackerStreamsRepository`, `ReleaseIssuesRepository`),
|
||||
salvo la saga de usuarios, que busca tickets asignados ahora mismo en Mongo.
|
||||
- **Comandos idempotentes**: un evento puede procesarse dos veces, así que cada comando comprueba si aún hay algo que
|
||||
hacer (`WriteAccess.live()` devuelve `null` si el agregado no existe o ya está borrado; desasignar solo si sigue
|
||||
asignado a esa persona…). Son comandos internos: ningún presenter los expone.
|
||||
- **Nada bloquea el stream**: si el dominio rechaza un comando (no encontrado, regla de negocio…), el bridge lo registra
|
||||
y sigue; los conflictos de concurrencia se reintentan hasta 5 veces y, si persisten, el evento queda pendiente.
|
||||
- **Autoría**: los cambios se atribuyen a quien provocó el evento (quien borró el proyecto o archivó la release).
|
||||
- **Entre contextos**: el webhook de Pireon guarda `UserDeactivatedEvent` en el event store (`EventRecorder`, stream
|
||||
`identity-user-<id>`), y la saga del tracker reacciona a él sin que los contextos se llamen entre sí.
|
||||
|
||||
### Consistencia eventual
|
||||
|
||||
Las mutations devuelven `CommandResult { id, key, version }` en cuanto los eventos están guardados; Mongo se actualiza
|
||||
|
||||
@@ -48,6 +48,26 @@ classDiagram
|
||||
Cualquiera puede salir de un proyecto (salvo el propietario). Los no miembros reciben _not found_, para no revelar que
|
||||
el proyecto existe.
|
||||
|
||||
### Proyectos públicos
|
||||
|
||||
Con `isPublic` (al crear, o después con `updateProject` / `PATCH /api/projects/{project}`, propietario o admin),
|
||||
**cualquiera puede leer** el proyecto, también sin token: el proyecto y sus miembros, tablero, tickets, comentarios,
|
||||
historial, releases (progreso y burnup), actividad, seguimiento y estadísticas. Escribir sigue siendo cosa de los
|
||||
miembros según su rol (sin token: 401; con token pero sin ser miembro: 403). Un usuario con sesión que no es miembro
|
||||
lee igual que un anónimo. Las subscriptions exigen sesión; con ella, también se pueden seguir proyectos públicos.
|
||||
|
||||
`publicProjects` (GraphQL) y `GET /api/explore/projects` (REST, sin token) los listan. Al volver a privado, quien no es
|
||||
miembro vuelve a recibir _not found_. Los enlaces a tickets de otros proyectos solo se muestran si el visitante puede
|
||||
leer esos proyectos.
|
||||
|
||||
### Borrar un proyecto
|
||||
|
||||
Solo el **propietario**, escribiendo la clave del proyecto para confirmar (`deleteProject(projectId, confirmKey)` o
|
||||
`DELETE /api/projects/{project}?confirm=CLAVE`). El proyecto desaparece al momento y, a continuación, una saga borra
|
||||
sus tickets (y sus comentarios), sus releases y los enlaces que tickets de otros proyectos tenían hacia los suyos (ver
|
||||
[Arquitectura](02-arquitectura.md)). La clave queda libre para un proyecto nuevo. Los eventos se quedan en el event
|
||||
store como auditoría, pero desde la API no se puede deshacer.
|
||||
|
||||
### Configuración por proyecto
|
||||
|
||||
Cada proyecto tiene sus **estados**, **transiciones**, **tipos de ticket** y **áreas** (`Project.settings`). Todos
|
||||
@@ -130,12 +150,12 @@ stateDiagram-v2
|
||||
Todo cambio es un evento guardado en el event store (stream = id del agregado). Llevan `actorId`, `occurredAt`,
|
||||
`aggregateId`, `projectId` y `aggregateVersion` (la posición en el stream).
|
||||
|
||||
| Agregado | Eventos |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Project | `ProjectCreatedEvent`, `ProjectUpdatedEvent`, `ProjectSettingsChangedEvent`, `ProjectMemberAddedEvent`, `ProjectMemberRoleChangedEvent`, `ProjectMemberRemovedEvent` |
|
||||
| Issue | `IssueCreatedEvent`, `IssueUpdatedEvent`, `IssueStatusChangedEvent`, `IssueAssignedEvent`, `IssueLinkedEvent`, `IssueUnlinkedEvent`, `IssueReleaseChangedEvent` |
|
||||
| Comment | `CommentAddedEvent`, `CommentEditedEvent`, `CommentDeletedEvent` |
|
||||
| Release | `ReleaseCreatedEvent`, `ReleaseUpdatedEvent`, `ReleaseShippedEvent`, `ReleaseArchivedEvent` |
|
||||
| Agregado | Eventos |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Project | `ProjectCreatedEvent`, `ProjectUpdatedEvent`, `ProjectSettingsChangedEvent`, `ProjectMemberAddedEvent`, `ProjectMemberRoleChangedEvent`, `ProjectMemberRemovedEvent`, `ProjectDeletedEvent` |
|
||||
| Issue | `IssueCreatedEvent`, `IssueUpdatedEvent`, `IssueStatusChangedEvent`, `IssueAssignedEvent`, `IssueLinkedEvent`, `IssueUnlinkedEvent`, `IssueReleaseChangedEvent`, `IssueDeletedEvent` |
|
||||
| Comment | `CommentAddedEvent`, `CommentEditedEvent`, `CommentDeletedEvent` |
|
||||
| Release | `ReleaseCreatedEvent`, `ReleaseUpdatedEvent`, `ReleaseShippedEvent`, `ReleaseArchivedEvent`, `ReleaseDeletedEvent` |
|
||||
|
||||
`IssueStatusChangedEvent` lleva además `meta` con el nombre y la categoría de los dos estados en el momento del
|
||||
cambio, para que el historial y las estadísticas no cambien si luego se renombra o recategoriza un estado.
|
||||
@@ -161,34 +181,37 @@ reconstruye el read model (ver [Despliegue](07-despliegue.md)).
|
||||
|
||||
## 3.2 GraphQL
|
||||
|
||||
Todas las operaciones requieren `Authorization: Bearer <token de Pireon>` (en subscriptions, en el `connection_init`).
|
||||
Las operaciones requieren `Authorization: Bearer <token de Pireon>` (en subscriptions, en el `connection_init`), salvo
|
||||
las lecturas de proyectos públicos (`project`, `issue`, `issues`, `release`, `activity`, `publicProjects` y sus campos),
|
||||
que funcionan también sin token.
|
||||
|
||||
### Queries
|
||||
|
||||
| Query | Devuelve |
|
||||
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `me` | El usuario autenticado, con sus roles en Pireon |
|
||||
| `users(search, limit)` | Usuarios de Pireon (para asignar o añadir miembros) |
|
||||
| `user(id)` | Un usuario |
|
||||
| `myProjects` | Proyectos de los que soy miembro |
|
||||
| `myIssues(assignedToMe, statuses, categories, limit, offset)` | «Mi trabajo»: tickets de todos mis proyectos, asignados a mí (por defecto) o de cualquiera (p. ej. `categories: [QA]`) |
|
||||
| `upcomingReleases(limit)` | Releases planificadas de mis proyectos, por fecha objetivo |
|
||||
| `project(idOrKey)` | Un proyecto con su `settings`. Campos calculados: `board(releaseId, assigneeId, area, perColumn)`, `stats`, `analytics(days)`, `releases` |
|
||||
| `issues(filter, limit, offset)` | Búsqueda de tickets (estado, tipo, área, prioridad, persona, release, etiquetas, texto) |
|
||||
| `issue(idOrKey)` | Un ticket. Campos calculados: `allowedTransitions`, `linkedIssues`, `comments`, `history` |
|
||||
| `release(id)` | Una release. Campos calculados: `progress`, `burnup`, `issues` |
|
||||
| `activity(projectId, issueId?, limit, before?)` | Feed de actividad, del más reciente al más antiguo |
|
||||
| Query | Devuelve |
|
||||
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `me` | El usuario autenticado, con sus roles en Pireon |
|
||||
| `users(search, limit)` | Usuarios de Pireon (para asignar o añadir miembros) |
|
||||
| `user(id)` | Un usuario |
|
||||
| `myProjects` | Proyectos de los que soy miembro |
|
||||
| `myIssues(assignedToMe, statuses, categories, limit, offset)` | «Mi trabajo»: tickets de todos mis proyectos, asignados a mí (por defecto) o de cualquiera (p. ej. `categories: [QA]`) |
|
||||
| `upcomingReleases(limit)` | Releases planificadas de mis proyectos, por fecha objetivo |
|
||||
| `publicProjects` | Proyectos públicos (sin token) |
|
||||
| `project(idOrKey)` | Un proyecto (mío, o público) con su `settings`. Campos calculados: `board(releaseId, assigneeId, area, perColumn)`, `stats`, `analytics(days)`, `releases` |
|
||||
| `issues(filter, limit, offset)` | Búsqueda de tickets (estado, tipo, área, prioridad, persona, release, etiquetas, texto) |
|
||||
| `issue(idOrKey)` | Un ticket. Campos calculados: `allowedTransitions`, `linkedIssues`, `comments`, `history` |
|
||||
| `release(id)` | Una release. Campos calculados: `progress`, `burnup`, `issues` |
|
||||
| `activity(projectId, issueId?, limit, before?)` | Feed de actividad, del más reciente al más antiguo |
|
||||
|
||||
### Mutations
|
||||
|
||||
Todas devuelven `CommandResult { id, key, version }`.
|
||||
|
||||
| Ámbito | Mutations |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Proyectos | `createProject`, `updateProject`, `updateProjectSettings`, `addProjectMember`, `changeProjectMemberRole`, `removeProjectMember` |
|
||||
| Tickets | `createIssue`, `updateIssue`, `changeIssueStatus`, `assignIssue`, `linkIssues`, `unlinkIssues`, `setIssueRelease` |
|
||||
| Comentarios | `addComment`, `editComment`, `deleteComment` |
|
||||
| Releases | `createRelease`, `updateRelease`, `publishRelease(force)`, `archiveRelease` |
|
||||
| Ámbito | Mutations |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Proyectos | `createProject`, `updateProject`, `updateProjectSettings`, `deleteProject(confirmKey)`, `addProjectMember`, `changeProjectMemberRole`, `removeProjectMember` |
|
||||
| Tickets | `createIssue`, `updateIssue`, `changeIssueStatus`, `assignIssue`, `linkIssues`, `unlinkIssues`, `setIssueRelease` |
|
||||
| Comentarios | `addComment`, `editComment`, `deleteComment` |
|
||||
| Releases | `createRelease`, `updateRelease`, `publishRelease(force)`, `archiveRelease` |
|
||||
|
||||
### Subscriptions
|
||||
|
||||
@@ -367,9 +390,10 @@ la consuman o generen un cliente. Son otro adaptador de entrada (`presenters/htt
|
||||
|
||||
### Autenticación: siempre a través de Pireon
|
||||
|
||||
Cualquier app o usuario que la consuma pasa por el OAuth de Pireon: todas las rutas, salvo `health`, `auth` y el
|
||||
webhook, exigen `Authorization: Bearer <token de Pireon>` (el `AuthGuard` global, igual que en GraphQL). El documento
|
||||
declara dos esquemas:
|
||||
Cualquier app o usuario que la consuma pasa por el OAuth de Pireon: todas las rutas, salvo `health`, `auth`, el
|
||||
webhook, `explore` y las lecturas de proyectos públicos, exigen `Authorization: Bearer <token de Pireon>` (el
|
||||
`AuthGuard` global, igual que en GraphQL). En el documento OpenAPI, las rutas donde el token es opcional lo indican con
|
||||
un requisito de seguridad vacío (`{}`). Declara dos esquemas:
|
||||
|
||||
- **`pireon-oauth`** (OAuth2): _Authorization Code + PKCE_ contra `oauth.pireon.dev/oauth/authorize` y, si
|
||||
`PIREON_PASSWORD_GRANT=true`, _password_. El intercambio de tokens va por `POST /api/auth/token` (Pireon no tiene
|
||||
@@ -386,10 +410,11 @@ Proyectos y tickets se pueden indicar por **id o por clave** (`WEB`, `WEB-12`).
|
||||
| Recurso | Rutas |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Usuarios | `GET /api/users/me`, `GET /api/users?search=`, `GET /api/users/{id}` |
|
||||
| Proyectos | `GET/POST /api/projects`, `GET/PATCH /api/projects/{project}`, `PUT …/settings`, `…/board`, `…/stats`, `…/analytics?days=`, `…/activity`, `…/members` (POST, PATCH, DELETE) |
|
||||
| Proyectos | `GET/POST /api/projects`, `GET/PATCH/DELETE /api/projects/{project}` (`DELETE …?confirm=CLAVE`), `PUT …/settings`, `…/board`, `…/stats`, `…/analytics?days=`, `…/activity`, `…/members` (POST, PATCH, DELETE) |
|
||||
| Tickets | `GET /api/me/issues` (mi trabajo), `GET/POST /api/projects/{project}/issues`, `GET/PATCH /api/issues/{issue}`, `PUT …/status`, `PUT …/assignee`, `PUT …/release`, `POST/DELETE …/links`, `GET/POST …/comments`, `GET …/history` |
|
||||
| Comentarios | `PATCH/DELETE /api/comments/{commentId}` |
|
||||
| Releases | `GET /api/me/releases` (próximas), `GET/POST /api/projects/{project}/releases`, `GET/PATCH /api/releases/{id}`, `…/progress`, `…/burnup`, `…/issues`, `POST …/publish`, `POST …/archive` |
|
||||
| Explorar | `GET /api/explore/projects` (proyectos públicos, sin token) |
|
||||
| Sistema | `GET /api/health`, `GET /api/auth/config`, `POST /api/auth/token`, `POST /api/webhooks/pireon` |
|
||||
|
||||
Los filtros de tickets aceptan `status` (ids del proyecto) y `category` (`?category=QA`), repetidos o separados por
|
||||
|
||||
@@ -114,6 +114,7 @@ Qué gana la página:
|
||||
| `AuthFacade` | `AuthStore` | raíz | Sesión, usuario, login (PKCE o email y contraseña de Pireon), renovación del token, restaurar al arrancar |
|
||||
| `ProjectsFacade` | `ProjectsStore` | raíz | Mis proyectos; se actualiza cuando me añaden o quitan (`myProjectsChanged`) |
|
||||
| `HomeFacade` | `HomeStore` | raíz | «Mi trabajo» en la portada: mis tickets abiertos por categoría (QA fallidos primero), esperando QA y próximas releases; se recarga al volver a la pestaña |
|
||||
| `ExploreFacade` | `ExploreStore` | raíz | Proyectos públicos («Explorar»), también sin sesión |
|
||||
| `ProjectFacade` | `ProjectStore` | ruta `/p/:key` | Proyecto, miembros, stats, releases, actividad en vivo, búsqueda de usuarios, configuración (`workflow`, `saveSettings`) y estadísticas (`loadAnalytics`, `loadBurnup`) |
|
||||
| `BoardFacade` | `BoardStore` | ruta `board` | Tablero kanban, filtros, movimiento optimista, nuevo ticket, cambios en vivo |
|
||||
| `IssueFacade` | `IssueStore` | ruta `issues/:issueKey` | Ticket, comentarios en vivo, historial y todas sus acciones |
|
||||
@@ -223,6 +224,18 @@ visualización de la plataforma:
|
||||
la tabla, para que nada dependa del color ni del ratón.
|
||||
- Mientras se recalcula un periodo, se queda la gráfica anterior atenuada.
|
||||
|
||||
### Proyectos públicos y visitantes sin sesión
|
||||
|
||||
- Solo la portada (`/`) exige sesión. `/explorar` y las páginas de proyecto (`/p/:key/…`) no: la API decide qué ve
|
||||
cada visitante, y las páginas ya ocultan lo que no puede cambiar (`canContribute`, `canManage`).
|
||||
- Sin sesión, la cabecera muestra «Explorar» e «Iniciar sesión»; dentro de un proyecto público aparece un aviso de modo
|
||||
lectura, y en uno privado (o inexistente), el enlace para iniciar sesión volviendo a esa página (`returnUrl`).
|
||||
- El tiempo real necesita sesión: `GraphqlRealtimeGateway` no abre subscriptions sin token (si lo hiciera, la API
|
||||
cerraría el WebSocket y el cliente reintentaría sin fin). Los visitantes anónimos ven los datos al cargar la página.
|
||||
- Configuración tiene el interruptor «Público» (propietario y admins) y, solo para el propietario, «Eliminar el
|
||||
proyecto», que exige escribir la clave. Al borrarlo se vuelve a la portada; si otro lo borra mientras lo tienes
|
||||
abierto, la subscription avisa (`ProjectChanged.deleted`) y también se vuelve a la portada.
|
||||
|
||||
## 4.6 Rutas
|
||||
|
||||
| Ruta | Página |
|
||||
@@ -230,6 +243,7 @@ visualización de la plataforma:
|
||||
| `/login` | Inicio de sesión (solo sin sesión) |
|
||||
| `/auth/callback` | Retorno de Pireon (PKCE) |
|
||||
| `/` | Inicio: «Ir a…» (`WEB-12`), mi trabajo (`ts-my-work`) y mis proyectos |
|
||||
| `/explorar` | Proyectos públicos (sin sesión) |
|
||||
| `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros) |
|
||||
| `/p/:key/issues/:issueKey` | Ticket |
|
||||
| `/p/:key/releases` | Releases |
|
||||
@@ -239,5 +253,6 @@ visualización de la plataforma:
|
||||
| `/p/:key/activity` | Actividad en vivo |
|
||||
| `/p/:key/members` | Miembros |
|
||||
|
||||
Todo salvo `/login` y `/auth/callback` está protegido por `authGuard`. Los parámetros de ruta llegan como inputs de
|
||||
Solo `/` está protegida por `authGuard` (redirige a `/login?returnUrl=…`); `/login` y `/auth/callback` son para quien
|
||||
no tiene sesión, y el resto se puede abrir sin ella si el proyecto es público. Los parámetros de ruta llegan como inputs de
|
||||
los componentes (`withComponentInputBinding`).
|
||||
|
||||
@@ -29,7 +29,11 @@ sequenceDiagram
|
||||
- Se cachea el **hash SHA-256** del token, nunca el token en claro.
|
||||
- Si Pireon marca al usuario como inactivo, el token se rechaza.
|
||||
- El guard es global: todo requiere token salvo lo marcado con `@Public()` (health, configuración de login, proxy de
|
||||
tokens y webhook).
|
||||
tokens, webhook y `GET /api/explore/projects`).
|
||||
- `@OptionalAuth()` (o `@ApiOptionalPireonAuth()` en REST) deja pasar sin token: es para las lecturas que permiten los
|
||||
proyectos públicos (proyecto, tablero, tickets, comentarios, releases, actividad, estadísticas). Sin token, el
|
||||
usuario es `undefined` y el visitante es anónimo; un token presente pero inválido sigue dando 401. Las escrituras, las
|
||||
subscriptions y lo «mío» (`myProjects`, `myIssues`, `/api/me/…`) siguen exigiendo sesión.
|
||||
- En subscriptions, el token se valida una vez al abrir el WebSocket (`connection_init`).
|
||||
- `@RequireAppRoles('…')` permite exigir roles del cliente OAuth en Pireon (los admins globales de Pireon los tienen todos).
|
||||
|
||||
@@ -120,7 +124,9 @@ caduque la caché):
|
||||
2. Pon el secret que devuelve en `PIREON_WEBHOOK_SECRET`.
|
||||
|
||||
La API verifica la firma `X-Webhook-Signature` (HMAC-SHA256 del cuerpo), marca al usuario como inactivo y borra sus
|
||||
sesiones cacheadas. Sin `PIREON_WEBHOOK_SECRET`, el endpoint rechaza todas las llamadas.
|
||||
sesiones cacheadas. Además guarda `UserDeactivatedEvent` en el event store, y una saga del tracker deja sin asignar sus
|
||||
tickets abiertos en todos los proyectos (ver [Arquitectura](02-arquitectura.md)). Sin `PIREON_WEBHOOK_SECRET`, el
|
||||
endpoint rechaza todas las llamadas.
|
||||
|
||||
## 5.4 Usuarios en TaskStream
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user