Add organisation invitations and one-time-code gateway enrolment
Invitations: token shown once, only its hash stored, 7-day expiry, the accepting account's email must match. Enrolment codes: 15 minutes, single use, hashed; POST /auth/enrol returns the farm's existing gateway token without rotating it and is rate-limited per IP. No email is sent; the inviter shares the link. Migration 0007, unit tests and scripts/e2e-onboarding.sh.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
FailureLimiter,
|
||||
codeKey,
|
||||
hashSecret,
|
||||
newEnrolmentCode,
|
||||
newInviteToken,
|
||||
normaliseCode,
|
||||
} from "../src/onboarding/tokens";
|
||||
|
||||
describe("enrolment codes", () => {
|
||||
test("have the shape that was promised and do not repeat", () => {
|
||||
const codes = new Set(Array.from({ length: 200 }, newEnrolmentCode));
|
||||
expect(codes.size).toBe(200);
|
||||
for (const code of codes) expect(code).toMatch(/^[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{5}$/);
|
||||
});
|
||||
|
||||
test("a code typed carelessly still finds the one that was issued", () => {
|
||||
const issued = "AB0C1-DEF23";
|
||||
for (const typed of ["ab0c1-def23", "AB0C1DEF23", " ab0c1 def23 ", "ABOC1-DEF23", "AB0Cl-DEF23", "ABOCI-DEF23"]) {
|
||||
expect(codeKey(typed)).toBe(codeKey(issued));
|
||||
}
|
||||
expect(codeKey("AB0C1-DEF24")).not.toBe(codeKey(issued));
|
||||
});
|
||||
|
||||
test("normalising is idempotent", () => {
|
||||
expect(normaliseCode(normaliseCode("ab-oc1"))).toBe(normaliseCode("ab-oc1"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("invitation tokens", () => {
|
||||
test("are long, URL-safe and unique, and only their hash is kept", () => {
|
||||
const a = newInviteToken();
|
||||
expect(a).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
||||
expect(newInviteToken()).not.toBe(a);
|
||||
expect(hashSecret(a)).toMatch(/^[0-9a-f]{64}$/);
|
||||
expect(hashSecret(a)).not.toContain(a);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FailureLimiter", () => {
|
||||
test("blocks after too many failures and forgets them after the window", () => {
|
||||
const limiter = new FailureLimiter(3, 1000);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(limiter.blocked("ip", i)).toBe(false);
|
||||
limiter.fail("ip", i);
|
||||
}
|
||||
expect(limiter.blocked("ip", 10)).toBe(true);
|
||||
expect(limiter.blocked("other", 10)).toBe(false);
|
||||
expect(limiter.blocked("ip", 2000)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user