lanczos commited on
Commit
acdd723
·
verified ·
1 Parent(s): ea27f41

deploy: labeling server

Browse files
labeling/static/app.js CHANGED
@@ -4,6 +4,7 @@ const AXES = ["art_style", "color", "art_medium", "lighting"];
4
  const TOKEN_STORAGE_KEY = "aamcq_token";
5
  const THEME_STORAGE_KEY = "aamcq_theme";
6
  const ROUNDS_KEY = "aamcq_rounds_done";
 
7
  const FIRST_SESSION_CAP = 20;
8
  const REPEAT_SESSION_CAP = 10;
9
 
@@ -53,13 +54,38 @@ function nextSessionCap() {
53
  return rounds === 0 ? FIRST_SESSION_CAP : REPEAT_SESSION_CAP;
54
  }
55
 
 
 
 
 
 
 
56
  async function registerFresh(cap) {
57
- const resp = await fetch(`/api/register?cap=${cap}`, { method: "POST" });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  if (!resp.ok) {
59
  throw new Error(
60
  "No ?token= in URL and anonymous registration is disabled on this server."
61
  );
62
  }
 
63
  const { token } = await resp.json();
64
  localStorage.setItem(TOKEN_STORAGE_KEY, token);
65
  return token;
 
4
  const TOKEN_STORAGE_KEY = "aamcq_token";
5
  const THEME_STORAGE_KEY = "aamcq_theme";
6
  const ROUNDS_KEY = "aamcq_rounds_done";
7
+ const PASSWORD_SESSION_KEY = "aamcq_access_password";
8
  const FIRST_SESSION_CAP = 20;
9
  const REPEAT_SESSION_CAP = 10;
10
 
 
54
  return rounds === 0 ? FIRST_SESSION_CAP : REPEAT_SESSION_CAP;
55
  }
56
 
57
+ async function attemptRegister(cap, password) {
58
+ const params = new URLSearchParams({ cap: String(cap) });
59
+ if (password) params.set("password", password);
60
+ return fetch(`/api/register?${params.toString()}`, { method: "POST" });
61
+ }
62
+
63
  async function registerFresh(cap) {
64
+ // Try with whatever password we have cached (could be empty).
65
+ let password = sessionStorage.getItem(PASSWORD_SESSION_KEY) || "";
66
+ let resp = await attemptRegister(cap, password);
67
+
68
+ // 403 means server wants a password — prompt (and re-prompt on mismatch).
69
+ while (resp.status === 403) {
70
+ sessionStorage.removeItem(PASSWORD_SESSION_KEY);
71
+ const entered = window.prompt(
72
+ password
73
+ ? "Wrong access password. Try again:"
74
+ : "Enter the access password to start labeling:"
75
+ );
76
+ if (entered == null) {
77
+ throw new Error("Access password required.");
78
+ }
79
+ password = entered;
80
+ resp = await attemptRegister(cap, password);
81
+ }
82
+
83
  if (!resp.ok) {
84
  throw new Error(
85
  "No ?token= in URL and anonymous registration is disabled on this server."
86
  );
87
  }
88
+ if (password) sessionStorage.setItem(PASSWORD_SESSION_KEY, password);
89
  const { token } = await resp.json();
90
  localStorage.setItem(TOKEN_STORAGE_KEY, token);
91
  return token;
labeling/static/index.html CHANGED
@@ -41,6 +41,6 @@
41
  <span id="error"></span>
42
  </footer>
43
  </main>
44
- <script src="/app.js?v=10"></script>
45
  </body>
46
  </html>
 
41
  <span id="error"></span>
42
  </footer>
43
  </main>
44
+ <script src="/app.js?v=11"></script>
45
  </body>
46
  </html>
spaces/space_entry.py CHANGED
@@ -14,6 +14,7 @@ Env vars:
14
  AAMCQ_PER_ANNOTATOR_CAP default: 20
15
  AAMCQ_LABELS_PER_ITEM default: 3
16
  AAMCQ_BACKUP_INTERVAL default: 60 (seconds)
 
17
  """
18
 
19
  from __future__ import annotations
@@ -41,6 +42,7 @@ MCQ_PATH = DATA_DIR / "mcq_unlabeled.jsonl"
41
  BACKUP_INTERVAL = int(os.environ.get("AAMCQ_BACKUP_INTERVAL", "60"))
42
  PER_ANNOTATOR_CAP = int(os.environ.get("AAMCQ_PER_ANNOTATOR_CAP", "20"))
43
  LABELS_PER_ITEM = int(os.environ.get("AAMCQ_LABELS_PER_ITEM", "3"))
 
44
 
45
 
46
  def _require_token() -> str:
@@ -141,7 +143,12 @@ def main() -> int:
141
  anonymous_register=True,
142
  max_labels_per_item=LABELS_PER_ITEM,
143
  max_labels_per_annotator=PER_ANNOTATOR_CAP,
 
144
  )
 
 
 
 
145
 
146
  @app.on_event("startup")
147
  async def _start_backup() -> None:
 
14
  AAMCQ_PER_ANNOTATOR_CAP default: 20
15
  AAMCQ_LABELS_PER_ITEM default: 3
16
  AAMCQ_BACKUP_INTERVAL default: 60 (seconds)
17
+ AAMCQ_ACCESS_PASSWORD optional; if set, /api/register requires it
18
  """
19
 
20
  from __future__ import annotations
 
42
  BACKUP_INTERVAL = int(os.environ.get("AAMCQ_BACKUP_INTERVAL", "60"))
43
  PER_ANNOTATOR_CAP = int(os.environ.get("AAMCQ_PER_ANNOTATOR_CAP", "20"))
44
  LABELS_PER_ITEM = int(os.environ.get("AAMCQ_LABELS_PER_ITEM", "3"))
45
+ ACCESS_PASSWORD = os.environ.get("AAMCQ_ACCESS_PASSWORD") or None
46
 
47
 
48
  def _require_token() -> str:
 
143
  anonymous_register=True,
144
  max_labels_per_item=LABELS_PER_ITEM,
145
  max_labels_per_annotator=PER_ANNOTATOR_CAP,
146
+ access_password=ACCESS_PASSWORD,
147
  )
148
+ if ACCESS_PASSWORD:
149
+ print("access password gate: ON")
150
+ else:
151
+ print("access password gate: OFF (set AAMCQ_ACCESS_PASSWORD to enable)")
152
 
153
  @app.on_event("startup")
154
  async def _start_backup() -> None:
src/aamcq/annotation/api.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import os
6
  import sqlite3
7
  from pathlib import Path
@@ -41,6 +42,7 @@ def create_app(
41
  anonymous_register: bool = False,
42
  max_labels_per_item: int = 3,
43
  max_labels_per_annotator: int | None = None,
 
44
  ) -> FastAPI:
45
  """Labeling server.
46
 
@@ -58,6 +60,10 @@ def create_app(
58
  + token on demand, so a single public URL can serve any number of
59
  concurrent anonymous annotators (each browser session = one annotator).
60
  Intended for public-URL crowdsourcing.
 
 
 
 
61
  """
62
  db_path = Path(db_path or DEFAULT_DB)
63
  image_dir = Path(image_dir or DEFAULT_IMAGE_DIR)
@@ -72,6 +78,7 @@ def create_app(
72
  app.state.anonymous_register = anonymous_register
73
  app.state.max_labels_per_item = max_labels_per_item
74
  app.state.max_labels_per_annotator = max_labels_per_annotator
 
75
 
76
  def get_conn() -> sqlite3.Connection:
77
  return app.state.conn
@@ -111,6 +118,7 @@ def create_app(
111
  @app.post("/api/register")
112
  def api_register(
113
  cap: int | None = Query(default=None, ge=1, le=10000),
 
114
  conn: sqlite3.Connection = Depends(get_conn),
115
  ):
116
  """Mint a fresh anonymous annotator. Only enabled when anonymous_register.
@@ -118,9 +126,16 @@ def create_app(
118
  Optional `?cap=N` sets a per-annotator label cap that overrides the
119
  server default (used by the frontend to give the first session a
120
  larger quota than subsequent ones).
 
 
 
121
  """
122
  if not app.state.anonymous_register:
123
  raise HTTPException(status_code=404, detail="anonymous register disabled")
 
 
 
 
124
  existing = {row["annotator_id"] for row in conn.execute(
125
  "SELECT annotator_id FROM annotators"
126
  )}
 
2
 
3
  from __future__ import annotations
4
 
5
+ import hmac
6
  import os
7
  import sqlite3
8
  from pathlib import Path
 
42
  anonymous_register: bool = False,
43
  max_labels_per_item: int = 3,
44
  max_labels_per_annotator: int | None = None,
45
+ access_password: str | None = None,
46
  ) -> FastAPI:
47
  """Labeling server.
48
 
 
60
  + token on demand, so a single public URL can serve any number of
61
  concurrent anonymous annotators (each browser session = one annotator).
62
  Intended for public-URL crowdsourcing.
63
+
64
+ `access_password`: if set, `/api/register` requires a matching
65
+ `?password=` query param (constant-time compared). Cheap anti-spam
66
+ gate for public Spaces — existing tokens keep working regardless.
67
  """
68
  db_path = Path(db_path or DEFAULT_DB)
69
  image_dir = Path(image_dir or DEFAULT_IMAGE_DIR)
 
78
  app.state.anonymous_register = anonymous_register
79
  app.state.max_labels_per_item = max_labels_per_item
80
  app.state.max_labels_per_annotator = max_labels_per_annotator
81
+ app.state.access_password = access_password
82
 
83
  def get_conn() -> sqlite3.Connection:
84
  return app.state.conn
 
118
  @app.post("/api/register")
119
  def api_register(
120
  cap: int | None = Query(default=None, ge=1, le=10000),
121
+ password: str | None = Query(default=None, max_length=256),
122
  conn: sqlite3.Connection = Depends(get_conn),
123
  ):
124
  """Mint a fresh anonymous annotator. Only enabled when anonymous_register.
 
126
  Optional `?cap=N` sets a per-annotator label cap that overrides the
127
  server default (used by the frontend to give the first session a
128
  larger quota than subsequent ones).
129
+
130
+ If `access_password` was set at startup, `?password=` must match
131
+ (constant-time compared) or we return 403.
132
  """
133
  if not app.state.anonymous_register:
134
  raise HTTPException(status_code=404, detail="anonymous register disabled")
135
+ expected = app.state.access_password
136
+ if expected:
137
+ if not password or not hmac.compare_digest(password, expected):
138
+ raise HTTPException(status_code=403, detail="wrong access password")
139
  existing = {row["annotator_id"] for row in conn.execute(
140
  "SELECT annotator_id FROM annotators"
141
  )}