From 3511fabc519cdd86ea9dbae14ea5e593f1b2cd69 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Sun, 18 Jan 2026 14:27:25 +0000 Subject: [PATCH] added stripe callback --- .github/workflows/stripe-to-invoice.yml | 5 +- .../app/api/stripe/callback/route.ts | 88 +++++++++++++++++++ .../app/api/stripe/connect/route.ts | 26 ++++++ stripe_to_invoice/app/login/page.tsx | 2 +- stripe_to_invoice/deployment/deployment.yaml | 6 ++ stripe_to_invoice/deployment/secrets/.env | 2 + 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 stripe_to_invoice/app/api/stripe/callback/route.ts create mode 100644 stripe_to_invoice/app/api/stripe/connect/route.ts diff --git a/.github/workflows/stripe-to-invoice.yml b/.github/workflows/stripe-to-invoice.yml index e66bee2..0381277 100644 --- a/.github/workflows/stripe-to-invoice.yml +++ b/.github/workflows/stripe-to-invoice.yml @@ -128,6 +128,7 @@ jobs: AWS_ACCESS_KEY_ID=$PROD_AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=$PROD_AWS_SECRET_ACCESS_KEY SES_FROM_EMAIL=$PROD_SES_FROM_EMAIL + STRIPE_REDIRECT_URI=$PROD_SES_STRIPE_REDIRECT_URI else STRIPE_SECRET_KEY="$DEV_STRIPE_SECRET_KEY" STRIPE_CLIENT_ID="$DEV_STRIPE_CLIENT_ID" @@ -136,6 +137,7 @@ jobs: AWS_ACCESS_KEY_ID=$DEV_AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=$DEV_AWS_SECRET_ACCESS_KEY SES_FROM_EMAIL=$DEV_SES_FROM_EMAIL + STRIPE_REDIRECT_URI=$DEV_SES_STRIPE_REDIRECT_URI fi : "${STRIPE_SECRET_KEY:?missing STRIPE_SECRET_KEY}" : "${STRIPE_CLIENT_ID:?missing STRIPE_CLIENT_ID}" @@ -145,8 +147,9 @@ jobs: : "${AWS_ACCESS_KEY_ID:?missing AWS_ACCESS_KEY_ID}" : "${AWS_SECRET_ACCESS_KEY:?missing AWS_SECRET_ACCESS_KEY}" : "${SES_FROM_EMAIL:?missing SES_FROM_EMAIL}" + : "${STRIPE_REDIRECT_URI:?missing STRIPE_REDIRECT_URI}" - export STRIPE_SECRET_KEY STRIPE_CLIENT_ID NAMESPACE APP_URL AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY SES_FROM_EMAIL + export STRIPE_SECRET_KEY STRIPE_CLIENT_ID NAMESPACE APP_URL AWS_REGION AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY SES_FROM_EMAIL STRIPE_REDIRECT_URI envsubst < stripe_to_invoice/deployment/secrets/stripe-secrets.yaml \ | kubectl apply -f - diff --git a/stripe_to_invoice/app/api/stripe/callback/route.ts b/stripe_to_invoice/app/api/stripe/callback/route.ts new file mode 100644 index 0000000..443eaac --- /dev/null +++ b/stripe_to_invoice/app/api/stripe/callback/route.ts @@ -0,0 +1,88 @@ +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +type StripeOAuthResponse = { + access_token: string; + refresh_token: string; + stripe_user_id: string; + scope: string; +}; + +export async function GET(req: NextRequest) { + const cookieStore = await cookies(); + const session = cookieStore.get("session"); + + // Safety: user must still be logged in + if (!session) { + return NextResponse.redirect( + new URL("/login", process.env.NEXT_PUBLIC_BASE_URL) + ); + } + + const { searchParams } = new URL(req.url); + const code = searchParams.get("code"); + const error = searchParams.get("error"); + + if (error) { + console.error("Stripe OAuth error:", error); + return NextResponse.redirect( + new URL("/connect/stripe?error=oauth_failed", process.env.NEXT_PUBLIC_BASE_URL) + ); + } + + if (!code) { + return NextResponse.json( + { error: "Missing OAuth code" }, + { status: 400 } + ); + } + + // Exchange code for access token + const tokenRes = await fetch("https://connect.stripe.com/oauth/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + client_secret: process.env.STRIPE_SECRET_KEY!, + }), + }); + + if (!tokenRes.ok) { + const text = await tokenRes.text(); + console.error("Stripe token exchange failed:", text); + + return NextResponse.redirect( + 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) + "...", + }); + + // MVP success redirect + return NextResponse.redirect( + new URL("/connect/stripe/success", process.env.NEXT_PUBLIC_BASE_URL) + ); +} diff --git a/stripe_to_invoice/app/api/stripe/connect/route.ts b/stripe_to_invoice/app/api/stripe/connect/route.ts new file mode 100644 index 0000000..c065a95 --- /dev/null +++ b/stripe_to_invoice/app/api/stripe/connect/route.ts @@ -0,0 +1,26 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET() { + const cookieStore = await cookies(); + const session = cookieStore.get("session"); + + // Safety: must be logged in + if (!session) { + return NextResponse.redirect( + new URL("/login", process.env.NEXT_PUBLIC_BASE_URL) + ); + } + + const params = new URLSearchParams({ + response_type: "code", + client_id: process.env.STRIPE_CLIENT_ID!, + scope: "read_only", + redirect_uri: process.env.STRIPE_REDIRECT_URI!, + }); + + const stripeAuthUrl = + `https://connect.stripe.com/oauth/authorize?${params.toString()}`; + + return NextResponse.redirect(stripeAuthUrl); +} diff --git a/stripe_to_invoice/app/login/page.tsx b/stripe_to_invoice/app/login/page.tsx index 913b4b7..e505048 100644 --- a/stripe_to_invoice/app/login/page.tsx +++ b/stripe_to_invoice/app/login/page.tsx @@ -44,7 +44,7 @@ export default function LoginPage() { setEmail(e.target.value)} diff --git a/stripe_to_invoice/deployment/deployment.yaml b/stripe_to_invoice/deployment/deployment.yaml index d39a919..750ccf6 100644 --- a/stripe_to_invoice/deployment/deployment.yaml +++ b/stripe_to_invoice/deployment/deployment.yaml @@ -78,6 +78,12 @@ spec: name: stripe-secrets key: SES_FROM_EMAIL + - name: STRIPE_REDIRECT_URI + valueFrom: + secretKeyRef: + name: stripe-secrets + key: STRIPE_REDIRECT_URI + imagePullSecrets: - name: registrypullsecret diff --git a/stripe_to_invoice/deployment/secrets/.env b/stripe_to_invoice/deployment/secrets/.env index 92441dc..5eae4b1 100644 --- a/stripe_to_invoice/deployment/secrets/.env +++ b/stripe_to_invoice/deployment/secrets/.env @@ -6,6 +6,7 @@ DEV_AWS_REGION=eu-west-2 DEV_AWS_ACCESS_KEY_ID=AKIAQL67W6HI2547OPVG DEV_AWS_SECRET_ACCESS_KEY=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 DEV_SES_FROM_EMAIL=no-reply@juntekim.com +PROD_STRIPE_REDIRECT_URI=https://stripe-to-invoice.dev.juntekim.com/api/stripe/callback # Prod @@ -16,4 +17,5 @@ PROD_AWS_REGION=eu-west-2 PROD_AWS_ACCESS_KEY_ID=AKIAQL67W6HI2547OPVG PROD_AWS_SECRET_ACCESS_KEY=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 PROD_SES_FROM_EMAIL=no-reply@juntekim.com +PROD_STRIPE_REDIRECT_URI=https://stripe-to-invoice.dev.juntekim.com/api/stripe/callback