mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-20 07:57:39 +02:00
Clean up auth
This commit is contained in:
@@ -21,7 +21,9 @@ const mockUser: SelectUser = {
|
||||
isActive: true,
|
||||
confirmed: true,
|
||||
token: null,
|
||||
tokenValidUntil: null
|
||||
tokenValidUntil: null,
|
||||
passphraseHash: null,
|
||||
recoveryPassphrase: null
|
||||
};
|
||||
|
||||
describe("JWT Utils", () => {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { error, json, type RequestHandler } from "@sveltejs/kit";
|
||||
import { z } from "zod";
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import { centralDb } from "$lib/server/db";
|
||||
import { tenant, user, userSession } from "$lib/server/db/central-schema";
|
||||
import { tenant, user } from "$lib/server/db/central-schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ValidationError, NotFoundError } from "$lib/server/utils/errors";
|
||||
import { UserService } from "$lib/server/services/user-service";
|
||||
import { generateTokens } from "$lib/server/auth/jwt-utils";
|
||||
import { generateAccessToken } from "$lib/server/auth/jwt-utils";
|
||||
import { UniversalLogger } from "$lib/logger";
|
||||
import { registerOpenAPIRoute } from "$lib/server/openapi";
|
||||
|
||||
@@ -59,86 +58,26 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
|
||||
throw error(404, "Tenant not found");
|
||||
}
|
||||
|
||||
// Get current session token from cookie
|
||||
const sessionToken = cookies.get("session");
|
||||
if (!sessionToken) {
|
||||
logger.error("No session token found in cookies", { userId: locals.user.id });
|
||||
throw error(401, "No valid session found");
|
||||
}
|
||||
|
||||
// Get current session data
|
||||
const sessionData = await SessionService.validateSession(sessionToken);
|
||||
if (!sessionData) {
|
||||
logger.error("Invalid session token", { userId: locals.user.id });
|
||||
throw error(401, "Invalid session");
|
||||
}
|
||||
// Current user is already authenticated via authHandle
|
||||
|
||||
// Update user's active tenant in the database
|
||||
const updatedUser = await UserService.updateUser(locals.user.id, {
|
||||
const updatedUser = await UserService.updateUser(locals.user.userId as string, {
|
||||
tenantId: tenantId || null
|
||||
});
|
||||
|
||||
if (!updatedUser) {
|
||||
logger.error("Failed to update user tenant", {
|
||||
userId: locals.user.id,
|
||||
userId: locals.user.userId,
|
||||
tenantId
|
||||
});
|
||||
throw error(500, "Failed to update user data");
|
||||
}
|
||||
|
||||
// Get the session ID from the database
|
||||
const sessionInfo = await centralDb
|
||||
.select({ id: userSession.id })
|
||||
.from(userSession)
|
||||
.where(eq(userSession.sessionToken, sessionData.sessionToken))
|
||||
.limit(1);
|
||||
// Generate new access token with updated tenant context
|
||||
const newAccessToken = await generateAccessToken(updatedUser, (locals.user.sessionId as string) || "temp-session");
|
||||
|
||||
if (sessionInfo.length === 0) {
|
||||
logger.error("Session not found in database", { userId: locals.user.id });
|
||||
throw error(500, "Session not found");
|
||||
}
|
||||
|
||||
// Generate new tokens with updated tenant context
|
||||
const newTokens = await generateTokens(updatedUser, sessionInfo[0].id);
|
||||
|
||||
// Update session tokens in the database
|
||||
await centralDb
|
||||
.update(user)
|
||||
.set({
|
||||
tenantId: tenantId || null,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.id, locals.user.id));
|
||||
|
||||
// Update the user session with new tokens
|
||||
await centralDb
|
||||
.update(userSession)
|
||||
.set({
|
||||
accessToken: newTokens.accessToken,
|
||||
refreshToken: newTokens.refreshToken,
|
||||
lastUsedAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(userSession.sessionToken, sessionData.sessionToken));
|
||||
|
||||
// Set new cookies with updated tokens
|
||||
cookies.set("session", sessionData.sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
});
|
||||
|
||||
cookies.set("accessToken", newTokens.accessToken, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 15 // 15 minutes
|
||||
});
|
||||
|
||||
cookies.set("refreshToken", newTokens.refreshToken, {
|
||||
// Set new access token cookie
|
||||
cookies.set("access_token", newAccessToken, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
@@ -147,7 +86,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
|
||||
});
|
||||
|
||||
logger.info("Tenant switched successfully", {
|
||||
userId: locals.user.id,
|
||||
userId: locals.user.userId,
|
||||
fromTenant: locals.user.tenantId,
|
||||
toTenant: tenantId
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ vi.mock("$lib/server/services/user-service", () => ({
|
||||
|
||||
// Mock JWT utils
|
||||
vi.mock("$lib/server/auth/jwt-utils", () => ({
|
||||
generateTokens: vi.fn()
|
||||
generateAccessToken: vi.fn()
|
||||
}));
|
||||
|
||||
// Mock universal logger
|
||||
@@ -92,7 +92,7 @@ vi.mock("$lib/server/utils/errors", () => ({
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import { centralDb } from "$lib/server/db";
|
||||
import { UserService } from "$lib/server/services/user-service";
|
||||
import { generateTokens } from "$lib/server/auth/jwt-utils";
|
||||
import { generateAccessToken } from "$lib/server/auth/jwt-utils";
|
||||
|
||||
describe("POST /api/admin/tenant", () => {
|
||||
const mockUser = {
|
||||
@@ -107,7 +107,9 @@ describe("POST /api/admin/tenant", () => {
|
||||
updatedAt: new Date(),
|
||||
lastLoginAt: null,
|
||||
token: null,
|
||||
tokenValidUntil: null
|
||||
tokenValidUntil: null,
|
||||
passphraseHash: null,
|
||||
recoveryPassphrase: null
|
||||
};
|
||||
|
||||
const mockSessionData = {
|
||||
@@ -130,7 +132,7 @@ describe("POST /api/admin/tenant", () => {
|
||||
request: {
|
||||
json: () => Promise.resolve(body)
|
||||
} as Request,
|
||||
locals: { user },
|
||||
locals: { user: { ...user, sessionId: "session-123" } },
|
||||
cookies: mockCookies,
|
||||
params: {},
|
||||
url: new URL("http://localhost/api/admin/tenant"),
|
||||
@@ -159,25 +161,14 @@ describe("POST /api/admin/tenant", () => {
|
||||
};
|
||||
vi.mocked(centralDb.select).mockReturnValue(mockSelectQuery as any);
|
||||
|
||||
// Mock session cookie
|
||||
mockCookies.get.mockReturnValue("session-123");
|
||||
|
||||
// Mock session validation
|
||||
const mockValidateSession = vi.mocked(SessionService.validateSession);
|
||||
mockValidateSession.mockResolvedValue(mockSessionData as any);
|
||||
|
||||
// Mock session ID lookup
|
||||
mockSelectQuery.limit.mockResolvedValueOnce([{ id: "session-id-123" }]);
|
||||
// No session handling needed anymore
|
||||
|
||||
// Mock user update
|
||||
const updatedUser = { ...mockUser, tenantId };
|
||||
vi.mocked(UserService.updateUser).mockResolvedValue(updatedUser as any);
|
||||
|
||||
// Mock token generation
|
||||
vi.mocked(generateTokens).mockResolvedValue({
|
||||
accessToken: "new-access-123",
|
||||
refreshToken: "new-refresh-123"
|
||||
});
|
||||
vi.mocked(generateAccessToken).mockResolvedValue("new-access-123");
|
||||
|
||||
// Mock database update queries
|
||||
const mockUpdateQuery = {
|
||||
@@ -193,17 +184,11 @@ describe("POST /api/admin/tenant", () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tenantId).toBe(tenantId);
|
||||
expect(result.user.tenantId).toBe(tenantId);
|
||||
expect(mockCookies.set).toHaveBeenCalledWith("session", "session-123", expect.any(Object));
|
||||
expect(mockCookies.set).toHaveBeenCalledWith(
|
||||
"accessToken",
|
||||
"access_token",
|
||||
"new-access-123",
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(mockCookies.set).toHaveBeenCalledWith(
|
||||
"refreshToken",
|
||||
"new-refresh-123",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 401 when user is not authenticated", async () => {
|
||||
|
||||
@@ -86,14 +86,13 @@ registerOpenAPIRoute("/auth/login", "POST", {
|
||||
},
|
||||
required: ["id", "email", "name", "role"]
|
||||
},
|
||||
accessToken: { type: "string", description: "JWT access token" },
|
||||
expiresAt: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
description: "Session expiration time"
|
||||
}
|
||||
},
|
||||
required: ["message", "user", "accessToken", "expiresAt"]
|
||||
required: ["message", "user", "expiresAt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,8 +272,8 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress,
|
||||
userAgent || undefined
|
||||
);
|
||||
|
||||
// Set HTTP-only cookie for session
|
||||
cookies.set("session", sessionData.sessionToken, {
|
||||
// Set HTTP-only cookie for access token
|
||||
cookies.set("access_token", sessionData.accessToken, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
@@ -298,7 +297,6 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress,
|
||||
role: sessionData.user.role,
|
||||
tenantId: sessionData.user.tenantId
|
||||
},
|
||||
accessToken: sessionData.accessToken,
|
||||
expiresAt: sessionData.expiresAt.toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { registerOpenAPIRoute } from "$lib/server/openapi";
|
||||
import { UniversalLogger } from "$lib/logger";
|
||||
@@ -8,7 +7,7 @@ const logger = new UniversalLogger().setContext("AuthLogoutAPI");
|
||||
|
||||
registerOpenAPIRoute("/auth/logout", "POST", {
|
||||
summary: "Logout user session",
|
||||
description: "Invalidate current user session and clear session cookie",
|
||||
description: "Invalidate current user session and clear access token cookie",
|
||||
tags: ["Authentication"],
|
||||
responses: {
|
||||
"200": {
|
||||
@@ -26,7 +25,7 @@ registerOpenAPIRoute("/auth/logout", "POST", {
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
description: "No active session found",
|
||||
description: "No access token found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
@@ -46,15 +45,14 @@ registerOpenAPIRoute("/auth/logout", "POST", {
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies }) => {
|
||||
try {
|
||||
const sessionToken = cookies.get("session");
|
||||
const accessToken = cookies.get("access_token");
|
||||
|
||||
if (!sessionToken) {
|
||||
return json({ error: "No active session found" }, { status: 400 });
|
||||
if (!accessToken) {
|
||||
return json({ error: "No access token found" }, { status: 400 });
|
||||
}
|
||||
|
||||
await SessionService.logout(sessionToken);
|
||||
|
||||
cookies.delete("session", {
|
||||
// Simply clear the access token cookie - no need to invalidate sessions
|
||||
cookies.delete("access_token", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
|
||||
@@ -8,25 +8,8 @@ const logger = new UniversalLogger().setContext("AuthRefreshAPI");
|
||||
|
||||
registerOpenAPIRoute("/auth/refresh", "POST", {
|
||||
summary: "Refresh access token",
|
||||
description: "Generate new access and refresh tokens using existing refresh token",
|
||||
description: "Generate new access and refresh tokens using the current session",
|
||||
tags: ["Authentication"],
|
||||
requestBody: {
|
||||
description: "Refresh token data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
refreshToken: {
|
||||
type: "string",
|
||||
description: "Current refresh token"
|
||||
}
|
||||
},
|
||||
required: ["refreshToken"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Token refresh successful",
|
||||
@@ -36,17 +19,15 @@ registerOpenAPIRoute("/auth/refresh", "POST", {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: { type: "string", description: "Success message" },
|
||||
accessToken: { type: "string", description: "New JWT access token" },
|
||||
refreshToken: { type: "string", description: "New refresh token" },
|
||||
expiresAt: { type: "string", format: "date-time", description: "New expiration time" }
|
||||
},
|
||||
required: ["message", "accessToken", "refreshToken", "expiresAt"]
|
||||
required: ["message", "expiresAt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
description: "Invalid refresh token",
|
||||
description: "No valid session found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
@@ -72,16 +53,26 @@ registerOpenAPIRoute("/auth/refresh", "POST", {
|
||||
}
|
||||
});
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
export const POST: RequestHandler = async ({ locals, cookies }) => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { refreshToken } = body;
|
||||
// Get current access token from cookie to extract session info
|
||||
const accessToken = cookies.get("access_token");
|
||||
|
||||
if (!refreshToken) {
|
||||
return json({ error: "Refresh token is required" }, { status: 400 });
|
||||
if (!accessToken) {
|
||||
return json({ error: "Access token is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await SessionService.refreshSession(refreshToken);
|
||||
// The user object is already available from authHandle,
|
||||
// so we can create a new session based on that
|
||||
if (!locals.user) {
|
||||
return json({ error: "Invalid user context" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await SessionService.createSession(
|
||||
locals.user.userId as string,
|
||||
"", // IP address - could be extracted from request if needed
|
||||
undefined // user agent
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return json({ error: "Invalid or expired refresh token" }, { status: 401 });
|
||||
@@ -89,10 +80,17 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
|
||||
logger.info("Token refresh successful");
|
||||
|
||||
// Set HTTP-only cookie for new access token
|
||||
cookies.set("access_token", result.accessToken, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
});
|
||||
|
||||
return json({
|
||||
message: "Token refresh successful",
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
expiresAt: result.expiresAt.toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { registerOpenAPIRoute } from "$lib/server/openapi";
|
||||
import { UniversalLogger } from "$lib/logger";
|
||||
@@ -67,41 +66,28 @@ registerOpenAPIRoute("/auth/session", "GET", {
|
||||
}
|
||||
});
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
try {
|
||||
const sessionToken = cookies.get("session");
|
||||
|
||||
if (!sessionToken) {
|
||||
return json({ authenticated: false, message: "No session found" }, { status: 401 });
|
||||
// User is already authenticated via authHandle
|
||||
if (!locals.user) {
|
||||
return json({ authenticated: false, message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await SessionService.validateSession(sessionToken);
|
||||
|
||||
if (!sessionData) {
|
||||
cookies.delete("session", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict"
|
||||
});
|
||||
return json({ authenticated: false, message: "Invalid or expired session" }, { status: 401 });
|
||||
}
|
||||
|
||||
logger.debug("Session validated", { userId: sessionData.user.id });
|
||||
logger.debug("Session check", { userId: locals.user.userId });
|
||||
|
||||
return json({
|
||||
authenticated: true,
|
||||
user: {
|
||||
id: sessionData.user.id,
|
||||
email: sessionData.user.email,
|
||||
name: sessionData.user.name,
|
||||
role: sessionData.user.role,
|
||||
tenantId: sessionData.user.tenantId
|
||||
id: locals.user.userId,
|
||||
email: locals.user.email,
|
||||
name: locals.user.name,
|
||||
role: locals.user.role,
|
||||
tenantId: locals.user.tenantId
|
||||
},
|
||||
expiresAt: sessionData.expiresAt.toISOString()
|
||||
expiresAt: new Date(locals.user.exp! * 1000).toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Session validation error:", { error: String(error) });
|
||||
logger.error("Session check error:", { error: String(error) });
|
||||
return json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { registerOpenAPIRoute } from "$lib/server/openapi";
|
||||
import { UniversalLogger } from "$lib/logger";
|
||||
@@ -109,72 +108,54 @@ registerOpenAPIRoute("/auth/sessions", "DELETE", {
|
||||
}
|
||||
});
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
try {
|
||||
const sessionToken = cookies.get("session");
|
||||
|
||||
if (!sessionToken) {
|
||||
// With access token-only approach, we don't track active sessions
|
||||
// Return current token info only
|
||||
if (!locals.user) {
|
||||
return json({ error: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await SessionService.validateSession(sessionToken);
|
||||
const currentSession = {
|
||||
id: locals.user.sessionId || "current",
|
||||
ipAddress: "unknown", // Would need to be tracked separately
|
||||
userAgent: "unknown", // Would need to be tracked separately
|
||||
createdAt: new Date(locals.user.iat! * 1000).toISOString(),
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
expiresAt: new Date(locals.user.exp! * 1000).toISOString(),
|
||||
current: true
|
||||
};
|
||||
|
||||
if (!sessionData) {
|
||||
return json({ error: "Invalid session" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessions = await SessionService.getActiveSessions(sessionData.user.id);
|
||||
|
||||
const formattedSessions = sessions.map((session) => ({
|
||||
id: session.id,
|
||||
ipAddress: session.ipAddress,
|
||||
userAgent: session.userAgent,
|
||||
createdAt: session.createdAt?.toISOString(),
|
||||
lastUsedAt: session.lastUsedAt?.toISOString(),
|
||||
expiresAt: session.expiresAt.toISOString(),
|
||||
current: session.sessionToken === sessionToken
|
||||
}));
|
||||
|
||||
logger.debug("Active sessions retrieved", {
|
||||
userId: sessionData.user.id,
|
||||
sessionCount: sessions.length
|
||||
logger.debug("Current session info retrieved", {
|
||||
userId: locals.user.userId
|
||||
});
|
||||
|
||||
return json({ sessions: formattedSessions });
|
||||
return json({ sessions: [currentSession] });
|
||||
} catch (error) {
|
||||
logger.error("Get sessions error:", { error: String(error) });
|
||||
return json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ cookies }) => {
|
||||
export const DELETE: RequestHandler = async ({ locals, cookies }) => {
|
||||
try {
|
||||
const sessionToken = cookies.get("session");
|
||||
|
||||
if (!sessionToken) {
|
||||
// With access token-only approach, just clear the current token
|
||||
if (!locals.user) {
|
||||
return json({ error: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await SessionService.validateSession(sessionToken);
|
||||
|
||||
if (!sessionData) {
|
||||
return json({ error: "Invalid session" }, { status: 401 });
|
||||
}
|
||||
|
||||
await SessionService.logoutAllSessions(sessionData.user.id);
|
||||
|
||||
cookies.delete("session", {
|
||||
cookies.delete("access_token", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict"
|
||||
});
|
||||
|
||||
logger.info("All sessions logged out", { userId: sessionData.user.id });
|
||||
logger.info("Access token cleared", { userId: locals.user.userId });
|
||||
|
||||
return json({ message: "All sessions logged out successfully" });
|
||||
return json({ message: "Logged out successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Logout all sessions error:", { error: String(error) });
|
||||
logger.error("Logout error:", { error: String(error) });
|
||||
return json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { registerOpenAPIRoute } from "$lib/server/openapi";
|
||||
import { UniversalLogger } from "$lib/logger";
|
||||
@@ -69,46 +68,32 @@ registerOpenAPIRoute("/auth/sessions/{sessionId}", "DELETE", {
|
||||
}
|
||||
});
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, cookies }) => {
|
||||
export const DELETE: RequestHandler = async ({ params, locals, cookies }) => {
|
||||
try {
|
||||
const sessionToken = cookies.get("session");
|
||||
const sessionIdToRevoke = params.sessionId;
|
||||
|
||||
if (!sessionToken) {
|
||||
if (!locals.user) {
|
||||
return json({ error: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
const sessionData = await SessionService.validateSession(sessionToken);
|
||||
|
||||
if (!sessionData) {
|
||||
return json({ error: "Invalid session" }, { status: 401 });
|
||||
// With access token-only approach, we can only revoke the current token
|
||||
const currentSessionId = locals.user.sessionId || "current";
|
||||
|
||||
if (sessionIdToRevoke !== currentSessionId) {
|
||||
return json({ error: "Can only revoke current session" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Get all user sessions to verify ownership
|
||||
const userSessions = await SessionService.getActiveSessions(sessionData.user.id);
|
||||
const targetSession = userSessions.find((session) => session.id === sessionIdToRevoke);
|
||||
// Clear access token cookie
|
||||
cookies.delete("access_token", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict"
|
||||
});
|
||||
|
||||
if (!targetSession) {
|
||||
return json({ error: "Session not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if trying to revoke current session
|
||||
if (targetSession.sessionToken === sessionToken) {
|
||||
// Clear current session cookie
|
||||
cookies.delete("session", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict"
|
||||
});
|
||||
}
|
||||
|
||||
await SessionService.revokeSession(sessionIdToRevoke);
|
||||
|
||||
logger.info("Session revoked", {
|
||||
userId: sessionData.user.id,
|
||||
revokedSessionId: sessionIdToRevoke,
|
||||
currentSession: targetSession.sessionToken === sessionToken
|
||||
logger.info("Access token revoked", {
|
||||
userId: locals.user.userId,
|
||||
revokedSessionId: sessionIdToRevoke
|
||||
});
|
||||
|
||||
return json({ message: "Session revoked successfully" });
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import { SessionService } from "$lib/server/auth/session-service";
|
||||
import { verifyAccessToken } from "$lib/server/auth/jwt-utils";
|
||||
import { UniversalLogger } from "$lib/logger";
|
||||
import type { JWTPayload } from "jose";
|
||||
import { AuthorizationService } from "$lib/server/auth/authorization-service";
|
||||
|
||||
const logger = new UniversalLogger().setContext("AuthHandle");
|
||||
|
||||
const SESSION_COOKIE_NAME = "session";
|
||||
const ACCESS_TOKEN_COOKIE_NAME = "access_token";
|
||||
const PROTECTED_PATHS = ["/api/admin", "/api/tenant-admin", "/api/tenants", "/api/auth/register"];
|
||||
const PUBLIC_PATHS = [
|
||||
"/api/auth/challenge",
|
||||
@@ -60,20 +58,23 @@ export const authHandle: Handle = async ({ event, resolve }) => {
|
||||
);
|
||||
}
|
||||
|
||||
let sessionToken: string | null = null;
|
||||
let accessToken: string | null = null;
|
||||
|
||||
const sessionCookie = event.cookies.get(SESSION_COOKIE_NAME);
|
||||
if (sessionCookie) {
|
||||
sessionToken = sessionCookie;
|
||||
// Get access token from cookie
|
||||
const accessTokenCookie = event.cookies.get(ACCESS_TOKEN_COOKIE_NAME);
|
||||
if (accessTokenCookie) {
|
||||
accessToken = accessTokenCookie;
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
accessToken = authHeader.substring(7);
|
||||
// Fallback: check Authorization header
|
||||
if (!accessToken) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
accessToken = authHeader.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionToken && !accessToken) {
|
||||
if (!accessToken) {
|
||||
logger.warn(`Authentication required for ${path}`);
|
||||
return new Response(JSON.stringify({ error: "Authentication required" }), {
|
||||
status: 401,
|
||||
@@ -81,39 +82,17 @@ export const authHandle: Handle = async ({ event, resolve }) => {
|
||||
});
|
||||
}
|
||||
|
||||
let user: JWTPayload | null = null;
|
||||
|
||||
if (accessToken) {
|
||||
user = await verifyAccessToken(accessToken);
|
||||
if (!user) {
|
||||
logger.warn(`Invalid access token for ${path}`);
|
||||
return new Response(JSON.stringify({ error: "Invalid access token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
} else if (sessionToken) {
|
||||
const sessionData = await SessionService.validateSession(sessionToken);
|
||||
if (!sessionData) {
|
||||
logger.warn(`Invalid session for ${path}`);
|
||||
return new Response(JSON.stringify({ error: "Invalid session" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
user = await verifyAccessToken(sessionData.accessToken);
|
||||
}
|
||||
|
||||
// Verify access token
|
||||
const user = await verifyAccessToken(accessToken);
|
||||
if (!user) {
|
||||
logger.warn(`Authentication failed for ${path}`);
|
||||
return new Response(JSON.stringify({ error: "Authentication failed" }), {
|
||||
logger.warn(`Invalid access token for ${path}`);
|
||||
return new Response(JSON.stringify({ error: "Invalid access token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
event.locals.user = user;
|
||||
event.locals.sessionToken = sessionToken || undefined;
|
||||
|
||||
if (isGlobalAdminPath && !AuthorizationService.hasRole(user, "GLOBAL_ADMIN")) {
|
||||
return new Response(JSON.stringify({ error: "Authentication failed" }), {
|
||||
|
||||
Reference in New Issue
Block a user