juntekim.com/stripe_to_invoice/app/api/auth/callback/route.ts

132 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { randomUUID } from "crypto";
import { and, eq, gt, isNull } from "drizzle-orm";
import { db } from "@/lib/db";
import { loginTokens, sessions, users, subscriptions } from "@/lib/schema";
import { hashToken } from "@/lib/auth/tokens";
export async function POST(req: Request) {
// --------------------------------------------------
// 1⃣ Parse token from request
// --------------------------------------------------
const { token } = await req.json();
if (!token) {
return NextResponse.json(
{ error: "Missing token" },
{ status: 400 }
);
}
// --------------------------------------------------
// 2⃣ Validate login token (magic link)
// --------------------------------------------------
const tokenHash = hashToken(token);
const loginToken = await db
.select()
.from(loginTokens)
.where(
and(
eq(loginTokens.tokenHash, tokenHash),
isNull(loginTokens.usedAt),
gt(loginTokens.expiresAt, new Date())
)
)
.limit(1)
.then((rows) => rows[0]);
if (!loginToken) {
return NextResponse.json(
{ error: "Invalid or expired token" },
{ status: 401 }
);
}
// --------------------------------------------------
// 3⃣ Ensure user still exists
// --------------------------------------------------
const user = await db
.select()
.from(users)
.where(eq(users.id, loginToken.userId))
.limit(1)
.then((rows) => rows[0]);
if (!user) {
return NextResponse.json(
{ error: "User not found" },
{ status: 404 }
);
}
// --------------------------------------------------
// 4⃣ Consume login token (one-time use)
// --------------------------------------------------
await db
.update(loginTokens)
.set({ usedAt: new Date() })
.where(eq(loginTokens.id, loginToken.id));
// --------------------------------------------------
// 5⃣ Ensure subscription record exists
// --------------------------------------------------
const existingSubscription = await db
.select()
.from(subscriptions)
.where(eq(subscriptions.userId, user.id))
.limit(1)
.then((rows) => rows[0]);
if (!existingSubscription) {
// Check if user was created within the last 2 weeks
const twoWeeksAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 14);
const userCreatedWithinTwoWeeks = new Date(user.createdAt) > twoWeeksAgo;
const subscriptionStatus = userCreatedWithinTwoWeeks ? "trialing" : "expired";
await db.insert(subscriptions).values({
id: randomUUID(),
userId: user.id,
status: subscriptionStatus,
stripeCustomerId: null,
stripeSubscriptionId: null,
currentPeriodStart: null,
currentPeriodEnd: null,
});
}
// --------------------------------------------------
// 6⃣ Create DB-backed session
// --------------------------------------------------
const sessionId = randomUUID();
const expiresAt = new Date(
Date.now() + 1000 * 60 * 60 * 24 * 14 // 14 days
);
await db.insert(sessions).values({
id: sessionId,
userId: user.id,
expiresAt,
});
// --------------------------------------------------
// 7⃣ Set secure session cookie
// --------------------------------------------------
const cookieStore = await cookies();
cookieStore.set("session", sessionId, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 60 * 60 * 24 * 14, // 14 days
});
// --------------------------------------------------
// 8⃣ Done
// --------------------------------------------------
return NextResponse.json({ ok: true });
}