mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(perm): one-time backfill of existing members to full access
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ce15f36e5e
commit
abf60141d9
2 files changed
+83
No files matched your search
@@ -182,12 +182,37 @@ function initTables(db: Database.Database): void {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-time backfill: existing `member` users created before the
|
||||||
|
* account-permissions feature are granted full access (all 5 capabilities +
|
||||||
|
* the `bots.all` marker), exactly once per database. Admins are skipped (they
|
||||||
|
* bypass permission checks). New members created after this runs are not
|
||||||
|
* affected — they get the basic tier via POST /api/users. A marker row in
|
||||||
|
* `schema_meta` makes this idempotent.
|
||||||
|
*/
|
||||||
|
export function backfillMemberPermissions(db: Database.Database): void {
|
||||||
|
db.exec(`CREATE TABLE IF NOT EXISTS schema_meta (key TEXT PRIMARY KEY, value TEXT)`);
|
||||||
|
const done = db.prepare("SELECT value FROM schema_meta WHERE key = 'perm_backfill_done'").get();
|
||||||
|
if (done) return;
|
||||||
|
const members = db.prepare("SELECT id FROM users WHERE role = 'member'").all() as { id: string }[];
|
||||||
|
const insCap = db.prepare("INSERT OR IGNORE INTO user_permissions (userId, permission) VALUES (?, ?)");
|
||||||
|
const tokens = ["player.control", "player.queue", "bot.manage", "platform.auth", "quality", "bots.all"];
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
for (const m of members) {
|
||||||
|
for (const t of tokens) insCap.run(m.id, t);
|
||||||
|
}
|
||||||
|
db.prepare("INSERT INTO schema_meta (key, value) VALUES ('perm_backfill_done', ?)").run(String(members.length));
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
}
|
||||||
|
|
||||||
export function createDatabase(dbPath: string): BotDatabase {
|
export function createDatabase(dbPath: string): BotDatabase {
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
db.pragma("journal_mode = WAL");
|
db.pragma("journal_mode = WAL");
|
||||||
db.pragma("foreign_keys = ON");
|
db.pragma("foreign_keys = ON");
|
||||||
initTables(db);
|
initTables(db);
|
||||||
migrateSchema(db);
|
migrateSchema(db);
|
||||||
|
backfillMemberPermissions(db);
|
||||||
|
|
||||||
const insertHistory = db.prepare(`
|
const insertHistory = db.prepare(`
|
||||||
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl)
|
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl)
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, afterEach } from "vitest";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import { createDatabase, backfillMemberPermissions, type BotDatabase } from "./database.js";
|
||||||
|
import { createPermissionStore, CAPABILITIES } from "./permissions.js";
|
||||||
|
|
||||||
|
describe("backfillMemberPermissions", () => {
|
||||||
|
let dbFile: string;
|
||||||
|
let db: BotDatabase;
|
||||||
|
function fresh() {
|
||||||
|
dbFile = path.join(os.tmpdir(), `mig-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
||||||
|
db = createDatabase(dbFile);
|
||||||
|
}
|
||||||
|
afterEach(() => {
|
||||||
|
db.close();
|
||||||
|
for (const s of ["", "-wal", "-shm"]) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(dbFile + s, { force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("grants existing members full access + bots.all, skips admins, once", () => {
|
||||||
|
fresh();
|
||||||
|
// simulate a pre-feature DB: clear the marker that createDatabase set, add users, no perm rows
|
||||||
|
db.db.prepare("DELETE FROM schema_meta WHERE key = 'perm_backfill_done'").run();
|
||||||
|
const now = Date.now();
|
||||||
|
const ins = db.db.prepare(
|
||||||
|
"INSERT INTO users (id,username,passwordHash,createdAt,updatedAt,role) VALUES (?,?,?,?,?,?)"
|
||||||
|
);
|
||||||
|
ins.run("m1", "mem", "x", now, now, "member");
|
||||||
|
ins.run("a1", "adm", "x", now, now, "admin");
|
||||||
|
|
||||||
|
backfillMemberPermissions(db.db);
|
||||||
|
|
||||||
|
const store = createPermissionStore(db.db);
|
||||||
|
expect(store.getCapabilities("m1").sort()).toEqual([...CAPABILITIES].sort());
|
||||||
|
expect(store.getBotAccess("m1")).toBe("all");
|
||||||
|
expect(store.getCapabilities("a1")).toEqual([]);
|
||||||
|
expect(store.getBotAccess("a1")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is idempotent — running again does not change or re-grant", () => {
|
||||||
|
fresh();
|
||||||
|
db.db.prepare("DELETE FROM schema_meta WHERE key = 'perm_backfill_done'").run();
|
||||||
|
const now = Date.now();
|
||||||
|
db.db
|
||||||
|
.prepare("INSERT INTO users (id,username,passwordHash,createdAt,updatedAt,role) VALUES (?,?,?,?,?,?)")
|
||||||
|
.run("m1", "mem", "x", now, now, "member");
|
||||||
|
backfillMemberPermissions(db.db);
|
||||||
|
// member restricted afterwards
|
||||||
|
createPermissionStore(db.db).setPermissions("m1", { capabilities: [], bots: [] });
|
||||||
|
// second run must NOT re-grant (marker present)
|
||||||
|
backfillMemberPermissions(db.db);
|
||||||
|
expect(createPermissionStore(db.db).getCapabilities("m1")).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user