Multi-tenancy : espaces de travail + fix auth reload (rate limiter OPTIONS)
- Modèles Organization + OrgMember, migration Alembic (SQLite compatible) - organization_id nullable sur Document, Decision, Mandate, VotingProtocol - Service, schéma, router /organizations + dependency get_active_org_id - Seed : Duniter G1 + Axiom Team ; tout le contenu seed attaché à Duniter G1 - Backend : list/create filtrés par header X-Organization - Frontend : store organizations, WorkspaceSelector réel, useApi injecte l'org - Fix critique : rate_limiter exclut les requêtes OPTIONS (CORS preflight) → résout le bug "Failed to fetch /auth/me" au reload (429 sur preflight) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -149,10 +149,15 @@ export const useAuthStore = defineStore('auth', {
|
||||
const identity = await $api<DuniterIdentity>('/auth/me')
|
||||
this.identity = identity
|
||||
} catch (err: any) {
|
||||
this.error = err?.data?.detail || err?.message || 'Session invalide'
|
||||
this.token = null
|
||||
this.identity = null
|
||||
this._clearToken()
|
||||
const status = (err as any)?.status ?? 0
|
||||
this.error = err?.message || 'Session invalide'
|
||||
// N'effacer le token que sur 401/403 (session réellement invalide)
|
||||
// Les erreurs réseau ou 5xx sont transitoires — conserver la session
|
||||
if (status === 401 || status === 403) {
|
||||
this.token = null
|
||||
this.identity = null
|
||||
this._clearToken()
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
|
||||
71
frontend/app/stores/organizations.ts
Normal file
71
frontend/app/stores/organizations.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export interface Organization {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
org_type: string
|
||||
is_transparent: boolean
|
||||
color: string | null
|
||||
icon: string | null
|
||||
description: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface OrgState {
|
||||
organizations: Organization[]
|
||||
activeSlug: string | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useOrganizationsStore = defineStore('organizations', {
|
||||
state: (): OrgState => ({
|
||||
organizations: [],
|
||||
activeSlug: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
active: (state): Organization | null =>
|
||||
state.organizations.find(o => o.slug === state.activeSlug) ?? state.organizations[0] ?? null,
|
||||
|
||||
hasOrganizations: (state): boolean => state.organizations.length > 0,
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchOrganizations() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
try {
|
||||
const { $api } = useApi()
|
||||
const orgs = await $api<Organization[]>('/organizations/')
|
||||
// Duniter G1 first, then alphabetical
|
||||
this.organizations = orgs.sort((a, b) => {
|
||||
if (a.slug === 'duniter-g1') return -1
|
||||
if (b.slug === 'duniter-g1') return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
// Restore persisted active slug, or default to first org
|
||||
const stored = import.meta.client ? localStorage.getItem('libredecision_org') : null
|
||||
if (stored && this.organizations.some(o => o.slug === stored)) {
|
||||
this.activeSlug = stored
|
||||
} else if (this.organizations.length > 0) {
|
||||
this.activeSlug = this.organizations[0].slug
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.error = err?.message || 'Erreur lors du chargement des organisations'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
setActive(slug: string) {
|
||||
if (this.organizations.some(o => o.slug === slug)) {
|
||||
this.activeSlug = slug
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem('libredecision_org', slug)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user