Skip to content

Commit 354ac57

Browse files
committed
make login page background image configurable via LOGIN_IMAGE
The login page hardcoded the bundled splash. Read LOGIN_IMAGE (a filename in /public) in getServerSideProps and use it, falling back to the bundled splash when unset. Same-origin only by design (the CSP is img-src 'self'). A shared helper (lib/loginImage.ts) resolves the path for both the login page and proxy.ts, so the auth allowlist always matches the URL the page requests — without it the /public image is gated and 307-redirected to login, leaving the splash blank. Documented in .env_example.
1 parent 0b29d5b commit 354ac57

4 files changed

Lines changed: 36 additions & 2 deletions

File tree

.env_example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ REMINDER_RENEWAL_COUNT=5
9696
# 🖼️ Background template for user ID labels (filename in /public directory)
9797
USERID_LABEL_IMAGE=userlabeltemplate.jpg
9898

99+
# 🖼️ Login page background image (filename in /public directory)
100+
# Leave unset to use the built-in default splash image.
101+
# LOGIN_IMAGE="schule_login.jpg"
102+
99103
# 📐 Size and layout
100104
USERLABEL_WIDTH="42vw"
101105
USERLABEL_PER_PAGE=6

lib/loginImage.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Resolve the configured login background image (LOGIN_IMAGE env) to the URL the
3+
* login page renders:
4+
* - a bare filename ("schule_login.jpg") is served from /public → "/schule_login.jpg"
5+
* - an already-rooted path ("/img/x.jpg") is returned as-is
6+
* - unset → null, and the login page falls back to the bundled splash
7+
*
8+
* Same-origin only by design: the CSP is `img-src 'self'`, so the image must be
9+
* served from this app (i.e. /public). Shared by the login page (to render it)
10+
* and proxy.ts (to let it through the auth gate) so the allowlisted path always
11+
* matches the requested one.
12+
*/
13+
export function resolveLoginImage(): string | null {
14+
const raw = process.env.LOGIN_IMAGE?.trim();
15+
if (!raw) return null;
16+
return raw.startsWith("/") ? raw : `/${raw}`;
17+
}

pages/auth/login.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ import { getCsrfToken } from "next-auth/react";
1010
import Head from "next/head";
1111

1212
import { t } from "@/lib/i18n";
13+
import { resolveLoginImage } from "@/lib/loginImage";
1314
import loginsplash from "./loginsplashscreen.jpg";
1415

1516
export default function Login({
1617
csrfToken,
18+
loginImage,
1719
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
20+
// Configurable via LOGIN_IMAGE (a file in /public); falls back to the bundled
21+
// splash when unset.
22+
const splashSrc = loginImage ?? loginsplash.src;
1823
const [user, setUser] = useState("");
1924
const [password, setPassword] = useState("");
2025
const [error, setError] = useState("");
@@ -61,7 +66,7 @@ export default function Login({
6166
<div
6267
className="hidden sm:block sm:w-5/12 md:w-7/12 relative"
6368
style={{
64-
backgroundImage: `url(${loginsplash.src})`,
69+
backgroundImage: `url(${splashSrc})`,
6570
backgroundRepeat: "no-repeat",
6671
backgroundSize: "cover",
6772
backgroundPosition: "center",
@@ -163,9 +168,12 @@ export default function Login({
163168
}
164169

165170
export async function getServerSideProps(context: GetServerSidePropsContext) {
171+
const loginImage = resolveLoginImage();
172+
166173
return {
167174
props: {
168175
csrfToken: (await getCsrfToken(context)) ?? null,
176+
loginImage,
169177
},
170178
};
171179
}

proxy.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { resolveLoginImage } from "@/lib/loginImage";
12
import { withAuth } from "next-auth/middleware";
23
import { NextRequest, NextResponse } from "next/server";
34

@@ -41,12 +42,16 @@ export default withAuth(
4142

4243
// Routes explicitly excluded from authentication.
4344
// Keep this list narrow — every entry here is a public attack surface.
45+
// The configured login background lives in /public and must load on the
46+
// (unauthenticated) login page, so allow exactly that one file.
47+
const loginImage = resolveLoginImage();
4448
const isPublicRoute =
4549
pathname === "/publicbookview" ||
4650
pathname === "/catalog" ||
4751
pathname.startsWith("/api/images") ||
4852
pathname === "/api/version" ||
49-
pathname.startsWith("/api/public/");
53+
pathname.startsWith("/api/public/") ||
54+
(loginImage !== null && pathname === loginImage);
5055

5156
if (isPublicRoute) return true;
5257

0 commit comments

Comments
 (0)