429 lines
16 KiB
TypeScript
429 lines
16 KiB
TypeScript
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>
|
||
);
|
||
};
|