Commit graph

158 commits

Author SHA1 Message Date
6888663148 feat(helcim): add transaction list + card update helpers
- listHelcimCustomerTransactions(customerCode): GET /card-transactions/
  with customerCode filter, sorts newest-first, caps at 50, normalizes
  Helcim status (APPROVED/DECLINED) + type (refund) into
  paid/refunded/failed/other.
- updateHelcimCustomerDefaultPaymentMethod(customerId, cardToken):
  resolves cardToken -> cardId via /customers/{id}/cards, then PATCHes
  /customers/{id}/cards/{cardId}/default.
- updateHelcimSubscriptionPaymentMethod(subscriptionId, cardToken):
  wraps updateHelcimSubscription with a cardToken payload.
- helcimUpdateCardSchema: Zod schema { cardToken: string } for the
  upcoming /api/helcim/update-card route.
- Unit tests for all three helpers (success + error paths).
2026-04-19 16:24:16 +01:00
0ca38e5588 fix(auth): expose helcimCustomerId on /api/auth/member
The member account page gates the Helcim customer portal link on
`memberData.helcimCustomerId`, but this endpoint (the source for
`useAuth().memberData`) omitted the field, so the link hid for every
member regardless of Helcim enrollment. Add the field to the response.
2026-04-19 13:04:46 +01:00
8ceaebb268 fix(helcim): tolerate empty response body on DELETE (204)
helcimFetch called response.json() unconditionally, which threw
"Unexpected end of JSON input" on Helcim's 204 No Content responses
(e.g. DELETE /subscriptions/:id). The error was silently swallowed by
the best-effort cancel path in cancel-subscription, masking cases where
the Helcim-side cancel actually succeeded.
2026-04-18 22:06:33 +01:00
fb337a4277 feat(account): display cadence and annual pricing in tier selector 2026-04-18 18:08:10 +01:00
0eeed94772 feat(contribution): free-to-paid uses cadence plan id, persists billingCadence 2026-04-18 17:37:35 +01:00
e8c81cf062 feat(contribution): paid-to-paid tier swap via recurringAmount PATCH 2026-04-18 17:32:22 +01:00
4b5ea9bbd8 fix(helcim): restore subscriptionStartDate on paid-tier activation 2026-04-18 17:28:05 +01:00
8d43804c7f feat(helcim): create subscription by cadence with recurringAmount
Replace tier-based plan lookup with cadence-keyed lookup, compute
recurringAmount via getTierAmount, persist billingCadence on member.
Delete both manual-fallback blocks; Helcim failure now surfaces as 500.
2026-04-18 17:25:14 +01:00
be0e6e7699 refactor(config): cadence-keyed plan id, add getTierAmount, drop per-tier helcimPlanId 2026-04-18 17:19:05 +01:00
35197c465b feat(schemas): accept cadence field on subscription + contribution updates 2026-04-18 17:16:09 +01:00
47f2d666dd fix(helcim): use Number(id) in wrapped PATCH /subscriptions body 2026-04-18 17:14:24 +01:00
de4bfdcc16 feat(member): add billingCadence field to schema 2026-04-18 17:12:45 +01:00
4f567e9586 refactor(helcim): wrapped PATCH body, first-activation welcome email guard
Moves updateHelcimSubscription to the live-verified wrapped shape
(PATCH /subscriptions { subscriptions: [{ id, ...payload }] }), adds a prior-
status check so sendWelcomeEmail only fires on pending_payment to active
transitions, short-circuits get-or-create-customer when a valid
helcimCustomerId is already on file, and replaces member.save() Slack-status
writes with findByIdAndUpdate({ runValidators: false }) to avoid save-time
validator pitfalls.
2026-04-18 17:06:30 +01:00
15329e3e84 refactor(events): gate member benefits on hasMemberAccess
Extracts hasMemberAccess(member) in tickets.js and uses it across event
registration, ticket purchase, and series purchase flows so guest, suspended,
and cancelled records no longer count as members while pending_payment still
does.
2026-04-18 17:06:17 +01:00
c5e901ed24 feat(signup): community guidelines agreement and policies routes
Introduces /community-guidelines and /policies/{privacy,terms,[slug]} pages,
swaps the signup/invite checkbox from agreedToTerms to agreedToGuidelines,
adds Member.agreement.acceptedAt, and stamps the field when a Helcim
customer is created.
2026-04-18 17:06:10 +01:00
2c834da40a chore: remove placeholder payment block from members/create 2026-04-17 17:16:32 +01:00
3ba633cce2 chore: remove dead guest-register event route
The /api/events/[id]/guest-register endpoint has no production
callers: it's superseded by tickets/purchase.post.js, which
handles guest Member upsert via status:"guest" when
body.createAccount is true. Drops the route file, its
source-assertion tests, guestRegisterSchema, and its validation
coverage.
2026-04-17 16:36:34 +01:00
6f9e6a3d98 feat(events): guest accounts for public event registration
Non-members who register for an event now get a persistent identity:
with consent, a status:"guest" Member is upserted and an auth cookie is
set so the "You're Registered" state survives a page refresh.

Tiered auto-login matches passwordless-auth norms — auto-login is only
safe when the account holds no privileges:
- New email → create guest + cookie
- Returning guest → cookie
- Existing non-guest (active/pending/etc.) → attach ticket only, no
  cookie, confirmation email includes a sign-in link

Guests are gated on status === "guest", so admin/middleware code that
keys on status === "active" naturally excludes them. Guests are also
treated as non-members for ticket pricing/validation to prevent picking
up member-only pricing on their second registration.
2026-04-16 21:23:31 +01:00
7e7672d52b New SiteContent. 2026-04-16 21:11:14 +01:00
02222a5c16 Copy and layout improvements. 2026-04-16 21:11:05 +01:00
39eb9e039a fix(auth): auto-submit OIDC logout form to eliminate xsrf desync
Some checks failed
Test / vitest (push) Failing after 6m9s
Test / playwright (push) Has been skipped
Test / visual (push) Has been skipped
Test / Notify on failure (push) Successful in 2s
Users clicking sign-out in the wiki were getting 'xsrf token invalid'.
The old logoutSource extracted the xsrf from oidc-provider's form into
a separate short-lived cookie and bounced through /auth/logout-confirm,
but that dance kept desyncing — the xsrf on the eventual submit didn't
always match the session state on /oidc/session/end/confirm.

Drop the custom confirmation page and auto-submit oidc-provider's own
form inline from logoutSource. The xsrf stays inside the original form
HTML the provider generated, so the validation is guaranteed to match.
Clicking sign-out in the wiki is already confirmation enough.

Also clear the Ghost Guild auth-token cookie in postLogoutSuccessSource
so signing out of the wiki fully signs the user out rather than leaving
a stale ghostguild.org session behind.
2026-04-15 18:26:51 +01:00
3ad22a8b67 fix(auth): survive missing OIDC interaction cookie on magic-link click
Some checks failed
Test / vitest (push) Failing after 6m13s
Test / visual (push) Has been skipped
Test / playwright (push) Has been skipped
Test / Notify on failure (push) Successful in 3s
Clicking the wiki magic-link email was producing SessionNotFound:
'interaction session id cookie not found' from
provider.interactionFinished, because that call requires the short-lived
_interaction cookie to be present on the request. It isn't, when:

- the user clicks the email on a different device or browser
- the interaction cookie already expired
- the user is in private/incognito browsing

Those unhandled errors previously bounced to /coming-soon via the
coming-soon middleware, stranding users on the pre-register page.

Instead of relying on the interaction cookie at the magic-link step:

1. Verify the JWT, look up the member, set the auth-token cookie.
2. Redirect the user back to https://wiki.ghostguild.org.
3. Outline re-initiates OIDC, which creates a fresh interaction whose
   cookie IS present on the same request, and [uid].get.ts SSOs the user
   in via the auth-token cookie we just set.

Also swap the createError throws for sendRedirect to /auth/oidc-error so
token/member/status failures land on the styled error page rather than
Nitro's default unhandled-error response.
2026-04-15 18:18:33 +01:00
1e9e9c4d97 fix(auth): stop wiki login loop to coming-soon and surface non-member state
Some checks failed
Test / vitest (push) Failing after 6m9s
Test / playwright (push) Has been skipped
Test / visual (push) Has been skipped
Test / Notify on failure (push) Successful in 2s
Members (and pre-registrants) hitting wiki.ghostguild.org were getting bounced
to /coming-soon with a "Pre-Register" link, even when the OIDC flow was
working correctly.

- Allowlist /auth/oidc-error, /auth/logout-confirm, /auth/logout-success,
  and /verify in the coming-soon middleware so OIDC errors and main-site
  magic links stop redirecting to the pre-register page.
- Raise OIDC Interaction TTL from 10m to 15m so it outlives the magic-link
  JWT and legitimate members don't hit expired-interaction errors when they
  click the email a few minutes late.
- Differentiate the "email isn't a registered member" response on the wiki
  login route and show a dedicated "Not a member yet" state with a
  pre-register link and contact email, instead of the misleading
  "Check your inbox" that silently failed.
2026-04-15 17:55:55 +01:00
2394248d53 Updates
Some checks failed
Test / vitest (push) Failing after 6m9s
Test / visual (push) Has been skipped
Test / playwright (push) Has been skipped
Test / Notify on failure (push) Successful in 2s
2026-04-15 17:45:09 +01:00
28040f44f4 refactor(board): atomic delete + query limit + composable cleanup
Some checks failed
Test / vitest (push) Failing after 7m17s
Test / playwright (push) Has been skipped
Test / visual (push) Has been skipped
Test / Notify on failure (push) Successful in 1s
Delete uses findOneAndDelete with author match (no TOCTOU window);
existence check only runs on miss to distinguish 403 vs 404. Posts
list capped at 200. Drop unused resolveTagChannel and refreshParams;
route slack URL building through the composable's slackUrl helper.
2026-04-15 12:47:53 +01:00
7292b11c0b feat(member): account/profile polish + tier upgrade flow
- Timezone: curated USelectMenu dropdown (app/config/timezones.js), preserves unknown saved values
- Profile save now uses useToast() for success/error; remove inline save banner
- Nav onboarding dot nudged down 1px for optical alignment with lowercase text
- Onboarding: skip a suggestion with POST /api/onboarding/track {skip}; member.onboarding.skipped map; does not affect graduation
- CirclePicker takes :saved-value so 'Current' badge stays until save completes
- PrivacyToggle is binary (USwitch labeled Private); member schema enum reduced to ['members','private']; zod coerces legacy 'public'
- New /member/payment-setup page: HelcimPay $0 verify + update-contribution, wired from account.vue via requiresPaymentSetup redirect
- Helcim portal: NUXT_PUBLIC_HELCIM_PORTAL_URL env + account.vue 'Manage billing in Helcim' link
- Migration script: scripts/migrate-privacy-public-to-members.js
2026-04-14 20:35:37 +01:00
9a560f2a3b feat(board): redesign classifieds + Slack channel creation
Adds AdminGhost bot token for admin-only Slack channel creation, refreshes
BoardPostCard/Form layouts, and expands admin board-channels management.
2026-04-14 20:20:17 +01:00
1fc937a26a refactor(board): delete old board routes, absorb slackHandle into profile PATCH
- Delete server/api/members/me/board.patch.js and server/api/board/suggestions.get.js
- Add boardSlackHandle to memberProfileUpdateSchema; remove boardPrivacy
- profile.patch.js: write boardSlackHandle -> board.slackHandle; drop boardPrivacy
- Remove privacy.board field from Member model
- onboarding/status.get.js: hasProfileTags now requires only craftTags; hasEngagedBoard uses BoardPost.exists
- onboarding/track.post.js: graduation check uses BoardPost.exists instead of board.topics elemMatch
- members/[id].get.js and directory.get.js: reduce board response to slackHandle only; drop connectionTag and peerSupport filters
2026-04-14 16:29:45 +01:00
6a440a846d feat: board post + channel API routes
Implements Phase 2a of board classifieds redesign:

- GET/POST /api/board/posts (list with tag/author filters, create)
- PATCH/DELETE /api/board/posts/:id (author-only)
- GET /api/board/channels (member)
- POST /api/admin/board-channels (admin)
- PATCH/DELETE /api/admin/board-channels/:id (admin)

Adds board_post_created activity type.
2026-04-14 16:25:42 +01:00
8e5f4a2d7c add unique index on slackChannelId in BoardChannel model 2026-04-14 16:23:23 +01:00
1da59021a3 feat(board): add BoardPost + BoardChannel models and zod schemas
- Add BoardPost model (author, title, seeking/offering, note, tags) with
  validator requiring at least one of seeking/offering
- Add BoardChannel model (name, slackChannelId, tagSlugs)
- Add boardPost/boardChannel create+update Zod schemas
- Trim Member.board subdoc to only slackHandle (drop topics, details,
  offerPeerSupport, availability, personalMessage)
- Remove old boardUpdateSchema
2026-04-14 16:21:04 +01:00
a0f60bcdc0 fix: rename hasEngagedEcology → hasEngagedBoard in onboarding status, clean up stale ecology references 2026-04-14 12:25:24 +01:00
091ec58073 rename communityEcology → board across backend
Model, schemas, API routes, activity log, and all server handlers
updated. Old ecology/ and community-ecology routes removed, new
board/ routes added. Tests updated and new board-suggestions tests
written (10 cases).
2026-04-14 12:00:15 +01:00
59d6e97787 Member/Ecology revamp.
Some checks failed
Test / vitest (push) Failing after 7m23s
Test / playwright (push) Has been skipped
Test / visual (push) Has been skipped
Test / Notify on failure (push) Successful in 2s
2026-04-14 09:25:09 +01:00
c6b970a621 Design token updates.
Some checks failed
Test / vitest (push) Successful in 10m47s
Test / playwright (push) Failing after 9m11s
Test / visual (push) Failing after 9m11s
Test / Notify on failure (push) Successful in 2s
2026-04-11 23:24:38 +01:00
de3bcc479a fix(auth): rewire OIDC logout/error flow through Nuxt pages
Some checks failed
Test / playwright (push) Blocked by required conditions
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
Test / vitest (push) Has been cancelled
Migrate three render callbacks in oidc-provider (logoutSource,
postLogoutSuccessSource, renderError) from the baked guildPageShell
helper to Nuxt pages under app/pages/auth/, so they go through the
font module and design system instead of a shadow copy.

- Delete guildPageShell (~103 lines of shadow design system).
- Add /auth/logout-success, /auth/oidc-error, /auth/logout-confirm
  pages built on dashed-box + btn + main.css tokens.
- renderError now allow-lists error + error_description into query
  params and lets Vue default interpolation escape them, closing an
  XSS where OIDC error fields were concatenated into raw HTML.
- logoutSource extracts the xsrf from oidc-provider's stable form
  output, sets it as an httpOnly 2-minute cookie, and redirects to
  /auth/logout-confirm. The confirm page reads the cookie during SSR,
  persists the value to useState, and clears the cookie so it's
  strictly one-time use. Defensive fallback keeps the raw auto-submit
  form if oidc-provider ever changes its form format.
- Fix form actions emitting http:// in production at the root cause:
  oidc-provider extends Koa but calls super() with no args, so
  app.proxy defaults to false and ctx.protocol ignores
  X-Forwarded-Proto. Set _provider.proxy = true after construction;
  remove the bogus proxy:true config key (silently ignored) and the
  form.replace('http://', 'https://') symptom patch. Make the
  x-forwarded-proto override in the catchall conditional on
  production + missing header (was unconditional + dead code).
- Add site-wide .btn:focus-visible rule in main.css for WCAG 2.4.7.

Verified in browser: Brygada 1918 loads on all three pages, contrast
ratios pass AA in dark + light, XSS payload escapes to text nodes
only, Set-Cookie: Max-Age=0 enforces one-time xsrf use, no
horizontal overflow at 500px, no console errors.
2026-04-11 23:21:46 +01:00
50a358b294 feat(wiki): add batch tag remove mode to admin wiki page
Add add/remove toggle to batch tag picker. Clean up unused requireAdmin
import from wiki sync route.
2026-04-09 23:52:00 +01:00
a516f172fb refactor: extract escapeRegex and validateTagSlugs server utils
Deduplicate tag validation and regex escaping into shared auto-imported
utils. Add tag validation to wiki patch/batch-tag routes. Remove
duplicate tags field from event schema.
2026-04-09 23:51:56 +01:00
22530ac1e3 Merge branch 'worktree-agent-a2b84f8b' 2026-04-09 22:38:36 +01:00
20c961113d Merge branch 'worktree-agent-ac00ecc9' 2026-04-09 22:38:36 +01:00
8b2f6d5240 Merge branch 'worktree-agent-abf17134' 2026-04-09 22:38:36 +01:00
337664790f feat(events): add tag selector to admin event form 2026-04-09 22:38:20 +01:00
e4f2efd6d0 feat(wiki): add admin wiki management API routes 2026-04-09 22:36:44 +01:00
d3a5c1a3a7 feat(wiki): add tag-based wiki recommendations API 2026-04-09 22:36:19 +01:00
3a22a327fe Merge branch 'worktree-agent-a0ee41bb' 2026-04-09 22:34:09 +01:00
4a475ca5ba Merge branch 'worktree-agent-a54bb856'
# Conflicts:
#	server/models/wikiArticle.js
2026-04-09 22:34:09 +01:00
bda0fe6eb7 Merge branch 'worktree-agent-acfdfab5'
# Conflicts:
#	server/models/event.js
2026-04-09 22:33:54 +01:00
abca0fb7d6 Merge branch 'worktree-agent-a5a0c7d9' 2026-04-09 22:33:41 +01:00
b93c735442 Merge branch 'worktree-agent-a53b58a7' 2026-04-09 22:33:41 +01:00
905b5155e2 feat(wiki): add Outline utility and wiki sync API 2026-04-09 22:33:06 +01:00