andrewbejjani commited on
Commit
5c074ff
·
1 Parent(s): dc06d4c

Add app password protection

Browse files
Files changed (6) hide show
  1. .env.example +4 -0
  2. .gitignore +6 -0
  3. README.md +6 -3
  4. src/config.py +4 -0
  5. src/utils.py +7 -9
  6. ui_app.py +35 -1
.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ GROQ_API_KEY=
2
+ GROQ_MODEL=
3
+ APP_USERNAME=
4
+ APP_PASSWORD=
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .env
2
+ .env.*
3
+ !.env.example
4
+
5
+ __pycache__/
6
+ *.py[cod]
README.md CHANGED
@@ -4,11 +4,14 @@ sdk: docker
4
  app_port: 7860
5
  ---
6
 
7
- ## Hugging Face Secrets
8
 
9
- Set these in the Space settings:
 
10
 
11
  - `GROQ_API_KEY`: required for Groq model calls.
12
- - `HF_TOKEN`: optional, required only for the `Save Manual References` button.
 
 
13
 
14
  `Save Manual References` only enables on Hugging Face Spaces when `SPACE_ID` is present and `HF_TOKEN` is configured. It commits the current `refdata/manual_references.json` back to the Space repository.
 
4
  app_port: 7860
5
  ---
6
 
7
+ ## Environment
8
 
9
+ For local development, copy `.env.example` to `.env` and fill only the values you need.
10
+ Set production secrets in the Hugging Face Space settings, not in committed files.
11
 
12
  - `GROQ_API_KEY`: required for Groq model calls.
13
+ - `APP_PASSWORD`: optional, enables password protection for the running app.
14
+ - `APP_USERNAME`: optional, defaults to `mastermap` when `APP_PASSWORD` is set.
15
+ - `HF_TOKEN`: Hugging Face Space Secret only; optional, required only for the `Save Manual References` button.
16
 
17
  `Save Manual References` only enables on Hugging Face Spaces when `SPACE_ID` is present and `HF_TOKEN` is configured. It commits the current `refdata/manual_references.json` back to the Space repository.
src/config.py CHANGED
@@ -8,6 +8,10 @@ load_dotenv()
8
  # --- ENVIRONMENT VARIABLES to be set up in .env ---
9
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
10
  RAW_MODELS = os.getenv("GROQ_MODEL", "")
 
 
 
 
11
 
12
  # Parse models cleanly into a list
13
  AVAILABLE_MODELS = [m.strip() for m in RAW_MODELS.split(",") if m.strip()]
 
8
  # --- ENVIRONMENT VARIABLES to be set up in .env ---
9
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
10
  RAW_MODELS = os.getenv("GROQ_MODEL", "")
11
+ APP_USERNAME = os.getenv("APP_USERNAME", "")
12
+ APP_PASSWORD = os.getenv("APP_PASSWORD", "")
13
+ SPACE_ID = os.getenv("SPACE_ID", "")
14
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
15
 
16
  # Parse models cleanly into a list
17
  AVAILABLE_MODELS = [m.strip() for m in RAW_MODELS.split(",") if m.strip()]
src/utils.py CHANGED
@@ -1,8 +1,9 @@
1
- import os
2
  from pathlib import Path
3
  import re
4
  import unicodedata
5
 
 
 
6
  def strip_degrees_for_search(text):
7
  if not isinstance(text, str): return text
8
  degree_pattern = r'\b(MSc|MBA|BBA|BSc|Ph\.?D\.?|BA|MA|BS|MS|EMBA|Master|Bachelor|Masters|Bachelors|Licence)\b'
@@ -99,26 +100,23 @@ def prune_manual_refs_against_official(manual_refs, official_refs):
99
  MANUAL_REFERENCES_REPO_PATH = "refdata/manual_references.json"
100
 
101
  def reference_sync_status():
102
- space_id = os.getenv("SPACE_ID", "")
103
- has_token = bool(os.getenv("HF_TOKEN"))
104
-
105
- if not space_id:
106
  return {
107
  "enabled": False,
108
  "space_id": "",
109
  "reason": "Reference sync is only available on Hugging Face Spaces.",
110
  }
111
 
112
- if not has_token:
113
  return {
114
  "enabled": False,
115
- "space_id": space_id,
116
  "reason": "HF_TOKEN secret is missing from this Space.",
117
  }
118
 
119
  return {
120
  "enabled": True,
121
- "space_id": space_id,
122
  "reason": "",
123
  }
124
 
@@ -136,7 +134,7 @@ def save_manual_references_to_hub(app_root: Path):
136
  except ImportError as exc:
137
  raise RuntimeError("huggingface_hub is not installed.") from exc
138
 
139
- api = HfApi(token=os.environ["HF_TOKEN"])
140
  commit_info = api.upload_file(
141
  path_or_fileobj=str(manual_refs_path),
142
  path_in_repo=MANUAL_REFERENCES_REPO_PATH,
 
 
1
  from pathlib import Path
2
  import re
3
  import unicodedata
4
 
5
+ from src.config import HF_TOKEN, SPACE_ID
6
+
7
  def strip_degrees_for_search(text):
8
  if not isinstance(text, str): return text
9
  degree_pattern = r'\b(MSc|MBA|BBA|BSc|Ph\.?D\.?|BA|MA|BS|MS|EMBA|Master|Bachelor|Masters|Bachelors|Licence)\b'
 
100
  MANUAL_REFERENCES_REPO_PATH = "refdata/manual_references.json"
101
 
102
  def reference_sync_status():
103
+ if not SPACE_ID:
 
 
 
104
  return {
105
  "enabled": False,
106
  "space_id": "",
107
  "reason": "Reference sync is only available on Hugging Face Spaces.",
108
  }
109
 
110
+ if not HF_TOKEN:
111
  return {
112
  "enabled": False,
113
+ "space_id": SPACE_ID,
114
  "reason": "HF_TOKEN secret is missing from this Space.",
115
  }
116
 
117
  return {
118
  "enabled": True,
119
+ "space_id": SPACE_ID,
120
  "reason": "",
121
  }
122
 
 
134
  except ImportError as exc:
135
  raise RuntimeError("huggingface_hub is not installed.") from exc
136
 
137
+ api = HfApi(token=HF_TOKEN)
138
  commit_info = api.upload_file(
139
  path_or_fileobj=str(manual_refs_path),
140
  path_in_repo=MANUAL_REFERENCES_REPO_PATH,
ui_app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import sys
2
  import uuid
3
  from pathlib import Path
@@ -5,7 +6,13 @@ from pathlib import Path
5
  from flask import Flask, Response, jsonify, render_template, request, send_file
6
 
7
  from newest_model import PREFERRED_PRODUCTION_CHAT_MODELS, select_groq_chat_models
8
- from src.config import AVAILABLE_MODELS, DATA_DIR, DEFAULT_OUTPUT_SHEET_NAME
 
 
 
 
 
 
9
  from src.process_runner import stop_process, stream_process
10
  from src.utils import reference_sync_status, save_manual_references_to_hub
11
  from src.workbook_io import read_workbook_sheets, resolve_allowed_path, save_uploaded_excel
@@ -37,6 +44,33 @@ STATE = {
37
  "apply_blueprint_path": "",
38
  "apply_blueprint_filename": "",
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  @app.after_request
41
  def prevent_browser_cache(response):
42
  response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
 
1
+ import secrets
2
  import sys
3
  import uuid
4
  from pathlib import Path
 
6
  from flask import Flask, Response, jsonify, render_template, request, send_file
7
 
8
  from newest_model import PREFERRED_PRODUCTION_CHAT_MODELS, select_groq_chat_models
9
+ from src.config import (
10
+ APP_PASSWORD,
11
+ APP_USERNAME,
12
+ AVAILABLE_MODELS,
13
+ DATA_DIR,
14
+ DEFAULT_OUTPUT_SHEET_NAME,
15
+ )
16
  from src.process_runner import stop_process, stream_process
17
  from src.utils import reference_sync_status, save_manual_references_to_hub
18
  from src.workbook_io import read_workbook_sheets, resolve_allowed_path, save_uploaded_excel
 
44
  "apply_blueprint_path": "",
45
  "apply_blueprint_filename": "",
46
  }
47
+
48
+
49
+ def auth_required_response() -> Response:
50
+ return Response(
51
+ "Authentication required",
52
+ 401,
53
+ {"WWW-Authenticate": 'Basic realm="MasterMap Cleaner"'},
54
+ )
55
+
56
+
57
+ @app.before_request
58
+ def require_basic_auth():
59
+ if not APP_PASSWORD:
60
+ return None
61
+
62
+ auth = request.authorization
63
+ if not auth:
64
+ return auth_required_response()
65
+
66
+ valid_username = secrets.compare_digest(auth.username or "", APP_USERNAME)
67
+ valid_password = secrets.compare_digest(auth.password or "", APP_PASSWORD)
68
+ if valid_username and valid_password:
69
+ return None
70
+
71
+ return auth_required_response()
72
+
73
+
74
  @app.after_request
75
  def prevent_browser_cache(response):
76
  response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"