feat(users): guest role + reserved guest principal, excluded from count/list

This commit is contained in:
saopig1 committed 2026-06-25 11:14:47 +08:00
1 parent fb7f187ede
commit 60c8a5c993
2 files changed
+35 -4

No files matched your search

+26 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createDatabase, type BotDatabase } from "./database.js";
import { createUserStore, UsernameTakenError, type UserStore } from "./users.js";
import { createUserStore, UsernameTakenError, GUEST_USER_ID, GUEST_USERNAME, type UserStore } from "./users.js";
describe("UserStore", () => {
let botDb: BotDatabase;
@@ -184,3 +184,28 @@ describe("UserStore", () => {
expect(users.countAdmins()).toBe(1);
});
});
describe("guest row exclusion", () => {
let botDb: BotDatabase;
let users: UserStore;
beforeEach(() => {
botDb = createDatabase(":memory:");
users = createUserStore(botDb.db);
});
afterEach(() => {
botDb.close();
});
it("countUsers and listUsers ignore the reserved guest row", async () => {
await users.createUser("alice", "password123", "member");
// Insert the reserved guest row directly (mirrors the migration).
botDb.db.prepare(
"INSERT INTO users (id, username, passwordHash, createdAt, updatedAt, role) VALUES (?, ?, ?, ?, ?, 'guest')"
).run(GUEST_USER_ID, GUEST_USERNAME, "!", Date.now(), Date.now());
expect(users.countUsers()).toBe(1); // alice only
expect(users.listUsers().some((u) => u.id === GUEST_USER_ID)).toBe(false);
});
});
+9 -3
View File
@@ -4,7 +4,13 @@ import bcrypt from "bcryptjs";
const BCRYPT_ROUNDS = 12;
export type UserRole = "admin" | "member";
export type UserRole = "admin" | "member" | "guest";
/** Reserved synthetic principal for login-less guest sessions. The username is
* non-ASCII so it can never collide with an API-created account (which is
* validated against ^[A-Za-z0-9_\-.]{3,32}$). */
export const GUEST_USER_ID = "__guest__";
export const GUEST_USERNAME = "游客";
export interface UserRow {
id: string;
@@ -39,7 +45,7 @@ export class UsernameTakenError extends Error {
}
export function createUserStore(db: Database.Database): UserStore {
const countStmt = db.prepare("SELECT COUNT(*) AS n FROM users");
const countStmt = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role != 'guest'");
const countAdminsStmt = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'admin'");
const insertStmt = db.prepare(
"INSERT INTO users (id, username, passwordHash, createdAt, updatedAt, role) VALUES (?, ?, ?, ?, ?, ?)"
@@ -57,7 +63,7 @@ export function createUserStore(db: Database.Database): UserStore {
"UPDATE users SET role = ?, updatedAt = ? WHERE id = ?"
);
const listUsersStmt = db.prepare(
"SELECT id, username, createdAt, role FROM users ORDER BY createdAt ASC"
"SELECT id, username, createdAt, role FROM users WHERE role != 'guest' ORDER BY createdAt ASC"
);
const deleteUserStmt = db.prepare("DELETE FROM users WHERE id = ?");