add docs
This commit is contained in:
@@ -0,0 +1,219 @@
|
|||||||
|
# API Conventions
|
||||||
|
|
||||||
|
HTTP API design standards for Negareh backend. Swagger documentation is available at `/docs` when the server is running.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
No global path prefix. Routes encode the audience and resource directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /public/auth/otp/request
|
||||||
|
GET /admin/orders
|
||||||
|
GET /public/orders/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route naming
|
||||||
|
|
||||||
|
### Audience prefixes
|
||||||
|
|
||||||
|
| Prefix | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `public/` | Customer-facing endpoints |
|
||||||
|
| `admin/` | Back-office endpoints |
|
||||||
|
|
||||||
|
Auth endpoints embed the audience in the path: `public/auth/...`, `admin/auth/...`.
|
||||||
|
|
||||||
|
### HTTP methods
|
||||||
|
|
||||||
|
| Method | Usage |
|
||||||
|
|--------|-------|
|
||||||
|
| `GET` | Read single resource or list |
|
||||||
|
| `POST` | Create resource or action (e.g. OTP request) |
|
||||||
|
| `PATCH` | Partial update |
|
||||||
|
| `PUT` | Full replace (rare) |
|
||||||
|
| `DELETE` | Soft or hard delete |
|
||||||
|
|
||||||
|
### Path style
|
||||||
|
|
||||||
|
- Use **plural nouns** for collections: `/admin/orders`, `/public/tickets`.
|
||||||
|
- Use **path parameters** for IDs: `/admin/orders/:id`.
|
||||||
|
- Use **query parameters** for filtering, sorting, and pagination.
|
||||||
|
- Nest related actions under the resource: `/admin/orders/:id/assign-designer`.
|
||||||
|
|
||||||
|
Controllers use `@Controller()` with the full path on each method (no shared controller prefix), except where a module uses a short prefix like `@Controller('admin')`.
|
||||||
|
|
||||||
|
## Request format
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
|
||||||
|
| Header | Required | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| `Content-Type: application/json` | Yes (JSON bodies) | Standard JSON requests |
|
||||||
|
| `Authorization: Bearer <token>` | Protected routes | JWT access token |
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
All inputs are validated through DTO classes with `class-validator`. The global `ValidationPipe` has `transform: true`, so query strings are coerced to the correct types.
|
||||||
|
|
||||||
|
Invalid input returns `400` with validation messages in the error envelope.
|
||||||
|
|
||||||
|
### File uploads
|
||||||
|
|
||||||
|
Multipart uploads use `@fastify/multipart`. Upload endpoints live in the `uploader` module and return file metadata/URLs from S3.
|
||||||
|
|
||||||
|
## Response format
|
||||||
|
|
||||||
|
### Success (single resource)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 200,
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "01HXYZ...",
|
||||||
|
"phone": "09362532122"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success (paginated list)
|
||||||
|
|
||||||
|
Services return `PaginatedResult<T>`; the interceptor wraps it:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 200,
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{ "id": "01HXYZ...", "status": "pending" }
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"total": 42,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 10,
|
||||||
|
"totalPages": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination query parameters
|
||||||
|
|
||||||
|
Common fields on list DTOs:
|
||||||
|
|
||||||
|
| Param | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| `page` | number | `1` | Page number (1-based) |
|
||||||
|
| `limit` | number | `10` | Items per page |
|
||||||
|
| `search` | string | — | Free-text search |
|
||||||
|
| `orderBy` | string | `createdAt` | Sort field |
|
||||||
|
| `order` | `asc` \| `desc` | `desc` | Sort direction |
|
||||||
|
|
||||||
|
Some endpoints may use cursor-based pagination and return `nextCursor` at the top level.
|
||||||
|
|
||||||
|
### Error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 400,
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"message": ["Phone number is invalid"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`message` is always an array of strings, even for a single error.
|
||||||
|
|
||||||
|
### HTTP status codes
|
||||||
|
|
||||||
|
| Code | When |
|
||||||
|
|------|------|
|
||||||
|
| `200` | Successful GET, PATCH, DELETE |
|
||||||
|
| `201` | Successful POST (create) — when explicitly set |
|
||||||
|
| `400` | Validation failure, bad input |
|
||||||
|
| `401` | Missing or invalid token |
|
||||||
|
| `403` | Authenticated but insufficient permissions |
|
||||||
|
| `404` | Resource not found |
|
||||||
|
| `429` | Rate limit exceeded |
|
||||||
|
| `500` | Unexpected server error |
|
||||||
|
|
||||||
|
## Swagger
|
||||||
|
|
||||||
|
- Title: **Negareh API**
|
||||||
|
- Path: `/docs`
|
||||||
|
- Bearer auth scheme enabled — use the Authorize button to set JWT.
|
||||||
|
- Controllers document endpoints with `@ApiTags`, `@ApiOperation`, `@ApiBody`, `@ApiBearerAuth`.
|
||||||
|
- DTO fields use `@ApiProperty` for examples and descriptions.
|
||||||
|
|
||||||
|
## Controller pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@ApiTags('orders')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller()
|
||||||
|
export class OrderController {
|
||||||
|
constructor(private readonly orderService: OrderService) {}
|
||||||
|
|
||||||
|
// User route
|
||||||
|
@Get('public/orders')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiOperation({ summary: 'Get all orders with pagination and filters' })
|
||||||
|
findAll(@Query() dto: FindOrdersDto, @UserId() userId: string) {
|
||||||
|
return this.orderService.findOrdersAsUser(userId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin route
|
||||||
|
@Post('admin/orders')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.CREATE_ORDER)
|
||||||
|
@ApiOperation({ summary: 'Create order' })
|
||||||
|
create(@AdminId() adminId: string, @Body() body: CreateOrderAsAdminDto) {
|
||||||
|
return this.orderService.createOrderAsAdmin(adminId, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Identifiers
|
||||||
|
|
||||||
|
- Resource IDs are **ULIDs** (26-character strings).
|
||||||
|
- Pass IDs as path or body fields; do not use integer IDs.
|
||||||
|
|
||||||
|
## Dates and times
|
||||||
|
|
||||||
|
- Stored as `timestamptz` in UTC.
|
||||||
|
- Serialized as ISO 8601 strings in JSON responses.
|
||||||
|
- Token `expire` fields use Unix milliseconds.
|
||||||
|
|
||||||
|
## CORS
|
||||||
|
|
||||||
|
CORS is enabled for all origins in development with credentials support:
|
||||||
|
|
||||||
|
- Methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`
|
||||||
|
- `credentials: true`
|
||||||
|
|
||||||
|
Tighten `origin` in production deployments.
|
||||||
|
|
||||||
|
## Rate limiting
|
||||||
|
|
||||||
|
Global default: **5 requests per 60 seconds** per IP (`ThrottlerModule`).
|
||||||
|
|
||||||
|
Stricter limits on sensitive routes:
|
||||||
|
|
||||||
|
- OTP request: 3 per 180 seconds
|
||||||
|
- Refresh token: custom decorator (`@RefreshTokenRateLimit()`)
|
||||||
|
|
||||||
|
Exceeded limits return `429 Too Many Requests`.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
The API is currently unversioned (`v1` is implicit). Breaking changes should be coordinated with all clients (admin console, mobile app) before deployment.
|
||||||
|
|
||||||
|
## Adding a new endpoint checklist
|
||||||
|
|
||||||
|
1. Create or extend a DTO with validation and Swagger decorators.
|
||||||
|
2. Add a thin controller method with correct `public/` or `admin/` path.
|
||||||
|
3. Apply the appropriate guard and `@Permissions` if admin-only.
|
||||||
|
4. Implement logic in the service; use repository for queries.
|
||||||
|
5. Return typed data — the response interceptor handles wrapping.
|
||||||
|
6. Document with `@ApiOperation` and related Swagger decorators.
|
||||||
|
7. Add migration if new entities or columns are required.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
Negareh API is a NestJS backend built on **Fastify**, **PostgreSQL** (via MikroORM), and **Redis** (caching, OTP, queues). It powers both the customer-facing app and the admin console.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|------------|
|
||||||
|
| Runtime | Node.js, TypeScript |
|
||||||
|
| Framework | NestJS 11 |
|
||||||
|
| HTTP server | Fastify (`@nestjs/platform-fastify`) |
|
||||||
|
| ORM | MikroORM 6 (PostgreSQL driver) |
|
||||||
|
| Auth | JWT + refresh tokens, OTP via SMS |
|
||||||
|
| Cache / OTP | Redis (`@keyv/redis`, `CacheService`) |
|
||||||
|
| Jobs | BullMQ (`@nestjs/bullmq`) |
|
||||||
|
| Real-time | Socket.IO (notifications) |
|
||||||
|
| File storage | AWS S3-compatible bucket |
|
||||||
|
| Payments | Zarinpal gateway |
|
||||||
|
| API docs | Swagger at `/docs` |
|
||||||
|
|
||||||
|
## High-level layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.ts # Bootstrap: Fastify, pipes, interceptors, CORS, Swagger
|
||||||
|
├── app.module.ts # Root module — wires all feature modules
|
||||||
|
├── config/ # MikroORM, cache, HTTP, Swagger
|
||||||
|
├── core/ # Cross-cutting: exceptions, interceptors, middlewares
|
||||||
|
├── common/ # Shared entities, decorators, enums, interfaces
|
||||||
|
├── modules/ # Feature modules (domain boundaries)
|
||||||
|
└── seeders/ # Database seed data
|
||||||
|
|
||||||
|
database/
|
||||||
|
└── migrations/ # Versioned schema changes (MikroORM migrations)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature modules
|
||||||
|
|
||||||
|
Each domain lives under `src/modules/<name>/` with a consistent internal structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
modules/<feature>/
|
||||||
|
├── <feature>.module.ts
|
||||||
|
├── controllers/ # HTTP layer (thin)
|
||||||
|
├── providers/ # Business logic (services)
|
||||||
|
├── repositories/ # Data access (extends EntityRepository)
|
||||||
|
├── entities/ # MikroORM entities
|
||||||
|
├── dto/ # Request/response validation
|
||||||
|
├── listeners/ # Event handlers (@OnEvent)
|
||||||
|
├── events/ # Domain events (EventEmitter2)
|
||||||
|
└── enum/ # Feature-specific enums
|
||||||
|
```
|
||||||
|
|
||||||
|
Current modules:
|
||||||
|
|
||||||
|
- **auth** — OTP login, JWT, refresh tokens
|
||||||
|
- **user** — End-user accounts and credit
|
||||||
|
- **admin** — Admin accounts
|
||||||
|
- **roles** — Roles and permissions (RBAC)
|
||||||
|
- **product** — Products and categories
|
||||||
|
- **form-builder** — Dynamic product fields and options
|
||||||
|
- **request** — Customer requests / quotes
|
||||||
|
- **invoice** — Invoicing
|
||||||
|
- **order** — Order lifecycle
|
||||||
|
- **payment** — Online payments (Zarinpal)
|
||||||
|
- **print** — Print-related admin operations
|
||||||
|
- **ticket** — Support tickets
|
||||||
|
- **chat** — Order/chat messaging
|
||||||
|
- **notification** — SMS, push, WebSocket notifications
|
||||||
|
- **uploader** — File uploads to S3
|
||||||
|
- **announcements**, **learnings**, **criticisms** — Content and feedback
|
||||||
|
- **util** — Shared utilities (cache, phone normalization)
|
||||||
|
|
||||||
|
## Request lifecycle
|
||||||
|
|
||||||
|
1. **Fastify** receives the HTTP request.
|
||||||
|
2. **ValidationPipe** validates and transforms DTOs (`class-validator` + `class-transformer`).
|
||||||
|
3. **Guards** enforce authentication (`AuthGuard`, `AdminAuthGuard`) and permissions (`@Permissions`).
|
||||||
|
4. **Controller** delegates to a **service**.
|
||||||
|
5. **Service** uses **repositories** and/or `EntityManager` for persistence.
|
||||||
|
6. **ResponseInterceptor** wraps successful responses in a standard envelope.
|
||||||
|
7. **HttpExceptionFilter** formats error responses.
|
||||||
|
|
||||||
|
## Cross-cutting concerns
|
||||||
|
|
||||||
|
### Response envelope
|
||||||
|
|
||||||
|
Successful responses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 200,
|
||||||
|
"success": true,
|
||||||
|
"data": { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Paginated responses also include `meta`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 200,
|
||||||
|
"success": true,
|
||||||
|
"data": [],
|
||||||
|
"meta": { "total": 100, "page": 1, "limit": 10, "totalPages": 10 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"statusCode": 400,
|
||||||
|
"success": false,
|
||||||
|
"error": { "message": ["..."] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain events
|
||||||
|
|
||||||
|
Services emit events via `EventEmitter2`. Listeners in `listeners/` handle side effects (notifications, chat, payment follow-up) without bloating the main flow.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Use `ConfigService` for all environment variables. `ConfigModule` is registered globally in `AppModule`. Never read `process.env` directly in application code.
|
||||||
|
|
||||||
|
### Rate limiting
|
||||||
|
|
||||||
|
`@nestjs/throttler` is configured globally (5 requests per 60 seconds). Sensitive endpoints (OTP, refresh token) use stricter per-route limits via `@Throttle` or custom decorators.
|
||||||
|
|
||||||
|
## Design principles
|
||||||
|
|
||||||
|
- **Thin controllers, fat services** — HTTP concerns stay in controllers; business rules live in services.
|
||||||
|
- **Reuse before rewrite** — Search for existing services, DTOs, and repositories before adding new ones.
|
||||||
|
- **Module boundaries** — Features communicate through exported services or domain events, not by reaching into another module's internals.
|
||||||
|
- **No duplicated logic** — Extend existing modules rather than copying patterns.
|
||||||
|
- **Transactions for multi-entity writes** — Use `em.transactional()` when several rows must commit or roll back together.
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
| Integration | Purpose |
|
||||||
|
|-------------|---------|
|
||||||
|
| SMS.ir | OTP and transactional SMS |
|
||||||
|
| Zarinpal | Payment gateway |
|
||||||
|
| S3-compatible storage | File uploads |
|
||||||
|
| Redis | OTP cache, general caching, BullMQ |
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run start:dev # Watch mode on APP_PORT (default 4000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Swagger UI: `http://localhost:4000/docs`
|
||||||
|
|
||||||
|
Database helpers (see [database.md](./database.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run migration:up # Apply pending migrations
|
||||||
|
npm run db:seed # Run seeders
|
||||||
|
npm run db:reset # Drop, create, migrate, seed (destructive)
|
||||||
|
```
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
# Authentication & Authorization
|
||||||
|
|
||||||
|
Negareh API supports two actor types — **users** (customers) and **admins** (back-office). Both authenticate via **phone OTP** and receive **JWT access tokens** with **refresh tokens**.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ OTP request ┌─────────────┐
|
||||||
|
│ Client │ ──────────────────► │ Auth API │
|
||||||
|
│ │ ◄────────────────── │ (SMS/Redis)│
|
||||||
|
└─────────────┘ OTP code └─────────────┘
|
||||||
|
│ │
|
||||||
|
│ OTP verify │
|
||||||
|
└──────────────────────────────────► │
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Access JWT + Refresh │
|
||||||
|
│ token (hashed in DB) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## OTP flow
|
||||||
|
|
||||||
|
### User (public)
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/public/auth/otp/request` | POST | Send OTP to phone (creates user on verify) |
|
||||||
|
| `/public/auth/otp/verify` | POST | Verify OTP → returns tokens + user profile |
|
||||||
|
| `/public/auth/refresh` | POST | Exchange refresh token for new token pair |
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/admin/auth/otp/request` | POST | Send OTP (phone must exist in `admins` table) |
|
||||||
|
| `/admin/auth/otp/verify` | POST | Verify OTP → returns tokens + admin profile |
|
||||||
|
| `/admin/auth/refresh` | POST | Exchange refresh token for new token pair |
|
||||||
|
|
||||||
|
### OTP details
|
||||||
|
|
||||||
|
- Phone numbers are normalized to local format via `normalizePhoneToLocal()` (`09xxxxxxxxx`).
|
||||||
|
- OTP codes are 5-digit numeric values stored in **Redis** with TTL (`OTP_EXPIRATION_TIME`, default 240 seconds).
|
||||||
|
- OTP request endpoints are rate-limited (`@Throttle`: 3 requests per 180 seconds).
|
||||||
|
- SMS delivery uses SMS.ir patterns (configured via `SMS_*` env vars).
|
||||||
|
|
||||||
|
### Request / verify DTOs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Request OTP
|
||||||
|
{ "phone": "09362532122" }
|
||||||
|
|
||||||
|
// Verify OTP
|
||||||
|
{ "phone": "09362532122", "otp": "12345" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT tokens
|
||||||
|
|
||||||
|
### Access token
|
||||||
|
|
||||||
|
- Signed with `JWT_SECRET`.
|
||||||
|
- Expiration: `JWT_EXPIRATION_TIME` (seconds).
|
||||||
|
- Sent as `Authorization: Bearer <token>`.
|
||||||
|
|
||||||
|
**User payload:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{ userId: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Admin payload:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{ adminId: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Refresh token
|
||||||
|
|
||||||
|
- Opaque 64-character hex string (not a JWT).
|
||||||
|
- Stored **hashed** (SHA-256) in the `refresh_tokens` table.
|
||||||
|
- Expiration: `REFRESH_TOKEN_EXPIRE` (days).
|
||||||
|
- Rotated on each refresh — old token is deleted in the same transaction as the new pair.
|
||||||
|
- Type field distinguishes `user` vs `admin` tokens.
|
||||||
|
|
||||||
|
### Refresh response shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": { "token": "...", "expire": 1710000000000 },
|
||||||
|
"refreshToken": { "token": "...", "expire": 1711000000000 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`expire` values are Unix timestamps in milliseconds.
|
||||||
|
|
||||||
|
## Guards
|
||||||
|
|
||||||
|
### `AuthGuard` (users)
|
||||||
|
|
||||||
|
- Applied with `@UseGuards(AuthGuard)` on public routes that require a logged-in user.
|
||||||
|
- Verifies JWT from `Authorization` header.
|
||||||
|
- Sets `request.userId` from the token payload.
|
||||||
|
- Extract identity in controllers with `@UserId()`.
|
||||||
|
|
||||||
|
### `AdminAuthGuard` (admins)
|
||||||
|
|
||||||
|
- Applied with `@UseGuards(AdminAuthGuard)` on admin routes.
|
||||||
|
- Verifies JWT and sets `request.adminId`.
|
||||||
|
- Extract identity with `@AdminId()`.
|
||||||
|
- Reads `@Permissions(...)` metadata from the handler/class for RBAC checks.
|
||||||
|
|
||||||
|
## Role-based access control (RBAC)
|
||||||
|
|
||||||
|
Admins have a **role** with attached **permissions**. Permission names are defined in `PermissionEnum` (e.g. `view_orders`, `create_invoice`, `manage_admins`).
|
||||||
|
|
||||||
|
### Declaring required permissions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Get('admin/orders')
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@Permissions(PermissionEnum.VIEW_ORDERS)
|
||||||
|
findAllAsAdmin(@Query() dto: FindOrdersDto) {
|
||||||
|
return this.orderService.findOrdersAsAdmin(dto);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `@Permissions()` decorator stores required permission keys in metadata. `AdminAuthGuard` reads them via `Reflector`.
|
||||||
|
|
||||||
|
> **Note:** Permission enforcement against the database/cache is partially implemented. The guard structure and decorators are in place; ensure permission checks are active before relying on them in production.
|
||||||
|
|
||||||
|
## Route prefixes
|
||||||
|
|
||||||
|
Routes are namespaced by audience in the controller path (not a global prefix):
|
||||||
|
|
||||||
|
| Prefix | Audience | Guard |
|
||||||
|
|--------|----------|-------|
|
||||||
|
| `public/` | End users | `AuthGuard` (when protected) |
|
||||||
|
| `admin/` | Back-office | `AdminAuthGuard` |
|
||||||
|
|
||||||
|
Unauthenticated routes use `public/auth/...` or `admin/auth/...` without guards.
|
||||||
|
|
||||||
|
## WebSocket auth
|
||||||
|
|
||||||
|
Admin WebSocket connections use `WsAdminAuthGuard` in the notifications module. Follow the same JWT verification pattern as HTTP admin auth.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `JWT_SECRET` | Signing key for access tokens |
|
||||||
|
| `JWT_EXPIRATION_TIME` | Access token TTL (seconds) |
|
||||||
|
| `REFRESH_TOKEN_EXPIRE` | Refresh token TTL (days) |
|
||||||
|
| `OTP_EXPIRATION_TIME` | OTP cache TTL (seconds) |
|
||||||
|
| `REDIS_URI` / `REDIS_HOST` | OTP and cache storage |
|
||||||
|
| `SMS_BASE_URL`, `SMS_API_KEY`, `SMS_PATTERN_OTP` | SMS delivery |
|
||||||
|
|
||||||
|
## Security practices
|
||||||
|
|
||||||
|
- Refresh tokens are hashed at rest; raw tokens are only returned once to the client.
|
||||||
|
- Token refresh runs inside a database transaction to prevent race conditions.
|
||||||
|
- Rate limiting on OTP and refresh endpoints reduces brute-force risk.
|
||||||
|
- Never log tokens, OTP codes, or secrets.
|
||||||
|
- Use HTTPS in production; CORS is enabled with `credentials: true` for cookie support if needed.
|
||||||
|
|
||||||
|
## Client integration checklist
|
||||||
|
|
||||||
|
1. Request OTP with normalized Iranian mobile number.
|
||||||
|
2. Verify OTP and store both `accessToken.token` and `refreshToken.token`.
|
||||||
|
3. Send `Authorization: Bearer <accessToken>` on protected requests.
|
||||||
|
4. On 401, call the refresh endpoint with the refresh token.
|
||||||
|
5. Replace stored tokens with the new pair from the refresh response.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Coding Style
|
||||||
|
|
||||||
|
Conventions for writing and reviewing code in Negareh API. These align with the project's Cursor rules and existing patterns.
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
- **Strict typing** — Never use `any`. Prefer `unknown` when the type is genuinely unknown, then narrow.
|
||||||
|
- **Exported functions** — Add explicit return types.
|
||||||
|
- **Interfaces for DTOs** — Use classes with decorators for request bodies; interfaces for internal shapes and transformers.
|
||||||
|
- **Enums** — Use only when a fixed set of values is shared across layers (e.g. `PermissionEnum`, status enums). Avoid enums for one-off string unions.
|
||||||
|
- **Readonly** — Prefer `readonly` on constructor-injected dependencies and immutable fields.
|
||||||
|
- **Function size** — Keep functions under ~40 lines when practical; extract private methods for clarity.
|
||||||
|
|
||||||
|
## NestJS patterns
|
||||||
|
|
||||||
|
### Dependency injection
|
||||||
|
|
||||||
|
- Use **constructor injection** for all dependencies.
|
||||||
|
- Register providers in the feature module; export only what other modules need.
|
||||||
|
- Use `forwardRef()` when circular module dependencies are unavoidable (e.g. `AuthModule` ↔ `UserModule`).
|
||||||
|
|
||||||
|
### Controllers
|
||||||
|
|
||||||
|
- Keep controllers **thin** — validate input, call service, return result.
|
||||||
|
- Use `@ApiTags`, `@ApiOperation`, and `@ApiBearerAuth` for Swagger.
|
||||||
|
- Apply guards at method level: `@UseGuards(AuthGuard)` or `@UseGuards(AdminAuthGuard)`.
|
||||||
|
- Extract identity from decorators: `@UserId()`, `@AdminId()`.
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
- All **business logic** belongs in services (`providers/`).
|
||||||
|
- Throw NestJS `HttpException` subclasses (`BadRequestException`, `NotFoundException`, etc.).
|
||||||
|
- Use `ConfigService` instead of `process.env`.
|
||||||
|
|
||||||
|
### DTOs
|
||||||
|
|
||||||
|
Every request body and query object must have a DTO class:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class ExampleDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ example: 'value' })
|
||||||
|
@Transform(({ value }) => value?.trim())
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `class-validator` decorators for validation.
|
||||||
|
- Use `class-transformer` (`@Transform`) for normalization (e.g. phone numbers).
|
||||||
|
- Document fields with `@ApiProperty` for Swagger.
|
||||||
|
|
||||||
|
## File and naming conventions
|
||||||
|
|
||||||
|
| Artifact | Convention | Example |
|
||||||
|
|----------|------------|---------|
|
||||||
|
| Module | `<feature>.module.ts` | `order.module.ts` |
|
||||||
|
| Controller | `<feature>.controller.ts` in `controllers/` | `order.controller.ts` |
|
||||||
|
| Service | `<feature>.service.ts` in `providers/` | `order.service.ts` |
|
||||||
|
| Repository | `<entity>.repository.ts` in `repositories/` | `order.repository.ts` |
|
||||||
|
| Entity | `<entity>.entity.ts` in `entities/` | `order.entity.ts` |
|
||||||
|
| DTO | `<action>-<entity>.dto.ts` in `dto/` | `create-order.dto.ts` |
|
||||||
|
| Event | `<Entity><Action>Event` in `events/` | `OrderCreatedEvent` |
|
||||||
|
| Listener | `<feature>.listeners.ts` in `listeners/` | `order.listeners.ts` |
|
||||||
|
|
||||||
|
- Use **kebab-case** for file names.
|
||||||
|
- Use **PascalCase** for classes, **camelCase** for methods and variables.
|
||||||
|
- Folder names: `controllers`, `providers`, `repositories`, `entities`, `dto`, `listeners`, `events`, `enum`.
|
||||||
|
|
||||||
|
## MikroORM usage
|
||||||
|
|
||||||
|
- Prefer **EntityRepository** subclasses over raw `EntityManager` queries in services.
|
||||||
|
- Use **`populate`** instead of manual joins.
|
||||||
|
- Use **`wrap(entity).assign(dto)`** for partial updates.
|
||||||
|
- Use **`em.transactional()`** when updating multiple entities atomically.
|
||||||
|
- Use **`Collection`** for `OneToMany` relations.
|
||||||
|
- Avoid **N+1** — batch loads and selective `populate`.
|
||||||
|
- Avoid **raw SQL** unless necessary (triggers, complex reports). Migrations are the exception.
|
||||||
|
- Type repository injections explicitly.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Minimize database round-trips; batch where possible.
|
||||||
|
- Paginate large lists — return `PaginatedResult<T>` with `data` and `meta`.
|
||||||
|
- Select only required columns; avoid loading full entity graphs when a subset suffices.
|
||||||
|
- Index foreign keys and frequently filtered columns.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
- Use specific HTTP exceptions with clear messages.
|
||||||
|
- Shared user-facing messages live in `src/common/enums/message.enum.ts`.
|
||||||
|
- Do not swallow errors silently; log unexpected failures with NestJS `Logger`.
|
||||||
|
|
||||||
|
## Formatting and linting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run format # Prettier
|
||||||
|
npm run lint # ESLint (with fix)
|
||||||
|
npm run build # TypeScript compile check
|
||||||
|
```
|
||||||
|
|
||||||
|
Match the style of surrounding code. Do not reformat unrelated files in the same change.
|
||||||
|
|
||||||
|
## Before adding new code
|
||||||
|
|
||||||
|
1. Search for an existing implementation in `src/modules/` and `src/common/`.
|
||||||
|
2. Reuse existing services, DTOs, and repositories.
|
||||||
|
3. Follow the module's existing folder layout.
|
||||||
|
4. Extend rather than duplicate.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Database
|
||||||
|
|
||||||
|
Negareh API uses **PostgreSQL** with **MikroORM 6**. Schema changes are managed through migrations — never rely on `synchronize` in production.
|
||||||
|
|
||||||
|
## Connection
|
||||||
|
|
||||||
|
Configuration lives in `src/config/mikro-orm.config.ts` and reads from environment variables:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `DB_HOST` | PostgreSQL host |
|
||||||
|
| `DB_PORT` | PostgreSQL port |
|
||||||
|
| `DB_USER` | Database user |
|
||||||
|
| `DB_PASS` | Database password |
|
||||||
|
| `DB_NAME` | Database name |
|
||||||
|
| `NODE_ENV` | `production` disables auto schema ensure |
|
||||||
|
|
||||||
|
In non-production, MikroORM can auto-create/update the database schema on startup (`ensureDatabase`). In production, only migrations should change the schema.
|
||||||
|
|
||||||
|
## Entity conventions
|
||||||
|
|
||||||
|
All persistent models extend `BaseEntity`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/common/entities/base.entity.ts
|
||||||
|
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
|
||||||
|
export abstract class BaseEntity {
|
||||||
|
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
|
||||||
|
id: string = ulid();
|
||||||
|
|
||||||
|
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@Property({ nullable: true, columnType: 'timestamptz' })
|
||||||
|
deletedAt?: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- **Primary keys** — ULID strings (`char(26)`), not auto-increment integers.
|
||||||
|
- **Timestamps** — `timestamptz` with UTC (`forceUtcTimezone: true` in config).
|
||||||
|
- **Soft deletes** — Set `deletedAt` instead of hard-deleting rows. The `notDeleted` filter excludes soft-deleted records by default.
|
||||||
|
- **Indexes** — Add `@Index` on foreign keys and columns used in `WHERE` / `ORDER BY` clauses.
|
||||||
|
- **Table names** — Explicit `tableName` in `@Entity({ tableName: 'users' })` when the name differs from the class.
|
||||||
|
|
||||||
|
### Relations
|
||||||
|
|
||||||
|
- Use MikroORM decorators: `@ManyToOne`, `@OneToMany`, `@ManyToMany`.
|
||||||
|
- Initialize `OneToMany` with `new Collection<T>(this)`.
|
||||||
|
- Use `populate` in queries to load relations and avoid N+1.
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
wrap(entity).assign(partialDto);
|
||||||
|
await em.flush();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repositories
|
||||||
|
|
||||||
|
Custom data access extends `EntityRepository<Entity>`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
export class UserRepository extends EntityRepository<User> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, User);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllPaginated(dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||||
|
// build FilterQuery, findAndCount with limit/offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Register repositories as providers in the feature module alongside `MikroOrmModule.forFeature([Entity])`.
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
Migrations live in `database/migrations/` and are emitted as TypeScript.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Script | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| `npm run migration:create` | Generate migration from entity diff |
|
||||||
|
| `npm run migration:blank` | Create empty migration file |
|
||||||
|
| `npm run migration:up` | Apply pending migrations |
|
||||||
|
| `npm run migration:down` | Revert last migration |
|
||||||
|
| `npm run migration:list` | List all migrations |
|
||||||
|
| `npm run migration:pending` | Show unapplied migrations |
|
||||||
|
| `npm run migration:fresh` | Drop and re-run all migrations |
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
- **Always use migrations** for schema changes in shared environments.
|
||||||
|
- Migrations are **transactional** (`allOrNothing: true`).
|
||||||
|
- `safe: true` prevents destructive drops in generated migrations — review each file before applying.
|
||||||
|
- Complex logic (triggers, functions) belongs in migrations — see `Migration20260626140000` for invoice total recalculation triggers.
|
||||||
|
- **Never remove data** in migrations unless explicitly requested.
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. Modify entity files.
|
||||||
|
2. Run `npm run migration:create`.
|
||||||
|
3. Review the generated SQL in `database/migrations/`.
|
||||||
|
4. Run `npm run migration:up`.
|
||||||
|
5. Commit the migration file with the entity changes.
|
||||||
|
|
||||||
|
## Seeders
|
||||||
|
|
||||||
|
Seeders populate development/staging data. Entry point: `src/seeders/DatabaseSeeder.ts`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:seed # Run all seeders
|
||||||
|
npm run db:reset # Drop + create + migrate + seed (destructive)
|
||||||
|
```
|
||||||
|
|
||||||
|
Seeder structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/seeders/
|
||||||
|
├── DatabaseSeeder.ts # Orchestrates seed order
|
||||||
|
├── <entity>.seeder.ts # Per-entity seed logic
|
||||||
|
└── data/ # Static seed data arrays
|
||||||
|
```
|
||||||
|
|
||||||
|
Seed order matters — permissions and roles must exist before admins; categories before products.
|
||||||
|
|
||||||
|
## Query guidelines
|
||||||
|
|
||||||
|
- Avoid `SELECT *` in raw queries; MikroORM entity loads are fine but use `fields` option when you only need a subset.
|
||||||
|
- Design filters to hit indexes (`$ilike` on indexed columns sparingly; prefer exact match on indexed fields).
|
||||||
|
- Use `findAndCount` for paginated lists.
|
||||||
|
- Wrap multi-step writes in `em.transactional(async (em) => { ... })`.
|
||||||
|
|
||||||
|
## Connection pool
|
||||||
|
|
||||||
|
Pool settings are tuned per environment in `mikro-orm.config.ts`:
|
||||||
|
|
||||||
|
- Production: min 5, max 20 connections
|
||||||
|
- Development: min 2, max 10 connections
|
||||||
|
|
||||||
|
Statement and idle timeouts are set to 60 seconds to prevent hung transactions.
|
||||||
Reference in New Issue
Block a user