added stripe credentials to backend

This commit is contained in:
Jun-te Kim 2026-01-18 15:13:45 +00:00
parent 6eb1aefdb4
commit eb7ac127e1
4 changed files with 113 additions and 42 deletions

View file

@ -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)
);
}

View file

@ -0,0 +1,23 @@
export default function StripeRefreshPage() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md text-center space-y-4">
<h1 className="text-xl font-semibold">
Stripe connection incomplete
</h1>
<p className="text-gray-600">
Something interrupted the Stripe onboarding.
Please try again.
</p>
<a
href="/connect/stripe"
className="inline-block rounded-md bg-black px-4 py-2 text-white"
>
Retry Stripe setup
</a>
</div>
</div>
);
}

View file

@ -1,23 +1,51 @@
import Link from "next/link";
export default function StripeSuccessPage() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md text-center space-y-4">
<h1 className="text-2xl font-semibold">
Stripe Connected 🎉
</h1>
<main className="max-w-2xl mx-auto p-8 space-y-10">
<h1 className="text-2xl font-semibold">
Stripe connected 🎉
</h1>
<p className="text-gray-600">
Your Stripe account has been successfully connected.
You can now receive payments.
</p>
<p className="text-gray-600">
Your Stripe account is now linked. We can now automate payments and
reconciliation for you.
</p>
<a
href="/dashboard"
className="inline-block rounded-md bg-black px-4 py-2 text-white"
{/* Progress */}
<ol className="space-y-4">
<li className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>Logged in</span>
</li>
<li className="flex items-center gap-3">
<span className="text-green-600"></span>
<span>Stripe connected</span>
</li>
<li className="flex items-center gap-3 text-blue-600">
<span></span>
<span className="font-medium">Connect Xero</span>
</li>
</ol>
{/* Primary CTA */}
<div className="pt-6 flex gap-4">
<Link
href="/app"
className="inline-block rounded bg-black text-white px-5 py-3"
>
Continue setup
</Link>
<Link
href="/app"
className="inline-block rounded border px-5 py-3"
>
Go to dashboard
</a>
</Link>
</div>
</div>
</main>
);
}

View file

@ -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(),
});