feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(auth): configurable email-domain allowlist + staging setup templates - #256

Merged
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config
Jun 22, 2026
Merged

feat(auth): configurable email-domain allowlist + staging setup templates#256
AndresL230 merged 1 commit into
mainfrom
feat/staging-email-domain-config

Conversation

@AndresL230

@AndresL230AndresL230 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What

Backend + docs slice of staging wiring (kept separate from the frontend Phase-4 PR so it can merge independently of the currently-red Frontend CI).

Configurable sign-in domain

routes/auth.py hard-rejected any non-@bu.edu account. Staging needs to admit the team's accounts (it's gated by Cloudflare Access anyway), so this replaces the constant with config.ALLOWED_EMAIL_DOMAINS:

  • Comma-separated, default "bu.edu"prod behavior unchanged.
  • Empty value = allow any domain (safe on staging behind Access).
  • Logic extracted to _email_domain_allowed() with unit tests.

Operator templates

  • backend/.env.staging.example — placeholder env sheet (no real secrets; copy to gitignored .env.staging).
  • docs/staging/setup-checklist.md — the dashboard runbook: what to do per service, which secret goes where, and when to create which URL/DNS record.
  • .gitignore!.env.staging.example so the template is tracked.

Testing

pytest tests/test_auth_domain.py — 4 passed (default bu.edu, case-insensitivity, multi-domain, empty=any).

Relates to #246 (plan). Backend-only; the Frontend CI check (red repo-wide for unrelated lockfile/eslint reasons) does not exercise this code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Email domain allowlist for Google sign-in is now configurable per environment, replacing hard-coded restrictions. Empty configuration allows any domain.
  • Documentation

    • Added comprehensive staging environment setup guide with step-by-step configuration instructions, security best practices, and reference tables for secret management.

…plates
- ALLOWED_EMAIL_DOMAINS (comma-separated, default 'bu.edu', empty = any) replaces
the hardcoded @bu.edu gate in the OAuth callback, so staging can admit the team's
accounts (it is already gated by Cloudflare Access). Prod behavior unchanged.
- Extract _email_domain_allowed() helper + unit tests.
- Add backend/.env.staging.example (placeholder template) and
docs/staging/setup-checklist.md (operator runbook: secrets→where, URLs→when).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 22, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend43c6bc6Jun 22 2026, 05:27 AM

@coderabbitai

coderabbitaiBot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hard-coded @bu.edu domain restriction in Google OAuth sign-in is replaced with a configurable ALLOWED_EMAIL_DOMAINS allowlist parsed from an environment variable. A staging environment template file and operator setup checklist are also added, with the example file explicitly unignored in .gitignore.

Changes

Configurable email domain allowlist

Layer / File(s)Summary
ALLOWED_EMAIL_DOMAINS config constant
backend/config.py
Adds ALLOWED_EMAIL_DOMAINS parsed from an env var: comma-split, whitespace-trimmed, leading @ stripped, lowercased, empty entries filtered. Defaults to ["bu.edu"].
_email_domain_allowed helper, callback update, and tests
backend/routes/auth.py, backend/tests/test_auth_domain.py
Imports the config constant, adds _email_domain_allowed(email) that allows any domain when the list is empty, and replaces the email.endswith("@bu.edu") check in /google/callback with that helper. Four tests cover default, case-insensitive, multi-domain, and empty-list behavior.

Staging environment template and operator checklist

Layer / File(s)Summary
Staging env template, .gitignore, and operator checklist
.gitignore, backend/.env.staging.example, docs/staging/setup-checklist.md
Adds !.env.staging.example negation to .gitignore, populates the committed template with placeholder values and inline documentation for all staging env vars, and adds a full operator setup checklist covering Supabase, Google OAuth, Railway, Cloudflare, secrets placement tables, URL timing, and security requirements.

Sequence Diagram(s)

sequenceDiagram
participant Browser
participant callback as /google/callback
participant _email_domain_allowed
participant config as ALLOWED_EMAIL_DOMAINS
Browser->>callback: GET /google/callback?code=...
callback->>_email_domain_allowed: email from Google token
_email_domain_allowed->>config: read domain list
alt list is empty (staging open mode)
config-->>_email_domain_allowed: []
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain matches list entry
config-->>_email_domain_allowed: ["bu.edu", ...]
_email_domain_allowed-->>callback: True
callback-->>Browser: set session, redirect to app
else domain not in list
_email_domain_allowed-->>callback: False
callback-->>Browser: redirect ?error=invalid_domain
end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A list of domains, not just one,
The allowlist hops under the sun.
Empty means open — any friend may pass,
Or lock it to bu.edu like it was.
Staging secrets live in example files,
The rabbit checks them, hops for miles! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 42.86% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the main changes: configurable email-domain allowlist for authentication and staging setup templates.
Description check✅ PassedThe PR description covers the main changes, testing performed, and relates to issue #246, but does not fully follow the provided template structure.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staging-email-domain-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/staging/setup-checklist.md (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Forward reference to seed_staging.py noted as not yet landed.

Line 54 mentions "once seed_staging.py lands" but does not currently exist in the codebase. Operators following this checklist will encounter an error at Step 6 seed step until that file is created. Consider updating the checklist or creating a placeholder task once seed_staging.py is available.

Do you want me to help create a TODO issue or placeholder seed_staging.py to unblock this step?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` at line 54, The checklist item at line 54
references a conditional action dependent on seed_staging.py landing, but this
file does not currently exist in the codebase. To unblock operators following
this checklist, either update the checklist item to remove the conditional
clause and provide concrete seeding instructions that work with the current
state of the repository, or create the seed_staging.py file with appropriate
placeholder content and update the checklist accordingly to reference it as an
available resource. Choose the approach that best aligns with your project's
current development status and timeline for adding the seed script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/config.py`:
- Around line 25-33: Add a startup validation check in the validate_config()
function to guard against accidentally allowing any email domain outside
staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is empty and raise an
error unless the current environment is explicitly staging or local. This
prevents the silent security risk where an empty ALLOWED_EMAIL_DOMAINS
configuration results in _email_domain_allowed() accepting any domain when it
should be restricted in production.
In `@docs/staging/setup-checklist.md`:
- Around line 42-44: The checklist contains a false claim that the
`[env.staging]` block is already present in `frontend/wrangler.toml`. Remove the
phrase "already in the repo (Phase 4)" from the line 42 statement, and instead
add a new checklist item or instruction that explicitly directs operators to
create or verify the `[env.staging]` configuration block exists in
`frontend/wrangler.toml` before attempting to run the `npm run
cf:deploy:staging` command. This ensures operators have the required staging
environment configuration in place before deployment.
---
Nitpick comments:
In `@docs/staging/setup-checklist.md`:
- Line 54: The checklist item at line 54 references a conditional action
dependent on seed_staging.py landing, but this file does not currently exist in
the codebase. To unblock operators following this checklist, either update the
checklist item to remove the conditional clause and provide concrete seeding
instructions that work with the current state of the repository, or create the
seed_staging.py file with appropriate placeholder content and update the
checklist accordingly to reference it as an available resource. Choose the
approach that best aligns with your project's current development status and
timeline for adding the seed script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e6b4912-a0df-426a-ae88-ae3cfacd82b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4521305 and 43c6bc6.

📒 Files selected for processing (6)
  • .gitignore
  • backend/.env.staging.example
  • backend/config.py
  • backend/routes/auth.py
  • backend/tests/test_auth_domain.py
  • docs/staging/setup-checklist.md

Comment threadbackend/config.py
Comment on lines +25 to +33
# Sign-in email-domain allowlist. Comma-separated; empty value = allow any domain.
# Default preserves prod's @bu.edu-only behavior. Staging can widen this (e.g.
# "bu.edu,saplinglearn.com") or set it empty to allow any Google account — safe
# on staging because Cloudflare Access already gates who reaches the app at all.
ALLOWED_EMAIL_DOMAINS = [
d.strip().lstrip("@").lower()
for d in os.getenv("ALLOWED_EMAIL_DOMAINS", "bu.edu").split(",")
if d.strip()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent accidental “allow any domain” outside staging/local.

An empty ALLOWED_EMAIL_DOMAINS currently becomes [], and _email_domain_allowed() then allows any domain. If this env var is accidentally blank in a non-staging environment, domain restriction is silently disabled. Add a startup guard in validate_config() so empty allowlist is only permitted where intended.

Suggested hardening
 def validate_config() -> None:
@@
if not GEMINI_API_KEY:
missing.append("GEMINI_API_KEY")
+ if APP_ENV.lower() not in {"local", "staging"} and not ALLOWED_EMAIL_DOMAINS:+ missing.append("ALLOWED_EMAIL_DOMAINS (must be non-empty outside local/staging)")
if not IS_LOCAL and len((SESSION_SECRET or "").strip().encode("utf-8")) < 32:
missing.append("SESSION_SECRET (must be set and >= 32 bytes)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/config.py` around lines 25 - 33, Add a startup validation check in
the validate_config() function to guard against accidentally allowing any email
domain outside staging/local environments. Check if ALLOWED_EMAIL_DOMAINS is
empty and raise an error unless the current environment is explicitly staging or
local. This prevents the silent security risk where an empty
ALLOWED_EMAIL_DOMAINS configuration results in _email_domain_allowed() accepting
any domain when it should be restricted in production.

Comment on lines +42 to +44
- [ ] The `[env.staging]` block in `frontend/wrangler.toml` is already in the repo (Phase 4). Deploy: `cd frontend && npm run cf:deploy:staging` → publishes a `frontend-staging` worker.
- [ ] Add the worker secrets for the Access hop (Step 5):
`wrangler secret put CF_ACCESS_CLIENT_ID --env staging` and `... CF_ACCESS_CLIENT_SECRET --env staging`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Verify that frontend/wrangler.toml contains the [env.staging] block.
grep -A5 '\[env\.staging\]' frontend/wrangler.toml 2>/dev/null &&echo"✓ Found"||echo"✗ Not found or file missing"

Repository: SaplingLearn/Sapling

Length of output: 92


🏁 Script executed:

# Check if frontend/wrangler.toml exists and examine its contentsif [ -f frontend/wrangler.toml ];thenecho"=== frontend/wrangler.toml exists ==="
cat frontend/wrangler.toml
elseecho"=== frontend/wrangler.toml does NOT exist ==="fi

Repository: SaplingLearn/Sapling

Length of output: 736


🏁 Script executed:

# Read the specific lines from the checklist to confirm contentif [ -f docs/staging/setup-checklist.md ];thenecho"=== Lines 40-50 from setup-checklist.md ==="
sed -n '40,50p' docs/staging/setup-checklist.md
elseecho"=== File not found ==="fi

Repository: SaplingLearn/Sapling

Length of output: 1046


🏁 Script executed:

# Search entire repo for [env.staging] referencesecho"=== Searching for [env.staging] in repo ==="
rg '\[env\.staging\]' --type-list | head -20
rg '\[env\.staging\]'2>/dev/null ||echo"No matches found"

Repository: SaplingLearn/Sapling

Length of output: 734


Remove or correct the false claim that [env.staging] is already in the repo.

Line 42 states the [env.staging] block is "already in the repo (Phase 4)", but it does not exist in frontend/wrangler.toml. Either add the configuration block to the file or update the checklist to include instructions for creating it before running the deploy command. As written, the checklist will fail when operators attempt to deploy to a non-existent staging environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/staging/setup-checklist.md` around lines 42 - 44, The checklist contains
a false claim that the `[env.staging]` block is already present in
`frontend/wrangler.toml`. Remove the phrase "already in the repo (Phase 4)" from
the line 42 statement, and instead add a new checklist item or instruction that
explicitly directs operators to create or verify the `[env.staging]`
configuration block exists in `frontend/wrangler.toml` before attempting to run
the `npm run cf:deploy:staging` command. This ensures operators have the
required staging environment configuration in place before deployment.

@AndresL230
AndresL230 merged commit 2e8ae9b into mainJun 22, 2026
4 of 6 checks passed
@AndresL230
AndresL230 deleted the feat/staging-email-domain-config branch June 27, 2026 04:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AndresL230