0bd4e5498e
- portal: new admin/ and partner/ surfaces with full component library (AppLauncher, Avatar, Badge, Card, Modal, Tabs, etc.), composables, layouts, partner-routing middleware, and supporting server APIs - pricing: Price schema/module with operator CRUD, pricing.vue catalog UI, Subscription extended with cycle/currency/perSeatAmount/seats snapshots for stable MRR aggregation - partner staff: User.partnerId, invite-partner-user DTO and flow, /partners/:slug/users endpoints, InvitePartnerUserModal, shared dezky-partner-staff Authentik group - /me: partner-aware endpoint returning user + partner context so portal can route between end-user and partner-admin surfaces - tenant: seats field for portfolio displays and future MRR calculations - operator: pricing page, signed-out page, useMe/useToast composables, ToastStack
52 lines
1.7 KiB
TypeScript
52 lines
1.7 KiB
TypeScript
// Cached fetch of the signed-in user's profile from platform-api (proxied
|
|
// via /api/me on the portal server). One state slot per render so all
|
|
// callers share the same payload — middleware fetches once, pages read
|
|
// from cache, no per-component re-fetch.
|
|
|
|
interface MeProfile {
|
|
_id: string
|
|
authentikSubjectId: string
|
|
email: string
|
|
name: string
|
|
role: string
|
|
active: boolean
|
|
platformAdmin: boolean
|
|
tenantIds: string[]
|
|
partnerId?: string
|
|
partner?: { _id: string; slug: string; name: string; status: string }
|
|
lastLoginAt?: string
|
|
}
|
|
|
|
interface MeResponse {
|
|
profile: MeProfile
|
|
tenants: unknown[]
|
|
subscriptions: unknown[]
|
|
}
|
|
|
|
export function useMe() {
|
|
const state = useState<MeResponse | null>('portal-me', () => null)
|
|
|
|
async function fetchMe(force = false): Promise<MeResponse | null> {
|
|
if (state.value && !force) return state.value
|
|
try {
|
|
// useRequestFetch on SSR forwards the incoming request's headers
|
|
// (including the nuxt-oidc-auth session cookie) when calling the
|
|
// Nitro route. Bare $fetch on SSR has no cookie context, so /api/me
|
|
// would 401, the middleware would skip the redirect, and the end-user
|
|
// page would flash before client-side rehydration finally redirects.
|
|
const fetcher = useRequestFetch()
|
|
state.value = await fetcher<MeResponse>('/api/me')
|
|
} catch {
|
|
state.value = null
|
|
}
|
|
return state.value
|
|
}
|
|
|
|
const profile = computed<MeProfile | null>(() => state.value?.profile ?? null)
|
|
const partner = computed(() => profile.value?.partner ?? null)
|
|
const isPartnerStaff = computed(() => !!profile.value?.partnerId)
|
|
const isPlatformAdmin = computed(() => !!profile.value?.platformAdmin)
|
|
|
|
return { state, profile, partner, isPartnerStaff, isPlatformAdmin, fetchMe }
|
|
}
|