- Update API contract to use nested router structure for sessions and devices (me.sessions.list, me.devices.getInfo, etc.) - Update frontend Svelte components to use new nested API paths - Fix test assertion patterns for consistency (remove async () => wrappers) - Fix test-db.ts findRepoRoot to use existsSync for directory checking (Bun.file().exists() returns false for directories) - Add ESLint config override for test files to handle expect().rejects patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
91 lines
2.5 KiB
TypeScript
91 lines
2.5 KiB
TypeScript
/**
|
|
* Session management procedures - list, revoke, revokeAll sessions
|
|
*/
|
|
|
|
import { ORPCError } from "@orpc/server";
|
|
import { authMiddleware, os } from "../base.js";
|
|
|
|
/**
|
|
* List sessions handler
|
|
* - Requires authentication
|
|
* - Returns all sessions for the current user
|
|
* - Includes isCurrent flag to identify active session
|
|
*/
|
|
export const listSessions = os.me.sessions.list
|
|
.use(authMiddleware)
|
|
.handler(async ({ context }) => {
|
|
const sessions = await context.db
|
|
.selectFrom("sessions")
|
|
.selectAll()
|
|
.where("user_id", "=", context.user.id)
|
|
.orderBy("created_at", "desc")
|
|
.execute();
|
|
|
|
return sessions.map((s) => ({
|
|
id: Number(s.id),
|
|
ip: s.ip_address ?? "",
|
|
city: s.city,
|
|
region: s.region,
|
|
country: s.country,
|
|
userAgent: s.user_agent ?? "",
|
|
trustedMode: s.trusted_mode,
|
|
createdAt: s.created_at,
|
|
isCurrent: s.id === context.session.id,
|
|
revokedAt: s.revoked_at,
|
|
}));
|
|
});
|
|
|
|
/**
|
|
* Revoke session handler
|
|
* - Requires authentication
|
|
* - Cannot revoke current session (use logout instead)
|
|
* @throws NOT_FOUND if session doesn't exist
|
|
* @throws BAD_REQUEST if trying to revoke current session
|
|
*/
|
|
export const revokeSession = os.me.sessions.revoke
|
|
.use(authMiddleware)
|
|
.handler(async ({ input, context }) => {
|
|
const { sessionId } = input;
|
|
|
|
// Prevent revoking current session (use logout instead)
|
|
if (String(sessionId) === context.session.id) {
|
|
throw new ORPCError("BAD_REQUEST", {
|
|
message: "Cannot revoke current session. Use logout instead.",
|
|
});
|
|
}
|
|
|
|
const result = await context.db
|
|
.updateTable("sessions")
|
|
.set({ revoked_at: new Date() })
|
|
.where("id", "=", String(sessionId))
|
|
.where("user_id", "=", context.user.id)
|
|
.where("revoked_at", "is", null)
|
|
.executeTakeFirst();
|
|
|
|
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
|
|
throw new ORPCError("NOT_FOUND", { message: "Session not found" });
|
|
}
|
|
|
|
return { success: true };
|
|
});
|
|
|
|
/**
|
|
* Revoke all sessions handler
|
|
* - Requires authentication
|
|
* - Revokes all sessions except current
|
|
*/
|
|
export const revokeAllSessions = os.me.sessions.revokeAll
|
|
.use(authMiddleware)
|
|
.handler(async ({ context }) => {
|
|
// Revoke all sessions except current
|
|
await context.db
|
|
.updateTable("sessions")
|
|
.set({ revoked_at: new Date() })
|
|
.where("user_id", "=", context.user.id)
|
|
.where("id", "!=", context.session.id)
|
|
.where("revoked_at", "is", null)
|
|
.execute();
|
|
|
|
return { success: true };
|
|
});
|