b53c7fb736
Features: - Category viewer ranges + subcategory templates (admin group modal, tree workspace) - Nomination enrichment via TwitchTracker API (NominationEnrichmentService, TwitchTrackerViewerStatsProvider) with admin tracking rules editor - Nomination group tracker: CategoryGroupName as primary identifier, CategoryId stays as nullable legacy field; StreamerIdentity table - Dynamic showact application form builder (AdminShowactFormBuilder, ShowactApplicationSchedule) - Session idle timeout setting (AdminSessionTimeoutCard) - Share URLs for X and Discord (SiteSettings, public extras) - Workflow rules now stored per season (falls back to global SiteSettings) - New admin routes: settings/access, settings/workflows, tracking-rules - New admin review workspace with subcategory tabs - AdminCategoriesView rebuilt with group/subcategory modals Migrations (all additive): - AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges, AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates, AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule, AddSeasonWorkflowRulesJson Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
import { ApiRequestError, api } from '../../lib/api'
|
|
import { clearSiteStatusCache } from '../../lib/siteStatus'
|
|
import type { AdminOperationalSettingsResponse } from '../../types/awards'
|
|
import type { AdminOperationalSettingsForm, AdminSettingsStatusSummary } from './adminSettingsTypes'
|
|
|
|
const fallbackMaintenanceTitle = 'Sternenpause'
|
|
const fallbackMaintenanceMessage = 'Die Award-Galaxie wird gerade liebevoll poliert. Schau gleich wieder vorbei.'
|
|
|
|
export function useAdminOperationalSettings() {
|
|
const operationalLoading = ref(true)
|
|
const operationalSaving = ref(false)
|
|
const operationalError = ref('')
|
|
const operationalSuccess = ref('')
|
|
const demoPasswordSet = ref(false)
|
|
const demoManagedByDatabase = ref(false)
|
|
const demoPasswordInput = ref('')
|
|
const twitchClientSecretSet = ref(false)
|
|
const twitchAuthConfigured = ref(false)
|
|
const twitchAuthManagedByDatabase = ref(false)
|
|
const twitchClientSecretInput = ref('')
|
|
const savedOperationalSnapshot = ref('')
|
|
|
|
const operationalForm = reactive<AdminOperationalSettingsForm>({
|
|
demoLoginEnabled: false,
|
|
demoLoginIdentifier: '',
|
|
demoLoginTwitchUserId: '',
|
|
demoLoginDisplayName: '',
|
|
twitchClientId: '',
|
|
twitchRedirectUri: '',
|
|
twitchScope: '',
|
|
sessionIdleTimeoutHours: 3,
|
|
maintenanceModeEnabled: false,
|
|
maintenanceTitle: fallbackMaintenanceTitle,
|
|
maintenanceMessage: fallbackMaintenanceMessage,
|
|
})
|
|
|
|
const demoPasswordHint = computed(() => {
|
|
if (demoPasswordInput.value.trim()) {
|
|
return 'Dieses neue Passwort wird beim Speichern gesetzt.'
|
|
}
|
|
|
|
if (demoPasswordSet.value) {
|
|
return demoManagedByDatabase.value
|
|
? 'Passwort ist gesetzt. Leer lassen, wenn es bleiben soll.'
|
|
: 'Aktuell kommt das Passwort aus der Demo-Config. Beim Speichern wird es in die DB übernommen.'
|
|
}
|
|
|
|
return 'Noch kein Demo-Passwort gesetzt. Zum Aktivieren bitte ein Passwort vergeben.'
|
|
})
|
|
|
|
const demoCredentialsComplete = computed(() =>
|
|
!operationalForm.demoLoginEnabled
|
|
|| (
|
|
Boolean(operationalForm.demoLoginIdentifier.trim())
|
|
&& Boolean(operationalForm.demoLoginTwitchUserId.trim())
|
|
&& Boolean(operationalForm.demoLoginDisplayName.trim())
|
|
&& (demoPasswordSet.value || Boolean(demoPasswordInput.value.trim()))
|
|
),
|
|
)
|
|
|
|
const twitchAuthComplete = computed(() =>
|
|
Boolean(operationalForm.twitchClientId.trim())
|
|
&& (twitchClientSecretSet.value || Boolean(twitchClientSecretInput.value.trim()))
|
|
)
|
|
|
|
const twitchSecretHint = computed(() => {
|
|
if (twitchClientSecretInput.value.trim()) {
|
|
return 'Dieses neue Secret wird beim Speichern gesetzt.'
|
|
}
|
|
|
|
if (twitchClientSecretSet.value) {
|
|
return 'Client Secret ist gesetzt. Leer lassen, wenn es bleiben soll.'
|
|
}
|
|
|
|
return 'Noch kein Client Secret gesetzt. Beim ersten Speichern erforderlich.'
|
|
})
|
|
|
|
const operationalSummary = computed<AdminSettingsStatusSummary[]>(() => [
|
|
{
|
|
label: 'Demo Login',
|
|
value: operationalForm.demoLoginEnabled ? 'Aktiv' : 'Aus',
|
|
note: operationalForm.demoLoginEnabled
|
|
? demoCredentialsComplete.value
|
|
? 'Öffentlicher Zugriff ist geschützt.'
|
|
: 'Aktivierung braucht vollständige Zugangsdaten.'
|
|
: 'Landingpage ist öffentlich erreichbar.',
|
|
tone: operationalForm.demoLoginEnabled
|
|
? demoCredentialsComplete.value ? 'warning' : 'danger'
|
|
: 'good',
|
|
},
|
|
{
|
|
label: 'Session Timeout',
|
|
value: `${operationalForm.sessionIdleTimeoutHours}h`,
|
|
note: `Logout nach ${operationalForm.sessionIdleTimeoutHours} Stunden ohne Aktivitaet.`,
|
|
tone: operationalForm.sessionIdleTimeoutHours >= 3 ? 'good' : 'danger',
|
|
},
|
|
{
|
|
label: 'Wartungsmodus',
|
|
value: operationalForm.maintenanceModeEnabled ? 'Aktiv' : 'Aus',
|
|
note: operationalForm.maintenanceModeEnabled
|
|
? 'Besucher sehen die Wartungsseite.'
|
|
: 'Öffentliche Seiten werden normal ausgeliefert.',
|
|
tone: operationalForm.maintenanceModeEnabled ? 'warning' : 'good',
|
|
},
|
|
{
|
|
label: 'Twitch OAuth',
|
|
value: twitchAuthConfigured.value ? 'Konfiguriert' : 'Fehlt',
|
|
note: twitchAuthConfigured.value
|
|
? twitchAuthManagedByDatabase.value ? 'Quelle: Datenbank' : 'Quelle: App-Konfiguration'
|
|
: 'Offizieller Twitch Login ist noch nicht aktiv.',
|
|
tone: twitchAuthConfigured.value ? 'good' : 'warning',
|
|
},
|
|
{
|
|
label: 'Passwort',
|
|
value: demoPasswordSet.value ? 'Gesetzt' : 'Fehlt',
|
|
note: demoManagedByDatabase.value ? 'Quelle: Datenbank' : 'Quelle: App-Konfiguration',
|
|
tone: demoPasswordSet.value ? 'good' : 'warning',
|
|
},
|
|
])
|
|
|
|
const hasUnsavedOperationalChanges = computed(() =>
|
|
Boolean(savedOperationalSnapshot.value) &&
|
|
buildOperationalSnapshot() !== savedOperationalSnapshot.value,
|
|
)
|
|
|
|
function applyOperationalSettings(response: AdminOperationalSettingsResponse) {
|
|
operationalForm.demoLoginEnabled = response.demoLoginEnabled
|
|
operationalForm.demoLoginIdentifier = response.demoLoginEmail
|
|
operationalForm.demoLoginTwitchUserId = response.demoLoginTwitchUserId
|
|
operationalForm.demoLoginDisplayName = response.demoLoginDisplayName
|
|
operationalForm.twitchClientId = response.twitchClientId
|
|
operationalForm.twitchRedirectUri = response.twitchRedirectUri
|
|
operationalForm.twitchScope = response.twitchScope
|
|
operationalForm.sessionIdleTimeoutHours = response.sessionIdleTimeoutHours
|
|
operationalForm.maintenanceModeEnabled = response.maintenanceModeEnabled
|
|
operationalForm.maintenanceTitle = response.maintenanceTitle
|
|
operationalForm.maintenanceMessage = response.maintenanceMessage
|
|
demoPasswordSet.value = response.demoLoginPasswordSet
|
|
demoManagedByDatabase.value = response.demoLoginManagedByDatabase
|
|
twitchClientSecretSet.value = response.twitchClientSecretSet
|
|
twitchAuthConfigured.value = response.twitchAuthConfigured
|
|
twitchAuthManagedByDatabase.value = response.twitchAuthManagedByDatabase
|
|
demoPasswordInput.value = ''
|
|
twitchClientSecretInput.value = ''
|
|
rememberSavedOperationalSettings()
|
|
}
|
|
|
|
function buildOperationalSnapshot() {
|
|
return JSON.stringify({
|
|
demoLoginEnabled: operationalForm.demoLoginEnabled,
|
|
demoLoginIdentifier: operationalForm.demoLoginIdentifier,
|
|
demoLoginTwitchUserId: operationalForm.demoLoginTwitchUserId,
|
|
demoLoginDisplayName: operationalForm.demoLoginDisplayName,
|
|
twitchClientId: operationalForm.twitchClientId,
|
|
twitchRedirectUri: operationalForm.twitchRedirectUri,
|
|
twitchScope: operationalForm.twitchScope,
|
|
sessionIdleTimeoutHours: operationalForm.sessionIdleTimeoutHours,
|
|
twitchClientSecretInput: twitchClientSecretInput.value,
|
|
maintenanceModeEnabled: operationalForm.maintenanceModeEnabled,
|
|
maintenanceTitle: operationalForm.maintenanceTitle,
|
|
maintenanceMessage: operationalForm.maintenanceMessage,
|
|
demoPasswordInput: demoPasswordInput.value,
|
|
})
|
|
}
|
|
|
|
function rememberSavedOperationalSettings() {
|
|
savedOperationalSnapshot.value = buildOperationalSnapshot()
|
|
}
|
|
|
|
async function loadOperationalSettings(options: { silent?: boolean } = {}) {
|
|
if (!options.silent) {
|
|
operationalLoading.value = true
|
|
operationalError.value = ''
|
|
}
|
|
|
|
try {
|
|
applyOperationalSettings(await api.getAdminOperationalSettings())
|
|
} catch (error) {
|
|
operationalError.value = error instanceof ApiRequestError
|
|
? error.message
|
|
: 'Operational Settings konnten nicht geladen werden.'
|
|
} finally {
|
|
if (!options.silent) {
|
|
operationalLoading.value = false
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateOperationalSettings() {
|
|
if (demoPasswordInput.value.trim() && demoPasswordInput.value.trim().length < 10) {
|
|
operationalError.value = 'Das Demo-Passwort muss mindestens 10 Zeichen lang sein.'
|
|
return false
|
|
}
|
|
|
|
if (operationalForm.demoLoginEnabled && !demoCredentialsComplete.value) {
|
|
operationalError.value = 'Demo-Login braucht Login, Twitch-ID, Anzeigenamen und ein gesetztes Passwort.'
|
|
return false
|
|
}
|
|
|
|
const hasAnyTwitchAuthInput = Boolean(
|
|
operationalForm.twitchClientId.trim()
|
|
|| operationalForm.twitchRedirectUri.trim()
|
|
|| operationalForm.twitchScope.trim()
|
|
|| twitchClientSecretInput.value.trim()
|
|
|| twitchClientSecretSet.value,
|
|
)
|
|
|
|
if (hasAnyTwitchAuthInput && !operationalForm.twitchClientId.trim()) {
|
|
operationalError.value = 'Twitch OAuth braucht eine Client-ID.'
|
|
return false
|
|
}
|
|
|
|
if (operationalForm.twitchClientId.trim() && !twitchAuthComplete.value) {
|
|
operationalError.value = 'Twitch OAuth braucht beim ersten Speichern ein Client Secret.'
|
|
return false
|
|
}
|
|
|
|
if (!Number.isInteger(operationalForm.sessionIdleTimeoutHours) || operationalForm.sessionIdleTimeoutHours < 3) {
|
|
operationalError.value = 'Session-Timeout muss mindestens 3 Stunden betragen.'
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
async function saveOperationalSettings() {
|
|
operationalError.value = ''
|
|
operationalSuccess.value = ''
|
|
|
|
if (!validateOperationalSettings()) {
|
|
return
|
|
}
|
|
|
|
operationalSaving.value = true
|
|
try {
|
|
const result = await api.updateAdminOperationalSettings({
|
|
...operationalForm,
|
|
demoLoginEmail: operationalForm.demoLoginIdentifier,
|
|
demoLoginPassword: demoPasswordInput.value.trim() || undefined,
|
|
twitchClientSecret: twitchClientSecretInput.value.trim() || undefined,
|
|
})
|
|
|
|
demoPasswordSet.value = result.demoLoginPasswordSet
|
|
demoManagedByDatabase.value = true
|
|
twitchClientSecretSet.value = result.twitchClientSecretSet
|
|
demoPasswordInput.value = ''
|
|
twitchClientSecretInput.value = ''
|
|
await loadOperationalSettings({ silent: true })
|
|
clearSiteStatusCache()
|
|
operationalSuccess.value = 'Demo-Zugang, Session-Timeout und Wartungsmodus wurden gespeichert.'
|
|
} catch (error) {
|
|
operationalError.value = error instanceof ApiRequestError
|
|
? error.message
|
|
: 'Operational Settings konnten nicht gespeichert werden.'
|
|
} finally {
|
|
operationalSaving.value = false
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => [
|
|
operationalForm.demoLoginEnabled,
|
|
operationalForm.demoLoginIdentifier,
|
|
operationalForm.demoLoginTwitchUserId,
|
|
operationalForm.demoLoginDisplayName,
|
|
operationalForm.twitchClientId,
|
|
operationalForm.twitchRedirectUri,
|
|
operationalForm.twitchScope,
|
|
operationalForm.sessionIdleTimeoutHours,
|
|
twitchClientSecretInput.value,
|
|
operationalForm.maintenanceModeEnabled,
|
|
operationalForm.maintenanceTitle,
|
|
operationalForm.maintenanceMessage,
|
|
demoPasswordInput.value,
|
|
],
|
|
() => {
|
|
if (operationalLoading.value || operationalSaving.value) return
|
|
operationalError.value = ''
|
|
operationalSuccess.value = ''
|
|
},
|
|
)
|
|
|
|
onMounted(loadOperationalSettings)
|
|
|
|
return {
|
|
operationalLoading,
|
|
operationalSaving,
|
|
operationalError,
|
|
operationalSuccess,
|
|
operationalForm,
|
|
demoPasswordSet,
|
|
demoManagedByDatabase,
|
|
demoPasswordInput,
|
|
twitchClientSecretSet,
|
|
twitchAuthConfigured,
|
|
twitchAuthManagedByDatabase,
|
|
twitchClientSecretInput,
|
|
demoPasswordHint,
|
|
twitchSecretHint,
|
|
demoCredentialsComplete,
|
|
twitchAuthComplete,
|
|
operationalSummary,
|
|
hasUnsavedOperationalChanges,
|
|
loadOperationalSettings,
|
|
saveOperationalSettings,
|
|
}
|
|
}
|