From 163e48ab273bce2b8650af6806ef90b2adf5bf97 Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Wed, 17 Jun 2026 14:21:55 -0700 Subject: [PATCH] feat: viz over user verification types (#3271) --- .../migrations/mysql/mysql_mig_15.sql | 47 ++++++++++++ .../migrations/postgres/postgres_mig_4.sql | 26 +++++++ .../sqlite/0060_add_card_fingerprint.sql | 25 +++++++ .../controllers/auth/AuthController.test.ts | 71 +++++++++++++++++++ .../controllers/auth/AuthController.ts | 15 +--- src/backend/services/auth/OIDCService.ts | 21 ++++++ 6 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 src/backend/clients/database/migrations/mysql/mysql_mig_15.sql create mode 100644 src/backend/clients/database/migrations/postgres/postgres_mig_4.sql create mode 100644 src/backend/clients/database/migrations/sqlite/0060_add_card_fingerprint.sql diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_15.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_15.sql new file mode 100644 index 000000000..23829241d --- /dev/null +++ b/src/backend/clients/database/migrations/mysql/mysql_mig_15.sql @@ -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 . + +-- 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; diff --git a/src/backend/clients/database/migrations/postgres/postgres_mig_4.sql b/src/backend/clients/database/migrations/postgres/postgres_mig_4.sql new file mode 100644 index 000000000..4f1c867e8 --- /dev/null +++ b/src/backend/clients/database/migrations/postgres/postgres_mig_4.sql @@ -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 . + +-- 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); diff --git a/src/backend/clients/database/migrations/sqlite/0060_add_card_fingerprint.sql b/src/backend/clients/database/migrations/sqlite/0060_add_card_fingerprint.sql new file mode 100644 index 000000000..4c4834925 --- /dev/null +++ b/src/backend/clients/database/migrations/sqlite/0060_add_card_fingerprint.sql @@ -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 . + +-- 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`); diff --git a/src/backend/controllers/auth/AuthController.test.ts b/src/backend/controllers/auth/AuthController.test.ts index 1e0f36b98..8cea64bb9 100644 --- a/src/backend/controllers/auth/AuthController.test.ts +++ b/src/backend/controllers/auth/AuthController.test.ts @@ -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) => { diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 96647f86a..386a1af22 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -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 diff --git a/src/backend/services/auth/OIDCService.ts b/src/backend/services/auth/OIDCService.ts index 59603ff39..c4fac5626 100644 --- a/src/backend/services/auth/OIDCService.ts +++ b/src/backend/services/auth/OIDCService.ts @@ -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,