generated from rakenduste-programmeerimine-2024/template
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: update middleware, route handling, & validation to match the AP…
…I & RLS (#27)
- Loading branch information
1 parent
922e37c
commit d51aaa3
Showing
7 changed files
with
154 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,25 @@ | ||
import { verifyToken } from "@/utils/jwt" | ||
import { updateSession } from "@/utils/supabase/middleware" | ||
import { type NextRequest, NextResponse } from "next/server" | ||
import { NextRequest, NextResponse } from "next/server" | ||
import { getUserIdFromRequest } from "./utils/api/request-utils" | ||
|
||
const jwtConfig = { | ||
secret: new TextEncoder().encode(process.env.JWT_SECRET), | ||
} | ||
|
||
export async function middleware(request: NextRequest) { | ||
const userId = await getUserIdFromRequest(request) | ||
const requestHeaders = new Headers(request.headers) | ||
let userId = await getUserIdFromRequest(request) | ||
|
||
// Optionally attach userId to the request if present | ||
if (userId) { | ||
request.headers.set("x-user-id", userId) | ||
requestHeaders.set("x-user-id", userId) | ||
} else { | ||
console.info("No user ID found in token, proceeding as unauthenticated") | ||
requestHeaders.set("x-user-id", "anonymous") | ||
} | ||
|
||
return updateSession(request) // Continue with session logic | ||
} | ||
|
||
export const config = { | ||
matcher: [ | ||
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", | ||
], | ||
} | ||
|
||
/** | ||
* Extracts user ID from the request's authorization header. | ||
* Supports both `NextRequest` and `Request`. | ||
* @param req - The incoming request. | ||
* @returns User ID if valid, otherwise null. | ||
*/ | ||
export async function getUserIdFromRequest( | ||
req: Request | NextRequest, | ||
): Promise<string | null> { | ||
const authHeader = req.headers.get("Authorization") | ||
if (!authHeader || !authHeader.startsWith("Bearer ")) { | ||
return null | ||
} | ||
const response = NextResponse.next({ | ||
request: { | ||
headers: requestHeaders, | ||
}, | ||
}) | ||
|
||
const token = authHeader.slice(7) // Remove "Bearer " prefix | ||
try { | ||
const decoded = verifyToken<{ sub: string }>(token) | ||
return decoded.sub | ||
} catch { | ||
return null // Token verification failed | ||
} | ||
return response | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import * as jose from "jose" | ||
import { NextRequest, NextResponse } from "next/server" | ||
|
||
const jwtConfig = { | ||
secret: new TextEncoder().encode(process.env.JWT_SECRET), | ||
} | ||
|
||
export async function getUserIdFromRequest(request: NextRequest) { | ||
const token = request.headers.get("Authorization") | ||
let userId: string | null = null | ||
|
||
if (token) { | ||
try { | ||
const decoded = await jose.jwtVerify( | ||
token.replace("Bearer ", ""), | ||
jwtConfig.secret, | ||
) | ||
|
||
userId = decoded.payload.sub || null | ||
} catch (err) { | ||
console.error("Token verification failed:", err) | ||
} | ||
} | ||
|
||
return userId | ||
} | ||
|
||
export async function getUserIdOrError( | ||
request: NextRequest, | ||
): Promise<string | NextResponse> { | ||
const userId = await getUserIdFromRequest(request) | ||
|
||
if (!userId) { | ||
return NextResponse.json( | ||
{ error: "User is not authenticated" }, | ||
{ status: 401 }, | ||
) | ||
} | ||
|
||
return userId | ||
} | ||
|
||
export async function fetchUserId( | ||
baseUrl: string, | ||
entityId: string, | ||
pathName: string, | ||
): Promise<string | null> { | ||
try { | ||
const url = `${baseUrl}/api/${pathName}/${entityId}/user-id` | ||
const response = await fetch(url) | ||
const data = await response.json() | ||
|
||
if (response.ok && data?.user_id) { | ||
return data.user_id | ||
} else { | ||
return null | ||
} | ||
} catch (error) { | ||
console.error(`Error fetching user ID for resource:`, error) | ||
return null | ||
} | ||
} | ||
|
||
export async function getBaseUrl(request: NextRequest): Promise<string> { | ||
// Retrieve the protocol (http or https) and host from the headers | ||
const protocol = request.headers.get("x-forwarded-proto") || "http" // Default to "http" | ||
const host = request.headers.get("host") | ||
|
||
if (!host) { | ||
throw new Error("Host header is missing") | ||
} | ||
|
||
// Return the full base URL (e.g., http://localhost:3000) | ||
return `${protocol}://${host}` | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,53 @@ | ||
import { getUserIdFromRequest } from "@/middleware" | ||
import { createErrorResponse } from "./error-response" | ||
import { | ||
fetchUserId, | ||
getBaseUrl, | ||
getUserIdFromRequest, | ||
} from "@/utils/api/request-utils" | ||
import { createErrorResponse } from "@/utils/api/error-response" | ||
import { NextRequest, NextResponse } from "next/server" | ||
|
||
/** | ||
* Validates that the request contains a valid user token. | ||
* @param req - The incoming request. | ||
* @returns User ID if valid, or an error response if invalid. | ||
*/ | ||
export async function validateUserToken( | ||
req: Request | NextRequest, | ||
): Promise<string | NextResponse> { | ||
const userId = await getUserIdFromRequest(req) | ||
if (!userId) { | ||
return createErrorResponse("Unauthorized", 401) | ||
} | ||
return userId | ||
function getResourceTypeFromPath(path: string): string | null { | ||
const pathname = new URL(path).pathname | ||
const parts = pathname.split("/") | ||
const resourceType = parts[2] // Assuming the path is like '/api/resource-type/{id}' | ||
return resourceType || null | ||
} | ||
|
||
/** | ||
* Validates that the request contains a valid user token. | ||
* @param req - The incoming request. | ||
* @param expectedUserId - The expected user ID. | ||
* @returns The user ID if valid, or an error response if invalid. | ||
*/ | ||
export async function validateRequest( | ||
req: Request, | ||
expectedUserId: string, | ||
request: NextRequest, | ||
requiresAuth: boolean = true, | ||
resourceId: string | null = null, | ||
): Promise<string | NextResponse> { | ||
const userId = await getUserIdFromRequest(req as any); | ||
const userId = await getUserIdFromRequest(request) | ||
|
||
// Handle anonymous access if authentication is not required | ||
if (!userId) { | ||
return createErrorResponse("Unauthorized", 401); | ||
if (requiresAuth) { | ||
return createErrorResponse("Unauthorized", 401) // Authentication required | ||
} | ||
return NextResponse.next() // Allow anonymous access for read-only actions | ||
} | ||
|
||
if (userId !== expectedUserId) { | ||
return createErrorResponse("Forbidden", 403); | ||
// Enforce user ID check for authenticated actions | ||
if (resourceId) { | ||
const resourceType = getResourceTypeFromPath(request.url) | ||
if (!resourceType) { | ||
return createErrorResponse("Resource type not found", 404) | ||
} | ||
|
||
let expectedUserId: string | null = null | ||
|
||
if (resourceType === "profiles") { | ||
expectedUserId = resourceId | ||
} else { | ||
let baseUrl = await getBaseUrl(request) | ||
expectedUserId = await fetchUserId(baseUrl, resourceId, resourceType) | ||
} | ||
|
||
if (expectedUserId && userId !== expectedUserId) { | ||
return createErrorResponse("Forbidden", 403) // Forbidden if user IDs don't match | ||
} | ||
} | ||
|
||
return userId; | ||
return userId // Return valid user ID | ||
} |