thecr3w-dev/frontend/admin-panel/src/App.tsx

429 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { ManagedApp, DeploymentManifestItem, ExternalServerItem, AuditEventItem } from './types';
export const App: React.FC = () => {
const [activeTab, setActiveTab] = useState<'dashboard' | 'apps' | 'deployments' | 'servers' | 'iam' | 'audit'>('dashboard');
const [apps] = useState<ManagedApp[]>([
{
appId: 'com.thecr3w.app',
name: 'TheCr3w Mobile',
description: 'Aplikacja Android dla członków ekosystemu',
isPublic: true,
platforms: ['android'],
defaultChannel: 'stable'
},
{
appId: 'thecr3w-desktop',
name: 'TheCr3w Desktop',
description: 'Klient stacjonarny Windows z automatycznym updaterem',
isPublic: true,
platforms: ['windows'],
defaultChannel: 'stable'
}
]);
const [manifests, setManifests] = useState<DeploymentManifestItem[]>([
{
manifestId: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
project: 'thecr3w-backend',
environment: 'production',
serviceName: 'backend',
image: 'git.thecr3wnet.com/thecr3w/backend',
immutableDigest: 'sha256:7a59d8134bb8df389a9f24255198ec51787c9dc08269d7b420f1885b0d0ce0bf',
manifestSha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
status: 'pending_approval',
createdAt: new Date().toISOString(),
createdBy: 'forgejo-actions-bot'
},
{
manifestId: '1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d',
project: 'thecr3w-download-center',
environment: 'production',
serviceName: 'download-center',
image: 'git.thecr3wnet.com/thecr3w/download-center',
immutableDigest: 'sha256:9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b',
manifestSha256: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
status: 'deployed',
createdAt: new Date(Date.now() - 86400000).toISOString(),
createdBy: 'admin'
}
]);
const [servers] = useState<ExternalServerItem[]>([
{
serverId: '77a8b9c0-1122-3344-5566-778899aabbcc',
name: 'Backup Mirror SFTP',
protocol: 'sftp',
host: 'mirror.thecr3wnet.com',
port: 22,
remoteRootPath: '/var/www/dist',
hostKeyFingerprint: 'SHA256:7kX9JvM81uLwQzRp09b...',
status: 'active',
updatedAt: new Date().toISOString()
}
]);
const [auditLogs] = useState<AuditEventItem[]>([
{
eventId: '11111111-2222-3333-4444-555555555555',
actor: 'admin',
action: 'RELEASE_PUBLISHED',
resourceType: 'release',
resourceId: 'com.thecr3w.app:1.2.0',
correlationId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
createdAt: new Date().toISOString(),
success: true,
ipAddress: '89.58.46.176'
},
{
eventId: '22222222-3333-4444-5555-666666666666',
actor: 'forgejo-actions-bot',
action: 'MANIFEST_REGISTERED',
resourceType: 'deployment_manifest',
resourceId: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
correlationId: 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff',
createdAt: new Date(Date.now() - 3600000).toISOString(),
success: true,
ipAddress: '89.58.46.176'
}
]);
function handleApprove(manifestId: string) {
setManifests((prev) =>
prev.map((m) => (m.manifestId === manifestId ? { ...m, status: 'approved' } : m))
);
alert(`Manifest ${manifestId} zatwierdzony. Oczekiwanie na uruchomienie wdrożenia przez Deployment Agenta.`);
}
function handleReject(manifestId: string) {
setManifests((prev) =>
prev.map((m) => (m.manifestId === manifestId ? { ...m, status: 'rejected' } : m))
);
}
return (
<div className="layout">
<aside className="sidebar">
<div className="sidebar-header">
<span>⚙️ TheCr3w Admin</span>
</div>
<nav className="nav-links">
<button
className={`nav-btn ${activeTab === 'dashboard' ? 'active' : ''}`}
onClick={() => setActiveTab('dashboard')}
>
📊 Dashboard
</button>
<button
className={`nav-btn ${activeTab === 'apps' ? 'active' : ''}`}
onClick={() => setActiveTab('apps')}
>
📱 Aplikacje & Release
</button>
<button
className={`nav-btn ${activeTab === 'deployments' ? 'active' : ''}`}
onClick={() => setActiveTab('deployments')}
>
🚀 Wdrożenia & Akceptacje
</button>
<button
className={`nav-btn ${activeTab === 'servers' ? 'active' : ''}`}
onClick={() => setActiveTab('servers')}
>
🌐 Serwery SFTP / FTPS
</button>
<button
className={`nav-btn ${activeTab === 'iam' ? 'active' : ''}`}
onClick={() => setActiveTab('iam')}
>
🔐 Keycloak IAM
</button>
<button
className={`nav-btn ${activeTab === 'audit' ? 'active' : ''}`}
onClick={() => setActiveTab('audit')}
>
📜 Audyt & Logi
</button>
</nav>
<div style={{ padding: '1rem', borderTop: '1px solid var(--border-color)', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
<div>OIDC Realm: <strong>master</strong></div>
<div>Klient: <strong>thecr3w-admin-panel</strong></div>
</div>
</aside>
<main className="main-content">
{activeTab === 'dashboard' && (
<div>
<div className="page-header">
<h1 className="page-title">Dashboard Operacyjny</h1>
<span className="badge badge-success">ŚRODOWISKO ZDROWE</span>
</div>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Aktywne Aplikacje</div>
<div className="stat-value">{apps.length}</div>
</div>
<div className="stat-card">
<div className="stat-label">Oczekujące Akceptacje Wdrożeń</div>
<div className="stat-value" style={{ color: 'var(--warning)' }}>
{manifests.filter((m) => m.status === 'pending_approval').length}
</div>
</div>
<div className="stat-card">
<div className="stat-label">Podłączone Serwery Zewnętrzne</div>
<div className="stat-value">{servers.length}</div>
</div>
<div className="stat-card">
<div className="stat-label">Zdarzenia Audytowe (24h)</div>
<div className="stat-value">{auditLogs.length}</div>
</div>
</div>
<div className="card">
<h2 style={{ fontSize: '1.2rem', marginBottom: '1rem' }}>Wymagane Działania: Zatwierdzenia Wdrożeń</h2>
<table className="data-table">
<thead>
<tr>
<th>Projekt</th>
<th>Środowisko</th>
<th>Immutable Digest</th>
<th>Autor</th>
<th>Status</th>
<th>Akcja</th>
</tr>
</thead>
<tbody>
{manifests
.filter((m) => m.status === 'pending_approval')
.map((m) => (
<tr key={m.manifestId}>
<td><strong>{m.project}</strong></td>
<td><span className="badge badge-warning">{m.environment}</span></td>
<td><code>{m.immutableDigest.substring(0, 24)}...</code></td>
<td>{m.createdBy}</td>
<td><span className="badge badge-warning">Oczekuje na SHA256</span></td>
<td>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button className="btn btn-primary" onClick={() => handleApprove(m.manifestId)}>
Zatwierdź
</button>
<button className="btn btn-danger" onClick={() => handleReject(m.manifestId)}>
Odrzuć
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'apps' && (
<div>
<div className="page-header">
<h1 className="page-title">Zarządzanie Aplikacjami Natywnymi</h1>
<button className="btn btn-primary">+ Dodaj Aplikację</button>
</div>
<div className="card">
<table className="data-table">
<thead>
<tr>
<th>ID Aplikacji</th>
<th>Nazwa</th>
<th>Platformy</th>
<th>Domyślny Kanał</th>
<th>Publiczna</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{apps.map((a) => (
<tr key={a.appId}>
<td><code>{a.appId}</code></td>
<td><strong>{a.name}</strong></td>
<td>{a.platforms.join(', ')}</td>
<td><span className="badge badge-info">{a.defaultChannel}</span></td>
<td>{a.isPublic ? <span className="badge badge-success">TAK</span> : <span className="badge badge-danger">NIE</span>}</td>
<td>
<button className="btn btn-secondary">Zarządzaj Wydaniami</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'deployments' && (
<div>
<div className="page-header">
<h1 className="page-title">Historia Wdrożeń Kontenerów Docker</h1>
</div>
<div className="card">
<table className="data-table">
<thead>
<tr>
<th>Manifest ID</th>
<th>Projekt</th>
<th>Usługa</th>
<th>Immutable Digest</th>
<th>Status</th>
<th>Utworzono</th>
</tr>
</thead>
<tbody>
{manifests.map((m) => (
<tr key={m.manifestId}>
<td><code>{m.manifestId.substring(0, 8)}...</code></td>
<td><strong>{m.project}</strong></td>
<td>{m.serviceName}</td>
<td><code>{m.immutableDigest.substring(0, 28)}...</code></td>
<td>
<span className={`badge ${m.status === 'deployed' ? 'badge-success' : m.status === 'approved' ? 'badge-info' : 'badge-warning'}`}>
{m.status.toUpperCase()}
</span>
</td>
<td>{new Date(m.createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'servers' && (
<div>
<div className="page-header">
<h1 className="page-title">Serwery Zewnętrzne (SFTP / FTPS)</h1>
<button className="btn btn-primary">+ Dodaj Serwer</button>
</div>
<div className="card">
<table className="data-table">
<thead>
<tr>
<th>Nazwa</th>
<th>Protokół</th>
<th>Host & Port</th>
<th>Zdalna Ścieżka</th>
<th>Host Key Pinning</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{servers.map((s) => (
<tr key={s.serverId}>
<td><strong>{s.name}</strong></td>
<td><span className="badge badge-info">{s.protocol.toUpperCase()}</span></td>
<td>{s.host}:{s.port}</td>
<td><code>{s.remoteRootPath}</code></td>
<td><code>{s.hostKeyFingerprint}</code></td>
<td><span className="badge badge-success">{s.status.toUpperCase()}</span></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'iam' && (
<div>
<div className="page-header">
<h1 className="page-title">Zarządzanie Tożsamością i Rolami (Keycloak IAM)</h1>
</div>
<div className="card">
<h2 style={{ fontSize: '1.2rem', marginBottom: '0.75rem' }}>Konfiguracja OIDC w Realm `master`</h2>
<p style={{ color: 'var(--text-muted)', marginBottom: '1.25rem', fontSize: '0.9rem' }}>
Wszystkie uprawnienia TheCr3w-dev są zdefiniowane jako role klienta (Client Roles) w obrębie <code>thecr3w-backend</code>,
co zapobiega nadaniu uprawnień administracyjnych serwera Keycloak.
</p>
<table className="data-table">
<thead>
<tr>
<th>Rola Klienta</th>
<th>Zakres Uprawnień</th>
<th>Przypisane Grupy / Konta</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>thecr3w:admin</code></td>
<td>Pełna administracja całym systemem, zarządzanie kluczami i audyt</td>
<td>Grupa: <code>thecr3w-administrators</code></td>
</tr>
<tr>
<td><code>thecr3w:release-manager</code></td>
<td>Publikacja wydań, upload R2, podpisywanie Ed25519, wycofywanie</td>
<td>Grupa: <code>thecr3w-release-managers</code></td>
</tr>
<tr>
<td><code>thecr3w:approver</code></td>
<td>Zatwierdzanie wdrożeń produkcyjnych (weryfikacja SHA256)</td>
<td>Grupa: <code>thecr3w-approvers</code></td>
</tr>
<tr>
<td><code>thecr3w:deployer</code></td>
<td>Zgłaszanie manifestów i synchronizacja SFTP/FTPS</td>
<td>Konto: <code>forgejo-ci-service-account</code></td>
</tr>
<tr>
<td><code>thecr3w:viewer</code></td>
<td>Dostęp tylko do odczytu: podgląd dashboardu i logów</td>
<td>Wszyscy uwierzytelnieni użytkownicy TheCr3w</td>
</tr>
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'audit' && (
<div>
<div className="page-header">
<h1 className="page-title">Niezmienny Dziennik Audytowy</h1>
</div>
<div className="card">
<table className="data-table">
<thead>
<tr>
<th>Data i Czas</th>
<th>Aktor</th>
<th>Akcja</th>
<th>Zasób</th>
<th>ID Zasobu</th>
<th>IP</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{auditLogs.map((log) => (
<tr key={log.eventId}>
<td>{new Date(log.createdAt).toLocaleString()}</td>
<td><strong>{log.actor}</strong></td>
<td><code>{log.action}</code></td>
<td>{log.resourceType}</td>
<td><code>{log.resourceId}</code></td>
<td>{log.ipAddress || '—'}</td>
<td>{log.success ? <span className="badge badge-success">OK</span> : <span className="badge badge-danger">FAIL</span>}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</main>
</div>
);
};