Fix team profile auth recovery
This commit is contained in:
@@ -36,3 +36,8 @@ public sealed record AuthSessionDto(
|
|||||||
string? TeamLogin = null,
|
string? TeamLogin = null,
|
||||||
string? BoundTwitchUserId = null,
|
string? BoundTwitchUserId = null,
|
||||||
string? BoundTwitchDisplayName = null);
|
string? BoundTwitchDisplayName = null);
|
||||||
|
|
||||||
|
public sealed record TwitchBindingDisconnectResponse(
|
||||||
|
bool Disconnected,
|
||||||
|
bool LoggedOut,
|
||||||
|
AuthSessionDto? Session);
|
||||||
|
|||||||
@@ -85,11 +85,8 @@ public static class TeamAccountBootstrapper
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.CreatedByTwitchId == SeedActor
|
if (!string.Equals(member.PasswordHash, seed.Credentials.Value.Hash, StringComparison.Ordinal)
|
||||||
&& (string.IsNullOrWhiteSpace(member.UpdatedByTwitchId)
|
|| !string.Equals(member.PasswordSalt, seed.Credentials.Value.Salt, StringComparison.Ordinal))
|
||||||
|| string.Equals(member.UpdatedByTwitchId, SeedActor, StringComparison.Ordinal))
|
|
||||||
&& (!string.Equals(member.PasswordHash, seed.Credentials.Value.Hash, StringComparison.Ordinal)
|
|
||||||
|| !string.Equals(member.PasswordSalt, seed.Credentials.Value.Salt, StringComparison.Ordinal)))
|
|
||||||
{
|
{
|
||||||
member.PasswordHash = seed.Credentials.Value.Hash;
|
member.PasswordHash = seed.Credentials.Value.Hash;
|
||||||
member.PasswordSalt = seed.Credentials.Value.Salt;
|
member.PasswordSalt = seed.Credentials.Value.Salt;
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ public static partial class AuthEndpoints
|
|||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var teamMember = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
|
||||||
|
if (teamMember is not null)
|
||||||
|
{
|
||||||
|
return Results.Json(
|
||||||
|
new { message = "Team-Accounts werden nicht ueber die automatische Teilnahme-Datenloeschung entfernt." },
|
||||||
|
statusCode: StatusCodes.Status403Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
var twitchUserId = session.TwitchUserId;
|
var twitchUserId = session.TwitchUserId;
|
||||||
await using var transaction = await db.Database.BeginTransactionAsync(context.RequestAborted);
|
await using var transaction = await db.Database.BeginTransactionAsync(context.RequestAborted);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ public static partial class AuthEndpoints
|
|||||||
.WithName("StartTwitchAuthorization")
|
.WithName("StartTwitchAuthorization")
|
||||||
.WithOpenApi();
|
.WithOpenApi();
|
||||||
|
|
||||||
|
group.MapDelete("/twitch/binding", DisconnectTwitchBinding)
|
||||||
|
.RequireRateLimiting(ApplicationDefaults.AuthRateLimitPolicy)
|
||||||
|
.WithName("DisconnectTwitchBinding")
|
||||||
|
.WithOpenApi();
|
||||||
|
|
||||||
group.MapGet("/twitch/callback", CompleteTwitchAuthorization)
|
group.MapGet("/twitch/callback", CompleteTwitchAuthorization)
|
||||||
.RequireRateLimiting(ApplicationDefaults.AuthRateLimitPolicy)
|
.RequireRateLimiting(ApplicationDefaults.AuthRateLimitPolicy)
|
||||||
.WithName("CompleteTwitchAuthorization")
|
.WithName("CompleteTwitchAuthorization")
|
||||||
|
|||||||
@@ -212,6 +212,62 @@ public static partial class AuthEndpoints
|
|||||||
return RedirectToTwitchCallback(oauthState, "connected");
|
return RedirectToTwitchCallback(oauthState, "connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> DisconnectTwitchBinding(
|
||||||
|
HttpContext context,
|
||||||
|
AwardsDbContext db,
|
||||||
|
IUserSessionService userSessionService)
|
||||||
|
{
|
||||||
|
var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted);
|
||||||
|
if (session is null)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var member = await FindTeamMemberForSessionAsync(db, session, context.RequestAborted);
|
||||||
|
if (member is null || !member.IsActive)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new { message = "Dieser Account nutzt keinen aktiven Team-Login." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var boundTwitchUserId = NormalizeTwitchUserId(member.BoundTwitchUserId);
|
||||||
|
if (string.IsNullOrWhiteSpace(boundTwitchUserId))
|
||||||
|
{
|
||||||
|
return Results.Ok(new TwitchBindingDisconnectResponse(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
await ToAuthSessionDtoAsync(db, session, cancellationToken: context.RequestAborted)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTeamLogin = ReadTeamLoginFromSession(session.TwitchUserId);
|
||||||
|
var currentSessionUsesBoundTwitch = string.IsNullOrWhiteSpace(currentTeamLogin)
|
||||||
|
&& string.Equals(NormalizeTwitchUserId(session.TwitchUserId), boundTwitchUserId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var linkedSession in db.UserSessions.Where(item => item.TwitchUserId == boundTwitchUserId))
|
||||||
|
{
|
||||||
|
linkedSession.IsActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
member.BoundTwitchUserId = null;
|
||||||
|
member.BoundTwitchDisplayName = null;
|
||||||
|
member.TwitchBoundAt = null;
|
||||||
|
member.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
member.UpdatedByTwitchId = session.TwitchUserId;
|
||||||
|
|
||||||
|
if (currentSessionUsesBoundTwitch)
|
||||||
|
{
|
||||||
|
session.IsActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(context.RequestAborted);
|
||||||
|
|
||||||
|
return Results.Ok(new TwitchBindingDisconnectResponse(
|
||||||
|
true,
|
||||||
|
currentSessionUsesBoundTwitch,
|
||||||
|
currentSessionUsesBoundTwitch
|
||||||
|
? null
|
||||||
|
: await ToAuthSessionDtoAsync(db, session, cancellationToken: context.RequestAborted)));
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<IResult> CompleteTwitchTeamLoginAsync(
|
private static async Task<IResult> CompleteTwitchTeamLoginAsync(
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
TwitchOAuthState oauthState,
|
TwitchOAuthState oauthState,
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ VTSA_TEAM_OWNER_PASSWORD=<set-secure-owner-password>
|
|||||||
VTSA_TEAM_CREATOR_PASSWORD=<set-secure-creator-password>
|
VTSA_TEAM_CREATOR_PASSWORD=<set-secure-creator-password>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
These fixed owner/creator credentials are authoritative on API startup. If a
|
||||||
|
personal team account gets locked out after an admin reset, redeploying with the
|
||||||
|
configured environment password restores the login.
|
||||||
|
|
||||||
Frontend app-wide demo gate:
|
Frontend app-wide demo gate:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -68,6 +68,27 @@ async function bindTeamTwitch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function disconnectTeamTwitch() {
|
||||||
|
accountActionError.value = ''
|
||||||
|
accountActionSuccess.value = ''
|
||||||
|
try {
|
||||||
|
const response = await authStore.disconnectTwitchBinding()
|
||||||
|
if (response.loggedOut) {
|
||||||
|
closeAccountModal()
|
||||||
|
await router.replace({ name: 'login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountActionSuccess.value = response.disconnected
|
||||||
|
? 'Twitch-Verknüpfung wurde entfernt.'
|
||||||
|
: 'Dieser Team-Account war nicht mit Twitch verknüpft.'
|
||||||
|
} catch (error) {
|
||||||
|
accountActionError.value = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Twitch konnte gerade nicht entknüpft werden.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteMyData() {
|
async function deleteMyData() {
|
||||||
accountActionError.value = ''
|
accountActionError.value = ''
|
||||||
try {
|
try {
|
||||||
@@ -202,6 +223,7 @@ const linkInactive = linkBase + 'background:transparent;color:#6f6685;font-weigh
|
|||||||
@cancel-delete="cancelAccountDeletion"
|
@cancel-delete="cancelAccountDeletion"
|
||||||
@logout="doLogout"
|
@logout="doLogout"
|
||||||
@bind-team-twitch="bindTeamTwitch"
|
@bind-team-twitch="bindTeamTwitch"
|
||||||
|
@disconnect-team-twitch="disconnectTeamTwitch"
|
||||||
@confirm-delete="deleteMyData"
|
@confirm-delete="deleteMyData"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -23,13 +23,32 @@ defineEmits<{
|
|||||||
'cancel-delete': []
|
'cancel-delete': []
|
||||||
logout: []
|
logout: []
|
||||||
'bind-team-twitch': []
|
'bind-team-twitch': []
|
||||||
|
'disconnect-team-twitch': []
|
||||||
'confirm-delete': []
|
'confirm-delete': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const twitchUserId = computed(() => props.session?.twitchUserId ?? '')
|
const isTeamPasswordSession = computed(() => Boolean(props.session?.teamLogin && props.session.twitchUserId.startsWith('team:')))
|
||||||
|
const isTwitchAuthenticated = computed(() => Boolean(props.session && !isTeamPasswordSession.value))
|
||||||
|
const profileHandle = computed(() => props.session?.teamLogin ?? props.session?.twitchUserId ?? '')
|
||||||
const role = computed(() => props.session?.role ?? 'viewer')
|
const role = computed(() => props.session?.role ?? 'viewer')
|
||||||
const isTeamSession = computed(() => Boolean(props.session?.teamLogin))
|
const isTeamSession = computed(() => Boolean(props.session?.teamLogin))
|
||||||
|
const isParticipantSession = computed(() => !isTeamSession.value)
|
||||||
const canBindTwitch = computed(() => isTeamSession.value && !props.session?.mustChangePassword)
|
const canBindTwitch = computed(() => isTeamSession.value && !props.session?.mustChangePassword)
|
||||||
|
const hasBoundTwitch = computed(() => Boolean(props.session?.boundTwitchUserId))
|
||||||
|
const showTeamLoginDetail = computed(() => Boolean(props.session?.teamLogin && !isTeamSession.value))
|
||||||
|
const authStatusLabel = computed(() => {
|
||||||
|
if (isTeamPasswordSession.value) return 'Angemeldet mit Team-Login'
|
||||||
|
if (isTeamSession.value) return 'Admin-Login über Twitch'
|
||||||
|
return 'Angemeldet über Twitch'
|
||||||
|
})
|
||||||
|
const authStatusText = computed(() => {
|
||||||
|
if (isTeamPasswordSession.value && hasBoundTwitch.value) return 'Twitch ist verknüpft, diese Session läuft aber über den Team-Login.'
|
||||||
|
if (isTeamPasswordSession.value) return 'Du bist mit Login und Passwort angemeldet.'
|
||||||
|
if (isTeamSession.value) return 'Diese Admin-Session nutzt deinen verknüpften Twitch-Account.'
|
||||||
|
return 'Diese Session nutzt deinen Twitch-Account.'
|
||||||
|
})
|
||||||
|
const primaryIdentityLabel = computed(() => isTeamSession.value ? 'Team-Login' : 'Twitch-ID')
|
||||||
|
const logoutLabel = computed(() => !isTeamSession.value && isTwitchAuthenticated.value ? 'Von Twitch abmelden' : 'Abmelden')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -43,7 +62,7 @@ const canBindTwitch = computed(() => isTeamSession.value && !props.session?.must
|
|||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 28px 18px;border-bottom:1px solid #f1ecfb;">
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 28px 18px;border-bottom:1px solid #f1ecfb;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Mein Profil</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;">@{{ twitchUserId }}</h2>
|
<h2 style="font-family:'Cormorant Garamond',serif;font-weight:700;font-size:24px;margin:0;color:#3f3556;">@{{ profileHandle }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
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="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;"
|
||||||
@@ -53,50 +72,65 @@ const canBindTwitch = computed(() => isTeamSession.value && !props.session?.must
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:24px 28px;display:flex;flex-direction:column;gap:16px;">
|
<div style="padding:24px 28px;display:flex;flex-direction:column;gap:16px;">
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:13px 16px;border-radius:14px;background:#f6f1fd;border:1px solid #ede4fb;">
|
<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 width="18" height="18" viewBox="0 0 24 24" fill="#9146FF">
|
<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="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" />
|
<path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span style="font-size:14px;color:#5f44ad;font-weight:500;">Angemeldet über Twitch</span>
|
<span style="font-size:14px;color:#5f44ad;font-weight:800;">{{ authStatusLabel }}</span>
|
||||||
|
<span style="grid-column:2;font-size:12.5px;color:#6f6685;line-height:1.45;">{{ authStatusText }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:16px;border-radius:14px;background:#fafafa;border:1px solid #f0eafc;">
|
<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>
|
<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 style="display:flex;flex-direction:column;gap:7px;">
|
||||||
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
|
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
|
||||||
<span style="color:#6f6685;">Twitch-ID</span>
|
<span style="color:#6f6685;">{{ primaryIdentityLabel }}</span>
|
||||||
<span style="font-weight:600;color:#3f3556;">@{{ twitchUserId }}</span>
|
<span style="font-weight:600;color:#3f3556;">@{{ profileHandle }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
|
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
|
||||||
<span style="color:#6f6685;">Rolle</span>
|
<span style="color:#6f6685;">Rolle</span>
|
||||||
<span style="font-weight:700;color:#8b6cdb;text-transform:uppercase;font-size:11px;letter-spacing:1px;">{{ role }}</span>
|
<span style="font-weight:700;color:#8b6cdb;text-transform:uppercase;font-size:11px;letter-spacing:1px;">{{ role }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="session?.teamLogin" style="display:flex;justify-content:space-between;font-size:13.5px;">
|
<div v-if="showTeamLoginDetail" style="display:flex;justify-content:space-between;font-size:13.5px;">
|
||||||
<span style="color:#6f6685;">Team-Login</span>
|
<span style="color:#6f6685;">Team-Login</span>
|
||||||
<span style="font-weight:600;color:#3f3556;">@{{ session.teamLogin }}</span>
|
<span style="font-weight:600;color:#3f3556;">@{{ session?.teamLogin }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="session?.boundTwitchUserId" style="display:flex;justify-content:space-between;font-size:13.5px;">
|
<div v-if="session?.boundTwitchUserId" style="display:flex;justify-content:space-between;font-size:13.5px;">
|
||||||
<span style="color:#6f6685;">Gebundenes Twitch</span>
|
<span style="color:#6f6685;">Gebundenes Twitch</span>
|
||||||
<span style="font-weight:600;color:#3f3556;">@{{ session.boundTwitchUserId }}</span>
|
<span style="font-weight:600;color:#3f3556;">@{{ session.boundTwitchUserId }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
|
<div v-if="isParticipantSession" style="display:flex;justify-content:space-between;font-size:13.5px;">
|
||||||
<span style="color:#6f6685;">Einreichungen</span>
|
<span style="color:#6f6685;">Einreichungen</span>
|
||||||
<span style="font-weight:600;color:#3f3556;">Clips, Votes, Nominierungen</span>
|
<span style="font-weight:600;color:#3f3556;">Clips, Votes, Nominierungen</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
|
<div v-if="isParticipantSession" style="display:flex;justify-content:space-between;font-size:13.5px;">
|
||||||
<span style="color:#6f6685;">Löschfrist</span>
|
<span style="color:#6f6685;">Löschfrist</span>
|
||||||
<span style="font-weight:600;color:#059669;">März 2027 (auto.)</span>
|
<span style="font-weight:600;color:#059669;">März 2027 (auto.)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<section
|
||||||
v-if="isTeamSession"
|
v-if="isTeamSession"
|
||||||
style="padding:16px;border-radius:14px;background:#f8f5ff;border:1px solid #ede4fb;display:grid;gap:12px;"
|
style="padding:16px;border-radius:14px;background:#f8f5ff;border:1px solid #ede4fb;display:grid;gap:12px;"
|
||||||
@submit.prevent="$emit('bind-team-twitch')"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#8b6cdb;margin:0 0 4px;">Twitch verbinden</p>
|
<p style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#8b6cdb;margin:0 0 4px;">Optionaler Admin-Login</p>
|
||||||
<p style="font-size:12.5px;color:#6f6685;margin:0;line-height:1.45;">Verbinde deinen privaten Twitch-Account über den offiziellen Twitch Login. Danach kannst du dich im Admin-Panel per Twitch anmelden.</p>
|
<p v-if="hasBoundTwitch" style="font-size:12.5px;color:#6f6685;margin:0;line-height:1.45;">
|
||||||
|
Dein Team-Account ist mit Twitch verbunden. Entfernst du die Verknüpfung, ist der Admin-Login per Twitch nicht mehr möglich.
|
||||||
|
</p>
|
||||||
|
<p v-else style="font-size:12.5px;color:#6f6685;margin:0;line-height:1.45;">
|
||||||
|
Verbinde deinen privaten Twitch-Account über den offiziellen Twitch Login. Danach kannst du dich im Admin-Panel per Twitch anmelden.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="session?.boundTwitchUserId" style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 12px;border-radius:12px;background:white;border:1px solid #ede4fb;font-size:13px;">
|
<div v-if="session?.boundTwitchUserId" style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 12px;border-radius:12px;background:white;border:1px solid #ede4fb;font-size:13px;">
|
||||||
<span style="color:#6f6685;">Aktuell verbunden</span>
|
<span style="color:#6f6685;">Aktuell verbunden</span>
|
||||||
@@ -106,17 +140,32 @@ const canBindTwitch = computed(() => isTeamSession.value && !props.session?.must
|
|||||||
<p v-if="accountActionError" style="font-size:12.5px;color:#be123c;margin:0;font-weight:700;">{{ accountActionError }}</p>
|
<p v-if="accountActionError" style="font-size:12.5px;color:#be123c;margin:0;font-weight:700;">{{ accountActionError }}</p>
|
||||||
<p v-if="accountActionSuccess" style="font-size:12.5px;color:#047857;margin:0;font-weight:700;">{{ accountActionSuccess }}</p>
|
<p v-if="accountActionSuccess" style="font-size:12.5px;color:#047857;margin:0;font-weight:700;">{{ accountActionSuccess }}</p>
|
||||||
<button
|
<button
|
||||||
|
v-if="!hasBoundTwitch"
|
||||||
:disabled="authLoading || !canBindTwitch"
|
:disabled="authLoading || !canBindTwitch"
|
||||||
type="submit"
|
type="button"
|
||||||
style="display:flex;align-items:center;justify-content:center;gap:8px;padding:11px 14px;border-radius:12px;border:none;background:#6f4fd1;color:white;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:800;cursor:pointer;disabled:opacity:.6;"
|
style="display:flex;align-items:center;justify-content:center;gap:8px;padding:11px 14px;border-radius:12px;border:none;background:#6f4fd1;color:white;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:800;cursor:pointer;disabled:opacity:.6;"
|
||||||
|
@click="$emit('bind-team-twitch')"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" 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="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" />
|
<path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ authLoading ? 'Twitch wird geöffnet...' : session?.boundTwitchUserId ? 'Twitch neu verbinden' : 'Mit Twitch verbinden' }}
|
{{ authLoading ? 'Twitch wird geöffnet...' : 'Mit Twitch verbinden' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<button
|
||||||
|
v-else
|
||||||
|
:disabled="authLoading"
|
||||||
|
type="button"
|
||||||
|
style="display:flex;align-items:center;justify-content:center;gap:8px;padding:11px 14px;border-radius:12px;border:1px solid #fecdd3;background:#fff5f5;color:#e11d48;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:800;cursor:pointer;disabled:opacity:.6;"
|
||||||
|
@click="$emit('disconnect-team-twitch')"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M18 6 6 18" />
|
||||||
|
<path d="m6 6 12 12" />
|
||||||
|
</svg>
|
||||||
|
{{ authLoading ? 'Entknüpft...' : 'Twitch entknüpfen' }}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
<button
|
<button
|
||||||
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="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%;"
|
||||||
@click="$emit('open-privacy')"
|
@click="$emit('open-privacy')"
|
||||||
@@ -135,9 +184,9 @@ const canBindTwitch = computed(() => isTeamSession.value && !props.session?.must
|
|||||||
<polyline points="16 17 21 12 16 7" />
|
<polyline points="16 17 21 12 16 7" />
|
||||||
<line x1="21" y1="12" x2="9" y2="12" />
|
<line x1="21" y1="12" x2="9" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
Abmelden
|
{{ logoutLabel }}
|
||||||
</button>
|
</button>
|
||||||
<template v-if="!deleteConfirm">
|
<template v-if="isParticipantSession && !deleteConfirm">
|
||||||
<button
|
<button
|
||||||
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="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%;"
|
||||||
@click="$emit('request-delete')"
|
@click="$emit('request-delete')"
|
||||||
@@ -150,7 +199,7 @@ const canBindTwitch = computed(() => isTeamSession.value && !props.session?.must
|
|||||||
Meine Daten löschen (Löschrecht Art. 17 DSGVO)
|
Meine Daten löschen (Löschrecht Art. 17 DSGVO)
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="isParticipantSession">
|
||||||
<div style="padding:16px;border-radius:14px;background:#fff5f5;border:1.5px solid #fecdd3;">
|
<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: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 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>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { AuthSession } from '../../types/awards'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
privacyModalOpen: boolean
|
privacyModalOpen: boolean
|
||||||
privacyContentHtml: string
|
privacyContentHtml: string
|
||||||
onClosePrivacy: () => void
|
onClosePrivacy: () => void
|
||||||
privacyModalStop: (event: Event) => void
|
privacyModalStop: (event: Event) => void
|
||||||
accountModalOpen: boolean
|
accountModalOpen: boolean
|
||||||
|
session: AuthSession | null
|
||||||
twitchUser: string
|
twitchUser: string
|
||||||
role: string
|
role: string
|
||||||
deleteNotConfirm: boolean
|
deleteNotConfirm: boolean
|
||||||
@@ -19,6 +24,24 @@ const props = defineProps<{
|
|||||||
onCancelDelete: () => void
|
onCancelDelete: () => void
|
||||||
onConfirmDelete: () => Promise<void> | void
|
onConfirmDelete: () => Promise<void> | void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const isTeamPasswordSession = computed(() => Boolean(props.session?.teamLogin && props.session.twitchUserId.startsWith('team:')))
|
||||||
|
const isTeamSession = computed(() => Boolean(props.session?.teamLogin))
|
||||||
|
const isParticipantSession = computed(() => !isTeamSession.value)
|
||||||
|
const profileHandle = computed(() => props.session?.teamLogin ?? props.session?.twitchUserId ?? props.twitchUser)
|
||||||
|
const showTeamLoginDetail = computed(() => Boolean(props.session?.teamLogin && !isTeamSession.value))
|
||||||
|
const statusLabel = computed(() => {
|
||||||
|
if (isTeamPasswordSession.value) return 'Angemeldet mit Team-Login'
|
||||||
|
if (isTeamSession.value) return 'Admin-Login über Twitch'
|
||||||
|
return 'Angemeldet über Twitch'
|
||||||
|
})
|
||||||
|
const statusText = 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.'
|
||||||
|
})
|
||||||
|
const primaryIdentityLabel = computed(() => isTeamSession.value ? 'Team-Login' : 'Twitch-ID')
|
||||||
|
const logoutLabel = computed(() => !isTeamSession.value ? 'Von Twitch abmelden' : 'Abmelden')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -48,22 +71,27 @@ const props = defineProps<{
|
|||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 28px 18px;border-bottom:1px solid #f1ecfb;">
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 28px 18px;border-bottom:1px solid #f1ecfb;">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Mein Profil</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;">@{{ props.twitchUser }}</h2>
|
<h2 style="font-family:'Cormorant Garamond',serif;font-weight:700;font-size:24px;margin:0;color:#3f3556;">@{{ profileHandle }}</h2>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
<div style="padding:24px 28px;display:flex;flex-direction:column;gap:16px;">
|
<div style="padding:24px 28px;display:flex;flex-direction:column;gap:16px;">
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:13px 16px;border-radius:14px;background:#f6f1fd;border:1px solid #ede4fb;">
|
<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 width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b6cdb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
<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>
|
||||||
<span style="font-size:14px;color:#5f44ad;font-weight:500;">Angemeldet über Twitch</span>
|
<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>
|
||||||
<div style="padding:16px;border-radius:14px;background:#fafafa;border:1px solid #f0eafc;">
|
<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>
|
<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 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;">Twitch-ID</span><span style="font-weight:600;color:#3f3556;">@{{ props.twitchUser }}</span></div>
|
<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 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 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;">Einreichungen</span><span style="font-weight:600;color:#3f3556;">Clips, Votes, Nominierungen</span></div>
|
||||||
<div 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-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>
|
</div>
|
||||||
</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;">
|
<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;">
|
||||||
@@ -72,15 +100,15 @@ const props = defineProps<{
|
|||||||
</button>
|
</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;">
|
<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>
|
<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>
|
||||||
Abmelden
|
{{ logoutLabel }}
|
||||||
</button>
|
</button>
|
||||||
<template v-if="props.deleteNotConfirm">
|
<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;">
|
<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>
|
<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)
|
Meine Daten löschen (Löschrecht Art. 17 DSGVO)
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="props.deleteConfirm">
|
<template v-if="isParticipantSession && props.deleteConfirm">
|
||||||
<div style="padding:16px;border-radius:14px;background:#fff5f5;border:1.5px solid #fecdd3;">
|
<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: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 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>
|
||||||
|
|||||||
@@ -346,6 +346,7 @@ const { setRootEl, landingLoaderVisible, handleNominationSubmit, handleClipSubmi
|
|||||||
:on-close-privacy="onClosePrivacy"
|
:on-close-privacy="onClosePrivacy"
|
||||||
:privacy-modal-stop="privacyModalStop"
|
:privacy-modal-stop="privacyModalStop"
|
||||||
:account-modal-open="accountModalOpen"
|
:account-modal-open="accountModalOpen"
|
||||||
|
:session="authStore.session"
|
||||||
:twitch-user="twitchUser"
|
:twitch-user="twitchUser"
|
||||||
:role="role"
|
:role="role"
|
||||||
:delete-not-confirm="deleteNotConfirm"
|
:delete-not-confirm="deleteNotConfirm"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
HomeSelectedArchive,
|
HomeSelectedArchive,
|
||||||
HomeSelectionOption,
|
HomeSelectionOption,
|
||||||
} from './homeModalTypes'
|
} from './homeModalTypes'
|
||||||
|
import type { AuthSession } from '../../types/awards'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modalOpen: boolean
|
modalOpen: boolean
|
||||||
@@ -62,6 +63,7 @@ defineProps<{
|
|||||||
onClosePrivacy: () => void
|
onClosePrivacy: () => void
|
||||||
privacyModalStop: (event: Event) => void
|
privacyModalStop: (event: Event) => void
|
||||||
accountModalOpen: boolean
|
accountModalOpen: boolean
|
||||||
|
session: AuthSession | null
|
||||||
twitchUser: string
|
twitchUser: string
|
||||||
role: string
|
role: string
|
||||||
deleteNotConfirm: boolean
|
deleteNotConfirm: boolean
|
||||||
@@ -136,6 +138,7 @@ defineProps<{
|
|||||||
:on-close-privacy="onClosePrivacy"
|
:on-close-privacy="onClosePrivacy"
|
||||||
:privacy-modal-stop="privacyModalStop"
|
:privacy-modal-stop="privacyModalStop"
|
||||||
:account-modal-open="accountModalOpen"
|
:account-modal-open="accountModalOpen"
|
||||||
|
:session="session"
|
||||||
:twitch-user="twitchUser"
|
:twitch-user="twitchUser"
|
||||||
:role="role"
|
:role="role"
|
||||||
:delete-not-confirm="deleteNotConfirm"
|
:delete-not-confirm="deleteNotConfirm"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
DemoLoginPayload,
|
DemoLoginPayload,
|
||||||
LoginPayload,
|
LoginPayload,
|
||||||
TeamLoginPayload,
|
TeamLoginPayload,
|
||||||
|
TwitchBindingDisconnectResponse,
|
||||||
TwitchAuthorizePayload,
|
TwitchAuthorizePayload,
|
||||||
TwitchAuthorizeResponse,
|
TwitchAuthorizeResponse,
|
||||||
} from '../../types/awards'
|
} from '../../types/awards'
|
||||||
@@ -22,6 +23,10 @@ export const authApi = {
|
|||||||
requestJson<AuthSession>('/api/auth/password/change', jsonRequest('POST', payload)),
|
requestJson<AuthSession>('/api/auth/password/change', jsonRequest('POST', payload)),
|
||||||
startTwitchAuthorization: (payload: TwitchAuthorizePayload) =>
|
startTwitchAuthorization: (payload: TwitchAuthorizePayload) =>
|
||||||
requestJson<TwitchAuthorizeResponse>('/api/auth/twitch/authorize', jsonRequest('POST', payload)),
|
requestJson<TwitchAuthorizeResponse>('/api/auth/twitch/authorize', jsonRequest('POST', payload)),
|
||||||
|
disconnectTwitchBinding: () =>
|
||||||
|
requestJson<TwitchBindingDisconnectResponse>('/api/auth/twitch/binding', {
|
||||||
|
method: 'DELETE',
|
||||||
|
}),
|
||||||
logout: () =>
|
logout: () =>
|
||||||
requestJson<{ loggedOut: boolean }>('/api/auth/logout', jsonRequest('POST', {})),
|
requestJson<{ loggedOut: boolean }>('/api/auth/logout', jsonRequest('POST', {})),
|
||||||
deleteMyData: () =>
|
deleteMyData: () =>
|
||||||
|
|||||||
@@ -132,6 +132,17 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async disconnectTwitchBinding() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await api.disconnectTwitchBinding()
|
||||||
|
this.session = response.session
|
||||||
|
writeStoredToken(response.session?.sessionToken ?? null)
|
||||||
|
return response
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
async completeOAuthSession(sessionToken: string) {
|
async completeOAuthSession(sessionToken: string) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,3 +44,9 @@ export interface TwitchAuthorizePayload {
|
|||||||
export interface TwitchAuthorizeResponse {
|
export interface TwitchAuthorizeResponse {
|
||||||
authorizationUrl: string
|
authorizationUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TwitchBindingDisconnectResponse {
|
||||||
|
disconnected: boolean
|
||||||
|
loggedOut: boolean
|
||||||
|
session: AuthSession | null
|
||||||
|
}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ async function submitPasswordChange() {
|
|||||||
variant="login"
|
variant="login"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required
|
required
|
||||||
placeholder="Demo-Passwort eingeben"
|
placeholder="Team-Passwort eingeben"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user