constantinSch commited on
Commit
b0eb97c
·
1 Parent(s): f3d9354

Enhance authentication with HMAC tokens and implement login overlay in UI

Browse files
Files changed (2) hide show
  1. app.py +21 -56
  2. index.html +79 -4
app.py CHANGED
@@ -5,6 +5,8 @@ No accounts required -- annotators identify themselves by name.
5
  Annotations are persisted in a SQLite database.
6
 
7
  Optional password protection: set APP_PASSWORD as an environment variable.
 
 
8
  """
9
 
10
  import hashlib
@@ -15,8 +17,7 @@ import sqlite3
15
  from functools import cache
16
  from pathlib import Path
17
 
18
- from flask import Flask, Response, jsonify, make_response, redirect, request, send_file
19
- from werkzeug.middleware.proxy_fix import ProxyFix
20
 
21
  DATASET_PATH = Path(os.environ.get("DATASET_PATH", "evaluation_dataset.jsonl"))
22
  DB_PATH = Path(os.environ.get("DB_PATH", "/data/annotations.db"))
@@ -26,10 +27,9 @@ ANNOTATION_FIELDS = (
26
  )
27
 
28
  APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
 
29
 
30
  app = Flask(__name__)
31
- app.secret_key = os.environ.get("SECRET_KEY", os.urandom(24).hex())
32
- app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
33
 
34
 
35
  # ---------------------------------------------------------------------------
@@ -105,71 +105,36 @@ def merge_items_with_annotations(
105
 
106
 
107
  # ---------------------------------------------------------------------------
108
- # Optional password protection
109
  # ---------------------------------------------------------------------------
110
 
111
- LOGIN_HTML = """<!DOCTYPE html>
112
- <html lang="de"><head><meta charset="UTF-8">
113
- <meta name="viewport" content="width=device-width,initial-scale=1">
114
- <title>Anmeldung</title>
115
- <style>
116
- body { font-family: system-ui, sans-serif; background: #f5f5f5; }
117
- .box { max-width: 320px; margin: 120px auto; background: #fff;
118
- padding: 32px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1);
119
- text-align: center; }
120
- h2 { margin-bottom: 16px; font-size: 20px; }
121
- input { padding: 10px; width: 100%%; border: 1px solid #ccc; border-radius: 4px;
122
- font-size: 15px; margin-bottom: 12px; box-sizing: border-box; }
123
- button { padding: 10px 24px; border: none; border-radius: 4px;
124
- background: #2563eb; color: #fff; font-size: 15px; cursor: pointer; }
125
- button:hover { background: #1d4ed8; }
126
- .err { color: #dc2626; font-size: 13px; margin-bottom: 8px; }
127
- </style></head><body>
128
- <div class="box">
129
- <h2>Zusammenfassungs-Evaluation</h2>
130
- %(error)s
131
- <form method="post">
132
- <input type="password" name="password" placeholder="Passwort" autofocus>
133
- <button type="submit">Weiter</button>
134
- </form>
135
- </div></body></html>"""
136
-
137
 
138
  def _make_auth_token() -> str:
139
  """Create an HMAC token derived from the app password and secret key."""
140
- return hmac.new(
141
- app.secret_key.encode() if isinstance(app.secret_key, str) else app.secret_key,
142
- APP_PASSWORD.encode(),
143
- hashlib.sha256,
144
- ).hexdigest()
145
-
146
-
147
- def _is_authenticated() -> bool:
148
- return request.cookies.get("auth") == _make_auth_token()
149
 
150
 
151
  @app.before_request
152
  def check_auth():
153
  if not APP_PASSWORD:
154
  return None
155
- if request.path == "/login":
 
 
156
  return None
157
- if _is_authenticated():
 
158
  return None
159
- if request.path.startswith("/api/"):
160
- return jsonify({"error": "unauthorized"}), 401
161
- return redirect("/login")
162
-
163
-
164
- @app.route("/login", methods=["GET", "POST"])
165
- def login():
166
- if request.method == "POST":
167
- if request.form.get("password") == APP_PASSWORD:
168
- resp = make_response(redirect("/"))
169
- resp.set_cookie("auth", _make_auth_token(), httponly=True, samesite="Lax")
170
- return resp
171
- return LOGIN_HTML % {"error": '<p class="err">Falsches Passwort.</p>'}, 401
172
- return LOGIN_HTML % {"error": ""}
173
 
174
 
175
  # ---------------------------------------------------------------------------
 
5
  Annotations are persisted in a SQLite database.
6
 
7
  Optional password protection: set APP_PASSWORD as an environment variable.
8
+ Authentication uses HMAC tokens stored in the browser via localStorage,
9
+ avoiding cookies/sessions that can break behind reverse proxies.
10
  """
11
 
12
  import hashlib
 
17
  from functools import cache
18
  from pathlib import Path
19
 
20
+ from flask import Flask, Response, jsonify, request, send_file
 
21
 
22
  DATASET_PATH = Path(os.environ.get("DATASET_PATH", "evaluation_dataset.jsonl"))
23
  DB_PATH = Path(os.environ.get("DB_PATH", "/data/annotations.db"))
 
27
  )
28
 
29
  APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
30
+ SECRET_KEY = os.environ.get("SECRET_KEY", os.urandom(24).hex())
31
 
32
  app = Flask(__name__)
 
 
33
 
34
 
35
  # ---------------------------------------------------------------------------
 
105
 
106
 
107
  # ---------------------------------------------------------------------------
108
+ # Optional password protection (token-based, no cookies/sessions)
109
  # ---------------------------------------------------------------------------
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def _make_auth_token() -> str:
113
  """Create an HMAC token derived from the app password and secret key."""
114
+ key = SECRET_KEY.encode() if isinstance(SECRET_KEY, str) else SECRET_KEY
115
+ return hmac.new(key, APP_PASSWORD.encode(), hashlib.sha256).hexdigest()
 
 
 
 
 
 
 
116
 
117
 
118
  @app.before_request
119
  def check_auth():
120
  if not APP_PASSWORD:
121
  return None
122
+ if request.path in ("/", "/login"):
123
+ return None
124
+ if request.path.startswith("/api/login"):
125
  return None
126
+ token = request.headers.get("Authorization", "").removeprefix("Bearer ")
127
+ if token == _make_auth_token():
128
  return None
129
+ return jsonify({"error": "unauthorized"}), 401
130
+
131
+
132
+ @app.route("/api/login", methods=["POST"])
133
+ def api_login():
134
+ data = request.get_json()
135
+ if data and data.get("password") == APP_PASSWORD:
136
+ return jsonify({"token": _make_auth_token()})
137
+ return jsonify({"error": "wrong_password"}), 401
 
 
 
 
 
138
 
139
 
140
  # ---------------------------------------------------------------------------
index.html CHANGED
@@ -162,6 +162,29 @@
162
  text-align: center; padding: 60px 20px; color: #666; font-size: 15px;
163
  }
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  .badge-promptd {
166
  background: #fef3c7; color: #92400e; padding: 3px 10px;
167
  border-radius: 12px; font-size: 12px; font-weight: 600;
@@ -183,6 +206,17 @@
183
  </head>
184
  <body>
185
 
 
 
 
 
 
 
 
 
 
 
 
186
  <header>
187
  <h1>Zusammenfassungs-Evaluation</h1>
188
  <div class="progress-box">
@@ -361,6 +395,47 @@ let filteredRows = [];
361
  let currentIdx = 0;
362
  let dirty = false;
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  /* ---------- Silent session ID (one per browser, prevents overwrites) ---------- */
365
 
366
  function getSessionId() {
@@ -376,7 +451,7 @@ const SESSION_ID = getSessionId();
376
  /* ---------- Data loading ---------- */
377
 
378
  async function loadEntries() {
379
- const res = await fetch("/api/entries?annotator=" + encodeURIComponent(SESSION_ID));
380
  if (!res.ok) { allRows = []; applyFilters(); return; }
381
  allRows = await res.json();
382
  applyFilters();
@@ -408,7 +483,7 @@ function applyFilters() {
408
  }
409
 
410
  async function refreshProgress() {
411
- const res = await fetch("/api/progress?annotator=" + encodeURIComponent(SESSION_ID));
412
  if (!res.ok) return;
413
  const p = await res.json();
414
  document.getElementById("progress-text").textContent = p.annotated + " / " + p.total;
@@ -552,7 +627,7 @@ async function saveAnnotation() {
552
 
553
  const res = await fetch("/api/annotate", {
554
  method: "POST",
555
- headers: { "Content-Type": "application/json" },
556
  body: JSON.stringify(data),
557
  });
558
 
@@ -607,7 +682,7 @@ function toggleOriginal() {
607
  }
608
 
609
  /* ---------- Init ---------- */
610
- loadEntries();
611
  </script>
612
  </body>
613
  </html>
 
162
  text-align: center; padding: 60px 20px; color: #666; font-size: 15px;
163
  }
164
 
165
+ /* Login overlay */
166
+ .login-overlay {
167
+ position: fixed; inset: 0; background: #f5f5f5; z-index: 100;
168
+ display: flex; align-items: center; justify-content: center;
169
+ }
170
+ .login-overlay.hidden { display: none; }
171
+ .login-box {
172
+ max-width: 320px; width: 100%; background: #fff;
173
+ padding: 32px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1);
174
+ text-align: center;
175
+ }
176
+ .login-box h2 { margin-bottom: 16px; font-size: 20px; }
177
+ .login-box input {
178
+ padding: 10px; width: 100%; border: 1px solid #ccc; border-radius: 4px;
179
+ font-size: 15px; margin-bottom: 12px; box-sizing: border-box;
180
+ }
181
+ .login-box button {
182
+ padding: 10px 24px; border: none; border-radius: 4px;
183
+ background: #2563eb; color: #fff; font-size: 15px; cursor: pointer;
184
+ }
185
+ .login-box button:hover { background: #1d4ed8; }
186
+ .login-error { color: #dc2626; font-size: 13px; margin-bottom: 8px; }
187
+
188
  .badge-promptd {
189
  background: #fef3c7; color: #92400e; padding: 3px 10px;
190
  border-radius: 12px; font-size: 12px; font-weight: 600;
 
206
  </head>
207
  <body>
208
 
209
+ <div class="login-overlay" id="login-overlay">
210
+ <div class="login-box">
211
+ <h2>Zusammenfassungs-Evaluation</h2>
212
+ <p class="login-error" id="login-error" style="display:none">Falsches Passwort.</p>
213
+ <form onsubmit="handleLogin(event)">
214
+ <input type="password" id="login-password" placeholder="Passwort" autofocus>
215
+ <button type="submit">Weiter</button>
216
+ </form>
217
+ </div>
218
+ </div>
219
+
220
  <header>
221
  <h1>Zusammenfassungs-Evaluation</h1>
222
  <div class="progress-box">
 
395
  let currentIdx = 0;
396
  let dirty = false;
397
 
398
+ /* ---------- Auth ---------- */
399
+
400
+ function getAuthToken() {
401
+ return localStorage.getItem("auth_token") || "";
402
+ }
403
+
404
+ function authHeaders() {
405
+ const token = getAuthToken();
406
+ const h = {};
407
+ if (token) h["Authorization"] = "Bearer " + token;
408
+ return h;
409
+ }
410
+
411
+ async function handleLogin(e) {
412
+ e.preventDefault();
413
+ const pw = document.getElementById("login-password").value;
414
+ const res = await fetch("/api/login", {
415
+ method: "POST",
416
+ headers: { "Content-Type": "application/json" },
417
+ body: JSON.stringify({ password: pw }),
418
+ });
419
+ if (res.ok) {
420
+ const data = await res.json();
421
+ localStorage.setItem("auth_token", data.token);
422
+ document.getElementById("login-overlay").classList.add("hidden");
423
+ loadEntries();
424
+ } else {
425
+ document.getElementById("login-error").style.display = "";
426
+ }
427
+ }
428
+
429
+ async function checkAuth() {
430
+ const res = await fetch("/api/progress?annotator=_ping", { headers: authHeaders() });
431
+ if (res.status === 401) {
432
+ document.getElementById("login-overlay").classList.remove("hidden");
433
+ } else {
434
+ document.getElementById("login-overlay").classList.add("hidden");
435
+ loadEntries();
436
+ }
437
+ }
438
+
439
  /* ---------- Silent session ID (one per browser, prevents overwrites) ---------- */
440
 
441
  function getSessionId() {
 
451
  /* ---------- Data loading ---------- */
452
 
453
  async function loadEntries() {
454
+ const res = await fetch("/api/entries?annotator=" + encodeURIComponent(SESSION_ID), { headers: authHeaders() });
455
  if (!res.ok) { allRows = []; applyFilters(); return; }
456
  allRows = await res.json();
457
  applyFilters();
 
483
  }
484
 
485
  async function refreshProgress() {
486
+ const res = await fetch("/api/progress?annotator=" + encodeURIComponent(SESSION_ID), { headers: authHeaders() });
487
  if (!res.ok) return;
488
  const p = await res.json();
489
  document.getElementById("progress-text").textContent = p.annotated + " / " + p.total;
 
627
 
628
  const res = await fetch("/api/annotate", {
629
  method: "POST",
630
+ headers: { "Content-Type": "application/json", ...authHeaders() },
631
  body: JSON.stringify(data),
632
  });
633
 
 
682
  }
683
 
684
  /* ---------- Init ---------- */
685
+ checkAuth();
686
  </script>
687
  </body>
688
  </html>