Skip to content

Commit fa38e8c

Browse files
use site host and resolve verify chain
1 parent eade670 commit fa38e8c

11 files changed

Lines changed: 93 additions & 23 deletions

File tree

src/app/about/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type { Metadata } from "next";
22
import Link from "next/link";
3+
import { siteHost } from "@/lib/site";
4+
5+
const site = siteHost();
36

47
export const metadata: Metadata = {
5-
title: "About — worldhello.io",
8+
title: site ? `About — ${site}` : "About",
69
description: "Why worldhello exists, and the privacy-preserving techniques behind it.",
710
};
811

src/app/api/auth/link/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { newLinkCode } from "@/lib/codes";
66
import { logApiError, logApiReject } from "@/lib/api-log";
77
import { LINK_TTL_MS } from "@/lib/token";
88
import { admitLinkCreate } from "@/lib/ratelimit";
9+
import { siteBaseUrl } from "@/lib/site";
910

1011
export const runtime = "nodejs";
1112

@@ -54,7 +55,7 @@ export async function POST(req: NextRequest) {
5455
}
5556
}
5657

57-
const base = process.env.NEXT_PUBLIC_BASE_URL || req.nextUrl.origin;
58+
const base = siteBaseUrl(req.nextUrl.origin);
5859
const url = `${base}/?link=${code}`;
5960

6061
return NextResponse.json({

src/app/api/auth/magic/route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { sendMail } from "@/lib/mailer";
88
import { logApiReject } from "@/lib/api-log";
99
import { admitMagic } from "@/lib/ratelimit";
1010
import { clientIp } from "@/lib/geo";
11+
import { siteBaseUrl, siteHost } from "@/lib/site";
1112

1213
export const runtime = "nodejs";
1314

@@ -41,13 +42,14 @@ export async function POST(req: NextRequest) {
4142
expiresAt,
4243
});
4344

44-
const base = process.env.NEXT_PUBLIC_BASE_URL || req.nextUrl.origin;
45+
const base = siteBaseUrl(req.nextUrl.origin);
46+
const host = siteHost(req.nextUrl.origin);
4547
const link = `${base}/api/auth/verify?token=${encodeURIComponent(token)}`;
4648

4749
await sendMail({
4850
to: email,
49-
subject: "Verify your worldhello.io account",
50-
html: `<p>Click below to verify your account on worldhello.io:</p><p><a href="${link}">Verify my account</a></p><p>Confirm on the same device that requested this email. This proves the network belongs to you — it is not for linking other devices. Expires in 30 minutes. If you didn't request this, ignore it.</p>`,
51+
subject: `Verify your ${host} account`,
52+
html: `<p>Click below to verify your account on ${host}:</p><p><a href="${link}">Verify my account</a></p><p>Confirm on the same device that requested this email. Linked devices stay connected. Expires in 30 minutes. If you didn't request this, ignore it.</p>`,
5153
}).catch((err) => {
5254
console.error("[auth/magic] send failed:", err);
5355
});

src/app/api/auth/verify/route.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { verifyNonce } from "@/lib/token";
33
import { logApiReject } from "@/lib/api-log";
44
import { executeMagicVerify, magicTokenStatus } from "@/lib/magic-verify";
55
import type { VerifyFailureReason } from "@/lib/verify-feedback";
6+
import { siteHost } from "@/lib/site";
67

78
export const runtime = "nodejs";
89

@@ -27,12 +28,13 @@ export async function GET(req: NextRequest) {
2728
}
2829

2930
const safeToken = escapeHtml(token!);
31+
const host = escapeHtml(siteHost(req.nextUrl.origin));
3032
const html = `<!DOCTYPE html>
3133
<html lang="en">
3234
<head>
3335
<meta charset="utf-8" />
3436
<meta name="viewport" content="width=device-width, initial-scale=1" />
35-
<title>Verify email — worldhello.io</title>
37+
<title>Verify email — ${host}</title>
3638
<style>
3739
body { font-family: system-ui, sans-serif; max-width: 28rem; margin: 4rem auto; padding: 0 1rem; color: #e8e8e8; background: #0a0a0f; }
3840
h1 { font-size: 1.25rem; font-weight: 600; }
@@ -43,7 +45,7 @@ export async function GET(req: NextRequest) {
4345
</head>
4446
<body>
4547
<h1>Verify your account</h1>
46-
<p>Confirm on <strong>the same device</strong> that requested this email. This verifies your account — it does not link other devices.</p>
48+
<p>Confirm on <strong>the same device</strong> that requested this email. Your linked devices stay connected — this makes this device the verified primary for the group.</p>
4749
<form method="POST" action="/api/auth/verify">
4850
<input type="hidden" name="token" value="${safeToken}" />
4951
<button type="submit">Verify my account</button>

src/app/layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { Geist, Geist_Mono } from "next/font/google";
33
import { Analytics } from "@vercel/analytics/next";
44
import "./globals.css";
55
import Providers from "@/components/Providers";
6+
import { siteHost } from "@/lib/site";
7+
8+
const site = siteHost();
69

710
const geistSans = Geist({
811
variable: "--font-geist-sans",
@@ -15,11 +18,11 @@ const geistMono = Geist_Mono({
1518
});
1619

1720
export const metadata: Metadata = {
18-
title: "worldhello.io — watch your link reach the world",
21+
title: site ? `${site} — watch your link reach the world` : "watch your link reach the world",
1922
description:
2023
"Share one link and watch it travel the globe in real time. Anonymous, no sign-up.",
2124
openGraph: {
22-
title: "worldhello.io",
25+
title: site || "watch your link reach the world",
2326
description: "Share one link, watch your reach spread across the world.",
2427
type: "website",
2528
},

src/components/sections/Footer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Link from "next/link";
2+
import { siteHost } from "@/lib/site";
23

34
export default function Footer() {
5+
const site = siteHost();
46
return (
57
<footer className="border-t border-white/5 px-6 py-10">
68
<div className="mx-auto w-full max-w-6xl">
@@ -11,7 +13,7 @@ export default function Footer() {
1113
</p>
1214
<div className="mt-8 flex flex-col items-center justify-between gap-4 text-sm text-muted sm:flex-row">
1315
<div className="flex items-center gap-4">
14-
<span>worldhello.io</span>
16+
{site ? <span>{site}</span> : null}
1517
<Link href="/about" className="hover:text-fg">
1618
About &amp; privacy
1719
</Link>

src/components/sections/Header.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { siteHost } from "@/lib/site";
4+
35
/** The worldhello globe mark — wireframe sphere with blue (incoming) + purple
46
* (outgoing) arcs and a you-node. Matches app/icon.svg. */
57
function LogoMark() {
@@ -20,13 +22,13 @@ function LogoMark() {
2022
}
2123

2224
export default function Header({ fpLabel }: { fpLabel: string }) {
25+
const site = siteHost(typeof window !== "undefined" ? window.location.origin : "");
26+
2327
return (
2428
<header className="fixed inset-x-0 top-0 z-50 flex items-center justify-between px-6 py-4 backdrop-blur-sm">
2529
<a href="#" className="flex items-center gap-2.5">
2630
<LogoMark />
27-
<span className="text-base font-semibold">
28-
worldhello<span className="text-muted">.io</span>
29-
</span>
31+
{site ? <span className="text-base font-semibold">{site}</span> : null}
3032
</a>
3133
<div className="flex items-center gap-3">
3234
<span className="chip hidden items-center gap-2 !rounded-full !px-3 !py-1.5 font-mono text-xs text-muted sm:flex">

src/lib/magic-verify.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ export async function magicTokenStatus(nonce: string): Promise<MagicTokenStatus>
1919
}
2020

2121
/**
22-
* Consume a magic-link token and link the requesting device to its email account.
23-
* Single SQL round-trip: delete token → upsert account → update node.
22+
* Consume a magic-link token and attach the device group to its email account.
23+
* Single SQL round-trip: delete token → upsert email account → move verifying
24+
* device plus any already-linked siblings onto that account.
2425
* Caller must confirm `deviceLocalId` matches the cookie on the requesting device.
2526
*/
2627
export async function executeMagicVerify(
@@ -46,6 +47,11 @@ export async function executeMagicVerify(
4647
AND local_id = ${deviceLocalId}
4748
RETURNING email_hash, local_id
4849
),
50+
pre AS (
51+
SELECT n.account_id AS old_account_id
52+
FROM nodes n
53+
WHERE n.local_id = (SELECT local_id FROM consumed LIMIT 1)
54+
),
4955
acc AS (
5056
INSERT INTO accounts (email_hash)
5157
SELECT email_hash FROM consumed
@@ -56,11 +62,17 @@ export async function executeMagicVerify(
5662
UPDATE nodes n
5763
SET
5864
account_id = (SELECT id FROM acc LIMIT 1),
59-
verified = true,
65+
verified = (n.local_id = (SELECT local_id FROM consumed LIMIT 1)),
6066
ephemeral = false
61-
WHERE n.local_id = (SELECT local_id FROM consumed LIMIT 1)
62-
AND EXISTS (SELECT 1 FROM consumed)
67+
WHERE EXISTS (SELECT 1 FROM consumed)
6368
AND EXISTS (SELECT 1 FROM acc)
69+
AND (
70+
n.local_id = (SELECT local_id FROM consumed LIMIT 1)
71+
OR (
72+
(SELECT old_account_id FROM pre LIMIT 1) IS NOT NULL
73+
AND n.account_id = (SELECT old_account_id FROM pre LIMIT 1)
74+
)
75+
)
6476
RETURNING n.id
6577
)
6678
SELECT

src/lib/share.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
/** Share intents per platform. DESIGN §8. */
22

3+
import { siteBaseUrl, siteHost } from "@/lib/site";
4+
5+
function clientOrigin(): string {
6+
return typeof window !== "undefined" ? window.location.origin : "";
7+
}
8+
39
export function shareUrl(code: string): string {
4-
const base = process.env.NEXT_PUBLIC_BASE_URL || (typeof window !== "undefined" ? window.location.origin : "");
5-
return `${base}/${code}`;
10+
return `${siteBaseUrl(clientOrigin())}/${code}`;
611
}
712

8-
const TEXT = "I just joined the chain on worldhello.io — see how far our connection travels 🌍";
13+
export function shareBlurb(): string {
14+
const host = siteHost(clientOrigin());
15+
return `I just joined the chain on ${host} — see how far our connection travels 🌍`;
16+
}
917

1018
export type Platform = "x" | "whatsapp" | "telegram" | "facebook" | "linkedin";
1119

1220
export function intentUrl(platform: Platform, url: string): string {
1321
const u = encodeURIComponent(url);
14-
const t = encodeURIComponent(TEXT);
22+
const t = encodeURIComponent(shareBlurb());
1523
switch (platform) {
1624
case "x":
1725
return `https://twitter.com/intent/tweet?text=${t}&url=${u}`;
@@ -29,7 +37,7 @@ export function intentUrl(platform: Platform, url: string): string {
2937
export async function nativeShare(url: string): Promise<boolean> {
3038
if (typeof navigator !== "undefined" && navigator.share) {
3139
try {
32-
await navigator.share({ title: "worldhello.io", text: TEXT, url });
40+
await navigator.share({ title: siteHost(clientOrigin()), text: shareBlurb(), url });
3341
return true;
3442
} catch {
3543
return false;

src/lib/site.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, it, expect, afterEach, vi } from "vitest";
2+
import { siteBaseUrl, siteHost } from "./site";
3+
4+
describe("site", () => {
5+
afterEach(() => {
6+
vi.unstubAllEnvs();
7+
});
8+
9+
it("reads base URL from env", () => {
10+
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "https://example.com");
11+
expect(siteBaseUrl()).toBe("https://example.com");
12+
expect(siteHost()).toBe("example.com");
13+
});
14+
15+
it("falls back when env is unset", () => {
16+
vi.stubEnv("NEXT_PUBLIC_BASE_URL", "");
17+
expect(siteBaseUrl("http://localhost:3000")).toBe("http://localhost:3000");
18+
expect(siteHost("http://localhost:3000")).toBe("localhost:3000");
19+
});
20+
});

0 commit comments

Comments
 (0)