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>
187 lines
5.5 KiB
TypeScript
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>
|