build: include production build artifacts in repo

This commit is contained in:
S. Esmaeili
2026-10-24 10:11:05 +03:30
parent dfd8a82809
commit 7399812d0e
213 changed files with 65702 additions and 2 deletions
+69
View File
@@ -0,0 +1,69 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { Controller, Get, Module } from "@nestjs/common";
import { APP_FILTER, APP_GUARD } from "@nestjs/core";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { AuthModule } from "./auth/auth.controller.js";
import { CatalogModule } from "./catalog/catalog.module.js";
import { CommonModule } from "./common/common.module.js";
import { PrismaExceptionFilter } from "./common/prisma-exception.filter.js";
import { ContactModule } from "./contact/contact.module.js";
import { DashboardModule } from "./dashboard/dashboard.module.js";
import { MediaModule } from "./media/media.controller.js";
import { PrismaModule, PrismaService } from "./prisma/prisma.service.js";
import { PublicModule } from "./public/public.module.js";
import { SectionsModule } from "./sections/sections.module.js";
import { UsersModule } from "./users/users.controller.js";
let HealthController = class HealthController {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async health() {
await this.prisma.$queryRaw `SELECT 1`;
return { ok: true };
}
};
__decorate([
Get(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], HealthController.prototype, "health", null);
HealthController = __decorate([
Controller("health"),
__metadata("design:paramtypes", [PrismaService])
], HealthController);
let AppModule = class AppModule {
};
AppModule = __decorate([
Module({
imports: [
ThrottlerModule.forRoot([{ ttl: 60_000, limit: 300 }]),
PrismaModule,
CommonModule,
AuthModule,
UsersModule,
MediaModule,
CatalogModule,
SectionsModule,
ContactModule,
PublicModule,
DashboardModule,
],
controllers: [HealthController],
providers: [
{ provide: APP_GUARD, useClass: ThrottlerGuard },
{ provide: APP_FILTER, useClass: PrismaExceptionFilter },
],
})
], AppModule);
export { AppModule };
//# sourceMappingURL=app.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE1D,IACM,gBAAgB,GADtB,MACM,gBAAgB;IACS;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAGhD,AAAN,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAA,UAAU,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;CACF,CAAA;AAJO;IADL,GAAG,EAAE;;;;8CAIL;AAPG,gBAAgB;IADrB,UAAU,CAAC,QAAQ,CAAC;qCAEkB,aAAa;GAD9C,gBAAgB,CAQrB;AAuBM,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,SAAS;IArBrB,MAAM,CAAC;QACN,OAAO,EAAE;YAEP,eAAe,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACtD,YAAY;YACZ,YAAY;YACZ,UAAU;YACV,WAAW;YACX,WAAW;YACX,aAAa;YACb,cAAc;YACd,aAAa;YACb,YAAY;YACZ,eAAe;SAChB;QACD,WAAW,EAAE,CAAC,gBAAgB,CAAC;QAC/B,SAAS,EAAE;YACT,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAE;YAChD,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,qBAAqB,EAAE;SACzD;KACF,CAAC;GACW,SAAS,CAAG"}
+133
View File
@@ -0,0 +1,133 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Body, Controller, Get, HttpCode, Module, Post, Req, Res } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { Throttle } from "@nestjs/throttler";
import { changePasswordSchema, loginSchema, } from "@imen/shared";
import { env, isProd } from "../config/env.js";
import { AdminAuth, CurrentUser } from "../common/auth.js";
import { ZodPipe } from "../common/zod.pipe.js";
import { AuthService } from "./auth.service.js";
const COOKIE = "imen_rt";
const COOKIE_PATH = "/api/admin/auth";
const ua = (req) => req.headers["user-agent"] ?? "";
const readCookie = (req) => req.cookies?.[COOKIE];
let AuthController = class AuthController {
auth;
constructor(auth) {
this.auth = auth;
}
async login(body, req, res) {
return this.respond(res, await this.auth.login(body, ua(req)));
}
async refresh(req, res) {
try {
return this.respond(res, await this.auth.refresh(readCookie(req), ua(req)));
}
catch (e) {
res.clearCookie(COOKIE, { path: COOKIE_PATH });
throw e;
}
}
async logout(req, res) {
await this.auth.logout(readCookie(req));
res.clearCookie(COOKIE, { path: COOKIE_PATH });
}
me(user) {
return this.auth.me(user.id);
}
async changePassword(body, user) {
await this.auth.changePassword(user.id, body);
}
respond(res, t) {
res.cookie(COOKIE, t.refreshToken, {
httpOnly: true,
secure: isProd && env.COOKIE_SECURE,
sameSite: "strict",
path: COOKIE_PATH,
expires: t.refreshExpiresAt,
});
return { accessToken: t.accessToken, user: t.user };
}
};
__decorate([
Post("login"),
HttpCode(200),
Throttle({ default: { limit: 8, ttl: 60_000 } }),
__param(0, Body(new ZodPipe(loginSchema))),
__param(1, Req()),
__param(2, Res({ passthrough: true })),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "login", null);
__decorate([
Post("refresh"),
HttpCode(200),
Throttle({ default: { limit: 30, ttl: 60_000 } }),
__param(0, Req()),
__param(1, Res({ passthrough: true })),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "refresh", null);
__decorate([
Post("logout"),
HttpCode(204),
__param(0, Req()),
__param(1, Res({ passthrough: true })),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "logout", null);
__decorate([
Get("me"),
AdminAuth(),
__param(0, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], AuthController.prototype, "me", null);
__decorate([
Post("change-password"),
HttpCode(204),
AdminAuth(),
__param(0, Body(new ZodPipe(changePasswordSchema))),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "changePassword", null);
AuthController = __decorate([
Controller("admin/auth"),
__metadata("design:paramtypes", [AuthService])
], AuthController);
export { AuthController };
let AuthModule = class AuthModule {
};
AuthModule = __decorate([
Module({
imports: [
JwtModule.register({
global: true,
secret: env.JWT_SECRET,
signOptions: { expiresIn: env.JWT_ACCESS_TTL },
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
], AuthModule);
export { AuthModule };
//# sourceMappingURL=auth.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
+127
View File
@@ -0,0 +1,127 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { createHash, randomBytes } from "node:crypto";
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { hash, verify } from "@node-rs/argon2";
import { env } from "../config/env.js";
import { AuditService } from "../common/common.module.js";
import { PrismaService } from "../prisma/prisma.service.js";
export const hashPassword = (password) => hash(password);
const sha256 = (v) => createHash("sha256").update(v).digest("hex");
export const toUserDto = (u) => ({
id: u.id,
phone: u.phone,
name: u.name,
role: u.role,
isActive: u.isActive,
lastLoginAt: u.lastLoginAt?.toISOString() ?? null,
createdAt: u.createdAt.toISOString(),
});
const INVALID_LOGIN = "شماره موبایل یا رمز عبور اشتباه است";
const DUMMY_HASH = hashPassword(randomBytes(16).toString("hex"));
let AuthService = class AuthService {
prisma;
jwt;
audit;
constructor(prisma, jwt, audit) {
this.prisma = prisma;
this.jwt = jwt;
this.audit = audit;
}
async login(input, userAgent) {
const user = await this.prisma.adminUser.findUnique({ where: { phone: input.phone } });
const ok = await verify(user?.passwordHash ?? (await DUMMY_HASH), input.password).catch(() => false);
if (!user || !ok || !user.isActive)
throw new UnauthorizedException(INVALID_LOGIN);
const updated = await this.prisma.adminUser.update({
where: { id: user.id },
data: { lastLoginAt: new Date() },
});
this.audit.log(user.id, "login", "user", user.id, user.phone);
return this.issue(updated, userAgent);
}
async refresh(token, userAgent) {
if (!token)
throw new UnauthorizedException("نشست شما منقضی شده است");
const stored = await this.prisma.refreshToken.findUnique({
where: { tokenHash: sha256(token) },
include: { user: true },
});
if (!stored)
throw new UnauthorizedException("نشست شما منقضی شده است");
if (stored.revokedAt) {
await this.revokeAll(stored.userId);
throw new UnauthorizedException("نشست شما منقضی شده است");
}
if (stored.expiresAt < new Date() || !stored.user.isActive) {
throw new UnauthorizedException("نشست شما منقضی شده است");
}
await this.prisma.refreshToken.update({
where: { id: stored.id },
data: { revokedAt: new Date() },
});
return this.issue(stored.user, userAgent);
}
async logout(token) {
if (!token)
return;
await this.prisma.refreshToken.updateMany({
where: { tokenHash: sha256(token), revokedAt: null },
data: { revokedAt: new Date() },
});
}
async changePassword(userId, input) {
const user = await this.prisma.adminUser.findUniqueOrThrow({ where: { id: userId } });
if (!(await verify(user.passwordHash, input.currentPassword).catch(() => false))) {
throw new BadRequestException("رمز عبور فعلی اشتباه است");
}
await this.prisma.adminUser.update({
where: { id: userId },
data: { passwordHash: await hashPassword(input.newPassword) },
});
this.audit.log(userId, "change-password", "user", userId, user.phone);
}
async me(userId) {
return toUserDto(await this.prisma.adminUser.findUniqueOrThrow({ where: { id: userId } }));
}
revokeAll(userId) {
return this.prisma.refreshToken.updateMany({
where: { userId, revokedAt: null },
data: { revokedAt: new Date() },
});
}
async issue(user, userAgent) {
const payload = { sub: user.id, role: user.role };
const accessToken = await this.jwt.signAsync(payload);
const refreshToken = randomBytes(48).toString("base64url");
const refreshExpiresAt = new Date(Date.now() + env.REFRESH_TTL_DAYS * 86_400_000);
await this.prisma.refreshToken.create({
data: {
userId: user.id,
tokenHash: sha256(refreshToken),
expiresAt: refreshExpiresAt,
userAgent: userAgent.slice(0, 300),
},
});
void this.prisma.refreshToken
.deleteMany({ where: { expiresAt: { lt: new Date(Date.now() - 86_400_000) } } })
.catch(() => undefined);
return { accessToken, refreshToken, refreshExpiresAt, user: toUserDto(user) };
}
};
AuthService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
JwtService,
AuditService])
], AuthService);
export { AuthService };
//# sourceMappingURL=auth.service.js.map
File diff suppressed because one or more lines are too long
+214
View File
@@ -0,0 +1,214 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Body, Controller, Delete, Get, HttpCode, Module, Param, Patch, Post, Put, Query, } from "@nestjs/common";
import { categoryInputSchema, productInputSchema, productListQuerySchema, reorderSchema, } from "@imen/shared";
import { z } from "zod";
import { AdminAuth, CurrentUser, Roles } from "../common/auth.js";
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
import { MediaModule } from "../media/media.controller.js";
import { CategoriesService } from "./categories.service.js";
import { ProductsService } from "./products.service.js";
const publishSchema = z.object({ isPublished: z.boolean() });
let CategoriesController = class CategoriesController {
categories;
constructor(categories) {
this.categories = categories;
}
list() {
return this.categories.list();
}
get(id) {
return this.categories.get(id);
}
create(body, user) {
return this.categories.create(body, user.id);
}
async reorder(body, user) {
await this.categories.reorder(body.ids, user.id);
}
update(id, body, user) {
return this.categories.update(id, body, user.id);
}
async remove(id, user) {
await this.categories.remove(id, user.id);
}
};
__decorate([
Get(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "list", null);
__decorate([
Get(":id"),
__param(0, Param("id", IdPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "get", null);
__decorate([
Post(),
__param(0, Body(new ZodPipe(categoryInputSchema))),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "create", null);
__decorate([
Post("reorder"),
HttpCode(204),
__param(0, Body(new ZodPipe(reorderSchema))),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], CategoriesController.prototype, "reorder", null);
__decorate([
Put(":id"),
__param(0, Param("id", IdPipe)),
__param(1, Body(new ZodPipe(categoryInputSchema))),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object, Object]),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "update", null);
__decorate([
Roles("ADMIN"),
Delete(":id"),
HttpCode(204),
__param(0, Param("id", IdPipe)),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", Promise)
], CategoriesController.prototype, "remove", null);
CategoriesController = __decorate([
AdminAuth(),
Controller("admin/categories"),
__metadata("design:paramtypes", [CategoriesService])
], CategoriesController);
export { CategoriesController };
let ProductsController = class ProductsController {
products;
constructor(products) {
this.products = products;
}
list(q) {
return this.products.list(q);
}
get(id) {
return this.products.get(id);
}
create(body, user) {
return this.products.create(body, user.id);
}
async reorder(body, user) {
await this.products.reorder(body.ids, user.id);
}
duplicate(id, user) {
return this.products.duplicate(id, user.id);
}
publish(id, body, user) {
return this.products.setPublished(id, body.isPublished, user.id);
}
update(id, body, user) {
return this.products.update(id, body, user.id);
}
async remove(id, user) {
await this.products.remove(id, user.id);
}
};
__decorate([
Get(),
__param(0, Query(new ZodPipe(productListQuerySchema))),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], ProductsController.prototype, "list", null);
__decorate([
Get(":id"),
__param(0, Param("id", IdPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], ProductsController.prototype, "get", null);
__decorate([
Post(),
__param(0, Body(new ZodPipe(productInputSchema))),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", void 0)
], ProductsController.prototype, "create", null);
__decorate([
Post("reorder"),
HttpCode(204),
__param(0, Body(new ZodPipe(reorderSchema))),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], ProductsController.prototype, "reorder", null);
__decorate([
Post(":id/duplicate"),
__param(0, Param("id", IdPipe)),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", void 0)
], ProductsController.prototype, "duplicate", null);
__decorate([
Patch(":id/publish"),
__param(0, Param("id", IdPipe)),
__param(1, Body(new ZodPipe(publishSchema))),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object, Object]),
__metadata("design:returntype", void 0)
], ProductsController.prototype, "publish", null);
__decorate([
Put(":id"),
__param(0, Param("id", IdPipe)),
__param(1, Body(new ZodPipe(productInputSchema))),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object, Object]),
__metadata("design:returntype", void 0)
], ProductsController.prototype, "update", null);
__decorate([
Roles("ADMIN"),
Delete(":id"),
HttpCode(204),
__param(0, Param("id", IdPipe)),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", Promise)
], ProductsController.prototype, "remove", null);
ProductsController = __decorate([
AdminAuth(),
Controller("admin/products"),
__metadata("design:paramtypes", [ProductsService])
], ProductsController);
export { ProductsController };
let CatalogModule = class CatalogModule {
};
CatalogModule = __decorate([
Module({
imports: [MediaModule],
controllers: [CategoriesController, ProductsController],
providers: [CategoriesService, ProductsService],
})
], CatalogModule);
export { CatalogModule };
//# sourceMappingURL=catalog.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"catalog.module.js","sourceRoot":"","sources":["../../src/catalog/catalog.module.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EACL,IAAI,EACJ,UAAU,EACV,MAAM,EACN,GAAG,EACH,QAAQ,EACR,MAAM,EACN,KAAK,EACL,KAAK,EACL,IAAI,EACJ,GAAG,EACH,KAAK,GACN,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,mBAAmB,EAEnB,kBAAkB,EAElB,sBAAsB,EAEtB,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAiB,WAAW,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAItD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,UAA6B;QAA7B,eAAU,GAAV,UAAU,CAAmB;IAAG,CAAC;IAG9D,IAAI;QACF,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IAChC,CAAC;IAGD,GAAG,CAAsB,EAAU;QACjC,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC;IAGD,MAAM,CACoC,IAAmB,EAC5C,IAAc;QAE7B,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/C,CAAC;IAIK,AAAN,KAAK,CAAC,OAAO,CAAmC,IAAkB,EAAiB,IAAc;QAC/F,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC;IAGD,MAAM,CACiB,EAAU,EACS,IAAmB,EAC5C,IAAc;QAE7B,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC;IAKK,AAAN,KAAK,CAAC,MAAM,CAAsB,EAAU,EAAiB,IAAc;QACzE,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC;CACF,CAAA;AAtCC;IADC,GAAG,EAAE;;;;gDAGL;AAGD;IADC,GAAG,CAAC,KAAK,CAAC;IACN,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;;;;+CAEvB;AAGD;IADC,IAAI,EAAE;IAEJ,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAA;IACtC,WAAA,WAAW,EAAE,CAAA;;;;kDAGf;AAIK;IAFL,IAAI,CAAC,SAAS,CAAC;IACf,QAAQ,CAAC,GAAG,CAAC;IACC,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,CAAA;IAAsB,WAAA,WAAW,EAAE,CAAA;;;;mDAEjF;AAGD;IADC,GAAG,CAAC,KAAK,CAAC;IAER,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnB,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAA;IACtC,WAAA,WAAW,EAAE,CAAA;;;;kDAGf;AAKK;IAHL,KAAK,CAAC,OAAO,CAAC;IACd,MAAM,CAAC,KAAK,CAAC;IACb,QAAQ,CAAC,GAAG,CAAC;IACA,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAAc,WAAA,WAAW,EAAE,CAAA;;;;kDAE3D;AAzCU,oBAAoB;IAFhC,SAAS,EAAE;IACX,UAAU,CAAC,kBAAkB,CAAC;qCAEY,iBAAiB;GAD/C,oBAAoB,CA0ChC;;AAIM,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACA;IAA7B,YAA6B,QAAyB;QAAzB,aAAQ,GAAR,QAAQ,CAAiB;IAAG,CAAC;IAG1D,IAAI,CAA6C,CAAmB;QAClE,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;IAGD,GAAG,CAAsB,EAAU;QACjC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC;IAGD,MAAM,CAAwC,IAAkB,EAAiB,IAAc;QAC7F,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;IAIK,AAAN,KAAK,CAAC,OAAO,CAAmC,IAAkB,EAAiB,IAAc;QAC/F,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAGD,SAAS,CAAsB,EAAU,EAAiB,IAAc;QACtE,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;IAGD,OAAO,CACgB,EAAU,EACG,IAAoC,EACvD,IAAc;QAE7B,OAAO,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC;IAGD,MAAM,CACiB,EAAU,EACQ,IAAkB,EAC1C,IAAc;QAE7B,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;IAKK,AAAN,KAAK,CAAC,MAAM,CAAsB,EAAU,EAAiB,IAAc;QACzE,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,CAAC;CACF,CAAA;AAjDC;IADC,GAAG,EAAE;IACA,WAAA,KAAK,CAAC,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAA;;;;8CAE/C;AAGD;IADC,GAAG,CAAC,KAAK,CAAC;IACN,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;;;;6CAEvB;AAGD;IADC,IAAI,EAAE;IACC,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAA;IAAsB,WAAA,WAAW,EAAE,CAAA;;;;gDAE/E;AAIK;IAFL,IAAI,CAAC,SAAS,CAAC;IACf,QAAQ,CAAC,GAAG,CAAC;IACC,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,CAAA;IAAsB,WAAA,WAAW,EAAE,CAAA;;;;iDAEjF;AAGD;IADC,IAAI,CAAC,eAAe,CAAC;IACX,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAAc,WAAA,WAAW,EAAE,CAAA;;;;mDAExD;AAGD;IADC,KAAK,CAAC,aAAa,CAAC;IAElB,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnB,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,CAAA;IAChC,WAAA,WAAW,EAAE,CAAA;;;;iDAGf;AAGD;IADC,GAAG,CAAC,KAAK,CAAC;IAER,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnB,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAA;IACrC,WAAA,WAAW,EAAE,CAAA;;;;gDAGf;AAKK;IAHL,KAAK,CAAC,OAAO,CAAC;IACd,MAAM,CAAC,KAAK,CAAC;IACb,QAAQ,CAAC,GAAG,CAAC;IACA,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAAc,WAAA,WAAW,EAAE,CAAA;;;;gDAE3D;AApDU,kBAAkB;IAF9B,SAAS,EAAE;IACX,UAAU,CAAC,gBAAgB,CAAC;qCAEY,eAAe;GAD3C,kBAAkB,CAqD9B;;AAOM,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,aAAa;IALzB,MAAM,CAAC;QACN,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,WAAW,EAAE,CAAC,oBAAoB,EAAE,kBAAkB,CAAC;QACvD,SAAS,EAAE,CAAC,iBAAiB,EAAE,eAAe,CAAC;KAChD,CAAC;GACW,aAAa,CAAG"}
+133
View File
@@ -0,0 +1,133 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { ConflictException, Injectable } from "@nestjs/common";
import { AuditService, RevalidateService } from "../common/common.module.js";
import { MediaService } from "../media/media.service.js";
import { PrismaService } from "../prisma/prisma.service.js";
const withCover = { coverImage: true };
let CategoriesService = class CategoriesService {
prisma;
media;
audit;
revalidate;
constructor(prisma, media, audit, revalidate) {
this.prisma = prisma;
this.media = media;
this.audit = audit;
this.revalidate = revalidate;
}
async list() {
const rows = await this.prisma.category.findMany({
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
include: { ...withCover, _count: { select: { products: true, groups: true } } },
});
return rows.map(({ coverImage, _count, ...c }) => ({
...c,
coverImage: coverImage && this.media.toDto(coverImage),
productCount: _count.products,
groupCount: _count.groups,
}));
}
async get(id) {
const { coverImage, ...c } = await this.prisma.category.findUniqueOrThrow({
where: { id },
include: {
...withCover,
groups: {
orderBy: { sortOrder: "asc" },
include: { _count: { select: { products: true } } },
},
},
});
return {
...c,
coverImage: coverImage && this.media.toDto(coverImage),
groups: c.groups.map(({ _count, ...g }) => ({ ...g, productCount: _count.products })),
};
}
async create(input, userId) {
this.assertUniqueGroupSlugs(input);
const { groups, ...data } = input;
const last = await this.prisma.category.aggregate({ _max: { sortOrder: true } });
const category = await this.prisma.category.create({
data: {
...data,
sortOrder: (last._max.sortOrder ?? -1) + 1,
groups: {
create: groups.map((g, i) => ({ slug: g.slug, name: g.name, desc: g.desc, sortOrder: i })),
},
},
});
this.audit.log(userId, "create", "category", category.id, category.name);
this.revalidate.trigger();
return this.get(category.id);
}
async update(id, input, userId) {
this.assertUniqueGroupSlugs(input);
const { groups, ...data } = input;
await this.prisma.$transaction(async (tx) => {
const category = await tx.category.update({ where: { id }, data });
const keepIds = groups.flatMap((g) => (g.id ? [g.id] : []));
await tx.productGroup.deleteMany({ where: { categoryId: id, id: { notIn: keepIds } } });
for (const g of groups) {
if (g.id) {
await tx.productGroup.update({
where: { id: g.id, categoryId: id },
data: { slug: `__tmp-${g.id}` },
});
}
}
for (const [i, g] of groups.entries()) {
const fields = { slug: g.slug, name: g.name, desc: g.desc, sortOrder: i };
if (g.id) {
await tx.productGroup.update({ where: { id: g.id, categoryId: id }, data: fields });
}
else {
await tx.productGroup.create({ data: { ...fields, categoryId: id } });
}
}
this.audit.log(userId, "update", "category", id, category.name);
});
this.revalidate.trigger();
return this.get(id);
}
async remove(id, userId) {
const category = await this.prisma.category.findUniqueOrThrow({
where: { id },
include: { _count: { select: { products: true } } },
});
if (category._count.products > 0) {
throw new ConflictException(`این دسته ${category._count.products} محصول دارد؛ ابتدا محصولات را حذف یا جابه‌جا کنید`);
}
await this.prisma.category.delete({ where: { id } });
this.audit.log(userId, "delete", "category", id, category.name);
this.revalidate.trigger();
}
async reorder(ids, userId) {
await this.prisma.$transaction(ids.map((id, i) => this.prisma.category.update({ where: { id }, data: { sortOrder: i } })));
this.audit.log(userId, "reorder", "category", "", `${ids.length} دسته`);
this.revalidate.trigger();
}
assertUniqueGroupSlugs(input) {
const slugs = input.groups.map((g) => g.slug);
if (new Set(slugs).size !== slugs.length) {
throw new ConflictException("اسلاگ زیرگروه‌ها باید یکتا باشد");
}
}
};
CategoriesService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
MediaService,
AuditService,
RevalidateService])
], CategoriesService);
export { CategoriesService };
//# sourceMappingURL=categories.service.js.map
File diff suppressed because one or more lines are too long
+182
View File
@@ -0,0 +1,182 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { BadRequestException, Injectable } from "@nestjs/common";
import { AuditService, RevalidateService } from "../common/common.module.js";
import { MediaService } from "../media/media.service.js";
import { PrismaService } from "../prisma/prisma.service.js";
let ProductsService = class ProductsService {
prisma;
media;
audit;
revalidate;
constructor(prisma, media, audit, revalidate) {
this.prisma = prisma;
this.media = media;
this.audit = audit;
this.revalidate = revalidate;
}
async list(q) {
const where = {
...(q.categoryId && { categoryId: q.categoryId }),
...(q.groupId && { groupId: q.groupId }),
...(q.status && { isPublished: q.status === "published" }),
...(q.q && {
OR: [
{ name: { contains: q.q, mode: "insensitive" } },
{ nameEn: { contains: q.q, mode: "insensitive" } },
{ code: { contains: q.q, mode: "insensitive" } },
],
}),
};
const [rows, total] = await this.prisma.$transaction([
this.prisma.product.findMany({
where,
orderBy: [{ category: { sortOrder: "asc" } }, { sortOrder: "asc" }, { id: "asc" }],
skip: (q.page - 1) * q.pageSize,
take: q.pageSize,
include: {
image: true,
category: { select: { id: true, name: true } },
group: { select: { id: true, name: true } },
},
}),
this.prisma.product.count({ where }),
]);
return {
items: rows.map(({ image, ...p }) => ({ ...p, image: image && this.media.toDto(image) })),
total,
page: q.page,
pageSize: q.pageSize,
};
}
async get(id) {
const { image, gallery, ...p } = await this.prisma.product.findUniqueOrThrow({
where: { id },
include: { image: true, gallery: { orderBy: { sortOrder: "asc" }, include: { media: true } } },
});
return {
...p,
image: image && this.media.toDto(image),
gallery: gallery.map((g) => this.media.toDto(g.media)),
};
}
async create(input, userId) {
await this.assertGroup(input);
const { galleryIds, ...data } = input;
const last = await this.prisma.product.aggregate({
where: { categoryId: input.categoryId },
_max: { sortOrder: true },
});
const product = await this.prisma.product.create({
data: {
...data,
sortOrder: (last._max.sortOrder ?? -1) + 1,
gallery: { create: unique(galleryIds).map((mediaId, i) => ({ mediaId, sortOrder: i })) },
},
});
this.audit.log(userId, "create", "product", product.id, product.name);
this.revalidate.trigger();
return this.get(product.id);
}
async update(id, input, userId) {
await this.assertGroup(input);
const { galleryIds, ...data } = input;
const product = await this.prisma.product.update({
where: { id },
data: {
...data,
gallery: {
deleteMany: {},
create: unique(galleryIds).map((mediaId, i) => ({ mediaId, sortOrder: i })),
},
},
});
this.audit.log(userId, "update", "product", id, product.name);
this.revalidate.trigger();
return this.get(id);
}
async setPublished(id, isPublished, userId) {
const product = await this.prisma.product.update({ where: { id }, data: { isPublished } });
this.audit.log(userId, isPublished ? "publish" : "unpublish", "product", id, product.name);
this.revalidate.trigger();
return { id, isPublished };
}
async duplicate(id, userId) {
const src = await this.prisma.product.findUniqueOrThrow({
where: { id },
include: { gallery: true },
});
let n = 1;
const taken = async (code, slug) => (await this.prisma.product.count({ where: { OR: [{ code }, { slug }] } })) > 0;
while (await taken(`${src.code}-${n}`, `${src.slug}-copy-${n}`))
n++;
const copy = await this.prisma.product.create({
data: {
categoryId: src.categoryId,
groupId: src.groupId,
code: `${src.code}-${n}`,
slug: `${src.slug}-copy-${n}`,
name: `${src.name} (کپی)`,
nameEn: src.nameEn,
desc: src.desc,
specs: src.specs,
variants: src.variants,
details: src.details,
brand: src.brand,
origin: src.origin,
sizes: src.sizes,
standards: src.standards,
features: src.features,
applications: src.applications,
attributes: (src.attributes ?? []),
packaging: src.packaging,
minOrder: src.minOrder,
seoTitle: src.seoTitle,
seoDescription: src.seoDescription,
imageId: src.imageId,
sortOrder: src.sortOrder + 1,
isPublished: false,
gallery: {
create: src.gallery.map((g) => ({ mediaId: g.mediaId, sortOrder: g.sortOrder })),
},
},
});
this.audit.log(userId, "duplicate", "product", copy.id, copy.name);
return this.get(copy.id);
}
async remove(id, userId) {
const product = await this.prisma.product.delete({ where: { id } });
this.audit.log(userId, "delete", "product", id, product.name);
this.revalidate.trigger();
}
async reorder(ids, userId) {
await this.prisma.$transaction(ids.map((id, i) => this.prisma.product.update({ where: { id }, data: { sortOrder: i } })));
this.audit.log(userId, "reorder", "product", "", `${ids.length} محصول`);
this.revalidate.trigger();
}
async assertGroup(input) {
if (!input.groupId)
return;
const group = await this.prisma.productGroup.findUnique({ where: { id: input.groupId } });
if (group?.categoryId !== input.categoryId) {
throw new BadRequestException("زیرگروه انتخاب‌شده متعلق به این دسته نیست");
}
}
};
ProductsService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
MediaService,
AuditService,
RevalidateService])
], ProductsService);
export { ProductsService };
const unique = (ids) => [...new Set(ids)];
//# sourceMappingURL=products.service.js.map
File diff suppressed because one or more lines are too long
+60
View File
@@ -0,0 +1,60 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { createParamDecorator, ForbiddenException, Injectable, SetMetadata, UnauthorizedException, UseGuards, applyDecorators, } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { PrismaService } from "../prisma/prisma.service.js";
const ROLES_KEY = "roles";
let JwtAuthGuard = class JwtAuthGuard {
jwt;
reflector;
prisma;
constructor(jwt, reflector, prisma) {
this.jwt = jwt;
this.reflector = reflector;
this.prisma = prisma;
}
async canActivate(ctx) {
const req = ctx.switchToHttp().getRequest();
const [scheme, token] = (req.headers.authorization ?? "").split(" ");
if (scheme !== "Bearer" || !token)
throw new UnauthorizedException("ابتدا وارد شوید");
let payload;
try {
payload = await this.jwt.verifyAsync(token);
}
catch {
throw new UnauthorizedException("نشست شما منقضی شده است");
}
const user = await this.prisma.adminUser.findUnique({ where: { id: payload.sub } });
if (!user?.isActive)
throw new UnauthorizedException("حساب کاربری غیرفعال است");
req.user = { id: user.id, phone: user.phone, name: user.name, role: user.role };
const roles = this.reflector.getAllAndOverride(ROLES_KEY, [
ctx.getHandler(),
ctx.getClass(),
]);
if (roles?.length && !roles.includes(user.role)) {
throw new ForbiddenException("برای این کار دسترسی ندارید");
}
return true;
}
};
JwtAuthGuard = __decorate([
Injectable(),
__metadata("design:paramtypes", [JwtService,
Reflector,
PrismaService])
], JwtAuthGuard);
export { JwtAuthGuard };
export const AdminAuth = (...roles) => applyDecorators(SetMetadata(ROLES_KEY, roles), UseGuards(JwtAuthGuard));
export const Roles = (...roles) => SetMetadata(ROLES_KEY, roles);
export const CurrentUser = createParamDecorator((_, ctx) => ctx.switchToHttp().getRequest().user);
//# sourceMappingURL=auth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/common/auth.ts"],"names":[],"mappings":"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"}
+70
View File
@@ -0,0 +1,70 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var AuditService_1, RevalidateService_1;
import { Global, Injectable, Logger, Module } from "@nestjs/common";
import { env } from "../config/env.js";
import { PrismaService } from "../prisma/prisma.service.js";
let AuditService = AuditService_1 = class AuditService {
prisma;
logger = new Logger(AuditService_1.name);
constructor(prisma) {
this.prisma = prisma;
}
log(userId, action, entity, entityId = "", summary = "") {
this.prisma.auditLog
.create({ data: { userId, action, entity, entityId: String(entityId), summary } })
.catch((e) => this.logger.warn(`audit log failed: ${String(e)}`));
}
};
AuditService = AuditService_1 = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService])
], AuditService);
export { AuditService };
let RevalidateService = RevalidateService_1 = class RevalidateService {
logger = new Logger(RevalidateService_1.name);
timer;
trigger() {
if (!env.WEB_REVALIDATE_URL)
return;
clearTimeout(this.timer);
this.timer = setTimeout(() => void this.run(), 1500);
this.timer.unref();
}
async run() {
try {
const res = await fetch(env.WEB_REVALIDATE_URL, {
method: "POST",
headers: { "x-revalidate-secret": env.REVALIDATE_SECRET ?? "" },
signal: AbortSignal.timeout(5000),
});
if (!res.ok)
this.logger.warn(`site revalidation returned ${res.status}`);
}
catch (e) {
this.logger.warn(`site revalidation failed: ${String(e)}`);
}
}
};
RevalidateService = RevalidateService_1 = __decorate([
Injectable()
], RevalidateService);
export { RevalidateService };
let CommonModule = class CommonModule {
};
CommonModule = __decorate([
Global(),
Module({
providers: [AuditService, RevalidateService],
exports: [AuditService, RevalidateService],
})
], CommonModule);
export { CommonModule };
//# sourceMappingURL=common.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"common.module.js","sourceRoot":"","sources":["../../src/common/common.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAIrD,IAAM,YAAY,oBAAlB,MAAM,YAAY;IAGM;IAFZ,MAAM,GAAG,IAAI,MAAM,CAAC,cAAY,CAAC,IAAI,CAAC,CAAC;IAExD,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAEtD,GAAG,CACD,MAAqB,EACrB,MAAc,EACd,MAAc,EACd,WAA4B,EAAE,EAC9B,OAAO,GAAG,EAAE;QAEZ,IAAI,CAAC,MAAM,CAAC,QAAQ;aACjB,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;aACjF,KAAK,CAAC,CAAC,CAAU,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/E,CAAC;CACF,CAAA;AAhBY,YAAY;IADxB,UAAU,EAAE;qCAI0B,aAAa;GAHvC,YAAY,CAgBxB;;AAOM,IAAM,iBAAiB,yBAAvB,MAAM,iBAAiB;IACX,MAAM,GAAG,IAAI,MAAM,CAAC,mBAAiB,CAAC,IAAI,CAAC,CAAC;IACrD,KAAK,CAA6B;IAE1C,OAAO;QACL,IAAI,CAAC,GAAG,CAAC,kBAAkB;YAAE,OAAO;QACpC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAEO,KAAK,CAAC,GAAG;QACf,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,kBAAmB,EAAE;gBAC/C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,qBAAqB,EAAE,GAAG,CAAC,iBAAiB,IAAI,EAAE,EAAE;gBAC/D,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,8BAA8B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5E,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,6BAA6B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;CACF,CAAA;AAvBY,iBAAiB;IAD7B,UAAU,EAAE;GACA,iBAAiB,CAuB7B;;AAOM,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,YAAY;IALxB,MAAM,EAAE;IACR,MAAM,CAAC;QACN,SAAS,EAAE,CAAC,YAAY,EAAE,iBAAiB,CAAC;QAC5C,OAAO,EAAE,CAAC,YAAY,EAAE,iBAAiB,CAAC;KAC3C,CAAC;GACW,YAAY,CAAG"}
+59
View File
@@ -0,0 +1,59 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Catch, HttpStatus } from "@nestjs/common";
import { Prisma } from "../generated/prisma/client.js";
const FIELD_LABELS = {
slug: "اسلاگ",
code: "کد",
email: "ایمیل",
phone: "شماره موبایل",
};
function uniqueFields(meta) {
if (meta?.target)
return Array.isArray(meta.target) ? meta.target : [meta.target];
const constraint = meta?.driverAdapterError?.cause?.constraint;
if (constraint?.fields)
return constraint.fields;
const match = constraint?.index?.match(/^[^_]+_(.+)_key$/);
return match ? match[1].split("_") : [];
}
let PrismaExceptionFilter = class PrismaExceptionFilter {
catch(error, host) {
const res = host.switchToHttp().getResponse();
switch (error.code) {
case "P2002": {
const fields = uniqueFields(error.meta)
.filter((f) => !f.endsWith("Id"))
.map((f) => FIELD_LABELS[f] ?? f)
.join("، ");
return res.status(HttpStatus.CONFLICT).json({
statusCode: HttpStatus.CONFLICT,
message: fields ? `مقدار «${fields}» تکراری است` : "مقدار تکراری است",
});
}
case "P2003":
return res.status(HttpStatus.CONFLICT).json({
statusCode: HttpStatus.CONFLICT,
message: "این مورد به داده‌های دیگری وابسته است",
});
case "P2025":
return res
.status(HttpStatus.NOT_FOUND)
.json({ statusCode: HttpStatus.NOT_FOUND, message: "مورد پیدا نشد" });
default:
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
message: "خطای پایگاه داده",
});
}
}
};
PrismaExceptionFilter = __decorate([
Catch(Prisma.PrismaClientKnownRequestError)
], PrismaExceptionFilter);
export { PrismaExceptionFilter };
//# sourceMappingURL=prisma-exception.filter.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma-exception.filter.js","sourceRoot":"","sources":["../../src/common/prisma-exception.filter.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAsB,KAAK,EAAwB,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7F,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAEvD,MAAM,YAAY,GAA2B;IAC3C,IAAI,EAAE,OAAO;IACb,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,cAAc;CACtB,CAAC;AAQF,SAAS,YAAY,CAAC,IAA4B;IAChD,IAAI,IAAI,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAClF,MAAM,UAAU,GAAG,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,UAAU,CAAC;IAC/D,IAAI,UAAU,EAAE,MAAM;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAEjD,MAAM,KAAK,GAAG,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3C,CAAC;AAIM,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAChC,KAAK,CAAC,KAA2C,EAAE,IAAmB;QACpE,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,WAAW,EAAY,CAAC;QAExD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,IAA8B,CAAC;qBAC9D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;qBAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;qBAChC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;oBAC1C,UAAU,EAAE,UAAU,CAAC,QAAQ;oBAC/B,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,MAAM,cAAc,CAAC,CAAC,CAAC,kBAAkB;iBACtE,CAAC,CAAC;YACL,CAAC;YACD,KAAK,OAAO;gBACV,OAAO,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;oBAC1C,UAAU,EAAE,UAAU,CAAC,QAAQ;oBAC/B,OAAO,EAAE,uCAAuC;iBACjD,CAAC,CAAC;YACL,KAAK,OAAO;gBACV,OAAO,GAAG;qBACP,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC;qBAC5B,IAAI,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;YAC1E;gBACE,OAAO,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC;oBACvD,UAAU,EAAE,UAAU,CAAC,qBAAqB;oBAC5C,OAAO,EAAE,kBAAkB;iBAC5B,CAAC,CAAC;QACP,CAAC;IACH,CAAC;CACF,CAAA;AA/BY,qBAAqB;IADjC,KAAK,CAAC,MAAM,CAAC,6BAA6B,CAAC;GAC/B,qBAAqB,CA+BjC"}
+23
View File
@@ -0,0 +1,23 @@
import { BadRequestException } from "@nestjs/common";
import { toIssues } from "@imen/shared";
import { z } from "zod";
z.config(z.locales.fa());
export class ZodPipe {
schema;
constructor(schema) {
this.schema = schema;
}
transform(value) {
const result = this.schema.safeParse(value);
if (!result.success) {
throw new BadRequestException({
statusCode: 400,
message: "اطلاعات واردشده معتبر نیست",
issues: toIssues(result.error),
});
}
return result.data;
}
}
export const IdPipe = new ZodPipe(z.coerce.number().int().positive());
//# sourceMappingURL=zod.pipe.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"zod.pipe.js","sourceRoot":"","sources":["../../src/common/zod.pipe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAsB,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;AAGzB,MAAM,OAAO,OAAO;IACW;IAA7B,YAA6B,MAAS;QAAT,WAAM,GAAN,MAAM,CAAG;IAAG,CAAC;IAE1C,SAAS,CAAC,KAAc;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,mBAAmB,CAAC;gBAC5B,UAAU,EAAE,GAAG;gBACf,OAAO,EAAE,4BAA4B;gBACrC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC;aAC/B,CAAC,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;CACF;AAGD,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC"}
+43
View File
@@ -0,0 +1,43 @@
import { existsSync } from "node:fs";
import { z } from "zod";
if (existsSync(".env"))
process.loadEnvFile(".env");
const bool = (fallback) => z
.enum(["true", "false", "1", "0"])
.optional()
.transform((v) => (v === undefined ? fallback : v === "true" || v === "1"));
const schema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
PORT: z.coerce.number().int().default(4000),
CORS_ORIGINS: z.string().default(""),
TRUST_PROXY: z.coerce.number().int().min(0).default(1),
DATABASE_URL: z.string().min(1),
JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"),
JWT_ACCESS_TTL: z.string().default("15m"),
REFRESH_TTL_DAYS: z.coerce.number().int().positive().default(30),
COOKIE_SECURE: bool(true),
S3_ENDPOINT: z.string().optional(),
S3_REGION: z.string().default("us-east-1"),
S3_BUCKET: z.string().default("imen-media"),
S3_ACCESS_KEY: z.string().default(""),
S3_SECRET_KEY: z.string().default(""),
S3_FORCE_PATH_STYLE: bool(true),
S3_ENSURE_BUCKET: bool(false),
MEDIA_PUBLIC_URL: z
.string()
.default("/media")
.transform((v) => v.replace(/\/+$/, "")),
WEB_REVALIDATE_URL: z.string().optional(),
REVALIDATE_SECRET: z.string().optional(),
ADMIN_PHONE: z.string().optional(),
ADMIN_PASSWORD: z.string().optional(),
ADMIN_NAME: z.string().default("مدیر سایت"),
});
const parsed = schema.safeParse(process.env);
if (!parsed.success) {
console.error("Invalid environment variables:\n" + z.prettifyError(parsed.error));
process.exit(1);
}
export const env = parsed.data;
export const isProd = env.NODE_ENV === "production";
//# sourceMappingURL=env.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,IAAI,UAAU,CAAC,MAAM,CAAC;IAAE,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AAEpD,MAAM,IAAI,GAAG,CAAC,QAAiB,EAAE,EAAE,CACjC,CAAC;KACE,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;KACjC,QAAQ,EAAE;KACV,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;AAEhF,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;IAC9E,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAEpC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAEtD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE/B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,2CAA2C,CAAC;IAC3E,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACzC,gBAAgB,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAChE,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC;IAEzB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC;IAC1C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IAC3C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,mBAAmB,EAAE,IAAI,CAAC,IAAI,CAAC;IAC/B,gBAAgB,EAAE,IAAI,CAAC,KAAK,CAAC;IAC7B,gBAAgB,EAAE,CAAC;SAChB,MAAM,EAAE;SACR,OAAO,CAAC,QAAQ,CAAC;SACjB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE1C,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACzC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAExC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC;CAC5C,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACpB,OAAO,CAAC,KAAK,CAAC,kCAAkC,GAAG,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC;AAC/B,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC"}
+163
View File
@@ -0,0 +1,163 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Body, Controller, Delete, Get, HttpCode, Injectable, Module, Param, Patch, Post, Query, Req, } from "@nestjs/common";
import { Throttle } from "@nestjs/throttler";
import { contactInputSchema, MESSAGE_STATUSES, messageUpdateSchema, } from "@imen/shared";
import { z } from "zod";
import { AdminAuth, CurrentUser, Roles } from "../common/auth.js";
import { AuditService } from "../common/common.module.js";
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
import { PrismaService } from "../prisma/prisma.service.js";
const listQuery = z.object({
status: z.enum(MESSAGE_STATUSES).optional(),
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(20),
});
export const toMessageDto = (m) => ({
id: m.id,
name: m.name,
org: m.org,
phone: m.phone,
message: m.message,
status: m.status,
createdAt: m.createdAt.toISOString(),
});
let ContactService = class ContactService {
prisma;
audit;
constructor(prisma, audit) {
this.prisma = prisma;
this.audit = audit;
}
async submit(input, ip, userAgent) {
if (input.website)
return;
await this.prisma.contactMessage.create({
data: {
name: input.name,
org: input.org,
phone: input.phone,
message: input.message,
ip: ip.slice(0, 64),
userAgent: userAgent.slice(0, 300),
},
});
}
async list(q) {
const where = q.status ? { status: q.status } : {};
const [items, total] = await this.prisma.$transaction([
this.prisma.contactMessage.findMany({
where,
orderBy: { id: "desc" },
skip: (q.page - 1) * q.pageSize,
take: q.pageSize,
}),
this.prisma.contactMessage.count({ where }),
]);
return { items: items.map(toMessageDto), total, page: q.page, pageSize: q.pageSize };
}
async setStatus(id, status) {
return toMessageDto(await this.prisma.contactMessage.update({ where: { id }, data: { status } }));
}
async remove(id, userId) {
const m = await this.prisma.contactMessage.delete({ where: { id } });
this.audit.log(userId, "delete", "message", id, m.name);
}
};
ContactService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
AuditService])
], ContactService);
export { ContactService };
let PublicContactController = class PublicContactController {
contact;
constructor(contact) {
this.contact = contact;
}
async submit(body, req) {
await this.contact.submit(body, req.ip ?? "", req.headers["user-agent"] ?? "");
return { ok: true };
}
};
__decorate([
Post(),
HttpCode(201),
Throttle({ default: { limit: 5, ttl: 10 * 60_000 } }),
__param(0, Body(new ZodPipe(contactInputSchema))),
__param(1, Req()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], PublicContactController.prototype, "submit", null);
PublicContactController = __decorate([
Controller("public/contact"),
__metadata("design:paramtypes", [ContactService])
], PublicContactController);
export { PublicContactController };
let MessagesController = class MessagesController {
contact;
constructor(contact) {
this.contact = contact;
}
list(q) {
return this.contact.list(q);
}
update(id, body) {
return this.contact.setStatus(id, body.status);
}
async remove(id, user) {
await this.contact.remove(id, user.id);
}
};
__decorate([
Get(),
__param(0, Query(new ZodPipe(listQuery))),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], MessagesController.prototype, "list", null);
__decorate([
Patch(":id"),
__param(0, Param("id", IdPipe)),
__param(1, Body(new ZodPipe(messageUpdateSchema))),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", void 0)
], MessagesController.prototype, "update", null);
__decorate([
Roles("ADMIN"),
Delete(":id"),
HttpCode(204),
__param(0, Param("id", IdPipe)),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", Promise)
], MessagesController.prototype, "remove", null);
MessagesController = __decorate([
AdminAuth(),
Controller("admin/messages"),
__metadata("design:paramtypes", [ContactService])
], MessagesController);
export { MessagesController };
let ContactModule = class ContactModule {
};
ContactModule = __decorate([
Module({
controllers: [PublicContactController, MessagesController],
providers: [ContactService],
})
], ContactModule);
export { ContactModule };
//# sourceMappingURL=contact.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"contact.module.js","sourceRoot":"","sources":["../../src/contact/contact.module.ts"],"names":[],"mappings":"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"}
+116
View File
@@ -0,0 +1,116 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { Controller, Get, Module } from "@nestjs/common";
import { SECTION_KEYS } from "@imen/shared";
import { AdminAuth } from "../common/auth.js";
import { toMessageDto } from "../contact/contact.module.js";
import { PrismaService } from "../prisma/prisma.service.js";
const DAY_NAMES = ["یکشنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه", "پنج‌شنبه", "جمعه", "شنبه"];
let DashboardController = class DashboardController {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async get() {
const [categories, products, draftProducts, newMessages, readMessages, archivedMessages, media, messages, activity, categoryRows, sectionRows,] = await Promise.all([
this.prisma.category.count(),
this.prisma.product.count(),
this.prisma.product.count({ where: { isPublished: false } }),
this.prisma.contactMessage.count({ where: { status: "NEW" } }),
this.prisma.contactMessage.count({ where: { status: "READ" } }),
this.prisma.contactMessage.count({ where: { status: "ARCHIVED" } }),
this.prisma.media.count(),
this.prisma.contactMessage.findMany({ orderBy: { id: "desc" }, take: 6 }),
this.prisma.auditLog.findMany({
orderBy: { id: "desc" },
take: 10,
include: { user: { select: { name: true } } },
}),
this.prisma.category.findMany({
select: { id: true, name: true, _count: { select: { products: true } } },
orderBy: { products: { _count: "desc" } },
take: 7,
}),
this.prisma.siteSection.findMany(),
]);
let hiddenCount = 0;
for (const s of sectionRows) {
if (s.data && typeof s.data === "object" && s.data.enabled === false) {
hiddenCount++;
}
}
const now = new Date();
const trend = [];
for (let i = 6; i >= 0; i--) {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i, 0, 0, 0);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i, 23, 59, 59, 999);
const count = await this.prisma.contactMessage.count({
where: { createdAt: { gte: start, lte: end } },
});
trend.push({
date: start.toISOString().slice(0, 10),
dayName: DAY_NAMES[start.getDay()],
messages: count,
});
}
return {
categories,
products,
draftProducts,
newMessages,
media,
recentMessages: messages.map(toMessageDto),
recentActivity: activity.map((a) => ({
id: a.id,
action: a.action,
entity: a.entity,
summary: a.summary,
user: a.user?.name ?? null,
createdAt: a.createdAt.toISOString(),
})),
categoryStats: categoryRows.map((c) => ({
id: c.id,
name: c.name,
count: c._count.products,
})),
messageBreakdown: {
new: newMessages,
read: readMessages,
archived: archivedMessages,
total: newMessages + readMessages + archivedMessages,
},
sectionsSummary: {
total: SECTION_KEYS.length,
visible: SECTION_KEYS.length - hiddenCount,
hidden: hiddenCount,
},
trend,
};
}
};
__decorate([
Get(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], DashboardController.prototype, "get", null);
DashboardController = __decorate([
AdminAuth(),
Controller("admin/dashboard"),
__metadata("design:paramtypes", [PrismaService])
], DashboardController);
export { DashboardController };
let DashboardModule = class DashboardModule {
};
DashboardModule = __decorate([
Module({ controllers: [DashboardController] })
], DashboardModule);
export { DashboardModule };
//# sourceMappingURL=dashboard.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"dashboard.module.js","sourceRoot":"","sources":["../../src/dashboard/dashboard.module.ts"],"names":[],"mappings":"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"}
+5
View File
@@ -0,0 +1,5 @@
import * as Prisma from './internal/prismaNamespaceBrowser.js';
export { Prisma };
export * as $Enums from './enums.js';
export * from './enums.js';
//# sourceMappingURL=browser.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../../src/generated/prisma/browser.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,MAAM,MAAM,sCAAsC,CAAA;AAC9D,OAAO,EAAE,MAAM,EAAE,CAAA;AACjB,OAAO,KAAK,MAAM,MAAM,YAAY,CAAA;AACpC,cAAc,YAAY,CAAC"}
+10
View File
@@ -0,0 +1,10 @@
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
import * as $Class from "./internal/class.js";
import * as Prisma from "./internal/prismaNamespace.js";
export * as $Enums from './enums.js';
export * from "./enums.js";
export const PrismaClient = $Class.getPrismaClientClass();
export { Prisma };
//# sourceMappingURL=client.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/generated/prisma/client.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,UAAU,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAItE,OAAO,KAAK,MAAM,MAAM,qBAAqB,CAAA;AAC7C,OAAO,KAAK,MAAM,MAAM,+BAA+B,CAAA;AAEvD,OAAO,KAAK,MAAM,MAAM,YAAY,CAAA;AACpC,cAAc,YAAY,CAAA;AAgB1B,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,oBAAoB,EAAE,CAAA;AAEzD,OAAO,EAAE,MAAM,EAAE,CAAA"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=commonInputTypes.js.map
@@ -0,0 +1 @@
{"version":3,"file":"commonInputTypes.js","sourceRoot":"","sources":["../../../src/generated/prisma/commonInputTypes.ts"],"names":[],"mappings":""}
+10
View File
@@ -0,0 +1,10 @@
export const Role = {
ADMIN: 'ADMIN',
EDITOR: 'EDITOR'
};
export const MessageStatus = {
NEW: 'NEW',
READ: 'READ',
ARCHIVED: 'ARCHIVED'
};
//# sourceMappingURL=enums.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"enums.js","sourceRoot":"","sources":["../../../src/generated/prisma/enums.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACR,CAAA;AAKV,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,UAAU;CACZ,CAAA"}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"class.js","sourceRoot":"","sources":["../../../../src/generated/prisma/internal/class.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,OAAO,MAAM,+BAA+B,CAAA;AAIxD,MAAM,MAAM,GAAkC;IAC5C,iBAAiB,EAAE,EAAE;IACrB,eAAe,EAAE,QAAQ;IACzB,eAAe,EAAE,0CAA0C;IAC3D,gBAAgB,EAAE,YAAY;IAC9B,cAAc,EAAE,qlMAAqlM;IACrmM,kBAAkB,EAAE;QAClB,QAAQ,EAAE,EAAE;QACZ,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;KACZ;IACD,wBAAwB,EAAE;QACxB,SAAS,EAAE,EAAE;QACb,OAAO,EAAE,EAAE;KACZ;CACF,CAAA;AAED,MAAM,CAAC,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,83QAA83Q,CAAC,CAAA;AACp6Q,MAAM,CAAC,sBAAsB,GAAG;IAC9B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,w0LAAw0L,CAAC;IAC71L,KAAK,EAAE,0psCAA0psC;CAClqsC,CAAA;AAED,KAAK,UAAU,kBAAkB,CAAC,UAAkB;IAClD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;IACnD,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,CAAC,YAAY,GAAG;IACpB,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,MAAM,CAAC,8DAA8D,CAAC;IAEpG,0BAA0B,EAAE,KAAK,IAAI,EAAE;QACrC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,0EAA0E,CAAC,CAAA;QACzG,OAAO,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC;IAED,UAAU,EAAE,6BAA6B;CAC1C,CAAA;AA4OD,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAC,eAAe,CAAC,MAAM,CAAuC,CAAA;AAC9E,CAAC"}
@@ -0,0 +1,181 @@
import * as runtime from "@prisma/client/runtime/client";
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError;
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError;
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError;
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError;
export const PrismaClientValidationError = runtime.PrismaClientValidationError;
export const sql = runtime.sqltag;
export const empty = runtime.empty;
export const join = runtime.join;
export const raw = runtime.raw;
export const Sql = runtime.Sql;
export const Decimal = runtime.Decimal;
export const getExtensionContext = runtime.Extensions.getExtensionContext;
export const prismaVersion = {
client: "7.10.1",
engine: "d1b3f377ca835d73fd50954a5e22128b9ebdd6b8"
};
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull,
JsonNull: runtime.NullTypes.JsonNull,
AnyNull: runtime.NullTypes.AnyNull,
};
export const DbNull = runtime.DbNull;
export const JsonNull = runtime.JsonNull;
export const AnyNull = runtime.AnyNull;
export const ModelName = {
AdminUser: 'AdminUser',
RefreshToken: 'RefreshToken',
Media: 'Media',
Category: 'Category',
ProductGroup: 'ProductGroup',
Product: 'Product',
ProductImage: 'ProductImage',
SiteSection: 'SiteSection',
ContactMessage: 'ContactMessage',
AuditLog: 'AuditLog'
};
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
export const AdminUserScalarFieldEnum = {
id: 'id',
phone: 'phone',
name: 'name',
passwordHash: 'passwordHash',
role: 'role',
isActive: 'isActive',
lastLoginAt: 'lastLoginAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
export const RefreshTokenScalarFieldEnum = {
id: 'id',
userId: 'userId',
tokenHash: 'tokenHash',
expiresAt: 'expiresAt',
revokedAt: 'revokedAt',
userAgent: 'userAgent',
createdAt: 'createdAt'
};
export const MediaScalarFieldEnum = {
id: 'id',
key: 'key',
mime: 'mime',
size: 'size',
width: 'width',
height: 'height',
alt: 'alt',
originalName: 'originalName',
createdAt: 'createdAt'
};
export const CategoryScalarFieldEnum = {
id: 'id',
slug: 'slug',
code: 'code',
name: 'name',
nameEn: 'nameEn',
shortDesc: 'shortDesc',
fullDesc: 'fullDesc',
icon: 'icon',
coverImageId: 'coverImageId',
sortOrder: 'sortOrder',
isPublished: 'isPublished',
seoTitle: 'seoTitle',
seoDescription: 'seoDescription',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
export const ProductGroupScalarFieldEnum = {
id: 'id',
categoryId: 'categoryId',
slug: 'slug',
name: 'name',
desc: 'desc',
sortOrder: 'sortOrder'
};
export const ProductScalarFieldEnum = {
id: 'id',
categoryId: 'categoryId',
groupId: 'groupId',
code: 'code',
slug: 'slug',
name: 'name',
nameEn: 'nameEn',
desc: 'desc',
specs: 'specs',
variants: 'variants',
details: 'details',
brand: 'brand',
origin: 'origin',
sizes: 'sizes',
standards: 'standards',
features: 'features',
applications: 'applications',
attributes: 'attributes',
packaging: 'packaging',
minOrder: 'minOrder',
seoTitle: 'seoTitle',
seoDescription: 'seoDescription',
imageId: 'imageId',
sortOrder: 'sortOrder',
isPublished: 'isPublished',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
export const ProductImageScalarFieldEnum = {
productId: 'productId',
mediaId: 'mediaId',
sortOrder: 'sortOrder'
};
export const SiteSectionScalarFieldEnum = {
key: 'key',
data: 'data',
updatedById: 'updatedById',
updatedAt: 'updatedAt'
};
export const ContactMessageScalarFieldEnum = {
id: 'id',
name: 'name',
org: 'org',
phone: 'phone',
message: 'message',
status: 'status',
ip: 'ip',
userAgent: 'userAgent',
createdAt: 'createdAt'
};
export const AuditLogScalarFieldEnum = {
id: 'id',
userId: 'userId',
action: 'action',
entity: 'entity',
entityId: 'entityId',
summary: 'summary',
createdAt: 'createdAt'
};
export const SortOrder = {
asc: 'asc',
desc: 'desc'
};
export const JsonNullValueInput = {
JsonNull: JsonNull
};
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
export const NullsOrder = {
first: 'first',
last: 'last'
};
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
};
export const defineExtension = runtime.Extensions.defineExtension;
//# sourceMappingURL=prismaNamespace.js.map
@@ -0,0 +1 @@
{"version":3,"file":"prismaNamespace.js","sourceRoot":"","sources":["../../../../src/generated/prisma/internal/prismaNamespace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
import * as runtime from "@prisma/client/runtime/index-browser";
export const Decimal = runtime.Decimal;
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull,
JsonNull: runtime.NullTypes.JsonNull,
AnyNull: runtime.NullTypes.AnyNull,
};
export const DbNull = runtime.DbNull;
export const JsonNull = runtime.JsonNull;
export const AnyNull = runtime.AnyNull;
export const ModelName = {
AdminUser: 'AdminUser',
RefreshToken: 'RefreshToken',
Media: 'Media',
Category: 'Category',
ProductGroup: 'ProductGroup',
Product: 'Product',
ProductImage: 'ProductImage',
SiteSection: 'SiteSection',
ContactMessage: 'ContactMessage',
AuditLog: 'AuditLog'
};
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
export const AdminUserScalarFieldEnum = {
id: 'id',
phone: 'phone',
name: 'name',
passwordHash: 'passwordHash',
role: 'role',
isActive: 'isActive',
lastLoginAt: 'lastLoginAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
export const RefreshTokenScalarFieldEnum = {
id: 'id',
userId: 'userId',
tokenHash: 'tokenHash',
expiresAt: 'expiresAt',
revokedAt: 'revokedAt',
userAgent: 'userAgent',
createdAt: 'createdAt'
};
export const MediaScalarFieldEnum = {
id: 'id',
key: 'key',
mime: 'mime',
size: 'size',
width: 'width',
height: 'height',
alt: 'alt',
originalName: 'originalName',
createdAt: 'createdAt'
};
export const CategoryScalarFieldEnum = {
id: 'id',
slug: 'slug',
code: 'code',
name: 'name',
nameEn: 'nameEn',
shortDesc: 'shortDesc',
fullDesc: 'fullDesc',
icon: 'icon',
coverImageId: 'coverImageId',
sortOrder: 'sortOrder',
isPublished: 'isPublished',
seoTitle: 'seoTitle',
seoDescription: 'seoDescription',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
export const ProductGroupScalarFieldEnum = {
id: 'id',
categoryId: 'categoryId',
slug: 'slug',
name: 'name',
desc: 'desc',
sortOrder: 'sortOrder'
};
export const ProductScalarFieldEnum = {
id: 'id',
categoryId: 'categoryId',
groupId: 'groupId',
code: 'code',
slug: 'slug',
name: 'name',
nameEn: 'nameEn',
desc: 'desc',
specs: 'specs',
variants: 'variants',
details: 'details',
brand: 'brand',
origin: 'origin',
sizes: 'sizes',
standards: 'standards',
features: 'features',
applications: 'applications',
attributes: 'attributes',
packaging: 'packaging',
minOrder: 'minOrder',
seoTitle: 'seoTitle',
seoDescription: 'seoDescription',
imageId: 'imageId',
sortOrder: 'sortOrder',
isPublished: 'isPublished',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
export const ProductImageScalarFieldEnum = {
productId: 'productId',
mediaId: 'mediaId',
sortOrder: 'sortOrder'
};
export const SiteSectionScalarFieldEnum = {
key: 'key',
data: 'data',
updatedById: 'updatedById',
updatedAt: 'updatedAt'
};
export const ContactMessageScalarFieldEnum = {
id: 'id',
name: 'name',
org: 'org',
phone: 'phone',
message: 'message',
status: 'status',
ip: 'ip',
userAgent: 'userAgent',
createdAt: 'createdAt'
};
export const AuditLogScalarFieldEnum = {
id: 'id',
userId: 'userId',
action: 'action',
entity: 'entity',
entityId: 'entityId',
summary: 'summary',
createdAt: 'createdAt'
};
export const SortOrder = {
asc: 'asc',
desc: 'desc'
};
export const JsonNullValueInput = {
JsonNull: JsonNull
};
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
export const NullsOrder = {
first: 'first',
last: 'last'
};
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
};
//# sourceMappingURL=prismaNamespaceBrowser.js.map
@@ -0,0 +1 @@
{"version":3,"file":"prismaNamespaceBrowser.js","sourceRoot":"","sources":["../../../../src/generated/prisma/internal/prismaNamespaceBrowser.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=models.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../../src/generated/prisma/models.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=AdminUser.js.map
@@ -0,0 +1 @@
{"version":3,"file":"AdminUser.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/AdminUser.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=AuditLog.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"AuditLog.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/AuditLog.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=Category.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Category.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/Category.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ContactMessage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ContactMessage.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/ContactMessage.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=Media.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Media.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/Media.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=Product.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Product.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/Product.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ProductGroup.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ProductGroup.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/ProductGroup.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ProductImage.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ProductImage.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/ProductImage.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=RefreshToken.js.map
@@ -0,0 +1 @@
{"version":3,"file":"RefreshToken.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/RefreshToken.ts"],"names":[],"mappings":""}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=SiteSection.js.map
@@ -0,0 +1 @@
{"version":3,"file":"SiteSection.js","sourceRoot":"","sources":["../../../../src/generated/prisma/models/SiteSection.ts"],"names":[],"mappings":""}
+21
View File
@@ -0,0 +1,21 @@
import { env } from "./config/env.js";
import { NestFactory } from "@nestjs/core";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import { AppModule } from "./app.module.js";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.set("trust proxy", env.TRUST_PROXY);
app.setGlobalPrefix("api");
app.use(helmet());
app.use(cookieParser());
const origins = env.CORS_ORIGINS.split(",")
.map((o) => o.trim())
.filter(Boolean);
if (origins.length)
app.enableCors({ origin: origins, credentials: true });
app.enableShutdownHooks();
await app.listen(env.PORT);
}
await bootstrap();
//# sourceMappingURL=main.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,MAAM,CAAyB,SAAS,CAAC,CAAC;IAGxE,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;IACxC,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC3B,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAClB,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;IAExB,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;SACxC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,IAAI,OAAO,CAAC,MAAM;QAAE,GAAG,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3E,GAAG,CAAC,mBAAmB,EAAE,CAAC;IAC1B,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,SAAS,EAAE,CAAC"}
+95
View File
@@ -0,0 +1,95 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Body, Controller, Delete, Get, HttpCode, Module, Param, Patch, Post, Query, UploadedFile, UseInterceptors, } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { mediaUpdateSchema } from "@imen/shared";
import { z } from "zod";
import { AdminAuth, CurrentUser, Roles } from "../common/auth.js";
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
import { MediaService } from "./media.service.js";
const listQuery = z.object({
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(40),
q: z.string().trim().max(100).optional(),
});
let MediaController = class MediaController {
media;
constructor(media) {
this.media = media;
}
list(q) {
return this.media.list(q.page, q.pageSize, q.q);
}
upload(file, alt, user) {
return this.media.upload(file, typeof alt === "string" ? alt : "", user.id);
}
update(id, body, user) {
return this.media.updateAlt(id, body.alt, user.id);
}
async remove(id, user) {
await this.media.remove(id, user.id);
}
};
__decorate([
Get(),
__param(0, Query(new ZodPipe(listQuery))),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], MediaController.prototype, "list", null);
__decorate([
Post(),
UseInterceptors(FileInterceptor("file", { limits: { fileSize: 15 * 1024 * 1024 } })),
__param(0, UploadedFile()),
__param(1, Body("alt")),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object, Object]),
__metadata("design:returntype", void 0)
], MediaController.prototype, "upload", null);
__decorate([
Patch(":id"),
__param(0, Param("id", IdPipe)),
__param(1, Body(new ZodPipe(mediaUpdateSchema))),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object, Object]),
__metadata("design:returntype", void 0)
], MediaController.prototype, "update", null);
__decorate([
Roles("ADMIN"),
Delete(":id"),
HttpCode(204),
__param(0, Param("id", IdPipe)),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", Promise)
], MediaController.prototype, "remove", null);
MediaController = __decorate([
AdminAuth(),
Controller("admin/media"),
__metadata("design:paramtypes", [MediaService])
], MediaController);
export { MediaController };
let MediaModule = class MediaModule {
};
MediaModule = __decorate([
Module({
controllers: [MediaController],
providers: [MediaService],
exports: [MediaService],
})
], MediaModule);
export { MediaModule };
//# sourceMappingURL=media.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"media.controller.js","sourceRoot":"","sources":["../../src/media/media.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EACL,IAAI,EACJ,UAAU,EACV,MAAM,EACN,GAAG,EACH,QAAQ,EACR,MAAM,EACN,KAAK,EACL,KAAK,EACL,IAAI,EACJ,KAAK,EACL,YAAY,EACZ,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAiB,WAAW,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IACzB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7D,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;CACzC,CAAC,CAAC;AAII,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAGpD,IAAI,CAAgC,CAA6B;QAC/D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAID,MAAM,CACY,IAAqC,EACxC,GAAY,EACV,IAAc;QAE7B,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IAGD,MAAM,CACiB,EAAU,EACO,IAAwC,EAC/D,IAAc;QAE7B,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IAKK,AAAN,KAAK,CAAC,MAAM,CAAsB,EAAU,EAAiB,IAAc;QACzE,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC;CACF,CAAA;AA7BC;IADC,GAAG,EAAE;IACA,WAAA,KAAK,CAAC,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC,CAAA;;;;2CAElC;AAID;IAFC,IAAI,EAAE;IACN,eAAe,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;IAElF,WAAA,YAAY,EAAE,CAAA;IACd,WAAA,IAAI,CAAC,KAAK,CAAC,CAAA;IACX,WAAA,WAAW,EAAE,CAAA;;;;6CAGf;AAGD;IADC,KAAK,CAAC,KAAK,CAAC;IAEV,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnB,WAAA,IAAI,CAAC,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAA;IACpC,WAAA,WAAW,EAAE,CAAA;;;;6CAGf;AAKK;IAHL,KAAK,CAAC,OAAO,CAAC;IACd,MAAM,CAAC,KAAK,CAAC;IACb,QAAQ,CAAC,GAAG,CAAC;IACA,WAAA,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAAc,WAAA,WAAW,EAAE,CAAA;;;;6CAE3D;AAhCU,eAAe;IAF3B,SAAS,EAAE;IACX,UAAU,CAAC,aAAa,CAAC;qCAEY,YAAY;GADrC,eAAe,CAiC3B;;AAOM,IAAM,WAAW,GAAjB,MAAM,WAAW;CAAG,CAAA;AAAd,WAAW;IALvB,MAAM,CAAC;QACN,WAAW,EAAE,CAAC,eAAe,CAAC;QAC9B,SAAS,EAAE,CAAC,YAAY,CAAC;QACzB,OAAO,EAAE,CAAC,YAAY,CAAC;KACxB,CAAC;GACW,WAAW,CAAG"}
+138
View File
@@ -0,0 +1,138 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var MediaService_1;
import { randomUUID } from "node:crypto";
import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, } from "@nestjs/common";
import { env } from "../config/env.js";
import { AuditService, RevalidateService } from "../common/common.module.js";
import { PrismaService } from "../prisma/prisma.service.js";
import { ACCEPTED_IMAGE_TYPES, S3Storage, toWebp } from "./s3-storage.js";
let MediaService = MediaService_1 = class MediaService {
prisma;
audit;
revalidate;
logger = new Logger(MediaService_1.name);
storage = new S3Storage();
constructor(prisma, audit, revalidate) {
this.prisma = prisma;
this.audit = audit;
this.revalidate = revalidate;
}
async onModuleInit() {
if (!env.S3_ENSURE_BUCKET)
return;
try {
await this.storage.ensureBucket();
}
catch (e) {
this.logger.error(`could not prepare bucket "${env.S3_BUCKET}": ${String(e)}`);
}
}
url(key) {
return this.storage.publicUrl(key);
}
toDto(m) {
return {
id: m.id,
url: this.url(m.key),
alt: m.alt,
mime: m.mime,
size: m.size,
width: m.width,
height: m.height,
originalName: m.originalName,
createdAt: m.createdAt.toISOString(),
};
}
async list(page, pageSize, q) {
const where = q
? {
OR: [
{ originalName: { contains: q, mode: "insensitive" } },
{ alt: { contains: q, mode: "insensitive" } },
],
}
: {};
const [items, total] = await this.prisma.$transaction([
this.prisma.media.findMany({
where,
orderBy: { id: "desc" },
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.media.count({ where }),
]);
return { items: items.map((m) => this.toDto(m)), total, page, pageSize };
}
async upload(file, alt, userId) {
if (!file)
throw new BadRequestException("فایلی ارسال نشده است");
if (!ACCEPTED_IMAGE_TYPES.includes(file.mimetype)) {
throw new BadRequestException("فقط تصاویر JPG، PNG، WebP، AVIF و GIF پذیرفته می‌شوند");
}
let image;
try {
image = await toWebp(file.buffer);
}
catch {
throw new BadRequestException("فایل تصویر قابل پردازش نیست");
}
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, "0");
const key = `uploads/${now.getFullYear()}/${month}/${randomUUID()}.webp`;
await this.storage.put(key, image.data, "image/webp");
const media = await this.prisma.media.create({
data: {
key,
mime: "image/webp",
size: image.size,
width: image.width,
height: image.height,
alt: alt.trim().slice(0, 200),
originalName: Buffer.from(file.originalname, "latin1").toString("utf8").slice(0, 200),
},
});
this.audit.log(userId, "upload", "media", media.id, media.originalName);
return this.toDto(media);
}
async updateAlt(id, alt, userId) {
const media = await this.prisma.media.update({ where: { id }, data: { alt } });
this.audit.log(userId, "update", "media", id, media.originalName);
this.revalidate.trigger();
return this.toDto(media);
}
async remove(id, userId) {
const media = await this.prisma.media.findUnique({
where: { id },
include: { _count: { select: { categoryCovers: true, productImages: true, productGallery: true } } },
});
if (!media)
throw new NotFoundException("فایل پیدا نشد");
const { categoryCovers, productImages, productGallery } = media._count;
const sections = await this.prisma.$queryRaw `
SELECT key FROM "SiteSection" WHERE data::text LIKE ${"%" + media.key + "%"}`;
if (categoryCovers + productImages + productGallery + sections.length > 0) {
throw new ConflictException("این تصویر در محصولات، دسته‌ها یا بخش‌های سایت استفاده شده و قابل حذف نیست");
}
await this.prisma.media.delete({ where: { id } });
await this.storage.remove(media.key).catch((e) => {
this.logger.warn(`could not delete object ${media.key}: ${String(e)}`);
});
this.audit.log(userId, "delete", "media", id, media.originalName);
}
};
MediaService = MediaService_1 = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
AuditService,
RevalidateService])
], MediaService);
export { MediaService };
//# sourceMappingURL=media.service.js.map
File diff suppressed because one or more lines are too long
+69
View File
@@ -0,0 +1,69 @@
import { CreateBucketCommand, DeleteObjectCommand, HeadBucketCommand, PutBucketPolicyCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
import sharp from "sharp";
import { env } from "../config/env.js";
export class S3Storage {
client = new S3Client({
endpoint: env.S3_ENDPOINT,
region: env.S3_REGION,
forcePathStyle: env.S3_FORCE_PATH_STYLE,
credentials: { accessKeyId: env.S3_ACCESS_KEY, secretAccessKey: env.S3_SECRET_KEY },
});
bucket = env.S3_BUCKET;
publicUrl(key) {
return `${env.MEDIA_PUBLIC_URL}/${key}`;
}
async put(key, body, contentType) {
await this.client.send(new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: body,
ContentType: contentType,
CacheControl: "public, max-age=31536000, immutable",
}));
}
async remove(key) {
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
}
async ensureBucket() {
try {
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
}
catch {
await this.client.send(new CreateBucketCommand({ Bucket: this.bucket }));
}
try {
await this.client.send(new PutBucketPolicyCommand({
Bucket: this.bucket,
Policy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: { AWS: ["*"] },
Action: ["s3:GetObject"],
Resource: [`arn:aws:s3:::${this.bucket}/*`],
},
],
}),
}));
}
catch {
}
}
}
export const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/png",
"image/webp",
"image/avif",
"image/gif",
];
export async function toWebp(input) {
const { data, info } = await sharp(input)
.rotate()
.resize({ width: 2000, height: 2000, fit: "inside", withoutEnlargement: true })
.webp({ quality: 82 })
.toBuffer({ resolveWithObject: true });
return { data, width: info.width, height: info.height, size: info.size };
}
//# sourceMappingURL=s3-storage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"s3-storage.js","sourceRoot":"","sources":["../../src/media/s3-storage.ts"],"names":[],"mappings":"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"}
+35
View File
@@ -0,0 +1,35 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { Global, Injectable, Module } from "@nestjs/common";
import { PrismaPg } from "@prisma/adapter-pg";
import { env } from "../config/env.js";
import { PrismaClient } from "../generated/prisma/client.js";
export const createPrismaClient = () => new PrismaClient({ adapter: new PrismaPg({ connectionString: env.DATABASE_URL }) });
let PrismaService = class PrismaService extends PrismaClient {
constructor() {
super({ adapter: new PrismaPg({ connectionString: env.DATABASE_URL }) });
}
async onModuleDestroy() {
await this.$disconnect();
}
};
PrismaService = __decorate([
Injectable(),
__metadata("design:paramtypes", [])
], PrismaService);
export { PrismaService };
let PrismaModule = class PrismaModule {
};
PrismaModule = __decorate([
Global(),
Module({ providers: [PrismaService], exports: [PrismaService] })
], PrismaModule);
export { PrismaModule };
//# sourceMappingURL=prisma.service.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.service.js","sourceRoot":"","sources":["../../src/prisma/prisma.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAwB,MAAM,gBAAgB,CAAC;AAClF,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,EAAE,CACrC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,QAAQ,CAAC,EAAE,gBAAgB,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;AAG/E,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,YAAY;IAC7C;QACE,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,QAAQ,CAAC,EAAE,gBAAgB,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3B,CAAC;CACF,CAAA;AARY,aAAa;IADzB,UAAU,EAAE;;GACA,aAAa,CAQzB;;AAIM,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,YAAY;IAFxB,MAAM,EAAE;IACR,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC;GACpD,YAAY,CAAG"}
+244
View File
@@ -0,0 +1,244 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Controller, Get, Injectable, Module, NotFoundException, Param } from "@nestjs/common";
import { MediaModule } from "../media/media.controller.js";
import { MediaService } from "../media/media.service.js";
import { PrismaService } from "../prisma/prisma.service.js";
import { SectionsModule, SectionsService } from "../sections/sections.module.js";
const visible = { isPublished: true, category: { isPublished: true } };
const RELATED_LIMIT = 4;
const isAttribute = (a) => !!a && typeof a === "object" && typeof a.name === "string" && typeof a.value === "string";
let PublicService = class PublicService {
prisma;
media;
sections;
constructor(prisma, media, sections) {
this.prisma = prisma;
this.media = media;
this.sections = sections;
}
async site() {
const [sections, categories] = await Promise.all([this.sections.getAll(), this.categories()]);
return { sections, categories };
}
async categories() {
const rows = await this.prisma.category.findMany({
where: { isPublished: true },
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
include: {
coverImage: true,
_count: { select: { products: { where: { isPublished: true } } } },
},
});
return rows.map((c) => ({
slug: c.slug,
code: c.code,
name: c.name,
nameEn: c.nameEn,
shortDesc: c.shortDesc,
icon: c.icon,
coverImage: c.coverImage && this.media.url(c.coverImage.key),
productCount: c._count.products,
}));
}
async category(slug) {
const c = await this.prisma.category.findFirst({
where: { slug, isPublished: true },
include: {
coverImage: true,
groups: { orderBy: { sortOrder: "asc" } },
products: {
where: { isPublished: true },
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
include: { image: true, group: { select: { slug: true } } },
},
},
});
if (!c)
throw new NotFoundException("دسته‌بندی یافت نشد");
const products = c.products.map((p) => this.toPublicProduct(p));
const usedGroups = new Set(products.map((p) => p.group));
return {
slug: c.slug,
code: c.code,
name: c.name,
nameEn: c.nameEn,
shortDesc: c.shortDesc,
fullDesc: c.fullDesc,
icon: c.icon,
seoTitle: c.seoTitle,
seoDescription: c.seoDescription,
coverImage: c.coverImage && this.media.url(c.coverImage.key),
productCount: products.length,
groups: c.groups
.filter((g) => usedGroups.has(g.slug))
.map((g) => ({ slug: g.slug, name: g.name, desc: g.desc })),
products,
};
}
async product(slug) {
const p = await this.prisma.product.findFirst({
where: { slug, ...visible },
include: {
image: true,
gallery: { orderBy: { sortOrder: "asc" }, include: { media: true } },
group: true,
category: { include: { coverImage: true } },
},
});
if (!p)
throw new NotFoundException("محصول یافت نشد");
const media = new Map();
for (const m of [p.image, ...p.gallery.map((g) => g.media)])
if (m)
media.set(m.id, m);
const siblings = await this.prisma.product.findMany({
where: { categoryId: p.categoryId, isPublished: true, id: { not: p.id } },
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
include: { image: true, group: { select: { slug: true } } },
});
const sameGroup = (s) => p.groupId !== null && s.groupId === p.groupId;
const related = [...siblings.filter(sameGroup), ...siblings.filter((s) => !sameGroup(s))]
.slice(0, RELATED_LIMIT)
.map((s) => this.toPublicProduct(s));
return {
...this.toPublicProduct(p),
details: p.details,
brand: p.brand,
origin: p.origin,
sizes: p.sizes,
standards: p.standards,
features: p.features,
applications: p.applications,
attributes: Array.isArray(p.attributes) ? p.attributes.filter(isAttribute) : [],
packaging: p.packaging,
minOrder: p.minOrder,
seoTitle: p.seoTitle,
seoDescription: p.seoDescription,
images: [...media.values()].map((m) => ({
url: this.media.url(m.key),
alt: m.alt || p.name,
width: m.width,
height: m.height,
})),
groupName: p.group?.name ?? null,
category: {
slug: p.category.slug,
code: p.category.code,
name: p.category.name,
nameEn: p.category.nameEn,
fullDesc: p.category.fullDesc,
coverImage: p.category.coverImage && this.media.url(p.category.coverImage.key),
},
related,
updatedAt: p.updatedAt.toISOString(),
};
}
async productRefs() {
const rows = await this.prisma.product.findMany({
where: visible,
orderBy: [{ category: { sortOrder: "asc" } }, { sortOrder: "asc" }],
select: { slug: true, updatedAt: true },
});
return rows.map((r) => ({ slug: r.slug, updatedAt: r.updatedAt.toISOString() }));
}
toPublicProduct(p) {
return {
code: p.code,
slug: p.slug,
name: p.name,
nameEn: p.nameEn,
desc: p.desc,
specs: p.specs,
variants: p.variants,
image: p.image && this.media.url(p.image.key),
group: p.group?.slug ?? null,
};
}
};
PublicService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
MediaService,
SectionsService])
], PublicService);
export { PublicService };
let PublicController = class PublicController {
publicData;
constructor(publicData) {
this.publicData = publicData;
}
site() {
return this.publicData.site();
}
categories() {
return this.publicData.categories();
}
category(slug) {
return this.publicData.category(slug);
}
products() {
return this.publicData.productRefs();
}
product(slug) {
return this.publicData.product(slug);
}
};
__decorate([
Get("site"),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], PublicController.prototype, "site", null);
__decorate([
Get("categories"),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], PublicController.prototype, "categories", null);
__decorate([
Get("categories/:slug"),
__param(0, Param("slug")),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], PublicController.prototype, "category", null);
__decorate([
Get("products"),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], PublicController.prototype, "products", null);
__decorate([
Get("products/:slug"),
__param(0, Param("slug")),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], PublicController.prototype, "product", null);
PublicController = __decorate([
Controller("public"),
__metadata("design:paramtypes", [PublicService])
], PublicController);
export { PublicController };
let PublicModule = class PublicModule {
};
PublicModule = __decorate([
Module({
imports: [MediaModule, SectionsModule],
controllers: [PublicController],
providers: [PublicService],
})
], PublicModule);
export { PublicModule };
//# sourceMappingURL=public.module.js.map
File diff suppressed because one or more lines are too long
+191
View File
@@ -0,0 +1,191 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { BadRequestException, Body, Controller, ForbiddenException, Get, Injectable, Module, NotFoundException, Param, Patch, Put, ServiceUnavailableException, } from "@nestjs/common";
import { isSectionKey, SECTION_KEYS, sectionSchemas, toIssues, } from "@imen/shared";
import { AdminAuth, CurrentUser } from "../common/auth.js";
import { AuditService, RevalidateService } from "../common/common.module.js";
import { PrismaService } from "../prisma/prisma.service.js";
let SectionsService = class SectionsService {
prisma;
audit;
revalidate;
constructor(prisma, audit, revalidate) {
this.prisma = prisma;
this.audit = audit;
this.revalidate = revalidate;
}
async getAll() {
const rows = await this.prisma.siteSection.findMany();
const byKey = new Map(rows.map((r) => [r.key, r.data]));
const missing = SECTION_KEYS.filter((k) => !byKey.has(k));
if (missing.length) {
throw new ServiceUnavailableException(`محتوای سایت مقداردهی نشده است (${missing.join(", ")}) — pnpm db:seed را اجرا کنید`);
}
return Object.fromEntries(SECTION_KEYS.map((k) => [k, sectionSchemas[k].parse(byKey.get(k))]));
}
async get(key) {
if (!isSectionKey(key))
throw new NotFoundException("بخش پیدا نشد");
const row = await this.prisma.siteSection.findUnique({ where: { key } });
if (!row)
throw new NotFoundException("بخش پیدا نشد");
return { key, data: sectionSchemas[key].parse(row.data), updatedAt: row.updatedAt };
}
async update(key, body, user) {
if (!isSectionKey(key))
throw new NotFoundException("بخش پیدا نشد");
if (key === "settings" && user.role !== "ADMIN") {
throw new ForbiddenException("فقط مدیر کل می‌تواند تنظیمات سایت را تغییر دهد");
}
const parsed = sectionSchemas[key].safeParse(body);
if (!parsed.success) {
throw new BadRequestException({
statusCode: 400,
message: "اطلاعات واردشده معتبر نیست",
issues: toIssues(parsed.error),
});
}
const row = await this.prisma.siteSection.upsert({
where: { key },
create: { key, data: parsed.data, updatedById: user.id },
update: { data: parsed.data, updatedById: user.id },
});
this.audit.log(user.id, "update", "section", key, key);
this.revalidate.trigger();
return { key, data: parsed.data, updatedAt: row.updatedAt };
}
async listAdmin() {
const rows = await this.prisma.siteSection.findMany();
const byKey = new Map(rows.map((r) => [r.key, r]));
return SECTION_KEYS.map((key) => {
const row = byKey.get(key);
let data = null;
if (row) {
const parsed = sectionSchemas[key].safeParse(row.data);
data = parsed.success ? parsed.data : row.data;
}
return {
key,
updatedAt: row?.updatedAt?.toISOString() ?? null,
enabled: data && typeof data === "object" && "enabled" in data
? data.enabled !== false
: true,
};
});
}
async setVisibility(key, enabled, user) {
if (!isSectionKey(key))
throw new NotFoundException("بخش پیدا نشد");
if (key === "settings") {
throw new BadRequestException("تنظیمات سایت قابلیت تغییر وضعیت نمایش ندارد");
}
const row = await this.prisma.siteSection.findUnique({ where: { key } });
if (!row)
throw new NotFoundException("بخش پیدا نشد");
const currentData = row.data && typeof row.data === "object" ? { ...row.data } : {};
currentData.enabled = enabled;
const parsed = sectionSchemas[key].safeParse(currentData);
if (!parsed.success) {
throw new BadRequestException({
statusCode: 400,
message: "اطلاعات بخش معتبر نیست",
issues: toIssues(parsed.error),
});
}
const updated = await this.prisma.siteSection.update({
where: { key },
data: { data: parsed.data, updatedById: user.id },
});
this.audit.log(user.id, "update", "section", key, `تغییر وضعیت نمایش به ${enabled ? "نمایش" : "عدم نمایش"}`);
this.revalidate.trigger();
return {
key,
enabled: parsed.data.enabled !== false,
updatedAt: updated.updatedAt.toISOString(),
};
}
};
SectionsService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
AuditService,
RevalidateService])
], SectionsService);
export { SectionsService };
let SectionsController = class SectionsController {
sections;
constructor(sections) {
this.sections = sections;
}
list() {
return this.sections.listAdmin();
}
get(key) {
return this.sections.get(key);
}
update(key, body, user) {
return this.sections.update(key, body, user);
}
setVisibility(key, body, user) {
const enabled = body && typeof body.enabled === "boolean" ? body.enabled : Boolean(body?.enabled);
return this.sections.setVisibility(key, enabled, user);
}
};
__decorate([
Get(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], SectionsController.prototype, "list", null);
__decorate([
Get(":key"),
__param(0, Param("key")),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], SectionsController.prototype, "get", null);
__decorate([
Put(":key"),
__param(0, Param("key")),
__param(1, Body()),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", void 0)
], SectionsController.prototype, "update", null);
__decorate([
Patch(":key/visibility"),
__param(0, Param("key")),
__param(1, Body()),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", void 0)
], SectionsController.prototype, "setVisibility", null);
SectionsController = __decorate([
AdminAuth(),
Controller("admin/sections"),
__metadata("design:paramtypes", [SectionsService])
], SectionsController);
export { SectionsController };
let SectionsModule = class SectionsModule {
};
SectionsModule = __decorate([
Module({
controllers: [SectionsController],
providers: [SectionsService],
exports: [SectionsService],
})
], SectionsModule);
export { SectionsModule };
//# sourceMappingURL=sections.module.js.map
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
import { normalizeIrMobile } from "@imen/shared";
import { hashPassword } from "../auth/auth.service.js";
import { createPrismaClient } from "../prisma/prisma.service.js";
const prisma = createPrismaClient();
async function main() {
const phone = normalizeIrMobile("09128081392");
const password = "09128081392";
const name = "مدیر سایت";
const role = "ADMIN";
const passwordHash = await hashPassword(password);
const existing = await prisma.adminUser.findUnique({ where: { phone } });
if (existing) {
const updated = await prisma.adminUser.update({
where: { id: existing.id },
data: {
passwordHash,
name,
role,
},
});
console.log(`User already existed, password updated for: ${updated.phone} (ID: ${updated.id})`);
}
else {
const user = await prisma.adminUser.create({
data: {
phone,
passwordHash,
name,
role,
},
});
console.log(`User successfully created: ${user.phone} (ID: ${user.id}, Role: ${user.role})`);
}
}
try {
await main();
}
catch (e) {
console.error("Error creating user:", e);
process.exitCode = 1;
}
finally {
await prisma.$disconnect();
}
//# sourceMappingURL=create-admin.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"create-admin.js","sourceRoot":"","sources":["../../src/seed/create-admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,MAAM,MAAM,GAAG,kBAAkB,EAAE,CAAC;AAEpC,KAAK,UAAU,IAAI;IACjB,MAAM,KAAK,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,aAAa,CAAC;IAC/B,MAAM,IAAI,GAAG,WAAW,CAAC;IACzB,MAAM,IAAI,GAAG,OAAO,CAAC;IAErB,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IACzE,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;YAC5C,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE;YAC1B,IAAI,EAAE;gBACJ,YAAY;gBACZ,IAAI;gBACJ,IAAI;aACL;SACF,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,+CAA+C,OAAO,CAAC,KAAK,SAAS,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;IAClG,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;YACzC,IAAI,EAAE;gBACJ,KAAK;gBACL,YAAY;gBACZ,IAAI;gBACJ,IAAI;aACL;SACF,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,EAAE,WAAW,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC;AAED,IAAI,CAAC;IACH,MAAM,IAAI,EAAE,CAAC;AACf,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACX,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC;IACzC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC;QAAS,CAAC;IACT,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC"}
+904
View File
@@ -0,0 +1,904 @@
export const CATEGORIES = [
{
slug: "workwear",
code: "IP-C01",
name: "پوشاک کار و البسه صنعتی",
en: "INDUSTRIAL WORKWEAR",
shortDesc: "لباس‌کار صنعتی با پارچه فلامنت کجراه اعلا، دوخت سه سوزنه و آستر عرق‌گیر؛ مناسب محیط‌های کارگاهی و صنعتی.",
fullDesc: "مجموعه کامل پوشاک کار صنعتی شامل کاپشن و شلوار مهندسی، لباس جوشکاری، پیش‌بند دوبنده و تی‌شرت کار. تمامی مدل‌ها با پارچه فلامنت کجراه اعلا و کتان مقاوم، دوخت صنعتی سه سوزنه پیوسته برای جلوگیری از شکافتگی درزها و آستر عرق‌گیر اختصاصی تولید می‌شوند. موجود در سایزهای M تا 4XL.",
icon: "Shirt",
products: [
{
code: "IP-W01",
name: "لباسکار مدل آلفا",
en: "ALPHA WORKWEAR",
desc: "کاپشن و شلوار مهندسی با جیب‌های پاکتی و زیپ‌دار، نوار مغزی تزئینی و طراحی ارگونومیک برای تیم‌های فنی و مهندسی.",
specs: ["فلامنت کجراه اعلا", "دوخت سه سوزنه", "آستر عرق‌گیر", "سایز M تا 4XL"],
variants: [
"طوسی/آبی",
"مشکی/قرمز",
"مشکی/زرد",
"کرم/سرمه‌ای",
],
},
{
code: "IP-W02",
name: "لباسکار مدل اورجینال",
en: "ORIGINAL WORKWEAR",
desc: "کاپشن و شلوار استاندارد با دوام بالا، مناسب کادر فنی کارخانجات و خطوط تولید با ترکیب‌های صنعتی دورنگ.",
specs: ["کتان مقاوم", "دوخت سه سوزنه", "آستر عرق‌گیر", "سایز M تا 4XL"],
variants: ["ترکیب‌های صنعتی دورنگ"],
},
{
code: "IP-W03",
name: "لباسکار مدل بوفالو",
en: "BUFFALO WORKWEAR",
desc: "طراحی اسپرت با برش‌های تقویتی روی شانه و بازو، زیپ مخفی محافظ‌دار؛ مناسب محیط‌های صنعتی فعال.",
specs: ["فلامنت کجراه اعلا", "زیپ مخفی محافظ‌دار", "برش تقویتی شانه", "سایز M تا 4XL"],
variants: [
"سرمه‌ای/زرد",
"طوسی/قرمز",
"طوسی/خردلی",
"آبی/مشکی",
],
},
{
code: "IP-W04",
name: "لباسکار ست ورک",
en: "SET WORK",
desc: "ست اداری/کارگاهی با انعطاف بالا جهت فعالیت‌های فنی و تعمیراتی؛ قابل استفاده در محیط‌های ستادی و عملیاتی.",
specs: ["کتان مقاوم", "دوخت سه سوزنه", "طراحی منعطف", "سایز M تا 4XL"],
variants: ["ترکیب‌های چندگانه کارگاهی"],
},
{
code: "IP-W05",
name: "لباسکار جوشکاری",
en: "WELDING WORKWEAR",
desc: "متریال سنگین کتان یا چرم جهت مقاومت در برابر پاشش جرقه‌های مذاب؛ طراحی‌شده برای جوشکاران صنعتی.",
specs: ["مقاوم به جرقه مذاب", "کتان سنگین / چرم", "دوخت صنعتی", "سایز M تا 4XL"],
},
{
code: "IP-W06",
name: "پیش‌بند دوبنده",
en: "BIB OVERALLS",
desc: "آزادی عمل کامل کتف و شانه با جیب‌های چندمنظوره ابزار بر روی پیش‌سینه؛ ایده‌آل برای کارهای تعمیراتی و مونتاژ.",
specs: ["آزادی حرکت شانه", "جیب ابزار پیش‌سینه", "دوخت سه سوزنه", "سایز M تا 4XL"],
variants: ["سرمه‌ای", "خاکستری"],
},
{
code: "IP-W07",
name: "تی‌شرت صنعتی",
en: "INDUSTRIAL T-SHIRT",
desc: "تی‌شرت یقه‌دار جودون / پنبه‌ای مناسب فصول گرم و زیرپوش کاپشن کارگاهی در سوله‌های سرپوشیده.",
specs: ["پنبه / جودون", "یقه‌دار", "مناسب فصول گرم", "سایز M تا 4XL"],
},
],
},
{
slug: "safety-footwear",
code: "IP-C02",
name: "پایپوش و کفش ایمنی",
en: "SAFETY FOOTWEAR",
shortDesc: "کفش‌های اداری و کفش‌های ایمنی کارگری؛ راحت و شکیل برای محیط اداری، مقاوم و ضد لغزش برای کارگاه.",
fullDesc: "پایپوش ایمنی شامل کفش‌های اداری برای محیط‌های اداری و سبک صنعتی و کفش‌های ایمنی کارگری برای کارگاه‌ها و پروژه‌های ساختمانی؛ با زیره مقاوم و مواد مقاوم در برابر سایش و مواد شیمیایی سبک.",
icon: "Footprints",
groups: [
{ slug: "office", name: "کفش اداری", desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم." },
{ slug: "worker", name: "کفش ایمنی کارگری", desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک." },
],
products: [
{
code: "IP-F01",
group: "office",
name: "کفش اداری مدل ۱۱۷۴",
en: "OFFICE SHOE 1174",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم نبوک", "بنددار", "زیره مقاوم"],
image: "/images/products/shoes/office/1.webp",
},
{
code: "IP-F02",
group: "office",
name: "کفش اداری بی‌بند مدل ۶",
en: "SLIP-ON OFFICE SHOE",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "بدون بند", "زیره مقاوم"],
image: "/images/products/shoes/office/2.webp",
},
{
code: "IP-F03",
group: "office",
name: "کفش اداری بنددار مشکی",
en: "LACE-UP OFFICE SHOE",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "بنددار", "زیره مقاوم"],
image: "/images/products/shoes/office/3.webp",
},
{
code: "IP-F04",
group: "office",
name: "کفش اداری بی‌بند AAEA075",
en: "SLIP-ON OFFICE SHOE AAEA075",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "بدون بند", "زیره مقاوم"],
image: "/images/products/shoes/office/4.webp",
},
{
code: "IP-F05",
group: "office",
name: "کفش اداری شبرو",
en: "SHEBRO OFFICE SHOE",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "بنددار", "زیره مقاوم"],
image: "/images/products/shoes/office/5.webp",
},
{
code: "IP-F06",
group: "office",
name: "کفش اداری بی‌بند کلاسیک",
en: "CLASSIC SLIP-ON OFFICE SHOE",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "بدون بند", "زیره مقاوم"],
image: "/images/products/shoes/office/6.webp",
},
{
code: "IP-F07",
group: "office",
name: "کفش اداری کالج",
en: "LOAFER OFFICE SHOE",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "کالج", "زیره مقاوم"],
image: "/images/products/shoes/office/7.webp",
},
{
code: "IP-F08",
group: "office",
name: "کفش اداری دیپلمات",
en: "DIPLOMAT OFFICE SHOE",
desc: "مناسب محیط‌های اداری و سبک صنعتی، راحت، سبک و شکیل با زیره مقاوم.",
specs: ["چرم طبیعی", "بنددار", "طراحی رسمی"],
image: "/images/products/shoes/office/8.webp",
},
{
code: "IP-F09",
group: "worker",
name: "کفش ایمنی کارگری مدل ۱",
en: "WORKER SAFETY SHOE",
desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک.",
specs: ["مقاوم به سایش", "زیره ضد لغزش"],
image: "/images/products/shoes/worker/1.png",
},
{
code: "IP-F10",
group: "worker",
name: "کفش ایمنی کارگری مدل ۲",
en: "WORKER SAFETY SHOE",
desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک.",
specs: ["مقاوم به سایش", "زیره ضد لغزش"],
image: "/images/products/shoes/worker/2.png",
},
{
code: "IP-F11",
group: "worker",
name: "کفش ایمنی کارگری مدل ۹۰",
en: "WORKER SAFETY SHOE 90",
desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک.",
specs: ["مقاوم به سایش", "زیره ضد لغزش"],
image: "/images/products/shoes/worker/3.png",
},
{
code: "IP-F12",
group: "worker",
name: "کفش ایمنی پاتن پدیده تبریز مدل رایکا S1017",
en: "PATEN PADIDEH RAYKA S1017",
desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک.",
specs: ["پاتن پدیده تبریز", "مقاوم به سایش", "زیره ضد لغزش"],
image: "/images/products/shoes/worker/4.png",
},
{
code: "IP-F13",
group: "worker",
name: "کفش ایمنی پیما مدل دنا",
en: "PAYMA DENA SAFETY SHOE",
desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک.",
specs: ["پیما", "مقاوم به سایش", "زیره ضد لغزش"],
image: "/images/products/shoes/worker/5.png",
},
{
code: "IP-F14",
group: "worker",
name: "کفش کار کارگری ساق‌دار",
en: "WORKER SAFETY BOOT",
desc: "مناسب کارگاه‌ها و پروژه‌های ساختمانی، مقاوم در برابر سایش و مواد شیمیایی سبک.",
specs: ["ساق‌دار", "مقاوم به سایش", "زیره ضد لغزش"],
image: "/images/products/shoes/worker/6.png",
},
],
},
{
slug: "protective-gloves",
code: "IP-C03",
name: "دستکش‌های حفاظتی",
en: "PROTECTIVE GLOVES",
shortDesc: "دستکش‌های ضد برش، چرم، ضد اسید و ضد حلال برای کارهای سنگین صنعتی، جوشکاری و تماس با مواد شیمیایی.",
fullDesc: "دستکش‌های حفاظت فردی شامل مدل‌های ضد برش، چرم، ضد اسید و ضد حلال؛ از برندهای استادکار، امریکن سیفتی، ماکسی درای و NM Safety. هر مدل متناسب با نوع خطر محیط کار انتخاب شده است.",
icon: "Hand",
groups: [
{ slug: "cut", name: "دستکش ضد برش", desc: "ایمن در برابر تیغه‌ها و فلزات تیز." },
{ slug: "leather", name: "دستکش چرم", desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری." },
{ slug: "acid", name: "دستکش ضد اسید", desc: "محافظت کامل در برابر اسیدها و بازها." },
{ slug: "solvent", name: "دستکش ضد حلال", desc: "مقاومت بالا در برابر تماس با مواد شیمیایی." },
],
products: [
{
code: "IP-G01",
group: "cut",
name: "دستکش ضد برش لاتکسی",
en: "LATEX CUT-RESISTANT GLOVE",
desc: "ایمن در برابر تیغه‌ها و فلزات تیز.",
specs: ["ضد برش", "پوشش لاتکس"],
image: "/images/products/gloves/cut/1.webp",
},
{
code: "IP-G02",
group: "cut",
name: "دستکش ضد برش مدل ۲",
en: "CUT-RESISTANT GLOVE",
desc: "ایمن در برابر تیغه‌ها و فلزات تیز.",
specs: ["ضد برش"],
image: "/images/products/gloves/cut/2.webp",
},
{
code: "IP-G03",
group: "cut",
name: "دستکش ضد برش مدل ۳",
en: "CUT-RESISTANT GLOVE",
desc: "ایمن در برابر تیغه‌ها و فلزات تیز.",
specs: ["ضد برش"],
image: "/images/products/gloves/cut/3.webp",
},
{
code: "IP-G04",
group: "cut",
name: "دستکش ضد برش استادکار ۱۲ جفتی",
en: "OSTADKAR CUT-RESISTANT GLOVE",
desc: "ایمن در برابر تیغه‌ها و فلزات تیز.",
specs: ["ضد برش", "استادکار", "بسته ۱۲ جفتی"],
image: "/images/products/gloves/cut/4.webp",
},
{
code: "IP-G05",
group: "cut",
name: "دستکش ضد برش تانگ وانگ",
en: "TANG WANG CUT-RESISTANT GLOVE",
desc: "ایمن در برابر تیغه‌ها و فلزات تیز.",
specs: ["ضد برش", "تانگ وانگ"],
image: "/images/products/gloves/cut/5.webp",
},
{
code: "IP-G06",
group: "leather",
name: "دستکش چرم مدل ۰۷",
en: "LEATHER GLOVE",
desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری.",
specs: ["چرم"],
image: "/images/products/gloves/leather/1.webp",
},
{
code: "IP-G07",
group: "leather",
name: "دستکش چرم BMW",
en: "BMW LEATHER GLOVE",
desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری.",
specs: ["چرم", "ساق بلند"],
image: "/images/products/gloves/leather/2.webp",
},
{
code: "IP-G08",
group: "leather",
name: "دستکش کف چرم دوبل ساق پارچه‌ای",
en: "DOUBLE-PALM LEATHER GLOVE, FABRIC CUFF",
desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری.",
specs: ["کف چرم دوبل", "ساق پارچه‌ای"],
image: "/images/products/gloves/leather/3.webp",
},
{
code: "IP-G09",
group: "leather",
name: "دستکش کف چرم دوبل ساق چرم",
en: "DOUBLE-PALM LEATHER GLOVE, LEATHER CUFF",
desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری.",
specs: ["کف چرم دوبل", "ساق چرم"],
image: "/images/products/gloves/leather/4.webp",
},
{
code: "IP-G10",
group: "leather",
name: "دستکش هوبارت امریکن سیفتی",
en: "AMERICAN SAFETY HOBART GLOVE",
desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری.",
specs: ["چرم", "جوشکاری", "EN 388 / EN 407"],
image: "/images/products/gloves/leather/5.webp",
},
{
code: "IP-G11",
group: "leather",
name: "دستکش هوبارت طرح فرانسه",
en: "FRENCH-STYLE HOBART GLOVE",
desc: "محافظ دست در برابر حرارت، جرقه و سایش در کارهای سنگین و جوشکاری.",
specs: ["چرم", "جوشکاری"],
image: "/images/products/gloves/leather/6.webp",
},
{
code: "IP-G12",
group: "acid",
name: "دستکش ضد اسید مدل ۱",
en: "ACID-RESISTANT GLOVE",
desc: "محافظت کامل در برابر اسیدها و بازها.",
specs: ["ضد اسید"],
image: "/images/products/gloves/acid/1.webp",
},
{
code: "IP-G13",
group: "acid",
name: "دستکش ضد اسید مدل ۲",
en: "ACID-RESISTANT GLOVE",
desc: "محافظت کامل در برابر اسیدها و بازها.",
specs: ["ضد اسید"],
image: "/images/products/gloves/acid/2.webp",
},
{
code: "IP-G14",
group: "acid",
name: "دستکش ضد اسید مدل ۳",
en: "ACID-RESISTANT GLOVE",
desc: "محافظت کامل در برابر اسیدها و بازها.",
specs: ["ضد اسید"],
image: "/images/products/gloves/acid/3.webp",
},
{
code: "IP-G15",
group: "acid",
name: "دستکش ضد اسید مقاوم شیمیایی",
en: "CHEMICAL-RESISTANT GLOVE",
desc: "محافظت کامل در برابر اسیدها و بازها.",
specs: ["ضد اسید", "مقاوم شیمیایی", "ساق بلند"],
image: "/images/products/gloves/acid/4.webp",
},
{
code: "IP-G16",
group: "solvent",
name: "دستکش ضد حلال مدل ۱۳",
en: "SOLVENT-RESISTANT GLOVE",
desc: "مقاومت بالا در برابر تماس با مواد شیمیایی.",
specs: ["ضد حلال"],
image: "/images/products/gloves/solvent/1.webp",
},
{
code: "IP-G17",
group: "solvent",
name: "دستکش ماکسی درای پلاس 56-530",
en: "MAXIDRY PLUS 56-530",
desc: "مقاومت بالا در برابر تماس با مواد شیمیایی.",
specs: ["ضد حلال", "MaxiDry Plus"],
image: "/images/products/gloves/solvent/2.webp",
},
{
code: "IP-G18",
group: "solvent",
name: "دستکش ضد حلال نیتریلی",
en: "NITRILE SOLVENT-RESISTANT GLOVE",
desc: "مقاومت بالا در برابر تماس با مواد شیمیایی.",
specs: ["ضد حلال", "نیتریل"],
image: "/images/products/gloves/solvent/3.webp",
},
{
code: "IP-G19",
group: "solvent",
name: "دستکش نیتریل آزمایشگاهی ضد حلال NM Safety",
en: "NM SAFETY NITRILE LAB GLOVE",
desc: "مقاومت بالا در برابر تماس با مواد شیمیایی.",
specs: ["ضد حلال", "نیتریل", "آزمایشگاهی"],
image: "/images/products/gloves/solvent/4.webp",
},
],
},
{
slug: "safety-helmets",
code: "IP-C04",
name: "کلاه ایمنی",
en: "SAFETY HELMETS",
shortDesc: "کلاه‌های ایمنی کارگری، مهندسی، کار در ارتفاع و کپ ایمنی؛ مقاوم در برابر ضربه و سقوط اجسام.",
fullDesc: "انواع کلاه ایمنی صنعتی شامل مدل‌های کارگری، مهندسی، کار در ارتفاع با بند چانه قابل تنظیم و کپ ایمنی سبک. با پوسته ضربه‌گیر مقاوم، رگلاژ داخلی قابل تنظیم و تنوع رنگی استاندارد HSE.",
icon: "HardHat",
groups: [
{ slug: "worker", name: "کلاه کارگری", desc: "محافظت از سر در محیط‌های صنعتی و کارگاه‌ها، مقاوم در برابر ضربه و سقوط اجسام." },
{ slug: "engineering", name: "کلاه مهندسی", desc: "مناسب محیط‌های اداری و صنعتی سبک، راحت و شکیل با تهویه مناسب." },
{ slug: "height", name: "کار در ارتفاع", desc: "دارای بند چانه قابل تنظیم برای ثبات بیشتر هنگام کار در ارتفاع." },
{ slug: "cap", name: "کپ ایمنی", desc: "سبک و راحت، مناسب استفاده روزمره در محیط‌های صنعتی و اداری." },
],
products: [
{
code: "IP-H01",
group: "worker",
name: "کلاه ایمنی کارگری آبی",
en: "WORKER SAFETY HELMET",
desc: "محافظت از سر در محیط‌های صنعتی و کارگاه‌ها، مقاوم در برابر ضربه و سقوط اجسام.",
specs: ["پوسته ضربه‌گیر", "رگلاژ قابل تنظیم"],
image: "/images/products/helmets/2.webp",
},
{
code: "IP-H02",
group: "engineering",
name: "کلاه ایمنی مهندسی پادکس",
en: "PADEX ENGINEERING HELMET",
desc: "مناسب محیط‌های اداری و صنعتی سبک، راحت و شکیل با تهویه مناسب.",
specs: ["پادکس", "تهویه مناسب", "رگلاژ قابل تنظیم"],
image: "/images/products/helmets/11.webp",
},
{
code: "IP-H03",
group: "engineering",
name: "کلاه ایمنی مهندسی پادکس PADX",
en: "PADEX PADX ENGINEERING HELMET",
desc: "مناسب محیط‌های اداری و صنعتی سبک، راحت و شکیل با تهویه مناسب.",
specs: ["پادکس", "تهویه مناسب", "رگلاژ قابل تنظیم"],
image: "/images/products/helmets/12.webp",
},
{
code: "IP-H04",
group: "engineering",
name: "کلاه ایمنی مهندس پادکس (نمای داخلی)",
en: "PADEX HELMET HARNESS",
desc: "مناسب محیط‌های اداری و صنعتی سبک، راحت و شکیل با تهویه مناسب.",
specs: ["پادکس", "یراق داخلی", "رگلاژ قابل تنظیم"],
image: "/images/products/helmets/10.webp",
},
{
code: "IP-H05",
group: "height",
name: "کلاه ایمنی کار در ارتفاع با عینک",
en: "HEIGHT HELMET WITH VISOR",
desc: "دارای بند چانه قابل تنظیم برای ثبات بیشتر هنگام کار در ارتفاع.",
specs: ["بند چانه", "عینک داخلی", "رگلاژ قابل تنظیم"],
image: "/images/products/helmets/1.webp",
},
{
code: "IP-H06",
group: "height",
name: "کلاه ایمنی کار در ارتفاع uvex",
en: "UVEX HEIGHT HELMET",
desc: "دارای بند چانه قابل تنظیم برای ثبات بیشتر هنگام کار در ارتفاع.",
specs: ["uvex", "بند چانه", "تهویه مناسب"],
image: "/images/products/helmets/3.webp",
},
{
code: "IP-H07",
group: "height",
name: "کلاه ایمنی کار در ارتفاع سبز",
en: "HEIGHT SAFETY HELMET",
desc: "دارای بند چانه قابل تنظیم برای ثبات بیشتر هنگام کار در ارتفاع.",
specs: ["بند چانه", "رگلاژ قابل تنظیم"],
image: "/images/products/helmets/4.webp",
},
{
code: "IP-H08",
group: "height",
name: "کلاه ایمنی صنعتی MK8",
en: "MK8 INDUSTRIAL HELMET",
desc: "دارای بند چانه قابل تنظیم برای ثبات بیشتر هنگام کار در ارتفاع.",
specs: ["MK8", "بند چانه", "تهویه مناسب"],
image: "/images/products/helmets/9.webp",
},
{
code: "IP-H09",
group: "cap",
name: "کپ ایمنی ضربه‌گیر",
en: "SAFETY BUMP CAP",
desc: "سبک و راحت، مناسب استفاده روزمره در محیط‌های صنعتی و اداری.",
specs: ["نقاب‌دار", "ضربه‌گیر داخلی", "سبک‌وزن"],
image: "/images/products/helmets/5.webp",
},
],
},
{
slug: "eye-protection",
code: "IP-C05",
name: "عینک ایمنی و صنعتی",
en: "EYE PROTECTION",
shortDesc: "عینک‌های شفاف و دودی و گاگل‌های محافظتی با ویژگی‌های ضد ضربه، ضد خش، ضد بخار و UV.",
fullDesc: "عینک‌های ایمنی صنعتی شامل عینک‌های شفاف، دودی و UV و گاگل‌های کش‌دار از برندهای uvex، توتاص، کاناسیف و تک پلاست. ضد ضربه، ضد خش، ضد بخار و قابل تنظیم برای انواع سایز صورت.",
icon: "Glasses",
groups: [
{ slug: "clear", name: "عینک شفاف", desc: "محافظت از چشم‌ها در برابر برخورد اجسام و ابزار؛ ضد خش با دید واضح." },
{ slug: "tinted", name: "عینک دودی و UV", desc: "محافظت در برابر اشعه مضر خورشید و نور شدید." },
{ slug: "goggle", name: "گاگل", desc: "ضد بخار و ضد ضربه؛ جلوگیری از ایجاد مه در محیط‌های گرم یا مرطوب." },
],
products: [
{
code: "IP-E01",
group: "clear",
name: "عینک ایمنی شفاف دسته قابل تنظیم",
en: "ADJUSTABLE CLEAR SAFETY GLASSES",
desc: "محافظت از چشم‌ها در برابر برخورد اجسام و ابزار؛ قابل تنظیم برای انواع سایز صورت.",
specs: ["ضد ضربه", "قابل تنظیم"],
image: "/images/products/eyewear/1.webp",
},
{
code: "IP-E02",
group: "clear",
name: "عینک ایمنی روطبی",
en: "OVER-SPEC SAFETY GLASSES",
desc: "محافظت از چشم‌ها در برابر برخورد اجسام و ابزار؛ قابل استفاده روی عینک طبی.",
specs: ["ضد ضربه", "روطبی"],
image: "/images/products/eyewear/4.webp",
},
{
code: "IP-E03",
group: "clear",
name: "عینک ایمنی uvex i-5",
en: "UVEX I-5 SAFETY GLASSES",
desc: "محافظت از چشم‌ها در برابر برخورد اجسام و ابزار؛ ضد خش با دید واضح.",
specs: ["uvex", "ضد ضربه", "ضد خش"],
image: "/images/products/eyewear/5.webp",
},
{
code: "IP-E04",
group: "clear",
name: "عینک شیشه سفید تک پلاست",
en: "TAK PLAST CLEAR GLASSES",
desc: "محافظت از چشم‌ها در برابر برخورد اجسام و ابزار؛ دارای محافظ کناری.",
specs: ["تک پلاست", "محافظ کناری"],
image: "/images/products/eyewear/11.webp",
},
{
code: "IP-E05",
group: "tinted",
name: "عینک ایمنی uvex دودی",
en: "UVEX TINTED SAFETY GLASSES",
desc: "محافظت در برابر اشعه مضر خورشید و نور شدید.",
specs: ["uvex", "شیشه UV", "ضد ضربه"],
image: "/images/products/eyewear/6.webp",
},
{
code: "IP-E06",
group: "tinted",
name: "عینک ایمنی توتاص AT113",
en: "TOTAS AT113",
desc: "محافظت در برابر اشعه مضر خورشید و نور شدید.",
specs: ["توتاص", "شیشه UV", "ضد ضربه"],
image: "/images/products/eyewear/7.webp",
},
{
code: "IP-E07",
group: "tinted",
name: "عینک ایمنی کاناسیف 20105",
en: "CANASAFE 20105",
desc: "محافظت در برابر اشعه مضر و نور شدید جوشکاری.",
specs: ["کاناسیف", "شیشه UV", "قابل تنظیم"],
image: "/images/products/eyewear/9.webp",
},
{
code: "IP-E08",
group: "tinted",
name: "عینک توتاص AT116",
en: "TOTAS AT116",
desc: "محافظت در برابر اشعه مضر خورشید و نور شدید.",
specs: ["توتاص", "شیشه UV", "روطبی"],
image: "/images/products/eyewear/10.webp",
},
{
code: "IP-E09",
group: "goggle",
name: "گاگل uvex ultravision",
en: "UVEX ULTRAVISION GOGGLE",
desc: "ضد بخار و ضد ضربه؛ جلوگیری از ایجاد مه در محیط‌های گرم یا مرطوب.",
specs: ["uvex", "ضد بخار", "کش‌دار"],
image: "/images/products/eyewear/2.webp",
},
{
code: "IP-E10",
group: "goggle",
name: "گاگل ایمنی کش‌دار",
en: "SAFETY GOGGLE",
desc: "ضد بخار و ضد ضربه؛ جلوگیری از ایجاد مه در محیط‌های گرم یا مرطوب.",
specs: ["ضد بخار", "کش‌دار"],
image: "/images/products/eyewear/3.webp",
},
{
code: "IP-E11",
group: "goggle",
name: "گاگل ایمنی توتاص ATBM",
en: "TOTAS ATBM GOGGLE",
desc: "ضد بخار و ضد ضربه؛ جلوگیری از ایجاد مه در محیط‌های گرم یا مرطوب.",
specs: ["توتاص", "ضد بخار", "کش‌دار"],
image: "/images/products/eyewear/8.webp",
},
],
},
{
slug: "hearing-protection",
code: "IP-C06",
name: "حفاظت شنوایی",
en: "HEARING PROTECTION",
shortDesc: "ایرماف روگوشی با کاهش ۲۸ دسی‌بل و ایرپلاگ‌های اسفنجی و سیلیکونی برای محیط‌های پرسروصدا.",
fullDesc: "تجهیزات حفاظت شنوایی شامل ایرماف (محافظ روگوشی) با کاهش تراز صدا تا ۲۸ دسی‌بل و ایرپلاگ (داخل‌گوشی) در مدل‌های اسفنجی فرم‌گیر و پله‌ای سیلیکونی. مناسب محیط‌های با آلودگی صوتی بالای ۸۵ دسی‌بل، کمپرسورخانه‌ها و خطوط تولید سنگین.",
icon: "Ear",
groups: [
{ slug: "earmuff", name: "ایرماف", desc: "کاهش صدا تا ۲۸ دسی‌بل، مناسب محیط‌های صنعتی و کارگاهی، قابل تنظیم و راحت برای استفاده طولانی." },
{ slug: "earplug", name: "ایرپلاگ", desc: "محافظت در برابر صداهای شدید و نویز محیط، سبک و راحت، مدل‌های یکبار مصرف و چندبار مصرف." },
],
products: [
{
code: "IP-HP01",
group: "earmuff",
name: "ایرماف هلبرگ",
en: "HELLBERG EARMUFF",
desc: "کاهش صدا تا ۲۸ دسی‌بل، مناسب محیط‌های صنعتی و کارگاهی، قابل تنظیم و راحت برای استفاده طولانی.",
specs: ["Hellberg", "هدبند قابل تنظیم", "بالشتک نرم"],
image: "/images/products/hearing/1.webp",
},
{
code: "IP-HP02",
group: "earmuff",
name: "ایرماف uvex K2",
en: "UVEX K2 EARMUFF",
desc: "کاهش صدا تا ۲۸ دسی‌بل، مناسب محیط‌های صنعتی و کارگاهی، قابل تنظیم و راحت برای استفاده طولانی.",
specs: ["uvex", "هدبند قابل تنظیم", "بالشتک نرم"],
image: "/images/products/hearing/2.webp",
},
{
code: "IP-HP03",
group: "earmuff",
name: "ایرماف بوفالو 110",
en: "BUFFALO 110 EARMUFF",
desc: "کاهش صدا تا ۲۸ دسی‌بل، مناسب محیط‌های صنعتی و کارگاهی، قابل تنظیم و راحت برای استفاده طولانی.",
specs: ["بوفالو", "هدبند قابل تنظیم", "بالشتک نرم"],
image: "/images/products/hearing/4.webp",
},
{
code: "IP-HP04",
group: "earmuff",
name: "گوشی صداگیر 3M",
en: "3M EARMUFF",
desc: "کاهش صدا تا ۲۸ دسی‌بل، مناسب محیط‌های صنعتی و کارگاهی، قابل تنظیم و راحت برای استفاده طولانی.",
specs: ["3M", "هدبند قابل تنظیم", "بالشتک نرم"],
image: "/images/products/hearing/8.webp",
},
{
code: "IP-HP05",
group: "earplug",
name: "ایرپلاگ کاستا مکس فیت MAXFIT",
en: "COSTA MAXFIT EARPLUG",
desc: "محافظت در برابر صداهای شدید و نویز محیط، سبک و راحت.",
specs: ["سیلیکونی پله‌ای", "بنددار", "چندبار مصرف"],
image: "/images/products/hearing/3.webp",
},
{
code: "IP-HP06",
group: "earplug",
name: "ایرپلاگ اسفنجی Bilsom 303L",
en: "BILSOM 303L FOAM EARPLUG",
desc: "محافظت در برابر صداهای شدید و نویز محیط، سبک و راحت.",
specs: ["اسفنجی", "بنددار / بدون بند", "یکبار مصرف"],
image: "/images/products/hearing/5.webp",
},
{
code: "IP-HP07",
group: "earplug",
name: "ایرپلاگ اسفنجی ماگما",
en: "MAGMA FOAM EARPLUG",
desc: "محافظت در برابر صداهای شدید و نویز محیط، سبک و راحت.",
specs: ["اسفنجی", "یکبار مصرف"],
image: "/images/products/hearing/6.webp",
},
{
code: "IP-HP08",
group: "earplug",
name: "ایرپلاگ مکس فیت با قاب",
en: "MAXFIT EARPLUG WITH CASE",
desc: "محافظت در برابر صداهای شدید و نویز محیط، سبک و راحت.",
specs: ["سیلیکونی پله‌ای", "بنددار", "قاب نگهداری"],
image: "/images/products/hearing/7.webp",
},
],
},
{
slug: "respiratory-protection",
code: "IP-C07",
name: "حفاظت تنفسی",
en: "RESPIRATORY PROTECTION",
shortDesc: "ماسک‌های نیم‌صورت، تمام‌صورت، شیمیایی، P2/P3 و فیلتردار برای محافظت در برابر گازها و ذرات.",
fullDesc: "تجهیزات حفاظت تنفسی شامل ماسک‌های نیم‌صورت، تمام‌صورت، شیمیایی با کارتریج تخصصی، ماسک‌های فیلتردار P2/P3 برای ذرات میکرونی، ماسک‌های با فیلتر تعویضی اقتصادی و ماسک‌های سه‌لایه بهداشتی. مناسب کار با حلال‌های فرار، غبارهای معدنی، رنگ‌پاشی و خطوط شیمیایی.",
icon: "ShieldPlus",
groups: [
{ slug: "half", name: "ماسک نیم‌صورت", desc: "پوشش‌دهی بینی و دهان با فیلتر قابل تعویض؛ مناسب محیط‌های صنعتی با آلودگی متوسط و بخارات شیمیایی." },
{ slug: "full", name: "ماسک تمام‌صورت", desc: "محافظت کامل از صورت و چشم‌ها، مناسب کار با مواد شیمیایی و محیط‌های پرخطر." },
{ slug: "particulate", name: "ماسک فیلتردار P2/P3", desc: "محافظت در برابر گرد و غبار و ذرات معلق در هوا." },
{ slug: "medical", name: "ماسک سه‌لایه", desc: "ماسک سبک و راحت، مناسب استفاده روزمره و محیط‌های با آلودگی کم." },
],
products: [
{
code: "IP-R01",
group: "half",
name: "ماسک نیم‌صورت با فیلتر تعویضی",
en: "HALF-FACE RESPIRATOR WITH FILTERS",
desc: "پوشش‌دهی بینی و دهان با فیلتر قابل تعویض برای استفاده طولانی‌مدت.",
specs: ["نیم‌صورت", "فیلتر تعویضی", "دو کارتریج"],
image: "/images/products/respiratory/5.webp",
},
{
code: "IP-R02",
group: "half",
name: "ماسک نیم‌صورت نورث 5500",
en: "NORTH 5500 HALF MASK",
desc: "پوشش‌دهی بینی و دهان، مناسب محیط‌های صنعتی با آلودگی متوسط.",
specs: ["North", "نیم‌صورت", "فیلتر تعویضی"],
image: "/images/products/respiratory/6.webp",
},
{
code: "IP-R03",
group: "half",
name: "ماسک شیمیایی نیم‌صورت 3M",
en: "3M HALF-FACE RESPIRATOR",
desc: "محافظت در برابر بخارات و گازهای شیمیایی با کارتریج و فیلتر قابل تعویض.",
specs: ["3M", "کارتریج شیمیایی", "فیلتر تعویضی"],
image: "/images/products/respiratory/7.webp",
},
{
code: "IP-R04",
group: "half",
name: "ماسک نیم‌صورت نورث 7700",
en: "NORTH 7700 HALF MASK",
desc: "پوشش‌دهی بینی و دهان با بدنه سیلیکونی، مناسب استفاده طولانی‌مدت.",
specs: ["North", "سیلیکونی", "فیلتر تعویضی"],
image: "/images/products/respiratory/11.webp",
},
{
code: "IP-R05",
group: "half",
name: "ماسک نیم‌صورت نورث 7700 (نمای جانبی)",
en: "NORTH 7700 HALF MASK",
desc: "پوشش‌دهی بینی و دهان با بدنه سیلیکونی، مناسب استفاده طولانی‌مدت.",
specs: ["North", "سیلیکونی", "فیلتر تعویضی"],
image: "/images/products/respiratory/10.webp",
},
{
code: "IP-R06",
group: "full",
name: "ماسک تمام‌صورت",
en: "FULL-FACE RESPIRATOR",
desc: "محافظت کامل از صورت و چشم‌ها، مناسب کار با مواد شیمیایی و محیط‌های پرخطر.",
specs: ["تمام‌صورت", "شیشه ضد ضربه", "فیلتر تعویضی"],
image: "/images/products/respiratory/4.webp",
},
{
code: "IP-R07",
group: "full",
name: "ماسک تنفسی دراگر",
en: "DRÄGER FULL-FACE RESPIRATOR",
desc: "محافظت کامل از صورت و چشم‌ها، مناسب کار با مواد شیمیایی و محیط‌های پرخطر.",
specs: ["Dräger", "تمام‌صورت", "فیلتر چندمنظوره"],
image: "/images/products/respiratory/8.webp",
},
{
code: "IP-R08",
group: "particulate",
name: "ماسک فیلتردار تاشو",
en: "FOLDABLE PARTICULATE MASK",
desc: "محافظت در برابر گرد و غبار و ذرات معلق در هوا.",
specs: ["P2", "تاشو", "یکبار مصرف"],
image: "/images/products/respiratory/1.webp",
},
{
code: "IP-R09",
group: "particulate",
name: "ماسک فیلتردار سوپاپ‌دار",
en: "VALVED PARTICULATE MASK",
desc: "محافظت در برابر گرد و غبار و ذرات معلق در هوا.",
specs: ["P2 / P3", "سوپاپ بازدمی", "یکبار مصرف"],
image: "/images/products/respiratory/2.webp",
},
{
code: "IP-R10",
group: "particulate",
name: "ماسک فیلتردار دراگر سوپاپ‌دار",
en: "DRÄGER VALVED PARTICULATE MASK",
desc: "محافظت در برابر گرد و غبار و ذرات معلق در هوا.",
specs: ["Dräger", "سوپاپ بازدمی", "P2 / P3"],
image: "/images/products/respiratory/3.webp",
},
{
code: "IP-R11",
group: "medical",
name: "ماسک سه‌لایه پزشکی",
en: "THREE-LAYER MEDICAL MASK",
desc: "ماسک سبک و راحت، مناسب استفاده روزمره و محیط‌های با آلودگی کم.",
specs: ["سه‌لایه", "یکبار مصرف", "بسته ۵۰ عددی"],
image: "/images/products/respiratory/9.webp",
},
],
},
{
slug: "traffic-equipment",
code: "IP-C08",
name: "تجهیزات ترافیکی و ایمنی محیطی",
en: "TRAFFIC & SITE SAFETY",
shortDesc: "مخروط، نیوجرسی، استوانه، سرعت‌گیر، آینه محدب و سایر تجهیزات ایمن‌سازی محوطه صنعتی.",
fullDesc: "تجهیزات ترافیکی و ایمنی محیطی شامل مخروط ترافیکی، نیوجرسی پلاستیکی، استوانه و بولارد، چراغ و تابلوهای هشداردهنده، باتوم ترافیکی، نوارهای خطر، سرعت‌گیر لاستیکی، آینه‌های محدب، محافظ گوشه ستون و گل‌میخ خورشیدی. مناسب ایمن‌سازی محوطه کارخانه، انبارها، رمپ‌ها و پارکینگ‌ها.",
icon: "TriangleAlert",
products: [
{
code: "IP-T01",
name: "مخروط ترافیکی",
en: "TRAFFIC CONE",
desc: "PVC انعطاف‌پذیر با شبرنگ‌های استاندارد روز و شب؛ مقاوم در برابر واژگونی و ضربه خودرو.",
specs: ["PVC انعطاف‌پذیر", "شبرنگ استاندارد", "مقاوم به واژگونی", "قابل رؤیت شبانه"],
},
{
code: "IP-T02",
name: "نیوجرسی پلاستیکی",
en: "NEW JERSEY BARRIER",
desc: "بدنه پلی‌اتیلنی مدولار قابل اتصال با قابلیت پرشوندگی با آب یا ماسه جهت تثبیت و ایجاد حائل.",
specs: ["پلی‌اتیلن مدولار", "قابل اتصال", "پرشدنی با آب/ماسه", "مقاوم UV"],
},
{
code: "IP-T03",
name: "استوانه ترافیکی",
en: "TRAFFIC BOLLARD",
desc: "پلی‌اورتان ارتجاعی انعطاف‌پذیر پس از ضربه؛ مجهز به حلقه‌های شبرنگ هشدار برای تفکیک مسیر.",
specs: ["پلی‌اورتان ارتجاعی", "بازگشت پس از ضربه", "شبرنگ هشدار", "نصب آسان"],
},
{
code: "IP-T04",
name: "چراغ و تابلوی هشدار",
en: "WARNING LIGHT & SIGN",
desc: "چراغ‌های چشمک‌زن سولار، تابلوی دستی ایست/حرکت و تابلوهای علائم هشداردهنده ترافیکی.",
specs: ["سولار / باتری", "LED چشمک‌زن", "تابلوی Stop/Go", "علائم استاندارد"],
},
{
code: "IP-T05",
name: "باتوم ترافیکی",
en: "TRAFFIC BATON",
desc: "باتوم LED دار دستی جهت هدایت ترافیک در شب و محیط‌های کم‌نور؛ سبک و ارگونومیک.",
specs: ["LED داخلی", "قابل رؤیت شبانه", "سبک‌وزن", "ارگونومیک"],
},
{
code: "IP-T06",
name: "نوار خطر",
en: "CAUTION TAPE",
desc: "نوار شطرنجی و خط‌کشی زرد/قرمز هشدار محوطه گودبرداری و مناطق خطرناک با رول‌های بازتابنده نور.",
specs: ["زرد / قرمز هشدار", "بازتابنده نور", "رول بلند", "مقاوم به پارگی"],
},
{
code: "IP-T07",
name: "سرعت‌گیر لاستیکی",
en: "RUBBER SPEED BUMP",
desc: "قطعات لاستیکی سنگین مدولار با مقاومت وزنی بالا و المان‌های بازتابش شبانه؛ نصب آسان.",
specs: ["لاستیکی مدولار", "مقاومت وزنی بالا", "بازتاب شبانه", "نصب پیچی"],
},
{
code: "IP-T08",
name: "آینه محدب ترافیکی",
en: "CONVEX TRAFFIC MIRROR",
desc: "آینه نشکن فریم‌دار و کلاه‌دار جهت پوشش نقاط کور پیچ‌ها، تقاطع‌ها و درب انبار.",
specs: ["نشکن", "فریم‌دار و کلاه‌دار", "پوشش نقاط کور", "مقاوم به آب و باد"],
},
{
code: "IP-T09",
name: "محافظ گوشه ستون",
en: "CORNER GUARD",
desc: "ضربه‌گیر لاستیکی شبرنگ‌دار جهت نصب روی ستون‌ها برای ممانعت از آسیب لیفتراک و خودروهای سنگین.",
specs: ["لاستیکی ضربه‌گیر", "شبرنگ‌دار", "نصب دیواری", "محافظ ستون"],
},
{
code: "IP-T10",
name: "گل‌میخ ترافیکی خورشیدی",
en: "SOLAR ROAD STUD",
desc: "بدنه آلومینیومی مستحکم مجهز به پنل خورشیدی و LED جهت تعیین مسیر و خط‌کشی شبانه.",
specs: ["آلومینیوم مستحکم", "پنل خورشیدی", "LED", "مقاوم به عبور خودرو"],
},
],
},
];
//# sourceMappingURL=categories.js.map
File diff suppressed because one or more lines are too long
+268
View File
@@ -0,0 +1,268 @@
export const SECTIONS = {
settings: {
name: "ایمن پیشگامان",
nameEn: "Imen Pishgaman Safety",
title: "ایمن پیشگامان | تجهیزات ایمنی و حفاظت فردی صنعتی",
description: "ایمن پیشگامان — تأمین‌کننده تخصصی تجهیزات حفاظت فردی (PPE)، بازرسی، آموزش و راهکارهای ایمنی صنعتی برای سازمان‌هایی که جان انسان‌ها را جدی می‌گیرند.",
phone: "+98-21-91008800",
phoneDisplay: "۰۲۱ - ۹۱ ۰۰ ۸۸ ۰۰",
email: "info@imenpishgaman.ir",
address: {
street: "خیابان کارگر شمالی، خیابان فر، کوچه دوم، پلاک ۱۲",
locality: "تهران",
country: "IR",
},
workingHours: "شنبه تا پنجشنبه — ۸:۰۰ تا ۱۷:۰۰",
openingHours: {
days: ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"],
opens: "08:00",
closes: "17:00",
},
foundingDate: "2007",
socials: [
{ type: "website", url: "/" },
{ type: "email", url: "mailto:info@imenpishgaman.ir" },
{ type: "phone", url: "tel:+982191008800" },
],
ogImage: {
url: "asset:hero.jpg",
alt: "کارگر صنعتی با تجهیزات ایمنی کامل",
width: 1792,
height: 1008,
},
},
hero: {
enabled: true,
badge: "تأمین‌کننده رسمی تجهیزات حفاظت فردی (PPE)",
title: "ایمنی یعنی",
titleHighlight: "بازگشت به خانه",
intro: "ایمن پیشگامان با بیش از ۱۸ سال تجربه، تأمین‌کننده تخصصی تجهیزات ایمنی و حفاظت فردی برای سازمان‌هایی است که جان انسان‌ها را جدی می‌گیرند؛ از کلاه ایمنی تا هارنس نجات، از مشاوره تا بازرسی و آموزش.",
primaryCta: { label: "درخواست مشاوره رایگان", href: "#contact" },
secondaryCta: { label: "مشاهده محصولات", href: "#products" },
chips: [
{ icon: "ShieldCheck", label: "ISO 9001:2015" },
{ icon: "BadgeCheck", label: "CE CERTIFIED" },
{ icon: "Award", label: "EN STANDARDS" },
],
miniStats: [
{ value: "۱۸+", label: "سال تجربه در صنعت ایمنی" },
{ value: "۸۵۰+", label: "سازمان تحت پوشش" },
{ value: "۹۸٪", label: "رضایت مشتریان" },
],
image: { url: "asset:hero.jpg", alt: "کارگر صنعتی با تجهیزات ایمنی کامل" },
lastInspection: "۱۴۰۴/۰۸/۱۲",
},
tape: {
enabled: true,
items: [
"ISO 9001:2015",
"CE CERTIFIED",
"گارانتی اصالت کالا",
"تحویل سراسری کشوری",
"مشاوره و بازرسی تخصصی رایگان",
"EN STANDARDS",
"پشتیبانی ۲۴/۷ قراردادهای سازمانی",
"تست نمونه قبل از خرید",
],
},
stats: {
enabled: true,
items: [
{ end: 18, suffix: "+", label: "سال تجربه در صنعت ایمنی", note: "SINCE 2007" },
{ end: 2400, suffix: "+", label: "کد کالای ایمنی", note: "PPE CATALOG" },
{ end: 850, suffix: "+", label: "مشتری سازمانی فعال", note: "B2B CLIENTS" },
{ end: 98, suffix: "٪", label: "رضایت مشتریان", note: "SATISFACTION" },
],
},
productsIntro: {
enabled: true,
head: {
tag: "۰۱ — محصولات",
title: "خط اول دفاع،\nتجهیزات ماست",
highlight: "",
desc: "هر محصول پیش از عرضه، زیر ذره‌بین کنترل کیفیت و تطبیق با استانداردهای جهانی قرار می‌گیرد.",
},
ctaLabel: "دریافت کاتالوگ کامل",
trust: [
{
icon: "BadgeCheck",
title: "گارانتی اصالت کالا",
desc: "تمام محصولات با فاکتور رسمی و کد رهگیری اصالت",
},
{
icon: "PackageCheck",
title: "تست نمونه قبل از خرید",
desc: "نمونه‌های نمایشی برای ارزیابی تیم HSE شما",
},
{
icon: "ShieldCheck",
title: "امکان قرارداد سالانه",
desc: "قیمت ثابت و تأمین مستمر برای سازمان‌های بزرگ",
},
],
},
about: {
enabled: true,
head: {
tag: "۰۲ — درباره ما",
title: "ایمنی برای ما شعار نیست؛",
highlight: "قرارداد است.",
desc: "",
},
body: "از سال ۱۳۸۶، زمانی که اولین کلاه ایمنی را به یک کارگاه کوچک در جنوب تهران فروختیم، باور ما همین یک جمله بود: پشت هر تجهیز ایمنی، یک خانواده هست که منتظر بازگشت عزیزش است. امروز با بیش از ۸۵۰ سازمان، از پالایشگاه‌ها تا معادن و سایت‌های ساختمانی، در کنار کسانی هستیم که ایمنی را یک خط قرمز می‌دانند.",
image: { url: "asset:about.jpg", alt: "انبار تجهیزات ایمنی ایمن پیشگامان" },
badge: { value: "۱۸+", label: "سال حفاظت از\nجان نیروی کار" },
timeline: [
{ year: "۱۳۸۶", text: "تأسیس ایمن پیشگامان در تهران با تمرکز بر تجهیزات حفاظت فردی" },
{ year: "۱۳۹۲", text: "توسعه شبکه توزیع به تمام ۳۱ استان کشور" },
{ year: "۱۳۹۸", text: "راه‌اندازی آزمایشگاه کنترل کیفیت و تست تجهیزات" },
{ year: "۱۴۰۲", text: "افتتاح مرکز تخصصی آموزش و بازرسی HSE" },
],
features: [
{ icon: "Globe", label: "نماینده رسمی ۴۰+ برند جهانی" },
{ icon: "Users", label: "تیم مهندسی و بازرسی HSE" },
{ icon: "Headset", label: "پشتیبانی ۲۴/۷ قراردادها" },
],
},
services: {
enabled: true,
head: {
tag: "۰۳ — خدمات",
title: "فراتر از تأمین؛\nهمراهی کامل ایمنی",
highlight: "",
desc: "ما در ایمن پیشگامان، تجهیزات را تحویل می‌دهیم و کنار شما می‌مانیم؛ از نخستین ارزیابی ریسک تا آخرین تست دوره‌ای.",
},
items: [
{
icon: "ClipboardCheck",
title: "بازرسی و ارزیابی ریسک",
desc: "تیم مهندسی ما با بازدید از سایت شما، خطرات را شناسایی، اولویت‌بندی و برای هر کدام راهکار اجرایی مشخص با برآورد هزینه ارائه می‌دهد.",
items: [
"ارزیابی HAZOP و HAZID",
"شناسایی و اولویت‌بندی خطرات",
"گزارش مدیریتی با راهکار اجرایی",
],
},
{
icon: "GraduationCap",
title: "آموزش تخصصی ایمنی",
desc: "از دوره‌های عمومی تا تربیت مربی؛ آموزش‌های عملی و میدانی که ایمنی را از دفتر کلان‌ها به رفتار روزمره پرسنل تبدیل می‌کند.",
items: [
"دوره‌های عمومی و تخصصی HSE",
"تربیت مربی ایمنی سایت",
"مانور و شبیه‌سازی شرایط اضطراری",
],
},
{
icon: "Gauge",
title: "تست و کالیبراسیون تجهیزات",
desc: "تجهیزات ایمنی فقط وقتی قابل اعتمادند که تست شده باشند. تست دوره‌ای، سرویس و صدور گواهی معتبر را به تیم تخصصی ما بسپارید.",
items: [
"تست هارنس و لاین نجات",
"کالیبراسیون آشکارسازهای گاز",
"سرویس دوره‌ای و صدور گواهی",
],
},
{
icon: "Truck",
title: "تأمین و لجستیک سازمانی",
desc: "با قرارداد سالانه، قیمت‌گذاری شفاف و انبار محلی در سایت شما، نگرانی کمبود تجهیزات را از فهرست دغدغه‌هایتان حذف کنید.",
items: [
"قرارداد سالانه با قیمت ثابت",
"انبار محلی در سایت شما",
"تحویل سراسری در ۴۸ ساعت",
],
},
],
},
standards: {
enabled: true,
head: {
tag: "۰۴ — استانداردها",
title: "مهرتأیید جهانی\nروی هر محصول",
highlight: "",
desc: "ما فقط تجهیزاتی را عرضه می‌کنیم که تأییدیه معتبر بین‌المللی و تست آزمایشگاهی داشته باشند؛ بدون استثنا.",
},
note: "شماره سریال تمام محصولات در پایگاه داده استانداردهای اروپا قابل استعلام است و گواهی تست هر بچ تحویل مشتری می‌شود.",
ctaLabel: "درخواست گواهی تست محصولات",
items: [
{ code: "EN 397", label: "کلاه ایمنی صنعتی" },
{ code: "EN 388", label: "دستکش‌های مکانیکی" },
{ code: "EN 166", label: "محافظ چشم و صورت" },
{ code: "EN 361", label: "هارنس ضدسقوط" },
{ code: "ISO 20345", label: "کفش ایمنی" },
{ code: "ISO 9001", label: "سیستم مدیریت کیفیت" },
],
},
industries: {
enabled: true,
head: {
tag: "۰۵ — صنایع",
title: "حضور در سخت‌ترین\nمیدان‌های کاری",
highlight: "",
desc: "جایی که دمای محیط از ۵۰ درجه گذر می‌کند یا ارتفاع کار به ده‌ها متر می‌رسد، تجهیزات ما باید بی‌نقص عمل کنند — و عمل می‌کنند.",
},
items: [
{ name: "نفت و گاز", nameEn: "OIL & GAS", count: "۱۴۰+", image: { url: "asset:industries/oil-gas.jpg", alt: "نفت و گاز" } },
{ name: "ساختمان و عمران", nameEn: "CONSTRUCTION", count: "۲۱۰+", image: { url: "asset:industries/construction.jpg", alt: "ساختمان و عمران" } },
{ name: "پتروشیمی", nameEn: "PETROCHEMICAL", count: "۹۰+", image: { url: "asset:industries/petrochemical.jpg", alt: "پتروشیمی" } },
{ name: "صنایع معدنی", nameEn: "MINING", count: "۷۵+", image: { url: "asset:industries/mining.jpg", alt: "صنایع معدنی" } },
{ name: "برق و انرژی", nameEn: "POWER & ENERGY", count: "۶۰+", image: { url: "asset:industries/power-energy.jpg", alt: "برق و انرژی" } },
{ name: "حمل‌ونقل و لجستیک", nameEn: "TRANSPORT", count: "۴۵+", image: { url: "asset:industries/transport.jpg", alt: "حمل‌ونقل و لجستیک" } },
],
},
testimonials: {
enabled: true,
head: { tag: "۰۶ — اعتماد", title: "آنچه مشتریان ما می‌گویند", highlight: "", desc: "" },
items: [
{
text: "ایمن پیشگامان فقط تأمین‌کننده نیست؛ شریک ایمنی ماست. بازرسی‌های دوره‌ای و آموزش‌های میدانی‌شان نرخ حوادث سایت را به کمترین رقم تاریخ مجموعه رساند.",
name: "مهندس رضا کاظمی",
role: "مدیر HSE — مجتمع پتروشیمی خلیج فارس",
},
{
text: "کیفیت تجهیزات و سرعت پاسخ‌گویی در سطحی بود که قرارداد سالانه را بدون هیچ تردیدی تمدید کردیم؛ از خط تولید تا دفتر مرکزی.",
name: "سارا محمدی",
role: "سرپرست تدارکات — هلدینگ ساختمانی آرکان",
},
{
text: "۱۴۰ کیلومتر دورتر از سایت، در عرض ۲۴ ساعت تجهیزات حیاتی را تحویل گرفتیم. به این می‌گویند شریک واقعی کاری.",
name: "امیرحسین راد",
role: "مدیر پروژه — صنایع معدنی کاوه",
},
],
},
contact: {
enabled: true,
head: {
tag: "۰۷ — تماس",
title: "بیایید ایمنی را\nبا هم جدی بگیریم",
highlight: "",
desc: "فرم را پر کنید تا کارشناسان ما برای هماهنگی جلسه بازدید رایگان سایت شما تماس بگیرند؛ یا مستقیم با ما در ارتباط باشید.",
},
emergencyTitle: "پاسخ‌گویی اضطراری ۲۴/۷",
emergencyText: "برای مشتریان دارای قرارداد سازمانی، خط ویژه حوادث و تأمین فوری تجهیزات در تمام ساعات شبانه‌روز فعال است.",
formTag: "// فرم درخواست مشاوره",
formTitle: "در کمتر از ۲۴ ساعت\nبا شما تماس می‌گیریم",
successTitle: "درخواست شما ثبت شد",
successText: "کارشناسان ما تا ۲۴ ساعت آینده برای هماهنگی جلسه بازدید با شما تماس می‌گیرند.",
},
footer: {
enabled: true,
about: "تأمین‌کننده تخصصی تجهیزات حفاظت فردی و راهکارهای ایمنی صنعتی؛ همراه سازمان‌ها از سال ۱۳۸۶ در مسیر صفر شدن حوادث کاری.",
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
certLine: "ISO 9001:2015 — CE CERTIFIED PPE SUPPLIER",
emergencyNote: "پشتیبانی اضطراری: ۲۴/۷",
},
};
export const CATEGORY_COVERS = {
workwear: "products/vest.jpg",
"safety-footwear": "products/shoes.jpg",
"protective-gloves": "products/gloves.jpg",
"safety-helmets": "products/helmet.jpg",
"eye-protection": "products/goggles.jpg",
"hearing-protection": "products/hearing.jpg",
"respiratory-protection": "products/respiratory.jpg",
"traffic-equipment": "products/traffic.jpg",
};
//# sourceMappingURL=sections.js.map
File diff suppressed because one or more lines are too long
+270
View File
@@ -0,0 +1,270 @@
import { env } from "../config/env.js";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { normalizeIrMobile, SECTION_KEYS, sectionSchemas } from "@imen/shared";
import { hashPassword } from "../auth/auth.service.js";
import { S3Storage, toWebp } from "../media/s3-storage.js";
import { createPrismaClient } from "../prisma/prisma.service.js";
import { CATEGORIES } from "./data/categories.js";
import { CATEGORY_COVERS, SECTIONS } from "./data/sections.js";
const ASSETS_DIR = process.env.SEED_ASSETS_DIR ??
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../seed-assets");
const OVERWRITE_SECTIONS = ["1", "true"].includes(process.env.SEED_OVERWRITE_SECTIONS ?? "");
const prisma = createPrismaClient();
const storage = new S3Storage();
const uploaded = new Map();
async function asset(rel, alt = "") {
const cached = uploaded.get(rel);
if (cached)
return cached;
const key = `seed/${rel.replace(/\.[^.]+$/, "")}.webp`;
let media = await prisma.media.findUnique({ where: { key } });
if (!media) {
const image = await toWebp(await readFile(path.join(ASSETS_DIR, rel)));
try {
await storage.put(key, image.data, "image/webp");
}
catch {
try {
const targetPath = path.resolve(ASSETS_DIR, "../../.s3data/imen-media", key);
await mkdir(path.dirname(targetPath), { recursive: true });
await writeFile(targetPath, image.data);
}
catch { }
}
media = await prisma.media.create({
data: {
key,
mime: "image/webp",
size: image.size,
width: image.width,
height: image.height,
alt,
originalName: path.basename(rel),
},
});
}
const result = { id: media.id, url: storage.publicUrl(key) };
uploaded.set(rel, result);
return result;
}
async function resolveAssets(value) {
if (Array.isArray(value))
return Promise.all(value.map(resolveAssets));
if (value && typeof value === "object") {
const entries = await Promise.all(Object.entries(value).map(async ([k, v]) => {
if (k === "url" && typeof v === "string" && v.startsWith("asset:")) {
return [k, (await asset(v.slice("asset:".length))).url];
}
return [k, await resolveAssets(v)];
}));
return Object.fromEntries(entries);
}
return value;
}
async function seedSections() {
for (const key of SECTION_KEYS) {
const exists = await prisma.siteSection.findUnique({ where: { key } });
if (exists && !OVERWRITE_SECTIONS)
continue;
const data = sectionSchemas[key].parse(await resolveAssets(SECTIONS[key]));
await prisma.siteSection.upsert({ where: { key }, create: { key, data }, update: { data } });
console.log(` section ${key}`);
}
}
async function seedCatalog() {
for (const [ci, c] of CATEGORIES.entries()) {
if (await prisma.category.findUnique({ where: { slug: c.slug } }))
continue;
const coverPath = CATEGORY_COVERS[c.slug];
const cover = coverPath ? await asset(coverPath, c.name) : null;
const category = await prisma.category.create({
data: {
slug: c.slug,
code: c.code,
name: c.name,
nameEn: c.en,
shortDesc: c.shortDesc,
fullDesc: c.fullDesc,
icon: c.icon,
coverImageId: cover?.id ?? null,
sortOrder: ci,
groups: {
create: (c.groups ?? []).map((g, i) => ({
slug: g.slug,
name: g.name,
desc: g.desc,
sortOrder: i,
})),
},
},
include: { groups: true },
});
const groupIds = new Map(category.groups.map((g) => [g.slug, g.id]));
for (const [pi, p] of c.products.entries()) {
const image = p.image ? await asset(p.image.replace(/^\/images\//, ""), p.name) : null;
await prisma.product.create({
data: {
categoryId: category.id,
groupId: p.group ? (groupIds.get(p.group) ?? null) : null,
code: p.code,
slug: p.code.toLowerCase(),
name: p.name,
nameEn: p.en,
desc: p.desc,
specs: [...p.specs],
variants: [...(p.variants ?? [])],
imageId: image?.id ?? null,
sortOrder: pi,
},
});
}
console.log(` category ${c.slug} (${c.products.length} products)`);
}
}
async function seedAdmin() {
if (!env.ADMIN_PHONE || !env.ADMIN_PASSWORD) {
console.log(" ADMIN_PHONE / ADMIN_PASSWORD not set — skipping admin user");
return;
}
const phone = normalizeIrMobile(env.ADMIN_PHONE);
if (!/^09\d{9}$/.test(phone)) {
console.log(` ADMIN_PHONE invalid (${env.ADMIN_PHONE}) — skipping admin user`);
return;
}
const existing = await prisma.adminUser.findUnique({ where: { phone } });
if (existing)
return;
const legacy = await prisma.adminUser.findFirst({
where: { role: "ADMIN", phone: { contains: "@" } },
orderBy: { id: "asc" },
});
if (legacy) {
await prisma.adminUser.update({
where: { id: legacy.id },
data: { phone, name: env.ADMIN_NAME, passwordHash: await hashPassword(env.ADMIN_PASSWORD) },
});
console.log(` admin user migrated to ${phone}`);
return;
}
await prisma.adminUser.create({
data: {
phone,
name: env.ADMIN_NAME,
role: "ADMIN",
passwordHash: await hashPassword(env.ADMIN_PASSWORD),
},
});
console.log(` admin user ${phone}`);
}
async function seedMessages() {
const count = await prisma.contactMessage.count();
if (count > 0)
return;
const samples = [
{
name: "مهندس رضا کاظمی",
org: "مجتمع پتروشیمی خلیج فارس",
phone: "09121112233",
message: "درخواست پیش‌فاکتور ۵۰۰ عدد هارنس ایمنی دو قلاب و کلاه ایمنی استاندارد EN 397 برای فاز ۲ پتروشیمی.",
status: "NEW",
createdAt: new Date(Date.now() - 1000 * 60 * 35),
},
{
name: "سارا محمدی",
org: "هلدینگ ساختمانی آرکان",
phone: "09122223344",
message: "استعلام قیمت و هماهنگی جلسه بازدید جهت عقد قرارداد سالانه تأمین تجهیزات حفاظت فردی ۱۲ پروژه فعال.",
status: "NEW",
createdAt: new Date(Date.now() - 1000 * 60 * 120),
},
{
name: "امیرحسین راد",
org: "صنایع معدنی کاوه",
phone: "09123334455",
message: "درخواست تأمین فوری ۲۰۰ جفت دستکش ایمنی ضدبرش و کفش ایمنی ضداسید و پنجه کامپوزیت برای سایت یزد.",
status: "READ",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 8),
},
{
name: "مهندس علیرضا بهرامی",
org: "شرکت نفت و گاز پارس",
phone: "09124445566",
message: "تقاضای ارسال کاتالوگ و نمونه عینک ایمنی استاندارد EN 166 و محافظ گوش ایرماف جهت آزمایش واحد HSE.",
status: "NEW",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
},
{
name: "مریم رضایی",
org: "داروسازی رازک",
phone: "09125556677",
message: "درخواست مشاوره تخصصی در رابطه با ماسک‌های فیلتردار ذرات شیمیایی و لباس‌های یکبار مصرف آزمایشگاهی.",
status: "READ",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 36),
},
{
name: "مهندس مهدی یوسفی",
org: "فولاد مبارکه اصفهان",
phone: "09126667788",
message: "درخواست برگزاری جلسه فنی برای تمدید قرارداد تأمین لباس کار نسوز، شیلد محافظ صورت و پابند نسوز.",
status: "ARCHIVED",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 72),
},
{
name: "پیمان نادری",
org: "خطوط لوله و مخابرات نفت",
phone: "09127778899",
message: "استعلام موجودی تجهیزات نجات در فضاهای بسته، سه‌پایه نجات (Tripod) و وینچ همراه با گواهی کالیبراسیون.",
status: "NEW",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 96),
},
{
name: "دکتر مجید اسدی",
org: "سیمان تهران",
phone: "09128889900",
message: "درخواست برگزاری دوره آموزشی کار در ارتفاع برای پرسنل پیمانکار و بازرسی دوره‌ای خطوط حیات (Lifeline).",
status: "READ",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 120),
},
];
for (const msg of samples) {
await prisma.contactMessage.create({ data: msg });
}
console.log(` seeded ${samples.length} sample contact messages`);
}
async function main() {
if (env.S3_ENSURE_BUCKET) {
try {
await storage.ensureBucket();
}
catch {
console.log(" ensureBucket skipped");
}
}
console.log("seeding sections…");
await seedSections();
console.log("seeding catalog…");
await seedCatalog();
console.log("seeding admin…");
await seedAdmin();
console.log("seeding messages…");
await seedMessages();
const [categories, products, media] = await Promise.all([
prisma.category.count(),
prisma.product.count(),
prisma.media.count(),
]);
console.log(`done: ${categories} categories, ${products} products, ${media} media files`);
}
try {
await main();
}
catch (e) {
console.error(e);
process.exitCode = 1;
}
finally {
await prisma.$disconnect();
}
//# sourceMappingURL=seed.js.map
File diff suppressed because one or more lines are too long
+144
View File
@@ -0,0 +1,144 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { BadRequestException, Body, Controller, Delete, Get, HttpCode, Injectable, Module, Param, Post, Put, } from "@nestjs/common";
import { userCreateSchema, userUpdateSchema, } from "@imen/shared";
import { AdminAuth, CurrentUser } from "../common/auth.js";
import { AuditService } from "../common/common.module.js";
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
import { AuthModule } from "../auth/auth.controller.js";
import { AuthService, hashPassword, toUserDto } from "../auth/auth.service.js";
import { PrismaService } from "../prisma/prisma.service.js";
let UsersService = class UsersService {
prisma;
auth;
audit;
constructor(prisma, auth, audit) {
this.prisma = prisma;
this.auth = auth;
this.audit = audit;
}
async list() {
const users = await this.prisma.adminUser.findMany({ orderBy: { id: "asc" } });
return users.map(toUserDto);
}
async create(input, actor) {
const user = await this.prisma.adminUser.create({
data: {
phone: input.phone,
name: input.name,
role: input.role,
passwordHash: await hashPassword(input.password),
},
});
this.audit.log(actor.id, "create", "user", user.id, user.phone);
return toUserDto(user);
}
async update(id, input, actor) {
if (id === actor.id && (input.role !== "ADMIN" || !input.isActive)) {
throw new BadRequestException("نمی‌توانید نقش یا وضعیت حساب خودتان را تغییر دهید");
}
const user = await this.prisma.adminUser.update({
where: { id },
data: {
name: input.name,
role: input.role,
isActive: input.isActive,
...(input.password ? { passwordHash: await hashPassword(input.password) } : {}),
},
});
if (input.password || !input.isActive)
await this.auth.revokeAll(id);
this.audit.log(actor.id, "update", "user", id, user.phone);
return toUserDto(user);
}
async remove(id, actor) {
if (id === actor.id)
throw new BadRequestException("نمی‌توانید حساب خودتان را حذف کنید");
const user = await this.prisma.adminUser.delete({ where: { id } });
this.audit.log(actor.id, "delete", "user", id, user.phone);
}
};
UsersService = __decorate([
Injectable(),
__metadata("design:paramtypes", [PrismaService,
AuthService,
AuditService])
], UsersService);
export { UsersService };
let UsersController = class UsersController {
users;
constructor(users) {
this.users = users;
}
list() {
return this.users.list();
}
create(body, actor) {
return this.users.create(body, actor);
}
update(id, body, actor) {
return this.users.update(id, body, actor);
}
async remove(id, actor) {
await this.users.remove(id, actor);
}
};
__decorate([
Get(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], UsersController.prototype, "list", null);
__decorate([
Post(),
__param(0, Body(new ZodPipe(userCreateSchema))),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", void 0)
], UsersController.prototype, "create", null);
__decorate([
Put(":id"),
__param(0, Param("id", IdPipe)),
__param(1, Body(new ZodPipe(userUpdateSchema))),
__param(2, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object, Object]),
__metadata("design:returntype", void 0)
], UsersController.prototype, "update", null);
__decorate([
Delete(":id"),
HttpCode(204),
__param(0, Param("id", IdPipe)),
__param(1, CurrentUser()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Object]),
__metadata("design:returntype", Promise)
], UsersController.prototype, "remove", null);
UsersController = __decorate([
AdminAuth("ADMIN"),
Controller("admin/users"),
__metadata("design:paramtypes", [UsersService])
], UsersController);
export { UsersController };
let UsersModule = class UsersModule {
};
UsersModule = __decorate([
Module({
imports: [AuthModule],
controllers: [UsersController],
providers: [UsersService],
})
], UsersModule);
export { UsersModule };
//# sourceMappingURL=users.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"users.controller.js","sourceRoot":"","sources":["../../src/users/users.controller.ts"],"names":[],"mappings":"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"}