fix: auth race condition on refresh, section edit dialog, time slot duplicate, autocomplete disable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import { useOrganisationStore } from '@/stores/useOrganisationStore'
|
||||
import type { MeResponse, Organisation, User } from '@/types/auth'
|
||||
|
||||
@@ -11,8 +12,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const organisations = ref<Organisation[]>([])
|
||||
const appRoles = ref<string[]>([])
|
||||
const permissions = ref<string[]>([])
|
||||
const isInitialized = ref(false)
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
// Requires both a token AND a validated user — token alone is not enough
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const isSuperAdmin = computed(() => appRoles.value?.includes('super_admin') ?? false)
|
||||
|
||||
const currentOrganisation = computed(() => {
|
||||
@@ -64,6 +67,40 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
orgStore.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once on app startup. If a token exists in localStorage,
|
||||
* validates it by calling GET /auth/me. On 401, clears everything.
|
||||
* Safe to call multiple times — subsequent calls return the same promise.
|
||||
*/
|
||||
let initializePromise: Promise<void> | null = null
|
||||
|
||||
function initialize(): Promise<void> {
|
||||
if (isInitialized.value) return Promise.resolve()
|
||||
if (!initializePromise) {
|
||||
initializePromise = doInitialize()
|
||||
}
|
||||
return initializePromise
|
||||
}
|
||||
|
||||
async function doInitialize(): Promise<void> {
|
||||
if (!token.value) {
|
||||
isInitialized.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: MeResponse }>('/auth/me')
|
||||
setUser(data.data)
|
||||
}
|
||||
catch {
|
||||
// Token invalid/expired — clear everything
|
||||
logout()
|
||||
}
|
||||
finally {
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
@@ -71,11 +108,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
appRoles,
|
||||
permissions,
|
||||
isAuthenticated,
|
||||
isInitialized,
|
||||
isSuperAdmin,
|
||||
currentOrganisation,
|
||||
setToken,
|
||||
setUser,
|
||||
setActiveOrganisation,
|
||||
logout,
|
||||
initialize,
|
||||
}
|
||||
})
|
||||
|
||||
24
apps/app/src/stores/useNotificationStore.ts
Normal file
24
apps/app/src/stores/useNotificationStore.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type NotificationType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export const useNotificationStore = defineStore('notification', () => {
|
||||
const visible = ref(false)
|
||||
const message = ref('')
|
||||
const type = ref<NotificationType>('info')
|
||||
const timeout = ref(5000)
|
||||
|
||||
function show(msg: string, color: NotificationType = 'error', duration = 5000) {
|
||||
message.value = msg
|
||||
type.value = color
|
||||
timeout.value = duration
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
return { visible, message, type, timeout, show, hide }
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const ACTIVE_ORG_KEY = 'crewli_active_org'
|
||||
const ACTIVE_EVENT_KEY = 'crewli_active_event'
|
||||
@@ -7,6 +7,7 @@ const ACTIVE_EVENT_KEY = 'crewli_active_event'
|
||||
export const useOrganisationStore = defineStore('organisation', () => {
|
||||
const activeOrganisationId = ref<string | null>(localStorage.getItem(ACTIVE_ORG_KEY))
|
||||
const activeEventId = ref<string | null>(localStorage.getItem(ACTIVE_EVENT_KEY))
|
||||
const hasOrganisation = computed(() => !!activeOrganisationId.value)
|
||||
|
||||
function setActiveOrganisation(id: string) {
|
||||
activeOrganisationId.value = id
|
||||
@@ -28,6 +29,7 @@ export const useOrganisationStore = defineStore('organisation', () => {
|
||||
return {
|
||||
activeOrganisationId,
|
||||
activeEventId,
|
||||
hasOrganisation,
|
||||
setActiveOrganisation,
|
||||
setActiveEvent,
|
||||
clear,
|
||||
|
||||
Reference in New Issue
Block a user