Harden audit privacy and admin modals
CI - Build & Verify / Build, Typecheck & Hygiene (push) Successful in 56s
CI - Build & Verify / Deploy to award.noveria.net (push) Successful in 1m5s

This commit is contained in:
AzuTear
2026-06-26 20:47:07 +02:00
parent aeade132f9
commit 9393d433fa
17 changed files with 741 additions and 315 deletions
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { LogOut, Shield, Trash2, X } from '@lucide/vue'
import type { AuthSession } from '../../types/awards'
@@ -29,13 +30,14 @@ const isTeamPasswordSession = computed(() => Boolean(props.session?.teamLogin &&
const isTeamSession = computed(() => Boolean(props.session?.teamLogin))
const isParticipantSession = computed(() => !isTeamSession.value)
const profileHandle = computed(() => props.session?.teamLogin ?? props.session?.twitchUserId ?? props.twitchUser)
const displayInitial = computed(() => (props.session?.displayName ?? profileHandle.value).charAt(0).toUpperCase())
const showTeamLoginDetail = computed(() => Boolean(props.session?.teamLogin && !isTeamSession.value))
const statusLabel = computed(() => {
const authStatusLabel = computed(() => {
if (isTeamPasswordSession.value) return 'Angemeldet mit Team-Login'
if (isTeamSession.value) return 'Admin-Login über Twitch'
return 'Angemeldet über Twitch'
})
const statusText = computed(() => {
const authStatusText = computed(() => {
if (isTeamPasswordSession.value) return 'Diese Session läuft über deinen Team-Login.'
if (isTeamSession.value) return 'Diese Admin-Session nutzt deinen verknüpften Twitch-Account.'
return 'Diese Session nutzt deinen Twitch-Account.'
@@ -45,19 +47,34 @@ const logoutLabel = computed(() => !isTeamSession.value ? 'Von Twitch abmelden'
</script>
<template>
<!-- Privacy Modal -->
<template v-if="props.privacyModalOpen">
<div class="home-modal-overlay" @click="props.onClosePrivacy" style="position:fixed;inset:0;z-index:400;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(40,24,70,.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);">
<div class="home-modal" @click="props.privacyModalStop" style="position:relative;width:100%;max-width:680px;max-height:88vh;overflow:hidden;display:flex;flex-direction:column;background:#fff;border-radius:24px;box-shadow:0 40px 90px rgba(40,24,70,.4);">
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 32px 18px;border-bottom:1px solid #f1ecfb;flex:none;">
<div
class="fixed inset-0 z-[400] flex items-center justify-center p-6"
style="background:rgba(40,24,70,0.52);backdrop-filter:blur(6px);"
@click="props.onClosePrivacy"
>
<div
class="flex max-h-[88vh] w-full max-w-[680px] flex-col overflow-hidden rounded-3xl bg-white shadow-[0_40px_90px_rgba(40,24,70,0.4)]"
@click="props.privacyModalStop"
>
<div class="flex shrink-0 items-center justify-between gap-4 border-b border-[#f1ecfb] px-8 py-5">
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Rechtliches</div>
<h2 style="font-family:'Cormorant Garamond',serif;font-weight:700;font-size:26px;margin:0;color:#3f3556;">Datenschutzerklärung</h2>
<p class="text-[10px] font-bold uppercase tracking-[0.2em] text-[#8b6cdb]">Rechtliches</p>
<h2 class="font-['Cormorant_Garamond'] text-2xl font-bold text-[#3f3556]">Datenschutzerklärung</h2>
</div>
<button @click="props.onClosePrivacy" style="width:34px;height:34px;border-radius:50%;border:none;background:#f1ecfb;color:#8b6cdb;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;" style-hover="background:#e6dcf6;"></button>
<button
type="button"
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-[#f1ecfb] text-[#8b6cdb] transition hover:bg-[#e6dcf6]"
aria-label="Datenschutz schließen"
@click="props.onClosePrivacy"
>
<X class="h-4 w-4" />
</button>
</div>
<div style="overflow-y:auto;padding:28px 32px;flex:1;font-family:'Outfit',sans-serif;font-size:14px;line-height:1.7;color:#6f6685;">
<div v-if="props.privacyContentHtml" class="home-privacy-content" v-html="props.privacyContentHtml" />
<div v-else style="margin:0;padding:14px 16px;border-radius:16px;border:1px solid #f1ecfb;background:#fcfbff;">
<div class="flex-1 overflow-y-auto px-8 py-7 text-sm leading-relaxed text-[#6f6685]">
<div v-if="props.privacyContentHtml" class="privacy-content" v-html="props.privacyContentHtml" />
<div v-else class="rounded-2xl border border-[#f1ecfb] bg-[#fcfbff] px-4 py-3 text-sm">
Datenschutzerklärung wird geladen.
</div>
</div>
@@ -65,60 +82,150 @@ const logoutLabel = computed(() => !isTeamSession.value ? 'Von Twitch abmelden'
</div>
</template>
<!-- Account Modal -->
<template v-if="props.accountModalOpen">
<div class="home-modal-overlay" @click="props.onCloseAccount" style="position:fixed;inset:0;z-index:300;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(40,24,70,.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);">
<div class="home-modal" @click="props.accountModalStop" style="position:relative;width:100%;max-width:480px;background:#fff;border-radius:24px;box-shadow:0 40px 90px rgba(40,24,70,.4);overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 28px 18px;border-bottom:1px solid #f1ecfb;">
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Mein Profil</div>
<h2 style="font-family:'Cormorant Garamond',serif;font-weight:700;font-size:24px;margin:0;color:#3f3556;">@{{ profileHandle }}</h2>
</div>
<button @click="props.onCloseAccount" style="width:34px;height:34px;border-radius:50%;border:none;background:#f1ecfb;color:#8b6cdb;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;" style-hover="background:#e6dcf6;"></button>
</div>
<div style="padding:24px 28px;display:flex;flex-direction:column;gap:16px;">
<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 12px;padding:14px 16px;border-radius:16px;background:#f6f1fd;border:1px solid #ede4fb;">
<svg v-if="isTeamPasswordSession" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b6cdb" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 7a4 4 0 1 1-2.8-3.82"/><path d="M13 5 21 13"/><path d="M21 13 18 16"/><path d="M18 13 16 15"/></svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b6cdb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 2L2.4 6v14.5h5V23h2.7l2.5-2.5h4L21.6 16V2H4zm15.3 13.1l-2.9 2.9h-4.4l-2.5 2.5v-2.5H6.7V3.7h12.6v11.4z"/><path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z"/></svg>
<span style="font-size:14px;color:#5f44ad;font-weight:800;">{{ statusLabel }}</span>
<span style="grid-column:2;font-size:12.5px;color:#6f6685;line-height:1.45;">{{ statusText }}</span>
</div>
<div style="padding:16px;border-radius:14px;background:#fafafa;border:1px solid #f0eafc;">
<p style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#a99fc0;margin:0 0 10px;">Gespeicherte Daten</p>
<div style="display:flex;flex-direction:column;gap:7px;">
<div class="home-account-data-row" style="display:flex;justify-content:space-between;font-size:13.5px;"><span style="color:#6f6685;">{{ primaryIdentityLabel }}</span><span style="font-weight:600;color:#3f3556;">@{{ profileHandle }}</span></div>
<div v-if="showTeamLoginDetail" class="home-account-data-row" style="display:flex;justify-content:space-between;font-size:13.5px;"><span style="color:#6f6685;">Team-Login</span><span style="font-weight:600;color:#3f3556;">@{{ props.session?.teamLogin }}</span></div>
<div v-if="props.session?.boundTwitchUserId" class="home-account-data-row" style="display:flex;justify-content:space-between;font-size:13.5px;"><span style="color:#6f6685;">Gebundenes Twitch</span><span style="font-weight:600;color:#3f3556;">@{{ props.session.boundTwitchUserId }}</span></div>
<div class="home-account-data-row" style="display:flex;justify-content:space-between;font-size:13.5px;"><span style="color:#6f6685;">Rolle</span><span style="font-weight:700;color:#8b6cdb;text-transform:uppercase;font-size:11px;letter-spacing:1px;">{{ props.role }}</span></div>
<div v-if="isParticipantSession" class="home-account-data-row" style="display:flex;justify-content:space-between;font-size:13.5px;"><span style="color:#6f6685;">Einreichungen</span><span style="font-weight:600;color:#3f3556;">Clips, Votes, Nominierungen</span></div>
<div v-if="isParticipantSession" class="home-account-data-row" style="display:flex;justify-content:space-between;font-size:13.5px;"><span style="color:#6f6685;">Löschfrist</span><span style="font-weight:600;color:#059669;">März 2027 (auto.)</span></div>
<div v-else class="home-account-data-row" style="display:flex;justify-content:space-between;gap:16px;font-size:13.5px;"><span style="color:#6f6685;">Accountverwaltung</span><span style="font-weight:600;color:#3f3556;text-align:right;">Team-Accounts bleiben bestehen und werden im Admin-Panel verwaltet.</span></div>
<div
class="fixed inset-0 z-[300] flex items-end justify-center p-4 sm:items-center sm:p-6"
style="background:rgba(40,24,70,0.52);backdrop-filter:blur(6px);"
@click="props.onCloseAccount"
>
<div
class="w-full max-w-[480px] overflow-hidden rounded-3xl bg-white shadow-[0_40px_90px_rgba(40,24,70,0.38)]"
@click="props.accountModalStop"
>
<!-- Header -->
<div class="flex items-center justify-between gap-4 border-b border-[#f1ecfb] bg-gradient-to-br from-[#f7f2ff] to-[#fff7ed] px-7 py-5">
<div class="flex min-w-0 items-center gap-3.5">
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-gradient-to-br from-[#8b6cdb] to-[#7355c8] text-base font-bold text-white shadow-[0_6px_18px_rgba(124,86,196,0.3)]">
{{ displayInitial }}
</div>
<div class="min-w-0">
<p class="text-[10px] font-bold uppercase tracking-[0.2em] text-[#8b6cdb]">Mein Profil</p>
<h2 class="truncate font-['Cormorant_Garamond'] text-2xl font-bold leading-tight text-[#3f3556]">
@{{ profileHandle }}
</h2>
</div>
</div>
<button @click="props.onOpenPrivacy" style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:12px;border:1px solid #ede4fb;background:#f9f6ff;color:#6a4fb8;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;width:100%;" style-hover="background:#f1ecfb;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Datenschutzerklärung lesen
<button
type="button"
class="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-[#f1ecfb] text-[#8b6cdb] transition hover:bg-[#e6dcf6]"
aria-label="Profil schließen"
@click="props.onCloseAccount"
>
<X class="h-4 w-4" />
</button>
<button @click="props.onLogout" style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:12px;border:1px solid #e2e8f0;background:#f8fafc;color:#64748b;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;width:100%;" style-hover="background:#f1f5f9;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
{{ logoutLabel }}
</button>
<template v-if="isParticipantSession && props.deleteNotConfirm">
<button @click="props.onRequestDelete" style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:12px;border:1px solid #fecdd3;background:#fff5f5;color:#e11d48;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;width:100%;" style-hover="background:#ffe4e6;">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg>
Meine Daten löschen (Löschrecht Art. 17 DSGVO)
</button>
</template>
<template v-if="isParticipantSession && props.deleteConfirm">
<div style="padding:16px;border-radius:14px;background:#fff5f5;border:1.5px solid #fecdd3;">
<p style="font-size:13.5px;color:#9f1239;font-weight:600;margin:0 0 6px;">Alle deine Daten werden sofort gelöscht.</p>
<p style="font-size:12.5px;color:#e11d48;margin:0 0 14px;line-height:1.5;">Das umfasst deine Votes, Nominierungen und Clip-Einreichungen. Diese Aktion kann nicht rückgängig gemacht werden.</p>
<p v-if="props.accountActionError" style="font-size:12.5px;color:#be123c;margin:0 0 12px;line-height:1.5;font-weight:600;">{{ props.accountActionError }}</p>
<div class="home-account-confirm-actions" style="display:flex;gap:8px;">
<button :disabled="props.authLoading" @click="props.onCancelDelete" style="flex:1;padding:10px;border-radius:10px;border:1px solid #e6dcf6;background:white;color:#6f6685;font-family:'Outfit',sans-serif;font-weight:600;font-size:13px;cursor:pointer;" style-hover="background:#f9f6ff;">Abbrechen</button>
<button :disabled="props.authLoading" @click="props.onConfirmDelete" style="flex:1;padding:10px;border-radius:10px;border:none;background:#e11d48;color:white;font-family:'Outfit',sans-serif;font-weight:700;font-size:13px;cursor:pointer;" style-hover="opacity:0.9;">{{ props.authLoading ? 'Löscht …' : 'Jetzt löschen' }}</button>
</div>
<!-- Scrollable body -->
<div class="max-h-[72vh] overflow-y-auto">
<div class="space-y-3 p-6">
<!-- Auth status -->
<div class="grid grid-cols-[auto_1fr] items-start gap-x-3 gap-y-1 rounded-2xl border border-[#ede4fb] bg-[#f6f1fd] px-4 py-3.5">
<div class="mt-0.5 shrink-0">
<svg v-if="isTeamPasswordSession" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b6cdb" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M15 7a4 4 0 1 1-2.8-3.82"/><path d="M13 5 21 13"/><path d="M21 13 18 16"/><path d="M18 13 16 15"/>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="#9146FF" aria-hidden="true">
<path d="M4 2L2.4 6v14.5h5V23h2.7l2.5-2.5h4L21.6 16V2H4zm15.3 13.1l-2.9 2.9h-4.4l-2.5 2.5v-2.5H6.7V3.7h12.6v11.4z"/>
<path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z"/>
</svg>
</div>
<span class="text-sm font-extrabold text-[#5f44ad]">{{ authStatusLabel }}</span>
<span class="col-start-2 text-xs leading-relaxed text-[#6f6685]">{{ authStatusText }}</span>
</div>
<!-- Saved data -->
<div class="rounded-2xl border border-[#ede4fb] bg-[#fafafa] px-4 py-3.5">
<p class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-[#a99fc0]">Gespeicherte Daten</p>
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-[#6f6685]">{{ primaryIdentityLabel }}</span>
<span class="font-semibold text-[#3f3556]">@{{ profileHandle }}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-[#6f6685]">Rolle</span>
<span class="text-[10px] font-bold uppercase tracking-[0.1em] text-[#8b6cdb]">{{ props.role }}</span>
</div>
<div v-if="showTeamLoginDetail" class="flex items-center justify-between text-sm">
<span class="text-[#6f6685]">Team-Login</span>
<span class="font-semibold text-[#3f3556]">@{{ props.session?.teamLogin }}</span>
</div>
<div v-if="props.session?.boundTwitchUserId" class="flex items-center justify-between text-sm">
<span class="text-[#6f6685]">Gebundenes Twitch</span>
<span class="font-semibold text-[#3f3556]">@{{ props.session.boundTwitchUserId }}</span>
</div>
<template v-if="isParticipantSession">
<div class="flex items-center justify-between text-sm">
<span class="text-[#6f6685]">Einreichungen</span>
<span class="font-semibold text-[#3f3556]">Clips, Votes, Nominierungen</span>
</div>
</template>
<template v-else>
<div class="flex items-start justify-between gap-4 text-sm">
<span class="shrink-0 text-[#6f6685]">Accountverwaltung</span>
<span class="text-right font-semibold text-[#3f3556]">Team-Accounts werden im Admin-Panel verwaltet.</span>
</div>
</template>
</div>
</div>
</template>
<!-- Privacy -->
<button
type="button"
class="flex w-full items-center gap-3 rounded-xl border border-[#ede4fb] bg-[#f9f6ff] px-4 py-3 text-left text-sm font-semibold text-[#6a4fb8] transition hover:bg-[#f1ecfb]"
@click="props.onOpenPrivacy"
>
<Shield class="h-4 w-4 shrink-0" />
Datenschutzerklärung lesen
</button>
<!-- Logout -->
<button
type="button"
class="flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-left text-sm font-semibold text-slate-500 transition hover:bg-slate-100"
@click="props.onLogout"
>
<LogOut class="h-4 w-4 shrink-0" />
{{ logoutLabel }}
</button>
<!-- Delete request -->
<template v-if="isParticipantSession && props.deleteNotConfirm">
<button
type="button"
class="flex w-full items-center gap-3 rounded-xl border border-rose-100 bg-rose-50 px-4 py-3 text-left text-sm font-semibold text-rose-600 transition hover:bg-rose-100"
@click="props.onRequestDelete"
>
<Trash2 class="h-4 w-4 shrink-0" />
Meine Daten löschen (Löschrecht Art. 17 DSGVO)
</button>
</template>
<!-- Delete confirmation -->
<template v-if="isParticipantSession && props.deleteConfirm">
<div class="rounded-2xl border-[1.5px] border-rose-200 bg-rose-50 p-4">
<p class="text-sm font-semibold text-rose-800">Alle deine Daten werden sofort gelöscht.</p>
<p class="mt-1 text-xs leading-relaxed text-rose-600">Das umfasst Votes, Nominierungen und Clip-Einreichungen. Diese Aktion kann nicht rückgängig gemacht werden.</p>
<p v-if="props.accountActionError" class="mt-2 text-xs font-bold text-rose-700">{{ props.accountActionError }}</p>
<div class="mt-4 flex gap-2">
<button
:disabled="props.authLoading"
type="button"
class="flex-1 rounded-xl border border-[#e6dcf6] bg-white py-2.5 text-sm font-semibold text-[#6f6685] transition hover:bg-[#f9f6ff] disabled:opacity-50"
@click="props.onCancelDelete"
>Abbrechen</button>
<button
:disabled="props.authLoading"
type="button"
class="flex-1 rounded-xl bg-rose-600 py-2.5 text-sm font-bold text-white transition hover:bg-rose-700 disabled:opacity-50"
@click="props.onConfirmDelete"
>{{ props.authLoading ? 'Löscht …' : 'Jetzt löschen' }}</button>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
@@ -126,23 +233,20 @@ const logoutLabel = computed(() => !isTeamSession.value ? 'Von Twitch abmelden'
</template>
<style scoped>
.home-privacy-content :deep(p) {
.privacy-content :deep(p) {
margin: 0 0 14px;
}
.home-privacy-content :deep(p:last-child),
.home-privacy-content :deep(ul:last-child),
.home-privacy-content :deep(ol:last-child) {
.privacy-content :deep(p:last-child),
.privacy-content :deep(ul:last-child),
.privacy-content :deep(ol:last-child) {
margin-bottom: 0;
}
.home-privacy-content :deep(ul),
.home-privacy-content :deep(ol) {
.privacy-content :deep(ul),
.privacy-content :deep(ol) {
margin: 0 0 14px 20px;
padding-left: 16px;
}
.home-privacy-content :deep(li) {
.privacy-content :deep(li) {
margin: 3px 0;
}
</style>