feat: viz over user verification types (#3271)
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled

This commit is contained in:
Daniel Salazar
2026-06-17 14:21:55 -07:00
committed by GitHub
parent 0b41d992a4
commit 163e48ab27
6 changed files with 192 additions and 13 deletions
@@ -0,0 +1,47 @@
-- Copyright (C) 2024-present Puter Technologies Inc.
--
-- This file is part of Puter.
--
-- Puter is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published
-- by the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
-- Card fingerprint column. Mirrors SQLite migration 0060. `card_fingerprint`
-- is the Stripe card fingerprint (stable per card number) recorded when a user
-- clears card verification — the card sibling of `phone`, indexed like it so
-- admin tooling can find the accounts that verified with a given card. The card
-- itself never touches our DB, only Stripe's fingerprint for it.
--
-- Idempotent: the column add uses _puter_add_col (defined in mig_1, which
-- leaves it resident for later migrations); the index add is guarded against
-- INFORMATION_SCHEMA.STATISTICS so the directory replays safely.
CALL _puter_add_col('user', 'card_fingerprint', '`card_fingerprint` varchar(128) DEFAULT NULL');
DROP PROCEDURE IF EXISTS _puter_add_user_card_fingerprint_index;
DELIMITER //
CREATE PROCEDURE _puter_add_user_card_fingerprint_index()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'user'
AND INDEX_NAME = 'idx_user_card_fingerprint'
) THEN
ALTER TABLE `user` ADD INDEX `idx_user_card_fingerprint` (`card_fingerprint`);
END IF;
END//
DELIMITER ;
CALL _puter_add_user_card_fingerprint_index();
DROP PROCEDURE IF EXISTS _puter_add_user_card_fingerprint_index;
@@ -0,0 +1,26 @@
-- Copyright (C) 2024-present Puter Technologies Inc.
--
-- This file is part of Puter.
--
-- Puter is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published
-- by the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
-- Card fingerprint column. Mirrors SQLite migration 0060. `card_fingerprint`
-- is the Stripe card fingerprint (stable per card number) recorded when a user
-- clears card verification — the card sibling of `phone`, indexed like it so
-- admin tooling can find the accounts that verified with a given card. The card
-- itself never touches our DB, only Stripe's fingerprint for it.
-- Idempotent via IF NOT EXISTS.
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS card_fingerprint varchar(128);
CREATE INDEX IF NOT EXISTS idx_user_card_fingerprint ON "user" (card_fingerprint);
@@ -0,0 +1,25 @@
-- Copyright (C) 2024-present Puter Technologies Inc.
--
-- This file is part of Puter.
--
-- Puter is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published
-- by the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
-- Card fingerprint column. `card_fingerprint` is the Stripe card fingerprint
-- (stable per card number) recorded when a user clears card verification — the
-- card sibling of `phone`. Indexed like `phone` so admin tooling can find the
-- accounts that verified with a given card. The card itself never touches our
-- DB, only Stripe's fingerprint for it. Server-only (not on the whoami
-- allowlist), so it never reaches clients.
ALTER TABLE `user` ADD COLUMN `card_fingerprint` varchar(128) DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_user_card_fingerprint ON `user` (`card_fingerprint`);
@@ -4086,6 +4086,77 @@ describe('AuthController.handleSignup additional branches', () => {
expect(claimed!.password).not.toBeNull();
});
it('claim clears stale phone/card gates on the placeholder when the decision no longer requires them', async () => {
// Placeholder seeded with both gates already set. A benign claim
// (no validate override → no requirements) must reset them rather
// than silently inheriting the stale requirement.
const targetEmail = `pseudo_${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,
requires_phone_verification: 1,
requires_card_verification: 1,
} 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!.requires_phone_verification).toBe(false);
expect(claimed!.requires_card_verification).toBe(false);
});
it('claim carries the phone/card gates when the decision requires them', async () => {
const targetEmail = `pseudo_${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);
await withSignupValidateOverride(
(event) => {
event.requires_phone_verification = true;
event.requires_card_verification = true;
},
async () => {
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!.requires_phone_verification).toBe(true);
expect(claimed!.requires_card_verification).toBe(true);
});
it('extension hook can require email confirmation via requires_email_confirmation=true', async () => {
await withSignupValidateOverride(
(event) => {
+2 -13
View File
@@ -690,24 +690,13 @@ export class AuthController extends PuterController {
email_confirm_code,
email_confirm_token,
email_confirmed: 0,
// Pseudo claims always require email confirmation — the
// validate hook can only tighten, not loosen, so `1`
// stays hardcoded here.
requires_email_confirmation: 1,
last_activity_ts: signupSqlTs,
// Record the v2 reputation from this claim (skip if unset so we
// don't clobber an existing score with a placeholder).
...(validateEvent.reputation != null
? { reputation: validateEvent.reputation }
: {}),
// Carry the phone gate onto the claimed account when required.
...(force_phone_verification
? { requires_phone_verification: 1 }
: {}),
// Likewise for the card gate.
...(force_card_verification
? { requires_card_verification: 1 }
: {}),
requires_phone_verification: force_phone_verification ? 1 : 0,
requires_card_verification: force_card_verification ? 1 : 0,
});
// Move from temp group to regular user group
+21
View File
@@ -482,6 +482,9 @@ export class OIDCService extends PuterService {
allow: true,
no_temp_user: false,
requires_email_confirmation: false,
requires_phone_verification: false,
requires_card_verification: false,
reputation: null as number | null,
message: null as string | null,
code: null as string | null,
};
@@ -532,6 +535,17 @@ export class OIDCService extends PuterService {
};
}
const cfg = this.config as {
always_require_phone_verification?: boolean;
always_require_card_verification?: boolean;
};
const force_phone_verification =
Boolean(validateEvent.requires_phone_verification) ||
Boolean(cfg.always_require_phone_verification);
const force_card_verification =
Boolean(validateEvent.requires_card_verification) ||
Boolean(cfg.always_require_card_verification);
const created = await this.stores.user.create({
username,
uuid: uuidv4(),
@@ -539,7 +553,14 @@ export class OIDCService extends PuterService {
email,
clean_email: cleanEmail(email),
free_storage: this.config.storage_capacity ?? null,
// Email is provider-verified, so the email step is always skipped;
// the phone/card gates still apply when the harness flagged them.
requires_email_confirmation: false,
requires_phone_verification: force_phone_verification,
requires_card_verification: force_card_verification,
...(validateEvent.reputation != null
? { reputation: validateEvent.reputation }
: {}),
audit_metadata: {
ip: clientIp,
ip_fwd: proxyIpChain,