diff --git a/backend/ecosystem.config.js b/backend/ecosystem.config.js index 4aafe98..7f74be6 100644 --- a/backend/ecosystem.config.js +++ b/backend/ecosystem.config.js @@ -4,7 +4,8 @@ module.exports = { script: '/var/www/autojobs/backend/start_server.py', interpreter: 'python3', env: { - DB_PATH: '/var/www/autojobs/autojobs.db' + DB_PATH: '/var/www/autojobs/autojobs.db', + STRIPE_SECRET_KEY: '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' }, autorestart: true, watch: false, diff --git a/backend/main.py b/backend/main.py index b576ffe..2927d70 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,8 +1,10 @@ """ AutoJobs API — FastAPI Backend 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.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel @@ -926,4 +928,107 @@ def admin_reset_usage(user_id: str = None): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + 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) + diff --git a/frontend/app/api/auth/signup/route.ts b/frontend/app/api/auth/signup/route.ts index 50b513c..e5033f0 100644 --- a/frontend/app/api/auth/signup/route.ts +++ b/frontend/app/api/auth/signup/route.ts @@ -2,35 +2,52 @@ import { NextRequest, NextResponse } from "next/server" import { cookies } from "next/headers" 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) { 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 { - 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", 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) { return NextResponse.json({ error: "Failed to create user" }, { status: 500 }) } + const data = await res.json() + const cookieStore = await cookies() - cookieStore.set("autojobs_user", email.split("@")[0], { + cookieStore.set("autojobs_user", email, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", - maxAge: 60 * 60 * 24 * 30 // 30 days + maxAge: 60 * 60 * 24 * 30 }) + + // 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" }) + return NextResponse.json({ status: "ok", user_id: data.id }) } catch { return NextResponse.json({ error: "Server error" }, { status: 500 }) } -} +} \ No newline at end of file diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx index 3605965..1e4bd89 100644 --- a/frontend/app/signup/page.tsx +++ b/frontend/app/signup/page.tsx @@ -1,10 +1,28 @@ "use client" -import { useState } from "react" -import { useRouter } from "next/navigation" +import { useState, Suspense } from "react" +import { useRouter, useSearchParams } from "next/navigation" import Link from "next/link" -export default function SignupPage() { +const PLAN_INFO: Record = { + 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 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({ name: "", email: "", password: "", confirmPassword: "" }) @@ -24,10 +42,47 @@ export default function SignupPage() { const res = await fetch("/api/auth/signup", { method: "POST", 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) { - 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 { const data = await res.json() setError(data.error || "Signup failed") @@ -39,14 +94,30 @@ export default function SignupPage() { } return ( -
+
-
+
AutoJobs

Create your account

+ +
+
+
+
Selected Plan
+
{plan.name}
+
{plan.apps}/month
+
+
+
{plan.price}
+ {planId !== "free" && planId !== "agency_enterprise" && ( +
billed monthly
+ )} +
+
+
{error && ( @@ -58,66 +129,47 @@ export default function SignupPage() {
- 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" - /> + 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" />
-
- 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" - /> + 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" />
-
- 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="••••••••" - /> + 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="••••••••" />
-
- 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="••••••••" - /> + 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="••••••••" />
-

Already have an account? Sign in

+

+ By signing up, you agree to our Terms of Service and Privacy Policy. +

) } + +export default function SignupPage() { + return ( +
Loading...
}> + + + ) +} \ No newline at end of file