Release 0.6.0
This commit is contained in:
1 parent
fd47910d09
commit
b80dded55d
40 files changed
+4080
-856
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { getAuthConfig } from '#lib/server/config.js';
|
||||
import { authenticateShare, getSharedShow, SHARE_SESSION_COOKIE } from '#lib/server/shares.js';
|
||||
|
||||
export const load: PageServerLoad = ({ params, cookies }) => {
|
||||
return getSharedShow(params.token, cookies.get(SHARE_SESSION_COOKIE));
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
password: async ({ params, request, cookies, getClientAddress }) => {
|
||||
const data = await request.formData();
|
||||
try {
|
||||
const session = await authenticateShare(
|
||||
params.token,
|
||||
String(data.get('password') || ''),
|
||||
getClientAddress()
|
||||
);
|
||||
cookies.set(SHARE_SESSION_COOKIE, session.token, {
|
||||
path: `/freigabe/${params.token}`,
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: getAuthConfig().secureCookies,
|
||||
expires: session.expires
|
||||
});
|
||||
return { success: true };
|
||||
} catch {
|
||||
return fail(401, { error: 'Passwort falsch oder Freigabe nicht mehr verfügbar.' });
|
||||
}
|
||||
}
|
||||
} satisfies Actions;
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { formatDuration } from '#lib/format.js';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
function berlinDate(value: string | null) {
|
||||
if (!value) return 'Termin nicht angegeben';
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short'
|
||||
}).format(new Date(value));
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Sendungsfreigabe | Sendewerk</title></svelte:head>
|
||||
|
||||
<main class="share-page">
|
||||
{#if data.status === 'unavailable'}
|
||||
<section class="quiet-panel share-gate">
|
||||
<p class="eyebrow">Lesefreigabe</p>
|
||||
<h1>Nicht verfügbar</h1>
|
||||
<p>Dieser Link ist ungültig, abgelaufen oder wurde widerrufen.</p>
|
||||
</section>
|
||||
{:else if data.status === 'password'}
|
||||
<section class="quiet-panel share-gate">
|
||||
<p class="eyebrow">Geschützte Lesefreigabe</p>
|
||||
<h1>Passwort erforderlich</h1>
|
||||
{#if form?.error}<div class="form-message error" role="alert">{form.error}</div>{/if}
|
||||
<form method="POST" action="?/password" class="compact-form">
|
||||
<label
|
||||
>Passwort<input
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/></label
|
||||
>
|
||||
<button class="primary-button" type="submit">Sendung öffnen</button>
|
||||
</form>
|
||||
</section>
|
||||
{:else}
|
||||
<header class="share-heading">
|
||||
<p class="eyebrow">
|
||||
{data.share.isCompleted ? 'Abgeschlossene Sendung' : 'Aktuelle Sendungsplanung'} · Nur lesen
|
||||
</p>
|
||||
<h1>{data.share.title}</h1>
|
||||
<p>{berlinDate(data.share.startsAtUtc)} Uhr</p>
|
||||
</header>
|
||||
<div class="shared-rundown">
|
||||
{#each data.share.sections as section (section)}
|
||||
<section class="quiet-panel shared-section">
|
||||
<header>
|
||||
<div>
|
||||
{#if section.category}<small
|
||||
style={`--tax-color: ${section.categoryColor ?? '#67dde8'}`}
|
||||
>{section.category}</small
|
||||
>{/if}
|
||||
<h2>{section.name}</h2>
|
||||
</div>
|
||||
{#if section.explicitDurationSeconds !== null}<span
|
||||
>{formatDuration(section.explicitDurationSeconds)}</span
|
||||
>{/if}
|
||||
</header>
|
||||
{#each section.usages as usage (usage)}
|
||||
<article class="shared-usage">
|
||||
<div class="shared-usage-heading">
|
||||
<div>
|
||||
<small
|
||||
>{usage.status === 'done'
|
||||
? 'Gesendet'
|
||||
: usage.status === 'skipped'
|
||||
? 'Übersprungen'
|
||||
: 'Offen'}</small
|
||||
>
|
||||
<h3>{usage.title}</h3>
|
||||
</div>
|
||||
<span>{formatDuration(usage.actualDurationSeconds || usage.durationSeconds)}</span>
|
||||
</div>
|
||||
{#if usage.media}<img
|
||||
src={`${page.url.pathname}/media/${usage.media.id}`}
|
||||
alt={usage.media.alt_text}
|
||||
/>{/if}
|
||||
{#if 'introHtml' in usage && usage.introHtml}<div class="shared-copy">
|
||||
<strong>Einleitung</strong>
|
||||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div>{@html usage.introHtml}</div>
|
||||
</div>{/if}
|
||||
{#if 'moderationHtml' in usage && usage.moderationHtml}<div class="shared-copy">
|
||||
<strong>Moderation</strong>
|
||||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div>{@html usage.moderationHtml}</div>
|
||||
</div>{/if}
|
||||
{#if 'interactionHtml' in usage && usage.interactionHtml}<div class="shared-copy">
|
||||
<strong>Interaktion</strong>
|
||||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div>{@html usage.interactionHtml}</div>
|
||||
</div>{/if}
|
||||
{#if 'transitionHtml' in usage && usage.transitionHtml}<div class="shared-copy">
|
||||
<strong>Überleitung</strong>
|
||||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div>{@html usage.transitionHtml}</div>
|
||||
</div>{/if}
|
||||
{#if 'researchHtml' in usage && usage.researchHtml}<details>
|
||||
<summary>Recherche</summary>
|
||||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div class="shared-copy">{@html usage.researchHtml}</div>
|
||||
</details>{/if}
|
||||
{#if 'sources' in usage && usage.sources?.length}<details>
|
||||
<summary>Quellen</summary>
|
||||
<ul>
|
||||
{#each usage.sources ?? [] as source (source)}<li>
|
||||
<!-- External source URLs are validated and stored server-side. -->
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={source.url} rel="noreferrer">{source.label || source.url}</a>
|
||||
</li>{/each}
|
||||
</ul>
|
||||
</details>{/if}
|
||||
{#if 'tools' in usage && usage.tools?.length}<details>
|
||||
<summary>Werkzeuge</summary>
|
||||
<ul>
|
||||
{#each usage.tools ?? [] as tool (tool)}<li>
|
||||
<strong>{tool.name}</strong>{#if tool.note}
|
||||
· {tool.note}{/if}
|
||||
</li>{/each}
|
||||
</ul>
|
||||
</details>{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSharedMedia, SHARE_SESSION_COOKIE } from '#lib/server/shares.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const media = await getSharedMedia(params.token, cookies.get(SHARE_SESSION_COOKIE), params.id);
|
||||
if (!media) error(404, 'Bild nicht gefunden');
|
||||
return new Response(media.data, {
|
||||
headers: {
|
||||
'Content-Type': media.mimeType,
|
||||
'Content-Length': String(media.byteSize),
|
||||
'Cache-Control': 'private, no-store',
|
||||
'Content-Security-Policy': "default-src 'none'; sandbox",
|
||||
'Cross-Origin-Resource-Policy': 'same-origin',
|
||||
'X-Content-Type-Options': 'nosniff'
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user