save this

[ci skip]
This commit is contained in:
shamoon
2026-01-20 21:19:39 -08:00
parent 872a3600aa
commit 0660b91d94
8 changed files with 299 additions and 27 deletions

View File

@@ -1,7 +1,11 @@
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";
export function middleware(req) {
// Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
const authEnabled = process.env.HOMEPAGE_AUTH_ENABLED === "true";
const authSecret = process.env.NEXTAUTH_SECRET || process.env.HOMEPAGE_AUTH_SECRET;
export async function middleware(req) {
// Host validation (status quo)
const host = req.headers.get("host");
const port = process.env.PORT || 3000;
let allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
@@ -15,9 +19,23 @@ export function middleware(req) {
);
return NextResponse.json({ error: "Host validation failed. See logs for more details." }, { status: 400 });
}
if (authEnabled) {
const token = await getToken({ req, secret: authSecret });
if (!token) {
const signInUrl = new URL("/auth/signin", req.url);
signInUrl.searchParams.set("callbackUrl", req.url);
return NextResponse.redirect(signInUrl);
}
}
return NextResponse.next();
}
export const config = {
matcher: "/api/:path*",
// Protect all app and API routes; allow Next.js internals, public assets, auth pages, and NextAuth endpoints.
matcher: [
"/",
"/((?!_next/static|_next/image|favicon.ico|robots.txt|manifest.json|sitemap.xml|icons/|api/auth|auth/).*)",
],
};

View File

@@ -1,4 +1,5 @@
/* eslint-disable react/jsx-props-no-spreading */
import { SessionProvider } from "next-auth/react";
import { appWithTranslation } from "next-i18next";
import Head from "next/head";
import "styles/globals.css";
@@ -69,28 +70,30 @@ const tailwindSafelist = [
function MyApp({ Component, pageProps }) {
return (
<SWRConfig
value={{
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
}}
>
<Head>
{/* https://nextjs.org/docs/messages/no-document-viewport-meta */}
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Head>
<ColorProvider>
<ThemeProvider>
<SettingsProvider>
<TabProvider>
<Component {...pageProps} />
</TabProvider>
</SettingsProvider>
</ThemeProvider>
</ColorProvider>
</SWRConfig>
<SessionProvider session={pageProps.session}>
<SWRConfig
value={{
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
}}
>
<Head>
{/* https://nextjs.org/docs/messages/no-document-viewport-meta */}
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Head>
<ColorProvider>
<ThemeProvider>
<SettingsProvider>
<TabProvider>
<Component {...pageProps} />
</TabProvider>
</SettingsProvider>
</ThemeProvider>
</ColorProvider>
</SWRConfig>
</SessionProvider>
);
}

View File

@@ -0,0 +1,79 @@
import NextAuth from "next-auth";
const authEnabled = process.env.HOMEPAGE_AUTH_ENABLED === "true";
const issuer = process.env.HOMEPAGE_OIDC_ISSUER;
const clientId = process.env.HOMEPAGE_OIDC_CLIENT_ID;
const clientSecret = process.env.HOMEPAGE_OIDC_CLIENT_SECRET;
const homepageAuthSecret = process.env.HOMEPAGE_AUTH_SECRET;
const homepageExternalUrl = process.env.HOMEPAGE_EXTERNAL_URL;
// Map HOMEPAGE_* envs to what NextAuth expects so users dont need NEXTAUTH_* explicitly.
if (!process.env.NEXTAUTH_SECRET && homepageAuthSecret) {
process.env.NEXTAUTH_SECRET = homepageAuthSecret;
}
if (!process.env.NEXTAUTH_URL && homepageExternalUrl) {
process.env.NEXTAUTH_URL = homepageExternalUrl;
}
const defaultScope = process.env.HOMEPAGE_OIDC_SCOPE || "openid email profile";
const cleanedIssuer = issuer ? issuer.replace(/\/+$/, "") : issuer;
if (
authEnabled &&
(!issuer || !clientId || !clientSecret || !process.env.NEXTAUTH_SECRET || !process.env.NEXTAUTH_URL)
) {
throw new Error(
"OIDC auth is enabled but required settings are missing. Please set HOMEPAGE_OIDC_ISSUER, HOMEPAGE_OIDC_CLIENT_ID, HOMEPAGE_OIDC_CLIENT_SECRET, HOMEPAGE_AUTH_SECRET, and HOMEPAGE_EXTERNAL_URL.",
);
}
let providers = [];
if (authEnabled) {
providers = [
{
id: "homepage-oidc",
name: process.env.HOMEPAGE_OIDC_NAME || "Homepage OIDC",
type: "oauth",
idToken: true,
issuer: cleanedIssuer,
wellKnown: `${cleanedIssuer}/.well-known/openid-configuration`,
clientId,
clientSecret,
authorization: {
params: {
scope: defaultScope,
},
},
profile(profile) {
return {
id: profile.sub ?? profile.id ?? profile.user_id ?? profile.uid ?? profile.email,
name: profile.name ?? profile.preferred_username ?? profile.nickname ?? profile.email,
email: profile.email ?? null,
image: profile.picture ?? null,
};
},
},
];
}
export default NextAuth({
providers,
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
pages: {
signIn: "/auth/signin",
},
debug: true,
logger: {
error: (...args) => console.error("[nextauth][error]", ...args),
warn: (...args) => console.warn("[nextauth][warn]", ...args),
debug: (...args) => console.debug("[nextauth][debug]", ...args),
},
events: {
signIn: async (message) => console.debug("[nextauth][event][signIn]", message),
signOut: async (message) => console.debug("[nextauth][event][signOut]", message),
error: async (message) => console.error("[nextauth][event][error]", message),
},
});

43
src/pages/auth/signin.jsx Normal file
View File

@@ -0,0 +1,43 @@
import { getProviders, signIn } from "next-auth/react";
export default function SignIn({ providers, callbackUrl }) {
if (!providers || Object.keys(providers).length === 0) {
return (
<main className="flex h-screen items-center justify-center">
<div className="p-6 text-center">
<h1 className="text-xl font-semibold">Authentication not configured</h1>
<p className="mt-2 text-sm text-gray-600">OIDC is disabled or missing required environment variables.</p>
</div>
</main>
);
}
return (
<main className="flex h-screen items-center justify-center">
<div className="rounded-lg border border-gray-200 bg-white p-8 shadow-md">
<h1 className="text-xl font-semibold text-gray-900">Sign in</h1>
<p className="mt-2 text-sm text-gray-600">Continue with your identity provider.</p>
<div className="mt-4 space-y-3">
{Object.values(providers).map((provider) => (
<button
key={provider.id}
type="button"
onClick={() => signIn(provider.id, { callbackUrl: callbackUrl || "/" })}
className="w-full rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
>
Sign in with {provider.name}
</button>
))}
</div>
</div>
</main>
);
}
export async function getServerSideProps(context) {
const providers = await getProviders();
const callbackUrl = context?.query?.callbackUrl ?? "/";
return {
props: { providers, callbackUrl },
};
}