fix(admin-web): proxy routes + pages use cookie-based auth

- Add getCurrentUserFromRequest() to auth-server.ts (checks cookies first, then Authorization header)
- Update all 34 proxy routes: getCurrentUser(req.headers.get('authorization')) → getCurrentUserFromRequest(req)
- Add createProxyFetch() shared helper in lib/proxy-fetch.ts (injects auth + product-id headers)
- Update 15 admin pages: replace inline fetch helpers with createProxyFetch
- Root cause: newer pages used bare fetch() without Authorization headers, causing 401s on all proxy routes
This commit is contained in:
saravanakumardb1 2026-03-21 20:31:30 -07:00
parent f9fa583cae
commit ce0074d6ee
69 changed files with 481 additions and 324 deletions

View File

@ -1,9 +1,9 @@
Last refresh: 2026-03-21T06:00:10Z (2026-03-20 23:00:10 PDT)
Cascade conversations: 50 (411M)
Memories: 91
Last refresh: 2026-03-21T21:44:43Z (2026-03-21 14:44:44 PDT)
Cascade conversations: 50 (387M)
Memories: 95
Implicit context: 20
Code tracker dirs: 132
File edit history: 3365 entries
Workspace storage: 35 workspaces
Code tracker dirs: 89
File edit history: 3457 entries
Workspace storage: 36 workspaces
Repo docs: 7 files across 2 repos
Repo workflows: 43 files across 10 repos

View File

@ -5,7 +5,6 @@ Product-specific workflows for ChronoMind AI-powered contextual clock.
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -9,16 +9,16 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows.
## Covered Repos (All 8 workspaces)
| Repo | Product | Workflows | Docs |
| ----------------------------------- | ---------------- | --------- | ---- |
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
| Repo | Product | Workflows | Docs |
|------|---------|-----------|------|
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
## Steps

View File

@ -8,16 +8,16 @@ Regenerates all 8 AI agent configuration files across all repos in the workspace
## Files Generated Per Repo
| File | Tool |
| --------------------------------- | ----------------------------------------------- |
| `AGENTS.md` | Universal (OpenAI Codex, Claude, Copilot, etc.) |
| `CLAUDE.md` | Claude Code |
| `.cursorrules` | Cursor AI |
| `.github/copilot-instructions.md` | GitHub Copilot |
| `.windsurfrules` | Windsurf / Cascade |
| `.clinerules` | Cline / Roo Code |
| `.aider.conf.yml` | Aider |
| `.editorconfig` | All editors |
| File | Tool |
|------|------|
| `AGENTS.md` | Universal (OpenAI Codex, Claude, Copilot, etc.) |
| `CLAUDE.md` | Claude Code |
| `.cursorrules` | Cursor AI |
| `.github/copilot-instructions.md` | GitHub Copilot |
| `.windsurfrules` | Windsurf / Cascade |
| `.clinerules` | Cline / Roo Code |
| `.aider.conf.yml` | Aider |
| `.editorconfig` | All editors |
## Steps

View File

@ -5,7 +5,6 @@ Product-specific workflows for NomGap fasting visualization app.
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -9,16 +9,16 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows.
## Covered Repos (All 8 workspaces)
| Repo | Product | Workflows | Docs |
| ----------------------------------- | ---------------- | --------- | ---- |
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
| Repo | Product | Workflows | Docs |
|------|---------|-----------|------|
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
## Steps

View File

@ -5,7 +5,6 @@ Product-specific workflows for FlowMonk, the agent-first planning and execution
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -5,7 +5,6 @@ Product-specific workflows for JarvisJr voice coaching app.
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -9,16 +9,16 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows.
## Covered Repos (All 8 workspaces)
| Repo | Product | Workflows | Docs |
| ----------------------------------- | ---------------- | --------- | ---- |
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
| Repo | Product | Workflows | Docs |
|------|---------|-----------|------|
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
## Steps

View File

@ -9,17 +9,14 @@ The primary interface for running audits is the **Sensor Audit Dashboard**.
## Steps
1. **Ensure the dashboard is running**
```bash
/Users/sd9235/code/mygh/learning_ai_mac_tooling/audit-ctl.sh start
```
2. **Open the dashboard**
```bash
/Users/sd9235/code/mygh/learning_ai_mac_tooling/audit-ctl.sh open
```
Navigate to **Run Audit** in the sidebar.
3. **Choose a mode in the dashboard UI**:

View File

@ -5,7 +5,6 @@ Product-specific workflows for PeakPulse sensor-driven adventure tracker.
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -19,12 +19,12 @@ cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodegen generate
3. Build all iOS targets:
```bash
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 16' -quiet build
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 17' -quiet build
```
4. Run unit tests:
```bash
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 16' -quiet test
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 17' -quiet test
```
5. Verify no print() statements in production code:
@ -51,9 +51,9 @@ cat /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/PeakPulse.entitl
cat /Users/sd9235/code/mygh/learning_ai_peakpulse/.env.example
```
9. Run platform-service peak-sessions tests:
9. Run backend tests:
```bash
cd /Users/sd9235/code/mygh/learning_ai_common_plat && npx vitest run services/platform-service/src/modules/peak-sessions/peak-sessions.test.ts
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/backend && npm test
```
10. Verify CI workflow exists and is valid:

View File

@ -9,16 +9,16 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows.
## Covered Repos (All 8 workspaces)
| Repo | Product | Workflows | Docs |
| ----------------------------------- | ---------------- | --------- | ---- |
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
| Repo | Product | Workflows | Docs |
|------|---------|-----------|------|
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
## Steps

View File

@ -5,7 +5,6 @@ Product-specific workflows for MindLyst role-based life OS.
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -9,16 +9,16 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows.
## Covered Repos (All 8 workspaces)
| Repo | Product | Workflows | Docs |
| ----------------------------------- | ---------------- | --------- | ---- |
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
| Repo | Product | Workflows | Docs |
|------|---------|-----------|------|
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
## Steps

View File

@ -49,7 +49,6 @@ See `iosApp/README_SETUP.md` for more details.
### 4. Verify Gradle/KMP builds
// turbo
```bash
cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
```
@ -67,7 +66,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosArm64
```
For simulator testing:
```bash
cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
```
@ -75,7 +73,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
### 2. Embed the shared framework in Xcode
If not already linked:
```bash
cd mindlyst-native && ./gradlew :shared:embedAndSignAppleFrameworkForXcode
```
@ -90,7 +87,6 @@ Or manually: open Xcode → Target → General → Build.
### 4. Clean build folder
// turbo
```bash
xcodebuild clean -project mindlyst-native/MindLyst.xcodeproj -scheme MindLyst -configuration Release 2>&1 | tail -3
```
@ -144,24 +140,24 @@ The `app-store-connect` export method auto-uploads the IPA.
### Key Paths
| Path | Purpose |
| ------------------------------------------- | ---------------------------------------- |
| `mindlyst-native/iosApp/` | Swift UI source files |
| `mindlyst-native/shared/` | KMP shared module (business logic) |
| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) |
| `mindlyst-native/gradle/libs.versions.toml` | Version catalog |
| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) |
| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload |
| Path | Purpose |
|------|---------|
| `mindlyst-native/iosApp/` | Swift UI source files |
| `mindlyst-native/shared/` | KMP shared module (business logic) |
| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) |
| `mindlyst-native/gradle/libs.versions.toml` | Version catalog |
| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) |
| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload |
### Build Identity
| Field | Value |
| ------------- | ----------------------- |
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.mindlyst.MindLyst` |
| Signing | Automatic |
| KMP Framework | `shared` (static) |
| Min iOS | 16.0 |
| Field | Value |
|-------|-------|
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.mindlyst.MindLyst` |
| Signing | Automatic |
| KMP Framework | `shared` (static) |
| Min iOS | 16.0 |
### KMP Architecture
@ -177,12 +173,12 @@ All business logic lives in `shared/src/commonMain/`. iOS code in `iosApp/` is a
### Troubleshooting
| Problem | Fix |
| ----------------------------- | ------------------------------------------------------------------------ |
| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| "Build number already exists" | Increment build number in step 3 |
| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` |
| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` |
| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
| Problem | Fix |
|---------|-----|
| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| "Build number already exists" | Increment build number in step 3 |
| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` |
| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` |
| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |

View File

@ -5,7 +5,6 @@ Product-specific workflows for LysnrAI voice-to-text dictation platform.
## Repo-management Workflows
Cross-repo workflows are maintained centrally in `learning_ai_common_plat/.windsurf/workflows/`:
- `/repo_sync-repos` — Pull latest from all repos
- `/repo_commit-workspace` — Commit changes across repos
- `/repo_backup-main-branch` — Backup main branches

View File

@ -9,16 +9,16 @@ Auto-discovers new repos, updates symlinks, and re-copies docs + workflows.
## Covered Repos (All 8 workspaces)
| Repo | Product | Workflows | Docs |
| ----------------------------------- | ---------------- | --------- | ---- |
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
| Repo | Product | Workflows | Docs |
|------|---------|-----------|------|
| `learning_voice_ai_agent` | LysnrAI | ✅ | ✅ |
| `learning_multimodal_memory_agents` | MindLyst | ✅ | ✅ |
| `learning_ai_clock` | ChronoMind | ✅ | — |
| `learning_ai_peakpulse` | PeakPulse | ✅ | — |
| `learning_ai_fastgap` | NomGap | ✅ | — |
| `learning_ai_jarvis_jr` | JarvisJr | ✅ | — |
| `learning_ai_common_plat` | Common Platform | ✅ | — |
| `learning_agent_monitoring_fx` | Agent Monitoring | ✅ | — |
## Steps

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import {
FlaskConical,
@ -67,13 +69,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/agent-evals/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/agent-evals');
export default function AgentEvalsPage() {
const [suites, setSuites] = useState<EvalSuite[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { Coins, Plus, MoreHorizontal, AlertTriangle, TrendingUp, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -73,13 +75,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/ai-budgets/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/ai-budgets');
export default function AIBudgetsPage() {
const [policies, setPolicies] = useState<BudgetPolicy[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { Mail, RotateCcw } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -49,13 +51,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/delivery/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/delivery');
export default function DeliveryPage() {
const [entries, setEntries] = useState<DeliveryEntry[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { Radio, Plus, MoreHorizontal, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -47,13 +49,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/event-subscriptions/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/event-subscriptions');
export default function EventSubscriptionsPage() {
const [subs, setSubs] = useState<EventSub[]>([]);
@ -65,7 +61,7 @@ export default function EventSubscriptionsPage() {
const loadData = useCallback(async () => {
setLoading(true);
const data = await apiFetch('list');
const data = await apiFetch('');
setSubs(
Array.isArray(data?.subscriptions) ? data.subscriptions : Array.isArray(data) ? data : []
);
@ -78,7 +74,7 @@ export default function EventSubscriptionsPage() {
async function handleCreate() {
setCreating(true);
await apiFetch('create', {
await apiFetch('', {
method: 'POST',
body: JSON.stringify({ eventType: newEvent, callbackUrl: newUrl }),
});

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { ShieldBan, Plus, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -48,13 +50,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/ip-rules/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/ip-rules');
export default function IpRulesPage() {
const [rules, setRules] = useState<IpRule[]>([]);
@ -67,7 +63,7 @@ export default function IpRulesPage() {
const loadData = useCallback(async () => {
setLoading(true);
const data = await apiFetch('list');
const data = await apiFetch('');
setRules(Array.isArray(data?.rules) ? data.rules : Array.isArray(data) ? data : []);
setLoading(false);
}, []);
@ -78,7 +74,7 @@ export default function IpRulesPage() {
async function handleCreate() {
setCreating(true);
await apiFetch('create', {
await apiFetch('', {
method: 'POST',
body: JSON.stringify({ cidr: newCidr, type: newType, reason: newReason || undefined }),
});

View File

@ -19,6 +19,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { createProxyFetch } from '@/lib/proxy-fetch';
interface Job {
id: string;
@ -58,21 +59,8 @@ function formatDate(iso: string) {
});
}
async function jobsFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/jobs/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
async function runsFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/runs/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const jobsFetch = createProxyFetch('/api/jobs');
const runsFetch = createProxyFetch('/api/runs');
export default function JobsPage() {
const [jobs, setJobs] = useState<Job[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { BookOpen, Plus, MoreHorizontal, Search, FileText, Trash2, Upload } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -49,13 +51,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/knowledge/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/knowledge');
export default function KnowledgePage() {
const [bases, setBases] = useState<KnowledgeBase[]>([]);

View File

@ -14,6 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { createProxyFetch } from '@/lib/proxy-fetch';
interface MaintenanceStatus {
mode: string;
@ -47,13 +48,7 @@ const modeConfig: Record<string, { label: string; color: string; description: st
},
};
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/maintenance/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/maintenance');
export default function MaintenancePage() {
const [status, setStatus] = useState<MaintenanceStatus>({ mode: 'off' });

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { Store, MoreHorizontal, Search, ThumbsUp, ThumbsDown } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -49,13 +51,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/marketplace/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/marketplace');
export default function MarketplacePage() {
const [listings, setListings] = useState<Listing[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { Building2, Plus, MoreHorizontal, Users, FolderOpen, Search, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -71,13 +73,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/orgs/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/orgs');
export default function OrganizationsPage() {
const [orgs, setOrgs] = useState<Org[]>([]);
@ -98,7 +94,7 @@ export default function OrganizationsPage() {
const loadOrgs = useCallback(async () => {
setLoading(true);
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
const data = await apiFetch(`list${qs}`);
const data = await apiFetch(qs);
if (data?.organizations) {
setOrgs(data.organizations);
setTotal(data.total ?? data.organizations.length);
@ -115,7 +111,7 @@ export default function OrganizationsPage() {
async function handleCreate() {
setCreating(true);
await apiFetch('create', {
await apiFetch('', {
method: 'POST',
body: JSON.stringify({ name: newName, slug: newSlug || undefined }),
});

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react';
import { Star, ThumbsUp, ThumbsDown, MoreHorizontal, Flag } from 'lucide-react';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@ -48,13 +49,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/reviews/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/reviews');
export default function ReviewsPage() {
const [reviews, setReviews] = useState<Review[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { MonitorSmartphone, Search, Globe } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -35,13 +37,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/sessions/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/sessions');
export default function SessionsAdminPage() {
const [sessions, setSessions] = useState<SessionEntry[]>([]);
@ -51,7 +47,7 @@ export default function SessionsAdminPage() {
const loadData = useCallback(async () => {
setLoading(true);
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
const data = await apiFetch(`list${qs}`);
const data = await apiFetch(qs);
setSessions(Array.isArray(data?.sessions) ? data.sessions : Array.isArray(data) ? data : []);
setLoading(false);
}, [search]);
@ -62,7 +58,7 @@ export default function SessionsAdminPage() {
async function handleRevoke(id: string) {
if (!confirm('Revoke this session?')) return;
await apiFetch(`${id}/revoke`, { method: 'POST' });
await apiFetch(`${id}`, { method: 'DELETE' });
loadData();
}

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import {
LifeBuoy,
@ -83,13 +85,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/support/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/support');
export default function SupportCasesPage() {
const [cases, setCases] = useState<SupportCase[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import {
ListOrdered,
@ -65,13 +67,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/waitlist/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/waitlist');
export default function WaitlistPage() {
const [entries, setEntries] = useState<WaitlistEntry[]>([]);

View File

@ -1,5 +1,7 @@
'use client';
import { createProxyFetch } from '@/lib/proxy-fetch';
import { useState, useEffect, useCallback } from 'react';
import { Webhook, Plus, MoreHorizontal, RotateCcw, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -57,13 +59,7 @@ function formatDate(iso: string) {
});
}
async function apiFetch(path: string, opts?: RequestInit) {
const res = await fetch(`/api/webhooks/${path}`, {
headers: { 'Content-Type': 'application/json' },
...opts,
});
return res.json();
}
const apiFetch = createProxyFetch('/api/webhooks');
export default function WebhooksPage() {
const [subs, setSubs] = useState<WebhookSub[]>([]);

View File

@ -1,11 +1,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as actiontrailClient from '@/lib/actiontrail-client';
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/agent-evals/${path.join('/')}`;

View File

@ -5,14 +5,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -8,7 +8,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { getCollection } from '@/lib/datastore';
import { getRequestProductId } from '@/lib/product-config';
interface CohortRow {
@ -37,7 +37,7 @@ function getMondayOfWeek(date: Date): string {
}
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -6,7 +6,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { getCollection } from '@/lib/datastore';
import { getRequestProductId } from '@/lib/product-config';
@ -32,7 +32,7 @@ interface RevenueResponse {
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as platformClient from '@/lib/platform-client';
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/delivery/${path.join('/')}`;

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/event-subscriptions/${path.join('/')}`;
@ -38,6 +38,9 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ path: strin
export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, ctx);
}
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, ctx);
}
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, ctx);
}

View File

@ -0,0 +1,39 @@
/**
* Event Subscriptions base route handles GET /api/event-subscriptions and POST /api/event-subscriptions.
* The [...path] catch-all only matches sub-paths like /api/event-subscriptions/:id.
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxyBase(req: NextRequest) {
try {
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const qs = new URL(req.url).search;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
'x-user-id': caller.id,
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
};
const fetchOptions: RequestInit = { method: req.method, headers };
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
const res = await fetch(`${PLATFORM_URL}/api/event-subscriptions${qs}`, fetchOptions);
const data = await res.json().catch(() => null);
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
} catch (error) {
logError('Event subscriptions base proxy error', error);
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
}
}
export async function GET(req: NextRequest) {
return proxyBase(req);
}
export async function POST(req: NextRequest) {
return proxyBase(req);
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/exports/${path.join('/')}`;

View File

@ -5,18 +5,17 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const EXTRACTION_SERVICE_URL =
process.env.EXTRACTION_SERVICE_URL || 'http://localhost:4005';
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:4005';
async function proxyToExtraction(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
@ -50,37 +49,22 @@ async function proxyToExtraction(
});
} catch (error) {
logError('Extraction proxy error', error);
return NextResponse.json(
{ error: 'Extraction service unavailable' },
{ status: 502 },
);
return NextResponse.json({ error: 'Extraction service unavailable' }, { status: 502 });
}
}
export async function GET(
req: NextRequest,
context: { params: Promise<{ path: string[] }> },
) {
export async function GET(req: NextRequest, context: { params: Promise<{ path: string[] }> }) {
return proxyToExtraction(req, context);
}
export async function POST(
req: NextRequest,
context: { params: Promise<{ path: string[] }> },
) {
export async function POST(req: NextRequest, context: { params: Promise<{ path: string[] }> }) {
return proxyToExtraction(req, context);
}
export async function PUT(
req: NextRequest,
context: { params: Promise<{ path: string[] }> },
) {
export async function PUT(req: NextRequest, context: { params: Promise<{ path: string[] }> }) {
return proxyToExtraction(req, context);
}
export async function DELETE(
req: NextRequest,
context: { params: Promise<{ path: string[] }> },
) {
export async function DELETE(req: NextRequest, context: { params: Promise<{ path: string[] }> }) {
return proxyToExtraction(req, context);
}

View File

@ -5,11 +5,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as growthClient from '@/lib/growth-client';
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || !['super_admin', 'admin'].includes(caller.role)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
@ -40,7 +40,7 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || caller.role !== 'super_admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

View File

@ -5,11 +5,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as growthClient from '@/lib/growth-client';
export async function POST(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || !['super_admin', 'admin'].includes(caller.role)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

View File

@ -5,12 +5,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as growthClient from '@/lib/growth-client';
import crypto from 'crypto';
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
@ -31,7 +31,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || !['super_admin', 'admin'].includes(caller.role)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/ratelimit/ip-rules/${path.join('/')}`;

View File

@ -0,0 +1,39 @@
/**
* IP Rules base route handles GET /api/ip-rules and POST /api/ip-rules.
* Maps to platform-service GET /api/ratelimit/ip-rules and POST /api/ratelimit/ip-rules.
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxyBase(req: NextRequest) {
try {
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const qs = new URL(req.url).search;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
'x-user-id': caller.id,
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
};
const fetchOptions: RequestInit = { method: req.method, headers };
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
const res = await fetch(`${PLATFORM_URL}/api/ratelimit/ip-rules${qs}`, fetchOptions);
const data = await res.json().catch(() => null);
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
} catch (error) {
logError('IP rules base proxy error', error);
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
}
}
export async function GET(req: NextRequest) {
return proxyBase(req);
}
export async function POST(req: NextRequest) {
return proxyBase(req);
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/jobs/${path.join('/')}`;

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/knowledge/${path.join('/')}`;

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/settings/maintenance/${path.join('/')}`;

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/marketplace/${path.join('/')}`;

View File

@ -5,14 +5,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -0,0 +1,39 @@
/**
* Orgs base route handles GET /api/orgs and POST /api/orgs.
* Maps to platform-service GET /api/orgs and POST /api/orgs.
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxyBase(req: NextRequest) {
try {
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const qs = new URL(req.url).search;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
'x-user-id': caller.id,
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
};
const fetchOptions: RequestInit = { method: req.method, headers };
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
const res = await fetch(`${PLATFORM_URL}/api/orgs${qs}`, fetchOptions);
const data = await res.json().catch(() => null);
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
} catch (error) {
logError('Orgs base proxy error', error);
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
}
}
export async function GET(req: NextRequest) {
return proxyBase(req);
}
export async function POST(req: NextRequest) {
return proxyBase(req);
}

View File

@ -6,7 +6,7 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { deactivatePromo } from '@/lib/growth-client';
import { logError } from '@/lib/logger';
@ -14,7 +14,7 @@ type RouteContext = { params: Promise<{ id: string }> };
export async function DELETE(req: NextRequest, ctx: RouteContext) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || !['super_admin', 'admin'].includes(caller.role)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
@ -29,7 +29,7 @@ export async function DELETE(req: NextRequest, ctx: RouteContext) {
export async function PATCH(req: NextRequest, ctx: RouteContext) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || !['super_admin', 'admin'].includes(caller.role)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

View File

@ -8,11 +8,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as growthClient from '@/lib/growth-client';
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
@ -27,7 +27,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller || !['super_admin', 'admin'].includes(caller.role)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

View File

@ -4,11 +4,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as growthClient from '@/lib/growth-client';
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/reviews/${path.join('/')}`;

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/runs/${path.join('/')}`;
@ -38,3 +38,6 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ path: strin
export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, ctx);
}
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
return proxy(req, ctx);
}

View File

@ -0,0 +1,39 @@
/**
* Runs base route handles GET /api/runs and POST /api/runs.
* Maps to platform-service GET /api/runs and POST /api/runs.
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxyBase(req: NextRequest) {
try {
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const qs = new URL(req.url).search;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
'x-user-id': caller.id,
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
};
const fetchOptions: RequestInit = { method: req.method, headers };
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
const res = await fetch(`${PLATFORM_URL}/api/runs${qs}`, fetchOptions);
const data = await res.json().catch(() => null);
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
} catch (error) {
logError('Runs base proxy error', error);
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
}
}
export async function GET(req: NextRequest) {
return proxyBase(req);
}
export async function POST(req: NextRequest) {
return proxyBase(req);
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/sessions/${path.join('/')}`;

View File

@ -0,0 +1,39 @@
/**
* Sessions base route handles GET /api/sessions and DELETE /api/sessions.
* Maps to platform-service GET /api/sessions and DELETE /api/sessions.
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxyBase(req: NextRequest) {
try {
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const qs = new URL(req.url).search;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': req.headers.get('x-request-id') || crypto.randomUUID(),
'x-user-id': caller.id,
'x-product-id': req.headers.get('x-product-id') || process.env.PRODUCT_ID || 'lysnrai',
};
const fetchOptions: RequestInit = { method: req.method, headers };
if (req.method !== 'GET' && req.method !== 'HEAD') fetchOptions.body = await req.text();
const res = await fetch(`${PLATFORM_URL}/api/sessions${qs}`, fetchOptions);
const data = await res.json().catch(() => null);
return NextResponse.json(data ?? { error: res.statusText }, { status: res.status });
} catch (error) {
logError('Sessions base proxy error', error);
return NextResponse.json({ error: 'Service unavailable' }, { status: 502 });
}
}
export async function GET(req: NextRequest) {
return proxyBase(req);
}
export async function DELETE(req: NextRequest) {
return proxyBase(req);
}

View File

@ -5,14 +5,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import * as billingClient from '@/lib/billing-client';
export async function GET(req: NextRequest) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -5,14 +5,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

View File

@ -3,14 +3,14 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getCurrentUserFromRequest } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
const PLATFORM_URL = process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003';
async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
try {
const caller = await getCurrentUser(req.headers.get('authorization'));
const caller = await getCurrentUserFromRequest(req);
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { path } = await params;
const targetPath = `/api/webhooks/${path.join('/')}`;

View File

@ -42,6 +42,25 @@ export async function getCurrentUser(authHeader: string | null): Promise<UserDoc
}
}
/**
* Get the current user from a Request object.
* Checks cookies first (Next.js httpOnly cookie), then Authorization header.
* Returns null if not authenticated.
*/
export async function getCurrentUserFromRequest(request: Request): Promise<UserDoc | null> {
const cookieHeader = request.headers.get('cookie') || '';
const tokenMatch = cookieHeader.match(/(?:^|;\s*)token=([^;]+)/);
const authHeader = request.headers.get('authorization');
if (tokenMatch) {
return getCurrentUser(`Bearer ${tokenMatch[1]}`);
}
if (authHeader) {
return getCurrentUser(authHeader);
}
return null;
}
/**
* Require an admin user from the request. Checks cookies first, then Authorization header.
* Throws Error("Unauthorized") if not authenticated or not admin/super_admin role.

View File

@ -0,0 +1,37 @@
/**
* Shared fetch helper for admin-web pages that call proxy API routes.
* Automatically injects Authorization + x-product-id headers from localStorage.
*/
function getAuthHeaders(): Record<string, string> {
if (typeof window === 'undefined') return {};
const headers: Record<string, string> = {};
const token = localStorage.getItem('admin_access_token');
if (token) headers['Authorization'] = `Bearer ${token}`;
const productId = localStorage.getItem('admin_selected_product');
if (productId) headers['x-product-id'] = productId;
return headers;
}
/**
* Creates a scoped fetch helper for a given proxy base path.
*
* Usage:
* const apiFetch = createProxyFetch('/api/jobs');
* const data = await apiFetch('list'); // GET /api/jobs/list
* await apiFetch('123', { method: 'PUT', body: ... });
*/
export function createProxyFetch(basePath: string) {
return async function proxyFetch(path: string, opts?: RequestInit) {
const url = !path || path.startsWith('?') ? `${basePath}${path}` : `${basePath}/${path}`;
const res = await fetch(url, {
...opts,
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
...opts?.headers,
},
});
return res.json();
};
}