Spaces:
Running
fix(routing): fix broken sign-in/signup links, admin redirect, and Google OAuth
Browse files- Website CTAs (Header, Footer, CTASection, PlanCard, plans page) now use
<a href="/app/..."> for cross-app navigation — Next.js <Link> was silently
doing SPA navigation within the Website, which has no /login or /signup routes.
- APP_URL changed from hardcoded localhost:3000 to /app (relative, works in any env).
- nginx admin redirect changed from exact-match to regex so /admin/login,
/admin/users, etc. are all covered.
- Google OAuth redirect_uri now includes the /app basePath prefix to match the
actual callback route at /app/auth/google/callback.
NOTE: Google Cloud Console must also have /app/auth/google/callback added as an
authorized redirect URI for Google OAuth to work end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Website/next-env.d.ts +1 -1
- Website/src/app/about/page.tsx +1 -1
- Website/src/app/plans/page.tsx +3 -3
- Website/src/app/use-cases/page.tsx +1 -1
- Website/src/components/CTASection.tsx +3 -3
- Website/src/components/Footer.tsx +9 -7
- Website/src/components/Header.tsx +12 -12
- Website/src/components/PlanCard.tsx +3 -3
- Website/src/lib/api.ts +1 -1
- backend/app/api/v1/auth.py +2 -1
- nginx.conf +2 -2
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
-
import "./.next/
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
|
|
| 1 |
/// <reference types="next" />
|
| 2 |
/// <reference types="next/image-types/global" />
|
| 3 |
+
import "./.next/types/routes.d.ts";
|
| 4 |
|
| 5 |
// NOTE: This file should not be edited
|
| 6 |
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -177,7 +177,7 @@ export default function AboutPage() {
|
|
| 177 |
headline="Ready to work with us?"
|
| 178 |
subheadline="Start with the Free plan or sign up to see LeadPilot in action."
|
| 179 |
primaryLabel="Start for Free"
|
| 180 |
-
primaryHref="/signup"
|
| 181 |
secondaryLabel="Contact Us"
|
| 182 |
secondaryHref="/contact"
|
| 183 |
/>
|
|
|
|
| 177 |
headline="Ready to work with us?"
|
| 178 |
subheadline="Start with the Free plan or sign up to see LeadPilot in action."
|
| 179 |
primaryLabel="Start for Free"
|
| 180 |
+
primaryHref="/app/signup"
|
| 181 |
secondaryLabel="Contact Us"
|
| 182 |
secondaryHref="/contact"
|
| 183 |
/>
|
|
@@ -10,7 +10,7 @@ import { useCatalog, type PlanCatalogEntry } from "@/lib/catalog";
|
|
| 10 |
const tiers = [
|
| 11 |
{
|
| 12 |
name: "Free", tagline: "Start capturing and qualifying leads today", price: "$0", priceNote: "/ month, forever",
|
| 13 |
-
available: true, highlighted: true, ctaLabel: "Get Started Free", ctaHref: "/signup",
|
| 14 |
features: ["Lead capture (limited monthly volume)", "AI qualification prompts (basic)", "Manual routing to team members", "Email notifications on new leads", "Basic analytics dashboard", "1 team seat", "Community support"],
|
| 15 |
notIncluded: ["Automated routing rules", "CRM integrations", "Advanced AI playbooks", "Priority support"],
|
| 16 |
},
|
|
@@ -240,9 +240,9 @@ export default function PlansPage() {
|
|
| 240 |
</ul>
|
| 241 |
|
| 242 |
{tier.available ? (
|
| 243 |
-
<
|
| 244 |
{tier.ctaLabel}
|
| 245 |
-
</
|
| 246 |
) : (
|
| 247 |
<a href="#waitlist" className="block text-center px-6 py-3 rounded-xl font-semibold text-sm mk-coming-soon-btn">
|
| 248 |
Join Waitlist
|
|
|
|
| 10 |
const tiers = [
|
| 11 |
{
|
| 12 |
name: "Free", tagline: "Start capturing and qualifying leads today", price: "$0", priceNote: "/ month, forever",
|
| 13 |
+
available: true, highlighted: true, ctaLabel: "Get Started Free", ctaHref: "/app/signup",
|
| 14 |
features: ["Lead capture (limited monthly volume)", "AI qualification prompts (basic)", "Manual routing to team members", "Email notifications on new leads", "Basic analytics dashboard", "1 team seat", "Community support"],
|
| 15 |
notIncluded: ["Automated routing rules", "CRM integrations", "Advanced AI playbooks", "Priority support"],
|
| 16 |
},
|
|
|
|
| 240 |
</ul>
|
| 241 |
|
| 242 |
{tier.available ? (
|
| 243 |
+
<a href={tier.ctaHref} className="block text-center px-6 py-3 rounded-xl font-semibold text-white text-sm mk-btn-primary">
|
| 244 |
{tier.ctaLabel}
|
| 245 |
+
</a>
|
| 246 |
) : (
|
| 247 |
<a href="#waitlist" className="block text-center px-6 py-3 rounded-xl font-semibold text-sm mk-coming-soon-btn">
|
| 248 |
Join Waitlist
|
|
@@ -222,7 +222,7 @@ export default function UseCasesPage() {
|
|
| 222 |
headline="Ready to try it in your business?"
|
| 223 |
subheadline="Start with the Free plan — no credit card, no commitment."
|
| 224 |
primaryLabel="Start for Free"
|
| 225 |
-
primaryHref="/signup"
|
| 226 |
secondaryLabel="See Plans"
|
| 227 |
secondaryHref="/plans"
|
| 228 |
/>
|
|
|
|
| 222 |
headline="Ready to try it in your business?"
|
| 223 |
subheadline="Start with the Free plan — no credit card, no commitment."
|
| 224 |
primaryLabel="Start for Free"
|
| 225 |
+
primaryHref="/app/signup"
|
| 226 |
secondaryLabel="See Plans"
|
| 227 |
secondaryHref="/plans"
|
| 228 |
/>
|
|
@@ -14,7 +14,7 @@ export default function CTASection({
|
|
| 14 |
headline = "Your leads aren't waiting. Neither should you.",
|
| 15 |
subheadline = "Start capturing and qualifying leads today — free, no credit card required.",
|
| 16 |
primaryLabel = "Start for Free",
|
| 17 |
-
primaryHref = "/signup",
|
| 18 |
secondaryLabel = "See Product",
|
| 19 |
secondaryHref = "/product",
|
| 20 |
}: Props) {
|
|
@@ -55,12 +55,12 @@ export default function CTASection({
|
|
| 55 |
</AnimateOnScroll>
|
| 56 |
<AnimateOnScroll animation="zoomIn" delay={160}>
|
| 57 |
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
| 58 |
-
<
|
| 59 |
href={primaryHref}
|
| 60 |
className="px-9 py-4 rounded-xl font-semibold text-base mk-btn-primary"
|
| 61 |
>
|
| 62 |
{primaryLabel}
|
| 63 |
-
</
|
| 64 |
<Link
|
| 65 |
href={secondaryHref}
|
| 66 |
className="px-9 py-4 rounded-xl font-semibold text-sm mk-btn-ghost-dark"
|
|
|
|
| 14 |
headline = "Your leads aren't waiting. Neither should you.",
|
| 15 |
subheadline = "Start capturing and qualifying leads today — free, no credit card required.",
|
| 16 |
primaryLabel = "Start for Free",
|
| 17 |
+
primaryHref = "/app/signup",
|
| 18 |
secondaryLabel = "See Product",
|
| 19 |
secondaryHref = "/product",
|
| 20 |
}: Props) {
|
|
|
|
| 55 |
</AnimateOnScroll>
|
| 56 |
<AnimateOnScroll animation="zoomIn" delay={160}>
|
| 57 |
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
| 58 |
+
<a
|
| 59 |
href={primaryHref}
|
| 60 |
className="px-9 py-4 rounded-xl font-semibold text-base mk-btn-primary"
|
| 61 |
>
|
| 62 |
{primaryLabel}
|
| 63 |
+
</a>
|
| 64 |
<Link
|
| 65 |
href={secondaryHref}
|
| 66 |
className="px-9 py-4 rounded-xl font-semibold text-sm mk-btn-ghost-dark"
|
|
@@ -11,7 +11,7 @@ const cols = {
|
|
| 11 |
Company: [
|
| 12 |
{ label: "About", href: "/about" },
|
| 13 |
{ label: "Contact", href: "/contact" },
|
| 14 |
-
{ label: "Start Free", href: "/signup" },
|
| 15 |
],
|
| 16 |
Legal: [
|
| 17 |
{ label: "Privacy Policy", href: "#" },
|
|
@@ -79,12 +79,12 @@ export default function MarketingFooter() {
|
|
| 79 |
</div>
|
| 80 |
|
| 81 |
{/* Mini CTA */}
|
| 82 |
-
<
|
| 83 |
-
href="/signup"
|
| 84 |
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold text-white mk-btn-primary"
|
| 85 |
>
|
| 86 |
Start for Free →
|
| 87 |
-
</
|
| 88 |
</div>
|
| 89 |
|
| 90 |
{/* Nav columns */}
|
|
@@ -99,9 +99,11 @@ export default function MarketingFooter() {
|
|
| 99 |
<ul className="space-y-3" role="list">
|
| 100 |
{links.map(({ label, href }) => (
|
| 101 |
<li key={label}>
|
| 102 |
-
|
| 103 |
-
{label}
|
| 104 |
-
|
|
|
|
|
|
|
| 105 |
</li>
|
| 106 |
))}
|
| 107 |
</ul>
|
|
|
|
| 11 |
Company: [
|
| 12 |
{ label: "About", href: "/about" },
|
| 13 |
{ label: "Contact", href: "/contact" },
|
| 14 |
+
{ label: "Start Free", href: "/app/signup" },
|
| 15 |
],
|
| 16 |
Legal: [
|
| 17 |
{ label: "Privacy Policy", href: "#" },
|
|
|
|
| 79 |
</div>
|
| 80 |
|
| 81 |
{/* Mini CTA */}
|
| 82 |
+
<a
|
| 83 |
+
href="/app/signup"
|
| 84 |
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold text-white mk-btn-primary"
|
| 85 |
>
|
| 86 |
Start for Free →
|
| 87 |
+
</a>
|
| 88 |
</div>
|
| 89 |
|
| 90 |
{/* Nav columns */}
|
|
|
|
| 99 |
<ul className="space-y-3" role="list">
|
| 100 |
{links.map(({ label, href }) => (
|
| 101 |
<li key={label}>
|
| 102 |
+
{href.startsWith("/app") ? (
|
| 103 |
+
<a href={href} className="text-sm mk-footer-link">{label}</a>
|
| 104 |
+
) : (
|
| 105 |
+
<Link href={href} className="text-sm mk-footer-link">{label}</Link>
|
| 106 |
+
)}
|
| 107 |
</li>
|
| 108 |
))}
|
| 109 |
</ul>
|
|
@@ -118,18 +118,18 @@ export default function MarketingHeader() {
|
|
| 118 |
|
| 119 |
{/* Desktop CTAs */}
|
| 120 |
<div className="hidden md:flex items-center gap-3">
|
| 121 |
-
<
|
| 122 |
-
href="/login"
|
| 123 |
className="text-sm font-medium transition-colors duration-200 mk-footer-link px-2 py-1"
|
| 124 |
>
|
| 125 |
Log in
|
| 126 |
-
</
|
| 127 |
-
<
|
| 128 |
-
href="/signup"
|
| 129 |
className="px-4 py-2 rounded-lg text-sm font-semibold text-white mk-btn-primary"
|
| 130 |
>
|
| 131 |
Start for Free
|
| 132 |
-
</
|
| 133 |
</div>
|
| 134 |
|
| 135 |
{/* Mobile toggle */}
|
|
@@ -199,20 +199,20 @@ export default function MarketingHeader() {
|
|
| 199 |
className="flex flex-col gap-2.5 mt-4 pt-4"
|
| 200 |
style={{ borderTop: "1px solid rgba(255,255,255,0.07)" }}
|
| 201 |
>
|
| 202 |
-
<
|
| 203 |
-
href="/login"
|
| 204 |
className="block py-2.5 rounded-lg text-sm font-medium text-center mk-footer-link"
|
| 205 |
onClick={() => setOpen(false)}
|
| 206 |
>
|
| 207 |
Log in
|
| 208 |
-
</
|
| 209 |
-
<
|
| 210 |
-
href="/signup"
|
| 211 |
className="block py-2.5 rounded-xl text-sm font-semibold text-white text-center mk-btn-primary"
|
| 212 |
onClick={() => setOpen(false)}
|
| 213 |
>
|
| 214 |
Start for Free
|
| 215 |
-
</
|
| 216 |
</div>
|
| 217 |
</div>
|
| 218 |
</div>
|
|
|
|
| 118 |
|
| 119 |
{/* Desktop CTAs */}
|
| 120 |
<div className="hidden md:flex items-center gap-3">
|
| 121 |
+
<a
|
| 122 |
+
href="/app/login"
|
| 123 |
className="text-sm font-medium transition-colors duration-200 mk-footer-link px-2 py-1"
|
| 124 |
>
|
| 125 |
Log in
|
| 126 |
+
</a>
|
| 127 |
+
<a
|
| 128 |
+
href="/app/signup"
|
| 129 |
className="px-4 py-2 rounded-lg text-sm font-semibold text-white mk-btn-primary"
|
| 130 |
>
|
| 131 |
Start for Free
|
| 132 |
+
</a>
|
| 133 |
</div>
|
| 134 |
|
| 135 |
{/* Mobile toggle */}
|
|
|
|
| 199 |
className="flex flex-col gap-2.5 mt-4 pt-4"
|
| 200 |
style={{ borderTop: "1px solid rgba(255,255,255,0.07)" }}
|
| 201 |
>
|
| 202 |
+
<a
|
| 203 |
+
href="/app/login"
|
| 204 |
className="block py-2.5 rounded-lg text-sm font-medium text-center mk-footer-link"
|
| 205 |
onClick={() => setOpen(false)}
|
| 206 |
>
|
| 207 |
Log in
|
| 208 |
+
</a>
|
| 209 |
+
<a
|
| 210 |
+
href="/app/signup"
|
| 211 |
className="block py-2.5 rounded-xl text-sm font-semibold text-white text-center mk-btn-primary"
|
| 212 |
onClick={() => setOpen(false)}
|
| 213 |
>
|
| 214 |
Start for Free
|
| 215 |
+
</a>
|
| 216 |
</div>
|
| 217 |
</div>
|
| 218 |
</div>
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
| 16 |
|
| 17 |
export default function PlanCard({
|
| 18 |
name, tagline, price, priceNote, features, notIncluded = [],
|
| 19 |
-
ctaLabel, ctaHref = "/signup", available, highlighted = false,
|
| 20 |
}: Props) {
|
| 21 |
return (
|
| 22 |
<div
|
|
@@ -79,9 +79,9 @@ export default function PlanCard({
|
|
| 79 |
</ul>
|
| 80 |
|
| 81 |
{available ? (
|
| 82 |
-
<
|
| 83 |
{ctaLabel}
|
| 84 |
-
</
|
| 85 |
) : (
|
| 86 |
<a href="#waitlist" className="block text-center px-6 py-3 rounded-xl font-semibold text-sm mk-coming-soon-btn">
|
| 87 |
Join Waitlist
|
|
|
|
| 16 |
|
| 17 |
export default function PlanCard({
|
| 18 |
name, tagline, price, priceNote, features, notIncluded = [],
|
| 19 |
+
ctaLabel, ctaHref = "/app/signup", available, highlighted = false,
|
| 20 |
}: Props) {
|
| 21 |
return (
|
| 22 |
<div
|
|
|
|
| 79 |
</ul>
|
| 80 |
|
| 81 |
{available ? (
|
| 82 |
+
<a href={ctaHref} className="block text-center px-6 py-3 rounded-xl font-semibold text-sm mk-btn-primary">
|
| 83 |
{ctaLabel}
|
| 84 |
+
</a>
|
| 85 |
) : (
|
| 86 |
<a href="#waitlist" className="block text-center px-6 py-3 rounded-xl font-semibold text-sm mk-coming-soon-btn">
|
| 87 |
Join Waitlist
|
|
@@ -209,7 +209,7 @@ export interface CatalogTemplate {
|
|
| 209 |
clone_count: number;
|
| 210 |
}
|
| 211 |
|
| 212 |
-
export const APP_URL = "
|
| 213 |
|
| 214 |
export async function getPlans(): Promise<CatalogPlan[]> {
|
| 215 |
try {
|
|
|
|
| 209 |
clone_count: number;
|
| 210 |
}
|
| 211 |
|
| 212 |
+
export const APP_URL = "/app";
|
| 213 |
|
| 214 |
export async function getPlans(): Promise<CatalogPlan[]> {
|
| 215 |
try {
|
|
@@ -512,8 +512,9 @@ def get_google_redirect_uri() -> str:
|
|
| 512 |
|
| 513 |
# Use the frontend base URL so the redirect_uri matches the frontend callback page,
|
| 514 |
# not the backend server (which runs on a different port in local dev).
|
|
|
|
| 515 |
base = (settings.APP_BASE_URL or settings.FRONTEND_URL).rstrip("/")
|
| 516 |
-
redirect_uri = f"{base}/auth/google/callback"
|
| 517 |
logger.info(f"Google OAuth: Generated redirect_uri={redirect_uri}")
|
| 518 |
return redirect_uri
|
| 519 |
|
|
|
|
| 512 |
|
| 513 |
# Use the frontend base URL so the redirect_uri matches the frontend callback page,
|
| 514 |
# not the backend server (which runs on a different port in local dev).
|
| 515 |
+
# The Frontend app is mounted at /app (basePath), so the callback route is /app/auth/google/callback.
|
| 516 |
base = (settings.APP_BASE_URL or settings.FRONTEND_URL).rstrip("/")
|
| 517 |
+
redirect_uri = f"{base}/app/auth/google/callback"
|
| 518 |
logger.info(f"Google OAuth: Generated redirect_uri={redirect_uri}")
|
| 519 |
return redirect_uri
|
| 520 |
|
|
@@ -42,8 +42,8 @@ http {
|
|
| 42 |
location = /signup {
|
| 43 |
return 301 /app/signup;
|
| 44 |
}
|
| 45 |
-
location
|
| 46 |
-
return 301 /app
|
| 47 |
}
|
| 48 |
|
| 49 |
# Backend API
|
|
|
|
| 42 |
location = /signup {
|
| 43 |
return 301 /app/signup;
|
| 44 |
}
|
| 45 |
+
location ~ ^/admin(/.*)?$ {
|
| 46 |
+
return 301 /app$request_uri;
|
| 47 |
}
|
| 48 |
|
| 49 |
# Backend API
|