refactor(db): drop operators.telegram_user_id (not used since v1.0)

The Telegram bot phase ended in Plan 3 — the operator now signs in
via username + password. Migration 0011 drops the legacy column +
its unique index. seed.ts no longer reads SEED_OPERATOR_TELEGRAM_ID;
docker-compose.base.yml swaps the env to SEED_OPERATOR_USERNAME
(default 'admin'); .env.development follows. Settings page shows
'Username' instead of 'Operator ID'. Auth-and-prod-hardening plan
doc updated to drop the synthetic telegram_user_id from the
create-user CLI script and createUserAction insert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 17:39:46 +08:00
parent a37b36196d
commit 46c0315559
9 changed files with 1067 additions and 18 deletions

View File

@ -4,7 +4,7 @@ SESSIONS_DIR=/data/sessions
MEDIA_DIR=/data/media MEDIA_DIR=/data/media
BOT_HEALTH_PORT=8081 BOT_HEALTH_PORT=8081
BOT_LOG_LEVEL=debug BOT_LOG_LEVEL=debug
SEED_OPERATOR_TELEGRAM_ID=818380985 SEED_OPERATOR_USERNAME=admin
SEED_OPERATOR_NAME="yiekheng (dev)" SEED_OPERATOR_NAME="yiekheng (dev)"
WEB_PORT=9000 WEB_PORT=9000
AUTH_SECRET=86f656580a58f03b6ccb43d257e0e801ecd5356e042e8886b3c7c569e29ff13c AUTH_SECRET=86f656580a58f03b6ccb43d257e0e801ecd5356e042e8886b3c7c569e29ff13c

View File

@ -16,7 +16,7 @@ export default async function SettingsPage() {
<CardContent className="space-y-3 text-sm"> <CardContent className="space-y-3 text-sm">
<Row label="Display name" value={op.displayName} /> <Row label="Display name" value={op.displayName} />
<Separator /> <Separator />
<Row label="Operator ID" value={String(op.telegramUserId)} mono /> <Row label="Username" value={op.username} mono />
<Separator /> <Separator />
<Row label="Default timezone" value={op.defaultTimezone} mono /> <Row label="Default timezone" value={op.defaultTimezone} mono />
<Separator /> <Separator />

View File

@ -19,7 +19,7 @@ services:
MEDIA_DIR: ${MEDIA_DIR:-/data/media} MEDIA_DIR: ${MEDIA_DIR:-/data/media}
BOT_HEALTH_PORT: ${BOT_HEALTH_PORT:-8081} BOT_HEALTH_PORT: ${BOT_HEALTH_PORT:-8081}
BOT_LOG_LEVEL: ${BOT_LOG_LEVEL:-info} BOT_LOG_LEVEL: ${BOT_LOG_LEVEL:-info}
SEED_OPERATOR_TELEGRAM_ID: ${SEED_OPERATOR_TELEGRAM_ID:-0} SEED_OPERATOR_USERNAME: ${SEED_OPERATOR_USERNAME:-admin}
SEED_OPERATOR_NAME: ${SEED_OPERATOR_NAME:-Operator} SEED_OPERATOR_NAME: ${SEED_OPERATOR_NAME:-Operator}
networks: networks:
- cmbot - cmbot

View File

@ -65,7 +65,6 @@ export const operators = pgTable(
"operators", "operators",
{ {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
telegramUserId: bigint("telegram_user_id", { mode: "number" }).notNull(),
username: text("username").notNull(), username: text("username").notNull(),
passwordHash: text("password_hash"), passwordHash: text("password_hash"),
displayName: text("display_name").notNull(), displayName: text("display_name").notNull(),
@ -74,7 +73,6 @@ export const operators = pgTable(
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, },
(t) => ({ (t) => ({
telegramUserIdUnique: uniqueIndex("operators_telegram_user_id_uq").on(t.telegramUserId),
usernameUnique: uniqueIndex("operators_username_uq").on(sql`lower(${t.username})`), usernameUnique: uniqueIndex("operators_username_uq").on(sql`lower(${t.username})`),
}), }),
); );
@ -1693,7 +1691,6 @@ export async function createUserAction(input: {
displayName: u, displayName: u,
role: input.role, role: input.role,
defaultTimezone: "Asia/Kuala_Lumpur", defaultTimezone: "Asia/Kuala_Lumpur",
telegramUserId: Date.now(),
}) })
.returning({ id: operators.id }); .returning({ id: operators.id });
revalidatePath("/settings/users"); revalidatePath("/settings/users");
@ -2186,8 +2183,8 @@ async function main() {
const hash = await bcrypt.hash(password, 12); const hash = await bcrypt.hash(password, 12);
const { db, pool } = createClient(url); const { db, pool } = createClient(url);
await db.execute( await db.execute(
sql`INSERT INTO operators (username, password_hash, display_name, role, telegram_user_id, default_timezone) sql`INSERT INTO operators (username, password_hash, display_name, role, default_timezone)
VALUES (${username}, ${hash}, ${username}, ${role}, ${Date.now()}, 'Asia/Kuala_Lumpur')`, VALUES (${username}, ${hash}, ${username}, ${role}, 'Asia/Kuala_Lumpur')`,
); );
await pool.end(); await pool.end();
console.log(`Created ${role} ${username}.`); console.log(`Created ${role} ${username}.`);

View File

@ -0,0 +1,2 @@
DROP INDEX IF EXISTS "operators_telegram_user_id_uq";--> statement-breakpoint
ALTER TABLE "operators" DROP COLUMN IF EXISTS "telegram_user_id";

File diff suppressed because it is too large Load Diff

View File

@ -78,6 +78,13 @@
"when": 1778405570914, "when": 1778405570914,
"tag": "0010_fancy_wolf_cub", "tag": "0010_fancy_wolf_cub",
"breakpoints": true "breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1778405817706,
"tag": "0011_premium_grandmaster",
"breakpoints": true
} }
] ]
} }

View File

@ -17,7 +17,6 @@ export const operators = pgTable(
"operators", "operators",
{ {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
telegramUserId: bigint("telegram_user_id", { mode: "number" }).notNull(),
username: text("username").notNull(), username: text("username").notNull(),
passwordHash: text("password_hash"), passwordHash: text("password_hash"),
displayName: text("display_name").notNull(), displayName: text("display_name").notNull(),
@ -26,7 +25,6 @@ export const operators = pgTable(
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, },
(t) => ({ (t) => ({
telegramUserIdUnique: uniqueIndex("operators_telegram_user_id_uq").on(t.telegramUserId),
usernameUnique: uniqueIndex("operators_username_uq").on(sql`lower(${t.username})`), usernameUnique: uniqueIndex("operators_username_uq").on(sql`lower(${t.username})`),
}), }),
); );

View File

@ -1,30 +1,25 @@
import { createClient, operators } from "./index.js"; import { createClient, operators } from "./index.js";
const databaseUrl = process.env.DATABASE_URL; const databaseUrl = process.env.DATABASE_URL;
const operatorTelegramId = process.env.SEED_OPERATOR_TELEGRAM_ID; const username = process.env.SEED_OPERATOR_USERNAME ?? "admin";
const operatorName = process.env.SEED_OPERATOR_NAME ?? "Operator"; const operatorName = process.env.SEED_OPERATOR_NAME ?? "Operator";
if (!databaseUrl) { if (!databaseUrl) {
console.error("DATABASE_URL not set"); console.error("DATABASE_URL not set");
process.exit(1); process.exit(1);
} }
if (!operatorTelegramId || operatorTelegramId === "0") {
console.error("SEED_OPERATOR_TELEGRAM_ID not set");
process.exit(1);
}
const { db, pool } = createClient(databaseUrl); const { db, pool } = createClient(databaseUrl);
await db await db
.insert(operators) .insert(operators)
.values({ .values({
telegramUserId: Number(operatorTelegramId), username,
username: process.env.SEED_OPERATOR_USERNAME ?? "admin",
displayName: operatorName, displayName: operatorName,
role: "admin", role: "admin",
defaultTimezone: "Asia/Kuala_Lumpur", defaultTimezone: "Asia/Kuala_Lumpur",
}) })
.onConflictDoNothing(); .onConflictDoNothing();
console.log(`Seeded operator with telegram_user_id=${operatorTelegramId}`); console.log(`Seeded operator '${username}'. Set a password via scripts/set-password.sh ${username}`);
await pool.end(); await pool.end();