mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(auth): two-role permission system (admin/member)
Adds an admin/member role to WebUI auth. /api/users and /api/audit are now gated by a requireAdmin middleware; all other authenticated endpoints accept both roles. Schema migration defaults all existing users to admin to preserve access. POST /api/users defaults new users to member; first-run setup always creates an admin. Adds PATCH /api/users/:id/role with last-admin demotion and deletion guards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b0b61f8fce
commit
a73f797bcb
18 files changed
+292
-60
No files matched your search
+42
-12
@@ -20,26 +20,26 @@ describe("UserStore", () => {
|
||||
});
|
||||
|
||||
it("createUser stores the user and bumps countUsers", async () => {
|
||||
const u = await users.createUser("alice", "pw-hunter2");
|
||||
const u = await users.createUser("alice", "pw-hunter2", "member");
|
||||
expect(u.id).toMatch(/^[0-9a-f-]{36}$/);
|
||||
expect(u.username).toBe("alice");
|
||||
expect(users.countUsers()).toBe(1);
|
||||
});
|
||||
|
||||
it("findByUsername is case-insensitive and returns null for missing", async () => {
|
||||
await users.createUser("Alice", "pw");
|
||||
await users.createUser("Alice", "pw-alice", "member");
|
||||
expect(users.findByUsername("ALICE")).not.toBeNull();
|
||||
expect(users.findByUsername("alice")).not.toBeNull();
|
||||
expect(users.findByUsername("bob")).toBeNull();
|
||||
});
|
||||
|
||||
it("createUser rejects duplicate usernames (case-insensitive)", async () => {
|
||||
await users.createUser("Alice", "pw");
|
||||
await expect(users.createUser("alice", "pw2")).rejects.toBeInstanceOf(UsernameTakenError);
|
||||
await users.createUser("Alice", "pw-alice", "member");
|
||||
await expect(users.createUser("alice", "pw-alice-2", "member")).rejects.toBeInstanceOf(UsernameTakenError);
|
||||
});
|
||||
|
||||
it("verifyPassword accepts correct password and rejects wrong one", async () => {
|
||||
await users.createUser("alice", "correct-horse-battery-staple");
|
||||
await users.createUser("alice", "correct-horse-battery-staple", "member");
|
||||
const row = users.findByUsername("alice");
|
||||
expect(row).not.toBeNull();
|
||||
expect(await users.verifyPassword("correct-horse-battery-staple", row!.passwordHash)).toBe(true);
|
||||
@@ -47,16 +47,16 @@ describe("UserStore", () => {
|
||||
});
|
||||
|
||||
it("changePassword updates the hash so the old password no longer verifies", async () => {
|
||||
const u = await users.createUser("alice", "old");
|
||||
await users.changePassword(u.id, "new");
|
||||
const u = await users.createUser("alice", "old-pw-pw", "member");
|
||||
await users.changePassword(u.id, "new-pw-pw");
|
||||
const row = users.findByUsername("alice");
|
||||
expect(await users.verifyPassword("old", row!.passwordHash)).toBe(false);
|
||||
expect(await users.verifyPassword("new", row!.passwordHash)).toBe(true);
|
||||
expect(await users.verifyPassword("old-pw-pw", row!.passwordHash)).toBe(false);
|
||||
expect(await users.verifyPassword("new-pw-pw", row!.passwordHash)).toBe(true);
|
||||
});
|
||||
|
||||
it("listUsers returns id+username+createdAt ascending, no password hash", async () => {
|
||||
await users.createUser("alice", "pw-alice");
|
||||
await users.createUser("bob", "pw-bob");
|
||||
await users.createUser("alice", "pw-alice", "member");
|
||||
await users.createUser("bob", "pw-bob-bob", "member");
|
||||
const list = users.listUsers();
|
||||
expect(list).toHaveLength(2);
|
||||
expect(list[0].username).toBe("alice");
|
||||
@@ -67,7 +67,7 @@ describe("UserStore", () => {
|
||||
});
|
||||
|
||||
it("deleteUser removes the row and returns true; returns false for unknown id", async () => {
|
||||
const u = await users.createUser("alice", "pw-alice");
|
||||
const u = await users.createUser("alice", "pw-alice", "member");
|
||||
expect(users.deleteUser(u.id)).toBe(true);
|
||||
expect(users.countUsers()).toBe(0);
|
||||
expect(users.deleteUser("not-a-real-id")).toBe(false);
|
||||
@@ -92,4 +92,34 @@ describe("UserStore", () => {
|
||||
expect(created).toHaveLength(1);
|
||||
expect(users.countUsers()).toBe(1);
|
||||
});
|
||||
|
||||
it("createFirstUser always creates an admin", async () => {
|
||||
const u = await users.createFirstUser("alice", "pw-alice");
|
||||
expect(u).not.toBeNull();
|
||||
expect(u!.role).toBe("admin");
|
||||
});
|
||||
|
||||
it("countAdmins reflects only role=admin", async () => {
|
||||
await users.createUser("alice", "pw-alice", "admin");
|
||||
await users.createUser("bob", "pw-bob-bob", "member");
|
||||
expect(users.countUsers()).toBe(2);
|
||||
expect(users.countAdmins()).toBe(1);
|
||||
});
|
||||
|
||||
it("setRole changes the role and returns true; false for unknown id", async () => {
|
||||
const u = await users.createUser("alice", "pw-alice", "member");
|
||||
expect(users.setRole(u.id, "admin")).toBe(true);
|
||||
expect(users.findById(u.id)!.role).toBe("admin");
|
||||
expect(users.setRole("nope", "admin")).toBe(false);
|
||||
});
|
||||
|
||||
it("listUsers includes role", async () => {
|
||||
await users.createUser("alice", "pw-alice", "admin");
|
||||
await users.createUser("bob", "pw-bob-bob", "member");
|
||||
const list = users.listUsers();
|
||||
const alice = list.find((u) => u.username === "alice")!;
|
||||
const bob = list.find((u) => u.username === "bob")!;
|
||||
expect(alice.role).toBe("admin");
|
||||
expect(bob.role).toBe("member");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user