This commit is contained in:
phaichayon
2026-06-22 13:36:00 +07:00
parent 827fd13fa7
commit 42f403a7ed
23 changed files with 1425 additions and 68 deletions

View File

@@ -1,5 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { auditAction } from '@/features/foundation/audit-log/service';
import {
auditCrmSecurityEvent,
buildCrmSecurityContext,
canViewQuotationPricing
} from '@/features/crm/security/server/service';
import {
generateApprovedQuotationPdf,
getStoredApprovedQuotationPdf
@@ -15,9 +20,27 @@ type Params = {
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationPdfDownload
});
const securityContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
if (!canViewQuotationPricing(securityContext)) {
await auditCrmSecurityEvent({
organizationId: organization.id,
userId: session.user.id,
action: 'pricing_hidden',
entityType: 'crm_quotation',
entityId: id,
reason: 'Approved quotation PDF download requires pricing visibility'
});
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const pdf = await getStoredApprovedQuotationPdf(id, organization.id, {
userId: session.user.id,
auditDownload: true
@@ -40,12 +63,30 @@ export async function GET(_request: NextRequest, { params }: Params) {
export async function POST(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationPdfGenerateApproved
});
const before = await getQuotationDetail(id, organization.id);
const securityContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
if (!canViewQuotationPricing(securityContext)) {
await auditCrmSecurityEvent({
organizationId: organization.id,
userId: session.user.id,
action: 'pricing_hidden',
entityType: 'crm_quotation',
entityId: id,
reason: 'Approved quotation PDF generation requires pricing visibility'
});
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const before = await getQuotationDetail(id, organization.id, securityContext);
const pdf = await generateApprovedQuotationPdf(id, organization.id, session.user.id);
const after = await getQuotationDetail(id, organization.id);
const after = await getQuotationDetail(id, organization.id, securityContext);
await auditAction({
organizationId: organization.id,

View File

@@ -1,5 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { buildQuotationDocumentData } from '@/features/crm/quotations/document/server/service';
import {
auditCrmSecurityEvent,
buildCrmSecurityContext,
canViewQuotationPricing
} from '@/features/crm/security/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
@@ -10,9 +15,27 @@ type Params = {
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationDocumentPreview
});
const securityContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
if (!canViewQuotationPricing(securityContext)) {
await auditCrmSecurityEvent({
organizationId: organization.id,
userId: session.user.id,
action: 'pricing_hidden',
entityType: 'crm_quotation',
entityId: id,
reason: 'Quotation document data requires pricing visibility'
});
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const documentData = await buildQuotationDocumentData(id, organization.id);
return NextResponse.json({

View File

@@ -1,5 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { getQuotationDocumentPreviewData } from '@/features/crm/quotations/document/server/service';
import {
auditCrmSecurityEvent,
buildCrmSecurityContext,
canViewQuotationPricing
} from '@/features/crm/security/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
@@ -10,9 +15,27 @@ type Params = {
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationDocumentPreview
});
const securityContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
if (!canViewQuotationPricing(securityContext)) {
await auditCrmSecurityEvent({
organizationId: organization.id,
userId: session.user.id,
action: 'pricing_hidden',
entityType: 'crm_quotation',
entityId: id,
reason: 'Quotation document preview requires pricing visibility'
});
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const preview = await getQuotationDocumentPreviewData(id, organization.id);
return NextResponse.json({

View File

@@ -1,5 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { generateQuotationPdf } from '@/features/foundation/pdf-generator/server/service';
import {
auditCrmSecurityEvent,
buildCrmSecurityContext,
canViewQuotationPricing
} from '@/features/crm/security/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
@@ -10,9 +15,27 @@ type Params = {
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationPdfDownload
});
const securityContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
if (!canViewQuotationPricing(securityContext)) {
await auditCrmSecurityEvent({
organizationId: organization.id,
userId: session.user.id,
action: 'pricing_hidden',
entityType: 'crm_quotation',
entityId: id,
reason: 'Quotation PDF download requires pricing visibility'
});
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const pdf = await generateQuotationPdf(id, organization.id);
return new NextResponse(pdf.buffer, {

View File

@@ -1,5 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { generateQuotationPreviewPdf } from '@/features/foundation/pdf-generator/server/service';
import {
auditCrmSecurityEvent,
buildCrmSecurityContext,
canViewQuotationPricing
} from '@/features/crm/security/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
@@ -10,9 +15,27 @@ type Params = {
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationPdfPreview
});
const securityContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
if (!canViewQuotationPricing(securityContext)) {
await auditCrmSecurityEvent({
organizationId: organization.id,
userId: session.user.id,
action: 'pricing_hidden',
entityType: 'crm_quotation',
entityId: id,
reason: 'Quotation PDF preview requires pricing visibility'
});
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const pdf = await generateQuotationPreviewPdf(id, organization.id);
return new NextResponse(pdf.buffer, {

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import { buildCrmSecurityContext } from '@/features/crm/security/server/service';
import {
getQuotationActivity,
getQuotationDetail,
@@ -18,11 +19,16 @@ type Params = {
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const accessContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const [quotation, activity] = await Promise.all([
getQuotationDetail(id, organization.id),
getQuotationDetail(id, organization.id, accessContext),
getQuotationActivity(id, organization.id)
]);
@@ -49,12 +55,23 @@ export async function GET(_request: NextRequest, { params }: Params) {
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationUpdate
});
const payload = quotationSchema.parse(await request.json());
const before = await getQuotationDetail(id, organization.id);
const updated = await updateQuotation(id, organization.id, session.user.id, payload);
const accessContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const before = await getQuotationDetail(id, organization.id, accessContext);
const updated = await updateQuotation(
id,
organization.id,
session.user.id,
payload,
accessContext
);
await auditUpdate({
organizationId: organization.id,
@@ -94,11 +111,16 @@ export async function PATCH(request: NextRequest, { params }: Params) {
export async function DELETE(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationDelete
});
const before = await getQuotationDetail(id, organization.id);
const updated = await softDeleteQuotation(id, organization.id, session.user.id);
const accessContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const before = await getQuotationDetail(id, organization.id, accessContext);
const updated = await softDeleteQuotation(id, organization.id, session.user.id, accessContext);
await auditDelete({
organizationId: organization.id,

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate } from '@/features/foundation/audit-log/service';
import { buildCrmSecurityContext } from '@/features/crm/security/server/service';
import { createQuotation, listQuotations } from '@/features/crm/quotations/server/service';
import { quotationSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
@@ -8,7 +9,7 @@ import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
export async function GET(request: NextRequest) {
try {
const { organization } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const { searchParams } = request.nextUrl;
@@ -22,6 +23,11 @@ export async function GET(request: NextRequest) {
const enquiry = searchParams.get('enquiry') ?? undefined;
const hot = searchParams.get('hot') ?? undefined;
const sort = searchParams.get('sort') ?? undefined;
const accessContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const result = await listQuotations(organization.id, {
page,
limit,
@@ -33,7 +39,7 @@ export async function GET(request: NextRequest) {
enquiry,
hot,
sort
});
}, accessContext);
const offset = (page - 1) * limit;
return NextResponse.json({
@@ -60,11 +66,16 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const { organization, session } = await requireOrganizationAccess({
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationCreate
});
const payload = quotationSchema.parse(await request.json());
const created = await createQuotation(organization.id, session.user.id, payload);
const accessContext = buildCrmSecurityContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const created = await createQuotation(organization.id, session.user.id, payload, accessContext);
await auditCreate({
organizationId: organization.id,