Add team roles and content management updates

This commit is contained in:
AzuTear
2026-06-25 19:52:46 +02:00
parent 54293f4c45
commit 693769e5a8
126 changed files with 6966 additions and 738 deletions
+241 -18
View File
@@ -2,6 +2,7 @@
import { computed, reactive, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import PasswordField from '../components/ui/PasswordField.vue'
import { useAuthStore } from '../stores/auth'
import { ApiRequestError } from '../lib/http'
@@ -14,7 +15,17 @@ const form = reactive({
password: '',
})
const passwordForm = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: '',
})
const loginMode = ref<'password' | 'twitch'>('password')
const errorMessage = ref('')
const successMessage = ref('')
const passwordChangeRequired = computed(() => Boolean(authStore.session?.mustChangePassword))
const redirectTarget = computed(() => {
const redirect = Array.isArray(route.query.redirect)
@@ -30,26 +41,86 @@ const redirectTarget = computed(() => {
async function submitLogin() {
errorMessage.value = ''
successMessage.value = ''
try {
await authStore.demoLogin({
login: form.login,
password: form.password,
})
} catch (error) {
try {
await authStore.teamLogin({
login: form.login,
password: form.password,
})
} catch (teamError) {
if (error instanceof ApiRequestError && error.status === 503) {
errorMessage.value = 'Der Demo-Login ist im Backend noch nicht vollständig konfiguriert.'
return
}
if (error instanceof ApiRequestError && error.status === 404 && teamError instanceof ApiRequestError && teamError.status === 404) {
errorMessage.value = 'Der Team-Login ist auf diesem Deployment nicht aktiviert.'
return
}
errorMessage.value = 'Login oder Passwort stimmt nicht.'
return
}
}
if (authStore.session?.mustChangePassword) {
passwordForm.currentPassword = form.password
form.password = ''
successMessage.value = 'Bitte setze jetzt ein eigenes Passwort für diesen Team-Login.'
return
}
await router.replace(redirectTarget.value)
}
async function startTwitchLogin() {
errorMessage.value = ''
successMessage.value = ''
try {
await authStore.startTwitchAuthorization({
purpose: 'team-login',
returnUrl: redirectTarget.value,
})
} catch (error) {
errorMessage.value = error instanceof Error
? error.message
: 'Twitch-Login konnte nicht gestartet werden.'
}
}
async function submitPasswordChange() {
errorMessage.value = ''
successMessage.value = ''
if (passwordForm.newPassword.length < 10) {
errorMessage.value = 'Das neue Passwort muss mindestens 10 Zeichen lang sein.'
return
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
errorMessage.value = 'Die neuen Passwörter stimmen nicht überein.'
return
}
try {
await authStore.changePassword({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword,
})
await router.replace(redirectTarget.value)
} catch (error) {
if (error instanceof ApiRequestError && error.status === 404) {
errorMessage.value = 'Der Demo-Login ist auf diesem Deployment nicht aktiviert.'
return
}
if (error instanceof ApiRequestError && error.status === 503) {
errorMessage.value = 'Der Demo-Login ist im Backend noch nicht vollständig konfiguriert.'
return
}
errorMessage.value = 'Login oder Passwort stimmt nicht.'
errorMessage.value = error instanceof ApiRequestError
? error.message
: 'Das Passwort konnte nicht geändert werden.'
}
}
</script>
@@ -69,13 +140,35 @@ async function submitLogin() {
</RouterLink>
<section class="login-card" aria-labelledby="login-title">
<div class="login-card__eyebrow">Demo Zugang</div>
<h1 id="login-title">Admin Login</h1>
<div class="login-card__eyebrow">{{ passwordChangeRequired ? 'Erster Login' : 'Team Zugang' }}</div>
<h1 id="login-title">{{ passwordChangeRequired ? 'Passwort ändern' : 'Admin Login' }}</h1>
<p>
Melde dich mit den Demo-Credentials an, um das Admin-Panel für die Präsentation zu öffnen.
<template v-if="passwordChangeRequired">
Dieser Account wurde mit einem temporären Passwort erstellt. Bitte vergib jetzt ein eigenes Passwort.
</template>
<template v-else>
Melde dich mit Team-Login an oder nutze deinen gebundenen Twitch-Account.
</template>
</p>
<form class="login-form" @submit.prevent="submitLogin">
<div v-if="!passwordChangeRequired" class="login-mode-toggle" role="tablist" aria-label="Login-Art">
<button
type="button"
:class="loginMode === 'password' ? 'login-mode-toggle__button login-mode-toggle__button--active' : 'login-mode-toggle__button'"
@click="loginMode = 'password'"
>
Login
</button>
<button
type="button"
:class="loginMode === 'twitch' ? 'login-mode-toggle__button login-mode-toggle__button--active' : 'login-mode-toggle__button'"
@click="loginMode = 'twitch'"
>
Twitch
</button>
</div>
<form v-if="!passwordChangeRequired && loginMode === 'password'" class="login-form" @submit.prevent="submitLogin">
<label>
<span>Login</span>
<input
@@ -89,25 +182,99 @@ async function submitLogin() {
<label>
<span>Passwort</span>
<input
<PasswordField
v-model="form.password"
type="password"
variant="login"
autocomplete="current-password"
required
placeholder="Demo-Passwort eingeben"
>
/>
</label>
<p v-if="errorMessage" class="login-form__error" role="alert">
{{ errorMessage }}
</p>
<p v-if="successMessage" class="login-form__success" role="status">
{{ successMessage }}
</p>
<button class="login-form__submit" type="submit" :disabled="authStore.loading">
<span v-if="authStore.loading">Sternentor öffnet...</span>
<span v-else>Admin Panel öffnen</span>
</button>
</form>
<form v-else-if="!passwordChangeRequired" class="login-form" @submit.prevent="startTwitchLogin">
<div class="login-form__oauth-note">
<strong>Offizieller Twitch Login</strong>
<span>Du wirst zu Twitch weitergeleitet und danach automatisch zurück ins Admin Panel gebracht.</span>
</div>
<p v-if="errorMessage" class="login-form__error" role="alert">
{{ errorMessage }}
</p>
<p v-if="successMessage" class="login-form__success" role="status">
{{ successMessage }}
</p>
<button class="login-form__submit" type="submit" :disabled="authStore.loading">
<span v-if="authStore.loading">Twitch wird geöffnet...</span>
<span v-else>Mit Twitch offiziell anmelden</span>
</button>
</form>
<form v-else class="login-form" @submit.prevent="submitPasswordChange">
<label>
<span>Aktuelles Passwort</span>
<PasswordField
v-model="passwordForm.currentPassword"
variant="login"
autocomplete="current-password"
required
placeholder="Temporäres Passwort"
/>
</label>
<label>
<span>Neues Passwort</span>
<PasswordField
v-model="passwordForm.newPassword"
variant="login"
autocomplete="new-password"
required
minlength="10"
placeholder="Mindestens 10 Zeichen"
/>
</label>
<label>
<span>Passwort wiederholen</span>
<PasswordField
v-model="passwordForm.confirmPassword"
variant="login"
autocomplete="new-password"
required
minlength="10"
placeholder="Neues Passwort bestätigen"
/>
</label>
<p v-if="errorMessage" class="login-form__error" role="alert">
{{ errorMessage }}
</p>
<p v-if="successMessage" class="login-form__success" role="status">
{{ successMessage }}
</p>
<button class="login-form__submit" type="submit" :disabled="authStore.loading">
<span v-if="authStore.loading">Passwort wird gespeichert...</span>
<span v-else>Passwort speichern</span>
</button>
</form>
<div class="login-card__footer">
<RouterLink to="/">Zurück zur Landingpage</RouterLink>
<span>Demo-Modus</span>
@@ -253,6 +420,35 @@ async function submitLogin() {
margin-top: 30px;
}
.login-mode-toggle {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin: 24px 0 18px;
padding: 6px;
border: 1px solid rgba(124, 86, 196, 0.16);
border-radius: 16px;
background: rgba(255, 255, 255, 0.65);
}
.login-mode-toggle__button {
min-height: 42px;
border: 0;
border-radius: 11px;
background: transparent;
color: #7c7192;
font-family: 'Outfit', sans-serif;
font-size: 14px;
font-weight: 800;
cursor: pointer;
}
.login-mode-toggle__button--active {
background: #6f4fd1;
color: #fff;
box-shadow: 0 10px 22px rgba(111, 79, 209, 0.24);
}
.login-form label {
display: grid;
gap: 9px;
@@ -283,6 +479,23 @@ async function submitLogin() {
transform: translateY(-1px);
}
.login-form__oauth-note {
display: grid;
gap: 6px;
padding: 16px;
border: 1px solid rgba(145, 70, 255, 0.16);
border-radius: 18px;
background: rgba(248, 245, 255, 0.88);
color: #6f6685;
font-size: 14px;
line-height: 1.5;
}
.login-form__oauth-note strong {
color: #3f3556;
font-size: 15px;
}
.login-form__error {
padding: 13px 15px;
border: 1px solid rgba(225, 29, 72, 0.18);
@@ -293,6 +506,16 @@ async function submitLogin() {
font-weight: 800;
}
.login-form__success {
padding: 13px 15px;
border: 1px solid rgba(22, 163, 74, 0.18);
border-radius: 18px;
background: rgba(240, 253, 244, 0.9);
color: #15803d !important;
font-size: 14px !important;
font-weight: 800;
}
.login-form__submit {
min-height: 58px;
border: 0;