Files
vtuber-awards/frontend/src/stores/auth.ts
T
AzuTear b53c7fb736 Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season
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>
2026-06-28 23:32:21 +02:00

187 lines
5.5 KiB
TypeScript

import { defineStore } from 'pinia'
import { AUTH_TOKEN_KEY, api } from '../lib/api'
import { ApiRequestError } from '../lib/http'
import type {
AuthSession,
ChangePasswordPayload,
DemoLoginPayload,
LoginPayload,
TeamLoginPayload,
TwitchAuthorizePayload,
} from '../types/awards'
const adminAccessRoles = new Set(['member', 'reviewer', 'organization_team', 'content_admin', 'admin', 'owner', 'creator'])
const contentManageRoles = new Set(['content_admin', 'admin', 'owner', 'creator'])
const adminWorkspaceRoles = new Set(['admin', 'owner', 'creator'])
const adminWorkspacePermissions = new Set([
'dashboard',
'years',
'nominations',
'categories',
'candidates',
'clips',
'risk',
'audit',
'analytics',
'winners',
])
function readStoredToken() {
if (typeof window === 'undefined') return null
return window.localStorage.getItem(AUTH_TOKEN_KEY)
}
function writeStoredToken(token: string | null) {
if (typeof window === 'undefined') return
if (token) {
window.localStorage.setItem(AUTH_TOKEN_KEY, token)
} else {
window.localStorage.removeItem(AUTH_TOKEN_KEY)
}
}
export const useAuthStore = defineStore('auth', {
state: () => ({
session: null as AuthSession | null,
hydrated: false,
loading: false,
}),
getters: {
isLoggedIn: (state) => Boolean(state.session),
isAdmin: (state) => adminAccessRoles.has(state.session?.role ?? ''),
canAccessAdmin: (state) => adminAccessRoles.has(state.session?.role ?? ''),
canManageContent: (state) => contentManageRoles.has(state.session?.role ?? '') || (state.session?.permissionKeys ?? []).includes('content'),
canManageAdminWorkspace: (state) => adminWorkspaceRoles.has(state.session?.role ?? '') || (state.session?.permissionKeys ?? []).some((key) => adminWorkspacePermissions.has(key)),
canManageOperationalSettings: (state) => state.session?.role === 'owner' || state.session?.role === 'creator',
canManageTeam: (state) => (state.session?.permissionKeys ?? []).includes('team'),
hasPermission: (state) => (permissionKey: string) => (state.session?.permissionKeys ?? []).includes(permissionKey),
isOwner: (state) => state.session?.role === 'owner',
isCreator: (state) => state.session?.role === 'creator',
isOwnerOrCreator: (state) => state.session?.role === 'owner' || state.session?.role === 'creator',
isTeamSession: (state) => Boolean(state.session?.teamLogin),
},
actions: {
clearSession() {
this.session = null
writeStoredToken(null)
},
async hydrate() {
if (!readStoredToken()) {
this.session = null
this.hydrated = true
return
}
try {
this.session = await api.getSession()
} catch (error) {
if (error instanceof ApiRequestError && (error.status === 401 || error.status === 403)) {
this.clearSession()
}
} finally {
this.hydrated = true
}
},
async login(payload: LoginPayload) {
this.loading = true
try {
const session = await api.login(payload)
this.session = session
writeStoredToken(session.sessionToken)
} finally {
this.loading = false
}
},
async demoLogin(payload: DemoLoginPayload) {
this.loading = true
try {
const session = await api.demoLogin(payload)
this.session = session
writeStoredToken(session.sessionToken)
} finally {
this.loading = false
}
},
async teamLogin(payload: TeamLoginPayload) {
this.loading = true
try {
const session = await api.teamLogin(payload)
this.session = session
writeStoredToken(session.sessionToken)
} finally {
this.loading = false
}
},
async changePassword(payload: ChangePasswordPayload) {
this.loading = true
try {
const session = await api.changePassword(payload)
this.session = session
writeStoredToken(session.sessionToken)
} finally {
this.loading = false
}
},
async startTwitchAuthorization(payload: Omit<TwitchAuthorizePayload, 'frontendOrigin'>) {
this.loading = true
try {
const response = await api.startTwitchAuthorization({
...payload,
frontendOrigin: window.location.origin,
})
window.location.assign(response.authorizationUrl)
} finally {
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) {
this.loading = true
try {
writeStoredToken(sessionToken)
this.session = await api.getSession()
this.hydrated = true
} catch (error) {
writeStoredToken(null)
this.session = null
throw error
} finally {
this.loading = false
}
},
async logout() {
this.loading = true
try {
await api.logout()
} finally {
this.clearSession()
this.loading = false
}
},
async deleteMyData() {
this.loading = true
try {
const result = await api.deleteMyData()
this.clearSession()
return result
} finally {
this.loading = false
}
},
},
})
export type AuthStore = ReturnType<typeof useAuthStore>