103 lines
3.2 KiB
JavaScript
103 lines
3.2 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { pb } from '../services/pocketbase'
|
|
import * as tenantRepository from '../repositories/tenant_repository'
|
|
|
|
// Global auth/tenant state — session persistence itself is handled by
|
|
// pb.authStore (localStorage-backed), this store layers Bettersight's
|
|
// tenant/tier/onboarding state on top and exposes the actions pages
|
|
// call, per CLAUDE.md §5b ("Stores → global state only, calls
|
|
// repositories for data, NO API calls directly").
|
|
export const useAuthStore = defineStore('auth', {
|
|
state: () => ({
|
|
email: null,
|
|
tenant: null,
|
|
loading: false,
|
|
error: null
|
|
}),
|
|
|
|
getters: {
|
|
isAuthenticated: () => pb.authStore.isValid,
|
|
tenantId: (state) => state.tenant?.id ?? null,
|
|
tier: (state) => state.tenant?.tier ?? null,
|
|
onboarding: (state) => state.tenant?.onboarding_checklist ?? {}
|
|
},
|
|
|
|
actions: {
|
|
async loginWithPassword(email, password) {
|
|
this.loading = true
|
|
this.error = null
|
|
try {
|
|
await pb.collection('users').authWithPassword(email, password)
|
|
await this.fetchTenant()
|
|
} catch (err) {
|
|
this.error = err?.response?.message || 'Invalid email or password'
|
|
throw err
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
async loginWithGoogle() {
|
|
this.loading = true
|
|
this.error = null
|
|
try {
|
|
await pb.collection('users').authWithOAuth2({ provider: 'google' })
|
|
await this.fetchTenant()
|
|
} catch (err) {
|
|
this.error = err?.message || 'Google sign-in failed'
|
|
throw err
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
async requestPasswordReset(email) {
|
|
await pb.collection('users').requestPasswordReset(email)
|
|
},
|
|
|
|
async confirmPasswordReset(token, password, passwordConfirm) {
|
|
await pb.collection('users').confirmPasswordReset(token, password, passwordConfirm)
|
|
},
|
|
|
|
async fetchTenant() {
|
|
this.email = pb.authStore.model?.email ?? null
|
|
this.tenant = await tenantRepository.getMe()
|
|
// No signup form exists in this dashboard's scope (account
|
|
// creation is via Stripe webhook or the manual onboarding
|
|
// runbook), so first login is the earliest point browser JS can
|
|
// capture the PM's timezone. Once persisted, getMe() returns it
|
|
// going forward and this never fires again for that tenant.
|
|
if (this.tenant && !this.tenant.timezone) {
|
|
try {
|
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
this.tenant.timezone = await tenantRepository.captureTimezone(timezone)
|
|
} catch {
|
|
// Side effect — never blocks login if this fails.
|
|
}
|
|
}
|
|
},
|
|
|
|
async restoreSession() {
|
|
if (!pb.authStore.isValid) return
|
|
try {
|
|
await pb.collection('users').authRefresh()
|
|
await this.fetchTenant()
|
|
} catch {
|
|
this.clearSession()
|
|
}
|
|
},
|
|
|
|
async updateOnboarding(updates) {
|
|
const checklist = await tenantRepository.updateOnboarding(updates)
|
|
if (this.tenant) this.tenant.onboarding_checklist = checklist
|
|
return checklist
|
|
},
|
|
|
|
clearSession() {
|
|
pb.authStore.clear()
|
|
this.email = null
|
|
this.tenant = null
|
|
}
|
|
}
|
|
})
|