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>
165 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|