Put images behind the session and make routes private by default
The /minio-images rewrite proxied straight onto the MinIO bucket with no authentication, and the bucket carries an anonymous read policy. Verified from the public internet: GET /minio-images?list-type=2 returned a full ListBucketResult naming all 33 objects, and any key then fetched 200. Every menu scan, drink photo and bar photo was enumerable and downloadable by anyone. Replaced with a route handler that requires a session and streams via the existing getImage(). The URL shape is unchanged, so stored imageUrls, uploadImage/getImageUrl and every <img> keep working. Nothing uses next/image and it must stay that way here: the optimizer fetches server-side without the session cookie. Route protection was an allowlist that had to be updated by hand for each new page, and had already been missed for /bar, /bartender and /recommend, which rendered to logged-out visitors. /recipes was in the middleware matcher but not the authorized() list, so it fell through too. Inverted both to a denylist so new pages are private by default. /api stays out of the matcher because authorized() answers with an HTML redirect and API clients need the JSON 401 those routes already return. Also fixes a cross-user write: POST /api/recipes took sourceDrinkId from the client with no ownership check, and the drink detail page loaded its recipes relation unfiltered, so one user could attach an arbitrary recipe to another user's drink where it rendered permanently and the owner could not delete it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,13 @@ import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { rateLimit } from "@/lib/rate-limit"
|
||||
|
||||
/**
|
||||
* The only routes reachable without a session. Matched exactly or as a path
|
||||
* prefix, so `/share` also covers `/share/<slug>`. Everything else is private -
|
||||
* see the `authorized` callback and the denylist matcher in src/middleware.ts.
|
||||
*/
|
||||
const PUBLIC_ROUTES = ["/login", "/register", "/share"]
|
||||
|
||||
const providers = [
|
||||
Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
@@ -66,19 +73,18 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
},
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
const isLoggedIn = !!auth?.user
|
||||
const isOnApp = nextUrl.pathname.startsWith("/dashboard") ||
|
||||
nextUrl.pathname.startsWith("/scan") ||
|
||||
nextUrl.pathname.startsWith("/drinks") ||
|
||||
nextUrl.pathname.startsWith("/rate") ||
|
||||
nextUrl.pathname.startsWith("/settings") ||
|
||||
nextUrl.pathname.startsWith("/wishlist")
|
||||
const { pathname } = nextUrl
|
||||
|
||||
if (isOnApp) {
|
||||
if (isLoggedIn) return true
|
||||
return false
|
||||
}
|
||||
// Everything is private unless named here. Paired with the denylist matcher
|
||||
// in middleware.ts, so a new page is protected by default rather than public
|
||||
// until somebody remembers to add it.
|
||||
const isPublic = PUBLIC_ROUTES.some(
|
||||
(p) => pathname === p || pathname.startsWith(`${p}/`)
|
||||
)
|
||||
|
||||
if (isLoggedIn && (nextUrl.pathname === "/login" || nextUrl.pathname === "/register")) {
|
||||
if (!isPublic && !isLoggedIn) return false
|
||||
|
||||
if (isLoggedIn && (pathname === "/login" || pathname === "/register")) {
|
||||
return Response.redirect(new URL("/dashboard", nextUrl))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user