You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(users): add exact count() for signup cap enforcement
* feat(auth): scaffold signup invitation model, schema, and CASL policy
* fix(auth): drop duplicate token unique index on Invitation (keep explicit index)
* feat(auth): invitation repository + service (token gen, validation, consume)
* feat(auth): admin invitation CRUD + public verify endpoint + email template
* fix(auth): inline invitation routes into auth.routes (single reg, before :strategy wildcard)
Removes auth.invitation.routes.js so the glob auto-loader never picks it
up, eliminating the double-registration bug (two DB hits per DELETE,
duplicate app.param binding). Invitation routes are now declared once
inside auth.routes.js, before the greedy /api/auth/:strategy wildcard.
* feat(auth): gate local signup by cap + invite token, consume on success
Two AND-ed guards on POST /api/auth/signup: (1) capacity ceiling
(config.sign.cap, invited users count toward the cap) blocks everyone
when total >= cap; (2) eligibility requires config.sign.up OR a valid
inviteToken query param (token consumed after account is fully
provisioned). Config gains sign.cap and sign.inviteExpiresInDays.
* feat(auth): gate OAuth signup by cap + email-matched invite, consume on success
* docs(auth): document signup access control (cap + invitations) in README + OpenAPI
* chore(auth): verify fixups for invitation signup gate
- fix: optional chain req.query?.inviteToken to guard against missing query
object (broke analytics unit tests that inject req without a query key)
- fix: mock InvitationService + add UserService.count mock in
analytics.identify.unit.tests to prevent MissingSchemaError on lazy
mongoose.model('Invitation') call at repository import time
- test: extend auth.invitation.unit.tests — email-send branch, findValidByEmail
null-guard, list/get/revoke delegation, invitationAbilities admin/non-admin
paths, invitationSubjectRegistration predicate (policy 100% coverage)
* fix(auth): enforce invite email-pin on no-email signup; hide token from admin list
* fix(auth): address CodeRabbit/Copilot review findings on invite gate
- email template: add non-empty <title> to signup-invite.html
- auth.controller: short-circuit UserService.count() when cap is null
- auth.controller: coerce sign.cap to Number + guard non-finite at gate
- auth.controller: only consume invite when it actually opened the gate
(sign.up=true = invite not required, burning it would be wrong)
- auth.invitation.controller: add full JSDoc (@param/@returns) to all
controller functions per project standard
- auth.invitations.yml: add InvitationListItem schema (token omitted);
admin list endpoint now references it instead of Invitation
- auth.invitation.model.mongoose.js: add JSDoc to addID virtual getter
- auth.invitation.integration.tests.js: add @returns to test helpers;
fix cap test to create admin before setting cap (was order-dependent)
- auth.signout.controller.unit.tests.js: complete InvitationService mock
- auth.silent.catch.unit.tests.js: complete InvitationService mock
* fix(auth): canonicalize invited signup email to invite (single-use under concurrent case-variants)
* test(users): isolate count unit test from Organization model (deterministic CI)
Add repository-level mocks for organizations.repository.js and
organizations.membership.repository.js so mongoose.model('Organization')
is never evaluated at module-link time with --maxWorkers=2.
Service-level mocks intercept call paths but ESM static imports on the
real service files are still resolved by the V8 VM module linker in CI,
causing MissingSchemaError when the Organization schema hasn't been
registered yet in that worker's context.
* fix(auth): require verified provider email before OAuth invite matching (gate bypass)
-**Signup access control** : invite-only signup (single-use token links) + hard account cap (beta gating) - admin-managed invitations, public signup auto-locks at the cap
39
40
-**CASL v2 Authorization** : document-level permission checks via [@casl/ability](https://casl.js.org/) - replaces route-level role rules with per-document conditions (ownership, org scope)
40
41
-**Migration System** : automatic database migrations at boot - tracks executed scripts in MongoDB - idempotent reruns
41
42
@@ -197,6 +198,33 @@ Both file types are optional and can be used independently or together. Per-modu
197
198
198
199
> See [MIGRATIONS.md](MIGRATIONS.md) for the full migration guide from route-level CASL to document-level CASL v2.
199
200
201
+
## :lock: Signup Access Control (cap + invitations)
202
+
203
+
Signup is governed by two AND-ed gates in `auth.controller.js`:
204
+
205
+
-**Capacity** — `config.sign.cap` is a hard ceiling on the **total** number of accounts (invited users included). Once reached, *all* signup is locked.
206
+
-**Eligibility** — `config.sign.up` (public self-serve) **OR** a valid invitation re-opens signup for a specific email.
207
+
208
+
Invitations are **single-use** and **expiring** (`config.sign.inviteExpiresInDays`, default 14). Local signup carries the token as a query param (`/signup?inviteToken=…`); OAuth signup matches the invite on the provider's verified email.
209
+
210
+
| Key | Default | Effect |
211
+
|-----|---------|--------|
212
+
|`sign.up`|`true`| Public self-serve signup enabled |
213
+
|`sign.cap`|`null`|`null` = unlimited; integer = hard ceiling on total accounts (invited included) |
214
+
|`sign.inviteExpiresInDays`|`14`| Invite link validity (days) |
215
+
216
+
Common setups:
217
+
-**Invite-only:**`sign.up = false` → only invitation holders can sign up.
218
+
-**Beta cap (e.g. 50):**`sign.cap = 50` → open self-serve until 50 accounts, then locked.
|`POST`|`/api/auth/invitations`| JWT+Admin | Create + email a signup invitation |
223
+
|`GET`|`/api/auth/invitations`| JWT+Admin | List invitations |
224
+
|`DELETE`|`/api/auth/invitations/:invitationId`| JWT+Admin | Revoke an invitation |
225
+
|`GET`|`/api/auth/invitations/verify/:token`| Public | Check a token → `{ valid, email }`|
226
+
|`POST`|`/api/auth/signup?inviteToken=…`| Public | Signup; a valid token bypasses closed signup |
227
+
200
228
## :credit_card: Billing — Version Namespace Contract
201
229
202
230
When `meterMode` is enabled, three values must be aligned so `getPlanByVersion` resolves correctly and plan ratios are applied (not the ratio=1 fallback):
0 commit comments