app.bettersight.io/frontend/src/stores/auth_store.js

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
}
}
})