Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | export const dynamic = "force-dynamic"; /** * User Sessions API Route * * Manages user sessions for the account security page. * GET - Get active sessions * DELETE - Revoke sessions */ import { NextRequest, NextResponse } from "next/server"; import { Session } from "next-auth"; import { withAuth, withErrorHandling, successResponse, errorResponse, ApiSuccessResponse, ApiErrorResponse, RouteContext, } from "@/lib/api"; import { getUserSessions, revokeSession, parseDeviceInfo, getActiveSessionCount, } from "@/lib/auth/session-security"; import { z } from "zod"; interface SessionInfo { id: string; browser: string; os: string; device: string; ipAddress: string | null; lastActive: Date | null; expires: Date; isCurrent: boolean; } interface SessionsResponse { sessions: SessionInfo[]; totalCount: number; } const revokeSessionSchema = z.object({ sessionToken: z.string().min(1).optional(), revokeAll: z.boolean().optional(), }); /** * GET /api/user/sessions * Get active sessions for current user */ async function handleGet( request: NextRequest, _context: RouteContext | undefined, session: Session ): Promise< NextResponse<ApiSuccessResponse<SessionsResponse> | ApiErrorResponse> > { const userId = session.user?.id; if (!userId) { return errorResponse("UNAUTHORIZED", "User ID not found", { status: 401 }); } // Get current session token from cookie const sessionToken = request.cookies.get("next-auth.session-token")?.value || request.cookies.get("__Secure-next-auth.session-token")?.value; const [sessions, totalCount] = await Promise.all([ getUserSessions(Number(userId), sessionToken), getActiveSessionCount(Number(userId)), ]); const formattedSessions: SessionInfo[] = sessions.map((s) => { const deviceInfo = parseDeviceInfo(s.userAgent); return { id: s.id, browser: deviceInfo.browser, os: deviceInfo.os, device: deviceInfo.device, ipAddress: s.ipAddress, lastActive: s.lastActive, expires: s.expires, isCurrent: s.isCurrent, }; }); return successResponse({ sessions: formattedSessions, totalCount, }); } /** * DELETE /api/user/sessions * Revoke one or all sessions */ async function handleDelete( request: NextRequest, _context: RouteContext | undefined, session: Session ): Promise<NextResponse<ApiSuccessResponse<{ success: boolean }> | ApiErrorResponse>> { const userId = session.user?.id; if (!userId) { return errorResponse("UNAUTHORIZED", "User ID not found", { status: 401 }); } const body = await request.json(); const validation = revokeSessionSchema.safeParse(body); if (!validation.success) { return errorResponse("VALIDATION_ERROR", validation.error.issues[0].message, { status: 400 }); } const { sessionToken, revokeAll } = validation.data; if (revokeAll) { // Revoke all sessions except current const currentToken = request.cookies.get("next-auth.session-token")?.value || request.cookies.get("__Secure-next-auth.session-token")?.value; const sessions = await getUserSessions(Number(userId)); for (const s of sessions) { if (s.sessionToken !== currentToken) { await revokeSession(s.sessionToken); } } return successResponse({ success: true }); } if (sessionToken) { // Revoke specific session // First verify it belongs to this user const sessions = await getUserSessions(Number(userId)); const sessionToRevoke = sessions.find((s) => s.sessionToken === sessionToken); if (!sessionToRevoke) { return errorResponse("NOT_FOUND", "Session not found", { status: 404 }); } if (sessionToRevoke.isCurrent) { return errorResponse("FORBIDDEN", "Cannot revoke current session", { status: 400 }); } await revokeSession(sessionToken); return successResponse({ success: true }); } return errorResponse("VALIDATION_ERROR", "Must provide sessionToken or revokeAll", { status: 400 }); } export const GET = withErrorHandling(withAuth(handleGet)); export const DELETE = withErrorHandling(withAuth(handleDelete)); |