Stripe checkout: products created, signup flow with plan selection, API endpoints
This commit is contained in:
@@ -4,7 +4,8 @@ module.exports = {
|
|||||||
script: '/var/www/autojobs/backend/start_server.py',
|
script: '/var/www/autojobs/backend/start_server.py',
|
||||||
interpreter: 'python3',
|
interpreter: 'python3',
|
||||||
env: {
|
env: {
|
||||||
DB_PATH: '/var/www/autojobs/autojobs.db'
|
DB_PATH: '/var/www/autojobs/autojobs.db',
|
||||||
|
STRIPE_SECRET_KEY: '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'
|
||||||
},
|
},
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
watch: false,
|
watch: false,
|
||||||
|
|||||||
+106
-1
@@ -1,8 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
AutoJobs API — FastAPI Backend
|
AutoJobs API — FastAPI Backend
|
||||||
Multi-user job search with per-user API keys + subscription plans
|
Multi-user job search with per-user API keys + subscription plans
|
||||||
|
import os
|
||||||
|
import stripe
|
||||||
"""
|
"""
|
||||||
from fastapi import FastAPI, HTTPException, Depends, APIRouter
|
from fastapi import FastAPI, HTTPException, Depends, APIRouter, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -927,3 +929,106 @@ def admin_reset_usage(user_id: str = None):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
# --- Stripe Price IDs ---
|
||||||
|
STRIPE_PRICE_IDS = {
|
||||||
|
"free": None,
|
||||||
|
"starter": "price_1TLp9OEqqBlW1z4N8zXPHPjM",
|
||||||
|
"pro": "price_1TLp9PEqqBlW1z4NmP5sVrA1",
|
||||||
|
"ultra": "price_1TLp9QEqqBlW1z4NpjjV13Kb",
|
||||||
|
"unlimited": "price_1TLp9REqqBlW1z4NR2c92fmM",
|
||||||
|
"agency_starter": "price_1TLp9SEqqBlW1z4Ny9MabLz3",
|
||||||
|
"agency_growth": "price_1TLp9TEqqBlW1z4N2t2noudV",
|
||||||
|
"agency_scale": "price_1TLp9UEqqBlW1z4NgWPSg7nr",
|
||||||
|
"agency_pro": "price_1TLp9VEqqBlW1z4NgtQWlUnv",
|
||||||
|
"agency_enterprise": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Stripe Checkout Endpoint ---
|
||||||
|
@app.post("/autojobs/api/create-checkout")
|
||||||
|
async def create_checkout(request: Request):
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
plan_id = body.get("plan_id")
|
||||||
|
user_id = body.get("user_id")
|
||||||
|
user_type = body.get("user_type", "private")
|
||||||
|
|
||||||
|
if not plan_id or not user_id:
|
||||||
|
return JSONResponse({"error": "Missing plan_id or user_id"}, status_code=400)
|
||||||
|
|
||||||
|
price_id = STRIPE_PRICE_IDS.get(plan_id)
|
||||||
|
if price_id is None and plan_id != "agency_enterprise":
|
||||||
|
return JSONResponse({"error": "Invalid plan"}, status_code=400)
|
||||||
|
|
||||||
|
user = db.get("SELECT * FROM users WHERE id = ?", (user_id,))
|
||||||
|
if not user:
|
||||||
|
return JSONResponse({"error": "User not found"}, status_code=404)
|
||||||
|
|
||||||
|
if plan_id == "free":
|
||||||
|
# Free plan - just update and return
|
||||||
|
db.run("UPDATE users SET plan = ?, user_type = ? WHERE id = ?", (plan_id, user_type, user_id))
|
||||||
|
return JSONResponse({"success": True, "plan": plan_id})
|
||||||
|
|
||||||
|
if plan_id == "agency_enterprise":
|
||||||
|
return JSONResponse({"error": "Contact us for enterprise pricing"}, status_code=400)
|
||||||
|
|
||||||
|
# Get or create Stripe customer
|
||||||
|
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY")
|
||||||
|
customer = stripe.Customer.create(
|
||||||
|
email=user["email"],
|
||||||
|
name=user["name"],
|
||||||
|
metadata={"autojobs_user_id": str(user_id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
customer=customer.id,
|
||||||
|
payment_method_types=["card"],
|
||||||
|
line_items=[{"price": price_id, "quantity": 1}],
|
||||||
|
mode="subscription",
|
||||||
|
success_url="https://hostpioneers.com/autojobs/dashboard?session_id={CHECKOUT_SESSION_ID}",
|
||||||
|
cancel_url="https://hostpioneers.com/autojobs/pricing?canceled=true",
|
||||||
|
metadata={"user_id": str(user_id), "plan_id": plan_id, "user_type": user_type},
|
||||||
|
subscription_data={"metadata": {"user_id": str(user_id), "plan_id": plan_id}}
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse({"checkout_url": checkout_session.url})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
return JSONResponse({"error": str(e), "trace": traceback.format_exc()}, status_code=500)
|
||||||
|
|
||||||
|
# --- Stripe Webhook ---
|
||||||
|
@app.post("/autojobs/api/stripe-webhook")
|
||||||
|
async def stripe_webhook(request: Request):
|
||||||
|
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY")
|
||||||
|
payload = await request.body()
|
||||||
|
sig = request.headers.get("stripe-signature", "")
|
||||||
|
webhook_secret = os.environ.get("STRIPE_WEBHOOK_SECRET", "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if webhook_secret:
|
||||||
|
event = stripe.Webhook.construct_event(payload, sig, webhook_secret)
|
||||||
|
else:
|
||||||
|
event = json.loads(payload)
|
||||||
|
|
||||||
|
event_type = event.get("type", "")
|
||||||
|
|
||||||
|
if event_type == "checkout.session.completed":
|
||||||
|
session = event["data"]["object"]
|
||||||
|
user_id = session["metadata"]["user_id"]
|
||||||
|
plan_id = session["metadata"]["plan_id"]
|
||||||
|
stripe_sub_id = session.get("subscription")
|
||||||
|
|
||||||
|
db.run("UPDATE users SET plan = ?, stripe_subscription_id = ?, stripe_customer_id = ? WHERE id = ?",
|
||||||
|
(plan_id, stripe_sub_id, session.get("customer"), user_id))
|
||||||
|
|
||||||
|
elif event_type == "customer.subscription.deleted":
|
||||||
|
sub = event["data"]["object"]
|
||||||
|
customer_id = sub.get("customer")
|
||||||
|
db.run("UPDATE users SET plan = 'free' WHERE stripe_customer_id = ?", (customer_id,))
|
||||||
|
|
||||||
|
return JSONResponse({"received": True})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
return JSONResponse({"error": str(e), "trace": traceback.format_exc()}, status_code=400)
|
||||||
|
|
||||||
|
|||||||
@@ -2,34 +2,51 @@ import { NextRequest, NextResponse } from "next/server"
|
|||||||
import { cookies } from "next/headers"
|
import { cookies } from "next/headers"
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const { email, password, name } = await req.json()
|
const { email, password, name, user_type, plan } = await req.json()
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
return NextResponse.json({ error: "Email and password required" }, { status: 400 })
|
return NextResponse.json({ error: "Email and password required" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production: hash password, store in DB
|
|
||||||
// For MVP: create user in backend DB, set cookie
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${process.env.API_URL || "http://localhost:8000"}/users`, {
|
const apiUrl = process.env.API_URL || "http://localhost:8000"
|
||||||
|
|
||||||
|
const res = await fetch(`${apiUrl}/autojobs/api/users`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ user_id: email.split("@")[0], email, name })
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
user_type: user_type || "private",
|
||||||
|
plan: plan || "free"
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return NextResponse.json({ error: "Failed to create user" }, { status: 500 })
|
return NextResponse.json({ error: "Failed to create user" }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
cookieStore.set("autojobs_user", email.split("@")[0], {
|
cookieStore.set("autojobs_user", email, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
maxAge: 60 * 60 * 24 * 30 // 30 days
|
maxAge: 60 * 60 * 24 * 30
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json({ status: "ok" })
|
// Also store user_id for checkout
|
||||||
|
if (data.id) {
|
||||||
|
cookieStore.set("autojobs_user_id", String(data.id), {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 60 * 24 * 30
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ status: "ok", user_id: data.id })
|
||||||
} catch {
|
} catch {
|
||||||
return NextResponse.json({ error: "Server error" }, { status: 500 })
|
return NextResponse.json({ error: "Server error" }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-48
@@ -1,10 +1,28 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useState } from "react"
|
import { useState, Suspense } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
export default function SignupPage() {
|
const PLAN_INFO: Record<string, { name: string; price: string; apps: string }> = {
|
||||||
|
free: { name: "Free", price: "$0/mo", apps: "5 apps" },
|
||||||
|
starter: { name: "Starter", price: "$29/mo", apps: "20 apps" },
|
||||||
|
pro: { name: "Pro", price: "$69/mo", apps: "100 apps" },
|
||||||
|
ultra: { name: "Ultra", price: "$149/mo", apps: "200 apps" },
|
||||||
|
unlimited: { name: "Unlimited", price: "$199/mo", apps: "Unlimited" },
|
||||||
|
agency_starter: { name: "Agency Starter", price: "$555/mo", apps: "1,000 submissions" },
|
||||||
|
agency_growth: { name: "Agency Growth", price: "$999/mo", apps: "3,000 submissions" },
|
||||||
|
agency_scale: { name: "Agency Scale", price: "$1,499/mo", apps: "5,000 submissions" },
|
||||||
|
agency_pro: { name: "Agency Pro", price: "$3,699/mo", apps: "10,000 submissions" },
|
||||||
|
agency_enterprise: { name: "Enterprise", price: "Contact Us", apps: "Unlimited" },
|
||||||
|
}
|
||||||
|
|
||||||
|
function SignupForm() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const planId = searchParams.get("plan") || "free"
|
||||||
|
const userType = searchParams.get("type") || "private"
|
||||||
|
const plan = PLAN_INFO[planId] || PLAN_INFO.free
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
name: "", email: "", password: "", confirmPassword: ""
|
name: "", email: "", password: "", confirmPassword: ""
|
||||||
})
|
})
|
||||||
@@ -24,10 +42,47 @@ export default function SignupPage() {
|
|||||||
const res = await fetch("/api/auth/signup", {
|
const res = await fetch("/api/auth/signup", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ name: form.name, email: form.email, password: form.password })
|
body: JSON.stringify({
|
||||||
|
name: form.name,
|
||||||
|
email: form.email,
|
||||||
|
password: form.password,
|
||||||
|
user_type: userType,
|
||||||
|
plan: planId
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
router.push("/autojobs/profile-setup")
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (planId === "free") {
|
||||||
|
router.push("/autojobs/dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planId === "agency_enterprise") {
|
||||||
|
router.push("/autojobs/dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkoutRes = await fetch("https://hostpioneers.com/autojobs/api/create-checkout", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: data.user_id,
|
||||||
|
plan_id: planId,
|
||||||
|
user_type: userType
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (checkoutRes.ok) {
|
||||||
|
const checkoutData = await checkoutRes.json()
|
||||||
|
if (checkoutData.checkout_url) {
|
||||||
|
window.location.href = checkoutData.checkout_url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/autojobs/dashboard")
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setError(data.error || "Signup failed")
|
setError(data.error || "Signup failed")
|
||||||
@@ -39,15 +94,31 @@ export default function SignupPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-900 flex items-center justify-center px-4">
|
<div className="min-h-screen bg-slate-900 flex items-center justify-center px-4 py-12">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-6">
|
||||||
<Link href="/autojobs" className="text-2xl font-bold text-white">
|
<Link href="/autojobs" className="text-2xl font-bold text-white">
|
||||||
Auto<span className="text-blue-400">Jobs</span>
|
Auto<span className="text-blue-400">Jobs</span>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="text-slate-400 mt-2">Create your account</p>
|
<p className="text-slate-400 mt-2">Create your account</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-800 rounded-2xl p-5 border border-blue-500/30 mb-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-slate-400">Selected Plan</div>
|
||||||
|
<div className="text-xl font-bold text-white">{plan.name}</div>
|
||||||
|
<div className="text-xs text-slate-400">{plan.apps}/month</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-2xl font-bold text-blue-400">{plan.price}</div>
|
||||||
|
{planId !== "free" && planId !== "agency_enterprise" && (
|
||||||
|
<div className="text-xs text-slate-400">billed monthly</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="bg-slate-800 rounded-2xl p-8 border border-slate-700">
|
<form onSubmit={handleSubmit} className="bg-slate-800 rounded-2xl p-8 border border-slate-700">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||||||
@@ -58,66 +129,47 @@ export default function SignupPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-slate-400 mb-1">Full Name</label>
|
<label className="block text-sm text-slate-400 mb-1">Full Name</label>
|
||||||
<input
|
<input required type="text" value={form.name} onChange={e => setForm({...form, name: e.target.value})}
|
||||||
required
|
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" placeholder="John Smith" />
|
||||||
type="text"
|
|
||||||
value={form.name}
|
|
||||||
onChange={e => setForm({...form, name: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
||||||
placeholder="John Smith"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-slate-400 mb-1">Email</label>
|
<label className="block text-sm text-slate-400 mb-1">Email</label>
|
||||||
<input
|
<input required type="email" value={form.email} onChange={e => setForm({...form, email: e.target.value})}
|
||||||
required
|
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" placeholder="you@example.com" />
|
||||||
type="email"
|
|
||||||
value={form.email}
|
|
||||||
onChange={e => setForm({...form, email: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
||||||
placeholder="you@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-slate-400 mb-1">Password</label>
|
<label className="block text-sm text-slate-400 mb-1">Password</label>
|
||||||
<input
|
<input required type="password" value={form.password} onChange={e => setForm({...form, password: e.target.value})}
|
||||||
required
|
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" placeholder="••••••••" />
|
||||||
type="password"
|
|
||||||
value={form.password}
|
|
||||||
onChange={e => setForm({...form, password: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-slate-400 mb-1">Confirm Password</label>
|
<label className="block text-sm text-slate-400 mb-1">Confirm Password</label>
|
||||||
<input
|
<input required type="password" value={form.confirmPassword} onChange={e => setForm({...form, confirmPassword: e.target.value})}
|
||||||
required
|
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500" placeholder="••••••••" />
|
||||||
type="password"
|
|
||||||
value={form.confirmPassword}
|
|
||||||
onChange={e => setForm({...form, confirmPassword: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button type="submit" disabled={loading}
|
||||||
type="submit"
|
className="w-full mt-6 py-3 bg-blue-500 hover:bg-blue-600 disabled:bg-slate-600 text-white rounded-xl font-semibold transition">
|
||||||
disabled={loading}
|
{loading ? "Creating account..." : planId === "free" ? "Create Free Account" : `Pay ${plan.price} & Subscribe`}
|
||||||
className="w-full mt-6 py-3 bg-blue-500 hover:bg-blue-600 disabled:bg-slate-600 text-white rounded-xl font-semibold transition"
|
|
||||||
>
|
|
||||||
{loading ? "Creating account..." : "Create Account"}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-center text-slate-400 mt-6 text-sm">
|
<p className="text-center text-slate-400 mt-6 text-sm">
|
||||||
Already have an account? <Link href="/autojobs/login" className="text-blue-400 hover:underline">Sign in</Link>
|
Already have an account? <Link href="/autojobs/login" className="text-blue-400 hover:underline">Sign in</Link>
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-center text-slate-500 mt-4 text-xs">
|
||||||
|
By signing up, you agree to our Terms of Service and Privacy Policy.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function SignupPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="min-h-screen bg-slate-900 flex items-center justify-center"><div className="text-white">Loading...</div></div>}>
|
||||||
|
<SignupForm />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user