Skip to content

Commit 6c9d1df

Browse files
committed
tighten supabase read access
1 parent a29d336 commit 6c9d1df

5 files changed

Lines changed: 498 additions & 54 deletions

File tree

src/app/actions.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,11 @@ export const deleteListingAction = async (
633633
const {
634634
data: { user },
635635
} = await supabase.auth.getUser();
636+
const {
637+
data: { session },
638+
} = await supabase.auth.getSession();
636639

637-
if (!user) {
640+
if (!user || !session?.access_token) {
638641
return redirect("/sign-in");
639642
}
640643
// Then continue with the delete listing
@@ -644,10 +647,11 @@ export const deleteListingAction = async (
644647
{
645648
method: "POST",
646649
headers: {
647-
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
650+
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
651+
Authorization: `Bearer ${session.access_token}`,
648652
"Content-Type": "application/json",
649653
},
650-
body: JSON.stringify({ slug }), // Send the slug in the request body
654+
body: JSON.stringify({ slug }),
651655
}
652656
);
653657

@@ -689,8 +693,11 @@ export const deleteAccountAction = async () => {
689693
const {
690694
data: { user },
691695
} = await supabase.auth.getUser();
696+
const {
697+
data: { session },
698+
} = await supabase.auth.getSession();
692699

693-
if (!user) {
700+
if (!user || !session?.access_token) {
694701
return redirect("/sign-in");
695702
}
696703

@@ -700,25 +707,24 @@ export const deleteAccountAction = async () => {
700707
{
701708
method: "POST",
702709
headers: {
703-
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
710+
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
711+
Authorization: `Bearer ${session.access_token}`,
704712
"Content-Type": "application/json",
705713
},
706-
body: JSON.stringify({ user_id: user.id }),
707714
}
708715
);
709716

710717
console.log("Response status:", response.status);
711718
console.log("Response ok:", response.ok);
712719

713-
// const data = await response.json();
714-
// console.log("Response data:", data);
715-
716-
redirectPath = `/sign-in?success=${encodeURIComponent(t("accountDeleted"))}`;
720+
const data = await response.json();
717721

718-
// if (!response.ok) {
719-
// console.error("Delete account failed:", data);
720-
// redirectPath = `/profile?error=Failed to delete account`
721-
// }
722+
if (!response.ok) {
723+
console.error("Delete account failed:", data);
724+
redirectPath = `/profile?error=${encodeURIComponent(t("deleteAccountFailed"))}`;
725+
} else {
726+
redirectPath = `/sign-in?success=${encodeURIComponent(t("accountDeleted"))}`;
727+
}
722728
} catch (error) {
723729
console.error("Delete account error:", error);
724730
redirectPath = `/profile?error=${encodeURIComponent(t("deleteAccountFailed"))}`;

supabase/functions/_shared/storage-utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ export async function deleteListingMedia(
2121
supabase: SupabaseClient,
2222
slug: string
2323
) {
24-
// Get listing media info
24+
// Service-role callers use this after authorising the requested listing operation.
2525
const { data: listing, error: fetchError } = await supabase
26-
// We can access the "listings" table here directly as we have a policy set allowing owners access to their full listings
2726
.from("listings")
2827
.select("avatar, photos")
2928
.eq("slug", slug)
30-
.single();
29+
.maybeSingle();
3130

3231
if (fetchError) throw fetchError;
32+
if (!listing) return;
3333

3434
const deletePromises = [];
3535

supabase/functions/delete-account/index.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
22
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
33
import { deleteListingMedia } from "../_shared/storage-utils.ts";
44

5+
function jsonResponse(body: Record<string, unknown>, status: number) {
6+
return new Response(JSON.stringify(body), {
7+
headers: {
8+
"Content-Type": "application/json",
9+
},
10+
status,
11+
});
12+
}
13+
14+
function getErrorMessage(error: unknown) {
15+
return error instanceof Error ? error.message : "Unexpected error";
16+
}
17+
518
serve(async (req) => {
619
const supabaseAdmin = createClient(
720
Deno.env.get("SUPABASE_URL") ?? "",
@@ -13,14 +26,34 @@ serve(async (req) => {
1326
},
1427
}
1528
);
16-
const { user_id } = await req.json();
29+
1730
try {
31+
if (req.method !== "POST") {
32+
return jsonResponse({ error: "Method not allowed" }, 405);
33+
}
34+
35+
const authHeader = req.headers.get("Authorization");
36+
const accessToken = authHeader?.replace("Bearer ", "");
37+
38+
if (!accessToken) {
39+
return jsonResponse({ error: "Missing access token" }, 401);
40+
}
41+
42+
const {
43+
data: { user },
44+
error: userError,
45+
} = await supabaseAdmin.auth.getUser(accessToken);
46+
47+
if (userError || !user) {
48+
return jsonResponse({ error: "Invalid access token" }, 401);
49+
}
50+
1851
// Get the user's profile to find avatar
1952
const { data: profile, error: profileError } = await supabaseAdmin
2053
.from("profiles")
2154
.select("avatar")
22-
.eq("id", user_id)
23-
.single();
55+
.eq("id", user.id)
56+
.maybeSingle();
2457
if (profileError) {
2558
console.error("Profile fetch error:", profileError);
2659
throw profileError;
@@ -40,7 +73,7 @@ serve(async (req) => {
4073
const { data: listings, error: listingsError } = await supabaseAdmin
4174
.from("listings")
4275
.select("slug")
43-
.eq("owner_id", user_id);
76+
.eq("owner_id", user.id);
4477
if (listingsError) {
4578
console.error("Listings fetch error:", listingsError);
4679
throw listingsError;
@@ -59,35 +92,15 @@ serve(async (req) => {
5992
}
6093
// Delete auth user (cascade will handle the rest)
6194
const { error: deleteUserError } =
62-
await supabaseAdmin.auth.admin.deleteUser(user_id);
95+
await supabaseAdmin.auth.admin.deleteUser(user.id);
6396
if (deleteUserError) {
6497
console.error("Auth user deletion error:", deleteUserError);
6598
throw deleteUserError;
6699
}
67100
console.log("Deleted auth user");
68-
return new Response(
69-
JSON.stringify({
70-
success: true,
71-
}),
72-
{
73-
headers: {
74-
"Content-Type": "application/json",
75-
},
76-
status: 200,
77-
}
78-
);
101+
return jsonResponse({ success: true }, 200);
79102
} catch (error) {
80103
console.error("Final error:", error);
81-
return new Response(
82-
JSON.stringify({
83-
error: error.message,
84-
}),
85-
{
86-
headers: {
87-
"Content-Type": "application/json",
88-
},
89-
status: 400,
90-
}
91-
);
104+
return jsonResponse({ error: getErrorMessage(error) }, 400);
92105
}
93106
});

supabase/functions/delete-listing/index.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
22
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
33
import { deleteListingMedia } from "../_shared/storage-utils.ts";
44

5+
function jsonResponse(body: Record<string, unknown>, status: number) {
6+
return new Response(JSON.stringify(body), {
7+
headers: { "Content-Type": "application/json" },
8+
status,
9+
});
10+
}
11+
12+
function getErrorMessage(error: unknown) {
13+
return error instanceof Error ? error.message : "Unexpected error";
14+
}
15+
516
serve(async (req) => {
617
const supabaseAdmin = createClient(
718
Deno.env.get("SUPABASE_URL") ?? "",
@@ -13,30 +24,60 @@ serve(async (req) => {
1324
},
1425
}
1526
);
16-
const { slug } = await req.json();
1727

1828
try {
29+
if (req.method !== "POST") {
30+
return jsonResponse({ error: "Method not allowed" }, 405);
31+
}
32+
33+
const { slug } = await req.json();
34+
35+
if (!slug || typeof slug !== "string") {
36+
return jsonResponse({ error: "Missing listing slug" }, 400);
37+
}
38+
39+
const authHeader = req.headers.get("Authorization");
40+
const accessToken = authHeader?.replace("Bearer ", "");
41+
42+
if (!accessToken) {
43+
return jsonResponse({ error: "Missing access token" }, 401);
44+
}
45+
46+
const {
47+
data: { user },
48+
error: userError,
49+
} = await supabaseAdmin.auth.getUser(accessToken);
50+
51+
if (userError || !user) {
52+
return jsonResponse({ error: "Invalid access token" }, 401);
53+
}
54+
55+
const { data: listing, error: listingError } = await supabaseAdmin
56+
.from("listings")
57+
.select("owner_id")
58+
.eq("slug", slug)
59+
.maybeSingle();
60+
61+
if (listingError) throw listingError;
62+
63+
if (!listing || listing.owner_id !== user.id) {
64+
return jsonResponse({ error: "Listing not found" }, 404);
65+
}
66+
1967
// Delete all media first
2068
await deleteListingMedia(supabaseAdmin, slug);
2169

2270
// Delete the listing
2371
const { error: deleteError } = await supabaseAdmin
24-
// We can access the "listings" table directly here as we're doing an operation through supabaseAdmin
2572
.from("listings")
2673
.delete()
2774
.eq("slug", slug);
2875

2976
if (deleteError) throw deleteError;
3077

31-
return new Response(JSON.stringify({ success: true }), {
32-
headers: { "Content-Type": "application/json" },
33-
status: 200,
34-
});
78+
return jsonResponse({ success: true }, 200);
3579
} catch (error) {
3680
console.error("Final error:", error);
37-
return new Response(JSON.stringify({ error: error.message }), {
38-
headers: { "Content-Type": "application/json" },
39-
status: 400,
40-
});
81+
return jsonResponse({ error: getErrorMessage(error) }, 400);
4182
}
4283
});

0 commit comments

Comments
 (0)