diff --git a/app/api/cron/autosync/route.ts b/app/api/cron/autosync/route.ts index f8d17a0..f744f03 100644 --- a/app/api/cron/autosync/route.ts +++ b/app/api/cron/autosync/route.ts @@ -9,7 +9,7 @@ export async function POST(req: Request) { try { const secret = req.headers.get("x-cron-secret"); - if (!env.cronSecret || !secret || secret !== env.cronSecret) { + if (!env.cronSharedSecret || !secret || secret !== env.cronSharedSecret) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/cron/health/route.ts b/app/api/cron/health/route.ts index 56a2c1d..e9c36f4 100644 --- a/app/api/cron/health/route.ts +++ b/app/api/cron/health/route.ts @@ -9,7 +9,7 @@ export async function POST(req: Request) { try { const secret = req.headers.get("x-cron-secret"); - if (!env.cronSecret || !secret || secret !== env.cronSecret) { + if (!env.cronSharedSecret || !secret || secret !== env.cronSharedSecret) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index beebb27..3629a4f 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -2,12 +2,18 @@ import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; import { NextResponse } from "next/server"; import { cookies } from "next/headers"; +import { env } from "@/lib/env"; import type { Database } from "@/lib/types"; export async function GET(request: Request) { const requestUrl = new URL(request.url); const code = requestUrl.searchParams.get("code"); - const next = requestUrl.searchParams.get("next") ?? "/dashboard"; + const nextParam = requestUrl.searchParams.get("next"); + const next = + nextParam && nextParam.startsWith("/") && !nextParam.startsWith("//") + ? nextParam + : "/dashboard"; + const redirectOrigin = env.appOrigin || requestUrl.origin; if (code) { const supabase = createRouteHandlerClient({ cookies }); @@ -15,18 +21,16 @@ export async function GET(request: Request) { if (error) { console.error(error); return NextResponse.redirect( - new URL(`/login?authError=1`, requestUrl.origin), + new URL(`/login?authError=1`, redirectOrigin), ); } const { data: { user }, } = await supabase.auth.getUser(); if (!user) { - return NextResponse.redirect( - new URL(`/login?noUser=1`, requestUrl.origin), - ); + return NextResponse.redirect(new URL(`/login?noUser=1`, redirectOrigin)); } } - return NextResponse.redirect(new URL(next, requestUrl.origin)); + return NextResponse.redirect(new URL(next, redirectOrigin)); } diff --git a/docs/deployment-production.md b/docs/deployment-production.md index 4cdca7a..181d13d 100644 --- a/docs/deployment-production.md +++ b/docs/deployment-production.md @@ -57,6 +57,34 @@ Before deploying, set production-safe values in `.env` (using `.env.example` as Keep secrets out of source control. +## Reverse proxy assumptions (Nginx Proxy Manager) + +This app is expected to run behind a reverse proxy in production. + +- TLS is terminated at the proxy. +- Proxy forwards standard host/protocol headers (`Host`, `X-Forwarded-Proto`, `X-Forwarded-Host`). +- Public origin is configured explicitly with `NEXT_PUBLIC_APP_URL`. + +To keep callback behavior stable behind proxy layers, auth redirect responses use the configured application origin from environment configuration rather than relying only on request host detection. + +## Callback URL expectations + +- `NEXT_PUBLIC_APP_URL` must be set to the public HTTPS URL used by users. +- Supabase auth redirect/callback configuration must allow: + - `/auth/callback` + +The callback route also validates the `next` parameter as an internal path to prevent unsafe external redirects. + +## Cron protection expectations + +Operational cron endpoints are protected with a shared secret. + +- Required header: `x-cron-secret` +- Server secret source: `CRON_SHARED_SECRET` +- Backward compatibility fallback: `CRON_SECRET` + +For new production setups, set `CRON_SHARED_SECRET` and use that value for all cron callers. + ## Jenkins pipeline flow The repository includes a declarative `Jenkinsfile` with the following stages: diff --git a/lib/env.ts b/lib/env.ts index b809dbe..1fb90ef 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -3,6 +3,18 @@ const requiredEnvs = [ "NEXT_PUBLIC_SUPABASE_ANON_KEY", ] as const; +const configuredAppUrl = process.env.NEXT_PUBLIC_APP_URL?.trim(); + +function getAppOrigin() { + if (!configuredAppUrl) return "http://localhost:3000"; + + try { + return new URL(configuredAppUrl).origin; + } catch { + return "http://localhost:3000"; + } +} + for (const key of requiredEnvs) { if (!process.env[key]) { // Keep as runtime warning for smoother local setup. @@ -12,9 +24,12 @@ for (const key of requiredEnvs) { } export const env = { - appUrl: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000", + appUrl: configuredAppUrl ?? "http://localhost:3000", + appOrigin: getAppOrigin(), jobSecret: process.env.JOB_SECRET ?? "", cronSecret: process.env.CRON_SECRET ?? "", + cronSharedSecret: + process.env.CRON_SHARED_SECRET ?? process.env.CRON_SECRET ?? "", encryptionKey: process.env.ENCRYPTION_KEY ?? "", pleskCredEncKey: process.env.PLESK_CRED_ENC_KEY ?? "", stripeSecretKey: process.env.STRIPE_SECRET_KEY ?? "",