feat: portal redesign, pricing catalog, partner-staff invites
- 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
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// Cached fetch of the signed-in operator's profile from platform-api.
|
||||
// Mirrors the portal's useMe — kept here so any future middleware /
|
||||
// layout in operator can read identity data SSR-safely without flashing
|
||||
// the wrong layout to the browser.
|
||||
//
|
||||
// No current consumer; the portal version is what motivated this pattern
|
||||
// (route middleware fetching /api/me with bare $fetch missed the session
|
||||
// cookie on SSR, causing a flash of the end-user dashboard before the
|
||||
// client-side redirect kicked in). Adding the same shape here means the
|
||||
// trap is pre-disarmed if operator ever grows comparable middleware.
|
||||
|
||||
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>('operator-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 and would
|
||||
// 401, producing a stale-state / wrong-layout flash on full reload.
|
||||
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 isPlatformAdmin = computed(() => !!profile.value?.platformAdmin)
|
||||
|
||||
return { state, profile, isPlatformAdmin, fetchMe }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Lightweight toast stack. Used by buttons/actions that want to confirm
|
||||
// they fired. Rendered by components/ToastStack.vue in the default layout.
|
||||
|
||||
export type ToastTone = 'info' | 'ok' | 'warn' | 'bad'
|
||||
|
||||
export interface Toast {
|
||||
id: number
|
||||
tone: ToastTone
|
||||
message: string
|
||||
hint?: string
|
||||
}
|
||||
|
||||
const toasts = ref<Toast[]>([])
|
||||
let counter = 0
|
||||
|
||||
export const useToast = () => {
|
||||
function push(tone: ToastTone, message: string, hint?: string) {
|
||||
const id = ++counter
|
||||
toasts.value = [...toasts.value, { id, tone, message, hint }]
|
||||
const ttl = tone === 'bad' ? 7000 : 4000
|
||||
setTimeout(() => {
|
||||
toasts.value = toasts.value.filter((t) => t.id !== id)
|
||||
}, ttl)
|
||||
}
|
||||
function dismiss(id: number) {
|
||||
toasts.value = toasts.value.filter((t) => t.id !== id)
|
||||
}
|
||||
return {
|
||||
toasts,
|
||||
push,
|
||||
info: (m: string, h?: string) => push('info', m, h),
|
||||
ok: (m: string, h?: string) => push('ok', m, h),
|
||||
warn: (m: string, h?: string) => push('warn', m, h),
|
||||
bad: (m: string, h?: string) => push('bad', m, h),
|
||||
dismiss,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user