Skip to content

Commit a396f08

Browse files
author
Ryan
committed
fix(prod): cross-origin auth + lenient CORS env + CLI URL
Backend - Settings.cors_origins now uses Annotated[..., NoDecode] + a field validator so the CORS_ORIGINS env var accepts JSON, comma-separated, or single-value forms instead of erroring with SettingsError. - Add session_cookie_samesite setting; auth callback uses it instead of the hardcoded "lax" so we can set "none" in cross-site prod (dploy.ryantanen.com -> *.modal.run) while keeping "lax" locally. - .env.example documents both new keys. CLI - Bundle config bakes in the stable modal deploy URL (hackprinceton--dploy-backend-fastapi-app.modal.run) instead of the ephemeral modal serve `-dev` URL that regenerates per session. - Bump @ryantanen/dploy to 0.0.5; v0.0.5 tag triggers npm publish via cli-release.yml. Made-with: Cursor
1 parent e32d8f0 commit a396f08

5 files changed

Lines changed: 33 additions & 16 deletions

File tree

backend/.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,20 @@ GITHUB_REDIRECT_URI=http://localhost:8000/api/v1/auth/github/callback
1717
# Where to redirect the user after a successful login.
1818
FRONTEND_URL=http://localhost:5173
1919

20-
# Session cookie. Set SESSION_COOKIE_SECURE=true behind HTTPS.
20+
# Session cookie.
21+
# - SESSION_COOKIE_SECURE=true behind HTTPS.
22+
# - SESSION_COOKIE_SAMESITE: "lax" for same-site (local dev, single-origin
23+
# prod), "none" when frontend and backend live on different origins
24+
# (e.g. dploy.ryantanen.com → *.modal.run). "none" requires SECURE=true.
2125
SESSION_COOKIE_NAME=dploy_session
2226
SESSION_COOKIE_SECURE=false
27+
SESSION_COOKIE_SAMESITE=lax
2328
SESSION_TTL_HOURS=168
2429

30+
# CORS allowlist for the frontend origin(s). JSON list. Required when the
31+
# frontend is on a different origin from the backend.
32+
# CORS_ORIGINS=["https://dploy.ryantanen.com","http://localhost:5173"]
33+
2534
# Used to sign short-lived values like the OAuth `state`. Use a long random
2635
# string in production: `python -c "import secrets; print(secrets.token_urlsafe(48))"`
2736
SESSION_SECRET=dev-only-change-me

backend/app/api/routes/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async def github_callback(
8888
max_age=settings.session_ttl_hours * 3600,
8989
httponly=True,
9090
secure=settings.session_cookie_secure,
91-
samesite="lax",
91+
samesite=settings.session_cookie_samesite,
9292
path="/",
9393
)
9494
return redirect

backend/app/core/config.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import json
12
from functools import lru_cache
3+
from typing import Annotated
24

3-
from pydantic_settings import BaseSettings, SettingsConfigDict
5+
from pydantic import field_validator
6+
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
47

58

69
class Settings(BaseSettings):
@@ -16,11 +19,25 @@ class Settings(BaseSettings):
1619
api_v1_prefix: str = "/api/v1"
1720
debug: bool = False
1821

19-
cors_origins: list[str] = [
22+
cors_origins: Annotated[list[str], NoDecode] = [
2023
"http://localhost:5173",
2124
"http://localhost:3000",
2225
]
2326

27+
@field_validator("cors_origins", mode="before")
28+
@classmethod
29+
def _parse_cors_origins(cls, v):
30+
if v is None or isinstance(v, list):
31+
return v
32+
if isinstance(v, str):
33+
s = v.strip()
34+
if not s:
35+
return []
36+
if s.startswith("["):
37+
return json.loads(s)
38+
return [origin.strip() for origin in s.split(",") if origin.strip()]
39+
return v
40+
2441
database_url: str = "sqlite+aiosqlite:///./app.db"
2542
db_echo: bool = False
2643

@@ -40,6 +57,7 @@ class Settings(BaseSettings):
4057
# Session cookie.
4158
session_cookie_name: str = "dploy_session"
4259
session_cookie_secure: bool = False
60+
session_cookie_samesite: str = "lax" # "lax" | "strict" | "none"
4361
session_ttl_hours: int = 24 * 7
4462

4563
# Used to HMAC-sign short-lived values like the OAuth `state` param. Set

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ryantanen/dploy",
3-
"version": "0.0.4",
3+
"version": "0.0.5",
44
"description": "Vercel-style just-deploy-it CLI. Point dploy at a GitHub repo and get a live URL.",
55
"type": "module",
66
"license": "MIT",

cli/tsup.bundle.config.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
import { defineConfig } from "tsup";
22

3-
// Production bundle config: inlines every npm dep into a single file so the
4-
// release tarball doesn't need `node_modules` on the user's machine. Used by
5-
// CI (`pnpm build:bundle`) — local `pnpm build` keeps deps external for fast
6-
// dev rebuilds.
7-
//
8-
// `react-devtools-core` is statically imported by ink/build/devtools.js but
9-
// only used at runtime when REACT_DEVTOOLS=true. We stub it at build time so
10-
// the CLI doesn't carry ~6MB of devtools client code we never use.
11-
// Production backend URL the published bundle points at by default. Users
12-
// can still override at runtime with `DPLOY_API_URL=...` (see config.ts).
13-
const PROD_API_URL = "https://hackprinceton--dploy-backend-fastapi-app-dev.modal.run";
3+
const PROD_API_URL = "https://hackprinceton--dploy-backend-fastapi-app.modal.run";
144

155
export default defineConfig({
166
entry: ["src/cli.tsx"],

0 commit comments

Comments
 (0)