Skip to content

Fix batch: reminders, password reset, honeypot, CalDAV, Zoom docs + MCP bearer - #57

Merged
shockalotti merged 7 commits into
mainfrom
fix/mcp-bearer-archived
Sep 17, 2026
Merged

shockalotti merged 7 commits into
mainfrom
fix/mcp-bearer-archived

Conversation

@shockalotti

Copy link
Copy Markdown
Contributor

Six commits, all already verified in production on booking.orchestratr.ai:

Closes #48. Closes #34.

…freshed

VerifyMCPBearer's OAuth branch never checked users.archived_at, so
archiving a member — the documented offboarding path — left their
connected agent bearer working, while sessions and API keys already
filtered on it. The refresh grant had the same gap: it would mint fresh
tokens for a dead account. Both queries now join users and require
archived_at IS NULL, mirroring the API-key fallback directly below.

Pinned by TestMCP_OAuthBearerRejectedWhenArchived (red-green verified
against the unfixed code on both assertions) and one ARCHITECTURE §6
sentence stating the invariant.
sendReminder joined users on et.user_id, so multi-host teams got
reminders naming the event-type creator instead of the booked host.
Join on bookings.host_id. loadCancellationData had the same shape,
so cancellation/reschedule emails use the booking host too.

Pinned by TestWorker_reminderNamesAssignedHost.
The login page linked /admin/forgot-password, which never existed. Add
POST /v1/auth/password/forgot (enumeration-safe, background send, skips
archived and email-login-disabled accounts) and POST
/v1/auth/password/reset (single-use hashed token, 1h TTL, revokes all
sessions, signs straight in), mirroring the magic-link flow, plus the
two public SPA pages.

Pinned by nine tests in password_reset_test.go.
Browsers map a field named company to the organization autofill entry
(ignoring autocomplete=off) and fill it for real humans, who are then
rejected as bots with 400 invalid submission. Rename to hp_extra on the
booking page and embed widget; the stale company key is now an ignored
unknown field, so old embeds keep working.
Discovery picked one VEVENT collection and ListCalendars reported only
that; conflict checks never read any other collection even when selected.
Now discovery returns all collections (seeded with server-side names),
ListCalendars lists them live with a stored-selection fallback, and
conflict checks REPORT every selected collection. The picker destination
(Book) is clearable and the Check/Book headers say what they mean.
An unpublished Zoom app only authorizes same-account users, and Zoom
refuses outsiders before Calnode is involved, so member connect failures
are policy, not a bug. Document the ranked options (built-in video first)
and say so on the settings page, where the old step 2 implied any team
member could connect.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

A successful password reset revokes sessions but leaves other unused reset tokens valid, so an earlier unused email can still take over the account.

Reviewed changes

This run reviewed all 25 files across the six commits (MCP archive, reminders, password reset, honeypot, CalDAV multi-calendar, Zoom docs).

  • MCP bearer offboarding. VerifyMCPBearer and refresh now join users and refuse archived accounts; the test rotates first so it asserts against the live pair.
  • Reminder host name. reminder.send and cancellation loadCancellationData load the assigned bookings.host_id instead of the event-type owner.
  • Self-service password reset. Hashed single-use tokens, generic 200 on forgot, login-rate-limited routes, and admin pages behind isPublicRoute.
  • Honeypot rename. Booking JSON/form/embed field is hp_extra so browser organization autofill no longer trips the bot check.
  • CalDAV every collection. Discovery seeds all VEVENT calendars, free/busy REPORTs each selected URL, live ListCalendars uses stored caldav_home_url, and the picker destination control is a clearable checkbox.
  • Zoom unpublished-app limit. Settings warning plus docs/ZOOM.md playbook.

ℹ️ Nitpicks

  • Poll already deletes expired/used magic_link_tokens and does not sweep the new password_reset_tokens table, so used and expired reset rows accumulate until someone adds the sibling DELETE.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Grok | 𝕏

// Unauthenticated request, so there is no current session to preserve — revoke
// everything (a compromised session must not survive the reset), then sign in.
if _, err := h.db.ExecContext(r.Context(),
`DELETE FROM sessions WHERE user_id = ?`, userID); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A successful reset deletes sessions but leaves every other unused password_reset_tokens row valid until expires_at. sendPasswordReset always INSERTs, so three forgot-password clicks yield three live links; using one does not kill the others, and a later unused email can set a new password and revoke the session this handler just created.

Technical details
# Invalidate outstanding reset tokens on success

## Affected sites
- `internal/handler/password_reset.go:161` — session revoke does not touch other reset tokens
- `internal/handler/password_reset.go:73` — each forgot request inserts another live token

## Required outcome
- After a password is actually changed, no previously issued reset token for that user can succeed
- A test that seeds two tokens, consumes one, and asserts the second returns 400

## Suggested approach
After the password UPDATE succeeds, `DELETE FROM password_reset_tokens WHERE user_id = ?` (or set `used_at` on all of them) before creating the new session.

# Conflicts:
#	frontend/src/routes/settings/zoom/+page.svelte

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this merge. The prior password-reset sibling-token finding is still open.

Reviewed changes

This run reviewed the merge of main (eb04dca) onto d4cb4f2 (commit 11f7271).

  • Dropped MCP-archive from this PR. That work already landed as #49, so VerifyMCPBearer, refresh, and the ARCHITECTURE.md offboarding note no longer appear in this diff.
  • Resolved Zoom-docs overlap with #52. docs/ZOOM.md now points at DEPLOY.md §8 for Zoom-side distribution instead of duplicating those options.
  • Kept this PR's settings warning. The Zoom settings page still carries the multi-member banner and now links VIDEO.md next to main's unpublished-app wording.

Pullfrog  | Fix it ➔ | View workflow run | Using Grok | 𝕏

@shockalotti
shockalotti merged commit 8c3cb5d into main Sep 17, 2026
5 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

1 participant