Separate admin page
This commit is contained in:
parent
cf37ccc649
commit
0efe631963
|
|
@ -19,6 +19,9 @@ const (
|
|||
|
||||
func pageTitle(s string) string { return s + " · Szabadpálya" }
|
||||
|
||||
// adminEntryPath must match ADMIN_ENTRY_PATH in web/src/lib/admin-entry.ts.
|
||||
const adminEntryPath = "/admin-belepes"
|
||||
|
||||
// bootstrap is what gets inlined into the document so the first route does not
|
||||
// have to fetch what the server already loaded to build the meta tags.
|
||||
type bootstrap struct {
|
||||
|
|
@ -74,6 +77,22 @@ func (s *Server) document(c *gin.Context) {
|
|||
case strings.HasPrefix(path, "/kampanyok/"):
|
||||
s.campaignDocument(c, strings.TrimPrefix(path, "/kampanyok/"))
|
||||
|
||||
// The way in for an admin. It is the landing page — the client only adds a
|
||||
// sign-in link to the header — so it loads exactly what "/" loads. Not
|
||||
// linked from anywhere public, hence noindex; the actual gate is still the
|
||||
// magic link and the admin allowlist, never this path being hard to guess.
|
||||
case path == adminEntryPath:
|
||||
stats, err := s.store.PlatformStats(ctx)
|
||||
if err != nil {
|
||||
s.failDocument(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("X-Robots-Tag", "noindex")
|
||||
s.render(c, http.StatusOK, shell.Document{
|
||||
Title: siteTitle,
|
||||
Description: siteDesc,
|
||||
}, bootstrap{Route: "home", Data: gin.H{"stats": stats}})
|
||||
|
||||
case path == "/belepes":
|
||||
s.render(c, http.StatusOK, shell.Document{
|
||||
Title: pageTitle("Belépés"),
|
||||
|
|
|
|||
|
|
@ -472,6 +472,25 @@ check_404() {
|
|||
}
|
||||
step "unknown routes are 404" check_404
|
||||
|
||||
# The admin's way in is the landing page with a sign-in link. Nothing links to
|
||||
# it, so the only thing holding it up is the server case that answers it: every
|
||||
# unrecognised path renders a 404 document, and it would fall into that.
|
||||
check_admin_entry() {
|
||||
local out
|
||||
out=$(curl -s -o /dev/null -D - -w '%{http_code}' "http://localhost:${APP_PORT}/admin-belepes")
|
||||
case "$out" in
|
||||
*200) ;;
|
||||
*) echo "the admin entry page returned something other than 200"; return 1 ;;
|
||||
esac
|
||||
printf '%s' "$out" | grep -qi 'X-Robots-Tag: *noindex' \
|
||||
|| { echo "the admin entry page is missing its noindex header"; return 1; }
|
||||
# And it must not be advertised on the public landing page.
|
||||
curl -s -D - -o /dev/null "http://localhost:${APP_PORT}/" | grep -qi 'X-Robots-Tag' \
|
||||
&& { echo "the landing page should not be noindex"; return 1; }
|
||||
return 0
|
||||
}
|
||||
step "admin entry page is served and unindexed" check_admin_entry
|
||||
|
||||
# A draft campaign must be as invisible as one that does not exist.
|
||||
check_draft_hidden() {
|
||||
local code
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Link } from "react-router";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { Logo } from "./Logo";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import { useSession } from "@/lib/use-session";
|
||||
import { ADMIN_ENTRY_PATH } from "@/lib/admin-entry";
|
||||
|
||||
const nav = [
|
||||
{ href: "/kampanyok", label: "Projektek" },
|
||||
|
|
@ -11,6 +12,7 @@ const nav = [
|
|||
|
||||
export function SiteHeader() {
|
||||
const { isAdmin: admin } = useSession();
|
||||
const onAdminEntry = useLocation().pathname === ADMIN_ENTRY_PATH;
|
||||
return (
|
||||
<header className="sticky top-0 z-40 border-b border-line bg-bg/85 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-6xl items-center gap-7 px-5 py-3.5">
|
||||
|
|
@ -30,12 +32,18 @@ export function SiteHeader() {
|
|||
</nav>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<ThemeToggle />
|
||||
{/* Signing in does nothing for a visitor: a session only unlocks
|
||||
/api/admin/*, and backing a campaign is planned to need no
|
||||
account at all. So the link is offered to an admin, or on the
|
||||
admin entry page, and to nobody else. */}
|
||||
{(admin || onAdminEntry) && (
|
||||
<Link
|
||||
to={admin ? "/admin" : "/belepes"}
|
||||
className="hidden text-sm font-medium text-muted transition-colors hover:text-fg sm:block"
|
||||
>
|
||||
{admin ? "Admin" : "Belépés"}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* The unadvertised way in for an admin: the landing page with a sign-in link in
|
||||
* the header. Nothing public links to it.
|
||||
*
|
||||
* This is a convenience, not a control — the gate is the magic link and the
|
||||
* admin allowlist. Must match adminEntryPath in api/internal/httpapi/document.go,
|
||||
* which serves the document for it.
|
||||
*/
|
||||
export const ADMIN_ENTRY_PATH = "/admin-belepes";
|
||||
|
|
@ -3,6 +3,7 @@ import { ApiError, api, type AdminCampaign, type Owner } from "./lib/api";
|
|||
import { takeBootstrap } from "./lib/bootstrap";
|
||||
import type { Campaign, CampaignSummary } from "./lib/types";
|
||||
import type { PlatformStats } from "./lib/api";
|
||||
import { ADMIN_ENTRY_PATH } from "./lib/admin-entry";
|
||||
import { RootLayout } from "./routes/RootLayout";
|
||||
import { AdminLayout } from "./routes/admin/AdminLayout";
|
||||
import Home from "./routes/Home";
|
||||
|
|
@ -14,6 +15,9 @@ import AdminHome from "./routes/admin/AdminHome";
|
|||
import EditCampaign from "./routes/admin/EditCampaign";
|
||||
|
||||
/** The landing page shows the headline numbers, not the campaigns themselves. */
|
||||
const homeLoader = async (): Promise<HomeData> =>
|
||||
takeBootstrap<HomeData>("home") ?? { stats: await api.stats() };
|
||||
|
||||
export interface HomeData {
|
||||
stats: PlatformStats;
|
||||
}
|
||||
|
|
@ -26,12 +30,9 @@ export const router = createBrowserRouter([
|
|||
{
|
||||
element: <RootLayout />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <Home />,
|
||||
loader: async (): Promise<HomeData> =>
|
||||
takeBootstrap<HomeData>("home") ?? { stats: await api.stats() },
|
||||
},
|
||||
{ path: "/", element: <Home />, loader: homeLoader },
|
||||
// The same page. The header notices the path and offers a sign-in link.
|
||||
{ path: ADMIN_ENTRY_PATH, element: <Home />, loader: homeLoader },
|
||||
{
|
||||
path: "/kampanyok",
|
||||
element: <Campaigns />,
|
||||
|
|
|
|||
Loading…
Reference in New Issue