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

@@ -68,7 +68,10 @@ For configuration options, examples and more, [please check out the homepage doc
## Security Notice 🔒
Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system) and Homepage currently does not (and is not planned to) include any authentication layer itself. If Homepage is reachable from any untrusted network, it **must** sit behind a reverse proxy (and/or VPN) that enforces authentication, TLS, and strictly validates Host headers. The built-in host check in Homepage is a best-effort guard and should not be treated as security when exposed publicly.
Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system). To keep your information private, if Homepage is reachable from any untrusted network, it:
1. **must** sit behind a reverse proxy (and/or VPN) that enforces authentication, TLS, and strictly validates Host headers. OR
2. An optional built-in OIDC login flow is available (opt-in) to gate the app without a reverse proxy; it is a simple “authenticated or not” guard (no per-user roles or personalization).
## With Docker

View File

@@ -37,4 +37,18 @@ The value is a comma-separated (no spaces) list of allowed hosts (sometimes with
If you are seeing errors about host validation, check the homepage logs and ensure that the host exactly as output in the logs is in the `HOMEPAGE_ALLOWED_HOSTS` list.
This can be disabled by setting `HOMEPAGE_ALLOWED_HOSTS` to `*` but this is not recommended. Public deployments must rely on a reverse proxy (and/or VPN) that enforces authentication, TLS, and unexpected Host headers; the built-in host check is a best-effort guard for local setups and is not a substitute for edge protections.
This can be disabled by setting `HOMEPAGE_ALLOWED_HOSTS` to `*` but this is not recommended. Public deployments must rely on a reverse proxy (and/or VPN) that enforces authentication, TLS, and blocks direct-IP access and unexpected Host headers; the built-in host check is a best-effort guard for local setups and is not a substitute for edge protections.
### Built-in OIDC authentication (optional, opt-in)
Homepage now supports a minimal OIDC login flow (no per-user roles or personalization) so you can deploy without a reverse proxy handling auth. Enable it with:
- `HOMEPAGE_AUTH_ENABLED=true`
- `HOMEPAGE_OIDC_ISSUER` (OIDC issuer URL, e.g., `https://auth.example.com/realms/homepage`)
- `HOMEPAGE_OIDC_CLIENT_ID`
- `HOMEPAGE_OIDC_CLIENT_SECRET`
- `HOMEPAGE_AUTH_SECRET` (random string for signing/encrypting cookies)
- `HOMEPAGE_EXTERNAL_URL` (external URL to your Homepage instance; used for callbacks)
- Optional: `HOMEPAGE_OIDC_NAME` (display name), `HOMEPAGE_OIDC_SCOPE` (defaults to `openid email profile`)
All app pages and `/api` routes will require a signed-in session. Static assets remain public. Homepage still does not implement per-user dashboards or roles; authentication is a simple gate only.

View File

@@ -29,6 +29,7 @@
"memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2",
"next": "^15.5.11",
"next-auth": "^4.24.10",
"next-i18next": "^12.1.0",
"ping": "^0.4.4",
"pretty-bytes": "^7.1.0",

111
pnpm-lock.yaml generated
View File

@@ -53,6 +53,9 @@ importers:
next:
specifier: ^15.5.11
version: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-auth:
specifier: ^4.24.10
version: 4.24.13(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-i18next:
specifier: ^12.1.0
version: 12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -803,6 +806,9 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -1683,6 +1689,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
core-js@3.40.0:
resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==}
@@ -2551,6 +2561,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@4.15.9:
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
jose@5.10.0:
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
@@ -2723,6 +2736,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
luxon@3.6.1:
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
engines: {node: '>=12'}
@@ -2831,6 +2848,20 @@ packages:
net@1.0.2:
resolution: {integrity: sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==}
next-auth@4.24.13:
resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==}
peerDependencies:
'@auth/core': 0.34.3
next: ^12.2.5 || ^13 || ^14 || ^15 || ^16
nodemailer: ^7.0.7
react: ^17.0.2 || ^18 || ^19
react-dom: ^17.0.2 || ^18 || ^19
peerDependenciesMeta:
'@auth/core':
optional: true
nodemailer:
optional: true
next-i18next@12.1.0:
resolution: {integrity: sha512-rhos/PVULmZPdC0jpec2MDBQMXdGZ3+Mbh/tZfrDtjgnVN3ucdq7k8BlwsJNww6FnqC8AC31n6dSYuqVzYsGsw==}
engines: {node: '>=12'}
@@ -2878,10 +2909,17 @@ packages:
oauth4webapi@3.3.0:
resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==}
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@2.2.0:
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
engines: {node: '>= 6'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -2910,12 +2948,19 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
oidc-token-hash@5.2.0:
resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==}
engines: {node: ^10.13.0 || >=12.0.0}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
openid-client@5.7.1:
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
openid-client@6.3.0:
resolution: {integrity: sha512-68LMqb/Whtq214B9c9kCtuniCKQrEqWJRTEoOEZlv2QV5VgqhjySCpBe4RXeU+pj/VNOi7erP/ixxfHtqR7FOw==}
@@ -3007,6 +3052,14 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
preact@10.28.2:
resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -3670,6 +3723,10 @@ packages:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
@@ -3868,6 +3925,9 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
@@ -4389,6 +4449,8 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@panva/hkdf@1.2.1': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -5255,6 +5317,8 @@ snapshots:
concat-map@0.0.1: {}
cookie@0.7.2: {}
core-js@3.40.0: {}
core-util-is@1.0.3: {}
@@ -6347,6 +6411,8 @@ snapshots:
jiti@2.6.1: {}
jose@4.15.9: {}
jose@5.10.0: {}
js-tokens@10.0.0: {}
@@ -6508,6 +6574,10 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
luxon@3.6.1: {}
lz-string@1.5.0: {}
@@ -6587,6 +6657,21 @@ snapshots:
net@1.0.2: {}
next-auth@4.24.13(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.28.4
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
next: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.28.2
preact-render-to-string: 5.2.6(preact@10.28.2)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
uuid: 8.3.2
next-i18next@12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.9
@@ -6635,8 +6720,12 @@ snapshots:
oauth4webapi@3.3.0: {}
oauth@0.9.15: {}
object-assign@4.1.1: {}
object-hash@2.2.0: {}
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
@@ -6676,6 +6765,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
oidc-token-hash@5.2.0: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -6684,6 +6775,13 @@ snapshots:
dependencies:
fn.name: 1.1.0
openid-client@5.7.1:
dependencies:
jose: 4.15.9
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.2.0
openid-client@6.3.0:
dependencies:
jose: 5.10.0
@@ -6766,6 +6864,13 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact-render-to-string@5.2.6(preact@10.28.2):
dependencies:
preact: 10.28.2
pretty-format: 3.8.0
preact@10.28.2: {}
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@@ -6781,6 +6886,8 @@ snapshots:
pretty-bytes@7.1.0: {}
pretty-format@3.8.0: {}
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
@@ -7544,6 +7651,8 @@ snapshots:
uuid@10.0.0: {}
uuid@8.3.2: {}
varint@6.0.0: {}
victory-vendor@37.3.6:
@@ -7780,6 +7889,8 @@ snapshots:
y18n@5.0.8: {}
yallist@4.0.0: {}
yallist@5.0.0: {}
yargs-parser@21.1.1: {}

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 },
};
}