fix: Persian duplicate-value errors; admin panel favicon uses the project logo

- PrismaExceptionFilter returned "Unique constraint failed on: mobile" in
  English for a duplicate. It now answers in Persian, naming the field
  (mobile, national code, member code, order number), with a safe generic
  fallback. Covered by a unit test.
- The admin panel still shipped the old "NZ" favicon. favicon.png/.ico are now
  generated from the project logo (the same one the user apps use), in both
  public/ and dist/, and the links carry ?v=2 so browsers drop the cached icon.
- Rebuilt apps/api/release/app.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-26 06:54:28 +03:30
co-authored by Claude Sonnet 5.5
parent 0c52adcd26
commit 600ce06d15
9 changed files with 74 additions and 18 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+15 -15
View File
@@ -1,17 +1,17 @@
<!doctype html> <!doctype html>
<html lang="fa" dir="rtl"> <html lang="fa" dir="rtl">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png?v=2" /><link rel="shortcut icon" href="/favicon.ico?v=2" />
<title>تهران تاج — پنل مدیریت</title> <title>تهران تاج — پنل مدیریت</title>
<script type="module" crossorigin src="/assets/index-DWCzDlmr.js"></script> <script type="module" crossorigin src="/assets/index-DWCzDlmr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DL8RxBUI.css"> <link rel="stylesheet" crossorigin href="/assets/index-DL8RxBUI.css">
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<!-- Removed by router.afterEach, ported from legacy henza_admin/src/router.js:147-153 --> <!-- Removed by router.afterEach, ported from legacy henza_admin/src/router.js:147-153 -->
<div id="loading-bg"></div> <div id="loading-bg"></div>
</div>␍ </div>
</body> </body>
</html> </html>
+2 -1
View File
@@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png?v=2" />
<link rel="shortcut icon" href="/favicon.ico?v=2" />
<title>%BRAND_TITLE%</title> <title>%BRAND_TITLE%</title>
</head> </head>
<body> <body>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+14 -1
View File
@@ -164101,8 +164101,21 @@ var require_prisma_exception_filter = __commonJS({
}; };
Object.defineProperty(exports2, "__esModule", { value: true }); Object.defineProperty(exports2, "__esModule", { value: true });
exports2.PrismaExceptionFilter = void 0; exports2.PrismaExceptionFilter = void 0;
exports2.uniqueViolationMessage = uniqueViolationMessage;
var common_12 = require_common(); var common_12 = require_common();
var client_1 = require("@prisma/client"); var client_1 = require("@prisma/client");
function uniqueViolationMessage(target) {
const fields = (Array.isArray(target) ? target : [target]).map((t) => String(t ?? "")).join(",").toLowerCase();
if (fields.includes("mobile"))
return "\u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A";
if (fields.includes("nationalcode"))
return "\u0627\u06CC\u0646 \u06A9\u062F \u0645\u0644\u06CC \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A";
if (fields.includes("membercode"))
return "\u0627\u06CC\u0646 \u06A9\u062F \u0639\u0636\u0648\u06CC\u062A \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A";
if (fields.includes("ordernumber"))
return "\u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u0633\u0641\u0627\u0631\u0634 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A";
return "\u0627\u06CC\u0646 \u0645\u0642\u062F\u0627\u0631 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A \u0648 \u062A\u06A9\u0631\u0627\u0631\u06CC \u0627\u0633\u062A";
}
var PrismaExceptionFilter = class PrismaExceptionFilter { var PrismaExceptionFilter = class PrismaExceptionFilter {
logger = new common_12.Logger("PrismaExceptionFilter"); logger = new common_12.Logger("PrismaExceptionFilter");
catch(exception, host) { catch(exception, host) {
@@ -164133,7 +164146,7 @@ var require_prisma_exception_filter = __commonJS({
return response.status(common_12.HttpStatus.CONFLICT).json({ return response.status(common_12.HttpStatus.CONFLICT).json({
statusCode: common_12.HttpStatus.CONFLICT, statusCode: common_12.HttpStatus.CONFLICT,
error: "Conflict", error: "Conflict",
message: `Unique constraint failed on: ${exception.meta?.target?.join(", ")}` message: uniqueViolationMessage(exception.meta?.target)
}); });
case "P2003": case "P2003":
return response.status(common_12.HttpStatus.BAD_REQUEST).json({ return response.status(common_12.HttpStatus.BAD_REQUEST).json({
@@ -0,0 +1,21 @@
import { uniqueViolationMessage } from './prisma-exception.filter';
describe('uniqueViolationMessage', () => {
it('names a duplicate mobile in Persian, whether Prisma sends columns or an index name', () => {
expect(uniqueViolationMessage(['mobile'])).toBe('این شماره موبایل قبلاً ثبت شده است');
expect(uniqueViolationMessage('Customer_mobile_key')).toBe('این شماره موبایل قبلاً ثبت شده است');
expect(uniqueViolationMessage('Admin_mobile_key')).toBe('این شماره موبایل قبلاً ثبت شده است');
});
it('names a duplicate national code', () => {
expect(uniqueViolationMessage(['nationalCode'])).toBe('این کد ملی قبلاً ثبت شده است');
});
it('never leaks the English Prisma wording, even for an unknown column or no target', () => {
for (const t of [['somethingElse'], undefined, null, []]) {
const msg = uniqueViolationMessage(t);
expect(msg).toBe('این مقدار قبلاً ثبت شده است و تکراری است');
expect(msg).not.toMatch(/[A-Za-z]/);
}
});
});
+22 -1
View File
@@ -2,6 +2,27 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nest
import { Response } from 'express'; import { Response } from 'express';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
/**
* Persian, user-facing text for a unique-index violation. The panel shows
* `message` as-is, and the raw Prisma wording ("Unique constraint failed on:
* mobile") is meaningless to the operator.
*
* `target` is the list of columns on Postgres, or the index name when the
* driver only reports that — so both are matched by substring.
*/
export function uniqueViolationMessage(target: unknown): string {
const fields = (Array.isArray(target) ? target : [target])
.map((t) => String(t ?? ''))
.join(',')
.toLowerCase();
if (fields.includes('mobile')) return 'این شماره موبایل قبلاً ثبت شده است';
if (fields.includes('nationalcode')) return 'این کد ملی قبلاً ثبت شده است';
if (fields.includes('membercode')) return 'این کد عضویت قبلاً ثبت شده است';
if (fields.includes('ordernumber')) return 'این شماره سفارش قبلاً ثبت شده است';
return 'این مقدار قبلاً ثبت شده است و تکراری است';
}
/** /**
* Without this, an unmigrated table (P2021), a bad foreign key (P2003), etc. * Without this, an unmigrated table (P2021), a bad foreign key (P2003), etc.
* all surfaced as an opaque "Internal Server Error" with no indication of * all surfaced as an opaque "Internal Server Error" with no indication of
@@ -44,7 +65,7 @@ export class PrismaExceptionFilter implements ExceptionFilter {
return response.status(HttpStatus.CONFLICT).json({ return response.status(HttpStatus.CONFLICT).json({
statusCode: HttpStatus.CONFLICT, statusCode: HttpStatus.CONFLICT,
error: 'Conflict', error: 'Conflict',
message: `Unique constraint failed on: ${(exception.meta?.target as string[] | undefined)?.join(', ')}`, message: uniqueViolationMessage(exception.meta?.target),
}); });
case 'P2003': // foreign key constraint violation case 'P2003': // foreign key constraint violation
return response.status(HttpStatus.BAD_REQUEST).json({ return response.status(HttpStatus.BAD_REQUEST).json({