feat: add invitation link limits
This commit is contained in:
82
.trellis/tasks/07-03-invite-link-limits/design.md
Normal file
82
.trellis/tasks/07-03-invite-link-limits/design.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Invite Link Limits Design
|
||||
|
||||
## Architecture
|
||||
|
||||
This change spans the persisted invitation model, the invitation create API, the registration consumption transaction, and the organization settings UI.
|
||||
|
||||
The invitation row remains the source of truth. The existing `expires_at` column continues to represent validity. New persisted counters will represent usage limits:
|
||||
|
||||
- `max_uses`: maximum successful invitation registrations allowed.
|
||||
- `used_count`: number of successful registrations that have consumed the invitation.
|
||||
|
||||
## Data Model
|
||||
|
||||
Add non-null integer columns to `organization_invitations`:
|
||||
|
||||
- `max_uses integer not null default 1`
|
||||
- `used_count integer not null default 0`
|
||||
|
||||
Existing invitation rows remain compatible: previous one-time invites become `max_uses = 1`, `used_count = 0` unless already accepted. The existing `status = accepted` already prevents use of accepted legacy rows.
|
||||
|
||||
## Invitation Creation Contract
|
||||
|
||||
`POST /api/organizations/[id]/invitations` accepts:
|
||||
|
||||
- `email?: string`
|
||||
- `roleId: string`
|
||||
- `validityDays?: number`
|
||||
- `maxUses?: number`
|
||||
|
||||
Defaults:
|
||||
|
||||
- `validityDays = 7`
|
||||
- `maxUses = 1`
|
||||
|
||||
Validation:
|
||||
|
||||
- `roleId` is still required.
|
||||
- `validityDays` must be a positive integer in an operationally reasonable range.
|
||||
- `maxUses` must be a positive integer in an operationally reasonable range.
|
||||
- Invalid input returns `jsonFailure(...)` with the existing structured API shape.
|
||||
|
||||
## Invitation Consumption
|
||||
|
||||
Registration only counts a use after account creation and membership insertion succeed inside the existing transaction.
|
||||
|
||||
An invitation is valid only if:
|
||||
|
||||
- `status === "active"`
|
||||
- `expiresAt >= now`
|
||||
- `usedCount < maxUses`
|
||||
|
||||
On successful invitation registration:
|
||||
|
||||
- Increment `usedCount`.
|
||||
- If the increment reaches `maxUses`, set `status = "accepted"` and fill `acceptedByAccountId` / `acceptedAt` with the consuming account details.
|
||||
- If remaining uses exist, keep `status = "active"` and leave accepted metadata empty.
|
||||
|
||||
The update should guard against concurrent over-consumption by updating with a `used_count < max_uses` predicate and checking that a row was returned.
|
||||
|
||||
## UI
|
||||
|
||||
`OrganizationInviteDialog` adds controls for:
|
||||
|
||||
- Validity days, default 7.
|
||||
- Maximum uses, default 1.
|
||||
|
||||
`OrganizationDetailClient` displays:
|
||||
|
||||
- Expiry time.
|
||||
- Usage as `usedCount / maxUses`.
|
||||
|
||||
Copy behavior remains unchanged.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The existing `/register?invite=...` URL format remains unchanged.
|
||||
|
||||
Existing rows without the new columns are migrated through default values. Existing one-time semantics are preserved by default.
|
||||
|
||||
## Rollback
|
||||
|
||||
If needed, UI inputs can be hidden and backend defaults can keep the old behavior. Dropping the columns would require a reverse migration only if deployed migrations must be rolled back.
|
||||
Reference in New Issue
Block a user