diff --git a/apps/api-server/src/__tests__/e2e/me.test.ts b/apps/api-server/src/__tests__/e2e/me.test.ts index 4caa9d3..95aedf5 100644 --- a/apps/api-server/src/__tests__/e2e/me.test.ts +++ b/apps/api-server/src/__tests__/e2e/me.test.ts @@ -8,6 +8,14 @@ * - me.updateProfile - update profile fields * - me.setPassword - set/change password * - me.delete - delete account + * - me.sessions.list - list all sessions + * - me.sessions.revoke - revoke a session + * - me.sessions.revokeAll - revoke all sessions except current + * - me.devices.getInfo - get current device info + * - me.devices.trust - trust current device + * - me.devices.listTrusted - list trusted devices + * - me.devices.untrust - untrust a device + * - me.devices.revokeAll - revoke all trusted devices */ import type { Database } from "@reviq/db-schema"; @@ -59,13 +67,19 @@ function getDb(): Kysely { function createAPIContext(options?: { sessionToken?: string; apiKey?: string; + deviceFingerprint?: string; }): APIContext { const reqHeaders = new Headers(); + const cookies: string[] = []; + if (options?.sessionToken) { - reqHeaders.set( - "cookie", - `${COOKIE_NAMES.SESSION_TOKEN}=${options.sessionToken}`, - ); + cookies.push(`${COOKIE_NAMES.SESSION_TOKEN}=${options.sessionToken}`); + } + if (options?.deviceFingerprint) { + cookies.push(`${COOKIE_NAMES.DEVICE_FINGERPRINT}=${options.deviceFingerprint}`); + } + if (cookies.length > 0) { + reqHeaders.set("cookie", cookies.join("; ")); } if (options?.apiKey) { reqHeaders.set("x-api-key", options.apiKey); @@ -82,26 +96,78 @@ function createAPIContext(options?: { } /** - * Create a real session in the database and return the token + * Create a real session in the database and return the token and session ID */ -async function createSession(userId: number): Promise { +async function createSession( + userId: number, + options?: { ipAddress?: string; userAgent?: string }, +): Promise<{ token: string; sessionId: number }> { const token = "test-session-" + String(Date.now()) + String(Math.random()); const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); - await getDb() + const result = await getDb() .insertInto("sessions") .values({ user_id: userId, token_hash: tokenHashValue, - ip_address: "127.0.0.1", - user_agent: "test-agent", + ip_address: options?.ipAddress ?? "127.0.0.1", + user_agent: options?.userAgent ?? "test-agent", expires_at: expiresAt, trusted_mode: false, }) - .execute(); + .returning("id") + .executeTakeFirstOrThrow(); - return token; + return { token, sessionId: Number(result.id) }; +} + +/** + * Create an authenticated API context for a user (creates session + context) + */ +async function createUserAPIContext( + userId: number, + options?: { deviceFingerprint?: string }, +): Promise<{ context: APIContext; token: string }> { + const { token } = await createSession(userId); + const context = createAPIContext({ + sessionToken: token, + deviceFingerprint: options?.deviceFingerprint, + }); + return { context, token }; +} + +/** + * Create a device in the database and return the fingerprint + */ +async function createDevice( + userId: number, + options?: { + fingerprint?: string; + isTrusted?: boolean; + name?: string; + userAgent?: string; + }, +): Promise<{ fingerprint: string; deviceId: number }> { + const fingerprint = + options?.fingerprint ?? + "test-fp-" + String(Date.now()) + String(Math.random()); + + const result = await getDb() + .insertInto("user_devices") + .values({ + user_id: userId, + device_fingerprint: fingerprint, + is_trusted: options?.isTrusted ?? false, + name: options?.name ?? null, + user_agent: options?.userAgent ?? "Mozilla/5.0 Test Browser", + ip_address: "127.0.0.1", + last_used_at: new Date(), + }) + .returning("id") + .executeTakeFirstOrThrow(); + + return { fingerprint, deviceId: Number(result.id) }; } /** @@ -159,7 +225,7 @@ describe("me.get", () => { .where("id", "=", user.id) .execute(); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); const result = await call(router.me.get, undefined, { context }); @@ -188,7 +254,7 @@ describe("me.get", () => { .where("id", "=", user.id) .execute(); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); const result = await call(router.me.get, undefined, { context }); @@ -204,7 +270,7 @@ describe("me.get", () => { passwordHash, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); const result = await call(router.me.get, undefined, { context }); @@ -218,7 +284,7 @@ describe("me.get", () => { isSuperuser: true, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); const result = await call(router.me.get, undefined, { context }); @@ -234,7 +300,7 @@ describe("me.authStatus", () => { displayName: "Session User", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); const result = await call(router.me.authStatus, undefined, { context }); @@ -280,7 +346,7 @@ describe("me.setupProfile", () => { .where("id", "=", user.id) .execute(); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call( @@ -316,7 +382,7 @@ describe("me.setupProfile", () => { .where("id", "=", user.id) .execute(); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call( @@ -346,7 +412,7 @@ describe("me.updateProfile", () => { displayName: "Original Name", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call( @@ -372,7 +438,7 @@ describe("me.updateProfile", () => { displayName: "Original", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call( @@ -411,7 +477,7 @@ describe("me.updateProfile", () => { .where("id", "=", user.id) .execute(); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call( @@ -442,7 +508,7 @@ describe("me.updateProfile", () => { displayName: "Stay Same", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call(router.me.updateProfile, {}, { context }); @@ -463,7 +529,7 @@ describe("me.setPassword", () => { email: "nopass@example.com", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); // Use a strong password @@ -492,7 +558,7 @@ describe("me.setPassword", () => { passwordHash: oldHash, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call( @@ -520,7 +586,7 @@ describe("me.setPassword", () => { passwordHash: oldHash, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await expect( @@ -541,7 +607,7 @@ describe("me.setPassword", () => { passwordHash: oldHash, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await expect( @@ -561,7 +627,7 @@ describe("me.setPassword", () => { email: "weak@example.com", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); // Password must be at least 8 chars to pass schema validation @@ -588,7 +654,7 @@ describe("me.delete", () => { passwordHash, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call(router.me.delete, { password }, { context }); @@ -608,7 +674,7 @@ describe("me.delete", () => { email: "nopassdelete@example.com", }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await expect( @@ -623,7 +689,7 @@ describe("me.delete", () => { passwordHash, }); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await expect( @@ -650,7 +716,7 @@ describe("me.delete", () => { }) .execute(); - const sessionToken = await createSession(user.id); + const { token: sessionToken } = await createSession(user.id); const context = createAPIContext({ sessionToken }); await call(router.me.delete, { password }, { context }); @@ -665,3 +731,569 @@ describe("me.delete", () => { expect(tokens).toHaveLength(0); }); }); + +// ===== Session Management Tests ===== + +describe("me.sessions.list", () => { + test("returns all sessions for user", async () => { + const user = await createTestUser(getDb(), { + email: "sessions@example.com", + }); + + // Create multiple sessions + const { token: sessionToken1, sessionId: id1 } = await createSession( + user.id, + { ipAddress: "192.168.1.1", userAgent: "Chrome/1.0" }, + ); + await createSession(user.id, { + ipAddress: "192.168.1.2", + userAgent: "Firefox/1.0", + }); + await createSession(user.id, { + ipAddress: "192.168.1.3", + userAgent: "Safari/1.0", + }); + + const context = createAPIContext({ sessionToken: sessionToken1 }); + const sessions = await call(router.me.sessions.list, undefined, { + context, + }); + + expect(sessions).toHaveLength(3); + // Sessions should be ordered by created_at desc + expect(sessions[0]?.userAgent).toBe("Safari/1.0"); + expect(sessions[1]?.userAgent).toBe("Firefox/1.0"); + expect(sessions[2]?.userAgent).toBe("Chrome/1.0"); + }); + + test("marks current session with isCurrent flag", async () => { + const user = await createTestUser(getDb(), { + email: "current@example.com", + }); + + const { token: sessionToken1, sessionId: id1 } = await createSession( + user.id, + ); + const { sessionId: id2 } = await createSession(user.id); + + const context = createAPIContext({ sessionToken: sessionToken1 }); + const sessions = await call(router.me.sessions.list, undefined, { + context, + }); + + expect(sessions).toHaveLength(2); + const current = sessions.find((s) => s.id === id1); + const other = sessions.find((s) => s.id === id2); + expect(current?.isCurrent).toBe(true); + expect(other?.isCurrent).toBe(false); + }); + + test("returns session metadata correctly", async () => { + const user = await createTestUser(getDb(), { + email: "metadata@example.com", + }); + + // Create session and update with location data + const { token: sessionToken, sessionId } = await createSession(user.id, { + ipAddress: "8.8.8.8", + userAgent: "TestAgent/1.0", + }); + + await getDb() + .updateTable("sessions") + .set({ + city: "San Francisco", + region: "CA", + country: "US", + trusted_mode: true, + }) + .where("id", "=", String(sessionId)) + .execute(); + + const context = createAPIContext({ sessionToken }); + const sessions = await call(router.me.sessions.list, undefined, { + context, + }); + + expect(sessions).toHaveLength(1); + const session = sessions[0]; + expect(session?.ip).toBe("8.8.8.8"); + expect(session?.userAgent).toBe("TestAgent/1.0"); + expect(session?.city).toBe("San Francisco"); + expect(session?.region).toBe("CA"); + expect(session?.country).toBe("US"); + expect(session?.trustedMode).toBe(true); + expect(session?.createdAt).toBeInstanceOf(Date); + expect(session?.revokedAt).toBeNull(); + }); +}); + +describe("me.sessions.revoke", () => { + test("revokes another session successfully", async () => { + const user = await createTestUser(getDb(), { + email: "revoke@example.com", + }); + + const { token: sessionToken1 } = await createSession(user.id); + const { sessionId: sessionId2 } = await createSession(user.id); + + const context = createAPIContext({ sessionToken: sessionToken1 }); + await call(router.me.sessions.revoke, { sessionId: sessionId2 }, { context }); + + // Verify session is revoked + const session = await getDb() + .selectFrom("sessions") + .select(["revoked_at"]) + .where("id", "=", String(sessionId2)) + .executeTakeFirstOrThrow(); + + expect(session.revoked_at).not.toBeNull(); + }); + + test("fails to revoke current session", async () => { + const user = await createTestUser(getDb(), { + email: "revokecurrent@example.com", + }); + + const { token: sessionToken, sessionId } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await expect( + call(router.me.sessions.revoke, { sessionId }, { context }), + ).rejects.toThrow("Cannot revoke current session"); + }); + + test("fails to revoke non-existent session", async () => { + const user = await createTestUser(getDb(), { + email: "revokenotfound@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await expect( + call(router.me.sessions.revoke, { sessionId: 999999 }, { context }), + ).rejects.toThrow("Session not found"); + }); + + test("fails to revoke already revoked session", async () => { + const user = await createTestUser(getDb(), { + email: "revokeagain@example.com", + }); + + const { token: sessionToken1 } = await createSession(user.id); + const { sessionId: sessionId2 } = await createSession(user.id); + + // Revoke the session directly + await getDb() + .updateTable("sessions") + .set({ revoked_at: new Date() }) + .where("id", "=", String(sessionId2)) + .execute(); + + const context = createAPIContext({ sessionToken: sessionToken1 }); + await expect( + call(router.me.sessions.revoke, { sessionId: sessionId2 }, { context }), + ).rejects.toThrow("Session not found"); + }); + + test("fails to revoke another user's session", async () => { + const user1 = await createTestUser(getDb(), { + email: "user1@example.com", + }); + const user2 = await createTestUser(getDb(), { + email: "user2@example.com", + }); + + const { token: sessionToken1 } = await createSession(user1.id); + const { sessionId: sessionId2 } = await createSession(user2.id); + + const context = createAPIContext({ sessionToken: sessionToken1 }); + await expect( + call(router.me.sessions.revoke, { sessionId: sessionId2 }, { context }), + ).rejects.toThrow("Session not found"); + }); +}); + +describe("me.sessions.revokeAll", () => { + test("revokes all sessions except current", async () => { + const user = await createTestUser(getDb(), { + email: "revokeall@example.com", + }); + + const { token: sessionToken1, sessionId: id1 } = await createSession( + user.id, + ); + const { sessionId: id2 } = await createSession(user.id); + const { sessionId: id3 } = await createSession(user.id); + + const context = createAPIContext({ sessionToken: sessionToken1 }); + await call(router.me.sessions.revokeAll, undefined, { context }); + + // Verify current session is NOT revoked + const currentSession = await getDb() + .selectFrom("sessions") + .select(["revoked_at"]) + .where("id", "=", String(id1)) + .executeTakeFirstOrThrow(); + expect(currentSession.revoked_at).toBeNull(); + + // Verify other sessions ARE revoked + const otherSessions = await getDb() + .selectFrom("sessions") + .select(["id", "revoked_at"]) + .where("id", "in", [String(id2), String(id3)]) + .execute(); + + for (const session of otherSessions) { + expect(session.revoked_at).not.toBeNull(); + } + }); + + test("does nothing when only current session exists", async () => { + const user = await createTestUser(getDb(), { + email: "onlyone@example.com", + }); + + const { token: sessionToken, sessionId } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + // Should not throw + await call(router.me.sessions.revokeAll, undefined, { context }); + + // Current session should still be valid + const session = await getDb() + .selectFrom("sessions") + .select(["revoked_at"]) + .where("id", "=", String(sessionId)) + .executeTakeFirstOrThrow(); + expect(session.revoked_at).toBeNull(); + }); +}); + +// ===== Device Management Tests ===== + +describe("me.devices.getInfo", () => { + test("returns device info for current device", async () => { + const user = await createTestUser(getDb(), { + email: "deviceinfo@example.com", + }); + + const { fingerprint, deviceId } = await createDevice(user.id, { + name: "My MacBook", + isTrusted: true, + userAgent: "Safari/17.0", + }); + + // Update with location data + await getDb() + .updateTable("user_devices") + .set({ + ip_address: "1.2.3.4", + city: "New York", + region: "NY", + country: "US", + }) + .where("id", "=", String(deviceId)) + .execute(); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ + sessionToken, + deviceFingerprint: fingerprint, + }); + + const info = await call(router.me.devices.getInfo, undefined, { context }); + + expect(info.id).toBe(deviceId); + expect(info.name).toBe("My MacBook"); + expect(info.ip).toBe("1.2.3.4"); + expect(info.city).toBe("New York"); + expect(info.region).toBe("NY"); + expect(info.country).toBe("US"); + expect(info.isTrusted).toBe(true); + expect(info.lastUsedAt).toBeInstanceOf(Date); + }); + + test("returns default name from user agent when name is null", async () => { + const user = await createTestUser(getDb(), { + email: "defaultname@example.com", + }); + + const { fingerprint } = await createDevice(user.id, { + userAgent: "Mozilla/5.0 (Macintosh)", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ + sessionToken, + deviceFingerprint: fingerprint, + }); + + const info = await call(router.me.devices.getInfo, undefined, { context }); + + expect(info.name).toBe("Mozilla device"); + }); + + test("fails without device fingerprint", async () => { + const user = await createTestUser(getDb(), { + email: "nofingerprint@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await expect( + call(router.me.devices.getInfo, undefined, { context }), + ).rejects.toThrow("No device fingerprint found"); + }); + + test("fails when device does not exist", async () => { + const user = await createTestUser(getDb(), { + email: "nodevice@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ + sessionToken, + deviceFingerprint: "nonexistent-fingerprint", + }); + + await expect( + call(router.me.devices.getInfo, undefined, { context }), + ).rejects.toThrow("Device not found"); + }); +}); + +describe("me.devices.trust", () => { + test("trusts current device with name", async () => { + const user = await createTestUser(getDb(), { + email: "trustdevice@example.com", + }); + + const { fingerprint, deviceId } = await createDevice(user.id, { + isTrusted: false, + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ + sessionToken, + deviceFingerprint: fingerprint, + }); + + await call( + router.me.devices.trust, + { name: "My Work Laptop" }, + { context }, + ); + + // Verify device is trusted with the new name + const device = await getDb() + .selectFrom("user_devices") + .select(["is_trusted", "name"]) + .where("id", "=", String(deviceId)) + .executeTakeFirstOrThrow(); + + expect(device.is_trusted).toBe(true); + expect(device.name).toBe("My Work Laptop"); + }); + + test("fails without device fingerprint", async () => { + const user = await createTestUser(getDb(), { + email: "trustnofp@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await expect( + call(router.me.devices.trust, { name: "Test" }, { context }), + ).rejects.toThrow("No device fingerprint found"); + }); + + test("fails when device does not exist", async () => { + const user = await createTestUser(getDb(), { + email: "trustnodevice@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ + sessionToken, + deviceFingerprint: "nonexistent", + }); + + await expect( + call(router.me.devices.trust, { name: "Test" }, { context }), + ).rejects.toThrow("Device not found"); + }); +}); + +describe("me.devices.listTrusted", () => { + test("returns only trusted devices", async () => { + const user = await createTestUser(getDb(), { + email: "listtrusted@example.com", + }); + + // Create trusted and untrusted devices + await createDevice(user.id, { isTrusted: true, name: "Trusted 1" }); + await createDevice(user.id, { isTrusted: true, name: "Trusted 2" }); + await createDevice(user.id, { isTrusted: false, name: "Untrusted" }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + const devices = await call(router.me.devices.listTrusted, undefined, { + context, + }); + + expect(devices).toHaveLength(2); + expect(devices.map((d) => d.name).sort()).toEqual([ + "Trusted 1", + "Trusted 2", + ]); + expect(devices.every((d) => d.isTrusted)).toBe(true); + }); + + test("returns empty list when no trusted devices", async () => { + const user = await createTestUser(getDb(), { + email: "notrusted@example.com", + }); + + await createDevice(user.id, { isTrusted: false }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + const devices = await call(router.me.devices.listTrusted, undefined, { + context, + }); + + expect(devices).toHaveLength(0); + }); + + test("returns default name when device name is null", async () => { + const user = await createTestUser(getDb(), { + email: "defaulttrusted@example.com", + }); + + await createDevice(user.id, { + isTrusted: true, + name: undefined, + userAgent: "Chrome/120", + }); + + // Set name to null explicitly + await getDb() + .updateTable("user_devices") + .set({ name: null }) + .where("user_id", "=", user.id) + .execute(); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + const devices = await call(router.me.devices.listTrusted, undefined, { + context, + }); + + expect(devices).toHaveLength(1); + expect(devices[0]?.name).toBe("Unknown device"); + }); +}); + +describe("me.devices.untrust", () => { + test("untrusts device by ID", async () => { + const user = await createTestUser(getDb(), { + email: "untrust@example.com", + }); + + const { deviceId } = await createDevice(user.id, { + isTrusted: true, + name: "Trusted Device", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await call(router.me.devices.untrust, { deviceId }, { context }); + + // Verify device is untrusted + const device = await getDb() + .selectFrom("user_devices") + .select(["is_trusted"]) + .where("id", "=", String(deviceId)) + .executeTakeFirstOrThrow(); + + expect(device.is_trusted).toBe(false); + }); + + test("fails to untrust non-existent device", async () => { + const user = await createTestUser(getDb(), { + email: "untrustnotfound@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await expect( + call(router.me.devices.untrust, { deviceId: 999999 }, { context }), + ).rejects.toThrow("Device not found"); + }); + + test("fails to untrust another user's device", async () => { + const user1 = await createTestUser(getDb(), { + email: "untrustuser1@example.com", + }); + const user2 = await createTestUser(getDb(), { + email: "untrustuser2@example.com", + }); + + const { deviceId } = await createDevice(user2.id, { isTrusted: true }); + + const { token: sessionToken } = await createSession(user1.id); + const context = createAPIContext({ sessionToken }); + + await expect( + call(router.me.devices.untrust, { deviceId }, { context }), + ).rejects.toThrow("Device not found"); + }); +}); + +describe("me.devices.revokeAll", () => { + test("untrusts all devices", async () => { + const user = await createTestUser(getDb(), { + email: "revokealldevices@example.com", + }); + + const { deviceId: id1 } = await createDevice(user.id, { isTrusted: true }); + const { deviceId: id2 } = await createDevice(user.id, { isTrusted: true }); + const { deviceId: id3 } = await createDevice(user.id, { isTrusted: false }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + await call(router.me.devices.revokeAll, undefined, { context }); + + // All devices should be untrusted + const devices = await getDb() + .selectFrom("user_devices") + .select(["id", "is_trusted"]) + .where("user_id", "=", user.id) + .execute(); + + expect(devices).toHaveLength(3); + expect(devices.every((d) => d.is_trusted === false)).toBe(true); + }); + + test("works when no devices exist", async () => { + const user = await createTestUser(getDb(), { + email: "revokenodevices@example.com", + }); + + const { token: sessionToken } = await createSession(user.id); + const context = createAPIContext({ sessionToken }); + + // Should not throw + await call(router.me.devices.revokeAll, undefined, { context }); + }); +}); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index 043125a..eca1c48 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -284,14 +284,18 @@ export const router = os.router({ rename: renamePasskey, delete: deletePasskey, }, - listSessions, - revokeSession, - revokeAllSessions, - getDeviceInfo, - trustDevice, - listTrustedDevices, - untrustDevice, - revokeAllTrustedDevices, + sessions: { + list: listSessions, + revoke: revokeSession, + revokeAll: revokeAllSessions, + }, + devices: { + getInfo: getDeviceInfo, + trust: trustDevice, + listTrusted: listTrustedDevices, + untrust: untrustDevice, + revokeAll: revokeAllTrustedDevices, + }, }, orgs: { list: orgsList, diff --git a/db/schema.sql b/db/schema.sql index b0d5ee9..fcda6b6 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,4 +1,4 @@ -\restrict KXTb98GlQCetYfS0eRd7LzGbBIiTxg53JFiqnSln3PIIhE3DD10jqFdLLY3AKZu +\restrict JcXyipc16dugUGJvd2oDJgA4cUi3A29rdzMF11XH6GPR94bG05YyvDzwhuyfGSd -- Dumped from database version 17.7 -- Dumped by pg_dump version 17.7 @@ -1084,7 +1084,7 @@ ALTER TABLE ONLY public.user_devices -- PostgreSQL database dump complete -- -\unrestrict KXTb98GlQCetYfS0eRd7LzGbBIiTxg53JFiqnSln3PIIhE3DD10jqFdLLY3AKZu +\unrestrict JcXyipc16dugUGJvd2oDJgA4cUi3A29rdzMF11XH6GPR94bG05YyvDzwhuyfGSd --