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