feat: partner enrichment, mutations, settings & branding + operator quick-wins
Backend (platform-api): computed tenant health plus industry/brandColor; partner-scoped tenant update/suspend/resume guarded by assertPartnerOwnsTenant; enriched partner users (MFA + access level) with invite/remove; partner settings and whitelabel branding persistence; Authentik authenticator counting and group removal. Audit on every mutation. Frontend (portal): all five partner pages on real data — dashboard alerts, customers edit/suspend, team MFA/access with invite/remove, editable settings, branding fetch/save. Operator: dashboard and infrastructure service health driven by real liveness probes; fabricated uptime/p95/error-rate removed.
This commit is contained in:
@@ -3,17 +3,54 @@
|
||||
// preview of how the partner topbar/header will look with the picked
|
||||
// primary color + display name.
|
||||
|
||||
defineProps<{ open: boolean }>()
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
export interface BrandIdentity {
|
||||
displayName?: string
|
||||
logoUrl?: string
|
||||
markUrl?: string
|
||||
faviconUrl?: string
|
||||
primaryColor?: string
|
||||
supportEmail?: string
|
||||
supportPhone?: string
|
||||
website?: string
|
||||
replyTo?: string
|
||||
}
|
||||
|
||||
const name = ref('NordicMSP')
|
||||
const props = defineProps<{ open: boolean; identity?: BrandIdentity }>()
|
||||
const emit = defineEmits<{ close: []; save: [payload: BrandIdentity] }>()
|
||||
|
||||
const name = ref('')
|
||||
const color = ref('#3F6BFF')
|
||||
const supportEmail = ref('support@nordicmsp.dk')
|
||||
const supportPhone = ref('+45 70 70 12 34')
|
||||
const website = ref('nordicmsp.dk')
|
||||
const replyTo = ref('no-reply@nordicmsp.dk')
|
||||
const supportEmail = ref('')
|
||||
const supportPhone = ref('')
|
||||
const website = ref('')
|
||||
const replyTo = ref('')
|
||||
|
||||
// Seed the form from the current identity each time the modal opens.
|
||||
function seed() {
|
||||
const i = props.identity ?? {}
|
||||
name.value = i.displayName ?? ''
|
||||
color.value = i.primaryColor ?? '#3F6BFF'
|
||||
supportEmail.value = i.supportEmail ?? ''
|
||||
supportPhone.value = i.supportPhone ?? ''
|
||||
website.value = i.website ?? ''
|
||||
replyTo.value = i.replyTo ?? ''
|
||||
}
|
||||
watch(() => props.open, (o) => { if (o) seed() }, { immediate: true })
|
||||
|
||||
const SWATCHES = ['#3F6BFF', '#0A2540', '#0066CC', '#5B8C5A', '#D4FF3A']
|
||||
|
||||
function save() {
|
||||
// Emit only `save` — the parent closes the modal after the async save
|
||||
// succeeds, so a failed save keeps the form open instead of losing edits.
|
||||
emit('save', {
|
||||
displayName: name.value,
|
||||
primaryColor: color.value,
|
||||
supportEmail: supportEmail.value,
|
||||
supportPhone: supportPhone.value,
|
||||
website: website.value,
|
||||
replyTo: replyTo.value,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -96,7 +133,7 @@ const SWATCHES = ['#3F6BFF', '#0A2540', '#0066CC', '#5B8C5A', '#D4FF3A']
|
||||
|
||||
<template #footer>
|
||||
<UiButton variant="ghost" @click="emit('close')">Cancel</UiButton>
|
||||
<UiButton variant="primary" @click="emit('close')">
|
||||
<UiButton variant="primary" @click="save">
|
||||
<template #leading><UiIcon name="check" :size="14" /></template>
|
||||
Save identity
|
||||
</UiButton>
|
||||
|
||||
@@ -6,7 +6,20 @@
|
||||
import { customers } from '~/data/customers'
|
||||
|
||||
defineProps<{ open: boolean }>()
|
||||
const emit = defineEmits<{ close: []; sent: [payload: { email: string; role: string }] }>()
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
sent: [
|
||||
payload: {
|
||||
name: string
|
||||
email: string
|
||||
role: string
|
||||
access: 'all' | 'specific' | 'none'
|
||||
specific: string[]
|
||||
requireMfa: boolean
|
||||
message: string
|
||||
},
|
||||
]
|
||||
}>()
|
||||
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
@@ -151,8 +164,8 @@ function planBadgeTone(p: string) {
|
||||
<UiButton variant="ghost" @click="emit('close')">Cancel</UiButton>
|
||||
<UiButton
|
||||
variant="primary"
|
||||
:disabled="!email"
|
||||
@click="emit('sent', { email, role }); emit('close')"
|
||||
:disabled="!email || !name"
|
||||
@click="emit('sent', { name, email, role, access, specific, requireMfa, message }); emit('close')"
|
||||
>
|
||||
<template #leading><UiIcon name="mail" :size="14" /></template>
|
||||
Send invitation
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface TeamMember {
|
||||
email: string
|
||||
role: string
|
||||
access: 'all' | 'specific' | 'none' | string
|
||||
// Number of customers when access is scoped; null when access is 'all'.
|
||||
accessCount?: number | null
|
||||
mfa: string
|
||||
lastSeen: string
|
||||
isOwner?: boolean
|
||||
|
||||
Reference in New Issue
Block a user