Skip to content

Authentication

ระบบรองรับ 2 auth scheme — JWT (Bearer) สำหรับ end-user และ API Key (X-API-Key) สำหรับ external system

JWT Flow

1. Login

http
POST /api/v1/auth/login
Content-Type: application/json

{ "email": "admin@example.com", "password": "..." }

ภายใน (จาก auth.service.ts):

  1. findUnique({ email: email.toLowerCase().trim() })
  2. ถ้าไม่เจอ / status !== 'active'401 Unauthorized
  3. bcrypt.compare(password, user.passwordHash) → ถ้า fail → 401
  4. update({ lastLoginAt: new Date() })
  5. generateTokens(user) — sign JWT

Response:

json
{
  "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 ต่อมา:

http
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:

typescript
{
  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 varDefaultบทบาท
JWT_ACCESS_TTL900 วินาที (15 นาที)access token expire
JWT_REFRESH_TTL604800 วินาที (7 วัน)refresh token expire

JWT signed ด้วย JWT_SECRET (HS256) — ต้องอย่างน้อย 16 ตัว (Joi enforce)

JWT_SECRET ไม่มี default

ถ้า JWT_SECRET ไม่ถูกตั้ง → app refuse to start (จาก AuthModule):

typescript
if (!secret) {
  throw new Error('FATAL: JWT_SECRET is not set...');
}

Refresh Token

  • Issue พร้อม access token ตอน login/register
  • Sign ด้วย JWT_SECRET เดียวกัน, expire JWT_REFRESH_TTL
  • ใช้ POST refresh endpoint เพื่อแลก access ใหม่ (รายละเอียดดูที่ Swagger)

Password Hashing

จาก auth.service.ts:

typescript
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:

ค่าความหมาย
activelogin ได้ปกติ
inactiveadmin disable แล้ว → 401
lockedfailed 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 (รองรับทั้งสอง)

http
X-API-Key: wms_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

หรือ

http
Authorization: ApiKey wms_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Hashing at Rest

จาก ApiKey model:

  • keyHashSHA-256 ของ raw key (เก็บ DB)
  • keyPrefix — 8-10 ตัวแรก เพื่อ identify ใน UI (เช่น wms_abcd…)
  • Raw key คืนให้ user ครั้งเดียว ตอนสร้าง — ไม่ recover ได้
typescript
// 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 once

Validation flow (ApiKeyGuard.canActivate())

  1. Extract key จาก header
  2. Hash → lookup api_key ที่ keyHash
  3. เช็ค active === true, ไม่ expired (expiresAt)
  4. เช็ค scope (@ApiKeyScopes('write') decorator vs key.scopes)
  5. เช็ค IP whitelist (exact หรือ CIDR — อ่าน X-Forwarded-For)
  6. เช็ค allowed origins (เฉพาะถ้ามี Origin header)
  7. เช็ค rate limit (sliding 60s window, in-memory)
  8. 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 ต้อง:

  1. เปลี่ยนรหัสผ่าน seed admin ทันที
  2. ตั้ง JWT_SECRET ที่ random + ยาว (≥ 32 chars)
  3. ห้าม commit .env เข้า repo

Logout

ฝั่ง backend ไม่มี state — logout = clear localStorage ที่ frontend (หากต้องการ revoke list ในอนาคต ให้ใช้ refresh token rotation + Redis blocklist)

เผยแพร่ภายใต้ Digital Outsourcing