mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-24 22:26:42 +00:00
feat: add signup disable config and fix telemetry startup (#3319)
* feat: add signup disable config and fix telemetry startup * fix: nits for config spread and 403 checks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Daniel Salazar
parent
a04d5fa5e0
commit
0801e1dc44
@@ -586,6 +586,24 @@ describe('AuthController.handleSignup', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects brand-new temp signups when registration is disabled', async () => {
|
||||
const authConfig = server.controllers.auth.config as {
|
||||
disable_user_signup?: boolean;
|
||||
};
|
||||
const prev = authConfig.disable_user_signup;
|
||||
authConfig.disable_user_signup = true;
|
||||
try {
|
||||
await expect(
|
||||
controller.handleSignup(makeReq({ is_temp: true }), makeRes()),
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 403,
|
||||
legacyCode: 'signup_disabled',
|
||||
});
|
||||
} finally {
|
||||
authConfig.disable_user_signup = prev;
|
||||
}
|
||||
});
|
||||
|
||||
it('emits puter.signup.success on successful signup', async () => {
|
||||
const baseline = heardSignupSuccess.length;
|
||||
const username = `s_${uniq()}`;
|
||||
@@ -607,6 +625,90 @@ describe('AuthController.handleSignup', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('still allows claiming a pseudo-user row when registration is disabled', async () => {
|
||||
const authConfig = server.controllers.auth.config as {
|
||||
disable_user_signup?: boolean;
|
||||
};
|
||||
const prev = authConfig.disable_user_signup;
|
||||
authConfig.disable_user_signup = true;
|
||||
try {
|
||||
const targetEmail = `disabled_claim_${uniq()}@test.local`;
|
||||
const placeholder = await server.stores.user.create({
|
||||
username: `placeholder_${uniq()}`,
|
||||
uuid: uuidv4(),
|
||||
password: null,
|
||||
email: targetEmail,
|
||||
clean_email: targetEmail,
|
||||
email_confirmed: 0,
|
||||
} as never);
|
||||
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({
|
||||
username: `claim_${uniq()}`,
|
||||
email: targetEmail,
|
||||
password: 'correct-horse-battery',
|
||||
}),
|
||||
res,
|
||||
);
|
||||
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
const claimed = await server.stores.user.getById(placeholder.id, {
|
||||
force: true,
|
||||
});
|
||||
expect(claimed!.username).not.toBe(placeholder.username);
|
||||
} finally {
|
||||
authConfig.disable_user_signup = prev;
|
||||
}
|
||||
});
|
||||
|
||||
it('does not reveal existing usernames or emails when registration is disabled', async () => {
|
||||
const username = `taken_${uniq()}`;
|
||||
const email = `${username}@test.local`;
|
||||
await controller.handleSignup(
|
||||
makeReq({ username, email, password: 'correct-horse-battery' }),
|
||||
makeRes(),
|
||||
);
|
||||
|
||||
const authConfig = server.controllers.auth.config as {
|
||||
disable_user_signup?: boolean;
|
||||
};
|
||||
const prev = authConfig.disable_user_signup;
|
||||
authConfig.disable_user_signup = true;
|
||||
try {
|
||||
// Taken username → the generic 403, not the duplicate error.
|
||||
await expect(
|
||||
controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `fresh_${uniq()}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
}),
|
||||
makeRes(),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 403,
|
||||
legacyCode: 'signup_disabled',
|
||||
});
|
||||
// Taken (non-claimable) email → same generic 403.
|
||||
await expect(
|
||||
controller.handleSignup(
|
||||
makeReq({
|
||||
username: `fresh_${uniq()}`,
|
||||
email,
|
||||
password: 'correct-horse-battery',
|
||||
}),
|
||||
makeRes(),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 403,
|
||||
legacyCode: 'signup_disabled',
|
||||
});
|
||||
} finally {
|
||||
authConfig.disable_user_signup = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// -- Signup device signal (fingerprint) --
|
||||
|
||||
@@ -522,6 +522,31 @@ export class AuthController extends PuterController {
|
||||
}
|
||||
}
|
||||
|
||||
// Signup-disabled gate. Runs before the duplicate checks so a
|
||||
// disabled endpoint doesn't reveal which usernames or emails
|
||||
// exist. Claiming a pre-existing placeholder row is still
|
||||
// allowed, so permanent signups look the email up first.
|
||||
if (this.config.disable_user_signup) {
|
||||
let claimable = false;
|
||||
if (!is_temp) {
|
||||
const existing =
|
||||
(await this.stores.user.getByEmail(body.email)) ??
|
||||
(await this.stores.user.getByCleanEmail(
|
||||
cleanEmail(body.email),
|
||||
));
|
||||
claimable = Boolean(
|
||||
existing &&
|
||||
!existing.email_confirmed &&
|
||||
existing.password === null,
|
||||
);
|
||||
}
|
||||
if (!claimable) {
|
||||
throw new HttpError(403, 'User registration is disabled.', {
|
||||
legacyCode: 'signup_disabled',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate username check
|
||||
if (await this.stores.user.getByUsername(body.username)) {
|
||||
throw new HttpError(
|
||||
|
||||
Reference in New Issue
Block a user