feat(login_action):login is working but register with email still error

parent 2b1aba07
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
NODE_ENV=development NODE_ENV=development
PORT=3001 PORT=3001
BACKEND_URL=http://localhost:3001 BACKEND_URL=http://localhost:3001
FRONTEND_URL=http://localhost:3000,http://localhost:3001 FRONTEND_URL=http://localhost:4001
PROJECT_NAME=SSO VietProDev PROJECT_NAME=SSO VietProDev
PROJECT_VERSION=1.0.0 PROJECT_VERSION=1.0.0
......
# Authors
Nguyen Thi Nguyet Que <quentn0620@gmail.com> - Original Author and Creator
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -7,12 +7,18 @@ services: ...@@ -7,12 +7,18 @@ services:
image: postgres:17-alpine image: postgres:17-alpine
container_name: sso-postgres container_name: sso-postgres
restart: unless-stopped restart: unless-stopped
ports: # ⚠️ Port 5432 is used by local Windows PostgreSQL (vietprodev_sso).
- '${POSTGRES_PORT:-5432}:5432' # The SSO server connects to local PostgreSQL, NOT to this container.
# Remove the port mapping to avoid conflict:
# ports:
# - '${POSTGRES_PORT:-5432}:5432'
environment: environment:
POSTGRES_USER: ${DB_USER:-postgres} POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-sso} # Container creates its own database named after POSTGRES_DB.
# This is SEPARATE from local PostgreSQL's vietprodev_sso.
# Migrations should run against the local PostgreSQL (pnpm db:setup).
POSTGRES_DB: ${DB_NAME:-vietprodev_sso}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
networks: networks:
...@@ -23,6 +29,31 @@ services: ...@@ -23,6 +29,31 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
# ─────────────────────────────────────────────
# postgres-backup: Full database backup instance on a SEPARATE port.
# Port 5433 avoids conflict with local Windows PostgreSQL (5432).
# This container is started MANUALLY: docker compose up -d postgres-backup
# ─────────────────────────────────────────────
postgres-backup:
image: postgres:17-alpine
container_name: sso-postgres-backup
restart: unless-stopped
ports:
- '5433:5432' # Exposed on 5433 (pgAdmin default), not 5432
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-vietprodev_sso}_backup
volumes:
- postgres_backup_data:/var/lib/postgresql/data
networks:
- sso-network
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres}']
interval: 10s
timeout: 5s
retries: 5
mongo: mongo:
image: mongo:7 image: mongo:7
container_name: sso-mongo container_name: sso-mongo
...@@ -198,6 +229,7 @@ networks: ...@@ -198,6 +229,7 @@ networks:
volumes: volumes:
postgres_data: postgres_data:
postgres_backup_data:
mongo_data: mongo_data:
redis_data: redis_data:
minio_data: minio_data:
...@@ -170,14 +170,16 @@ Folder names use **singular kebab-case**: `user`, `role`, `file`, `role-permissi ...@@ -170,14 +170,16 @@ Folder names use **singular kebab-case**: `user`, `role`, `file`, `role-permissi
``` ```
src/controllers/api/v1/ src/controllers/api/v1/
├── auth/ # Login, register, refresh, logout ├── auth/ # Login, register, refresh, logout, email verification
├── user/ # CRUD with role management ├── users/ # CRUD with role management
├── role/ # RBAC role management ├── roles/ # RBAC role management
├── permission/ # Permission definitions ├── permissions/ # Permission definitions
├── role-permission/ # Role ↔ Permission mapping ├── role-permission/ # Role ↔ Permission mapping
├── user-role/ # User ↔ Role assignment ├── user-role/ # User ↔ Role assignment
├── file/ # File upload and storage ├── files/ # File upload and storage
└── notifications/ # Push, email, in-app notifications ├── notifications/ # Push, email, in-app notifications
├── audit-logs/ # System audit logs
└── audit-dead-letters/ # Failed audit event DLQ
``` ```
Each module has: Controller · Service · Provider · Validator Each module has: Controller · Service · Provider · Validator
......
...@@ -77,7 +77,7 @@ DB_HOST=localhost ...@@ -77,7 +77,7 @@ DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USER=postgres DB_USER=postgres
DB_PASSWORD=your_password DB_PASSWORD=your_password
DB_NAME=bekind DB_NAME=vietprodev_sso
``` ```
### D3. Enable Redis ### D3. Enable Redis
......
# BEKIND Backend — Coding Conventions # SSO VietProDev Backend — Coding Conventions
Author: Nguyen Thi Nguyet Que Created on: May 2, 2026 Author: Nguyen Thi Nguyet Que Created on: May 2, 2026
...@@ -66,38 +66,39 @@ Architecture (Controller → Service → Provider → Model) ...@@ -66,38 +66,39 @@ Architecture (Controller → Service → Provider → Model)
``` ```
src/ src/
constants/ ├── constants/ # Enums, error codes, roles, statuses
controllers/api/v1/ ├── config/ # Env config with Zod validation
auth/ ├── controllers/
file/ │ ├── admin/ # Admin API (clients, users)
notifications/ │ └── api/v1/ # REST API (auto-mounted by express-automatic-routes)
permission/ │ ├── auth/ # Login, register, verify-email, resend-verification
role/ │ ├── users/
role-permission/ │ ├── roles/
user/ │ ├── permissions/
user-role/ │ ├── role-permission/
interfaces/ │ ├── user-role/
error/ │ ├── files/
handler/ │ ├── notifications/
violations/ │ ├── audit-logs/
middlewares/ │ └── audit-dead-letters/
mod/ ├── contracts/ # Zod schemas + OpenAPI paths
validators/ ├── dto/ # Data transfer objects
models/ ├── interfaces/ # Shared TypeScript types
providers/ │ └── error/
services/auth/ ├── middlewares/ # Auth, validators, rate-limiter, CSP
services/data/ ├── models/ # Sequelize models (auto-generated)
services/database/sequelize/ ├── oidc/ # OIDC/OAuth2 provider + interactions
services/file-system/ ├── providers/ # Data access layer (CRUD per model)
services/notification/ ├── services/
helpers/ │ ├── auth/ # Auth, token, password, email verification
services/scheduler/ │ ├── database/ # Sequelize, multi-pool, Redis adapters
jobs/ │ ├── file-system/ # Logging
services/storage/ │ ├── notification/ # Email, push, in-app notifications
templates/base/ │ └── scheduler/ # BullMQ jobs
templates/email/ ├── templates/
templates/swagger/ │ ├── base/ # Base classes (Provider, Controller)
utils/ │ └── email/ # Email HTML templates
└── utils/ # Pure helpers, Logger, auth utilities
``` ```
--- ---
...@@ -236,14 +237,15 @@ Use tsconfig paths — no relative imports beyond sibling files. ...@@ -236,14 +237,15 @@ Use tsconfig paths — no relative imports beyond sibling files.
## 7. Error Handling ## 7. Error Handling
Use `GenericError` from `#interfaces/error/generic`:
```typescript ```typescript
throw new MeUError(404, 'DB', `Record not found`); throw GenericError.create({ vi: 'Không tìm thấy', en: 'Not found' }, 'NOT_FOUND', 404);
throw new MeUError(409, 'DB', { duplicateFields }); throw GenericError.create({ vi: 'Email đã tồn tại', en: 'Email already exists' }, 'CONFLICT', 409, { field: 'email' });
``` ```
- `errorCode`: numeric (404, 409, -999) - Never return raw error objects or stack traces
- `errorType`: `'API'` | `'DB'` - Use `res.error(error)` in controllers — never `res.json({ error: error.stack })`
- `errorData`: optional context object
--- ---
......
This diff is collapsed.
...@@ -32,7 +32,7 @@ DB_HOST=localhost ...@@ -32,7 +32,7 @@ DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USER=your_db_user DB_USER=your_db_user
DB_PASSWORD=your_db_password DB_PASSWORD=your_db_password
DB_NAME=bekind DB_NAME=vietprodev_sso
REDIS_HOST=localhost REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
...@@ -70,7 +70,7 @@ See `.env.example` for the full list (email, storage, notifications, virus scan, ...@@ -70,7 +70,7 @@ See `.env.example` for the full list (email, storage, notifications, virus scan,
PostgreSQL must be running before this step. PostgreSQL must be running before this step.
```bash ```bash
psql -U postgres -c "CREATE DATABASE bekind;" psql -U postgres -c "CREATE DATABASE vietprodev_sso;"
``` ```
--- ---
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Bills API — Hướng dẫn chi tiết cho Frontend
Tài liệu này mô tả API `bills` (hóa đơn) để đội frontend hiểu cách gọi, cấu trúc dữ liệu, quy tắc validate, và logic nghiệp vụ liên quan.
**Base URL**: /api/v1/bills
---
## Tổng quan
- Mục đích: quản lý hóa đơn (tạo, liệt kê, xem chi tiết, cập nhật, xóa) gắn với `room``resident`.
- Yêu cầu chung: endpoint yêu cầu authentication; nhiều route yêu cầu quyền `admin` (middleware `requireAdmin`).
- Định dạng ngày:
- `billing_period`: `YYYY-MM` (ví dụ `2026-05`)
- `due_date`: `YYYY-MM-DD` (DATE)
- Các thời gian trả về (`created_at`, `updated_at`) dùng ISO 8601 datetime string
---
## Các trường chính (tổng hợp)
- `id` (UUID): id của hóa đơn
- `code` (string): mã hóa đơn sinh tự động
- `room_id` (UUID): id phòng
- `resident_id` (UUID): id cư dân (người liên quan đến hóa đơn)
- `billing_period` (string): kỳ thu, format `YYYY-MM`
- `due_date` (string): hạn trả, `YYYY-MM-DD`
- `payment_method` (string|null)
- `notes` (string|null)
- `total_amount` (number|null)
- `status` (string): `draft` | `paid` | `pending` | `overdue`
- `created_by` (UUID|null)
- `created_at`, `updated_at`, `deleted_at` (datetime strings)
### bill_items
Mỗi `bill` có một mảng `bill_items` mô tả các khoản phí thuộc hóa đơn.
- Trường trả về (`BillItem`):
- `id` (UUID)
- `bill_id` (UUID)
- `fee_name` (string)
- `quantity` (integer)
- `unit_price` (number)
- `created_at` (datetime)
- Trường gửi khi tạo (`BillItemCreate`):
- `fee_name`: required string
- `quantity`: optional integer (default 1)
- `unit_price`: required number >= 0
---
## Endpoint: POST /api/v1/bills — Tạo hóa đơn mới
- Authentication: có (middleware `verify`), thường yêu cầu `requireAdmin`.
- Body: `application/json` theo `BillCreateBody`.
### Request body (example)
```json
{
"room_id": "466256c1-0cac-4784-af74-8a16b5576853",
"resident_id": "77862ec4-3c2b-4e95-8c90-dce5226e76f1",
"billing_period": "2026-05",
"due_date": "2026-05-30",
"payment_method": "cash",
"notes": "Thanh toán tiền phòng tháng 5",
"total_amount": 5000000,
"status": "pending",
"bill_items": [
{ "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000 },
{ "fee_name": "Điện", "quantity": 1, "unit_price": 500000 },
{ "fee_name": "Nước", "quantity": 1, "unit_price": 500000 }
]
}
```
### Validation rules
- `room_id`, `resident_id`: UUID required
- `billing_period`: regex `^\d{4}-\d{2}$` (YYYY-MM)
- `due_date`: string date (no strict format enforced in validation beyond being a string; API expects `YYYY-MM-DD`)
- `status`: one of `draft`, `paid`, `pending`, `overdue` (default `pending`)
- `bill_items`: non-empty array minimum 1 item; each item must follow `BillItemCreate` schema
- `fee_name`: non-empty string, max 255
- `quantity`: integer >= 1 (default 1)
- `unit_price`: number >= 0
Nếu payload không hợp lệ, API trả `422` với hình thức lỗi:
```json
{
"type": "VALIDATION_ERROR",
"httpStatus": 422,
"messages": { "vi": "Dữ liệu không hợp lệ", "en": "Invalid data" },
"additionalData": { "errors": [ { "field": "bill_items.0.unit_price", "code": "INVALID_NUMBER_TYPE", "message": { ... } } ] }
}
```
### Business logic khi tạo
- Server sinh `code` tự động (`generateBillCode()`) nếu không truyền
- Tạo `bills` record trong transaction
- Nếu có `bill_items`, provider sẽ bulk-create các `bill_items` với `bill_id` gắn với `bill` vừa tạo
- `total_amount` nếu không truyền sẽ mặc định 0; tuy nhiên frontend nên tính tổng từ `bill_items` và gửi chính xác nếu muốn hiển thị ngay
- Ghi `created_by` từ `req.user.id` khi tạo
- Audit log sẽ được tạo (AuditLogService)
### Response
- 200 (hoặc success wrapper): dữ liệu `bill` đã tạo (kèm `bill_items` trả từ DB, có `id`, timestamps)
- Ví dụ:
```json
{
"success": true,
"data": {
"id": "...",
"code": "BILL-202605-0001",
"room_id": "...",
"resident_id": "...",
"billing_period": "2026-05",
"due_date": "2026-05-30",
"total_amount": 5000000,
"status": "pending",
"bill_items": [ { "id": "...", "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000, "created_at": "..." }, ... ],
"created_at": "...",
"updated_at": "..."
}
}
```
---
## Endpoint: GET /api/v1/bills — Danh sách (pagination, filters)
- Query params hỗ trợ bởi `QueryParamsSchema` (chung): `page`, `pageSize`, `sortField`, `sortOrder`, `filters`.
- Backend cung cấp `getAllOptimized` với join để trả `room_name`, `apartment_name`, `building_name`, `resident_full_name`, `resident_email`, `resident_phone`.
- Default pageSize: 20, max 100.
Ví dụ request:
```
GET /api/v1/bills?page=1&pageSize=20&sortField=created_at&sortOrder=desc
```
Response: { count, rows, page, pageSize }
Filter có thể truyền qua `filters` object (thực tế code áp dụng `...(options?.filters || {})` khi build `where`).
---
## Endpoint: GET /api/v1/bills/:id — Chi tiết hóa đơn
- Trả chi tiết bill kèm `bill_items`.
- Nếu không tìm thấy trả 404 (hoặc `null`) — tuỳ cách controller xử lý.
---
## Endpoint: PUT/PATCH /api/v1/bills/:id — Cập nhật
- Body theo `BillUpdateBody` schema (không cho cập nhật `bill_items` qua endpoint này; nếu cần update items, backend có provider/endpoint riêng hoặc cần triển khai).
- Validation tương tự `BillCreateBody` nhưng fields optional.
- Business rule: cập nhật `status` là supported. Nếu thay đổi `total_amount` frontend nên đồng bộ với `bill_items` service.
---
## Xử lý lỗi phổ biến
- 422 VALIDATION_ERROR: payload không hợp lệ (thường lỗi field types/format)
- 500 DATABASE / Sequelize errors: lỗi quan hệ/association—nếu gặp `SequelizeEagerLoadingError`, báo backend xem lại `init-models` associations
- 401/403: thiếu auth hoặc quyền
- 400 Bad JSON: body không phải JSON hợp lệ (ví dụ thiếu quote/comma) — kiểm tra payload JSON trước khi gửi
---
## Gợi ý UI / FE
- Khi tạo hóa đơn từ UI: tính `total_amount` client-side từ `bill_items` và gửi kèm để backend lưu, tránh mismatch
- Hiển thị trạng thái `pending`/`paid`/`overdue` rõ màu sắc
- Khi show form tạo: validate client-side trùng với server (billing_period format, due_date required, unit_price >= 0)
- Sau tạo: cập nhật danh sách bằng refetch `GET /api/v1/bills` hoặc realtime update nếu dùng websockets
---
## Các chú ý kỹ thuật cho FE-devs
- Authorization header: gửi token như bình thường (bearer) — backend sử dụng `verify` middleware.
- Request size: `bill_items` thường nhỏ; nếu tạo nhiều item > 100, cân nhắc batch API
- Định dạng JSON: ensure `due_date` là string `YYYY-MM-DD` khi gửi
- Nếu cần trường mở rộng (discount, tax), thỏa thuận trước với backend — hiện schema không support
---
## Ví dụ đầy đủ (curl)
```bash
curl -X 'POST' \
'http://localhost:3001/api/v1/bills' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
-d '{
"room_id": "466256c1-0cac-4784-af74-8a16b5576853",
"resident_id": "77862ec4-3c2b-4e95-8c90-dce5226e76f1",
"billing_period": "2026-05",
"due_date": "2026-05-30",
"payment_method": "cash",
"notes": "Thanh toán tiền phòng tháng 5",
"total_amount": 5000000,
"status": "pending",
"bill_items": [
{ "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000 },
{ "fee_name": "Điện", "quantity": 1, "unit_price": 500000 },
{ "fee_name": "Nước", "quantity": 1, "unit_price": 500000 }
]
}'
```
---
Nếu bạn muốn mình thêm:
- tệp OpenAPI/Swagger đoạn cho `bills` (examples + response schemas),
- hoặc tạo một mini-PR mẫu ở frontend (fetch helpers) để gọi endpoint này,
hãy nói mình biết ưu tiên nào nhé.
\ No newline at end of file
# Contract & Contract Types API Documentation
## Overview
The Contract API has been refactored to normalize `contract_type` from an ENUM into a separate
`contract_types` table. This allows for dynamic management of contract types without requiring
database schema changes.
**Key Change:** `contract_type` (string ENUM) → `contract_type_id` (UUID foreign key)
## Architecture
### Before (v1 - Deprecated)
```
contracts table:
- contract_type: ENUM('residential_lease', 'room_rental', 'service_agreement', 'other')
```
### After (v2 - Current)
```
contracts table:
- contract_type_id: UUID (FK → contract_types.id)
contract_types table:
- id: UUID (Primary Key)
- code: VARCHAR(50) - Code identifier (residential_lease, room_rental, etc.)
- name_vi: VARCHAR(100) - Vietnamese name
- name_en: VARCHAR(100) - English name
- description: TEXT - Detailed description
- created_at, updated_at, created_by, updated_by: Audit fields
- deleted_at, deleted_by: Soft delete fields
```
## ContractType Schema
### Response Format
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"code": "residential_lease",
"name_vi": "Thuê nhà/căn hộ",
"name_en": "Residential Lease",
"description": "Hợp đồng thuê toàn bộ nhà hoặc căn hộ",
"created_at": "2024-01-15T08:30:00Z",
"updated_at": "2024-01-15T08:30:00Z"
}
```
## Default Contract Types
The system auto-seeds 4 default contract types:
| Code | Vietnamese | English | Description |
| ------------------- | ---------------- | ----------------- | ------------------------------------- |
| `residential_lease` | Thuê nhà/căn hộ | Residential Lease | Hợp đồng thuê toàn bộ nhà hoặc căn hộ |
| `room_rental` | Thuê phòng | Room Rental | Hợp đồng thuê phòng đơn lẻ |
| `service_agreement` | Hợp đồng dịch vụ | Service Agreement | Hợp đồng cung cấp dịch vụ |
| `other` | Khác | Other | Các loại hợp đồng khác |
## Contract API
### Create Contract
**Endpoint:** `POST /api/v1/contracts`
**Request Body:**
```json
{
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_name": "Hợp đồng thuê phòng A101",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
"description": "Hợp đồng thuê phòng tại tòa nhà A",
"rent_amount": 5000000,
"deposit_amount": 10000000,
"payment_day": 15,
"signed_date": "2024-01-15",
"expiry_date": "2025-01-15"
}
```
**Response:**
```json
{
"success": true,
"data": {
"id": "789abcde-f012-3456-ghij-789012345678",
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_name": "Hợp đồng thuê phòng A101",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
"description": "Hợp đồng thuê phòng tại tòa nhà A",
"rent_amount": 5000000,
"deposit_amount": 10000000,
"payment_day": 15,
"signed_date": "2024-01-15",
"expiry_date": "2025-01-15",
"created_by": "user-id-here",
"updated_by": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"deleted_at": null
}
}
```
### Update Contract
**Endpoint:** `PUT /api/v1/contracts/{id}`
**Request Body (all fields optional):**
```json
{
"contract_number": "HĐ-2024-002",
"contract_name": "Updated name",
"contract_type_id": "different-uuid-here",
"description": "Updated description",
"rent_amount": 6000000,
"deposit_amount": 12000000,
"payment_day": 20,
"signed_date": "2024-02-01",
"expiry_date": "2025-02-01"
}
```
### Get Contract by ID
**Endpoint:** `GET /api/v1/contracts/{id}`
**Response:**
```json
{
"success": true,
"data": {
"id": "789abcde-f012-3456-ghij-789012345678",
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_name": "Hợp đồng thuê phòng A101",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
...
}
}
```
### List Contracts
**Endpoint:** `GET /api/v1/contracts`
**Query Parameters:**
```
?page=1&limit=20&orderBy=created_at&orderDir=DESC
```
**Response:**
```json
{
"success": true,
"data": {
"items": [
{
"id": "789abcde-f012-3456-ghij-789012345678",
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
...
}
],
"total": 42,
"page": 1,
"limit": 20
}
}
```
## Frontend Implementation Guide
### 1. Fetch Contract Types (on page load)
```typescript
// API Call to get all contract types
async function fetchContractTypes() {
const response = await fetch('/api/v1/contract-types', {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch contract types');
}
const data = await response.json();
return data.data; // Array of contract types
}
// Store in state/context
const [contractTypes, setContractTypes] = useState([]);
useEffect(() => {
fetchContractTypes().then(setContractTypes);
}, []);
```
### 2. Display Contract Type Dropdown
```tsx
<select
name="contract_type_id"
required
onChange={(e) => setFormData({ ...formData, contract_type_id: e.target.value })}
value={formData.contract_type_id}
>
<option value="">-- Chọn loại hợp đồng --</option>
{contractTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.name_vi} ({type.code})
</option>
))}
</select>
```
### 3. Create Contract
```typescript
async function createContract(contractData) {
const response = await fetch('/api/v1/contracts', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
resident_id: contractData.resident_id,
contract_number: contractData.contract_number,
contract_name: contractData.contract_name,
contract_type_id: contractData.contract_type_id, // UUID
description: contractData.description,
rent_amount: contractData.rent_amount,
deposit_amount: contractData.deposit_amount,
payment_day: contractData.payment_day,
signed_date: contractData.signed_date,
expiry_date: contractData.expiry_date,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return await response.json();
}
```
### 4. Display Contract with Type Name
```tsx
// After fetching contract and contract types
function ContractCard({ contract, contractTypes }) {
const contractType = contractTypes.find((t) => t.id === contract.contract_type_id);
return (
<div>
<h3>{contract.contract_name}</h3>
<p>
Loại HĐ: <strong>{contractType?.name_vi}</strong>
</p>
<p>Số HĐ: {contract.contract_number}</p>
<p>Giá thuê: {contract.rent_amount?.toLocaleString()} đ</p>
<p>Ngày hết hạn: {new Date(contract.expiry_date).toLocaleDateString('vi-VN')}</p>
</div>
);
}
```
### 5. Update Contract
```typescript
async function updateContract(contractId, updates) {
// Updates can include contract_type_id
const response = await fetch(`/api/v1/contracts/${contractId}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
});
if (!response.ok) {
throw new Error('Failed to update contract');
}
return await response.json();
}
```
## Important Notes
### ✅ What Changed
- Request/Response now uses `contract_type_id` (UUID) instead of `contract_type` (string)
- `contract_type_id` is a **required field** when creating contracts
- You must select an ID from the `contract_types` table
### ⚠️ Migration Checklist for Frontend
- [ ] Update form inputs to use `contract_type_id` instead of `contract_type`
- [ ] Fetch contract types on app initialization
- [ ] Store contract types in state/context for reuse
- [ ] Update dropdowns/selects to show `name_vi` but send `id`
- [ ] Update display logic to lookup type name from contract types array
- [ ] Update validation - ensure `contract_type_id` is UUID format
- [ ] Test create/update/read flows
### 🔍 Debugging Tips
**To see all available contract types:**
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/api/v1/contract-types
```
**To verify contract_type_id in a contract:**
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/api/v1/contracts/{contractId}
```
Look for the `contract_type_id` field - it should be a UUID.
## Error Handling
### Invalid contract_type_id
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid contract_type_id",
"details": {
"field": "contract_type_id",
"reason": "Contract type not found"
}
}
}
```
### Missing contract_type_id
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "contract_type_id is required"
}
}
```
## Related API Endpoints
- `GET /api/v1/contract-types` - List all contract types
- `POST /api/v1/contracts` - Create contract
- `GET /api/v1/contracts` - List contracts
- `GET /api/v1/contracts/{id}` - Get contract by ID
- `PUT /api/v1/contracts/{id}` - Update contract
- `DELETE /api/v1/contracts/{id}` - Delete contract (soft delete)
## Support
For questions or issues with the API, please refer to:
- [API Development Guide](./api-development.md)
- Backend team documentation
- Database schema: [Migration 016](../sql/migrations/016-add-contract-types-table.sql)
# Bulk create — Link files to Contract and Incident
Mục đích
- Cho phép liên kết nhiều file đã upload với một `contract_id` hoặc `incident_id` trong một lần gọi API.
Endpoints
- `POST /api/v1/contract-files` — Link nhiều file với một hợp đồng.
- `POST /api/v1/incident-files` — Link nhiều file với một sự cố.
Xác thực & quyền
- `POST /api/v1/contract-files`: yêu cầu `Bearer` token (middleware: `verify`).
- `POST /api/v1/incident-files`: yêu cầu `Bearer` token và quyền Admin (middleware: `verify`, `requireAdmin`).
Request body (JSON)
- Contract:
- `contract_id` (string, uuid) — Bắt buộc.
- `file_ids` (array[string, uuid]) — Bắt buộc, tối thiểu 1 phần tử.
- Incident:
- `incident_id` (string) — Bắt buộc.
- `file_ids` (array[string]) — Bắt buộc, tối thiểu 1 phần tử.
Ví dụ request — Contract
```json
{
"contract_id": "9fbbf51b-50a3-1aea-baa0-18596c0eebdf/i",
"file_ids": [
"9d9f1450-dd41-79d9-31f4-5b3903429ac3/i",
"f47ac10b-58cc-4372-a567-0e02b2c3d480/i"
]
}
```
Ví dụ request — Incident
```json
{
"incident_id": "9fbbf51b-50a3-1aea-baa0-18596c0eebdf/i",
"file_ids": [
"9d9f1450-dd41-79d9-31f4-5b3903429ac3/i"
]
}
```
Hành vi (Behavior)
- Với mỗi `file_id` trong `file_ids`, API sẽ tạo một bản ghi `ContractFile` hoặc `IncidentFile` tương ứng.
- Trường `created_by` được gán từ `req.user?.id` (người gọi API).
- Toàn bộ thao tác được thực hiện bằng `bulkCreate(...)` trong provider tương ứng (`ContractFileProvider.bulkCreate` hoặc `IncidentFileProvider.bulkCreate`) và chạy trong giao dịch — nếu một phần thất bại thì rollback cả nhóm (atomic).
- Controller trả về mảng các bản ghi vừa tạo (HTTP 201 cho contract-files, HTTP 200 cho incident-files theo controller hiện tại).
Ví dụ response
```json
{
"data": [
{
"id": "bb5ea6d5-fcd7-d0ca-d7ad-8f25a94bedd3",
"contract_id": "9fbbf51b-50a3-1aea-baa0-18596c0eebdf/i",
"file_id": "9d9f1450-dd41-79d9-31f4-5b3903429ac3/i",
"created_by": "user-uuid",
"created_at": "2026-05-18T12:34:56.000Z",
"updated_at": null,
"deleted_at": null
}
]
}
```
Schema validator & controller
- Contract schema: [src/contracts/contract-file/schema.ts](src/contracts/contract-file/schema.ts)
- Contract controller: [src/controllers/api/v1/contract-files/index.ts](src/controllers/api/v1/contract-files/index.ts)
- Incident schema: [src/contracts/incident-file/schema.ts](src/contracts/incident-file/schema.ts)
- Incident controller: [src/controllers/api/v1/incident-files/index.ts](src/controllers/api/v1/incident-files/index.ts)
Lưu ý & khuyến nghị
- `file_ids` là trường bắt buộc; `file_id` (đơn lẻ) không còn được dùng.
- Nếu cần tránh bản ghi trùng (duplicate) nên thực hiện dedupe trước khi gọi API hoặc thêm kiểm tra trong provider.
- Nếu database có ràng buộc khóa ngoại (FK) hoặc ràng buộc unique, lỗi có thể xuất hiện và toàn giao dịch sẽ rollback.
- Nếu muốn thay đổi mã trạng thái trả về cho consistency, cân nhắc trả `201 Created` cho cả hai endpoints.
Hành động tiếp theo (tuỳ chọn)
- Dọn dẹp: xóa hai file tài liệu cũ `contract-files-bulk-create.md``incident-files-bulk-create.md` nếu không cần giữ bản riêng.
- Commit thay đổi tài liệu vào repo.
This diff is collapsed.
**Incident API — Thay Đổi & Lưu Ý cho Frontend**
- **Tổng quan:**
- Backend đã di chuyển thông tin liên hệ của kỹ thuật viên (`full_name`, `phone`, `email`) từ bảng `technicians` sang bảng `users` (qua `technician.user_id`).
- Đã thêm hai trường trả về ở endpoint danh sách: `technician_phone``technician_email`.
**Endpoints & khác biệt chính**
- **GET /api/v1/incidents** (list) :
- Mục đích: trả về danh sách rút gọn (table/list view).
- Trường liên quan (quan trọng cho FE):
- `id`, `title`, `code`, `type_name`, `room_name`, `apartment_name`, `building_name`
- `reporter_name` (chuỗi), `technician_name` (chuỗi)
- `technician_phone` (string|null)
- `technician_email` (string|null)
- `priority`, `status`, `deadline`, `created_at`, `updated_at`
- Lưu ý: **Không có** trường `description` trong kết quả của endpoint này.
- Ví dụ (tóm tắt một item):
```json
{
"id": "...",
"title": "Bóng đèn hỏng",
"code": "INC-0001",
"technician_name": "Nguyễn Văn A",
"technician_phone": "0912345678",
"technician_email": "a@example.com",
"reporter_name": "Trần Thị B",
"priority": "high",
"status": "open"
}
```
- **GET /api/v1/incidents/:id** (detail) :
- Mục đích: trả về chi tiết đầy đủ của một incident.
- Trả về `description` (mô tả chi tiết) — đây là điểm khác biệt chính so với `GET /api/v1/incidents`.
- `technician` là đối tượng lồng (object) với cấu trúc:
- `technician.id`, `technician.name`, `technician.phone`, `technician.email` (tất cả có thể là `null` nếu không có user liên kết).
- `reporter` cũng trả về như một object: `id`, `name`, `phone`, `email`.
- Ví dụ (tóm tắt):
```json
{
"id": "...",
"title": "Bóng đèn hỏng",
"code": "INC-0001",
"description": "Đèn hành lang tầng 3 cháy",
"technician": {
"id": "tech-uuid",
"name": "Nguyễn Văn A",
"phone": "0912345678",
"email": "a@example.com"
},
"reporter": {
"id": "user-uuid",
"name": "Trần Thị B",
"phone": "0987654321",
"email": "b@example.com"
}
}
```
**Nguồn dữ liệu & mapping**
- Technician contact data hiện lấy từ bảng `users` thông qua quan hệ `technician.user_id`.
- Mapping FE cần cập nhật:
- List view: dùng `row.technician_name`, `row.technician_phone`, `row.technician_email`.
- Detail view: dùng `response.technician.name`, `response.technician.phone`, `response.technician.email`.
- Nếu giá trị là `null`, FE nên hiển thị fallback (ví dụ: `-` hoặc `Chưa có`).
**Tương thích ngược / Ghi chú triển khai**
- Trước đây một số code backend trả `technician.full_name`, `technician.phone`, `technician.email` trực tiếp từ bảng `technicians`. Hiện trường `full_name/phone/email` đã bị xóa khỏi bảng `technicians`.
- Đã giữ tên trường `technician_name` cho list (kết quả xây bằng CONCAT của `user.first_name` + `user.last_name`) để giảm tác động tới FE. Tuy nhiên:
- List: `technician_name` (chuỗi), `technician_phone`, `technician_email` (hai trường bổ sung mới).
- Detail: `technician` là object, **có cấu trúc khác** so với list — FE cần đọc theo object.
**Tham chiếu mã nguồn**
- Logic lấy dữ liệu và tên/trường mới ở: [src/providers/IncidentProvider.ts](src/providers/IncidentProvider.ts)
- Route list tạo ở: [src/controllers/api/v1/incidents/index.ts](src/controllers/api/v1/incidents/index.ts)
Nếu FE cần mình viết helper mapping (ví dụ: `normalizeIncidentForList` / `normalizeIncidentForDetail`) hoặc muốn ví dụ TypeScript interfaces, mình có thể thêm nhanh. Nếu có chỗ FE đang bị lỗi hiển thị, gửi ví dụ response hiện tại, mình sẽ kiểm tra.
This diff is collapsed.
This diff is collapsed.
# Posts API — Luồng chi tiết cho Frontend
Tài liệu này mô tả chi tiết các endpoint liên quan đến module **Posts** dành cho team Frontend: đường dẫn, quy tắc hiển thị theo vai trò, body/response mẫu, query params (pagination / filters) và các lưu ý khi triển khai UI.
> Tham khảo schema & đường dẫn backend: [src/contracts/post/schema.ts](src/contracts/post/schema.ts), [src/contracts/post/paths.ts](src/contracts/post/paths.ts)
---
## Tổng quan endpoints
- **GET** `/api/v1/posts` — Lấy danh sách (role-filtered).
- Mô tả: trả về danh sách phân trang; server sẽ lọc theo quyền/địa điểm người dùng.
- Quyền: token Bearer (Authorization) được sử dụng để xác định role; nếu không có token server sẽ xử lý như unauthenticated (xem phần Luồng truy cập).
- **POST** `/api/v1/posts` — Tạo bài viết (Admin only).
- **GET** `/api/v1/posts/{id}` — Lấy chi tiết một bài.
- **PUT** `/api/v1/posts/{id}` — Cập nhật bài (Admin only).
- **DELETE** `/api/v1/posts/{id}` — Soft-delete bài (Admin only).
---
## Luồng truy cập (visibility rules)
- Role **admin** / **system_admin**: có thể xem bài ở mọi `status` (draft, published, archived). Khi frontend dùng token của admin, server sẽ không ép filter status = 'published'.
- Người dùng bình thường (resident): chỉ thấy bài `status = 'published'` (mặc định) nhưng được lọc thêm theo chuỗi vị trí: user → resident → resident_stays → bed → room → apartment → building. Server sẽ trả các bài: `target_type = 'all'` hoặc các bài target trùng với building/apartment/room của resident.
- Unauthenticated (không có token): server sẽ trả **chỉ** các bài `target_type = 'all'` và (mặc định) `status = 'published'`.
Ghi chú đặc biệt:
- `target_type = 'floor'` được xử lý như `apartment` ở backend (xem provider logic).
- Để admin hiển thị draft/archived, FE chỉ cần gửi Authorization token của admin; không cần thêm parameter đặc biệt.
---
## Query params (pagination / filtering)
Các query param chung (theo `QueryParamsSchema`):
- `page` (number, default 1)
- `pageSize` (number, default 10, max 100)
- `sortField` (string)
- `sortOrder` (`asc` | `desc`)
- `filters` (string) — filter theo syntax của `sequelize-api-paginate`.
Ví dụ filter:
- `filters=status==published`
- `filters=status==draft` (admin only)
- `filters=created_at>=2024-01-01T00:00:00Z,created_at<=2024-12-31T23:59:59Z`
Ví dụ request list (published, trang 1, 20 item, sắp xếp theo published_at desc):
```bash
curl -X GET "https://api.example.com/api/v1/posts?filters=status==published&page=1&pageSize=20&sortField=published_at&sortOrder=desc" \
-H "Authorization: Bearer $TOKEN"
```
Lưu ý: FE thường không cần (và không nên) tự filter theo target_ids cho resident — backend sẽ trả các bài phù hợp với user dựa trên token.
---
## Body — Create / Update
Request create (`POST /api/v1/posts`) — tất cả trường theo `PostCreateBodySchema` (example):
```json
{
"title": "Thông báo bảo trì hệ thống",
"content": "Hệ thống sẽ bảo trì vào 22:00 - 23:00",
"targetType": "all",
"targetIds": [],
"isFeatured": false,
"status": "published",
"publishedAt": "2026-05-21T22:00:00Z"
}
```
Rules quan trọng:
- Nếu `targetType !== 'all'` thì `targetIds` phải khác rỗng (validated server-side).
- `status` có thể là: `draft`, `published`, `archived`.
Update (`PUT /api/v1/posts/{id}`): các field là optional; `content` có thể null.
---
## Response format (common)
All responses dùng envelope theo `ApiResponseSchema`:
```json
{
"success": true,
"data": { /* payload */ },
"trace_id": null,
"timestamp": "2026-05-21T10:00:00Z",
"errors": null
}
```
- List (`GET /api/v1/posts`) -> `data` shape:
```json
{
"count": 123,
"rows": [ /* PostListItem */ ],
"page": 1,
"pageSize": 20
}
```
- Post list item fields (`PostListItem`): `id`, `title`, `target_type`, `status`, `is_featured`, `author_id`, `author_name`, `published_at`, `created_at`, `updated_at`.
- Detail (`GET /api/v1/posts/{id}`) -> `PostDetail`: includes `content`, `target_ids` (array), `author` (object with `id`, `name`, `email`).
Ví dụ detail response `data`:
```json
{
"id":"...",
"title":"...",
"content":"...",
"target_type":"building",
"target_ids":["uuid-1","uuid-2"],
"status":"published",
"is_featured":false,
"author_id":"...",
"author":{"id":"...","name":"Nguyễn A","email":"a@example.com"},
"published_at":"2026-05-21T...",
"created_at":"...",
"updated_at":"..."
}
```
---
## Các luồng UI (Frontend)
1. Feed công khai (visitor):
- Gọi `GET /api/v1/posts` **không có token** → backend trả `target_type == 'all'` & `status == 'published'`.
- Hiển thị title, excerpt, author_name, published_at.
2. Resident logged-in (thường dùng app resident):
- Gọi `GET /api/v1/posts` với `Authorization` header.
- Backend sẽ tự resolve resident → room/apartment/building và trả những bài target phù hợp (kết hợp `post_targets`), chỉ `status = 'published'`.
3. Admin panel (create/update/delete/manage):
- Gọi `GET /api/v1/posts` với admin token → backend không ép status = 'published' nên admin có thể thấy draft/archived.
- Tạo bài: `POST /api/v1/posts` (admin only). FE form: chọn `targetType` và khi chọn `'building'|'apartment'|'floor'|'room'` thì show multi-select để pick `targetIds`.
- Update: `PUT /api/v1/posts/{id}`. Nếu thay đổi `targetType` từ 'all' → non-all cần cập nhật `targetIds`.
- Delete: `DELETE /api/v1/posts/{id}` sẽ soft-delete (FE có thể gọi và sau đó refresh list).
---
## Ví dụ cURL
- Lấy danh sách (visitor)
```bash
curl "https://api.example.com/api/v1/posts?page=1&pageSize=10"
```
- Lấy danh sách (admin xem draft)
```bash
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.example.com/api/v1/posts?filters=status==draft&page=1&pageSize=20"
```
- Tạo bài (admin)
```bash
curl -X POST "https://api.example.com/api/v1/posts" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Tít test","targetType":"all","targetIds":[],"status":"draft"}'
```
---
## Lưu ý kỹ thuật / Implementation tips
- Backend lưu `post_targets` trong bảng `post_targets` (mapping). Khi tạo/update, server thực hiện trong transaction nên FE không cần retry đặc biệt.
- Soft-delete: `DELETE /api/v1/posts/{id}` chỉ set `deleted_at` + `deleted_by`. Để khôi phục không có endpoint công khai — phải check bên backend nếu cần.
- Khi cần lọc theo `target_id` ở client (nếu có use-case đặc biệt), trao đổi với backend để hỗ trợ query phù hợp vì việc join `post_targets` đã được backend xử lý trong visibility logic.
---
Nếu muốn, mình có thể mở rộng phần này bằng các ví dụ React/Redux (service calls + typings), hoặc tạo Postman collection / Swagger UI snippet để FE import trực tiếp. Muốn mình làm phần nào tiếp theo?
# Post — Thumbnail (`thumbnail_id`)
Tổng quan
-------
Thêm cột `thumbnail_id` (UUID) vào bảng `posts` để tham chiếu tới bản ghi trong bảng `files` dùng làm thumbnail / ảnh đại diện cho bài viết.
API (FE hướng dẫn)
------------------
1) Create post (POST /api/v1/posts)
- Request body: thêm trường `thumbnailId` (UUID | null)
Ví dụ:
```json
{
"title": "posttest",
"content": "posttest",
"thumbnailId": "af62cea2-e329-45f3-bf55-b07eb2ee5618",
"targetType": "all",
"targetIds": [],
"isFeatured": false,
"status": "published"
}
```
2) Update post (PUT /api/v1/posts/{id})
- Gửi `thumbnailId` để thay đổi, hoặc `null` để xóa thumbnail.
Ví dụ (clear thumbnail):
```json
{ "thumbnailId": null }
```
3) Responses
- List (`GET /api/v1/posts`): mỗi item có `thumbnail_id` (UUID|null).
- Detail (`GET /api/v1/posts/{id}`): trả `thumbnail_id` và một object `thumbnail` (nếu file tồn tại, chưa bị xóa) với metadata theo `File` schema.
Ví dụ snippet (detail):
```json
{
"id": "...",
"title": "...",
"thumbnail_id": "af62cea2-e329-45f3-bf55-b07eb2ee5618",
"thumbnail": {
"id": "af62cea2-e329-45f3-bf55-b07eb2ee5618",
"path": "uploads/2026/05/abc.webp",
"name": "abc.jpg",
"mime": "image/jpeg",
"type": "image",
"size": 12345,
"compress_info": null,
"title": "...",
"description": null,
"note": null,
"is_library": false,
"created_at": "2026-05-29T...Z",
"updated_at": null,
"created_by": "...",
"updated_by": null
}
}
```
Lấy URL hiển thị (Presigned URL)
---------------------------------
Ghi chú quan trọng: trường `thumbnail` chỉ chứa metadata của file — *không* trả presigned URL trực tiếp trong object `thumbnail`.
Để hiển thị ảnh, FE cần lấy link truy cập:
- Single link: `GET /api/v1/files/{id}/link?mode=view&expires=3600` → trả link tạm thời.
- Batch link (khuyến nghị cho list): `POST /api/v1/files/batch-link` với body `{ "file_ids": ["<id1>","<id2>"] , "mode": "view" }` để lấy nhiều link trong 1 request.
Gợi ý triển khai FE
-------------------
- Khi render trang danh sách bài (có nhiều post), thu thập tất cả `thumbnail_id` không-null, gọi `POST /api/v1/files/batch-link` một lần để lấy URL cho các thumbnail, sau đó map `file_id``url` và gán `thumbnail_url` cho từng post.
- Khi render chi tiết bài, nếu `thumbnail` tồn tại có thể gọi `GET /api/v1/files/{id}/link` để lấy URL.
- Nếu `thumbnail_id``null` hoặc backend trả `thumbnail: null`, hiển thị placeholder (ví dụ: ảnh mặc định hoặc biểu tượng).
- Lưu cache URL ngắn hạn theo `expires_in` trả về; refresh khi hết hạn.
Edge cases / Behaviour
----------------------
- Nếu file bị xóa (soft-delete) hoặc không tồn tại, `thumbnail` sẽ không được trả (backend dùng `where: { deleted_at: null }`).
- Migration đặt `ON DELETE SET NULL` nên nếu file bị xóa cứng thumbnail_id sẽ tự động trở thành NULL.
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -67,13 +67,13 @@ async function generate() { ...@@ -67,13 +67,13 @@ async function generate() {
const spec = generator.generateDocument({ const spec = generator.generateDocument({
openapi: '3.0.3', openapi: '3.0.3',
info: { info: {
title: 'BEKIND API', title: 'SSO VietProDev API',
version: '1.0.0', version: '1.0.0',
description: 'BEKIND backend REST API. Spec is generated from Zod contracts — do not edit openapi.json manually.', description: 'SSO VietProDev backend REST API. Spec is generated from Zod contracts — do not edit openapi.json manually.',
}, },
servers: [ servers: [
{ url: '/', description: 'Current host' }, { url: '/', description: 'Current host' },
{ url: '/bekind', description: 'BEKIND API' }, { url: '/api/v1', description: 'SSO VietProDev API' },
], ],
}); });
......
This diff is collapsed.
This diff is collapsed.
{
"id": "sso-vietprodev-backend-env",
"name": "SSO VietProDev Backend",
"values": [
{
"key": "baseUrl",
"value": "http://localhost:3001",
"type": "default",
"enabled": true,
"description": "Base URL of the backend server"
},
{
"key": "clientId",
"value": "sso-client",
"type": "default",
"enabled": true,
"description": "OIDC client ID for Client Credentials flow"
},
{
"key": "clientSecret",
"value": "sso-client-secret-dev",
"type": "secret",
"enabled": true,
"description": "OIDC client secret for Client Credentials flow"
},
{
"key": "username",
"value": "testuser@vietprodev.com",
"type": "default",
"enabled": true,
"description": "Test user email for Password Grant flow"
},
{
"key": "password",
"value": "TestPass@123",
"type": "secret",
"enabled": true,
"description": "Test user password for Password Grant flow"
},
{
"key": "accessToken",
"value": "",
"type": "secret",
"enabled": true,
"description": "Access token — populated by Auth requests automatically"
},
{
"key": "refreshToken",
"value": "",
"type": "secret",
"enabled": true,
"description": "Refresh token — populated by Auth requests automatically"
},
{
"key": "userId",
"value": "",
"type": "default",
"enabled": true,
"description": "User ID — populated by Create User and Get User requests"
},
{
"key": "roleId",
"value": "",
"type": "default",
"enabled": true,
"description": "Role ID — populated by Role requests"
},
{
"key": "stagingUrl",
"value": "https://sso-staging.vietprodev.com",
"type": "default",
"enabled": false,
"description": "Staging environment base URL"
},
{
"key": "productionUrl",
"value": "https://sso.vietprodev.com",
"type": "default",
"enabled": false,
"description": "Production environment base URL"
}
],
"_postman_variable_scope": "collection",
"_postman_exported_at": "2026-06-12T00:00:00.000Z",
"_postman_exported_using": "Postman/10.26.0"
}
This diff is collapsed.
-- ============================================================================= -- =============================================================================
-- BeKind Backend — Auth Schema v3 -- SSO VietProDev Backend — Auth Schema v3
-- Tables: users, user_auth, roles, permissions, -- Tables: users, user_auth, roles, permissions,
-- user_roles, role_permissions, user_sessions, password_reset_tokens, email_verify_tokens, -- user_roles, role_permissions, user_sessions, password_reset_tokens, email_verify_tokens,
-- auth_audit_logs, refresh_token_audit_logs -- auth_audit_logs, refresh_token_audit_logs
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
-- Change billing_period from DATE to VARCHAR(7) to store as yyyy-mm format
ALTER TABLE bills ALTER COLUMN billing_period TYPE VARCHAR(7);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment