Initial commit: Meshkee CMS API

NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
Ali Reza
2026-07-21 17:52:36 +03:30
commit bb59d5e9ba
254 changed files with 37031 additions and 0 deletions
@@ -0,0 +1,158 @@
import {
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { PrismaService } from '../prisma/prisma.service';
import { BusinessSettings } from './business-settings.types';
import {
mergeBusinessSettings,
normalizeBusinessSettings,
toPrismaJson,
} from './business-settings.util';
import { UpdateBusinessSettingsDto } from './dto/update-business-settings.dto';
import { normalizeBusinessPrimaryColorId } from './business-primary-colors';
@Injectable()
export class BusinessSettingsService {
constructor(
private readonly prisma: PrismaService,
private readonly permissions: PermissionsService,
) {}
async get(businessIdRaw: string, actor: AuthUser) {
const businessId = BigInt(businessIdRaw);
await this.assertPermission(businessId, actor.id, 'business.read');
const business = await this.prisma.business.findUnique({
where: { id: businessId },
select: { id: true, settings: true },
});
if (!business) {
throw new NotFoundException('Business not found');
}
return {
businessId: business.id.toString(),
settings: normalizeBusinessSettings(business.settings),
};
}
async update(
businessIdRaw: string,
dto: UpdateBusinessSettingsDto,
actor: AuthUser,
) {
const businessId = BigInt(businessIdRaw);
await this.assertPermission(businessId, actor.id, 'business.update');
const business = await this.prisma.business.findUnique({
where: { id: businessId },
select: { id: true, settings: true },
});
if (!business) {
throw new NotFoundException('Business not found');
}
const current = normalizeBusinessSettings(business.settings);
const patch: Partial<BusinessSettings> = {};
if (dto.branding) {
patch.branding = {
primaryColor: normalizeBusinessPrimaryColorId(
dto.branding.primaryColor ?? current.branding.primaryColor,
),
};
}
if (dto.dashboard) {
patch.dashboard = {
comments: {
autoApprove:
dto.dashboard.comments?.autoApprove ??
current.dashboard.comments.autoApprove,
},
expertReviews: {
autoApprove:
dto.dashboard.expertReviews?.autoApprove ??
current.dashboard.expertReviews.autoApprove,
},
};
}
if (dto.store) {
patch.store = {
onlineSellEnabled:
dto.store.onlineSellEnabled ?? current.store.onlineSellEnabled,
orderProcessSteps:
dto.store.orderProcessSteps ?? current.store.orderProcessSteps,
};
}
const next = mergeBusinessSettings(current, patch);
const updated = await this.prisma.business.update({
where: { id: businessId },
data: { settings: toPrismaJson(next) },
select: { id: true },
});
return {
businessId: updated.id.toString(),
settings: next,
};
}
async getNormalizedSettings(businessId: bigint): Promise<BusinessSettings> {
const business = await this.prisma.business.findUnique({
where: { id: businessId },
select: { settings: true },
});
if (!business) {
throw new NotFoundException('Business not found');
}
return normalizeBusinessSettings(business.settings);
}
async isCommentsAutoApprove(businessId: bigint) {
const settings = await this.getNormalizedSettings(businessId);
return settings.dashboard.comments.autoApprove;
}
async isExpertReviewsAutoApprove(businessId: bigint) {
const settings = await this.getNormalizedSettings(businessId);
return settings.dashboard.expertReviews.autoApprove;
}
async isOnlineSellEnabled(businessId: bigint) {
const settings = await this.getNormalizedSettings(businessId);
return settings.store.onlineSellEnabled;
}
async getOrderProcessSteps(businessId: bigint) {
const settings = await this.getNormalizedSettings(businessId);
return settings.store.orderProcessSteps;
}
private async assertPermission(
businessId: bigint,
userId: bigint,
permission: string,
) {
const allowed = await this.permissions.hasBusinessPermission(
userId,
businessId,
permission,
);
if (!allowed) {
throw new ForbiddenException('Insufficient permissions');
}
}
}