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:
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model, Types } from 'mongoose'
|
||||
import { AuditService, type AuditActor } from '../audit/audit.service.js'
|
||||
import {
|
||||
PartnerBranding,
|
||||
PartnerBrandingDocument,
|
||||
} from '../schemas/partner-branding.schema.js'
|
||||
import type { PartnerBrandingDto } from './dto/partner-branding.dto.js'
|
||||
|
||||
@Injectable()
|
||||
export class PartnerBrandingService {
|
||||
constructor(
|
||||
@InjectModel(PartnerBranding.name)
|
||||
private readonly model: Model<PartnerBrandingDocument>,
|
||||
private readonly audit: AuditService,
|
||||
) {}
|
||||
|
||||
// Return the partner's branding doc, or a default-shaped (unsaved) doc so the
|
||||
// portal always has a stable shape to render before anything is saved.
|
||||
async get(
|
||||
partnerId: string | Types.ObjectId,
|
||||
): Promise<PartnerBrandingDocument | PartnerBranding> {
|
||||
const existing = await this.model.findOne({ partnerId }).exec()
|
||||
if (existing) return existing
|
||||
// No saved branding yet — return a default-shaped plain object (no persisted
|
||||
// _id) so the portal renders a stable shape without mistaking it for a
|
||||
// saved document.
|
||||
return {
|
||||
partnerId: new Types.ObjectId(String(partnerId)),
|
||||
identity: {},
|
||||
customerDefaults: [],
|
||||
emailTemplates: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Full replace (upsert) of the partner's branding. The page edits the whole
|
||||
// object and PUTs it back.
|
||||
async put(
|
||||
partnerId: string | Types.ObjectId,
|
||||
dto: PartnerBrandingDto,
|
||||
actor?: AuditActor,
|
||||
): Promise<PartnerBrandingDocument> {
|
||||
const updated = await this.model
|
||||
.findOneAndUpdate(
|
||||
{ partnerId },
|
||||
{
|
||||
$set: {
|
||||
identity: dto.identity ?? {},
|
||||
customerDefaults: dto.customerDefaults ?? [],
|
||||
emailTemplates: dto.emailTemplates ?? [],
|
||||
},
|
||||
},
|
||||
{ new: true, upsert: true, runValidators: true, setDefaultsOnInsert: true },
|
||||
)
|
||||
.exec()
|
||||
|
||||
void this.audit.record(
|
||||
{
|
||||
action: 'partner.branding_updated',
|
||||
resourceType: 'partner',
|
||||
resourceId: String(partnerId),
|
||||
metadata: {
|
||||
templates: dto.emailTemplates?.length ?? 0,
|
||||
defaults: dto.customerDefaults?.length ?? 0,
|
||||
},
|
||||
},
|
||||
actor,
|
||||
)
|
||||
return updated!
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user