Skip to content

Commit dab9666

Browse files
ComBbaclaude
andcommitted
fix: critical ingress routing + production hardening
Critical fix: - Remove /dashboard and /zero-prompt from API ingress rules. These paths are Next.js pages that need auth middleware protection. Frontend accesses API sub-routes via /api/ prefix already. Hardening: - Add trustHost:true to NextAuth config (reverse proxy support) - Read approval from DB on sign-in instead of deriving from domain (prevents 5-min access window for revoked users) - Use consistent email domain extraction (rsplit equivalent) - Use shared _PROTECTED_APP_NAMES constant in delete endpoint - Export UserMenu from shared barrel - Add web/.env.example documenting required env vars Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8d897d8 commit dab9666

5 files changed

Lines changed: 36 additions & 17 deletions

File tree

.do/app.yaml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,9 @@ ingress:
3434
match:
3535
path:
3636
prefix: /health
37-
- component:
38-
name: api
39-
match:
40-
path:
41-
prefix: /zero-prompt
42-
- component:
43-
name: api
44-
match:
45-
path:
46-
prefix: /dashboard
37+
# /dashboard and /zero-prompt are Next.js pages (auth-protected).
38+
# Their API sub-routes (/dashboard/stats, /zero-prompt/start, etc.)
39+
# are accessed via /api/ prefix by the frontend (DASHBOARD_API_URL).
4740
# Everything else → frontend (Next.js handles its own routing)
4841
- component:
4942
name: web

agent/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,7 +2070,7 @@ async def dashboard_auth_check_user(email: str):
20702070
@app.get("/dashboard/apps")
20712071
async def dashboard_list_apps():
20722072
"""List all live DigitalOcean apps with age and status."""
2073-
from .tools.digitalocean import list_apps
2073+
from .tools.digitalocean import _PROTECTED_APP_NAMES, list_apps
20742074

20752075
apps = await list_apps()
20762076
result = []
@@ -2086,15 +2086,15 @@ async def dashboard_list_apps():
20862086
"phase": active.get("phase", "UNKNOWN"),
20872087
"created_at": app.get("created_at", ""),
20882088
"updated_at": app.get("updated_at", ""),
2089-
"protected": name in {"vibedeploy"},
2089+
"protected": name in _PROTECTED_APP_NAMES,
20902090
})
20912091
return result
20922092

20932093

20942094
@app.delete("/dashboard/apps/{app_id}")
20952095
async def dashboard_delete_app(app_id: str):
20962096
"""Delete a specific generated app. Refuses to delete the production app."""
2097-
from .tools.digitalocean import delete_app, list_apps
2097+
from .tools.digitalocean import _PROTECTED_APP_NAMES, delete_app, list_apps
20982098

20992099
apps = await list_apps()
21002100
target = None
@@ -2106,7 +2106,7 @@ async def dashboard_delete_app(app_id: str):
21062106
raise HTTPException(status_code=404, detail="app_not_found")
21072107

21082108
name = target.get("spec", {}).get("name", "")
2109-
if name in {"vibedeploy"}:
2109+
if name in _PROTECTED_APP_NAMES:
21102110
raise HTTPException(status_code=403, detail="cannot_delete_production_app")
21112111

21122112
result = await delete_app(app_id)

web/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# === Google OAuth (required for authentication) ===
2+
GOOGLE_CLIENT_ID=
3+
GOOGLE_CLIENT_SECRET=
4+
5+
# === NextAuth.js (required) ===
6+
AUTH_SECRET= # Generate with: npx auth secret
7+
AUTH_URL=http://localhost:9001
8+
9+
# === Agent API ===
10+
NEXT_PUBLIC_AGENT_URL=http://localhost:8080
11+
VIBEDEPLOY_API_KEY= # Server-side only API key for agent backend

web/src/components/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { ErrorBoundary } from "./error-boundary";
2+
export { UserMenu } from "./user-menu";

web/src/lib/auth.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DASHBOARD_API_URL } from "@/lib/api";
66
const FIVE_MINUTES_MS = 5 * 60 * 1000;
77

88
export const { handlers, signIn, signOut, auth } = NextAuth({
9+
trustHost: true,
910
providers: [
1011
Google({
1112
clientId: process.env.GOOGLE_CLIENT_ID!,
@@ -40,11 +41,24 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
4041
},
4142

4243
async jwt({ token, user, trigger }) {
43-
// On initial sign-in, extract domain and set approval
44+
// On initial sign-in, extract domain and read approval from DB
4445
if (trigger === "signIn" && user?.email) {
45-
const domain = user.email.split("@")[1] ?? "";
46+
const domain = user.email.split("@").pop() ?? "";
4647
token.domain = domain;
47-
token.approved = domain === "2weeks.co";
48+
// Read actual approval status from DB instead of deriving from domain
49+
try {
50+
const res = await authenticatedFetch(
51+
`${DASHBOARD_API_URL}/dashboard/auth/check-user?email=${encodeURIComponent(user.email)}`,
52+
);
53+
if (res.ok) {
54+
const data = await res.json();
55+
token.approved = Boolean(data.approved);
56+
} else {
57+
token.approved = domain === "2weeks.co";
58+
}
59+
} catch {
60+
token.approved = domain === "2weeks.co";
61+
}
4862
token.approvedCheckedAt = Date.now();
4963
}
5064

0 commit comments

Comments
 (0)