diff --git a/.gitea/workflows/server-deploy.yml b/.gitea/workflows/server-deploy.yml index 350291b..64d1907 100644 --- a/.gitea/workflows/server-deploy.yml +++ b/.gitea/workflows/server-deploy.yml @@ -150,6 +150,7 @@ jobs: --build-arg NEXT_PUBLIC_ADMIN_ENV="${{ vars.ADMIN_ENV }}" \ --build-arg NEXT_PUBLIC_PADDLE_CLIENT_TOKEN="${{ vars.PADDLE_CLIENT_TOKEN }}" \ --build-arg NEXT_PUBLIC_PADDLE_ENV="${{ vars.PADDLE_ENV }}" \ + --build-arg NEXT_PUBLIC_SITE_URL="${{ vars.SITE_URL }}" \ -t "$IMAGE" \ -f adminsite/Dockerfile adminsite/ docker push "$IMAGE" diff --git a/adminsite/Dockerfile b/adminsite/Dockerfile index 1b38eee..b0acc44 100644 --- a/adminsite/Dockerfile +++ b/adminsite/Dockerfile @@ -26,6 +26,11 @@ ENV NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=$NEXT_PUBLIC_PADDLE_CLIENT_TOKEN ARG NEXT_PUBLIC_PADDLE_ENV=sandbox ENV NEXT_PUBLIC_PADDLE_ENV=$NEXT_PUBLIC_PADDLE_ENV +# Marketing site origin. Signup lives there (/start), not here; empty renders no +# link at all rather than one that 404s. +ARG NEXT_PUBLIC_SITE_URL= +ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL + RUN npm run build FROM node:26-alpine AS runner diff --git a/adminsite/app/layout.tsx b/adminsite/app/layout.tsx index 848c6f9..b195ee9 100644 --- a/adminsite/app/layout.tsx +++ b/adminsite/app/layout.tsx @@ -10,7 +10,7 @@ export const metadata: Metadata = { /* * The masthead deliberately does NOT live here. It belongs to the authenticated - * layouts, so /login, /signup, /verify and /accept-invite stop rendering a bar + * layouts, so /login, /verify and /accept-invite stop rendering a bar * whose navigation and account menu they cannot use. */ export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/adminsite/app/login/page.tsx b/adminsite/app/login/page.tsx index ed18e08..2ed74aa 100644 --- a/adminsite/app/login/page.tsx +++ b/adminsite/app/login/page.tsx @@ -2,12 +2,13 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; -import Link from "next/link"; import { API_BASE, ApiError, NotConnected, api } from "@/lib/api"; import { NotConnectedPanel } from "@/components/NotConnected"; import { Button } from "@/components/Button"; import { Field } from "@/components/Field"; +const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL ?? "").replace(/\/$/, ""); + export default function LoginPage() { const router = useRouter(); const [email, setEmail] = useState(""); @@ -91,14 +92,19 @@ export default function LoginPage() { -
+ {SITE_URL && ( + <> +
-

- No account?{" "} - - Create one for a self-hosted licence - -

+ {/* Signup lives on the marketing site's /start, not here. */} +

+ No account?{" "} + + Create one + +

+ + )}
); diff --git a/adminsite/app/signup/page.tsx b/adminsite/app/signup/page.tsx deleted file mode 100644 index 7761171..0000000 --- a/adminsite/app/signup/page.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ApiError, NotConnected, api } from "@/lib/api"; -import { Button } from "@/components/Button"; -import { Field } from "@/components/Field"; - -export default function SignupPage() { - const [form, setForm] = useState({ name: "", email: "", password: "", website: "" }); - const [state, setState] = useState<"idle" | "busy" | "sent">("idle"); - const [error, setError] = useState(null); - - async function submit(e: React.FormEvent) { - e.preventDefault(); - setState("busy"); - setError(null); - try { - await api.signup(form); - setState("sent"); - } catch (err) { - setState("idle"); - setError( - err instanceof NotConnected - ? "The licensing service is not reachable from this page." - : err instanceof ApiError - ? err.message - : "Could not create the account. Try again.", - ); - } - } - - return ( -
- {state === "sent" ? ( -
-

Check your email

-

- We sent a link to {form.email}. Open it to finish setting up your account — - it expires in 24 hours. Nothing is created until you do. -

-
- ) : ( - <> -

Create an account

-

- For self-hosted licences. If you run on our cloud, sign in with the same - details you use for your Vantage instance. -

-
- setForm({ ...form, name: e.target.value })} - /> - setForm({ ...form, email: e.target.value })} - /> - setForm({ ...form, password: e.target.value })} - error={error ?? undefined} - /> - {/* Honeypot: off-screen, unlabelled for humans, irresistible to bots. */} - setForm({ ...form, website: e.target.value })} - className="absolute left-[-9999px] h-0 w-0" - /> - - - - )} -
- ); -} diff --git a/adminsite/lib/api.ts b/adminsite/lib/api.ts index 0a89e31..dff5f18 100644 --- a/adminsite/lib/api.ts +++ b/adminsite/lib/api.ts @@ -296,8 +296,6 @@ export const api = { staffLogin: (email: string, password: string) => post("/auth/staff/login", { email, password }), logout: () => post<{ ok: boolean }>("/auth/logout"), - signup: (payload: { name: string; email: string; password: string; website?: string }) => - post<{ pending: boolean }>("/auth/signup", payload), verify: (token: string) => req<{ verified: boolean; needs_password?: boolean }>( `/auth/verify?token=${encodeURIComponent(token)}`, diff --git a/claude.md b/claude.md index 16a0d79..36a5f20 100644 --- a/claude.md +++ b/claude.md @@ -669,6 +669,7 @@ git push origin main # server + web deploy | `API_URL` | **not** a CI variable | `web` reads it at **runtime**, from the container environment — `next.config.ts` is evaluated when `server.js` boots in standalone mode, and the rewrites it feeds are server-side, never browser-side. Default `http://localhost:8080`; compose sets `http://server:8080`. `NEXT_PUBLIC_API_URL` is still honoured as a fallback for existing deployments. | | `SITE_API_URL` | Variable | **browser-reachable** sitesvc URL, baked into the `site` image. Required — if empty, both forms report "not connected" and submit nowhere. Must also be in sitesvc's `SITE_ORIGIN`. | | `SITE_CONTACT_EMAIL` | Variable | optional; address shown when a form is misconfigured | +| `SITE_URL` | Variable | browser URL of the marketing site, baked into `adminsite` so `/login` can point at `/start`. **Signup has no page in `adminsite` at all** — one signup form, on `site/`. Empty renders no link rather than one that 404s. | | `ADMIN_API_URL` | Variable | **browser-reachable** admin URL, baked into **both** the `adminsite` and `site` images — `site/start` posts account signups straight to admin. Same footgun as `SITE_API_URL`: wrong here and every request fails at runtime with the not-connected panel. | | `ADMIN_ENV` | Variable | `production` or `sandbox`; drives the persistent environment badge. Anything but `sandbox` reads as production. | | `HQ_URL` | Variable | optional; browser URL of the HQ portal, baked into `web` so an `hq`-sourced member links to where they are managed. Empty on self-hosted, which renders a plain label instead. |