File size: 2,101 Bytes
0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 2bc1ea3 0e06155 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | /**
* HTTP Basic Auth gate for the entire UI.
*
* Username is read from `UI_USERNAME` (defaults to "etiya") and password from
* `UI_PASSWORD`. Both are server-only env vars set as HF Space secrets — they
* are never inlined into the client bundle.
*
* The browser's native credentials prompt handles the UX. After the user
* authenticates once, the browser caches the credentials per-origin and
* sends them automatically on every subsequent request to this realm.
*/
import { NextRequest, NextResponse } from "next/server";
export const config = {
matcher: ["/((?!_next/static|_next/image|_next/data|favicon.ico).*)"],
};
const REALM = 'Basic realm="Etiya BSS Atelier", charset="UTF-8"';
export function middleware(req: NextRequest) {
const password = process.env.UI_PASSWORD;
const username = process.env.UI_USERNAME || "etiya";
// Fail-open if no password configured — useful for local dev with no setup.
if (!password) return NextResponse.next();
const auth = req.headers.get("authorization");
if (!auth?.startsWith("Basic ")) {
return unauthorised();
}
let user = "";
let pass = "";
try {
const decoded = atob(auth.slice("Basic ".length));
const colon = decoded.indexOf(":");
if (colon >= 0) {
user = decoded.slice(0, colon);
pass = decoded.slice(colon + 1);
}
} catch {
return unauthorised();
}
if (
!constantTimeEqual(user, username) ||
!constantTimeEqual(pass, password)
) {
return unauthorised();
}
return NextResponse.next();
}
function unauthorised() {
return new NextResponse("Authentication required.", {
status: 401,
headers: {
"WWW-Authenticate": REALM,
"Content-Type": "text/plain; charset=utf-8",
// Discourage caching the 401 response so re-prompts behave predictably.
"Cache-Control": "no-store",
},
});
}
function constantTimeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}
|