syncova-backup/apps/web/src/features/identity/IdentityPages.tsx
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

165 lines
5.6 KiB
TypeScript

/**
* Benutzer und Rollen.
*
* Beide Seiten zeigen ausschliesslich an. Anlegen und Aendern gehen weiterhin
* ueber die API: Eine Maske, die Rollen bearbeitet, muss die Sonderfaelle
* beherrschen — den letzten Administrator, mitgelieferte unveraenderliche Rollen
* — und die gehoeren geprueft, nicht nebenbei gebaut.
*/
import { useCallback } from 'react';
import { requestApi } from '../../api/client';
import { useApiResource } from '../../api/useApiResource';
import { EmptyState, ErrorState, LoadingState } from '../../components/PageState';
/** Ein Benutzerkonto. */
interface UserAccount {
/** Oeffentlicher Bezeichner. */
readonly id: string;
/** Anmeldename. */
readonly username: string;
/** Postanschrift. */
readonly email?: string;
/** Zustand des Kontos. */
readonly status: string;
/** Meldet einen eingerichteten zweiten Faktor. */
readonly mfa_enabled: boolean;
/** Zugewiesene Rollen. */
readonly roles?: readonly string[];
/** Letzte Anmeldung in UTC. */
readonly last_login_at?: string;
}
/** Eine Rolle. */
interface RoleDefinition {
/** Oeffentlicher Bezeichner. */
readonly id: string;
/** Name der Rolle. */
readonly name: string;
/** Beschreibung. */
readonly description?: string;
/** Zugeordnete Berechtigungen. */
readonly permissions?: readonly string[];
}
/** Zeigt die Benutzerkonten. */
export function UsersPage(): React.JSX.Element {
const loadUsers = useCallback(
(abortSignal: AbortSignal) => requestApi<readonly UserAccount[]>('/users', { signal: abortSignal }),
[],
);
const { loadState, data, loadError, reload } = useApiResource<readonly UserAccount[]>(loadUsers);
return (
<section className="page">
<header className="page__header">
<h1 className="page__title">Benutzer</h1>
{data !== null && <span className="page__meta">{data.length} Konten</span>}
</header>
{loadState === 'loading' && <LoadingState what="Die Benutzerkonten" />}
{loadState === 'failed' && loadError !== null && <ErrorState error={loadError} onRetry={reload} />}
{loadState === 'loaded' && data !== null && data.length === 0 && (
<EmptyState message="Es ist kein Konto vorhanden." />
)}
{loadState === 'loaded' && data !== null && data.length > 0 && (
<div className="table-wrapper">
<table className="data-table">
<thead>
<tr>
<th scope="col">Anmeldename</th>
<th scope="col">Rollen</th>
<th scope="col">Zustand</th>
<th scope="col">Zweiter Faktor</th>
</tr>
</thead>
<tbody>
{data.map((userAccount) => (
<tr key={userAccount.id}>
<td>
{userAccount.username}
{userAccount.email !== undefined && (
<span className="data-table__secondary">{userAccount.email}</span>
)}
</td>
<td>{userAccount.roles?.join(', ') ?? '—'}</td>
<td>
<span
className={
userAccount.status === 'active' ? 'badge badge--healthy' : 'badge badge--neutral'
}
>
{userAccount.status}
</span>
</td>
<td>
{/* Ein fehlender zweiter Faktor ist ein Sicherheitsbefund und
wird benannt, statt ihn zu verschweigen (PROMPT.md §90). */}
{userAccount.mfa_enabled ? (
<span className="badge badge--healthy">eingerichtet</span>
) : (
<span className="badge badge--warning">fehlt</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
);
}
/** Zeigt die Rollen samt Berechtigungen. */
export function RolesPage(): React.JSX.Element {
const loadRoles = useCallback(
(abortSignal: AbortSignal) => requestApi<readonly RoleDefinition[]>('/roles', { signal: abortSignal }),
[],
);
const { loadState, data, loadError, reload } = useApiResource<readonly RoleDefinition[]>(loadRoles);
return (
<section className="page">
<header className="page__header">
<h1 className="page__title">Rollen</h1>
{data !== null && <span className="page__meta">{data.length} Rollen</span>}
</header>
{loadState === 'loading' && <LoadingState what="Die Rollen" />}
{loadState === 'failed' && loadError !== null && <ErrorState error={loadError} onRetry={reload} />}
{loadState === 'loaded' && data !== null && (
<ul className="card-list">
{data.map((roleDefinition) => (
<li className="card" key={roleDefinition.id}>
<h2 className="card__title">{roleDefinition.name}</h2>
{roleDefinition.description !== undefined && (
<p className="card__text">{roleDefinition.description}</p>
)}
{/* Die Berechtigungen stehen ausgeschrieben da: „Backup Operator"
sagt einem Betreiber wenig, „backups.delete" alles. */}
<ul className="permission-list">
{(roleDefinition.permissions ?? []).map((permissionName) => (
<li className="permission-list__item" key={permissionName}>
{permissionName}
</li>
))}
</ul>
</li>
))}
</ul>
)}
</section>
);
}