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:
Yvv
2026-04-23 15:17:14 +02:00
parent 224e5b0f5e
commit 79e468b40f
31 changed files with 1296 additions and 159 deletions

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