added stripe credentials to backend
This commit is contained in:
parent
6eb1aefdb4
commit
eb7ac127e1
4 changed files with 113 additions and 42 deletions
|
|
@ -1,24 +1,26 @@
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { stripeAccounts } from "@/lib/schema/stripeAccounts";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
type StripeOAuthResponse = {
|
type StripeOAuthResponse = {
|
||||||
access_token: string;
|
stripe_user_id: string; // acct_...
|
||||||
refresh_token: string;
|
|
||||||
stripe_user_id: string;
|
|
||||||
scope: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const session = cookieStore.get("session");
|
const session = cookieStore.get("session");
|
||||||
|
|
||||||
// Safety: user must still be logged in
|
// 🔒 Must be logged in
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL("/login", process.env.NEXT_PUBLIC_BASE_URL)
|
new URL("/login", process.env.NEXT_PUBLIC_BASE_URL)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = session.value;
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const code = searchParams.get("code");
|
const code = searchParams.get("code");
|
||||||
const error = searchParams.get("error");
|
const error = searchParams.get("error");
|
||||||
|
|
@ -26,7 +28,10 @@ export async function GET(req: NextRequest) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Stripe OAuth error:", error);
|
console.error("Stripe OAuth error:", error);
|
||||||
return NextResponse.redirect(
|
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", {
|
const tokenRes = await fetch("https://connect.stripe.com/oauth/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -55,34 +60,36 @@ export async function GET(req: NextRequest) {
|
||||||
console.error("Stripe token exchange failed:", text);
|
console.error("Stripe token exchange failed:", text);
|
||||||
|
|
||||||
return NextResponse.redirect(
|
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;
|
const data = (await tokenRes.json()) as StripeOAuthResponse;
|
||||||
|
|
||||||
/**
|
// ✅ Persist Stripe account → user (UPSERT)
|
||||||
* TODO (NEXT STEP):
|
await db
|
||||||
* - Encrypt tokens
|
.insert(stripeAccounts)
|
||||||
* - Persist to DB against the current user
|
.values({
|
||||||
*
|
userId,
|
||||||
* Required fields:
|
stripeAccountId: data.stripe_user_id,
|
||||||
* - data.stripe_user_id (acct_...)
|
})
|
||||||
* - data.access_token
|
.onConflictDoUpdate({
|
||||||
* - data.refresh_token
|
target: stripeAccounts.userId,
|
||||||
* - mode: "test"
|
set: {
|
||||||
*/
|
stripeAccountId: data.stripe_user_id,
|
||||||
|
},
|
||||||
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) + "...",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// MVP success redirect
|
console.log("Stripe connected", {
|
||||||
|
userId,
|
||||||
|
stripeAccountId: data.stripe_user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Success redirect
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL("/connect/stripe/success", process.env.APP_URL)
|
new URL("/connect/stripe/success", process.env.NEXT_PUBLIC_BASE_URL)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
stripe_to_invoice/app/connect/stripe/refresh/page.tsx
Normal file
23
stripe_to_invoice/app/connect/stripe/refresh/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,51 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function StripeSuccessPage() {
|
export default function StripeSuccessPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<main className="max-w-2xl mx-auto p-8 space-y-10">
|
||||||
<div className="max-w-md text-center space-y-4">
|
<h1 className="text-2xl font-semibold">
|
||||||
<h1 className="text-2xl font-semibold">
|
Stripe connected 🎉
|
||||||
Stripe Connected 🎉
|
</h1>
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Your Stripe account has been successfully connected.
|
Your Stripe account is now linked. We can now automate payments and
|
||||||
You can now receive payments.
|
reconciliation for you.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a
|
{/* Progress */}
|
||||||
href="/dashboard"
|
<ol className="space-y-4">
|
||||||
className="inline-block rounded-md bg-black px-4 py-2 text-white"
|
<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
|
Go to dashboard
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
stripe_to_invoice/lib/schema/stripeAccounts.ts
Normal file
13
stripe_to_invoice/lib/schema/stripeAccounts.ts
Normal 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(),
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue