feat(auth): invite-only / capped signup gate#3715
Conversation
…ore :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.
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.
- 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)
|
Warning Review limit reached
More reviews will be available in 42 minutes and 33 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThe PR implements a complete invite-only and capacity-capped signup system. It adds an Invitation model with token-based single-use lifecycle, a service layer for create/find/consume operations, and integrates two-gate signup eligibility (capacity ceiling AND public/invite) into existing local and OAuth signup flows. All endpoints are documented in OpenAPI and protected by admin authorization policies. ChangesSignup invitations and capacity gating
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3715 +/- ##
==========================================
+ Coverage 89.73% 89.89% +0.15%
==========================================
Files 143 148 +5
Lines 4794 4888 +94
Branches 1505 1532 +27
==========================================
+ Hits 4302 4394 +92
- Misses 385 389 +4
+ Partials 107 105 -2
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds an invitation-based / capacity-capped signup gate to modules/auth. Introduces an Invitation Mongoose model with admin CRUD + public verify endpoints, and wires two AND-ed gates (capacity ceiling + eligibility via config.sign.up OR a valid invite) into both local and OAuth signup paths. Includes a new UserService.count() helper used by the cap check, a signup-invite email template, OpenAPI docs, README updates, and unit/integration tests.
Changes:
- New invitation primitive: model, schema, repository, service, policy, controller, routes, email template, and OpenAPI spec.
auth.controller.signupandcheckOAuthUserProfileenforce capacity + eligibility gates; invite is consumed atomically post-create.- Adds
UserService.count()/UserRepository.count()and updates existing auth unit tests to mock the new dependencies.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents the new signup access control feature and endpoints. |
| modules/auth/controllers/auth.controller.js | Adds capacity + invite gates to local and OAuth signup; consumes invite post-create. |
| modules/auth/controllers/auth.invitation.controller.js | New admin CRUD + public verify controller for invitations. |
| modules/auth/services/auth.invitation.service.js | Token generation, expiry/email-pin validation, best-effort email send, single-use consume. |
| modules/auth/repositories/auth.invitation.repository.js | Mongo access for invitations including atomic consume. |
| modules/auth/models/auth.invitation.model.mongoose.js | Mongoose schema with unique-token index. |
| modules/auth/models/auth.invitation.schema.js | Zod schema for admin create payload (email only). |
| modules/auth/policies/auth.invitation.policy.js | CASL path→subject registration and admin-only abilities. |
| modules/auth/routes/auth.routes.js | Registers verify (public + rate-limited) and admin-gated CRUD before the OAuth wildcard. |
| modules/auth/doc/auth.invitations.yml | OpenAPI documentation for the new endpoints. |
| modules/auth/config/auth.development.config.js | Adds sign.cap and sign.inviteExpiresInDays config keys. |
| modules/auth/tests/auth.invitation.unit.tests.js | Unit coverage for invitation service + policy. |
| modules/auth/tests/auth.invitation.integration.tests.js | End-to-end coverage of cap/invite gates for local + OAuth. |
| modules/auth/tests/auth.silent.catch.unit.tests.js | Adds mocks for count + invitation service to existing test. |
| modules/auth/tests/auth.signout.controller.unit.tests.js | Same dependency-mock updates. |
| modules/auth/tests/auth.config.controller.unit.tests.js | Same dependency-mock updates. |
| modules/users/services/users.service.js | New count(filter) delegating to repository. |
| modules/users/repositories/users.repository.js | New exact countDocuments helper. |
| modules/users/tests/users.service.count.unit.tests.js | Unit test for the new count helper. |
| lib/services/tests/analytics.identify.unit.tests.js | Adds invite-service + count mocks to keep tests passing. |
| config/templates/signup-invite.html | New invitation email template. |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/services/tests/analytics.identify.unit.tests.js (1)
38-42: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueConsider completing the InvitationService mock for consistency.
The InvitationService mock here includes only
findValidandconsume, whilelib/services/tests/analytics.identify.unit.tests.js(lines 50-59, 246-256, 362-372) mocks all seven methods (create,list,get,revoke,findValidByEmail,findValid,consume). For consistency and to avoid potential issues if the controller module accesses other service methods during initialization, consider using the complete mock.♻️ Align with the complete mock pattern
jest.unstable_mockModule('../../../modules/auth/services/auth.invitation.service.js', () => ({ - default: { findValid: jest.fn().mockResolvedValue(null), consume: jest.fn().mockResolvedValue(null) }, + default: { + findValid: jest.fn().mockResolvedValue(null), + findValidByEmail: jest.fn().mockResolvedValue(null), + consume: jest.fn().mockResolvedValue(null), + create: jest.fn(), + list: jest.fn(), + get: jest.fn(), + revoke: jest.fn(), + }, }));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/services/tests/analytics.identify.unit.tests.js` around lines 38 - 42, The InvitationService mock used inside setupSignupMocks only defines findValid and consume which can leave other methods undefined and break module initialization; update the jest.unstable_mockModule for the invitations service to return a default object that defines all seven methods (create, list, get, revoke, findValidByEmail, findValid, consume) as jest.fn() so the controller/module can safely call any method during import; if specific behavior is required in tests, override individual methods' mockResolvedValue/mockImplementation after creating the base mock.modules/auth/controllers/auth.controller.js (1)
61-66: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winComplete JSDoc for modified
signupfunction.The updated async
signuphandler is missing an explicit@returnstag in its JSDoc block.As per coding guidelines
**/*.js: “Every new or modified function must have a JSDoc header …@returnsfor async functions.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@modules/auth/controllers/auth.controller.js` around lines 61 - 66, The signup async handler's JSDoc is missing an explicit `@returns` tag; update the JSDoc above the signup function to include an `@returns` describing the promise return type (e.g., `@returns` {Promise<void>} or `@returns` {Promise<void|Object>} if you prefer to indicate the response payload) and a short phrase like "Resolves when the response has been sent" so the async nature of signup is documented; ensure the tag sits with the existing `@param` entries and matches the function name signup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/templates/signup-invite.html`:
- Line 4: The email template signup-invite.html currently contains an empty
<title></title>; replace it with a meaningful, non-empty document title (e.g.,
"Invite to [ProjectName] — Complete Your Signup" or similar) so the <title> tag
in signup-invite.html contains descriptive text for linting and proper metadata.
In `@modules/auth/controllers/auth.controller.js`:
- Around line 71-83: The current cap check uses UserService.count() and
config.sign.cap which is racy; replace the count-then-create logic with an
atomic reservation mechanism: introduce a reservation counter (e.g., a dedicated
"signupCounter" document) and perform an atomic increment (DB $inc) to reserve a
slot before creating the user, check the returned counter value against
config.sign.cap, and on downstream failure (user creation, invitation checks via
InvitationService.findValid, etc.) decrement/rollback the reservation; ensure
the same change is applied to the other signup path that currently uses
UserService.count() (the block referenced around the secondary check) and
maintain existing error flows (responses.error(...)) when the reservation fails
or cap is exceeded.
In `@modules/auth/controllers/auth.invitation.controller.js`:
- Around line 8-60: The controller functions create, list, remove, verify and
invitationByID currently only have shorthand `@function` comments — update each
(create, list, remove, verify, invitationByID) to include full JSDoc blocks per
project standard: add a descriptive summary, `@param` for req (Express.Request)
and res (Express.Response) and next (where applicable), document specific param
properties used (e.g., req.body.email, req.user, req.params.token,
req.invitation.id, id), and an `@returns` tag indicating the Promise/void response
(e.g., Promise<void> or sends HTTP response). Ensure the JSDoc sits immediately
above each function declaration and mentions any thrown errors or response
shapes to satisfy the linter.
In `@modules/auth/doc/auth.invitations.yml`:
- Around line 21-24: The admin list response currently references the Invitation
schema (data.items $ref '`#/components/schemas/Invitation`') which includes the
sensitive token field (defined on Invitation at lines ~126-127); create a
separate schema (e.g., InvitationList or InvitationWithoutToken) that duplicates
Invitation minus the token field, and update the admin list response to use that
new schema instead of '`#/components/schemas/Invitation`' so tokens are not
exposed.
In `@modules/auth/models/auth.invitation.model.mongoose.js`:
- Around line 26-29: The function addID used as the virtual getter for
InvitationMongoose ('InvitationMongoose.virtual("id").get(addID)') lacks the
mandatory JSDoc header; add a JSDoc block immediately above addID describing the
function, include a `@this` annotation indicating the mongoose document context
(e.g., `@this` {Object} or a more specific Invitation document type), include any
`@param` entries if appropriate (none expected here) and a `@returns` {string}
describing that it returns the hex string representation of this._id. Ensure the
JSDoc follows the repository style and appears above the addID function
definition.
In `@modules/auth/tests/auth.invitation.integration.tests.js`:
- Around line 45-103: Add JSDoc `@returns` tags for the two named async helpers
createAdminAndSignin and createUserAndSignin: above each function provide an
`@returns` describing the resolved Promise type (e.g. Promise resolving to the
supertest agent/cookie-bound agent used by tests) so the async return is
explicitly documented per the project JS rules; update the JSDoc for both
createAdminAndSignin and createUserAndSignin accordingly.
- Around line 192-197: The test is order-dependent because config.sign.cap is
set before createAdminAndSignin() creates an extra account; change the setup so
you create the admin and issue the invitation first (call createAdminAndSignin()
and post to /api/auth/invitations to get created), then compute the current
total via UserService.count() and set config.sign.cap to that total before
making the request that should be blocked; update the test around the symbols
createAdminAndSignin, UserService.count, config.sign.cap and the invitation POST
flow so the cap is applied after the admin account exists.
In `@modules/auth/tests/auth.signout.controller.unit.tests.js`:
- Around line 23-27: The InvitationService test mock only defines findValid and
consume; extend it to mirror the complete mock pattern used elsewhere by adding
the other methods (create, getBrut, update, remove, search, count) alongside
findValid and consume so any test that calls those methods won't fail—ensure
count uses mockResolvedValue(0) and the others use jest.fn() (or
mockResolvedValue/null where appropriate) on the mock for the InvitationService
in the tests file.
In `@modules/auth/tests/auth.silent.catch.unit.tests.js`:
- Around line 38-44: The InvitationService mock in the test only stubs findValid
and consume but should mirror the complete mock used elsewhere for consistency;
update the jest.unstable_mockModule for
'../../../modules/auth/services/auth.invitation.service.js' to include all
public methods used by the service (at minimum mock findValid and consume plus
the other methods used across tests such as create, revoke, validate, list and
count) so it matches the full mock pattern (see auth.controller.js signup flow
at lines ~66-83 for which methods are required) and ensure each method uses
jest.fn().mockResolvedValue(...) or mockReturnValue as appropriate.
---
Outside diff comments:
In `@lib/services/tests/analytics.identify.unit.tests.js`:
- Around line 38-42: The InvitationService mock used inside setupSignupMocks
only defines findValid and consume which can leave other methods undefined and
break module initialization; update the jest.unstable_mockModule for the
invitations service to return a default object that defines all seven methods
(create, list, get, revoke, findValidByEmail, findValid, consume) as jest.fn()
so the controller/module can safely call any method during import; if specific
behavior is required in tests, override individual methods'
mockResolvedValue/mockImplementation after creating the base mock.
In `@modules/auth/controllers/auth.controller.js`:
- Around line 61-66: The signup async handler's JSDoc is missing an explicit
`@returns` tag; update the JSDoc above the signup function to include an `@returns`
describing the promise return type (e.g., `@returns` {Promise<void>} or `@returns`
{Promise<void|Object>} if you prefer to indicate the response payload) and a
short phrase like "Resolves when the response has been sent" so the async nature
of signup is documented; ensure the tag sits with the existing `@param` entries
and matches the function name signup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b0ba9ee6-cb00-48ce-8bcb-45fbc3ca16b2
📒 Files selected for processing (21)
README.mdconfig/templates/signup-invite.htmllib/services/tests/analytics.identify.unit.tests.jsmodules/auth/config/auth.development.config.jsmodules/auth/controllers/auth.controller.jsmodules/auth/controllers/auth.invitation.controller.jsmodules/auth/doc/auth.invitations.ymlmodules/auth/models/auth.invitation.model.mongoose.jsmodules/auth/models/auth.invitation.schema.jsmodules/auth/policies/auth.invitation.policy.jsmodules/auth/repositories/auth.invitation.repository.jsmodules/auth/routes/auth.routes.jsmodules/auth/services/auth.invitation.service.jsmodules/auth/tests/auth.config.controller.unit.tests.jsmodules/auth/tests/auth.invitation.integration.tests.jsmodules/auth/tests/auth.invitation.unit.tests.jsmodules/auth/tests/auth.signout.controller.unit.tests.jsmodules/auth/tests/auth.silent.catch.unit.tests.jsmodules/users/repositories/users.repository.jsmodules/users/services/users.service.jsmodules/users/tests/users.service.count.unit.tests.js
- 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
…der concurrent case-variants)
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
modules/auth/controllers/auth.controller.js (1)
66-74:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd missing
@returnsto modifiedsignupJSDoc.
signupis async and modified in this PR, but its JSDoc lacks@returns.As per coding guidelines: "`**/*.js`: Every new or modified function must have a JSDoc header ... and `@returns` for async functions."Proposed fix
/** * `@desc` Endpoint to ask the service to create a user * `@param` {Object} req - Express request object * `@param` {Object} res - Express response object + * `@returns` {Promise<void>} Sends HTTP response for signup success or failure */ const signup = async (req, res) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@modules/auth/controllers/auth.controller.js` around lines 66 - 74, The JSDoc for the async controller function signup is missing an `@returns` tag; update the JSDoc block immediately above the signup function to include an `@returns` describing the returned Promise (e.g. `@returns` {Promise<void>} — or `@returns` {Promise<Express.Response>} if you prefer to document the response) and a short phrase like "resolves when the HTTP response has been sent", so the async nature is documented for the UserService/controller signup function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@modules/auth/controllers/auth.controller.js`:
- Around line 437-441: The code currently resolves oauthInvite using
profil.email regardless of whether the OAuth provider verified that email, which
can allow invite-based signups on untrusted claims; update the logic in the
block around oauthCap/capReached and oauthInvite so you only call
InvitationService.findValidByEmail(profil.email) when the provider has marked
the email as verified (e.g., check profil.email_verified or provider-specific
verified flag) and profil.email is present; if the email is unverified or
missing, treat oauthInvite as null (so the (!config.sign.up && !oauthInvite)
gate cannot be bypassed), ensuring you preserve the existing capReached
evaluation (UserService.count and config.sign.cap) and the surrounding
conditional that checks sign-up settings.
---
Outside diff comments:
In `@modules/auth/controllers/auth.controller.js`:
- Around line 66-74: The JSDoc for the async controller function signup is
missing an `@returns` tag; update the JSDoc block immediately above the signup
function to include an `@returns` describing the returned Promise (e.g. `@returns`
{Promise<void>} — or `@returns` {Promise<Express.Response>} if you prefer to
document the response) and a short phrase like "resolves when the HTTP response
has been sent", so the async nature is documented for the UserService/controller
signup function.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8937e54f-9b23-41c3-a1c3-2f3b610d7c00
📒 Files selected for processing (8)
config/templates/signup-invite.htmlmodules/auth/controllers/auth.controller.jsmodules/auth/controllers/auth.invitation.controller.jsmodules/auth/doc/auth.invitations.ymlmodules/auth/models/auth.invitation.model.mongoose.jsmodules/auth/tests/auth.invitation.integration.tests.jsmodules/auth/tests/auth.signout.controller.unit.tests.jsmodules/auth/tests/auth.silent.catch.unit.tests.js
…nistic 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.
Summary
Invitationprimitive inmodules/auth(model/schema/policy/repo/service); admin CRUD (/api/auth/invitations) + public verify (/api/auth/invitations/verify/:token); two AND-ed signup gates — capacity (config.sign.cap, hard ceiling incl. invited) + eligibility (config.sign.upOR valid invite); local signup carries token via?inviteToken=query, OAuth matches invite by provider email; single-use consume after successful create; configsign.cap+sign.inviteExpiresInDays; README + OpenAPI (modules/auth/doc/auth.invitations.yml).Scope
modules/auth(model, schema, policy, repo, service, controller, routes, template, doc),modules/users(count helper),config(sign.cap, sign.inviteExpiresInDays), READMEyes—modules/usersgainscount()used by auth signup gate; no breaking changes to existing user/auth APImedium— new auth path, additive only, behind config flagsValidation
npm run lintnpm test— 173 auth-module tests pass;npm run test:unit:coverage,npm run test:integration:coverage,npm run test:e2eall green locallyGuardrails check
.env*,secrets/**, keys, tokens)Optional: Infra/Stack alignment details
Before vs After (key changes only)
config.sign.upboolean onlysign.capceiling + invite token ORsign.up/api/auth/invitationsCRUD +/verify/:tokenmodules/auth/doc/auth.invitations.ymlsign.cap=0disables ceiling;sign.up=truebypasses invite gate; feature is fully config-gatedNotes for reviewers
emailfield (inferred from invite record); token not returned in admin list (select: 0); single-use consume is atomic (findOneAndUpdate + $set usedAt).pierreb-devkit/Vue, same branch namefeat/invite-only-signup-gate).sign.cap+sign.inviteExpiresInDaysto their env config when they want to use the gate.Summary by CodeRabbit
New Features
Documentation
Tests