Authentication
ระบบรองรับ 2 auth scheme — JWT (Bearer) สำหรับ end-user และ API Key (X-API-Key) สำหรับ external system
JWT Flow
1. Login
POST /api/v1/auth/login
Content-Type: application/json
{ "email": "admin@example.com", "password": "..." }ภายใน (จาก auth.service.ts):
findUnique({ email: email.toLowerCase().trim() })- ถ้าไม่เจอ /
status !== 'active'→401 Unauthorized bcrypt.compare(password, user.passwordHash)→ ถ้า fail →401update({ lastLoginAt: new Date() })generateTokens(user)— sign JWT
Response:
{
"user": {
"id": "uuid",
"email": "admin@example.com",
"firstName": "...",
"lastName": "...",
"role": "admin",
"language": "th",
"tenantId": "uuid",
"adminModules": ["users", "roles", "..."],
"adminModulesWrite": ["users", "..."]
},
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc..."
}2. Use access token
ทุก request ต่อมา:
GET /api/v1/items
Authorization: Bearer eyJhbGc...JwtAuthGuard (global default) verify token → attach payload ที่ request.user
3. JWT Payload Structure
จาก interfaces/jwt-payload.interface.ts:
{
sub: string; // user.id (standard JWT claim)
email: string;
role: string; // 'admin' | 'manager' | 'supervisor' | 'operator' | 'viewer'
tenantId: string | null;
adminModules: string[]; // per-module READ permission
adminModulesWrite: string[]; // per-module WRITE permission
iat: number;
exp: number;
}adminModules / adminModulesWrite คือกุญแจสำคัญของ per-module permission — RolesGuard อ่านสองช่องนี้ตรง ๆ
Token TTLs
ตั้งใน env (validate ด้วย Joi ใน app.module.ts):
| Env var | Default | บทบาท |
|---|---|---|
JWT_ACCESS_TTL | 900 วินาที (15 นาที) | access token expire |
JWT_REFRESH_TTL | 604800 วินาที (7 วัน) | refresh token expire |
JWT signed ด้วย JWT_SECRET (HS256) — ต้องอย่างน้อย 16 ตัว (Joi enforce)
JWT_SECRET ไม่มี default
ถ้า JWT_SECRET ไม่ถูกตั้ง → app refuse to start (จาก AuthModule):
if (!secret) {
throw new Error('FATAL: JWT_SECRET is not set...');
}Refresh Token
- Issue พร้อม access token ตอน login/register
- Sign ด้วย
JWT_SECRETเดียวกัน, expireJWT_REFRESH_TTL - ใช้ POST refresh endpoint เพื่อแลก access ใหม่ (รายละเอียดดูที่ Swagger)
Password Hashing
จาก auth.service.ts:
const SALT_ROUNDS = 12;
const passwordHash = await bcrypt.hash(dto.password, SALT_ROUNDS);- Algorithm: bcrypt
- Cost factor: 12 (≈ 250ms ต่อ hash บน laptop ปี 2024)
- เก็บเป็น string ที่ column
user.password_hash - Compare ด้วย
bcrypt.compare(plain, hash)— constant time
Account Lockout
user.status field:
| ค่า | ความหมาย |
|---|---|
active | login ได้ปกติ |
inactive | admin disable แล้ว → 401 |
locked | failed attempts เกิน threshold (manual ปลดล็อก) → 401 |
Login ตอนนี้ throw 401 'Account is inactive or locked' ถ้า status ไม่ใช่ active
API Key Authentication
ใช้กับ /api/v1/integration/v1/* เท่านั้น (ApiKeyGuard ติดที่ controller-level)
Header formats (รองรับทั้งสอง)
X-API-Key: wms_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxหรือ
Authorization: ApiKey wms_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxHashing at Rest
จาก ApiKey model:
keyHash— SHA-256 ของ raw key (เก็บ DB)keyPrefix— 8-10 ตัวแรก เพื่อ identify ใน UI (เช่นwms_abcd…)- Raw key คืนให้ user ครั้งเดียว ตอนสร้าง — ไม่ recover ได้
// pseudo from api-keys.service.ts
const rawKey = `wms_${crypto.randomBytes(32).toString('hex')}`;
const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');
await prisma.apiKey.create({ data: { keyHash, keyPrefix: rawKey.slice(0, 8), ... } });
return { ...record, rawKey }; // ⚠️ shown only this onceValidation flow (ApiKeyGuard.canActivate())
- Extract key จาก header
- Hash → lookup
api_keyที่keyHash - เช็ค
active === true, ไม่ expired (expiresAt) - เช็ค scope (
@ApiKeyScopes('write')decorator vskey.scopes) - เช็ค IP whitelist (exact หรือ CIDR — อ่าน
X-Forwarded-For) - เช็ค allowed origins (เฉพาะถ้ามี Origin header)
- เช็ค rate limit (sliding 60s window, in-memory)
- Attach
request.apiKey+ log async ลงapi_key_request
ดูรายละเอียดเต็ม: Push Integration API
Default Seed User
prisma/seed.ts สร้าง:
- 1 tenant: code =
DEFAULT - 1 admin user (email + password ดูที่ comment ในไฟล์ — เปลี่ยนตอน deploy production)
WARNING
Production deploy ต้อง:
- เปลี่ยนรหัสผ่าน seed admin ทันที
- ตั้ง
JWT_SECRETที่ random + ยาว (≥ 32 chars) - ห้าม commit
.envเข้า repo
Logout
ฝั่ง backend ไม่มี state — logout = clear localStorage ที่ frontend (หากต้องการ revoke list ในอนาคต ให้ใช้ refresh token rotation + Redis blocklist)