89691626f4
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.
73 lines
2.2 KiB
TypeScript
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!
|
|
}
|
|
}
|