devaanand commited on
Commit
3fde616
·
1 Parent(s): 4d1197e

fix(auth): trim whitespace from CCR_APP_URL to prevent sign-in issues

Browse files

docs: update deployment instructions and clarify CCR_APP_URL usage
test: add test for app_url to handle pasted whitespace in CCR_APP_URL
chore: update reverse proxy worker with correct upstream host

CHANGELOG.md CHANGED
@@ -64,6 +64,10 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on
64
  - Landing page links the PI's name (Mohammad Atari) to his website.
65
 
66
  ### Fixed
 
 
 
 
67
  - Existing local databases no longer 500 after schema additions: additive
68
  SQLite auto-migration adds missing columns at startup (Alembic replaces this in
69
  Phase 2 with Postgres).
 
64
  - Landing page links the PI's name (Mohammad Atari) to his website.
65
 
66
  ### Fixed
67
+ - Google sign-in no longer breaks when CCR_APP_URL is pasted with a trailing
68
+ newline or space: the app trims the value before building the sign-in return
69
+ URL. Untrimmed, the newline reached Supabase as %0A inside the redirect, so
70
+ the redirect never matched the allow list and browsers flagged the link.
71
  - Existing local databases no longer 500 after schema additions: additive
72
  SQLite auto-migration adds missing columns at startup (Alembic replaces this in
73
  Phase 2 with Postgres).
DEPLOY.md CHANGED
@@ -1,17 +1,23 @@
1
- # Deploying the dev instance (Hugging Face Space)
2
 
3
  The Space builds from this repo's Dockerfile. One-time setup lives in the
4
- Space settings; after that, deploys are just `git push hf main`.
 
 
 
 
 
 
5
 
6
  ## Space settings (Settings > Variables and secrets)
7
 
8
  Hugging Face keeps **Variables** and **Secrets** in two separate stores, and a
9
  name defined in BOTH puts the Space into `CONFIG_ERROR` ("Collision on
10
  variables and secrets names") before it even builds. Add each key below to one
11
- store only if the Space reports a config error after a settings change, look
12
  for a duplicated name first, not a bad value.
13
 
14
- Secrets (credentials encrypted, write-only once set):
15
 
16
  | Secret | Value |
17
  | ------------------ | ----------------------------------------------------------- |
@@ -20,17 +26,23 @@ Secrets (credentials — encrypted, write-only once set):
20
  | SUPABASE_ANON_KEY | from the same page (anon public key, NOT service_role) |
21
  | DATABASE_URL | Supabase session-pooler URI (see persistent storage below) |
22
 
23
- Variables (non-sensitive tuning visible in settings, safe to edit):
24
 
25
  | Variable | Value |
26
  | ---------------------- | --------------------------------------------------- |
27
- | CCR_APP_URL | https://devaanand-ccr-platform.hf.space |
28
  | CCR_COOKIE_SECURE | 1 |
29
  | CCR_MAX_ROWS | 20000 (global row ceiling; code default is 100000) |
30
  | CCR_MAX_UPLOAD_BYTES | optional; code default is 52428800 (50 MB) |
31
  | CCR_ANON_MAX_BYTES | optional; code default is 5242880 (5 MB) |
32
 
33
- `CCR_MAX_ROWS` is the limit that actually bounds a run embedding cost scales
 
 
 
 
 
 
34
  with rows and tokens, not file bytes, and on 2 vCPU it is *time*, not memory,
35
  that runs out first. Measured on the cpu-basic Space shape (2 vCPU / 16 GB):
36
  upload + parse peaks at roughly 5x file size, so even a 50 MB corpus costs
@@ -43,7 +55,7 @@ unauthenticated request cannot make the server parse a large file only to
43
  reject it at row 201.
44
 
45
  Embedding throughput at 2 threads, batch 64 (see `scripts/bench_models.py` to
46
- re-measure on the actual host these are derated estimates, not Space-measured):
47
 
48
  | model | ~15-word rows | ~60-word rows | ~250-word rows |
49
  | ------------- | ------------- | ------------- | -------------- |
@@ -53,7 +65,7 @@ re-measure on the actual host — these are derated estimates, not Space-measure
53
  At `CCR_MAX_ROWS=20000` that is ~25 s to ~3 min for MiniLM, but up to ~40 min
54
  for E5 Large on long documents. Jobs that long are also *fragile*: a Space
55
  restart marks any running job failed (`recover_orphaned_jobs`), so worst-case
56
- job duration not row count alone is the number to keep in view.
57
 
58
  Retention (CCR_ANON_TTL_HOURS=24) and model pre-warm are already defaults in
59
  the Dockerfile.
@@ -75,7 +87,9 @@ students that URL; no files need to be shared out of band.
75
  Google provider form.
76
  4. Authentication > URL Configuration: add BOTH redirect URLs:
77
  - http://127.0.0.1:8000/api/auth/google/callback
78
- - https://devaanand-ccr-platform.hf.space/api/auth/google/callback
 
 
79
  5. Project Settings > API: copy the Project URL and anon key into the Space
80
  secrets (and your local .env).
81
 
@@ -83,13 +97,14 @@ students that URL; no files need to be shared out of band.
83
 
84
  ```bash
85
  git push origin main # GitHub
86
- git push hf main # Hugging Face Space (rebuilds + redeploys)
87
  ```
88
 
89
- The hf remote has no stored token; use your HF username and a WRITE token as
90
- the password when prompted (or a credential helper).
 
91
 
92
- ## Caveats of the free dev instance
93
 
94
  - ~~Ephemeral disk~~ RESOLVED (2026-08-06, verified): the Space now runs with
95
  `DATABASE_URL` (Supabase Postgres) and the `CCR_STORAGE=s3` R2 secrets set,
@@ -97,9 +112,9 @@ the password when prompted (or a credential helper).
97
  restarts. The paragraph below ("Persistent storage") documents that setup
98
  for anyone redeploying from scratch. Without those secrets, the old caveat
99
  applies: SQLite + local files reset on every rebuild.
100
- - The Space sleeps after ~48 h idle; first visit wakes it (~1 min). This is
101
- the remaining free-tier limitation - an always-on tier or host arrives with
102
- the production launch.
103
 
104
  ## Persistent storage (make accounts/data survive restarts)
105
 
 
1
+ # Deploying the lab instance (Hugging Face Space)
2
 
3
  The Space builds from this repo's Dockerfile. One-time setup lives in the
4
+ Space settings; after that, deploys are just `git push prod main`.
5
+
6
+ This repo deploys to the Space `Culture-and-Morality-Lab/ccr-platform`, which
7
+ is served to the public at https://psychologicaltextanalysis.com through the
8
+ Cloudflare Worker in `deploy/reverse-proxy-worker.js` (Spaces cannot hold a
9
+ custom domain on any tier). Account-by-account setup and the migration from the
10
+ personal dev stack live in `deploy/PRODUCTION_RUNBOOK.md`.
11
 
12
  ## Space settings (Settings > Variables and secrets)
13
 
14
  Hugging Face keeps **Variables** and **Secrets** in two separate stores, and a
15
  name defined in BOTH puts the Space into `CONFIG_ERROR` ("Collision on
16
  variables and secrets names") before it even builds. Add each key below to one
17
+ store only - if the Space reports a config error after a settings change, look
18
  for a duplicated name first, not a bad value.
19
 
20
+ Secrets (credentials - encrypted, write-only once set):
21
 
22
  | Secret | Value |
23
  | ------------------ | ----------------------------------------------------------- |
 
26
  | SUPABASE_ANON_KEY | from the same page (anon public key, NOT service_role) |
27
  | DATABASE_URL | Supabase session-pooler URI (see persistent storage below) |
28
 
29
+ Variables (non-sensitive tuning - visible in settings, safe to edit):
30
 
31
  | Variable | Value |
32
  | ---------------------- | --------------------------------------------------- |
33
+ | CCR_APP_URL | https://psychologicaltextanalysis.com |
34
  | CCR_COOKIE_SECURE | 1 |
35
  | CCR_MAX_ROWS | 20000 (global row ceiling; code default is 100000) |
36
  | CCR_MAX_UPLOAD_BYTES | optional; code default is 52428800 (50 MB) |
37
  | CCR_ANON_MAX_BYTES | optional; code default is 5242880 (5 MB) |
38
 
39
+ Paste `CCR_APP_URL` with no trailing space or newline. The app builds the
40
+ Google sign-in return URL from it, so a stray newline arrives at Supabase as
41
+ `%0A` inside `redirect_to`: the redirect stops matching the allow list and
42
+ browsers flag the link as dangerous. The app trims the value since 2026-08-17,
43
+ but older deployments and any other URL variable still take it literally.
44
+
45
+ `CCR_MAX_ROWS` is the limit that actually bounds a run - embedding cost scales
46
  with rows and tokens, not file bytes, and on 2 vCPU it is *time*, not memory,
47
  that runs out first. Measured on the cpu-basic Space shape (2 vCPU / 16 GB):
48
  upload + parse peaks at roughly 5x file size, so even a 50 MB corpus costs
 
55
  reject it at row 201.
56
 
57
  Embedding throughput at 2 threads, batch 64 (see `scripts/bench_models.py` to
58
+ re-measure on the actual host - these are derated estimates, not Space-measured):
59
 
60
  | model | ~15-word rows | ~60-word rows | ~250-word rows |
61
  | ------------- | ------------- | ------------- | -------------- |
 
65
  At `CCR_MAX_ROWS=20000` that is ~25 s to ~3 min for MiniLM, but up to ~40 min
66
  for E5 Large on long documents. Jobs that long are also *fragile*: a Space
67
  restart marks any running job failed (`recover_orphaned_jobs`), so worst-case
68
+ job duration - not row count alone - is the number to keep in view.
69
 
70
  Retention (CCR_ANON_TTL_HOURS=24) and model pre-warm are already defaults in
71
  the Dockerfile.
 
87
  Google provider form.
88
  4. Authentication > URL Configuration: add BOTH redirect URLs:
89
  - http://127.0.0.1:8000/api/auth/google/callback
90
+ - https://psychologicaltextanalysis.com/api/auth/google/callback
91
+ Each entry must match what the app sends byte for byte, so keep the custom
92
+ domain (not the hf.space host) here once the Worker is live.
93
  5. Project Settings > API: copy the Project URL and anon key into the Space
94
  secrets (and your local .env).
95
 
 
97
 
98
  ```bash
99
  git push origin main # GitHub
100
+ git push prod main # Hugging Face Space (rebuilds + redeploys)
101
  ```
102
 
103
+ The prod remote has no stored token; use the lab HF username and a WRITE token
104
+ as the password when prompted (or a credential helper). Pushing to origin alone
105
+ changes nothing on the deployed instance.
106
 
107
+ ## Caveats of the deployed instance
108
 
109
  - ~~Ephemeral disk~~ RESOLVED (2026-08-06, verified): the Space now runs with
110
  `DATABASE_URL` (Supabase Postgres) and the `CCR_STORAGE=s3` R2 secrets set,
 
112
  restarts. The paragraph below ("Persistent storage") documents that setup
113
  for anyone redeploying from scratch. Without those secrets, the old caveat
114
  applies: SQLite + local files reset on every rebuild.
115
+ - The lab Space runs on cpu-upgrade hardware, so it does not sleep. On the
116
+ free cpu-basic tier a Space sleeps after ~48 h idle and the first visit
117
+ wakes it (~1 min), which is still the behaviour of the personal dev Space.
118
 
119
  ## Persistent storage (make accounts/data survive restarts)
120
 
_to_delete/git-index.lock.stale ADDED
File without changes
_to_delete/git-index.lock.stale2 ADDED
File without changes
backend/app/auth_google.py CHANGED
@@ -48,8 +48,15 @@ def _supabase_url() -> str:
48
 
49
  def app_url() -> str:
50
  """Public base URL of THIS app (redirect target). Local default matches
51
- the dev server; deployments set CCR_APP_URL."""
52
- return os.environ.get("CCR_APP_URL", "http://127.0.0.1:8000").rstrip("/")
 
 
 
 
 
 
 
53
 
54
 
55
  def begin() -> tuple[str, str]:
 
48
 
49
  def app_url() -> str:
50
  """Public base URL of THIS app (redirect target). Local default matches
51
+ the dev server; deployments set CCR_APP_URL.
52
+
53
+ strip() before rstrip(): a value pasted into a hosting dashboard can carry
54
+ a trailing newline. Untrimmed it rides into redirect_to as %0A, so the URL
55
+ no longer matches the Supabase redirect allow list AND browsers flag the
56
+ link (a raw newline in a URL parameter reads as header injection). Seen in
57
+ production 2026-08-17.
58
+ """
59
+ return os.environ.get("CCR_APP_URL", "http://127.0.0.1:8000").strip().rstrip("/")
60
 
61
 
62
  def begin() -> tuple[str, str]:
backend/tests/test_google_auth.py CHANGED
@@ -41,6 +41,14 @@ def test_login_redirects_to_supabase_with_pkce(client, google_env):
41
  assert auth_google.VERIFIER_COOKIE in resp.cookies
42
 
43
 
 
 
 
 
 
 
 
 
44
  def test_callback_creates_user_and_signs_in(client, google_env, monkeypatch):
45
  monkeypatch.setattr(
46
  auth_google, "exchange",
 
41
  assert auth_google.VERIFIER_COOKIE in resp.cookies
42
 
43
 
44
+ def test_app_url_ignores_pasted_whitespace(monkeypatch):
45
+ """A dashboard-pasted CCR_APP_URL can carry a trailing newline. Untrimmed
46
+ it reaches Supabase as %0A inside redirect_to, so the redirect never
47
+ matches the allow list (production, 2026-08-17)."""
48
+ monkeypatch.setenv("CCR_APP_URL", " https://example.org/\n")
49
+ assert auth_google.app_url() == "https://example.org"
50
+
51
+
52
  def test_callback_creates_user_and_signs_in(client, google_env, monkeypatch):
53
  monkeypatch.setattr(
54
  auth_google, "exchange",
deploy/reverse-proxy-worker.js CHANGED
@@ -44,7 +44,7 @@
44
  * CCR_APP_URL and the Supabase redirect URL above.
45
  */
46
 
47
- const UPSTREAM_HOST = "REPLACE-WITH-owner-space.hf.space";
48
 
49
  export default {
50
  async fetch(request) {
 
44
  * CCR_APP_URL and the Supabase redirect URL above.
45
  */
46
 
47
+ const UPSTREAM_HOST = "culture-and-morality-lab-ccr-platform.hf.space";
48
 
49
  export default {
50
  async fetch(request) {