feat(auth): add /api/users CRUD (list, create, delete, reset-password)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Sonnet 4.6 committed 2026-05-27 14:41:58 +08:00
1 parent f46b37192f
commit 175b8e6065
5 files changed
+257

No files matched your search

+19
View File
@@ -53,4 +53,23 @@ describe("UserStore", () => {
expect(await users.verifyPassword("old", row!.passwordHash)).toBe(false);
expect(await users.verifyPassword("new", 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");
const list = users.listUsers();
expect(list).toHaveLength(2);
expect(list[0].username).toBe("alice");
expect(list[1].username).toBe("bob");
expect(list[0]).not.toHaveProperty("passwordHash");
expect(list[0].id).toMatch(/^[0-9a-f-]{36}$/);
expect(typeof list[0].createdAt).toBe("number");
});
it("deleteUser removes the row and returns true; returns false for unknown id", async () => {
const u = await users.createUser("alice", "pw-alice");
expect(users.deleteUser(u.id)).toBe(true);
expect(users.countUsers()).toBe(0);
expect(users.deleteUser("not-a-real-id")).toBe(false);
});
});
+15
View File
@@ -19,6 +19,8 @@ export interface UserStore {
findById(id: string): UserRow | null;
verifyPassword(plain: string, hash: string): Promise<boolean>;
changePassword(userId: string, newPassword: string): Promise<void>;
listUsers(): Array<{ id: string; username: string; createdAt: number }>;
deleteUser(id: string): boolean;
}
export class UsernameTakenError extends Error {
@@ -42,6 +44,10 @@ export function createUserStore(db: Database.Database): UserStore {
const updatePasswordStmt = db.prepare(
"UPDATE users SET passwordHash = ?, updatedAt = ? WHERE id = ?"
);
const listUsersStmt = db.prepare(
"SELECT id, username, createdAt FROM users ORDER BY createdAt ASC"
);
const deleteUserStmt = db.prepare("DELETE FROM users WHERE id = ?");
return {
countUsers() {
@@ -79,5 +85,14 @@ export function createUserStore(db: Database.Database): UserStore {
const hash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
updatePasswordStmt.run(hash, Date.now(), userId);
},
listUsers() {
return listUsersStmt.all() as Array<{ id: string; username: string; createdAt: number }>;
},
deleteUser(id) {
const result = deleteUserStmt.run(id);
return result.changes > 0;
},
};
}