From b9866ddbebad5fccf5aa5c13dd82dc0e3d92e98a Mon Sep 17 00:00:00 2001
From: Neal Shah <30693865+ProgrammerIn-wonderland@users.noreply.github.com>
Date: Mon, 29 Jun 2026 09:51:00 -0400
Subject: [PATCH] add suspended_at col (#3321)
---
package-lock.json | 12 -----
.../clients/database/SqliteDatabaseClient.ts | 2 +
.../migrations/mysql/mysql_mig_16.sql | 46 +++++++++++++++++++
.../migrations/postgres/postgres_mig_5.sql | 25 ++++++++++
.../sqlite/0061_add_suspended_at.sql | 23 ++++++++++
src/backend/stores/user/UserStore.ts | 2 +
6 files changed, 98 insertions(+), 12 deletions(-)
create mode 100644 src/backend/clients/database/migrations/mysql/mysql_mig_16.sql
create mode 100644 src/backend/clients/database/migrations/postgres/postgres_mig_5.sql
create mode 100644 src/backend/clients/database/migrations/sqlite/0061_add_suspended_at.sql
diff --git a/package-lock.json b/package-lock.json
index 9d1117a5e..ed65668b9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1516,18 +1516,6 @@
"sisteransi": "^1.0.5"
}
},
- "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
- "version": "1.3.0",
- "extraneous": true,
- "inBundle": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
diff --git a/src/backend/clients/database/SqliteDatabaseClient.ts b/src/backend/clients/database/SqliteDatabaseClient.ts
index d6108b447..753351a0e 100644
--- a/src/backend/clients/database/SqliteDatabaseClient.ts
+++ b/src/backend/clients/database/SqliteDatabaseClient.ts
@@ -88,6 +88,8 @@ const AVAILABLE_MIGRATIONS: [number, string[]][] = [
[52, ['0057_add_user_reputation.sql']],
[53, ['0058_add_phone_verification.sql']],
[54, ['0059_add_card_verification.sql']],
+ [55, ['0060_add_card_fingerprint.sql']],
+ [56, ['0061_add_suspended_at.sql']],
];
export class SqliteDatabaseClient extends AbstractDatabaseClient {
diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_16.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_16.sql
new file mode 100644
index 000000000..6ef9d8261
--- /dev/null
+++ b/src/backend/clients/database/migrations/mysql/mysql_mig_16.sql
@@ -0,0 +1,46 @@
+-- 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 .
+
+-- Suspended-at column. Mirrors SQLite migration 0061. `suspended_at` is when
+-- the account was suspended, as unix seconds (NULL while not suspended) — the
+-- timestamp sibling of the boolean `suspended` flag, indexed so the
+-- signup-abuse harness can count an IP's recently-suspended accounts.
+--
+-- 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', 'suspended_at', '`suspended_at` bigint DEFAULT NULL');
+
+DROP PROCEDURE IF EXISTS _puter_add_user_suspended_at_index;
+DELIMITER //
+CREATE PROCEDURE _puter_add_user_suspended_at_index()
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'user'
+ AND INDEX_NAME = 'idx_user_suspended_at'
+ ) THEN
+ ALTER TABLE `user` ADD INDEX `idx_user_suspended_at` (`suspended_at`);
+ END IF;
+END//
+DELIMITER ;
+
+CALL _puter_add_user_suspended_at_index();
+
+DROP PROCEDURE IF EXISTS _puter_add_user_suspended_at_index;
diff --git a/src/backend/clients/database/migrations/postgres/postgres_mig_5.sql b/src/backend/clients/database/migrations/postgres/postgres_mig_5.sql
new file mode 100644
index 000000000..f5c5b4c4c
--- /dev/null
+++ b/src/backend/clients/database/migrations/postgres/postgres_mig_5.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 .
+
+-- Suspended-at column. Mirrors SQLite migration 0061. `suspended_at` is when
+-- the account was suspended, as unix seconds (NULL while not suspended) — the
+-- timestamp sibling of the boolean `suspended` flag, indexed so the
+-- signup-abuse harness can count an IP's recently-suspended accounts.
+-- Idempotent via IF NOT EXISTS.
+
+ALTER TABLE "user" ADD COLUMN IF NOT EXISTS suspended_at bigint;
+CREATE INDEX IF NOT EXISTS idx_user_suspended_at ON "user" (suspended_at);
diff --git a/src/backend/clients/database/migrations/sqlite/0061_add_suspended_at.sql b/src/backend/clients/database/migrations/sqlite/0061_add_suspended_at.sql
new file mode 100644
index 000000000..135bbfc2d
--- /dev/null
+++ b/src/backend/clients/database/migrations/sqlite/0061_add_suspended_at.sql
@@ -0,0 +1,23 @@
+-- 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 .
+
+-- When the account was suspended, as unix seconds; NULL while not suspended.
+-- The timestamp sibling of the existing boolean `suspended` flag, stamped by
+-- every suspension site. Indexed so the signup-abuse harness can count an IP's
+-- recently-suspended accounts (see extensions/abuse/v2 suspendedAccountsByIp).
+ALTER TABLE `user` ADD COLUMN `suspended_at` INTEGER DEFAULT NULL;
+CREATE INDEX IF NOT EXISTS idx_user_suspended_at ON `user` (`suspended_at`);
diff --git a/src/backend/stores/user/UserStore.ts b/src/backend/stores/user/UserStore.ts
index 7e66760d5..535189996 100644
--- a/src/backend/stores/user/UserStore.ts
+++ b/src/backend/stores/user/UserStore.ts
@@ -37,6 +37,8 @@ export interface UserRow {
email?: string | null;
/** True when an admin has suspended the account. */
suspended?: boolean;
+ /** When the account was suspended, as unix seconds; null while not suspended. */
+ suspended_at?: number | null;
/** True when the user has confirmed the email currently on file. */
email_confirmed?: boolean;
/** True for accounts that must confirm email before taking most actions. */