feat(ui): Frontend MVP — React 19 + AntD 6 + Vite + StaticFS-Wiring
Scaffold und Core-Infrastruktur 1:1 nach enconf-Pattern (netcell- webpanel/management-ui), reduziert auf EdgeGuard-Scope (kein reseller/ customer-Roles, keine codemirror/extensions). Stack: React 19 + AntD 6 + TS strict + Vite + TanStack-Query + zustand + react-i18next. Layout: AppLayout (Sider+Header+Content), Sidebar (Dashboard/Domains), Header (User-Dropdown + Logout). i18n mit de/en common.json. Pages: Login (POST /auth/login), Setup-Wizard (POST /setup/complete), Dashboard (Health-Polling + Statistics), Domains (volles CRUD via TanStack-Query gegen /domains-API). UpdateBanner-Komponente (/system/package-versions, alle 5 min poll, /system/upgrade trigger) ist von Tag 1 wie vom User gefordert eingebaut. API-Wiring: cmd/edgeguard-api/main.go mountUI() — gin StaticFS für /usr/share/edgeguard/ui/ (overridebar via EDGEGUARD_UI_DIR), echte Files werden direkt geserved, alle nicht-API-Pfade fallen via NoRoute auf index.html für React-Router-SPA. Wenn dist/ fehlt: HTML-Placeholder mit Build-Hinweis. Verifiziert: bun install + npx tsc -b strict (0 errors) + bun run build (12 chunks). End-to-end gegen /tmp/eg-api: / serviert echte React-index.html, /domains SPA-Fallback, /api/v1/* JSON, /assets/* direkt, /api/v1/nonexistent korrekt 404. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
71
management-ui/src/pages/Login/index.tsx
Normal file
71
management-ui/src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Button, Card, Form, Input, message, Typography } from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import type { SessionUser } from '../../stores/auth'
|
||||
|
||||
interface Props {
|
||||
onLogin: (u: SessionUser) => void
|
||||
}
|
||||
|
||||
interface LoginValues {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export default function LoginPage({ onLogin }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const onFinish = async (vals: LoginValues) => {
|
||||
try {
|
||||
const r = await apiClient.post('/auth/login', vals)
|
||||
if (isEnvelope(r.data)) {
|
||||
const u = r.data.data as SessionUser
|
||||
onLogin(u)
|
||||
navigate('/dashboard', { replace: true })
|
||||
return
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string; status?: number }
|
||||
if (err.status === 503) {
|
||||
// setup-mode → drop to wizard
|
||||
navigate('/setup', { replace: true })
|
||||
return
|
||||
}
|
||||
message.error(err.message ?? t('auth.loginFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5' }}>
|
||||
<Card style={{ width: 400 }}>
|
||||
<Typography.Title level={3} style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
{t('app.title')}
|
||||
</Typography.Title>
|
||||
<Form layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item
|
||||
label={t('auth.email')}
|
||||
name="email"
|
||||
rules={[{ required: true, type: 'email' }]}
|
||||
>
|
||||
<Input autoComplete="email" autoFocus />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('auth.password')}
|
||||
name="password"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password autoComplete="current-password" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
{t('auth.login')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user