Files
dezky/services/platform-api/src/me/partner-branding.service.ts
T
Ronni Baslund 89691626f4 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.
2026-05-30 08:03:07 +02:00

73 lines
2.2 KiB
TypeScript

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!
}
}