Skip to content

feat: add org member invite flow via email#107

Open
engineering-props-to wants to merge 3 commits intomainfrom
feat/proaaa-14-org-member-invite
Open

feat: add org member invite flow via email#107
engineering-props-to wants to merge 3 commits intomainfrom
feat/proaaa-14-org-member-invite

Conversation

@engineering-props-to
Copy link
Copy Markdown
Contributor

Summary

  • Adds email-based invitation flow so org admins can invite users who haven't yet signed up
  • New OrganizationInvite Prisma model with unique token, 24h expiry, role, revoke/accept lifecycle
  • Invite dialog on the admin members page (multi-email, role selector, optional personal message)
  • Pending invitations table with resend/revoke actions
  • Auth app /invite?token=... accept route — handles both new and existing users, redirects to org after join

Test plan

  • Admin can open Invite Members dialog from /org/[slug]/admin/members
  • Enter one or more comma-separated emails, select role (Member/Admin), optional message, submit
  • Invitation email is received with a unique join link (check MailDev at http://0.0.0.0:1080)
  • Clicking link as unauthenticated user redirects to sign-in, then auto-joins org after auth
  • Clicking link as already-authenticated user immediately joins org and redirects
  • Pending invitations appear in members table with expiry date
  • Resend refreshes expiry and sends a new email
  • Revoke removes invite from pending list
  • Duplicate invite for same email blocked with "Already invited" error
  • Invalid email format shows validation error
  • Org with allowUserInvites = false returns error from action
  • Run pnpm --filter @propsto/data db-migrate to apply the migration

Closes PROAAA-14

🤖 Generated with Claude Code

Adds email-based invitation for org admins to invite users who haven't yet signed up.

- Add OrganizationInvite Prisma model with unique token, expiry, revoke/accept lifecycle
- Repo layer: createInvite, getByToken, listPending, revoke, resend, accept, getByEmail
- Email: OrgInviteEmail template + sendOrgInviteEmail sender
- Server actions: inviteMembers, revokeInvite, resendInvite (gated by verifyOrgAdminAccess + allowUserInvites setting)
- UI: InviteDialog (multi-email, role select, optional message) + PendingInvites table (resend/revoke)
- Members page: adds Invite Members button and pending invites section for admins
- Auth app: /invite?token=... page validates token, accepts invite, adds user to org, redirects
- Migration: 20260322000000_add_organization_invite

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@github-actions
Copy link
Copy Markdown

🚀 App preview deployment successfull at https://app.pr-107.props.build

@github-actions
Copy link
Copy Markdown

🛑 Web preview deployment failed — View details

@github-actions
Copy link
Copy Markdown

🚀 Auth preview deployment successfull at https://auth.pr-107.props.build

Kit and others added 2 commits March 22, 2026 01:29
The react-email export tool renders templates with no arguments to generate
static previews. Add defaults for all required props to prevent the
`role.toLowerCase()` crash.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@github-actions
Copy link
Copy Markdown

🚀 Auth preview deployment successfull at https://auth.pr-107.props.build

@github-actions
Copy link
Copy Markdown

🚀 Web preview deployment successfull at https://web.pr-107.props.build

@github-actions
Copy link
Copy Markdown

🚀 App preview deployment successfull at https://app.pr-107.props.build

@github-actions
Copy link
Copy Markdown

E2E Tests Failed

Tested against:

View details

@github-actions
Copy link
Copy Markdown

🚀 Auth preview deployment successfull at https://auth.pr-107.props.build

@engineering-props-to
Copy link
Copy Markdown
Contributor Author

QA Review — org member invite flow (PR #107)

What Was Tested

UI renders cleanly, error states are all handled gracefully, and the auth redirect flow is correct.


Code Review Findings

🐛 Bug — Unique constraint will crash re-invites silently

The OrganizationInvite schema has @@unique([organizationId, email]). This means you can never create a second invite for the same email in the same org — even after the first invite expired, was revoked, or was accepted.

In inviteMembersAction (apps/app/src/app/(dashboard)/org/[orgSlug]/admin/members/actions.ts:80–87), the "Already invited" check only blocks when the existing invite is still active (not expired, not accepted, not revoked). For any other state, it falls through and calls createOrganizationInvite, which hits the unique constraint and fails with a Prisma error. This surfaces to the user as a generic "Failed to create invite" instead of anything actionable.

Fix: Use upsert in createOrganizationInvite, or delete the old record before creating, or relax the unique constraint to only one active invite per org+email.

⚠️ Security — Revoke and resend don't verify org ownership

revokeInviteAction and resendInviteAction verify the caller is an admin of orgSlug, but never check that inviteId belongs to that org. An admin of Org A could pass a known invite ID from Org B and revoke or resend it.

Fix: Add organizationId to the where clause in revokeOrganizationInvite and resendOrganizationInvite in packages/data/repos/organization-invite.ts.

⚠️ Resend doesn't rotate the token

resendOrganizationInvite only updates expiresAt and clears revokedAt. It does not generate a new token. If an admin resends because the previous link was forwarded to the wrong person, the old link remains valid.

Fix: Include a new token: cuid() in the update data when resending.


Decision

Changes needed — the UI and auth flows look solid, but there are two security issues and a functional bug (re-invite crash) that should be fixed. I'll file follow-up issues for each.

QA Engineer — automated review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant