From eb7ac127e1442c41accae15ec4db6a987da4cea1 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sun, 18 Jan 2026 15:13:45 +0000 Subject: [PATCH] added stripe credentials to backend --- .../app/api/stripe/callback/route.ts | 63 ++++++++++--------- .../app/connect/stripe/refresh/page.tsx | 23 +++++++ .../app/connect/stripe/success/page.tsx | 56 ++++++++++++----- .../lib/schema/stripeAccounts.ts | 13 ++++ 4 files changed, 113 insertions(+), 42 deletions(-) create mode 100644 stripe_to_invoice/app/connect/stripe/refresh/page.tsx create mode 100644 stripe_to_invoice/lib/schema/stripeAccounts.ts diff --git a/stripe_to_invoice/app/api/stripe/callback/route.ts b/stripe_to_invoice/app/api/stripe/callback/route.ts index d4f6d79..e73565f 100644 --- a/stripe_to_invoice/app/api/stripe/callback/route.ts +++ b/stripe_to_invoice/app/api/stripe/callback/route.ts @@ -1,24 +1,26 @@ import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { db } from "@/lib/db"; +import { stripeAccounts } from "@/lib/schema/stripeAccounts"; +import { eq } from "drizzle-orm"; type StripeOAuthResponse = { - access_token: string; - refresh_token: string; - stripe_user_id: string; - scope: string; + stripe_user_id: string; // acct_... }; export async function GET(req: NextRequest) { const cookieStore = await cookies(); const session = cookieStore.get("session"); - // Safety: user must still be logged in + // 🔒 Must be logged in if (!session) { return NextResponse.redirect( new URL("/login", process.env.NEXT_PUBLIC_BASE_URL) ); } + const userId = session.value; + const { searchParams } = new URL(req.url); const code = searchParams.get("code"); const error = searchParams.get("error"); @@ -26,7 +28,10 @@ export async function GET(req: NextRequest) { if (error) { console.error("Stripe OAuth error:", error); return NextResponse.redirect( - new URL("/connect/stripe?error=oauth_failed", process.env.NEXT_PUBLIC_BASE_URL) + new URL( + "/connect/stripe?error=oauth_failed", + process.env.NEXT_PUBLIC_BASE_URL + ) ); } @@ -37,7 +42,7 @@ export async function GET(req: NextRequest) { ); } - // Exchange code for access token + // 🔁 Exchange OAuth code const tokenRes = await fetch("https://connect.stripe.com/oauth/token", { method: "POST", headers: { @@ -55,34 +60,36 @@ export async function GET(req: NextRequest) { console.error("Stripe token exchange failed:", text); return NextResponse.redirect( - new URL("/connect/stripe?error=token_exchange_failed", process.env.NEXT_PUBLIC_BASE_URL) + new URL( + "/connect/stripe?error=token_exchange_failed", + process.env.NEXT_PUBLIC_BASE_URL + ) ); } const data = (await tokenRes.json()) as StripeOAuthResponse; - /** - * TODO (NEXT STEP): - * - Encrypt tokens - * - Persist to DB against the current user - * - * Required fields: - * - data.stripe_user_id (acct_...) - * - data.access_token - * - data.refresh_token - * - mode: "test" - */ - - console.log("Stripe OAuth success", { - stripe_account_id: data.stripe_user_id, - scope: data.scope, - has_access_token: Boolean(data.access_token), - has_refresh_token: Boolean(data.refresh_token), - access_token_preview: data.access_token?.slice(0, 8) + "...", + // ✅ Persist Stripe account → user (UPSERT) + await db + .insert(stripeAccounts) + .values({ + userId, + stripeAccountId: data.stripe_user_id, + }) + .onConflictDoUpdate({ + target: stripeAccounts.userId, + set: { + stripeAccountId: data.stripe_user_id, + }, }); - // MVP success redirect + console.log("Stripe connected", { + userId, + stripeAccountId: data.stripe_user_id, + }); + + // ✅ Success redirect return NextResponse.redirect( - new URL("/connect/stripe/success", process.env.APP_URL) + new URL("/connect/stripe/success", process.env.NEXT_PUBLIC_BASE_URL) ); } diff --git a/stripe_to_invoice/app/connect/stripe/refresh/page.tsx b/stripe_to_invoice/app/connect/stripe/refresh/page.tsx new file mode 100644 index 0000000..92af400 --- /dev/null +++ b/stripe_to_invoice/app/connect/stripe/refresh/page.tsx @@ -0,0 +1,23 @@ +export default function StripeRefreshPage() { + return ( +
+
+

+ Stripe connection incomplete +

+ +

+ Something interrupted the Stripe onboarding. + Please try again. +

+ + + Retry Stripe setup + +
+
+ ); +} diff --git a/stripe_to_invoice/app/connect/stripe/success/page.tsx b/stripe_to_invoice/app/connect/stripe/success/page.tsx index 65a5a39..c46b272 100644 --- a/stripe_to_invoice/app/connect/stripe/success/page.tsx +++ b/stripe_to_invoice/app/connect/stripe/success/page.tsx @@ -1,23 +1,51 @@ +import Link from "next/link"; + export default function StripeSuccessPage() { return ( -
-
-

- Stripe Connected 🎉 -

+
+

+ Stripe connected 🎉 +

-

- Your Stripe account has been successfully connected. - You can now receive payments. -

+

+ Your Stripe account is now linked. We can now automate payments and + reconciliation for you. +

- +
  • + + Logged in +
  • + +
  • + + Stripe connected +
  • + +
  • + + Connect Xero +
  • + + + {/* Primary CTA */} +
    + + Continue setup + + + Go to dashboard - +
    -
    + ); } diff --git a/stripe_to_invoice/lib/schema/stripeAccounts.ts b/stripe_to_invoice/lib/schema/stripeAccounts.ts new file mode 100644 index 0000000..4385192 --- /dev/null +++ b/stripe_to_invoice/lib/schema/stripeAccounts.ts @@ -0,0 +1,13 @@ +import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"; +import { users } from "./users"; + +export const stripeAccounts = pgTable("stripe_accounts", { + id: uuid("id").defaultRandom().primaryKey(), + userId: uuid("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + stripeAccountId: text("stripe_account_id").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); \ No newline at end of file