Răsfoiți Sursa

[Auth] Migrate group + movie API routes to requireUser

Replaces direct supabase.auth.getUser() / cookie-derived session reads with
the requireUser/getCurrentUser server boundary. Routes now uniformly accept
self-minted JWTs via HS256.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User 1 lună în urmă
părinte
comite
ae18887c37

+ 3 - 7
src/app/api/groups/[id]/invite/route.ts

@@ -1,17 +1,13 @@
 import { NextRequest, NextResponse } from "next/server";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import { generateInviteCode } from "@/lib/groups/invite-code";
 
 /** POST /api/groups/[id]/invite — Regenerate invite code (admin only) */
-export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 3 - 7
src/app/api/groups/[id]/leave/route.ts

@@ -1,17 +1,13 @@
 import { NextRequest, NextResponse } from "next/server";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import { deleteGroupAndData } from "@/lib/groups/delete-group";
 
 /** POST /api/groups/[id]/leave — Leave a group */
-export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 4 - 12
src/app/api/groups/[id]/members/route.ts

@@ -1,21 +1,17 @@
 import { NextRequest, NextResponse } from "next/server";
 import { z } from "zod";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 
 const removeMemberSchema = z.object({
   user_id: z.string().uuid("Invalid user ID"),
 });
 
 /** GET /api/groups/[id]/members — List group members */
-export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }
@@ -56,11 +52,7 @@ export async function DELETE(
 ) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 5 - 17
src/app/api/groups/[id]/route.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { z } from "zod";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import { groupNameSchema } from "@/lib/groups/validation";
 import { deleteGroupAndData } from "@/lib/groups/delete-group";
 
@@ -11,11 +11,7 @@ const renameSchema = z.object({ name: groupNameSchema });
 export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }
@@ -50,11 +46,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
 export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }
@@ -98,16 +90,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
 
 /** DELETE /api/groups/[id] — Delete group (admin only) */
 export async function DELETE(
-  _request: NextRequest,
+  request: NextRequest,
   { params }: { params: Promise<{ id: string }> },
 ) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 2 - 6
src/app/api/groups/[id]/transfer/route.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { z } from "zod";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 
 const transferSchema = z.object({
   new_admin_id: z.string().uuid("Invalid user ID"),
@@ -11,11 +11,7 @@ const transferSchema = z.object({
 export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
   try {
     const { id } = await params;
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 2 - 6
src/app/api/groups/join/route.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { z } from "zod";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import { normalizeInviteCode } from "@/lib/groups/invite-code";
 import { checkRateLimit } from "@/lib/groups/rate-limit";
 
@@ -28,11 +28,7 @@ export async function POST(request: NextRequest) {
       );
     }
 
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 2 - 6
src/app/api/groups/route.ts

@@ -1,7 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { z } from "zod";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
 import { getSupabaseAdminClient } from "@/lib/supabase/admin";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import { generateInviteCode } from "@/lib/groups/invite-code";
 import { groupNameSchema } from "@/lib/groups/validation";
 
@@ -10,11 +10,7 @@ const createGroupSchema = z.object({ name: groupNameSchema });
 /** POST /api/groups — Create a new group */
 export async function POST(request: NextRequest) {
   try {
-    const supabase = await getSupabaseServerClient();
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
-
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

+ 4 - 4
src/app/api/movies/[id]/_helpers.ts

@@ -1,4 +1,5 @@
-import { NextResponse } from "next/server";
+import { NextResponse, type NextRequest } from "next/server";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import type { Database } from "@/types/database";
 
 type MovieRow = Database["public"]["Tables"]["movies"]["Row"];
@@ -12,10 +13,9 @@ export async function verifyMovieAccess(
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   supabase: any,
   movieId: string,
+  request: NextRequest,
 ): Promise<MovieAccessResult> {
-  const {
-    data: { user },
-  } = await supabase.auth.getUser();
+  const user = await getCurrentUser(request);
   if (!user) {
     return { ok: false, response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }) };
   }

+ 3 - 3
src/app/api/movies/[id]/route.ts

@@ -10,13 +10,13 @@ const patchSchema = z.object({
 });
 
 export async function DELETE(
-  _request: NextRequest,
+  request: NextRequest,
   { params }: { params: Promise<{ id: string }> },
 ) {
   const { id } = await params;
   const supabase = await getSupabaseServerClient();
 
-  const access = await verifyMovieAccess(supabase, id);
+  const access = await verifyMovieAccess(supabase, id, request);
   if (!access.ok) return access.response;
 
   const { error } = await supabase.from("movies").delete().eq("id", id);
@@ -31,7 +31,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
   const { id } = await params;
   const supabase = await getSupabaseServerClient();
 
-  const access = await verifyMovieAccess(supabase, id);
+  const access = await verifyMovieAccess(supabase, id, request);
   if (!access.ok) return access.response;
 
   const body = await request.json().catch(() => null);

+ 1 - 1
src/app/api/movies/[id]/watched/route.ts

@@ -11,7 +11,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
   const { id } = await params;
   const supabase = await getSupabaseServerClient();
 
-  const access = await verifyMovieAccess(supabase, id);
+  const access = await verifyMovieAccess(supabase, id, request);
   if (!access.ok) return access.response;
 
   const body = await request.json().catch(() => null);

+ 16 - 8
src/app/api/movies/route.ts

@@ -1,6 +1,7 @@
 import { NextResponse, type NextRequest } from "next/server";
 import { z } from "zod";
 import { getSupabaseServerClient } from "@/lib/supabase/server";
+import { getCurrentUser } from "@/lib/auth/current-user";
 import { env } from "@/env";
 import { TMDB_API_BASE_URL, TRAILER_DOMAIN_ALLOWLIST } from "@/lib/constants";
 import type { Database } from "@/types/database";
@@ -45,14 +46,11 @@ function isAllowedTrailerDomain(url: string): boolean {
 
 export async function POST(request: NextRequest) {
   try {
-    const supabase = await getSupabaseServerClient();
-
-    const {
-      data: { user },
-    } = await supabase.auth.getUser();
+    const user = await getCurrentUser(request);
     if (!user) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }
+    const supabase = await getSupabaseServerClient();
 
     const body = await request.json();
     const parsed = addMovieSchema.safeParse(body);
@@ -89,13 +87,23 @@ export async function POST(request: NextRequest) {
       return NextResponse.json({ error: "Movie already in group" }, { status: 409 });
     }
 
-    // Fetch movie details from TMDB
+    // Fetch movie details from TMDB. TMDB_API_KEY is a v4 read-access token,
+    // so it must be sent as `Authorization: Bearer …` — the v3 `?api_key=`
+    // query form silently 401s with v4 tokens.
+    const tmdbHeaders = {
+      Authorization: `Bearer ${env.TMDB_API_KEY}`,
+      Accept: "application/json",
+    };
     const [detailsRes, videosRes] = await Promise.all([
-      fetch(`${TMDB_API_BASE_URL}/movie/${tmdb_id}?api_key=${env.TMDB_API_KEY}`),
-      fetch(`${TMDB_API_BASE_URL}/movie/${tmdb_id}/videos?api_key=${env.TMDB_API_KEY}`),
+      fetch(`${TMDB_API_BASE_URL}/movie/${tmdb_id}`, { headers: tmdbHeaders }),
+      fetch(`${TMDB_API_BASE_URL}/movie/${tmdb_id}/videos`, { headers: tmdbHeaders }),
     ]);
 
     if (!detailsRes.ok) {
+      const body = await detailsRes.text().catch(() => "");
+      console.error(
+        `TMDB details fetch failed: ${detailsRes.status} tmdb_id=${tmdb_id} body=${body.slice(0, 300)}`,
+      );
       return NextResponse.json({ error: "Movie not found on TMDB" }, { status: 404 });
     }