mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
feat(auth): add SessionStore with rolling renewal and at-rest token hashing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
68a2fb2943
commit
5914f41ea1
2 files changed
+190
No files matched your search
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createDatabase, type BotDatabase } from "./database.js";
|
||||
import { createUserStore, type UserStore } from "./users.js";
|
||||
import { createSessionStore, type SessionStore, SESSION_TTL_MS, SESSION_TOUCH_INTERVAL_MS } from "./sessions.js";
|
||||
|
||||
function sha256(token: string) {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
|
||||
describe("SessionStore", () => {
|
||||
let botDb: BotDatabase;
|
||||
let users: UserStore;
|
||||
let sessions: SessionStore;
|
||||
let userId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
botDb = createDatabase(":memory:");
|
||||
users = createUserStore(botDb.db);
|
||||
sessions = createSessionStore(botDb.db);
|
||||
const u = await users.createUser("alice", "pw");
|
||||
userId = u.id;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
botDb.close();
|
||||
});
|
||||
|
||||
it("createSession returns a raw token whose sha256 matches the DB row id", () => {
|
||||
const { token } = sessions.createSession(userId);
|
||||
const row = botDb.db.prepare("SELECT id FROM sessions").get() as { id: string };
|
||||
expect(row.id).toBe(sha256(token));
|
||||
expect(row.id).not.toBe(token);
|
||||
});
|
||||
|
||||
it("validateAndTouch returns the user for a fresh token", () => {
|
||||
const { token } = sessions.createSession(userId);
|
||||
const result = sessions.validateAndTouch(token);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.userId).toBe(userId);
|
||||
expect(result!.username).toBe("alice");
|
||||
});
|
||||
|
||||
it("validateAndTouch returns null and deletes the row for an expired session", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
|
||||
const { token } = sessions.createSession(userId);
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z").getTime() + SESSION_TTL_MS + 1000);
|
||||
expect(sessions.validateAndTouch(token)).toBeNull();
|
||||
const remaining = (botDb.db.prepare("SELECT COUNT(*) AS n FROM sessions").get() as { n: number }).n;
|
||||
expect(remaining).toBe(0);
|
||||
});
|
||||
|
||||
it("validateAndTouch does not write the DB if called again within the touch interval", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
|
||||
const { token } = sessions.createSession(userId);
|
||||
const before = botDb.db.prepare("SELECT lastSeenAt FROM sessions").get() as { lastSeenAt: number };
|
||||
vi.advanceTimersByTime(SESSION_TOUCH_INTERVAL_MS - 1000);
|
||||
sessions.validateAndTouch(token);
|
||||
const after = botDb.db.prepare("SELECT lastSeenAt FROM sessions").get() as { lastSeenAt: number };
|
||||
expect(after.lastSeenAt).toBe(before.lastSeenAt);
|
||||
});
|
||||
|
||||
it("validateAndTouch writes lastSeenAt and extends expiresAt past the touch interval", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
|
||||
const { token, expiresAt: initialExpiry } = sessions.createSession(userId);
|
||||
vi.advanceTimersByTime(SESSION_TOUCH_INTERVAL_MS + 1000);
|
||||
sessions.validateAndTouch(token);
|
||||
const row = botDb.db.prepare("SELECT lastSeenAt, expiresAt FROM sessions").get() as { lastSeenAt: number; expiresAt: number };
|
||||
expect(row.lastSeenAt).toBe(Date.now());
|
||||
expect(row.expiresAt).toBeGreaterThan(initialExpiry);
|
||||
});
|
||||
|
||||
it("deleteSession removes the row", () => {
|
||||
const { token } = sessions.createSession(userId);
|
||||
sessions.deleteSession(token);
|
||||
const remaining = (botDb.db.prepare("SELECT COUNT(*) AS n FROM sessions").get() as { n: number }).n;
|
||||
expect(remaining).toBe(0);
|
||||
expect(sessions.validateAndTouch(token)).toBeNull();
|
||||
});
|
||||
|
||||
it("deleteAllForUser keeps the exceptToken session", () => {
|
||||
const a = sessions.createSession(userId);
|
||||
const b = sessions.createSession(userId);
|
||||
sessions.deleteAllForUser(userId, a.token);
|
||||
expect(sessions.validateAndTouch(a.token)).not.toBeNull();
|
||||
expect(sessions.validateAndTouch(b.token)).toBeNull();
|
||||
});
|
||||
|
||||
it("cleanupExpired removes only expired rows", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
|
||||
sessions.createSession(userId); // expires later
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z").getTime() + SESSION_TTL_MS + 1000);
|
||||
sessions.createSession(userId); // fresh
|
||||
sessions.cleanupExpired();
|
||||
const remaining = (botDb.db.prepare("SELECT COUNT(*) AS n FROM sessions").get() as { n: number }).n;
|
||||
expect(remaining).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user