mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-07 16:30:52 +08:00
save this
[ci skip]
This commit is contained in:
@@ -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/).*)",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
79
src/pages/api/auth/[...nextauth].js
Normal file
79
src/pages/api/auth/[...nextauth].js
Normal 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 don’t 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
43
src/pages/auth/signin.jsx
Normal 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 },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user