Skip to content

feat(admin-teams): shareable invite link #232

@sage-ali

Description

@sage-ali

Context

Derived from the audit in #205. Depends on #228 (schema — team_link_invitations table) and #231 (magic link acceptance flow).

A second invite mode alongside email invites: a role-scoped link that any admin can copy and share. Anyone who receives the link can join — but must prove email ownership first (two-step flow). The link can be revoked and expires after 7 days.

What's needed

New entity + model action

TeamLinkInvitation entity mapped to team_link_invitations (created in #228).
TeamLinkInvitationModelAction with:

  • findActiveByTokenHash(hash) — active, non-expired row
  • findActiveByTeam(teamId) — current active link for a team
  • revokeLink(linkId) — sets status = 'revoked'

New endpoints

POST /admin/teams/:teamId/invite-link — generate a shareable link

  • If an active non-expired link already exists for the team, revoke it first
  • Generate 32-byte token, store SHA-256 hash in team_link_invitations with 7-day expiry
  • Returns { link: "${FRONTEND_URL}/join?token=<rawToken>", expiresAt }

GET /admin/teams/:teamId/invite-link — get the current active link

  • Returns the active link if one exists, 404 if none

DELETE /admin/teams/:teamId/invite-link/:linkId — revoke and delete a link

  • Sets status = 'revoked', returns 200

POST /admin/teams/invite-link/verify-email — step 1 of shareable link acceptance

  • Accepts { token: string, email: string } (public endpoint, no JWT)
  • Hashes token, looks up active non-expired team_link_invitations row
  • Checks the email is not already a team member
  • Generates a short-lived (15 min) email-invite token, stores hash in team_invitations with via = 'link' and the link_invitation_id reference
  • Sends magic link email to the provided address: ${FRONTEND_URL}/accept-invite?token=<rawToken>&via=link
  • Silent 200 on invalid token (no enumeration)

POST /admin/teams/invitations/accept — update to handle via=link

Acceptance criteria

  • Generating a new link revokes any existing active link for that team
  • Link rejects after 7 days
  • Link rejects after revocation
  • verify-email does not reveal whether the shareable token is valid or invalid in its response
  • A user already on the team cannot re-join via a shareable link
  • Full two-step flow works end to end: generate link → enter email → receive magic link email → accept → logged in with force_password_change

Depends on

#228, #231

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions