Release 0.2.0
This commit is contained in:
1 parent
7a49e66c37
commit
08e8875cb5
38 files changed
+2645
-93
No files matched your search
@@ -1,5 +1,97 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getTaxonomy } from '#lib/server/previews.js';
|
||||
import { getMailConfig } from '#lib/server/config.js';
|
||||
import {
|
||||
inviteUser,
|
||||
isRole,
|
||||
listOpenInvitations,
|
||||
listUsers,
|
||||
requireBarkeeper,
|
||||
revokeInvitation,
|
||||
setUserLocked,
|
||||
setUserRole,
|
||||
validEmail
|
||||
} from '#lib/server/auth.js';
|
||||
|
||||
export function load() {
|
||||
return { taxonomy: getTaxonomy() };
|
||||
export const load: PageServerLoad = ({ locals }) => {
|
||||
requireBarkeeper(locals.user);
|
||||
return {
|
||||
taxonomy: getTaxonomy(),
|
||||
users: listUsers(),
|
||||
invitations: listOpenInvitations(),
|
||||
mailTransport: getMailConfig().transport
|
||||
};
|
||||
};
|
||||
|
||||
function actionError(cause: unknown): string {
|
||||
const code = cause instanceof Error ? cause.message : '';
|
||||
if (code === 'USER_EXISTS') return 'Für diese E-Mail existiert bereits ein Konto.';
|
||||
if (code === 'SELF_LOCK') return 'Das eigene Konto kann nicht gesperrt werden.';
|
||||
if (code === 'LAST_BARKEEPER') return 'Mindestens ein aktiver Barkeeper muss erhalten bleiben.';
|
||||
if (code === 'NOT_FOUND') return 'Das Konto wurde nicht gefunden.';
|
||||
return 'Die Aktion konnte nicht ausgeführt werden.';
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
invite: async ({ locals, request }) => {
|
||||
const actor = requireBarkeeper(locals.user);
|
||||
const data = await request.formData();
|
||||
const email = String(data.get('email') || '').trim();
|
||||
const role = String(data.get('role') || '');
|
||||
if (!validEmail(email) || !isRole(role)) {
|
||||
return fail(400, { intent: 'invite', email, error: 'E-Mail und Rolle bitte prüfen.' });
|
||||
}
|
||||
try {
|
||||
const transport = await inviteUser(actor, email, role);
|
||||
return {
|
||||
intent: 'invite',
|
||||
success:
|
||||
transport === 'local'
|
||||
? 'Einladung wurde in der lokalen Mail-Outbox abgelegt.'
|
||||
: 'Einladung wurde versandt.'
|
||||
};
|
||||
} catch (cause) {
|
||||
return fail(400, { intent: 'invite', email, error: actionError(cause) });
|
||||
}
|
||||
},
|
||||
role: async ({ locals, request }) => {
|
||||
const actor = requireBarkeeper(locals.user);
|
||||
const data = await request.formData();
|
||||
const userId = String(data.get('userId') || '');
|
||||
const role = String(data.get('role') || '');
|
||||
if (!userId || !isRole(role)) return fail(400, { intent: 'role', error: 'Ungültige Rolle.' });
|
||||
try {
|
||||
setUserRole(actor, userId, role);
|
||||
return { intent: 'role', success: 'Rolle aktualisiert.' };
|
||||
} catch (cause) {
|
||||
return fail(400, { intent: 'role', error: actionError(cause) });
|
||||
}
|
||||
},
|
||||
lock: async ({ locals, request }) => {
|
||||
const actor = requireBarkeeper(locals.user);
|
||||
const data = await request.formData();
|
||||
try {
|
||||
setUserLocked(actor, String(data.get('userId') || ''), true);
|
||||
return { intent: 'lock', success: 'Konto gesperrt und bestehende Sitzungen beendet.' };
|
||||
} catch (cause) {
|
||||
return fail(400, { intent: 'lock', error: actionError(cause) });
|
||||
}
|
||||
},
|
||||
unlock: async ({ locals, request }) => {
|
||||
const actor = requireBarkeeper(locals.user);
|
||||
const data = await request.formData();
|
||||
try {
|
||||
setUserLocked(actor, String(data.get('userId') || ''), false);
|
||||
return { intent: 'unlock', success: 'Konto entsperrt.' };
|
||||
} catch (cause) {
|
||||
return fail(400, { intent: 'unlock', error: actionError(cause) });
|
||||
}
|
||||
},
|
||||
revokeInvitation: async ({ locals, request }) => {
|
||||
const actor = requireBarkeeper(locals.user);
|
||||
const data = await request.formData();
|
||||
revokeInvitation(actor, String(data.get('invitationId') || ''));
|
||||
return { intent: 'revokeInvitation', success: 'Einladung widerrufen.' };
|
||||
}
|
||||
} satisfies Actions;
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
let { data }: { data: PageData } = $props();
|
||||
import type { PageProps } from './$types';
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Verwaltung | Sendewerk</title></svelte:head>
|
||||
@@ -8,8 +8,108 @@
|
||||
<div class="page-wrap narrow-page">
|
||||
<header class="page-heading">
|
||||
<h1>Verwaltung</h1>
|
||||
<p class="page-intro">Abschnittskategorien, Formate und Themen im Überblick.</p>
|
||||
<p class="page-intro">Konten, Rollen und bestehende Taxonomie verwalten.</p>
|
||||
</header>
|
||||
{#if form?.error}<div class="form-message error" role="alert">{form.error}</div>{/if}
|
||||
{#if form?.success}<div class="form-message success" role="status">{form.success}</div>{/if}
|
||||
|
||||
<section class="quiet-panel account-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Konten & Rollen</p>
|
||||
<h2>Redaktionszugänge</h2>
|
||||
</div>
|
||||
<span class="mail-mode"
|
||||
>Mail: {data.mailTransport === 'local' ? 'lokale Outbox' : 'SMTP'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="account-list">
|
||||
{#each data.users as user (user.id)}
|
||||
<article class="account-row">
|
||||
<div><strong>{user.displayName}</strong><small>{user.email}</small></div>
|
||||
<span class:locked={user.lockedAtUtc} class="account-status"
|
||||
>{user.lockedAtUtc ? 'Gesperrt' : 'Aktiv'}</span
|
||||
>
|
||||
<form method="POST" action="?/role" class="inline-account-form">
|
||||
<input type="hidden" name="userId" value={user.id} />
|
||||
<label class="sr-only" for={`role-${user.id}`}>Rolle für {user.displayName}</label>
|
||||
<select id={`role-${user.id}`} name="role">
|
||||
<option value="barkeeper" selected={user.role === 'barkeeper'}>Barkeeper</option>
|
||||
<option value="thekencrew" selected={user.role === 'thekencrew'}>Thekencrew</option>
|
||||
</select>
|
||||
<button class="text-button" type="submit">Rolle speichern</button>
|
||||
</form>
|
||||
<form method="POST" action={user.lockedAtUtc ? '?/unlock' : '?/lock'}>
|
||||
<input type="hidden" name="userId" value={user.id} />
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="submit"
|
||||
disabled={user.id === data.user?.id}
|
||||
title={user.id === data.user?.id
|
||||
? 'Das eigene Konto kann nicht gesperrt werden.'
|
||||
: undefined}
|
||||
>{user.lockedAtUtc
|
||||
? 'Entsperren'
|
||||
: user.id === data.user?.id
|
||||
? 'Eigenes Konto'
|
||||
: 'Sperren'}</button
|
||||
>
|
||||
</form>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="management-grid">
|
||||
<section class="quiet-panel invite-panel">
|
||||
<p class="eyebrow">Einladung</p>
|
||||
<h2>Neues Konto einladen</h2>
|
||||
<form class="compact-form" method="POST" action="?/invite">
|
||||
<label
|
||||
>E-Mail<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
maxlength="254"
|
||||
value={form?.intent === 'invite' ? (form.email ?? '') : ''}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Rolle<select name="role"
|
||||
><option value="thekencrew">Thekencrew</option><option value="barkeeper"
|
||||
>Barkeeper</option
|
||||
></select
|
||||
></label
|
||||
>
|
||||
<button class="primary-button" type="submit">Einladung erstellen</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="quiet-panel invite-panel">
|
||||
<p class="eyebrow">Offene Einladungen</p>
|
||||
<h2>Noch nicht angenommen</h2>
|
||||
{#if data.invitations.length}
|
||||
<div class="invitation-list">
|
||||
{#each data.invitations as invitation (invitation.id)}
|
||||
<div>
|
||||
<span
|
||||
><strong>{invitation.email}</strong><small
|
||||
>{invitation.role === 'barkeeper' ? 'Barkeeper' : 'Thekencrew'}</small
|
||||
></span
|
||||
>
|
||||
<form method="POST" action="?/revokeInvitation">
|
||||
<input type="hidden" name="invitationId" value={invitation.id} /><button
|
||||
class="text-button"
|
||||
type="submit">Widerrufen</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}<p class="quiet-copy">Keine offenen Einladungen.</p>{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h2 class="section-heading">Taxonomie</h2>
|
||||
<div class="settings-grid">
|
||||
<section class="quiet-panel taxonomy-panel">
|
||||
<p class="eyebrow">Abschnittskategorien</p>
|
||||
@@ -35,5 +135,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<p class="settings-footnote">Konten und Rollen sind noch nicht verfügbar.</p>
|
||||
</div>
|
||||
Reference in new issue
Block a user