Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and post-commit watch notifications.
- `GET /v6/forums/health`
- `GET /v6/forums/readiness`
- `GET /v6/forums/api-docs`
- `GET /v6/forums/public/categories`
- `GET /v6/forums/public/topics`
- `GET /v6/forums/topics`
- `GET /v6/forums/topics/challenges/:challengeId`
- `GET /v6/forums/topics/:topicId/children`
Expand Down Expand Up @@ -141,6 +143,14 @@ PORT=3000
Health and readiness checks intentionally remain DB-only; they do not validate
event-bus, SendGrid template, producer, or outbound Auth0 readiness.

## Public forums

See [Public forums and migration](docs/public-forums.md) for guest reads, category
ACLs, target-member mapping, insert-only Vanilla import and cutover checks.
Public and general read routes accept anonymous callers, while mutations and
challenge lists still require authentication. Restricted category content is
filtered before statistics, search, pagination, watches and notifications.

## Authentication

Bearer tokens are validated with `tc-core-library-js` and normalized before guards
Expand Down
87 changes: 87 additions & 0 deletions docs/public-forums-dev-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Dev public-forums migration verification

Executed on 7 October 2026 (Australia/Hobart), using production Vanilla as a
read-only source and the AWS dev forums/Member databases as the target.
Production databases were not written to.

The API was deployed before importing restricted categories. The dev service
`forums-api-v6` is on task revision 18, using code commit `90f244a` and image tag
`public-forums-90f244a`. ECS reached a stable deployment and live public reads
return `Cache-Control: private, no-store`.

## Imported data

| Data | Rows |
| --- | ---: |
| Categories | 57 |
| Discussion threads | 1,355 |
| Starter posts and comments | 3,756 |
| Closure relationships | 4,295 |
| Mapped forum-only role memberships | 139 |
| Watches | 619 |
| Read states | 11,162 |
| Total imported data rows | 21,440 |

The immediate second apply found all 21,440 rows and inserted **zero** rows.
The dry-run, apply and rerun plan digest was:

`aeccd893e9d6486b0750dea69d9e00a51374772c309d3c9da08f342155dcab5b`

There were 1,079 source author IDs without a matching dev member. Their original
handles and content were preserved using non-login legacy IDs. 16,013 source
state records belonged to members absent from dev and were omitted. Production
mapping must be rerun against production Members; dev IDs must not be reused.
No native Vanilla media or reactions required copying. All 99 Filestack links
in the source post bodies were preserved unchanged.

## Existing challenge data

These counts and SHA-256 fingerprints were identical in dry-run, before and
after apply, and after the no-op rerun:

| Table | Rows | SHA-256 |
| --- | ---: | --- |
| Topic | 6,437 | `3dae055b315e049750e2aff4fbbed47cdfcd24f009792235709d950a3a012f83` |
| Post | 6,484 | `c0967e56431ddddbcdd832ad7e6f2cfa70274c9557781dcffdfd9dd4b394a5d3` |
| TopicWatch | 69 | `1e9c7c8269d3c4e6f6ecfefe7617e845f0f7b93f41f7c89e0e4d90ef90e84134` |
| TopicReadState | 6,522 | `ac0b498e58c2a643303783e8ba12d65a028487b474062a9c55bffcdf164ce21b` |
| TopicClosure | 6,437 | `2d1c882cb7691562120e1222254ecbf66500eb7f0924a216d951d7abc4079a9c` |

## Live dev verification

Read-only probes used short-lived dev tokens for a synthetic QA principal; no
member roles, posts, watches or read states were created by the probes. Category
IDs and topic totals matched an independently computed source ACL projection:

| Reader | Visible categories | Visible threads |
| --- | ---: | ---: |
| Guest | 30 | 1,111 |
| Member | 48 | 1,286 |
| Copilot | 52 | 1,323 |
| Reviewer | 49 | 1,288 |

Copilot and Reviewer Discussions returned 403 to guests, ordinary members and
the other role, and 200 to the required role. Invalid tokens returned 401.
Anonymous Watching returned zero results. Measured catalog requests completed
in approximately 1.4–3.5 seconds including network latency from the test host.

The API lint/build, production Docker build and 167 tests passed. Local real
PostgreSQL/HTTP tests cover posting, replies, reactions, watches, nested private
membership, migration conflicts and reruns without sending notifications.

## Remaining production gates

The [UI PR](https://github.com/topcoder-platform/platform-ui/pull/2444) contains
the new app and passed lint, dev/prod builds and 58 focused regression tests.
Connected Chrome verification covered desktop/mobile Figma layouts, category
navigation, search/pagination, guest login return URLs, signed-in composers,
live member suggestions and Markdown preview. The approved generated text
attachment uploaded to Filestack, inserted an HTTPS Markdown link, and rendered
in preview. No test posts were submitted to migrated discussions. Accept the dev
data/role mappings before production cutover. Follow
[the migration runbook](public-forums.md); production has not been migrated.

Full owner-readable reports were retained outside the repository at
`/tmp/public-forums-dev-{dry-run,applied,rerun}.json` and
`/tmp/public-forums-live-access.json`. No credentials, post contents or member
lists are included in this document.
129 changes: 129 additions & 0 deletions docs/public-forums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Public forums

Public categories and threads use the same Topic, Post, TopicClosure, watch,
read-state, moderation, reaction and notification services as challenge forums.
The public catalog is a projection over that engine, not a second post store.

## HTTP contract

- `GET /v6/forums/public/categories` returns the visible category tree, descriptions,
ordering, authorized subtree counts/participants, latest activity and `canCreate`.
- `GET /v6/forums/public/topics` accepts `categoryId`, `search`, `watching=true`,
`sort=active|recent|oldest`, `page` and `perPage` (maximum 100). Search includes
non-deleted post content. Visibility is applied before totals and pagination.
Identity-dependent read responses send `Cache-Control: private, no-store`.
- Existing general topic list, children and detail GET routes accept guests.
Challenge-specific lists still require authentication, and challenge visibility
still requires the existing challenge/resource checks. Supplied invalid tokens
are rejected; a missing token creates a non-elevated anonymous principal.
- Detail includes `permissions` for create-post, create-topic, watch, update and
delete. Clients should also respect `topic.locked`.
- Existing topic/post commands implement creation, replies, edits, reactions,
watches and mark-read. Every mutation requires authentication and policy checks.
Creating a public thread uses `parentTopicId` with the category ID and no
`challengeId`. Ordinary members cannot create category roots or delete content.

`PublicForumCategory` holds ordered metadata and separate `readRoles`,
`createRoles` and `replyRoles` arrays. Entries within an array are alternatives;
every category ancestor must allow reading. `$public` permits guests,
`$authenticated` permits members, and other entries match normalized token roles
or imported forum-only membership rows. Empty arrays deny ordinary members.
Administrators and appropriately scoped machines retain elevated access.
Category containers cannot receive threads or posts. Threads cannot receive
child threads. Watches on categories cover descendant threads through the
existing closure-based notification recipient lookup. Recipient authorization
checks the category ACL again at delivery time.

`PublicForumRoleMember` preserves Vanilla-only memberships without granting
Identity API roles or access to any other application. Copilot Discussions and
Reviewer Discussions explicitly require `copilot` and `reviewer`, respectively.
Other Private descendants retain their source permission categories and custom
memberships. A private ancestor cannot be bypassed by a permissive child.

## Insert-only migration

The additive Prisma migration creates three public metadata/provenance tables.
It does not alter or reset existing Topic/Post/challenge rows. Deploy the new API
**before importing**: older API versions do not understand public category ACLs.
Do not roll the service back to an older policy implementation while imported
restricted content is present. Roll forward with a fix instead.

The public importer is separate from `import:vanilla` (the challenge importer).
It reads production MySQL in a read-only, repeatable-read consistent snapshot.
It excludes the challenge category subtree, all group-linked ancestry and the
obsolete empty Groups container. A group-linked discussion inside an otherwise
public category aborts the import for review.

Preserved data includes category ancestry/order/descriptions, thread titles,
starter posts, nested replies, source timestamps, announcements, locks, author
handles, Markdown/mention/attachment URLs, known-member bookmarks, category
follows and read states. The two observed Rich-format source posts are converted
from Quill text/link operations; unsupported formats or embeds stop the import.
Referenced native Vanilla Media rows also stop the import until their files have
an explicit copy plan. The audited source has no referenced native media and no
reactions; its 99 Filestack links remain unchanged.

Authors are mapped by unique lowercase handle against **target-environment**
Members data. Missing authors keep their source handle and a non-login
`vanilla:<id>` identity. Missing target members' watches/read states are omitted
and reported; they must never be attached to a coincidentally equal numeric ID.
Only mapped target members receive forum-only role memberships. Global Vanilla
member/IP bans and challenge data are not imported. Existing view metrics remain
the shared API's distinct-reader counts, rather than Vanilla's cumulative hit
counter. Source-wide user identities/credentials are not copied.

Deterministic `pf…` IDs and immutable provenance make an unchanged rerun a no-op.
The importer refuses unowned ID collisions, changed source projections, changed
target content, invalid reply parents and cyclic ancestry. It never overwrites a
user edit. All inserts occur in one serializable transaction with an advisory
lock. Challenge Topic, Post, TopicClosure, watch and read-state fingerprints must
match before and after or the transaction rolls back. Dry runs use read-only
transactions and make no data changes.

### Commands

Run `nvm use` in this project, install with `pnpm install --frozen-lockfile`, and
run `pnpm build`. Supply credentials through the environment or a secrets manager:

```sh
# VANILLA_DB_URL points to the authorized production Vanilla source.
# FORUMS_DATABASE_URL and MEMBER_DB_URL both point to the intended target tenant.
# PUBLIC_IMPORT_TARGET_HOST must exactly match both target URL hostnames.
pnpm import:vanilla:public --report=/secure/path/public-forums-dry-run.json
pnpm import:vanilla:public --apply --report=/secure/path/public-forums-applied.json
pnpm import:vanilla:public --apply --report=/secure/path/public-forums-rerun.json
```

Reports are owner-readable files containing counts, a plan digest, missing source
author IDs and challenge fingerprints, without credentials or post content. UTC
is used for migration timestamps. Keep reports outside the repository. Check
that dry-run counts match source inventory, apply fingerprints match and the
immediate rerun inserts zero rows. If a source/target changes later, the conflict
requires reconciliation; the tool does not silently refresh already imported
content. Preserve the original reports to distinguish subsequent user activity.

For production cutover, freeze Vanilla posting, repeat the source/target audit
against production Members, deploy the API first, run dry-run/apply/rerun, verify
anonymous/member/copilot/reviewer/private access and then switch the UI entry
point. Production import is a separate step after dev testing is accepted.

## Verification

`pnpm lint`, `pnpm build` and `pnpm test --runInBand` run normal checks. The real
PostgreSQL/HTTP suite also verifies anonymous read-only behavior, private search
and watch filtering, token errors, role/local-membership boundaries, member
commands, full-post search, migration reruns, conflict rollback and unchanged
challenge fingerprints.

Use a disposable local database whose name ends in `_http_test`, apply Prisma
migrations, then run:

```sh
PUBLIC_FORUMS_TEST_DATABASE_URL=postgresql://postgres@127.0.0.1:55439/public_forums_http_test \
pnpm test --runInBand
```

That suite truncates its dedicated local test database, rejects remote hosts and
other database names, uses real SQL and policies, and substitutes JWT validation
and email publishing so it cannot notify real users. The standalone unit suite
runs without a database.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"import:vanilla:public": "node dist/scripts/public-vanilla-import.js"
},
"dependencies": {
"@nestjs/common": "^11.1.28",
Expand Down
Loading
Loading