diff --git a/Cargo.lock b/Cargo.lock index 0bda9fe..0a5b1f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2015,8 +2015,8 @@ dependencies = [ [[package]] name = "k-ap" -version = "0.1.7" -source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git?tag=v0.1.7#699258f830922830df956db8e5dea739ee1642aa" +version = "0.1.8" +source = "git+https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git?tag=v0.1.8#1949fce620a4d8f6ae9aa88412f5dbf4b7b0f089" dependencies = [ "activitypub_federation", "anyhow", @@ -4566,7 +4566,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/compose.prod.yml b/compose.prod.yml index 5036c02..eabe97b 100644 --- a/compose.prod.yml +++ b/compose.prod.yml @@ -47,11 +47,18 @@ services: labels: - "traefik.enable=true" - "traefik.docker.network=traefik" + # Original API subdomain — keep for backwards compat and direct API access - "traefik.http.routers.thoughts-api.rule=Host(`api.thoughts.gabrielkaszewski.dev`)" - "traefik.http.routers.thoughts-api.entrypoints=web,websecure" - "traefik.http.routers.thoughts-api.tls.certresolver=letsencrypt" - "traefik.http.routers.thoughts-api.service=thoughts-api" - "traefik.http.services.thoughts-api.loadbalancer.server.port=8000" + # Federation routes on the main domain — higher priority than the frontend catch-all + - "traefik.http.routers.thoughts-federation.rule=Host(`thoughts.gabrielkaszewski.dev`) && (PathPrefix(`/.well-known`) || PathPrefix(`/nodeinfo`) || Path(`/inbox`) || (Method(`POST`) && PathPrefix(`/users/`)))" + - "traefik.http.routers.thoughts-federation.entrypoints=web,websecure" + - "traefik.http.routers.thoughts-federation.tls.certresolver=letsencrypt" + - "traefik.http.routers.thoughts-federation.service=thoughts-api" + - "traefik.http.routers.thoughts-federation.priority=1000" worker: container_name: thoughts-worker @@ -77,6 +84,7 @@ services: environment: NEXT_PUBLIC_SERVER_SIDE_API_URL: http://api:8000 NEXT_PUBLIC_API_URL: https://api.thoughts.gabrielkaszewski.dev + NEXT_PUBLIC_FEDIVERSE_DOMAIN: thoughts.gabrielkaszewski.dev PORT: 3000 HOSTNAME: 0.0.0.0 depends_on: diff --git a/crates/adapters/activitypub/Cargo.toml b/crates/adapters/activitypub/Cargo.toml index f6c878b..1bc41df 100644 --- a/crates/adapters/activitypub/Cargo.toml +++ b/crates/adapters/activitypub/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.7" } +k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.8" } domain = { workspace = true } url = { workspace = true } serde = { workspace = true } diff --git a/crates/adapters/postgres-federation/Cargo.toml b/crates/adapters/postgres-federation/Cargo.toml index 818e02e..ca8b7b5 100644 --- a/crates/adapters/postgres-federation/Cargo.toml +++ b/crates/adapters/postgres-federation/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.7" } +k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.8" } sqlx = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } diff --git a/crates/bootstrap/Cargo.toml b/crates/bootstrap/Cargo.toml index e905d8e..1bcd2ab 100644 --- a/crates/bootstrap/Cargo.toml +++ b/crates/bootstrap/Cargo.toml @@ -14,7 +14,7 @@ postgres = { workspace = true } postgres-search = { workspace = true } postgres-federation = { workspace = true } activitypub = { workspace = true } -k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.7" } +k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.8" } nats = { workspace = true } event-transport = { workspace = true } auth = { workspace = true } diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 6302e06..6125db6 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -13,7 +13,7 @@ application = { workspace = true } nats = { workspace = true } event-transport = { workspace = true } event-payload = { workspace = true } -k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.7" } +k-ap = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-ap.git", tag = "v0.1.8" } activitypub = { workspace = true } postgres = { workspace = true } postgres-federation = { workspace = true } diff --git a/thoughts-frontend/app/users/[username]/page.tsx b/thoughts-frontend/app/users/[username]/page.tsx index 29f1ed2..2a91dd0 100644 --- a/thoughts-frontend/app/users/[username]/page.tsx +++ b/thoughts-frontend/app/users/[username]/page.tsx @@ -126,11 +126,13 @@ export default async function ProfilePage({ params }: ProfilePageProps) { const followingCount = localFollowingCount + remoteFollowingCount; const isFollowing = user.isFollowedByViewer; - const apiDomain = process.env.NEXT_PUBLIC_API_URL - ? new URL(process.env.NEXT_PUBLIC_API_URL).hostname - : null; + const fediverseDomain = + process.env.NEXT_PUBLIC_FEDIVERSE_DOMAIN ?? + (process.env.NEXT_PUBLIC_API_URL + ? new URL(process.env.NEXT_PUBLIC_API_URL).hostname + : null); const fediverseHandle = - user.local && apiDomain ? `@${user.username}@${apiDomain}` : null; + user.local && fediverseDomain ? `@${user.username}@${fediverseDomain}` : null; return (
diff --git a/thoughts-frontend/middleware.ts b/thoughts-frontend/middleware.ts index c6d0186..d2dedac 100644 --- a/thoughts-frontend/middleware.ts +++ b/thoughts-frontend/middleware.ts @@ -1,16 +1,68 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -export function middleware(request: NextRequest) { - const parts = request.nextUrl.pathname.split("/"); +const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - // /users/@user@instance or /users/%40user%40instance - if (parts.length === 3 && parts[1] === "users") { - const decoded = decodeURIComponent(parts[2]); - if (decoded.startsWith("@") && decoded.indexOf("@", 1) !== -1) { +function isApRequest(accept: string): boolean { + return ( + accept.includes("application/activity+json") || + accept.includes("application/ld+json") + ); +} + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + const parts = pathname.split("/"); + + if (parts.length >= 3 && parts[1] === "users") { + const segment = decodeURIComponent(parts[2]); + const accept = request.headers.get("accept") ?? ""; + + if (UUID_RE.test(segment)) { + const apiBase = + process.env.NEXT_PUBLIC_SERVER_SIDE_API_URL ?? "http://api:8000"; + + if (isApRequest(accept)) { + // AP GET request → proxy to backend (actor JSON, outbox, followers, following) + // Inbox POSTs are routed directly via Traefik to preserve the host header for signature verification + const forwardHeaders: Record = {}; + for (const [key, value] of request.headers.entries()) { + if (key.toLowerCase() !== "host") { + forwardHeaders[key] = value; + } + } + + const res = await fetch(`${apiBase}${pathname}`, { + headers: forwardHeaders, + }); + + // Buffer the body — streaming ReadableStream via NextResponse is unreliable in Edge runtime + const body = await res.text(); + return new NextResponse(body, { + status: res.status, + headers: { + "content-type": + res.headers.get("content-type") ?? "application/activity+json", + }, + }); + } + + // Browser request → redirect to the human-readable username URL + const res = await fetch(`${apiBase}/users/${segment}`); + if (res.ok) { + const user = await res.json(); + const url = request.nextUrl.clone(); + url.pathname = `/users/${user.username}`; + return NextResponse.redirect(url, 301); + } + } + + // Remote handle redirect: /users/@user@instance + if (segment.startsWith("@") && segment.indexOf("@", 1) !== -1) { const url = request.nextUrl.clone(); url.pathname = "/remote-actor"; - url.searchParams.set("handle", decoded); + url.searchParams.set("handle", segment); return NextResponse.rewrite(url); } }