Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"@proxyos/billing": "workspace:*",
"@proxyos/alerts": "workspace:*",
"@proxyos/logger": "workspace:*",
"@proxyos/analytics": "workspace:*",
"@proxyos/api": "workspace:*",
"@proxyos/connect": "workspace:*",
Expand Down
10 changes: 6 additions & 4 deletions apps/web/src/app/access-lists/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from 'react'
import { Badge, Button, Card, DataTable, Input, Select, SidePanel, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

type SatisfyMode = 'any' | 'all'

Expand Down Expand Up @@ -41,17 +42,18 @@ const emptyForm = (): FormState => ({
})

export default function AccessListsPage() {
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const list = trpc.accessLists.list.useQuery()
const createMut = trpc.accessLists.create.useMutation({ onSuccess: () => { utils.accessLists.list.invalidate(); setPanel(null) } })
const updateMut = trpc.accessLists.update.useMutation({ onSuccess: () => { utils.accessLists.list.invalidate(); setPanel(null) } })
const deleteMut = trpc.accessLists.delete.useMutation({ onSuccess: () => utils.accessLists.list.invalidate() })
const createMut = trpc.accessLists.create.useMutation({ onSuccess: () => { utils.accessLists.list.invalidate(); setPanel(null) }, onError: handleError })
const updateMut = trpc.accessLists.update.useMutation({ onSuccess: () => { utils.accessLists.list.invalidate(); setPanel(null) }, onError: handleError })
const deleteMut = trpc.accessLists.delete.useMutation({ onSuccess: () => utils.accessLists.list.invalidate(), onError: handleError })

const [panel, setPanel] = useState<'create' | { id: string } | null>(null)
const [form, setForm] = useState<FormState>(emptyForm())
const [testIp, setTestIp] = useState('')
const [testResult, setTestResult] = useState<{ result: 'allow' | 'deny'; matchedRule: string | null } | null>(null)
const testIpMut = trpc.accessLists.testIp.useMutation({ onSuccess: (r) => setTestResult(r) })
const testIpMut = trpc.accessLists.testIp.useMutation({ onSuccess: (r) => setTestResult(r), onError: handleError })

function openCreate() {
setForm(emptyForm())
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/app/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { use, useState, type CSSProperties, type ReactNode } from 'react'
import { Badge, Button, Card, DataTable, Dot, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

function AgentRoutesTab({ agentId }: { agentId: string }) {
const routes = trpc.routes.listByAgent.useQuery({ agentId })
Expand Down Expand Up @@ -63,11 +64,14 @@ export default function AgentDetailPage({ params }: { params: Promise<{ id: stri
const agent = trpc.agents.get.useQuery({ id })
const health = trpc.agents.getHealth.useQuery({ id })
const metrics = trpc.agents.getMetrics.useQuery({ id, range: 60 })
const [handleError] = useErrorHandler()
const revokeMut = trpc.agents.revokeToken.useMutation({
onSuccess: () => { void utils.agents.list.invalidate(); void agent.refetch() },
onError: handleError,
})
const deleteMut = trpc.agents.delete.useMutation({
onSuccess: () => window.history.back(),
onError: handleError,
})
const [tab, setTab] = useState<Tab>('routes')

Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/app/agents/[id]/scan/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import { use } from 'react'
import { Badge, Button, Card, Dot } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

export default function AgentScanPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const results = trpc.scanner.getResults.useQuery({ agentId: id })
const scanMut = trpc.scanner.scan.useMutation({
onSuccess: () => utils.scanner.getResults.invalidate(),
onError: handleError,
})
const exposeMut = trpc.scanner.exposeContainer.useMutation({
onSuccess: () => utils.scanner.getResults.invalidate(),
onError: handleError,
})

const containers = results.data?.results ?? []
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/app/agents/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import { useRouter } from 'next/navigation'
import { Button, Card, Input, StepIndicator } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

const STEPS = ['Details', 'Deploy', 'Confirm']

export default function NewAgentPage() {
const router = useRouter()
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const registerMutation = trpc.agents.register.useMutation({
onSuccess: () => utils.agents.list.invalidate(),
onError: handleError,
})

const [step, setStep] = useState(0)
Expand Down
43 changes: 41 additions & 2 deletions apps/web/src/app/api/auth/diagnose/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ export const dynamic = 'force-dynamic'

export async function GET(req: Request): Promise<Response> {
const cookieHeader = req.headers.get('cookie')
const cookiePresent = !!cookieHeader

if (!cookieHeader) {
return Response.json({
authenticated: false,
reason: 'No session cookie found. Please log in.',
action: 'login',
details: {
cookie_present: false,
token_valid: false,
},
// legacy fields kept for login page compat
status: 'no-cookie',
message: 'No session cookie found. Please log in.',
hint: 'If you just logged in and see this, check that your browser accepts cookies for this domain.',
Expand All @@ -18,21 +27,51 @@ export async function GET(req: Request): Promise<Response> {
if (!token) {
const presentKeys = cookieHeader.split(';').map(p => p.trim().split('=')[0]).filter(Boolean)
return Response.json({
authenticated: false,
reason: `Session cookie "${TOKEN_COOKIE}" not found in your request.`,
action: 'login',
details: {
cookie_present: cookiePresent,
token_valid: false,
present_cookies: presentKeys,
},
// legacy fields
status: 'no-token-cookie',
message: `Session cookie "${TOKEN_COOKIE}" not found in your request.`,
cookies: presentKeys,
hint: 'Your browser may have cookies from another app on the same domain.',
})
}

const parsed = verifyToken(token)
if (!parsed) {
return Response.json({
authenticated: false,
reason: "Session token is invalid or expired. The server's signing secret may have changed since you logged in.",
action: 'login',
details: {
cookie_present: cookiePresent,
token_valid: false,
token_expired: true,
},
// legacy fields
status: 'invalid-token',
message: "Session token is invalid or expired. The server's signing secret may have changed since you logged in.",
hint: 'Log in again. If this keeps happening, make sure PROXYOS_SECRET is stable across restarts.',
})
}

return Response.json({ status: 'ok', userId: parsed.userId, role: parsed.role })
return Response.json({
authenticated: true,
reason: 'Session is valid.',
action: 'none',
details: {
cookie_present: cookiePresent,
token_valid: true,
role: parsed.role,
},
// legacy fields
status: 'ok',
userId: parsed.userId,
role: parsed.role,
})
}
49 changes: 49 additions & 0 deletions apps/web/src/app/api/health/detailed/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextResponse } from 'next/server'
import { verifyToken, getTokenFromCookies } from '@proxyos/api/auth'
import { getDb } from '@proxyos/db'
import { checkDatabase, checkCaddy, checkDocker, checkAuth, checkDisk, settled } from '@proxyos/api/health-checks'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET(req: Request) {
const cookieHeader = req.headers.get('cookie')
if (!cookieHeader) return new NextResponse('Unauthorized', { status: 401 })

const token = getTokenFromCookies(cookieHeader)
if (!token) return new NextResponse('Unauthorized', { status: 401 })

const parsed = verifyToken(token)
if (!parsed || parsed.role !== 'admin') return new NextResponse('Unauthorized', { status: 401 })

const db = getDb()
const uptimeSeconds = Math.floor(process.uptime())

const [dbHealth, caddyHealth, dockerHealth, diskHealth] = await Promise.allSettled([
checkDatabase(db),
checkCaddy(),
checkDocker(),
Promise.resolve(checkDisk()),
])

const components = {
database: settled(dbHealth),
caddy_admin: settled(caddyHealth),
docker: settled(dockerHealth),
auth: checkAuth(),
disk: settled(diskHealth),
federation: { status: 'n/a' as const, reason: 'standalone mode' },
}

const statuses = Object.values(components).map(c => c.status)
const overall = statuses.includes('unhealthy') ? 'unhealthy'
: statuses.includes('degraded') ? 'degraded'
: 'healthy'

return NextResponse.json({
overall,
components,
version: process.env.APP_VERSION ?? 'dev',
uptime_seconds: uptimeSeconds,
})
}
35 changes: 35 additions & 0 deletions apps/web/src/app/api/logs/download/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getRecentLogs } from '@proxyos/logger'
import { NextResponse } from 'next/server'
import { verifyToken, getTokenFromCookies } from '@proxyos/api/auth'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

export async function GET(req: Request) {
// Authenticate and verify admin role
const cookieHeader = req.headers.get('cookie')

if (!cookieHeader) {
return new NextResponse('Unauthorized', { status: 401 })
}

const token = getTokenFromCookies(cookieHeader)
if (!token) {
return new NextResponse('Unauthorized', { status: 401 })
}

const parsed = verifyToken(token)
if (!parsed || parsed.role !== 'admin') {
return new NextResponse('Unauthorized', { status: 401 })
}

// User is authenticated and is admin; proceed with log download
const logs = getRecentLogs()
const text = logs.map(l => JSON.stringify(l)).join('\n')
return new NextResponse(text, {
headers: {
'Content-Type': 'text/plain',
'Content-Disposition': 'attachment; filename="proxyos-logs.txt"',
},
})
}
10 changes: 6 additions & 4 deletions apps/web/src/app/approvals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { useState } from 'react'
import { Badge, Button, Card, DataTable, Input, Toggle, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

export default function ApprovalsPage() {
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const config = trpc.approvals.getConfig.useQuery()
const list = trpc.approvals.list.useQuery({ status: 'pending' })
const history = trpc.approvals.list.useQuery({ status: 'all' })

const saveConfig = trpc.approvals.setConfig.useMutation({ onSuccess: () => utils.approvals.getConfig.invalidate() })
const approve = trpc.approvals.approve.useMutation({ onSuccess: () => { utils.approvals.list.invalidate() } })
const reject = trpc.approvals.reject.useMutation({ onSuccess: () => { utils.approvals.list.invalidate() } })
const purge = trpc.approvals.purgeExpired.useMutation({ onSuccess: () => utils.approvals.list.invalidate() })
const saveConfig = trpc.approvals.setConfig.useMutation({ onSuccess: () => utils.approvals.getConfig.invalidate(), onError: handleError })
const approve = trpc.approvals.approve.useMutation({ onSuccess: () => { utils.approvals.list.invalidate() }, onError: handleError })
const reject = trpc.approvals.reject.useMutation({ onSuccess: () => { utils.approvals.list.invalidate() }, onError: handleError })
const purge = trpc.approvals.purgeExpired.useMutation({ onSuccess: () => utils.approvals.list.invalidate(), onError: handleError })

const [enabled, setEnabled] = useState(config.data?.enabled ?? false)
const [approvers, setApprovers] = useState(config.data?.requiredApprovers?.toString() ?? '1')
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/automation/compose/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ import { useState } from 'react'
import { Badge, Button, Card, DataTable, Input, Toggle, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

export default function ComposeWatcherPage() {
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const list = trpc.automation.listComposeWatchers.useQuery(undefined, { refetchInterval: 10_000 })
const create = trpc.automation.createComposeWatcher.useMutation({
onSuccess: () => { utils.automation.listComposeWatchers.invalidate(); setShowForm(false); setPath(''); setInterval_('30') },
onError: handleError,
})
const toggle = trpc.automation.toggleComposeWatcher.useMutation({
onSuccess: () => utils.automation.listComposeWatchers.invalidate(),
onError: handleError,
})
const del = trpc.automation.deleteComposeWatcher.useMutation({
onSuccess: () => utils.automation.listComposeWatchers.invalidate(),
onError: handleError,
})

const [showForm, setShowForm] = useState(false)
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/app/certificates/acme/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link'
import { Badge, Button, Card, DataTable, Input, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

const ACME_URLS: Record<string, string> = {
letsencrypt: 'https://acme-v02.api.letsencrypt.org/directory',
Expand All @@ -19,13 +20,16 @@ const RATE_LIMIT: Record<string, number> = {
}

export default function AcmeAccountsPage() {
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const list = trpc.observability.listAcmeAccounts.useQuery()
const create = trpc.observability.createAcmeAccount.useMutation({
onSuccess: () => { utils.observability.listAcmeAccounts.invalidate(); setShowForm(false); setEmail(''); setCustomUrl('') },
onError: handleError,
})
const del = trpc.observability.deleteAcmeAccount.useMutation({
onSuccess: () => utils.observability.listAcmeAccounts.invalidate(),
onError: handleError,
})

const [showForm, setShowForm] = useState(false)
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/certificates/ct/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ import Link from 'next/link'
import { AlertBanner, Badge, Button, Card, DataTable, Input, Toggle, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

export default function CTMonitorPage() {
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const config = trpc.observability.getCTConfig.useQuery()
const alerts = trpc.observability.listCTAlerts.useQuery({ includeAcknowledged: false })
const setCfg = trpc.observability.setCTConfig.useMutation({
onSuccess: () => utils.observability.getCTConfig.invalidate(),
onError: handleError,
})
const ack = trpc.observability.acknowledgeCTAlert.useMutation({
onSuccess: () => utils.observability.listCTAlerts.invalidate(),
onError: handleError,
})
const check = trpc.observability.runCTCheck.useMutation({
onSuccess: () => utils.observability.listCTAlerts.invalidate(),
onError: handleError,
})

const [domain, setDomain] = useState('')
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/app/certificates/multi/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import Link from 'next/link'
import { Badge, Button, Card, DataTable, Input, td, th } from '~/components/ui'
import { Topbar, PageContent } from '~/components/shell'
import { trpc } from '~/lib/trpc'
import { useErrorHandler } from '@/hooks/useErrorHandler'

export default function MultiDomainCertsPage() {
const [handleError] = useErrorHandler()
const utils = trpc.useUtils()
const list = trpc.observability.listMultiDomainCerts.useQuery()
const create = trpc.observability.createMultiDomainCert.useMutation({
onSuccess: () => { utils.observability.listMultiDomainCerts.invalidate(); setShowForm(false); setDomains(''); setRoutes('') },
onError: handleError,
})
const del = trpc.observability.deleteMultiDomainCert.useMutation({
onSuccess: () => utils.observability.listMultiDomainCerts.invalidate(),
onError: handleError,
})

const [showForm, setShowForm] = useState(false)
Expand Down
Loading