761 lines
24 KiB
TypeScript
761 lines
24 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import router from '../router'
|
|
import api from '../api/axios'
|
|
import type { OrganizationItem } from '../types/organization'
|
|
|
|
/**
|
|
* Backend now sends address fields directly on the person object
|
|
* with address_ prefix (e.g. address_city, address_zip, address_floor).
|
|
* This interface is kept for backward compatibility but the actual
|
|
* data comes as flat fields on PersonData.
|
|
*/
|
|
export interface AddressData {
|
|
zip: string | null
|
|
city: string | null
|
|
street_name: string | null
|
|
street_type: string | null
|
|
house_number: string | null
|
|
stairwell: string | null
|
|
floor: string | null
|
|
door: string | null
|
|
parcel_id: string | null
|
|
full_address_text: string | null
|
|
}
|
|
|
|
export interface PersonData {
|
|
id: number
|
|
first_name: string | null
|
|
last_name: string | null
|
|
phone: string | null
|
|
mothers_last_name: string | null
|
|
mothers_first_name: string | null
|
|
birth_place: string | null
|
|
birth_date: string | null
|
|
identity_docs: Record<string, any> | null
|
|
ice_contact: Record<string, any> | null
|
|
is_active: boolean
|
|
/** @deprecated Backend now sends address_* fields flat on person */
|
|
address: AddressData | null
|
|
// Flat address fields (new backend format)
|
|
address_zip: string | null
|
|
address_city: string | null
|
|
address_street_name: string | null
|
|
address_street_type: string | null
|
|
address_house_number: string | null
|
|
address_stairwell: string | null
|
|
address_floor: string | null
|
|
address_door: string | null
|
|
address_hrsz: string | null
|
|
address_parcel_id: string | null
|
|
address_full_text: string | null
|
|
}
|
|
|
|
export interface UserProfile {
|
|
id: number
|
|
email: string
|
|
first_name: string | null
|
|
last_name: string | null
|
|
is_active: boolean
|
|
role: string
|
|
region_code: string
|
|
subscription_plan: string
|
|
scope_level: string
|
|
scope_id: string | null
|
|
ui_mode: string
|
|
active_organization_id: number | null
|
|
preferred_language: string
|
|
// person_id is set when KYC is complete (Person record exists)
|
|
person_id: number | null
|
|
// Nested person data with address
|
|
person: PersonData | null
|
|
// RBAC Phase 3: Rendszerszintű képességek (a SYSTEM_CAPABILITIES_MATRIX-ból)
|
|
system_capabilities?: Record<string, boolean>
|
|
// RBAC Phase 3: Szervezeti szintű képességek (org_id -> capability -> bool)
|
|
org_capabilities?: Record<string, Record<string, boolean>>
|
|
}
|
|
|
|
export const useAuthStore = defineStore('auth', () => {
|
|
// ────────────────────────────── State ──────────────────────────────
|
|
const user = ref<UserProfile | null>(null)
|
|
const token = ref<string | null>(localStorage.getItem('access_token'))
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const myOrganizations = ref<OrganizationItem[]>([])
|
|
const isInitialized = ref(false)
|
|
|
|
// ────────────────────────────── Getters ────────────────────────────
|
|
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
|
const isKycComplete = computed(() => {
|
|
// KYC is complete when the user has a linked Person record (person_id is set)
|
|
return !!user.value?.person_id
|
|
})
|
|
const userRole = computed(() => user.value?.role ?? 'guest')
|
|
|
|
/**
|
|
* Ellenőrzi, hogy a felhasználó rendelkezik-e admin jogosultsággal.
|
|
* A SUPERADMIN és ADMIN (és egyéb adminisztratív) szerepköröket ismeri fel.
|
|
*/
|
|
const isAdmin = computed(() => {
|
|
const role = user.value?.role
|
|
if (!role) return false
|
|
// CRITICAL: Backend sends UPPERCASE roles (SUPERADMIN, ADMIN, MODERATOR)
|
|
// Must use toUpperCase() to ensure case-insensitive matching
|
|
const adminRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR']
|
|
return adminRoles.includes(role.toUpperCase())
|
|
})
|
|
const userName = computed(() => {
|
|
if (!user.value) return ''
|
|
const { first_name, last_name } = user.value
|
|
if (first_name && last_name) return `${first_name} ${last_name}`
|
|
return first_name || last_name || user.value.email
|
|
})
|
|
const userId = computed(() => user.value?.id ?? null)
|
|
|
|
// ── RBAC Phase 3: Capability Helpers ───────────────────────────────
|
|
|
|
/**
|
|
* Ellenőrzi, hogy a felhasználó rendelkezik-e egy adott rendszerszintű képességgel.
|
|
* A képességek a backend /auth/me vagy /users/me végpontról érkeznek
|
|
* a SYSTEM_CAPABILITIES_MATRIX alapján.
|
|
* @param capability - A képesség neve (pl. 'can_manage_users')
|
|
* @returns true, ha a képesség elérhető a user szerepköréhez
|
|
*/
|
|
function hasSystemCapability(capability: string): boolean {
|
|
return user.value?.system_capabilities?.[capability] === true
|
|
}
|
|
|
|
/**
|
|
* Ellenőrzi, hogy a felhasználó rendelkezik-e egy adott szervezeti képességgel.
|
|
* A szervezeti képességek az OrgRole.permissions JSONB mezőből származnak.
|
|
* @param orgId - A szervezet azonosítója (string vagy number)
|
|
* @param capability - A képesség neve (pl. 'can_approve_expense')
|
|
* @returns true, ha a képesség elérhető a user számára az adott szervezetben
|
|
*/
|
|
function hasOrgCapability(orgId: string | number, capability: string): boolean {
|
|
const orgCaps = user.value?.org_capabilities?.[String(orgId)]
|
|
if (!orgCaps) return false
|
|
return orgCaps[capability] === true
|
|
}
|
|
|
|
/**
|
|
* Visszaadja az összes szervezeti képességet egy adott szervezethez.
|
|
* @param orgId - A szervezet azonosítója
|
|
* @returns A képességek objektuma, vagy üres objektum, ha nincs
|
|
*/
|
|
function getOrgCapabilities(orgId: string | number): Record<string, boolean> {
|
|
return user.value?.org_capabilities?.[String(orgId)] || {}
|
|
}
|
|
|
|
// ────────────────────────────── Helpers ────────────────────────────
|
|
|
|
/**
|
|
* Extract the raw error code from the backend response.
|
|
* Backend returns codes like "[AUTH.INVALID_CREDENTIALS]" or plain strings.
|
|
* Strips brackets and returns the clean code, or null if not a code.
|
|
*/
|
|
function extractErrorCode(err: any): string | null {
|
|
const detail = err.response?.data?.detail || err.response?.data?.message || ''
|
|
if (!detail) return null
|
|
// Strip surrounding brackets: [AUTH.INVALID_CREDENTIALS] -> AUTH.INVALID_CREDENTIALS
|
|
const match = detail.match(/^\[?([A-Z]+\.[A-Z_]+)\]?$/)
|
|
return match ? match[1] : null
|
|
}
|
|
|
|
/**
|
|
* Map a backend error code to a user-friendly i18n key path.
|
|
* Returns the i18n key (e.g. 'auth.invalid_credentials') or null if unmapped.
|
|
*/
|
|
function mapErrorCodeToI18nKey(code: string): string | null {
|
|
const map: Record<string, string> = {
|
|
'AUTH.INVALID_CREDENTIALS': 'auth.invalid_credentials',
|
|
'AUTH.USER_INACTIVE': 'auth.user_inactive',
|
|
'AUTH.USER_NOT_FOUND': 'auth.invalid_credentials',
|
|
'AUTH.EMAIL_EXISTS': 'auth.email_exists',
|
|
'AUTH.INVALID_TOKEN': 'auth.invalid_token',
|
|
'AUTH.TOKEN_EXPIRED': 'auth.token_expired',
|
|
'AUTH.RATE_LIMIT': 'auth.rate_limit',
|
|
}
|
|
return map[code] || null
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly error message from an API error response.
|
|
* Returns an i18n key path (e.g. 'auth.invalid_credentials') if the error
|
|
* matches a known code, or the raw detail string as fallback.
|
|
*/
|
|
function getErrorMessage(err: any, fallback: string): string {
|
|
const code = extractErrorCode(err)
|
|
if (code) {
|
|
const i18nKey = mapErrorCodeToI18nKey(code)
|
|
if (i18nKey) return i18nKey
|
|
}
|
|
// Fallback to raw detail or message
|
|
return err.response?.data?.detail ||
|
|
err.response?.data?.message ||
|
|
fallback
|
|
}
|
|
|
|
// ────────────────────────────── Actions ────────────────────────────
|
|
|
|
/**
|
|
* Login with email + password.
|
|
* The FastAPI /auth/login endpoint uses OAuth2PasswordRequestForm,
|
|
* which expects application/x-www-form-urlencoded with `username` and `password`.
|
|
* Supports optional device_fingerprint for Device-Hub tracking.
|
|
*/
|
|
async function login(email: string, password: string, deviceFingerprint?: string) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const body = new URLSearchParams()
|
|
body.append('username', email) // FastAPI OAuth2 form uses `username` key
|
|
body.append('password', password)
|
|
if (deviceFingerprint) {
|
|
body.append('device_fingerprint', deviceFingerprint)
|
|
}
|
|
|
|
const res = await api.post('/auth/login', body, {
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
})
|
|
|
|
const data = res.data as {
|
|
access_token: string
|
|
refresh_token?: string
|
|
token_type: string
|
|
is_active: boolean
|
|
}
|
|
|
|
// Persist token
|
|
localStorage.setItem('access_token', data.access_token)
|
|
if (data.refresh_token) {
|
|
localStorage.setItem('refresh_token', data.refresh_token)
|
|
}
|
|
token.value = data.access_token
|
|
|
|
// Immediately fetch the user profile
|
|
await fetchUser()
|
|
|
|
// Fetch organizations after successful login so the header can show them
|
|
await fetchMyOrganizations()
|
|
|
|
// Clear pending verification email after successful login
|
|
localStorage.removeItem('pending_verification_email')
|
|
|
|
// If KYC is not complete, redirect to KYC page
|
|
if (!isKycComplete.value) {
|
|
router.push('/complete-kyc')
|
|
}
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.loginFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the current user profile from /auth/me.
|
|
*/
|
|
async function fetchUser() {
|
|
if (!token.value) return
|
|
|
|
try {
|
|
const res = await api.get('/auth/me')
|
|
user.value = res.data as UserProfile
|
|
} catch (err: any) {
|
|
// If the token is invalid, clear everything
|
|
if (err.response?.status === 401) {
|
|
logout()
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a new user.
|
|
* The /auth/register endpoint expects JSON with UserLiteRegister schema.
|
|
* Default hidden values (region_code, lang, timezone) are appended here.
|
|
*/
|
|
async function register(data: {
|
|
email: string
|
|
password: string
|
|
first_name: string
|
|
last_name: string
|
|
}) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const payload = {
|
|
...data,
|
|
region_code: 'HU',
|
|
lang: 'hu',
|
|
timezone: 'Europe/Budapest',
|
|
}
|
|
|
|
const res = await api.post('/auth/register', payload)
|
|
|
|
// Save email to localStorage for verification page auto-fill
|
|
localStorage.setItem('pending_verification_email', data.email)
|
|
|
|
return res.data as {
|
|
status: string
|
|
message: string
|
|
user_id: number
|
|
email: string
|
|
}
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.registerFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resend the verification email.
|
|
* POST /auth/resend-verification with { email }.
|
|
*/
|
|
async function resendVerification(email: string) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.post('/auth/resend-verification', { email })
|
|
return res.data as { status: string; message: string }
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.resendFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request a password reset email.
|
|
*/
|
|
async function forgotPassword(email: string) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.post('/auth/forgot-password', { email })
|
|
return res.data as { status: string; message: string }
|
|
} catch (err: any) {
|
|
const code = extractErrorCode(err)
|
|
if (code === 'AUTH.USER_INACTIVE') {
|
|
error.value = 'auth.user_inactive'
|
|
} else {
|
|
error.value = getErrorMessage(err, 'auth.forgotFailed')
|
|
}
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset password using token from email.
|
|
* POST /auth/reset-password with { email, token, new_password }.
|
|
*/
|
|
async function resetPassword(email: string, token: string, newPassword: string) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.post('/auth/reset-password', {
|
|
email,
|
|
token,
|
|
new_password: newPassword,
|
|
})
|
|
return res.data as { status: string; message: string }
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.resetPasswordFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout — clear token, user state, and localStorage.
|
|
*/
|
|
function logout() {
|
|
token.value = null
|
|
user.value = null
|
|
error.value = null
|
|
localStorage.removeItem('access_token')
|
|
localStorage.removeItem('refresh_token')
|
|
// Redirect to the landing page
|
|
router.push('/')
|
|
}
|
|
|
|
/**
|
|
* Initialize the store on app load — if a token exists, fetch the user.
|
|
*/
|
|
async function init() {
|
|
if (token.value) {
|
|
try {
|
|
await fetchUser()
|
|
// Load organizations so the header switcher has data immediately
|
|
await fetchMyOrganizations()
|
|
} catch {
|
|
// Token invalid — already cleared by fetchUser → logout
|
|
}
|
|
}
|
|
// Mark initialization as complete so the router guard can await it
|
|
isInitialized.value = true
|
|
}
|
|
|
|
/**
|
|
* Verify email account using the token from the activation link (Magic Link).
|
|
* POST /auth/verify-email with { token, device_fingerprint }.
|
|
* Now returns JWT tokens on success, automatically logging the user in.
|
|
* Supports optional device_fingerprint for Device-Hub tracking.
|
|
*/
|
|
async function verifyAccount(verificationToken: string, deviceFingerprint?: string) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const payload: Record<string, any> = { token: verificationToken }
|
|
if (deviceFingerprint) {
|
|
payload.device_fingerprint = deviceFingerprint
|
|
}
|
|
const res = await api.post('/auth/verify-email', payload)
|
|
const data = res.data as {
|
|
access_token?: string
|
|
refresh_token?: string
|
|
token_type?: string
|
|
is_active?: boolean
|
|
status?: string
|
|
message?: string
|
|
}
|
|
|
|
// Magic Link: Ha a backend JWT tokent küld vissza, automatikus bejelentkezés
|
|
if (data.access_token) {
|
|
// Persist token
|
|
localStorage.setItem('access_token', data.access_token)
|
|
if (data.refresh_token) {
|
|
localStorage.setItem('refresh_token', data.refresh_token)
|
|
}
|
|
token.value = data.access_token
|
|
|
|
// Immediately fetch the user profile
|
|
await fetchUser()
|
|
|
|
// Clear pending verification email after successful verification + login
|
|
localStorage.removeItem('pending_verification_email')
|
|
}
|
|
|
|
return data
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.verificationFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Complete KYC (Know Your Customer) — full profile submission.
|
|
* POSTs the KYC data to /auth/complete-kyc, then refreshes the user profile.
|
|
* Returns true on success, throws on failure.
|
|
*/
|
|
async function completeKyc(kycData: Record<string, any>): Promise<boolean> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
await api.post('/auth/complete-kyc', kycData)
|
|
// Refresh the user profile so isKycComplete getter updates
|
|
await fetchUser()
|
|
return true
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.kycFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the user's Person record (profile data + address).
|
|
* PUT /users/me/person with PersonUpdate schema.
|
|
* On success, refreshes the local user state with the response.
|
|
*/
|
|
async function updatePerson(personData: Record<string, any>): Promise<boolean> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.put('/users/me/person', personData)
|
|
// Update the local user state with the fresh response
|
|
user.value = res.data as UserProfile
|
|
return true
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.kycFailed')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the current user's password.
|
|
* PUT /users/me/password with { current_password, new_password }.
|
|
* On success, returns { status: 'ok', message: string }.
|
|
*/
|
|
async function changePassword(currentPassword: string, newPassword: string): Promise<{ status: string; message: string }> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.put('/users/me/password', {
|
|
current_password: currentPassword,
|
|
new_password: newPassword,
|
|
})
|
|
return res.data as { status: string; message: string }
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'A jelszó módosítása sikertelen.')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the current user's account (soft delete).
|
|
* DELETE /users/me with optional reason as query parameter.
|
|
* On success, clears all local state and redirects to landing page.
|
|
* If the backend returns is_last_admin=true, the response includes
|
|
* a warning detail that the caller can display.
|
|
*/
|
|
async function deleteAccount(reason?: string): Promise<{ is_last_admin?: boolean }> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const params: Record<string, string> = {}
|
|
if (reason) {
|
|
params.reason = reason
|
|
}
|
|
const res = await api.delete('/users/me', { params })
|
|
// Check if backend returned is_last_admin warning
|
|
const isLastAdmin = res.data?.is_last_admin === true
|
|
// Clear all local state — same as logout but without API call
|
|
token.value = null
|
|
user.value = null
|
|
myOrganizations.value = []
|
|
localStorage.removeItem('access_token')
|
|
localStorage.removeItem('refresh_token')
|
|
router.push('/')
|
|
return { is_last_admin: isLastAdmin }
|
|
} catch (err: any) {
|
|
// If the error response contains is_last_admin, pass it through
|
|
if (err.response?.data?.is_last_admin) {
|
|
error.value = err.response?.data?.detail || 'profile.deleteError'
|
|
throw { ...err, is_last_admin: true }
|
|
}
|
|
error.value = err?.response?.data?.detail || 'profile.deleteError'
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request account restoration code (Step 1 of Account Restore).
|
|
* POST /auth/restore/request with { email }.
|
|
* Sends a 6-digit OTP to the user's email.
|
|
*/
|
|
async function requestRestore(email: string): Promise<{ status: string; message: string }> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.post('/auth/restore/request', { email })
|
|
return res.data as { status: string; message: string }
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.restoreError')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify account restoration code and set new password (Step 2 of Account Restore).
|
|
* POST /auth/restore/verify with { email, otp, new_password }.
|
|
* On success, returns JWT tokens and auto-logs in the user.
|
|
*/
|
|
async function verifyRestore(email: string, otp: string, newPassword: string): Promise<void> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.post('/auth/restore/verify', {
|
|
email,
|
|
otp,
|
|
new_password: newPassword,
|
|
})
|
|
const data = res.data as {
|
|
access_token: string
|
|
refresh_token?: string
|
|
token_type: string
|
|
}
|
|
|
|
// Persist token
|
|
localStorage.setItem('access_token', data.access_token)
|
|
if (data.refresh_token) {
|
|
localStorage.setItem('refresh_token', data.refresh_token)
|
|
}
|
|
token.value = data.access_token
|
|
|
|
// Fetch user profile and organizations
|
|
await fetchUser()
|
|
await fetchMyOrganizations()
|
|
} catch (err: any) {
|
|
error.value = getErrorMessage(err, 'auth.restoreError')
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear any stored error message.
|
|
*/
|
|
function clearError() {
|
|
error.value = null
|
|
}
|
|
|
|
// ──────────────────────── Organization Actions ──────────────────────
|
|
|
|
/**
|
|
* Fetch the current user's organizations from GET /api/v1/organizations/my.
|
|
* Stores them in myOrganizations state.
|
|
*/
|
|
async function fetchMyOrganizations() {
|
|
try {
|
|
const res = await api.get('/organizations/my')
|
|
myOrganizations.value = res.data as OrganizationItem[]
|
|
return myOrganizations.value
|
|
} catch (err: any) {
|
|
console.error('Failed to fetch organizations:', err)
|
|
myOrganizations.value = []
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the user's first business organization (org_type === 'business' or similar).
|
|
* Returns the organization item or null if none found.
|
|
*/
|
|
const businessOrganization = computed(() => {
|
|
return myOrganizations.value.find(
|
|
(org) => org.org_type === 'business' || org.org_type === 'fleet_owner'
|
|
) || null
|
|
})
|
|
|
|
/**
|
|
* Check if the user has any business organization.
|
|
*/
|
|
const hasBusinessOrganization = computed(() => {
|
|
return !!businessOrganization.value
|
|
})
|
|
|
|
/**
|
|
* Check if the user is currently in corporate mode
|
|
* (active_organization_id is set and matches a business org).
|
|
*/
|
|
const isCorporateMode = computed(() => {
|
|
return !!user.value?.active_organization_id
|
|
})
|
|
|
|
/**
|
|
* Switch the user's active organization.
|
|
* PATCH /api/v1/users/me/active-organization with { organization_id }.
|
|
* On success, updates the token and user state, then navigates.
|
|
*/
|
|
async function switchOrganization(orgId: number | null) {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const res = await api.patch('/users/me/active-organization', {
|
|
organization_id: orgId,
|
|
})
|
|
|
|
const data = res.data as {
|
|
user: UserProfile
|
|
access_token: string
|
|
token_type: string
|
|
}
|
|
|
|
// Update the stored token
|
|
localStorage.setItem('access_token', data.access_token)
|
|
token.value = data.access_token
|
|
|
|
// Update the user state with the fresh profile
|
|
user.value = data.user
|
|
} catch (err: any) {
|
|
error.value =
|
|
err.response?.data?.detail ||
|
|
err.response?.data?.message ||
|
|
'Failed to switch organization'
|
|
throw err
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
// State
|
|
user,
|
|
token,
|
|
isLoading,
|
|
error,
|
|
myOrganizations,
|
|
isInitialized,
|
|
|
|
// Getters
|
|
isAuthenticated,
|
|
isAdmin,
|
|
isKycComplete,
|
|
userRole,
|
|
userName,
|
|
userId,
|
|
businessOrganization,
|
|
hasBusinessOrganization,
|
|
isCorporateMode,
|
|
|
|
// RBAC Phase 3: Capability Helpers
|
|
hasSystemCapability,
|
|
hasOrgCapability,
|
|
getOrgCapabilities,
|
|
|
|
// Actions
|
|
login,
|
|
fetchUser,
|
|
register,
|
|
resendVerification,
|
|
verifyAccount,
|
|
forgotPassword,
|
|
resetPassword,
|
|
completeKyc,
|
|
updatePerson,
|
|
changePassword,
|
|
deleteAccount,
|
|
requestRestore,
|
|
verifyRestore,
|
|
logout,
|
|
init,
|
|
clearError,
|
|
fetchMyOrganizations,
|
|
switchOrganization,
|
|
}
|
|
})
|