added stripe callback

This commit is contained in:
Jun-te Kim 2026-01-18 14:27:25 +00:00
parent c1ddbee66a
commit 3511fabc51
6 changed files with 127 additions and 2 deletions

View file

@ -128,6 +128,7 @@ jobs:
AWS_ACCESS_KEY_ID=$PROD_AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID=$PROD_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=$PROD_AWS_SECRET_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$PROD_AWS_SECRET_ACCESS_KEY
SES_FROM_EMAIL=$PROD_SES_FROM_EMAIL SES_FROM_EMAIL=$PROD_SES_FROM_EMAIL
STRIPE_REDIRECT_URI=$PROD_SES_STRIPE_REDIRECT_URI
else else
STRIPE_SECRET_KEY="$DEV_STRIPE_SECRET_KEY" STRIPE_SECRET_KEY="$DEV_STRIPE_SECRET_KEY"
STRIPE_CLIENT_ID="$DEV_STRIPE_CLIENT_ID" STRIPE_CLIENT_ID="$DEV_STRIPE_CLIENT_ID"
@ -136,6 +137,7 @@ jobs:
AWS_ACCESS_KEY_ID=$DEV_AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID=$DEV_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=$DEV_AWS_SECRET_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$DEV_AWS_SECRET_ACCESS_KEY
SES_FROM_EMAIL=$DEV_SES_FROM_EMAIL SES_FROM_EMAIL=$DEV_SES_FROM_EMAIL
STRIPE_REDIRECT_URI=$DEV_SES_STRIPE_REDIRECT_URI
fi fi
: "${STRIPE_SECRET_KEY:?missing STRIPE_SECRET_KEY}" : "${STRIPE_SECRET_KEY:?missing STRIPE_SECRET_KEY}"
: "${STRIPE_CLIENT_ID:?missing STRIPE_CLIENT_ID}" : "${STRIPE_CLIENT_ID:?missing STRIPE_CLIENT_ID}"
@ -145,8 +147,9 @@ jobs:
: "${AWS_ACCESS_KEY_ID:?missing AWS_ACCESS_KEY_ID}" : "${AWS_ACCESS_KEY_ID:?missing AWS_ACCESS_KEY_ID}"
: "${AWS_SECRET_ACCESS_KEY:?missing AWS_SECRET_ACCESS_KEY}" : "${AWS_SECRET_ACCESS_KEY:?missing AWS_SECRET_ACCESS_KEY}"
: "${SES_FROM_EMAIL:?missing SES_FROM_EMAIL}" : "${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 \ envsubst < stripe_to_invoice/deployment/secrets/stripe-secrets.yaml \
| kubectl apply -f - | kubectl apply -f -

View file

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

View file

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

View file

@ -44,7 +44,7 @@ export default function LoginPage() {
<input <input
type="email" type="email"
placeholder="you@company.com" placeholder="enter@email.com"
className="w-full border rounded p-2" className="w-full border rounded p-2"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}

View file

@ -78,6 +78,12 @@ spec:
name: stripe-secrets name: stripe-secrets
key: SES_FROM_EMAIL key: SES_FROM_EMAIL
- name: STRIPE_REDIRECT_URI
valueFrom:
secretKeyRef:
name: stripe-secrets
key: STRIPE_REDIRECT_URI
imagePullSecrets: imagePullSecrets:
- name: registrypullsecret - name: registrypullsecret

View file

@ -6,6 +6,7 @@ DEV_AWS_REGION=eu-west-2
DEV_AWS_ACCESS_KEY_ID=AKIAQL67W6HI2547OPVG DEV_AWS_ACCESS_KEY_ID=AKIAQL67W6HI2547OPVG
DEV_AWS_SECRET_ACCESS_KEY=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 DEV_AWS_SECRET_ACCESS_KEY=qCTirw/OCdw6P2aVknGlyh8MQVMmOkrm0NrXTz4j
DEV_SES_FROM_EMAIL=no-reply@juntekim.com DEV_SES_FROM_EMAIL=no-reply@juntekim.com
PROD_STRIPE_REDIRECT_URI=https://stripe-to-invoice.dev.juntekim.com/api/stripe/callback
# Prod # Prod
@ -16,4 +17,5 @@ PROD_AWS_REGION=eu-west-2
PROD_AWS_ACCESS_KEY_ID=AKIAQL67W6HI2547OPVG PROD_AWS_ACCESS_KEY_ID=AKIAQL67W6HI2547OPVG
PROD_AWS_SECRET_ACCESS_KEY=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 PROD_AWS_SECRET_ACCESS_KEY=qCTirw/OCdw6P2aVknGlyh8MQVMmOkrm0NrXTz4j
PROD_SES_FROM_EMAIL=no-reply@juntekim.com PROD_SES_FROM_EMAIL=no-reply@juntekim.com
PROD_STRIPE_REDIRECT_URI=https://stripe-to-invoice.dev.juntekim.com/api/stripe/callback