Skip to content

Commit 7175512

Browse files
tomusdrwclaude
andcommitted
refactor: extract shared auth helper, fix error handling in Edge Functions
- Extract duplicated auth code from create-checkout and create-portal into _shared/auth.ts - Fix catch blocks to handle non-Error thrown values (use instanceof check instead of accessing .message directly) - Add demo/README.md with local development instructions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f60cc6a commit 7175512

5 files changed

Lines changed: 81 additions & 48 deletions

File tree

demo/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Shared UI Demo
2+
3+
A minimal app that tests the Supabase integration end-to-end: authentication, subscription status, quota-gated features, and Stripe checkout.
4+
5+
Deployed at: https://fluffylabs.dev/shared-ui/demo/
6+
7+
## Local development
8+
9+
```bash
10+
# From the repo root
11+
cp .env.stripe.example .env.stripe # or create with your values
12+
13+
# Set Supabase env vars
14+
export VITE_SUPABASE_URL=https://your-project.supabase.co
15+
export VITE_SUPABASE_ANON_KEY=your-anon-key
16+
17+
# Run the demo
18+
npx vite --config demo/vite.config.ts
19+
```
20+
21+
## What it tests
22+
23+
- **Login/Register** — AuthFlow component with email+password
24+
- **User Menu** — compact UserMenu in the header with settings/sign-out
25+
- **Settings** — theme selector + SubscriptionStatus with upgrade/manage buttons
26+
- **Pricing** — PricingCard component with checkout flow
27+
- **Quota-Gated Feature** — QuotaGate with 5 free uses/month, progress bar, and upgrade prompt

supabase/functions/_shared/auth.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
2+
import type { User } from "https://esm.sh/@supabase/supabase-js@2";
3+
import { corsHeaders } from "./cors.ts";
4+
5+
/**
6+
* Authenticate the user from the request's Authorization header.
7+
* Returns the user on success, or a 401 Response on failure.
8+
*/
9+
export async function authenticateUser(req: Request): Promise<{ user: User } | { response: Response }> {
10+
const authHeader = req.headers.get("Authorization");
11+
if (!authHeader) {
12+
return {
13+
response: new Response(JSON.stringify({ error: "Missing Authorization header" }), {
14+
status: 401,
15+
headers: { ...corsHeaders, "Content-Type": "application/json" },
16+
}),
17+
};
18+
}
19+
20+
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
21+
global: { headers: { Authorization: authHeader } },
22+
});
23+
24+
const {
25+
data: { user },
26+
error,
27+
} = await supabase.auth.getUser();
28+
29+
if (error || !user) {
30+
return {
31+
response: new Response(JSON.stringify({ error: "Unauthorized" }), {
32+
status: 401,
33+
headers: { ...corsHeaders, "Content-Type": "application/json" },
34+
}),
35+
};
36+
}
37+
38+
return { user };
39+
}

supabase/functions/create-checkout/index.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,20 @@
1010
// SUPABASE_ANON_KEY — (auto-set by Supabase)
1111
// SUPABASE_SERVICE_ROLE_KEY — (auto-set by Supabase)
1212

13-
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
1413
import { corsHeaders } from "../_shared/cors.ts";
1514
import { stripe } from "../_shared/stripe.ts";
1615
import { supabaseAdmin } from "../_shared/supabase-admin.ts";
16+
import { authenticateUser } from "../_shared/auth.ts";
1717

1818
Deno.serve(async (req) => {
19-
// Handle CORS preflight
2019
if (req.method === "OPTIONS") {
2120
return new Response("ok", { headers: corsHeaders });
2221
}
2322

2423
try {
25-
// Authenticate the user from the Authorization header
26-
const authHeader = req.headers.get("Authorization")!;
27-
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
28-
global: { headers: { Authorization: authHeader } },
29-
});
30-
31-
const {
32-
data: { user },
33-
error: authError,
34-
} = await supabase.auth.getUser();
35-
if (authError || !user) {
36-
return new Response(JSON.stringify({ error: "Unauthorized" }), {
37-
status: 401,
38-
headers: { ...corsHeaders, "Content-Type": "application/json" },
39-
});
40-
}
24+
const auth = await authenticateUser(req);
25+
if ("response" in auth) return auth.response;
26+
const { user } = auth;
4127

4228
// Check if user already has a Stripe customer ID
4329
const { data: subscription } = await supabaseAdmin
@@ -56,7 +42,6 @@ Deno.serve(async (req) => {
5642
});
5743
customerId = customer.id;
5844

59-
// Store the customer ID
6045
await supabaseAdmin.from("subscriptions").upsert(
6146
{
6247
user_id: user.id,
@@ -67,20 +52,13 @@ Deno.serve(async (req) => {
6752
);
6853
}
6954

70-
// Parse optional return URL from request body
7155
const body = await req.json().catch(() => ({}));
7256
const returnUrl = body.returnUrl || Deno.env.get("SITE_URL") || "http://localhost:5173";
7357

74-
// Create Checkout Session
7558
const session = await stripe.checkout.sessions.create({
7659
customer: customerId,
7760
mode: "subscription",
78-
line_items: [
79-
{
80-
price: Deno.env.get("STRIPE_PRICE_ID")!,
81-
quantity: 1,
82-
},
83-
],
61+
line_items: [{ price: Deno.env.get("STRIPE_PRICE_ID")!, quantity: 1 }],
8462
success_url: `${returnUrl}?checkout=success`,
8563
cancel_url: `${returnUrl}?checkout=cancelled`,
8664
});
@@ -89,8 +67,9 @@ Deno.serve(async (req) => {
8967
headers: { ...corsHeaders, "Content-Type": "application/json" },
9068
});
9169
} catch (err) {
70+
const message = err instanceof Error ? err.message : "Internal server error";
9271
console.error("create-checkout error:", err);
93-
return new Response(JSON.stringify({ error: err.message }), {
72+
return new Response(JSON.stringify({ error: message }), {
9473
status: 500,
9574
headers: { ...corsHeaders, "Content-Type": "application/json" },
9675
});

supabase/functions/create-portal/index.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,20 @@
99
// SUPABASE_ANON_KEY — (auto-set by Supabase)
1010
// SUPABASE_SERVICE_ROLE_KEY — (auto-set by Supabase)
1111

12-
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
1312
import { corsHeaders } from "../_shared/cors.ts";
1413
import { stripe } from "../_shared/stripe.ts";
1514
import { supabaseAdmin } from "../_shared/supabase-admin.ts";
15+
import { authenticateUser } from "../_shared/auth.ts";
1616

1717
Deno.serve(async (req) => {
1818
if (req.method === "OPTIONS") {
1919
return new Response("ok", { headers: corsHeaders });
2020
}
2121

2222
try {
23-
// Authenticate the user
24-
const authHeader = req.headers.get("Authorization")!;
25-
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!, {
26-
global: { headers: { Authorization: authHeader } },
27-
});
28-
29-
const {
30-
data: { user },
31-
error: authError,
32-
} = await supabase.auth.getUser();
33-
if (authError || !user) {
34-
return new Response(JSON.stringify({ error: "Unauthorized" }), {
35-
status: 401,
36-
headers: { ...corsHeaders, "Content-Type": "application/json" },
37-
});
38-
}
23+
const auth = await authenticateUser(req);
24+
if ("response" in auth) return auth.response;
25+
const { user } = auth;
3926

4027
// Get the user's Stripe customer ID
4128
const { data: subscription } = await supabaseAdmin
@@ -54,7 +41,6 @@ Deno.serve(async (req) => {
5441
const body = await req.json().catch(() => ({}));
5542
const returnUrl = body.returnUrl || Deno.env.get("SITE_URL") || "http://localhost:5173";
5643

57-
// Create a portal session
5844
const portalSession = await stripe.billingPortal.sessions.create({
5945
customer: subscription.stripe_customer_id,
6046
return_url: returnUrl,
@@ -64,8 +50,9 @@ Deno.serve(async (req) => {
6450
headers: { ...corsHeaders, "Content-Type": "application/json" },
6551
});
6652
} catch (err) {
53+
const message = err instanceof Error ? err.message : "Internal server error";
6754
console.error("create-portal error:", err);
68-
return new Response(JSON.stringify({ error: err.message }), {
55+
return new Response(JSON.stringify({ error: message }), {
6956
status: 500,
7057
headers: { ...corsHeaders, "Content-Type": "application/json" },
7158
});

supabase/functions/stripe-webhook/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ Deno.serve(async (req) => {
9393
headers: { "Content-Type": "application/json" },
9494
});
9595
} catch (err) {
96+
const message = err instanceof Error ? err.message : "Webhook processing failed";
9697
console.error("stripe-webhook error:", err);
97-
return new Response(JSON.stringify({ error: err.message }), {
98+
return new Response(JSON.stringify({ error: message }), {
9899
status: 400,
99100
headers: { "Content-Type": "application/json" },
100101
});

0 commit comments

Comments
 (0)