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>
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,17 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<title>تهران تاج — پنل مدیریت</title>
|
||||
<!doctype html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png?v=2" /><link rel="shortcut icon" href="/favicon.ico?v=2" />
|
||||
<title>تهران تاج — پنل مدیریت</title>
|
||||
<script type="module" crossorigin src="/assets/index-DWCzDlmr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DL8RxBUI.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Removed by router.afterEach, ported from legacy henza_admin/src/router.js:147-153 -->
|
||||
<div id="loading-bg"></div>
|
||||
</div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Removed by router.afterEach, ported from legacy henza_admin/src/router.js:147-153 -->
|
||||
<div id="loading-bg"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
@@ -164101,8 +164101,21 @@ var require_prisma_exception_filter = __commonJS({
|
||||
};
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.PrismaExceptionFilter = void 0;
|
||||
exports2.uniqueViolationMessage = uniqueViolationMessage;
|
||||
var common_12 = require_common();
|
||||
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 {
|
||||
logger = new common_12.Logger("PrismaExceptionFilter");
|
||||
catch(exception, host) {
|
||||
@@ -164133,7 +164146,7 @@ var require_prisma_exception_filter = __commonJS({
|
||||
return response.status(common_12.HttpStatus.CONFLICT).json({
|
||||
statusCode: common_12.HttpStatus.CONFLICT,
|
||||
error: "Conflict",
|
||||
message: `Unique constraint failed on: ${exception.meta?.target?.join(", ")}`
|
||||
message: uniqueViolationMessage(exception.meta?.target)
|
||||
});
|
||||
case "P2003":
|
||||
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]/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,27 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nest
|
||||
import { Response } from 'express';
|
||||
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.
|
||||
* 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({
|
||||
statusCode: HttpStatus.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
|
||||
return response.status(HttpStatus.BAD_REQUEST).json({
|
||||
|
||||