83 lines
2.7 KiB
Markdown
83 lines
2.7 KiB
Markdown
# 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.
|