feat: add subscription service — user auth, Stripe billing, API keys, dashboard

- NextAuth v5 with email+password credentials, JWT sessions
- Registration, login, email verification, password reset flows
- Stripe integration: Free (15/day), Starter ($5/1k/mo), Pro ($20/100k/mo)
- API key management (cb_ prefix) with hash-based validation
- Dashboard with generations history, settings, billing management
- Rate limiting: Redis daily counter (free), DB monthly (paid)
- Generate route auth: Bearer API key + session, anonymous allowed
- Worker userId propagation for generation history
- Pricing section on landing page, auth-aware navbar
- Middleware with route protection, CORS for codeboard.vectry.tech
- Docker env vars for auth, Stripe, email (smtp.migadu.com)
This commit is contained in:
Vectry
2026-02-10 20:08:13 +00:00
parent 7ff493a89a
commit 64ce70daa4
45 changed files with 3073 additions and 34 deletions

View File

@@ -1,3 +1,4 @@
import Link from "next/link";
import { RepoInput } from "@/components/repo-input";
import { ExampleRepoCard } from "@/components/example-repo-card";
import { ScrollSection } from "@/components/scroll-section";
@@ -17,6 +18,9 @@ import {
Terminal,
FileCode,
CheckCircle2,
Check,
Crown,
Zap,
} from "lucide-react";
export default function HomePage() {
@@ -74,6 +78,42 @@ export default function HomePage() {
},
];
const pricingTiers = [
{
name: "Free",
price: 0,
period: "forever",
description: "Get started with CodeBoard",
generations: "15 / day",
features: ["15 generations per day", "Public repository support", "Interactive documentation", "Architecture diagrams"],
cta: "Get Started",
href: "/register",
highlighted: false,
},
{
name: "Starter",
price: 5,
period: "month",
description: "For regular use",
generations: "1,000 / month",
features: ["1,000 generations per month", "Generation history", "API key access", "Priority support"],
cta: "Start Free Trial",
href: "/register",
highlighted: true,
},
{
name: "Pro",
price: 20,
period: "month",
description: "For teams & power users",
generations: "100,000 / month",
features: ["100,000 generations per month", "Full generation history", "Multiple API keys", "Dedicated support", "Custom integrations"],
cta: "Start Free Trial",
href: "/register",
highlighted: false,
},
];
const exampleRepos = [
{
name: "sindresorhus/p-limit",
@@ -271,8 +311,8 @@ export default function HomePage() {
</div>
<div className="hidden sm:block w-px bg-zinc-800" />
<div className="text-center">
<div className="text-2xl sm:text-3xl font-bold text-white">100%</div>
<div className="text-sm text-zinc-500">Free for public repos</div>
<div className="text-2xl sm:text-3xl font-bold text-white">Free</div>
<div className="text-sm text-zinc-500">tier to start</div>
</div>
<div className="hidden sm:block w-px bg-zinc-800" />
<div className="text-center">
@@ -402,6 +442,98 @@ export default function HomePage() {
</div>
</section>
<section id="pricing" className="py-20 lg:py-32">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollSection>
<div className="text-center mb-16">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass mb-6">
<Zap className="w-4 h-4 text-blue-400" />
<span className="text-sm text-zinc-300">Pricing</span>
</div>
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
Simple, Transparent <span className="gradient-text">Pricing</span>
</h2>
<p className="text-zinc-400 max-w-xl mx-auto">
Start free, scale when you need to
</p>
</div>
</ScrollSection>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
{pricingTiers.map((tier, i) => (
<ScrollSection key={tier.name} delay={i + 1}>
<div
className={`relative group h-full rounded-2xl p-8 transition-all duration-300 hover:-translate-y-1 ${
tier.highlighted
? "glass-strong border-blue-500/30 shadow-lg shadow-blue-500/10"
: "glass hover:bg-white/[0.05]"
}`}
>
{tier.highlighted && (
<>
<div className="absolute -inset-px rounded-2xl bg-gradient-to-b from-blue-500/20 via-transparent to-blue-500/10 -z-10" />
<div className="absolute -top-3 left-1/2 -translate-x-1/2 inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-500/20 border border-blue-500/30 text-xs font-medium text-blue-300">
<Crown className="w-3 h-3" />
Most Popular
</div>
</>
)}
<div className="mb-6">
<h3 className="text-lg font-semibold text-white mb-1">{tier.name}</h3>
<p className="text-sm text-zinc-500">{tier.description}</p>
</div>
<div className="mb-6">
<div className="flex items-baseline gap-1">
{tier.price === 0 ? (
<span className="text-4xl font-bold text-white">Free</span>
) : (
<>
<span className="text-4xl font-bold text-white">${tier.price}</span>
<span className="text-zinc-500">/ {tier.period}</span>
</>
)}
</div>
<div className="mt-2 text-sm text-zinc-400">
<span className="text-blue-400 font-medium">{tier.generations}</span> generations
</div>
</div>
<div className="mb-8 space-y-3">
{tier.features.map((feature) => (
<div key={feature} className="flex items-start gap-3">
<div className={`flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center mt-0.5 ${
tier.highlighted
? "bg-blue-500/20 text-blue-400"
: "bg-white/10 text-zinc-400"
}`}>
<Check className="w-3 h-3" />
</div>
<span className="text-sm text-zinc-300">{feature}</span>
</div>
))}
</div>
<div className="mt-auto">
<Link
href={tier.href}
className={`block w-full text-center py-3 px-6 rounded-xl text-sm font-medium transition-all duration-200 ${
tier.highlighted
? "btn-primary"
: "glass border border-white/10 text-white hover:bg-white/10 hover:border-white/20"
}`}
>
{tier.cta}
</Link>
</div>
</div>
</ScrollSection>
))}
</div>
</div>
</section>
<section className="py-20 lg:py-32">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollSection>
@@ -452,7 +584,7 @@ export default function HomePage() {
<span className="hidden sm:inline"></span>
<span>Free for public repositories</span>
<span className="hidden sm:inline"></span>
<span>No signup required</span>
<span>Free tier available</span>
</div>
</div>
</div>