Repository navigation
feat(enrollment): enrolment tokens and join requests for Business teams - #128
Merged
Merged
Conversation
A malformed body is now rejected after the registration flag and IP buckets run, so a closed server answers 403 and an over-limit IP 429. Also drops the check_user_budget forwarder, formats the limiter key only on failure, restores method and path on the auth rate-limit warning, and adds token-budget and dead-token tests.
- 068: token created_by is nullable with ON DELETE SET NULL so a creator can delete their account; a trigger clears email_proven_at on any email change (update_email no longer does it itself); indexes on pending user_id and token_id. - register charges the resolve bucket only when the token fails to load. - join_requests notifications resolve permissions with one bulk query. - redeem notifies when a re-redeem changes the reason, skips the created audit and notify for auto-admissions, and answers a concurrent second redeem with the settled state instead of 404. - EnrollError passes statuses through; enrolment routes share one require_team_invites layer; create role defaults like grants; at most 50 allowed domains; resend verification is capped at 5 an hour per user. - Tests that read or set RESEND_API_KEY hold the env lock via DeliveryGuard.
This was referenced Nov 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds enrolment tokens for Business teams: one company-wide token lets new machines and people sign up and ask to join a team. Joins are admitted automatically only for a proven email in an allowed domain with a free seat; everything else waits as a join request an admin approves or denies. Part of VoltiusApp/voltius#757 ("Server: enrolment tokens — Business"). The client part (admin UI) follows in a separate PR;
voltius-cli enrolland the first-launch screen come later.What changes
users.email_proven_at,team_enrollment_tokens(one live token per team, secret stored as SHA-256, at least one guard: allowed domains or admin approval),team_join_requests(one pending request per user and team). A trigger clearsemail_proven_atwheneverusers.emailchanges, so the rule holds for any code path, including an older binary after a rollback.email_proven_at. Accounts marked verified by migration 023 or by register's auto-verify (servers without email delivery) are not proven. Resend now sends a link to any unproven account, capped at 5 an hour per user./v1/metagainsemail_delivery.PERM_INVITE_MEMBERS, role checks shared with join grants):POST/GET/DELETE /v1/teams/:id/enrollment-tokento create (rotate), read and revoke;GET /v1/teams/:id/join-requests,POST …/:rid/approve,POST …/:rid/deny. Create and approve are Business-gated; read, revoke and deny are not.POST /v1/enrollment/resolve(own per-IP bucket, 60/min) andPOST /v1/auth/registerwith an optionalenrollmentfield. A valid token bypasses closed registration and the per-IP limits and is charged to a per-token budget of 500/day instead, so a fleet rollout from one office address works. A bad token is charged to the per-IP limits as before. Register with a token redeems it in the same call and returnsenrollment.status.POST /v1/enrollment/redeem. A redeemer without a public key is refused, as join grants do. Email verification re-runs admission for that user's waiting requests.join_requests:<team>event reaches only members who can invite; admission sends the existing membership events once.ENROLLMENT_NEEDS_GUARD,ENROLLMENT_NEEDS_APPROVAL(domain-only token on a server without email delivery),ENROLLMENT_UNAVAILABLE(402, lapsed team),ENROLLMENT_DOMAIN_MISMATCH(403); 404 unknown token or wrong secret (indistinguishable), 410 revoked or expired.Rollback
An older binary ignores the new tables. A rollback below this change keeps working; tokens and pending requests simply wait.
Tests
cargo test: 910 passed.cargo clippy --all-targets -- -D warningsclean. New tests cover the admission table, rotation, lapsed teams, the 50-machine resolve + register rollout from one IP, approve/verify and double-redeem races, keyless redeemers, the email-change trigger, and that every foreign key tousershas anON DELETErule.Known gaps, left for later