feat: organizations decide who sees whom; sharing only by owners
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s

- Server admins manage organizations (one per user); new admin section,
  organization on user creation and invites, setup creates the first one
- Sharing modes private / organizations / open, enforced centrally
  (CanSee, CanShareWith) for directory, all shares and groups; unknown and
  invisible targets give the same error
- Losing visibility (org change, mode change, leaving a group) prunes shares,
  group memberships and task assignments; dry run warns before tightening
- Only owners share; editors can no longer reshare lists or remove others'
  shares; legacy task_lists.team_id and old /organizations endpoints removed
  (FLAW-06, FLAW-10, FLAW-24)
- Schema version: outdated databases are wiped and recreated on start
- Fix setup sample data (createSampleData was ignored); setup and admin
  use a shared ToggleSwitch instead of checkboxes
- Docs: sharing-and-permissions rewritten, ui-patterns, REST, DB, flaws

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 22:00:33 +01:00
1 parent 73fad1c5db
commit 84395f3322
26 files changed
+1421 -609

No files matched your search

@@ -13,22 +13,47 @@ import {
Send,
AlertCircle,
UserPlus,
Building2,
Pencil,
} from 'lucide-react';
import {
Organization,
SharingMode,
SystemSettings,
User,
UserInvite,
UserRole,
} from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
import { api, setCurrentSharingMode, triggerHaptic } from '../../lib/api';
import { UserAvatar } from '../../components/UserAvatar';
import { ToggleSwitch } from '../../components/ToggleSwitch';
interface AdminSettingsViewProps {
currentUser: User;
onBack: () => void;
}
type AdminSection = 'users' | 'server' | 'smtp';
type AdminSection = 'users' | 'organizations' | 'server' | 'smtp';
const SHARING_MODES: { id: SharingMode; title: string; text: string }[] = [
{
id: 'private',
title: '1. Privat (Keiner sieht andere)',
text: 'Jeder arbeitet für sich allein. Keine Benutzersuche, kein Teilen, keine Gruppen.',
},
{
id: 'organizations',
title: '2. Organisationen (Empfohlen)',
text: 'Benutzer sehen nur Mitglieder ihrer eigenen Organisation. Wer keiner Organisation angehört, sieht niemanden.',
},
{
id: 'open',
title: '3. Offen (Alle sehen alle)',
text: 'Alle Benutzer dieser Instanz können sich gegenseitig finden und miteinander teilen.',
},
];
// Stricter modes come first; switching to a stricter one can remove shares
const modeRank = (m: SharingMode) => SHARING_MODES.findIndex((x) => x.id === m);
export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
currentUser,
@@ -44,9 +69,21 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
const [newUserEmail, setNewUserEmail] = useState('');
const [newUserPassword, setNewUserPassword] = useState('');
const [newUserRole, setNewUserRole] = useState<UserRole>('member');
const [newUserOrg, setNewUserOrg] = useState('');
const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState<UserRole>('member');
const [inviteOrg, setInviteOrg] = useState('');
// --- Organizations State ---
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [newOrgName, setNewOrgName] = useState('');
const [renamingOrgId, setRenamingOrgId] = useState<string | null>(null);
const [renameOrgVal, setRenameOrgVal] = useState('');
const [deletingOrgId, setDeletingOrgId] = useState<string | null>(null);
// Moving someone out of an organization removes their shares there, so it needs a confirmation
const [pendingMove, setPendingMove] = useState<{ user: User; orgId: string } | null>(null);
const [pendingMode, setPendingMode] = useState<{ mode: SharingMode; affected: number } | null>(null);
const [copiedInviteId, setCopiedInviteId] = useState<string | null>(null);
const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null);
@@ -54,7 +91,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
// --- Server & SMTP Settings State ---
const [settings, setSettings] = useState<SystemSettings>({
sharingMode: 'teams_only',
sharingMode: 'organizations',
allowRegistration: false,
instanceName: 'MindSift',
smtpHost: '',
@@ -80,9 +117,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
setInvites(invList);
if (isServerAdmin) {
const sys = await api.adminGetSettings();
const [sys, orgs] = await Promise.all([api.adminGetSettings(), api.adminGetOrganizations()]);
setOrganizations(orgs);
setSettings({
sharingMode: sys.sharingMode || 'teams_only',
sharingMode: sys.sharingMode || 'organizations',
allowRegistration: Boolean(sys.allowRegistration),
instanceName: sys.instanceName || 'MindSift',
smtpHost: sys.smtpHost || '',
@@ -116,8 +154,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
password: newUserPassword,
displayName: newUserName.trim() || newUserEmail.split('@')[0],
role: newUserRole,
organizationId: newUserOrg || undefined,
});
setUsers((prev) => [...prev, created]);
if (newUserOrg) await reloadOrganizations();
setNewUserName('');
setNewUserEmail('');
setNewUserPassword('');
@@ -136,6 +176,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
const inv = await api.adminCreateInvite({
email: inviteEmail.trim(),
role: inviteRole,
organizationId: inviteOrg || undefined,
});
setInvites((prev) => [inv, ...prev]);
setInviteEmail('');
@@ -222,6 +263,86 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
// ============================================================================
// HANDLERS: SERVER & SMTP SETTINGS
// ============================================================================
// ============================================================================
// HANDLERS: ORGANIZATIONS
// ============================================================================
const reloadOrganizations = async () => {
try {
setOrganizations(await api.adminGetOrganizations());
} catch {
// keep the old list
}
};
const handleCreateOrganization = async (e: React.FormEvent) => {
e.preventDefault();
if (!newOrgName.trim()) return;
try {
await api.adminCreateOrganization(newOrgName.trim());
setNewOrgName('');
await reloadOrganizations();
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Anlegen der Organisation' });
}
};
const handleRenameOrganization = async (orgId: string) => {
try {
await api.adminRenameOrganization(orgId, renameOrgVal.trim());
setRenamingOrgId(null);
await reloadOrganizations();
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Umbenennen' });
}
};
const handleDeleteOrganization = async (orgId: string) => {
try {
await api.adminDeleteOrganization(orgId);
setDeletingOrgId(null);
setUsers((prev) => prev.map((u) => (u.organizationId === orgId ? { ...u, organizationId: '' } : u)));
await reloadOrganizations();
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Löschen' });
}
};
const applyUserOrganization = async (u: User, orgId: string) => {
setPendingMove(null);
try {
await api.adminSetUserOrganization(u.id, orgId);
setUsers((prev) => prev.map((x) => (x.id === u.id ? { ...x, organizationId: orgId } : x)));
await reloadOrganizations();
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Zuordnen' });
}
};
const handleChangeUserOrganization = (u: User, orgId: string) => {
if (u.organizationId) setPendingMove({ user: u, orgId });
else applyUserOrganization(u, orgId);
};
const orgName = (id?: string) => organizations.find((o) => o.id === id)?.name || '';
// Tightening the mode first asks how much sharing would be lost
const handleSelectSharingMode = async (mode: SharingMode) => {
if (mode === settings.sharingMode) return;
if (modeRank(mode) < modeRank(settings.sharingMode)) {
try {
const affected = await api.adminSharingImpact(mode);
if (affected > 0) {
setPendingMode({ mode, affected });
return;
}
} catch {
// fall through and save
}
}
setPendingMode(null);
handleSaveSystemSettings({ sharingMode: mode });
};
const handleSaveSystemSettings = async (patch?: Partial<SystemSettings>) => {
setStatusMsg(null);
const next = { ...settings, ...(patch || {}) };
@@ -229,6 +350,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
try {
const saved = await api.adminUpdateSettings(next);
setSettings((prev) => ({ ...prev, ...saved }));
setCurrentSharingMode(saved.sharingMode);
setStatusMsg({ type: 'ok', text: 'Server-Einstellungen erfolgreich gespeichert.' });
} catch (err: any) {
setStatusMsg({ type: 'err', text: err.message || 'Fehler beim Speichern' });
@@ -327,6 +449,21 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{isServerAdmin && (
<>
<button
onClick={() => setSection('organizations')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'organizations'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-canvas/50'
}`}
>
<Building2 className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Organisationen</p>
<p className="text-[10px] text-faint">Wer wen sehen darf</p>
</div>
</button>
<button
onClick={() => setSection('server')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
@@ -383,7 +520,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</h3>
<form onSubmit={handleCreateUser} className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3">
<div className="min-w-0">
<label className="block text-muted mb-1">Anzeigename</label>
<input
@@ -432,6 +569,24 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<option value="admin">Server-Admin (Vollzugriff)</option>
</select>
</div>
{isServerAdmin && (
<div className="min-w-0">
<label className="block text-muted mb-1">Organisation</label>
<select
value={newUserOrg}
onChange={(e) => setNewUserOrg(e.target.value)}
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
>
<option value="">Keine Organisation</option>
{organizations.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
</div>
)}
</div>
<div className="flex justify-end">
@@ -454,7 +609,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</h3>
<form onSubmit={handleCreateInvite} className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="min-w-0">
<label className="block text-muted mb-1">
Gebundene E-Mail (optional)
@@ -479,6 +634,24 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<option value="admin">Server-Admin</option>
</select>
</div>
{isServerAdmin && (
<div className="min-w-0">
<label className="block text-muted mb-1">Organisation</label>
<select
value={inviteOrg}
onChange={(e) => setInviteOrg(e.target.value)}
className="w-full min-w-0 h-8 px-2 input-field bg-panel"
>
<option value="">Keine Organisation</option>
{organizations.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
</div>
)}
</div>
<div className="flex justify-end">
@@ -511,6 +684,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<span className="text-muted">
Rolle: <strong className="text-main">{inv.role}</strong>
{inv.email ? ` · Für: ${inv.email}` : ''}
{inv.organizationName ? ` · ${inv.organizationName}` : ''}
</span>
{inv.usedBy ? (
<span className="badge-neutral text-[10px]">
@@ -568,7 +742,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</span>
)}
</div>
<p className="text-muted text-[11px]">{u.email}</p>
<p className="text-muted text-[11px]">
{u.email}
{isServerAdmin && settings.sharingMode === 'organizations' && !u.organizationId && (
<span className="text-status-warn"> · ohne Organisation, sieht niemanden</span>
)}
</p>
</div>
</div>
@@ -585,6 +764,22 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<option value="admin">Server-Admin</option>
</select>
{isServerAdmin && (
<select
value={u.organizationId || ''}
onChange={(e) => handleChangeUserOrganization(u, e.target.value)}
className="h-7 px-2 input-field text-[11px] bg-panel max-w-[11rem]"
title="Organisation"
>
<option value="">Keine Organisation</option>
{organizations.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
)}
{isServerAdmin && (
<>
<button
@@ -619,6 +814,25 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</div>
</div>
{pendingMove?.user.id === u.id && (
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-border">
<AlertCircle className="w-3.5 h-3.5 text-status-warn shrink-0" />
<span className="flex-1 min-w-0 text-muted">
{u.displayName} verlässt „{orgName(u.organizationId)}“ und verliert dort alle Freigaben,
Gruppen und Aufgaben-Zuweisungen.
</span>
<button
onClick={() => applyUserOrganization(u, pendingMove.orgId)}
className="btn-primary h-7 px-3 text-[11px]"
>
{pendingMove.orgId ? `Nach „${orgName(pendingMove.orgId)}“ verschieben` : 'Entfernen'}
</button>
<button onClick={() => setPendingMove(null)} className="btn-action h-7 px-2 text-[11px]">
Abbrechen
</button>
</div>
)}
{resetPwdUserId === u.id && (
<div className="flex items-center gap-2 pt-2 border-t border-border">
<input
@@ -651,6 +865,131 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
{/* ================================================================= */}
{/* SECTION 2: ORGANISATIONEN (SERVER-ADMIN ONLY) */}
{/* ================================================================= */}
{section === 'organizations' && isServerAdmin && (
<div className="space-y-6">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">Organisationen</h2>
<p className="text-muted mt-0.5">
Jeder Benutzer gehört höchstens einer Organisation an. Im Modus „Organisationen“ sehen sich nur
Mitglieder derselben Organisation und können nur untereinander teilen und Gruppen bilden.
Die Zuordnung änderst du in der Benutzerliste.
</p>
{settings.sharingMode !== 'organizations' && (
<p className="mt-2 text-status-warn">
Der aktuelle Sichtbarkeits-Modus ist nicht „Organisationen“; die Zuordnung wirkt sich derzeit nicht aus.
</p>
)}
</div>
<form onSubmit={handleCreateOrganization} className="flex gap-2 max-w-md">
<input
type="text"
value={newOrgName}
onChange={(e) => setNewOrgName(e.target.value)}
placeholder="Name der neuen Organisation"
className="flex-1 min-w-0 h-8 px-2.5 input-field"
/>
<button type="submit" className="btn-primary h-8 px-3 text-xs flex items-center gap-1.5">
<Plus className="w-3.5 h-3.5" />
<span>Anlegen</span>
</button>
</form>
{organizations.length === 0 ? (
<p className="text-muted">Noch keine Organisation angelegt.</p>
) : (
<div className="border border-border divide-y divide-border bg-canvas rounded-card overflow-hidden">
{organizations.map((o) => {
const members = users.filter((u) => u.organizationId === o.id);
return (
<div key={o.id} className="p-3 space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
{renamingOrgId === o.id ? (
<div className="flex flex-1 min-w-0 gap-2">
<input
type="text"
value={renameOrgVal}
onChange={(e) => setRenameOrgVal(e.target.value)}
className="flex-1 min-w-0 h-7 px-2 input-field"
autoFocus
/>
<button onClick={() => handleRenameOrganization(o.id)} className="btn-primary h-7 px-3 text-[11px]">
Speichern
</button>
<button onClick={() => setRenamingOrgId(null)} className="btn-action h-7 px-2 text-[11px]">
Abbrechen
</button>
</div>
) : (
<div className="flex items-center gap-2 min-w-0">
<Building2 className="w-4 h-4 text-accent shrink-0" />
<span className="font-bold text-main truncate">{o.name}</span>
<span className="badge-count">{o.memberCount}</span>
</div>
)}
{renamingOrgId !== o.id && (
<div className="flex items-center gap-1.5">
<button
onClick={() => {
setRenamingOrgId(o.id);
setRenameOrgVal(o.name);
}}
className="btn-icon"
title="Umbenennen"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={() => setDeletingOrgId(deletingOrgId === o.id ? null : o.id)}
className="btn-icon btn-icon-danger"
title="Organisation löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
)}
</div>
{members.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{members.map((m) => (
<span
key={m.id}
className="inline-flex items-center gap-1.5 pl-0.5 pr-2 py-0.5 rounded-pill bg-panel border border-border text-[11px] text-main"
>
<UserAvatar user={m} size="xxs" showTitle={false} />
<span>{m.displayName}</span>
</span>
))}
</div>
)}
{deletingOrgId === o.id && (
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-border">
<AlertCircle className="w-3.5 h-3.5 text-status-warn shrink-0" />
<span className="flex-1 min-w-0 text-muted">
Die {o.memberCount} Mitglieder sind danach ohne Organisation und verlieren ihre
Freigaben, Gruppen und Zuweisungen untereinander.
</span>
<button onClick={() => handleDeleteOrganization(o.id)} className="btn-primary h-7 px-3 text-[11px]">
Löschen
</button>
<button onClick={() => setDeletingOrgId(null)} className="btn-action h-7 px-2 text-[11px]">
Abbrechen
</button>
</div>
)}
</div>
);
})}
</div>
)}
</div>
)}
{/* ================================================================= */}
{/* SECTION 3: SERVER & SHARING-ISOLATION (SERVER-ADMIN ONLY) */}
{/* ================================================================= */}
@@ -661,8 +1000,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
Server-Grundeinstellungen & Benutzer-Sichtbarkeit
</h2>
<p className="text-muted mt-0.5">
Als Server-Admin bestimmst du zentral für alle Nutzer, ob sie komplett isoliert
arbeiten, sich nur innerhalb ihrer Teams sehen oder ob alle Nutzer für alle sichtbar sind.
Als Server-Admin bestimmst du zentral, ob Nutzer komplett isoliert arbeiten, sich nur
innerhalb ihrer Organisation sehen oder ob alle Nutzer für alle sichtbar sind.
</p>
</div>
@@ -698,54 +1037,45 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<button
type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'strict_private' })}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'strict_private'
? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs text-main">1. Strikt Privat (Keiner sieht andere)</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">
Jeder Benutzer arbeitet komplett für sich allein. Keine geteilten Spaces und keine
Benutzersuche möglich.
</p>
</button>
<button
type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'teams_only' })}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'teams_only'
? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs text-main">2. Teams isoliert (Empfohlen)</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">
Benutzer sehen ausschließlich Personen, mit denen sie mindestens ein gemeinsames Team
teilen.
</p>
</button>
<button
type="button"
onClick={() => handleSaveSystemSettings({ sharingMode: 'open' })}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === 'open'
? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs text-main">3. Offen (Alle sehen alle)</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">
Alle registrierten Benutzer dieser Instanz können sich gegenseitig sehen und Spaces
miteinander teilen.
</p>
</button>
{SHARING_MODES.map((m) => (
<button
key={m.id}
type="button"
onClick={() => handleSelectSharingMode(m.id)}
className={`p-4 text-left border rounded-card transition-colors ${
settings.sharingMode === m.id
? 'bg-canvas border-accent text-main shadow-panel'
: 'bg-canvas/50 border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs text-main">{m.title}</p>
<p className="text-[11px] text-muted mt-1 leading-relaxed">{m.text}</p>
</button>
))}
</div>
{pendingMode && (
<div className="p-3 border border-status-warn/40 bg-status-warn/5 rounded-card flex flex-wrap items-center gap-2">
<AlertCircle className="w-4 h-4 text-status-warn shrink-0" />
<span className="flex-1 min-w-0 text-main">
Beim Wechsel zu „{SHARING_MODES.find((m) => m.id === pendingMode.mode)?.title.replace(/^\d\.\s*/, '')}“
fallen {pendingMode.affected} Freigaben und Gruppen-Mitgliedschaften weg, die danach nicht mehr erlaubt sind.
</span>
<button
onClick={() => {
const mode = pendingMode.mode;
setPendingMode(null);
handleSaveSystemSettings({ sharingMode: mode });
}}
className="btn-primary h-7 px-3 text-[11px]"
>
Trotzdem wechseln
</button>
<button onClick={() => setPendingMode(null)} className="btn-action h-7 px-2 text-[11px]">
Abbrechen
</button>
</div>
)}
</div>
{/* Public Registration Toggle Switch */}
@@ -762,25 +1092,11 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
<span className={`text-xs font-medium hidden sm:inline ${settings.allowRegistration ? 'text-accent' : 'text-muted'}`}>
{settings.allowRegistration ? 'Aktiviert (Offen)' : 'Gesperrt (Nur Einladung)'}
</span>
<button
type="button"
role="switch"
aria-checked={settings.allowRegistration}
onClick={() =>
handleSaveSystemSettings({ allowRegistration: !settings.allowRegistration })
}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-pill border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
settings.allowRegistration ? 'bg-accent' : 'bg-panel-elevated border border-border'
}`}
<ToggleSwitch
checked={settings.allowRegistration}
onChange={(allowRegistration) => handleSaveSystemSettings({ allowRegistration })}
title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'}
>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-5 w-5 transform rounded-pill bg-white shadow-md ring-0 transition duration-200 ease-in-out ${
settings.allowRegistration ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
/>
</div>
</div>
</div>