feat: premium workspace usernames
This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
import type { EmailOtpType } from "@supabase/supabase-js";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Stripe } from "stripe";
|
||||
|
||||
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||
import * as userRepo from "@kan/db/repository/user.repo";
|
||||
import { createNextClient } from "@kan/supabase/clients";
|
||||
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
if (!stripeSecretKey) {
|
||||
throw new Error("STRIPE_SECRET_KEY is not defined");
|
||||
}
|
||||
|
||||
const stripe = new Stripe(stripeSecretKey, {
|
||||
apiVersion: "2024-12-18.acacia",
|
||||
});
|
||||
|
||||
export default async function handler(req: NextRequest) {
|
||||
if (req.method !== "GET") {
|
||||
return new NextResponse(null, {
|
||||
@@ -54,9 +65,17 @@ export default async function handler(req: NextRequest) {
|
||||
const existingUser = await userRepo.getById(db, user.id);
|
||||
|
||||
if (!existingUser) {
|
||||
const stripeCustomer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
await userRepo.create(db, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
stripeCustomerId: stripeCustomer.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
135
apps/web/src/pages/api/stripe/create_checkout_session.ts
Normal file
135
apps/web/src/pages/api/stripe/create_checkout_session.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Stripe } from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
import * as userRepo from "@kan/db/repository/user.repo";
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { createNextClient } from "@kan/supabase/clients";
|
||||
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
if (!stripeSecretKey) {
|
||||
throw new Error("STRIPE_SECRET_KEY is not defined");
|
||||
}
|
||||
|
||||
const stripe = new Stripe(stripeSecretKey, {
|
||||
apiVersion: "2024-12-18.acacia",
|
||||
});
|
||||
|
||||
const usernameSchema = z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(24)
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/);
|
||||
|
||||
interface CheckoutSessionRequest {
|
||||
successUrl: string;
|
||||
cancelUrl: string;
|
||||
username: string;
|
||||
workspacePublicId: string;
|
||||
stripeCustomerId: string;
|
||||
}
|
||||
|
||||
export default async function handler(req: NextRequest) {
|
||||
if (req.method !== "POST") {
|
||||
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
||||
status: 405,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = NextResponse.next();
|
||||
|
||||
const db = createNextClient(req, response);
|
||||
|
||||
const { data } = await db.auth.getUser();
|
||||
|
||||
if (!data.user) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userRepo.getById(db, data.user.id);
|
||||
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: "User not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const body = (await req.json()) as CheckoutSessionRequest;
|
||||
const { successUrl, cancelUrl, username, workspacePublicId } = body;
|
||||
|
||||
if (!successUrl || !cancelUrl || !username || !workspacePublicId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Missing required fields" }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const usernameResult = usernameSchema.safeParse(username);
|
||||
|
||||
if (!usernameResult.success) {
|
||||
return new Response(JSON.stringify({ error: "Invalid username" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await workspaceRepo.getAllByUserId(db, user.id);
|
||||
|
||||
const isMemberOfWorkspace = workspace.some(
|
||||
({ workspace }) => workspace?.publicId === body.workspacePublicId,
|
||||
);
|
||||
|
||||
if (!isMemberOfWorkspace) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
line_items: [
|
||||
{
|
||||
price: "price_1QcpmyDlDJBL8JHbeqhe1Ruq",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.WEBSITE_URL}${successUrl}`,
|
||||
cancel_url: `${process.env.WEBSITE_URL}${cancelUrl}`,
|
||||
customer: user.stripeCustomerId ?? undefined,
|
||||
metadata: {
|
||||
username,
|
||||
workspacePublicId,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ url: session.url }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Error creating checkout session" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = "lhr1";
|
||||
export const dynamic = "force-dynamic";
|
||||
82
apps/web/src/pages/api/stripe/webhook.ts
Normal file
82
apps/web/src/pages/api/stripe/webhook.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { Stripe } from "stripe";
|
||||
|
||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||
import { createNextClient } from "@kan/supabase/clients";
|
||||
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
if (!stripeSecretKey) {
|
||||
throw new Error("STRIPE_SECRET_KEY is not defined");
|
||||
}
|
||||
|
||||
export const webCrypto = Stripe.createSubtleCryptoProvider();
|
||||
|
||||
const stripe: Stripe = new Stripe(stripeSecretKey, {
|
||||
apiVersion: "2024-12-18.acacia",
|
||||
httpClient: Stripe.createFetchHttpClient(),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextRequest) {
|
||||
if (req.method !== "POST") {
|
||||
return new Response(JSON.stringify({ message: "Method not allowed" }), {
|
||||
status: 405,
|
||||
});
|
||||
}
|
||||
|
||||
const sig = req.headers.get("stripe-signature");
|
||||
|
||||
if (!sig) {
|
||||
return new Response(JSON.stringify({ message: "No signature found" }), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.text();
|
||||
|
||||
const event = await stripe.webhooks.constructEventAsync(
|
||||
body,
|
||||
sig,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
undefined,
|
||||
webCrypto,
|
||||
);
|
||||
|
||||
const response = NextResponse.next();
|
||||
|
||||
const db = createNextClient(req, response);
|
||||
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
const checkoutSession = event.data.object;
|
||||
|
||||
const metaData = checkoutSession.metadata;
|
||||
|
||||
if (metaData?.workspacePublicId && metaData.username) {
|
||||
await workspaceRepo.update(
|
||||
db,
|
||||
metaData.workspacePublicId,
|
||||
undefined,
|
||||
metaData.username,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ received: true }), { status: 200 });
|
||||
} catch (err) {
|
||||
console.error("Webhook error:", err);
|
||||
return new Response(JSON.stringify({ message: "Webhook handler failed" }), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = "lhr1";
|
||||
export const dynamic = "force-dynamic";
|
||||
Reference in New Issue
Block a user