diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index b262ab7..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -/opt/config/CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e69de29 diff --git a/app/api/verify-code/route.ts b/app/api/verify-code/route.ts new file mode 100644 index 0000000..ccea410 --- /dev/null +++ b/app/api/verify-code/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server" + +const ACCESS_CODE = process.env.ACCESS_CODE || "42069" + +export async function POST(request: Request) { + const { code } = await request.json() + + if (code !== ACCESS_CODE) { + return NextResponse.json({ error: "Invalid code" }, { status: 401 }) + } + + const response = NextResponse.json({ success: true }) + response.cookies.set("jefflix-access", "granted", { + httpOnly: true, + secure: true, + sameSite: "lax", + maxAge: 60 * 60 * 24 * 365, // 1 year + path: "/", + }) + + return response +} diff --git a/app/gate/page.tsx b/app/gate/page.tsx new file mode 100644 index 0000000..38f4c67 --- /dev/null +++ b/app/gate/page.tsx @@ -0,0 +1,67 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { JefflixLogo } from "@/components/jefflix-logo" + +export default function GatePage() { + const [code, setCode] = useState("") + const [error, setError] = useState("") + const [loading, setLoading] = useState(false) + const router = useRouter() + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError("") + setLoading(true) + + const res = await fetch("/api/verify-code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code }), + }) + + if (res.ok) { + router.push("/") + router.refresh() + } else { + setError("Wrong code. Try again.") + setLoading(false) + } + } + + return ( +
+
+
+
+ +

+ Enter the access code to continue +

+
+ setCode(e.target.value)} + placeholder="Access code" + autoFocus + className="w-full text-center text-2xl tracking-widest px-4 py-3 rounded-lg border-2 border-border bg-card focus:outline-none focus:ring-2 focus:ring-ring" + /> + {error && ( +

{error}

+ )} + +
+
+
+
+ ) +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..ccd9ba7 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server" +import type { NextRequest } from "next/server" + +export function middleware(request: NextRequest) { + const hasAccess = request.cookies.get("jefflix-access")?.value === "granted" + + if (!hasAccess) { + return NextResponse.redirect(new URL("/gate", request.url)) + } + + return NextResponse.next() +} + +export const config = { + matcher: [ + /* + * Match all paths except: + * - /gate (the code entry page) + * - /api/verify-code (the verification endpoint) + * - /_next (Next.js internals) + * - /favicon.ico, /icon*, /apple-icon*, /og-image* (static assets) + */ + "/((?!gate|api/verify-code|_next|favicon\\.ico|icon|apple-icon|og-image).*)", + ], +} diff --git a/vpn-setup/README.md b/vpn-setup/README.md deleted file mode 100644 index bcbbd9b..0000000 --- a/vpn-setup/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Jefflix VPN Setup — Headscale + Tailscale - -Protects all `*.jefflix.lol` services behind the existing Headscale VPN at `vpn.jeffemmett.com`. - -## How It Works - -``` -Before (public): - Browser → Cloudflare → Tunnel → Traefik → Jellyfin/etc - -After (VPN-only): - Browser → Tailscale (WireGuard) → Traefik → Jellyfin/etc - (Only works if connected to the tailnet) -``` - -Traefik still routes by Host header — the only change is how traffic reaches it. - -## Quick Start - -SSH into the server and follow the phases in order: - -```bash -ssh netcup -``` - -Then run `setup.sh` (or follow the manual steps below). - -## Files - -| File | Purpose | -|------|---------| -| `setup.sh` | Full setup script (run on Netcup) | -| `coredns/Corefile` | CoreDNS config — resolves *.jefflix.lol to Tailscale IP | -| `coredns/docker-compose.yml` | CoreDNS container definition | -| `headscale-config-patch.yaml` | Split DNS addition for Headscale config | -| `cloudflared-config-clean.yml` | Cloudflare tunnel config with jefflix entries removed | -| `rollback.sh` | Emergency rollback script | diff --git a/vpn-setup/cutover.sh b/vpn-setup/cutover.sh deleted file mode 100755 index 35c44ca..0000000 --- a/vpn-setup/cutover.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ============================================================ -# Jefflix VPN Cutover Script -# Removes public access to *.jefflix.lol -# Run ONLY after setup.sh and testing VPN access works -# ============================================================ - -echo "========================================" -echo " Jefflix Public Access Cutover" -echo "========================================" -echo "" - -# Pre-flight check: verify VPN access works -TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "") -if [ -z "$TAILSCALE_IP" ]; then - echo "ERROR: Tailscale not running on this server. Run setup.sh first." - exit 1 -fi - -echo "Server Tailscale IP: $TAILSCALE_IP" -echo "" - -# Check DNS works -DIG_RESULT=$(dig +short @${TAILSCALE_IP} movies.jefflix.lol 2>/dev/null || echo "FAILED") -if [ "$DIG_RESULT" != "$TAILSCALE_IP" ]; then - echo "ERROR: CoreDNS not resolving correctly. Got: $DIG_RESULT (expected $TAILSCALE_IP)" - echo "Fix CoreDNS before proceeding." - exit 1 -fi -echo "✓ CoreDNS resolving correctly" - -echo "" -echo "This will REMOVE public access to all *.jefflix.lol services." -echo "Users will need Tailscale connected to vpn.jeffemmett.com to access Jefflix." -echo "" -read -p "Continue? [y/N] " -n 1 -r -echo "" - -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Aborted." - exit 0 -fi - -# --- Remove jefflix entries from Cloudflare tunnel --- -echo "" -echo "[Cutover] Removing *.jefflix.lol from Cloudflare tunnel..." - -TUNNEL_CONFIG="/root/cloudflared/config.yml" - -# Backup current config (timestamped) -cp "$TUNNEL_CONFIG" "${TUNNEL_CONFIG}.pre-cutover-$(date +%Y%m%d-%H%M%S)" - -# Remove all jefflix.lol hostname entries (hostname + service lines) -# This removes the "- hostname: *.jefflix.lol" and its " service:" line -python3 -c " -import yaml, sys - -with open('$TUNNEL_CONFIG', 'r') as f: - config = yaml.safe_load(f) - -original_count = len(config.get('ingress', [])) - -# Filter out jefflix.lol entries -config['ingress'] = [ - entry for entry in config.get('ingress', []) - if not (isinstance(entry.get('hostname', ''), str) and 'jefflix.lol' in entry.get('hostname', '')) -] - -removed = original_count - len(config['ingress']) - -with open('$TUNNEL_CONFIG', 'w') as f: - yaml.dump(config, f, default_flow_style=False, sort_keys=False) - -print(f' Removed {removed} jefflix.lol entries from tunnel config') -" - -# Restart cloudflared -echo " Restarting cloudflared..." -docker restart cloudflared -echo " ✓ Cloudflared restarted" - -# Wait and verify -sleep 5 -echo "" -echo "========================================" -echo " Cutover Complete" -echo "========================================" -echo "" -echo "Public access to *.jefflix.lol is now REMOVED." -echo "" -echo "Verify from a non-VPN device:" -echo " curl -I https://movies.jefflix.lol (should fail/404)" -echo "" -echo "Verify from a VPN device:" -echo " curl http://movies.jefflix.lol (should work)" -echo "" -echo "To ROLLBACK: run rollback.sh" diff --git a/vpn-setup/onboard-user.sh b/vpn-setup/onboard-user.sh deleted file mode 100755 index bfcf026..0000000 --- a/vpn-setup/onboard-user.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ============================================================ -# Generate a Jefflix VPN invite for a new user -# Usage: ./onboard-user.sh [username] -# ============================================================ - -USERNAME="${1:-}" - -if [ -z "$USERNAME" ]; then - echo "Usage: ./onboard-user.sh " - echo " Creates a pre-auth key for the user and prints setup instructions." - exit 1 -fi - -# Generate a single-use pre-auth key (7 day expiry) -echo "Generating pre-auth key for user: $USERNAME" -PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create \ - --user jefflix \ - --reusable=false \ - --expiration 168h 2>&1 | tail -1) - -echo "" -echo "============================================================" -echo " Send the following to $USERNAME:" -echo "============================================================" -echo "" -cat </dev/null || true -echo " ✓ Backups created" - -# --- Phase 1b: Create Headscale users --- -echo "" -echo "[Phase 1b] Creating Headscale users..." -docker exec headscale headscale users create server 2>/dev/null || echo " (user 'server' already exists)" -docker exec headscale headscale users create jefflix 2>/dev/null || echo " (user 'jefflix' already exists)" -echo " ✓ Users ready" - -# --- Phase 2: Install Tailscale --- -echo "" -echo "[Phase 2] Installing Tailscale..." -if command -v tailscale &>/dev/null; then - echo " Tailscale already installed: $(tailscale version)" -else - curl -fsSL https://tailscale.com/install.sh | sh - echo " ✓ Tailscale installed" -fi - -# Generate pre-auth key and join tailnet -echo "" -echo " Generating pre-auth key..." -PREAUTH_KEY=$(docker exec headscale headscale preauthkeys create --user server --reusable=false --expiration 1h 2>&1 | tail -1) -echo " Pre-auth key: $PREAUTH_KEY" - -echo "" -echo " Joining tailnet..." -tailscale up \ - --login-server=https://vpn.jeffemmett.com \ - --authkey="$PREAUTH_KEY" \ - --hostname=netcup-rs8000 \ - --accept-dns=false - -# Get the Tailscale IP -TAILSCALE_IP=$(tailscale ip -4) -echo " ✓ Joined tailnet with IP: $TAILSCALE_IP" - -# --- Phase 3: Deploy CoreDNS --- -echo "" -echo "[Phase 3] Deploying CoreDNS for *.jefflix.lol..." -mkdir -p /opt/apps/jefflix-dns - -# Write Corefile with the actual Tailscale IP -cat > /opt/apps/jefflix-dns/Corefile < /opt/apps/jefflix-dns/docker-compose.yml </dev/null || echo "FAILED") -if [ "$DIG_RESULT" = "$TAILSCALE_IP" ]; then - echo " ✓ DNS test passed: movies.jefflix.lol -> $TAILSCALE_IP" -else - echo " ⚠ DNS test returned: $DIG_RESULT (expected $TAILSCALE_IP)" - echo " Check CoreDNS logs: docker logs jefflix-dns" -fi - -# --- Phase 4: Configure Headscale split DNS --- -echo "" -echo "[Phase 4] Configuring Headscale split DNS..." - -HEADSCALE_CONFIG="/opt/apps/headscale-deploy/config/config.yaml" - -# Check if split DNS is already configured -if grep -q "jefflix.lol" "$HEADSCALE_CONFIG"; then - echo " Split DNS for jefflix.lol already in config, skipping" -else - # Add split DNS section to the nameservers block - # This uses sed to add the split DNS config after the global nameservers - sed -i '/nameservers:/,/^[^ ]/ { - /global:/,/^ [^ ]/ { - /- 1.0.0.1/a\ split:\n jefflix.lol:\n - '"${TAILSCALE_IP}"' - } - }' "$HEADSCALE_CONFIG" - echo " ✓ Split DNS added to Headscale config" -fi - -# Restart Headscale -cd /opt/apps/headscale-deploy && docker compose restart headscale -echo " ✓ Headscale restarted" -sleep 3 - -# Verify Headscale is healthy -docker exec headscale headscale nodes list >/dev/null 2>&1 && echo " ✓ Headscale healthy" || echo " ⚠ Headscale may need attention" - -echo "" -echo "========================================" -echo " Setup Complete (Phases 1-4)" -echo "========================================" -echo "" -echo "Server Tailscale IP: $TAILSCALE_IP" -echo "CoreDNS: running on $TAILSCALE_IP:53" -echo "Split DNS: jefflix.lol -> $TAILSCALE_IP" -echo "" -echo "NEXT STEPS:" -echo " 1. Connect YOUR device to the tailnet and test:" -echo " tailscale up --login-server=https://vpn.jeffemmett.com" -echo " dig movies.jefflix.lol (should return $TAILSCALE_IP)" -echo " curl http://movies.jefflix.lol (should return Jellyfin)" -echo "" -echo " 2. Once confirmed working, run cutover.sh to remove public access" -echo " 3. Then onboard users with pre-auth keys" -echo ""