คู่มือ API โปรไฟล์ผู้ใช้ — สำหรับนักพัฒนา

API นี้ให้แอปพลิเคชันภายนอกตรวจสอบสิทธิ์ผู้ใช้กับฐานข้อมูล ImpAcc และอ่านข้อมูล โปรไฟล์ของผู้ใช้รายนั้น ใช้ระบบ bearer token ของ Laravel Sanctum

โทเค็นที่ออกจาก /api/login จะไม่หมดอายุอัตโนมัติ หากต้องการยกเลิก ให้เรียก /api/logout

เวอร์ชัน: คู่มือนี้อธิบาย v1 (endpoint /api/* แบบปกติ) ส่วนเวอร์ชัน v2 ที่เพิ่มความปลอดภัย (opt-in) อยู่ภายใต้ /api/v2/* — โทเค็นมีวันหมดอายุ, จำกัดอัตราการเรียก, ตรวจสอบข้อมูลเข้มงวด, บันทึก audit — ดู ข้อ 8 ส่วน v3 (/api/v3/*) เพิ่ม การเข้ารหัส payload ของ response ต่อยอดจาก v2 — ดู ข้อ 9


1. ขั้นตอนการยืนยันตัวตน

            ┌──────────────┐  1. POST /api/login    ┌────────────────┐
            │              │  citizen_id + password │                │
            │  แอปของคุณ   │ ─────────────────────► │  API โปรไฟล์   │
            │  (ภายนอก)    │ ◄───────────────────── │ (usr.impacc)   │
            │              │     bearer token       │                │
            └──────────────┘                        └────────────────┘
                  │   2. เก็บ token (memory / secure storage)
                  │
                  │   3. ทุกคำขอครั้งต่อไปแนบ header:
                  │      Authorization: Bearer <token>
                  ▼
            GET  /api/profile
            GET  /api/permissions
            POST /api/logout

วิธีขอ bearer token มี 2 ทาง:

ทั้งสองทางคืนค่า { token, token_type, user } รูปแบบเดียวกัน ขั้นตอนหลังจากนั้นเหมือนกันทุกประการ

Header ที่ต้องส่ง

Header ค่า เมื่อไร
Accept application/json ทุกครั้ง
Content-Type application/json เมื่อ POST/PUT
Authorization Bearer <token> เมื่อเรียก EP ที่ป้องกัน

หากไม่ส่ง Accept: application/json Laravel อาจตอบกลับเป็นหน้า HTML แทน JSON เมื่อยืนยันตัวตนล้มเหลว ต้องส่ง header นี้เสมอ


2. รายการ Endpoint

2.1 POST /api/login

แลกเปลี่ยน credentials เป็น bearer token

ฟิลด์ใน Request body

ฟิลด์ ชนิด จำเป็น คำอธิบาย
citizen_id string ใช่ เลขประจำตัวประชาชน 13 หลักของผู้ใช้
password string ใช่ รหัสผ่านของผู้ใช้ (ส่งผ่าน HTTPS)
device_name string ไม่ ป้ายกำกับบันทึกไว้กับ token เช่น "mobile-app-ios"

ตัวอย่าง

curl -X POST https://uat-usr.impacc.work/api/login \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
        "citizen_id":  "1234567890123",
        "password":    "user-secret",
        "device_name": "partner-app-web"
      }'

สำเร็จ — 200 OK

{
  "token":      "12|H7c0gQ9b3l...redacted...XyZ",
  "token_type": "Bearer",
  "user": {
    "id":                1234,
    "citizen_id":        "1234567890123",
    "title":             "นาย",
    "firstname":         "สมชาย",
    "lastname":          "ใจดี",
    "email":             "somchai@example.com",
    "mobile":            "0812345678",
    "workgroup_id":      39,
    "division_id":       2,
    "status":            "1",
    "roles":             ["user"],
    "last_login":        "2026-05-19T08:42:11.000000Z",
    "current_profile":   { "...": "ดูโครงสร้างใน profiles[] ด้านล่าง" },
    "profiles": [
      {
        "id":                  1,
        "label":               "ต้นสังกัด",
        "kind":                "home",
        "dept1":               "สำนักงาน ป.ป.ท.",
        "dept2":               "สำนักงาน ป.ป.ท.",
        "dept3":               null,
        "officer_type_id":     4,
        "officer_type_name":   "ทั่วไป",
        "position_type_id":    3,
        "position_type_name":  "วิชาการ",
        "position":            "นักวิชาการ",
        "level":               "ปฏิบัติการ",
        "is_default":          true,
        "is_active":           true,
        "is_temporary":        false,
        "status":              "approved"
      },
      {
        "id":                  4,
        "label":               "คณะทำงานพิเศษ",
        "kind":                "mission",
        "dept1":               "รองเลขาธิการ ป.ป.ท. (คนที่ 1)",
        "dept2":               "งานรองเลขาธิการ ป.ป.ท. (คนที่ 1)",
        "officer_type_name":   "เจ้าหน้าที่ ป.ป.ท.",
        "is_default":          false,
        "is_active":           false,
        "is_temporary":        true,
        "mission_title":       "สืบสวน",
        "mission_note":        "ช่วยราชการคณะทำงาน X",
        "mission_start_date":  "2026-01-01",
        "mission_end_date":    "2026-06-30",
        "mission_order_no":    "123/2569",
        "mission_order_file":  "orders/2569-123.pdf",
        "status":              "approved"
      }
    ]
  }
}

profiles[] ถูกเรียงโดยให้โปรไฟล์ที่กำลังใช้งาน (is_active=true) ขึ้นก่อน ตามด้วยโปรไฟล์หลัก (is_default=true) ส่วนที่เหลือเรียงตาม id current_profile คือทางลัดชี้ไปยังแถวเดียวกันกับที่ผู้ใช้กำลังใช้งาน อยู่ (ถ้าไม่มี is_active จะ fallback เป็น is_default)

หากผู้ใช้ยังไม่มีแถวใน user_profiles เลย ค่า current_profile จะเป็น null และ profiles[] จะเป็น array ว่าง

อ็อบเจกต์ user ยังมีบล็อก permissions ด้วย — คือสิทธิ์การเข้าถึง แอปพลิเคชัน/เมนู/ส่วนย่อย ที่ผู้ใช้มีจริง โครงสร้างอธิบายไว้ใน หัวข้อ 2.4 GET /api/permissions ตัวอย่างด้านบน ตัดบล็อกนี้ออกเพื่อความกระชับ แต่จะมีอยู่เสมอในการตอบกลับจริง

เก็บ token ไว้ในที่ปลอดภัยและส่งใน header Authorization: Bearer <token> ทุกครั้งที่เรียก endpoint ที่ต้องยืนยันตัวตน

ล้มเหลว — 422 Unprocessable Entity (credential ไม่ถูกต้องหรือบัญชีถูกปิด)

{
  "message": "The provided credentials are incorrect.",
  "errors": {
    "citizen_id": ["The provided credentials are incorrect."]
  }
}

ล้มเหลว — 422 Unprocessable Entity (ส่งข้อมูลไม่ครบ)

{
  "message": "The citizen id field is required. (and 1 more error)",
  "errors": {
    "citizen_id": ["The citizen id field is required."],
    "password":   ["The password field is required."]
  }
}

2.2 GET /api/profile

ดึงข้อมูลโปรไฟล์ของผู้ใช้ที่ล็อกอินอยู่ อ็อบเจกต์ user เหมือนกับที่ได้จาก POST /api/login ทุกประการ — รวมถึง current_profile, profiles[] ทั้งหมด และบล็อก permissions (ดูหัวข้อ 2.4) ตัวอย่างด้านล่างแสดงเฉพาะฟิลด์ระดับผู้ใช้ เพื่อความกระชับ

Headers

Accept: application/json
Authorization: Bearer <token>

ตัวอย่าง

curl https://uat-usr.impacc.work/api/profile \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 12|H7c0gQ9b3l...XyZ"

สำเร็จ — 200 OK (โครงสร้างเหมือนฟิลด์ user ใน login response)

{
  "user": {
    "id":                1234,
    "citizen_id":        "1234567890123",
    "firstname":         "สมชาย",
    "lastname":          "ใจดี",
    "email":             "somchai@example.com",
    "mobile":            "0812345678",
    "workgroup":         "IT",
    "workgroup_id":      39,
    "division_id":       2,
    "organization":      "Impacc",
    "status":            "1",
    "roles":             ["user"],
    "last_login":        "2026-05-19T08:42:11.000000Z"
  }
}

ล้มเหลว — 401 Unauthenticated (ไม่มี token, token ผิด หรือถูกยกเลิก)

{ "message": "Unauthenticated." }

2.3 POST /api/logout

ยกเลิก bearer token ที่ใช้กับ request นี้ หลังเรียกแล้ว token จะใช้ไม่ได้อีก

Headers

Accept: application/json
Authorization: Bearer <token>

ตัวอย่าง

curl -X POST https://uat-usr.impacc.work/api/logout \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 12|H7c0gQ9b3l...XyZ"

สำเร็จ — 200 OK

{ "message": "Logged out" }

2.4 GET /api/permissions

คืนค่า สิทธิ์การเข้าถึงที่มีผลจริง ของผู้ใช้ที่ล็อกอินอยู่ ครอบคลุม 3 ระดับ: แอปพลิเคชัน → เมนู → ส่วนย่อย (section) อ็อบเจกต์เดียวกันนี้ ถูกฝังไว้เป็น user.permissions ในการตอบกลับของ login และ profile ด้วย endpoint นี้คืนเฉพาะอ็อบเจกต์นั้นเมื่อคุณต้องการแค่ข้อมูลสิทธิ์

วิธีคำนวณสิทธิ์

สิทธิ์จะ มีผล กับผู้ใช้เมื่อได้รับอนุญาตในระดับนั้นและไม่ถูกบล็อก:

สิทธิ์ที่มีผล = ( สิทธิ์จากกลุ่มของผู้ใช้ )        ← กลุ่มมาจากทั้งสองทาง
            ∪ ( สิทธิ์ที่ให้ผู้ใช้โดยตรง )           users.workgroup_id → workgroup_groups
            ∖ ( รายการบล็อกของผู้ใช้ )               และการเป็นสมาชิกโดยตรงใน user_groups

เมนูจะซ้อนอยู่ใต้แอปก็ต่อเมื่อแอปนั้นมีผลกับผู้ใช้ และ section จะซ้อนอยู่ ใต้เมนูที่มีผลในทำนองเดียวกัน

ระดับ section มีอยู่ใน schema แต่ปัจจุบันยังไม่มีข้อมูล ดังนั้น sections จึงเป็น [] สำหรับทุกเมนูในตอนนี้ และจะถูกเติมอัตโนมัติเมื่อมีการกำหนด สิทธิ์ section — ฝั่ง client ไม่ต้องแก้ไขใด ๆ

Headers

Accept: application/json
Authorization: Bearer <token>

ตัวอย่าง

curl https://uat-usr.impacc.work/api/permissions \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 12|H7c0gQ9b3l...XyZ"

สำเร็จ — 200 OK

{
  "permissions": {
    "group_ids": [1, 2, 3, 22, 23],
    "applications": [
      {
        "id":     22,
        "app_id": "11-22",
        "name":   "ระบบDIDC",
        "link":   "/miniapp/didc",
        "menus": [
          {
            "id":     81,
            "name":   "จัดการ Dashboard",
            "path":   "/dashboard-management",
            "level":  2,
            "parent": 75,
            "sections": []
          }
        ]
      },
      {
        "id":     13,
        "app_id": "11-13",
        "name":   "ระบบประชาสัมพันธ์-ภายนอก",
        "link":   "/miniapp/xcms",
        "menus": []
      }
    ]
  }
}

แอปที่ไม่มีเมนูที่มีผลจะคืนค่า "menus": [] (ผู้ใช้เข้าถึงแอปได้แต่ยังไม่มี เมนูใดถูกให้สิทธิ์/มีผลใต้แอปนั้น)

ล้มเหลว — 401 Unauthenticated

{ "message": "Unauthenticated." }

2.5 POST /api/sso/exchange

การแลกโทเค็นแบบ Single Sign-On — เป็น ทางเลือกแทน POST /api/login สำหรับแอปที่ถูกเปิดจากพอร์ทัล imPACC (impacc.work) โดยไม่ต้องรับรหัสผ่าน

เมื่อผู้ใช้ที่ล็อกอินพอร์ทัลอยู่แล้วเปิดแอปที่เชื่อมโยง พอร์ทัลจะสร้าง mToken แบบใช้ครั้งเดียวบนข้อมูลผู้ใช้ และ redirect เบราว์เซอร์มาที่แอป พร้อมโทเค็นนั้น (เช่น https://your-app?appId=...&mToken=...) แอปจะ POST mToken มาที่นี่เพื่อรับ bearer token และข้อมูลโปรไฟล์ โดยไม่ต้องจัดการ credential ของผู้ใช้

mToken ใช้ได้ครั้งเดียว จะถูกล้างทันทีที่แลกสำเร็จครั้งแรก จึงนำมาใช้ซ้ำ ไม่ได้ พอร์ทัลจะสร้าง mToken ใหม่ทุกครั้งที่เปิดแอป ควรแลกโดยเร็ว

ฟิลด์ใน Request body

ฟิลด์ ชนิด จำเป็น คำอธิบาย
mToken string ใช่ โทเค็นครั้งเดียวจากการ redirect ของพอร์ทัล
device_name string ไม่ ป้ายกำกับบันทึกไว้กับ token เช่น "sso-web"

ตัวอย่าง

curl -X POST https://uat-usr.impacc.work/api/sso/exchange \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "mToken": "0f3a…โทเค็น 64 ตัว…d9", "device_name": "sso-web" }'

สำเร็จ — 200 OK

โครงสร้างเหมือน POST /api/login ทุกประการ — มี token, token_type และ อ็อบเจกต์ user แบบเต็ม (รวม current_profile, profiles[] และบล็อก permissions จากหัวข้อ 2.4):

{
  "token":      "9|iU7SMJ2TjJ7feNnG…redacted…",
  "token_type": "Bearer",
  "user": { "id": 1, "citizen_id": "…", "permissions": { "…": "ดูหัวข้อ 2.4" } }
}

ล้มเหลว — 422 Unprocessable Entity (mToken ไม่ถูกต้อง ถูกใช้ไปแล้ว หมดอายุ หรือบัญชีถูกปิด)

{
  "message": "Invalid or expired SSO token.",
  "errors": { "mToken": ["Invalid or expired SSO token."] }
}

หลังแลกสำเร็จ ให้ใช้ token ที่ได้เหมือนกับโทเค็นจาก /login ทุกประการ คือแนบ Authorization: Bearer <token> ในทุกคำขอถัดไป และเรียก POST /api/logout เพื่อเพิกถอนโทเค็น


3. คำอธิบายฟิลด์ในโปรไฟล์

3.1 ฟิลด์ระดับผู้ใช้

ฟิลด์ ชนิด คำอธิบาย
id integer รหัสภายในระบบ
citizen_id string เลขประจำตัวประชาชน 13 หลัก ใช้เป็น identifier หลัก
title string คำนำหน้าภาษาไทย (นาย / นาง / นางสาว …)
firstname string ชื่อจริงภาษาไทย
lastname string นามสกุลภาษาไทย
title_english string คำนำหน้าภาษาอังกฤษ
firstname_english string ชื่อจริงภาษาอังกฤษ
lastname_english string นามสกุลภาษาอังกฤษ
email string อีเมล
mobile string เบอร์มือถือ
born_date date YYYY-MM-DD
workgroup string กลุ่มงาน / ทีม (จาก row users)
workgroup_id integer/null FK ตาราง workgroups ใช้คำนวณกลุ่ม/สิทธิ์ (ดูหัวข้อ 2.4)
division_id integer/null FK ตาราง divisions — สังกัด/กองของผู้ใช้
organization string ชื่อหน่วยงาน (จาก row users)
role_type1..3 string ประเภทบทบาท (เนื้อหาขึ้นกับโปรเจกต์)
status string "1" = ใช้งานได้, ค่าอื่น = ถูกระงับ
roles string[] ชื่อ role จาก Spatie permission
last_login datetime เวลา login ล่าสุด (ISO‑8601)
current_profile object/null ทางลัด: โปรไฟล์ที่ผู้ใช้กำลังใช้งานอยู่ (มีโครงสร้างเหมือนรายการใน profiles[])
profiles[] object[] รายการสังกัด/ภารกิจทั้งหมดของผู้ใช้ ดูหัวข้อ 3.2
permissions object สิทธิ์แอป/เมนู/section ที่ผู้ใช้มีจริง ดูหัวข้อ 2.4 และ 3.3

password, remember_token และไฟล์อัปโหลดจะไม่ถูกส่งกลับ

3.2 ฟิลด์ของโปรไฟล์ (OU / ภารกิจพิเศษ)

แต่ละรายการใน profiles[] คือสังกัดหนึ่งของผู้ใช้

ฟิลด์ ชนิด คำอธิบาย
id integer รหัส row ของโปรไฟล์ (คงที่ระหว่าง request)
label string ป้ายกำกับ เช่น "ต้นสังกัด", "สังกัดเพิ่มเติม", "ภารกิจพิเศษ"
kind enum ตัวจำแนกเชิงโครงสร้าง: "home", "secondary", "mission"
dept1 string กรมหรือสำนัก (DivisionLevel1)
dept2 string กอง (DivisionLevel2)
dept3 string กลุ่มงาน (DivisionLevel3)
officer_type_id integer FK ตาราง officer_types
officer_type_name string ชื่อจาก officer_types
position_type_id integer FK ตาราง ds_positions_types
position_type_name string ชื่อจาก ds_positions_types
position string ชื่อตำแหน่ง
description string คำอธิบายตำแหน่ง
level string ระดับ เช่น "ปฏิบัติการ", "ชำนาญการ"
management_position string ตำแหน่งสายบริหาร (ถ้ามี)
is_default bool true คือต้นสังกัด มีได้เพียงแถวเดียวต่อผู้ใช้
is_active bool true คือโปรไฟล์ที่ผู้ใช้กำลังใช้งานในระบบ
is_temporary bool true แสดงว่าเป็นภารกิจพิเศษ มีฟิลด์ mission_* ประกอบ
mission_title string ชื่อภารกิจ (เช่น "สืบสวน")
mission_note string บันทึกเพิ่มเติม
mission_start_date date YYYY-MM-DD
mission_end_date date YYYY-MM-DD หลังจากนี้ถือว่าภารกิจสิ้นสุด
mission_order_no string เลขที่คำสั่ง
mission_order_file string path ของไฟล์คำสั่ง relative จาก storage
status string "approved", "pending", "ended", …
approved_at datetime เวลาที่อนุมัติ

การตีความ kind

kind เงื่อนไข ความหมาย
"home" is_default=true ต้นสังกัด/สังกัดถาวร
"mission" is_default=false, is_temporary=true ภารกิจพิเศษมีกำหนดเวลา
"secondary" is_default=false, is_temporary=false สังกัดเพิ่มเติมที่ไม่ใช่ภารกิจ

แอปที่ต้องการเฉพาะสังกัดหลักของผู้ใช้ สามารถใช้ current_profile ตรง ๆ ส่วนแอปที่ต้องแสดงรายการสังกัด (เช่น เมนู "สลับสังกัด") ให้วน profiles[]

3.3 ฟิลด์สิทธิ์ (permissions)

อ็อบเจกต์ permissions (คืนเดี่ยว ๆ จากหัวข้อ 2.4 และฝังอยู่ใน user.permissions) มีโครงสร้างดังนี้:

ฟิลด์ ชนิด คำอธิบาย
group_ids int[] ทุกกลุ่มที่ผู้ใช้สังกัด (จากกลุ่มงาน ∪ โดยตรง)
applications[] object[] แอปที่มีผล เรียงตาม sequence แล้วตาม id
applications[].id integer รหัสแอปภายใน (applications.id)
applications[].app_id string รหัสแอปภายนอก (เช่น "11-22")
applications[].name string ชื่อแอปที่แสดงผล
applications[].link string path/URL เข้าแอป (ถ้ามี)
applications[].menus[] object[] เมนูที่มีผลใต้แอปนี้ ([] ถ้าไม่มี)
menus[].id integer รหัสเมนูภายใน (menu_applications.id)
menus[].name string ชื่อเมนูที่แสดงผล
menus[].path string path/route ของเมนู
menus[].level int/null ระดับชั้นของเมนู (1 = ระดับบนสุด)
menus[].parent int/null รหัสเมนูแม่ ใช้จัดซ้อนใน sidebar
menus[].sections[] object[] section ที่มีผลใต้เมนูนี้ ([] จนกว่าจะมีข้อมูล section)
sections[].id integer รหัส section ภายใน (section_menus.id)
sections[].name string ชื่อ section ที่แสดงผล

จะปรากฏเฉพาะรายการที่ ได้รับสิทธิ์และไม่ถูกบล็อก เท่านั้น การที่ไม่มี แอป เมนู หรือ section ใด หมายความว่าผู้ใช้ไม่มีสิทธิ์เข้าถึงรายการนั้น แนะนำให้สร้างชุด "path ที่อนุญาต" จาก menus[].path ไว้ใช้คุม route guard ฝั่ง client


4. รหัสสถานะ HTTP

สถานะ ความหมาย
200 สำเร็จ
401 ไม่มี/token ผิด/ถูกยกเลิก — ให้ผู้ใช้ login ใหม่
419 CSRF/session ไม่ตรง — มักเกิดจากไม่ใส่ Accept: application/json
422 Validation ผิด (credential ผิด / ส่งข้อมูลไม่ครบ)
429 เรียกเกินอัตราที่อนุญาต (ค่าเริ่มต้น 60 req/min/IP)
500 เซิร์ฟเวอร์ผิดพลาด — ให้บันทึก response body แล้วแจ้งทีม API

5. แนวทางการจัดการ Token


6. ตัวอย่างการเชื่อมต่อ

6.1 JavaScript (fetch)

async function login(citizenId, password) {
  const r = await fetch('https://uat-usr.impacc.work/api/login', {
    method:  'POST',
    headers: {
      'Accept':       'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      citizen_id:  citizenId,
      password,
      device_name: 'web-partner-app',
    }),
  });
  if (!r.ok) throw new Error(`Login failed: ${r.status}`);
  return r.json(); // { token, token_type, user }
}

async function getProfile(token) {
  const r = await fetch('https://uat-usr.impacc.work/api/profile', {
    headers: {
      'Accept':        'application/json',
      'Authorization': `Bearer ${token}`,
    },
  });
  if (r.status === 401) throw new Error('Token expired');
  return r.json();
}

6.2 PHP (Guzzle)

use GuzzleHttp\Client;

$http = new Client(['base_uri' => 'https://uat-usr.impacc.work/api/']);

$login = $http->post('login', [
    'headers' => ['Accept' => 'application/json'],
    'json'    => [
        'citizen_id'  => '1234567890123',
        'password'    => $userPassword,
        'device_name' => 'partner-backend',
    ],
]);
$token = json_decode((string) $login->getBody(), true)['token'];

$profile = $http->get('profile', [
    'headers' => [
        'Accept'        => 'application/json',
        'Authorization' => "Bearer {$token}",
    ],
]);

6.3 Python (requests)

import requests

BASE = "https://uat-usr.impacc.work/api"

def login(citizen_id, password):
    r = requests.post(f"{BASE}/login", json={
        "citizen_id":  citizen_id,
        "password":    password,
        "device_name": "partner-python",
    }, headers={"Accept": "application/json"})
    r.raise_for_status()
    return r.json()["token"]

def profile(token):
    r = requests.get(f"{BASE}/profile", headers={
        "Accept":        "application/json",
        "Authorization": f"Bearer {token}",
    })
    r.raise_for_status()
    return r.json()["user"]

7. การติดต่อทีมงาน

หากต้องการขอสิทธิ์เข้าใช้งานบน production หรือต้องการ allow‑list IP/โดเมน กรุณาติดต่อทีม API ของ ImpAcc พร้อมระบุข้อมูลต่อไปนี้


8. API v2 — เวอร์ชันเพิ่มความปลอดภัย (opt-in)

มีเวอร์ชัน v2 ที่เพิ่มความปลอดภัยให้เรียกใช้ภายใต้ prefix /api/v2 โดยใช้ สัญญา request/response เหมือน v1 ทุกประการ (ข้อ 2–3 ใช้ได้เหมือนเดิม รวมถึง payload user และบล็อก permissions) แต่เพิ่มมาตรการด้านล่าง v1 ไม่มีการเปลี่ยนแปลง — ระบบเดิมใช้งานได้ต่อ จะใช้ v2 ก็เพียงเปลี่ยน prefix ของ URL

Endpoint path v2
เข้าสู่ระบบ POST /api/v2/login
แลกโทเค็น SSO POST /api/v2/sso/exchange
โปรไฟล์ GET /api/v2/profile
สิทธิ์ GET /api/v2/permissions
ออกจากระบบ POST /api/v2/logout

8.1 สิ่งที่ v2 เพิ่มจาก v1

มาตรการ รายละเอียด
โทเค็นหมดอายุ bearer token หมดอายุใน 8 ชั่วโมง และ response มีฟิลด์ expires_in (วินาที) ส่วน v1 โทเค็นไม่หมดอายุ
ตรวจสอบเข้มงวด citizen_id ต้องเป็นตัวเลข 13 หลักพอดี (^[0-9]{13}$) ไม่งั้นได้ 422
จำกัดอัตราเรียก POST /api/v2/login5 ครั้ง/นาที/IP; POST /api/v2/sso/exchange10 ครั้ง/นาที/IP เกินกำหนดได้ 429
mToken มีอายุจำกัด mToken ถูกปฏิเสธหากเก่ากว่า 120 วินาที (นอกจากใช้ได้ครั้งเดียว) ควรแลกทันที
ตรวจสถานะถูกต้อง ใช้ฟิลด์ active (ไม่ใช่ status) ในการตรวจบัญชี จึงไม่ปฏิเสธผู้ใช้ที่ถูกต้องผิดพลาด
บันทึก audit ทุกการ login / sso-exchange / logout (ทั้งสำเร็จและล้มเหลว) ถูกบันทึกฝั่งเซิร์ฟเวอร์พร้อม IP และ user-agent

8.2 Token response (v2)

POST /api/v2/login และ POST /api/v2/sso/exchange คืนค่ารูปแบบเดียวกับ v1 บวกฟิลด์ expires_in:

{
  "token":      "42|nQlGT1UXax4Iu...redacted...",
  "token_type": "Bearer",
  "expires_in": 28800,
  "user": { "id": 1, "citizen_id": "...", "permissions": { "...": "ดูข้อ 2.4" } }
}

ใช้โทเค็นเหมือน v1 (Authorization: Bearer <token>) แต่ ต้องต่ออายุก่อน expires_in หมด เมื่อหมดอายุแล้ว การเรียก endpoint ที่ป้องกันจะได้ 401 และต้อง login / แลกโทเค็นใหม่

8.3 ตัวอย่าง

# v2 login (สังเกต citizen_id 13 หลัก และ prefix v2)
curl -X POST https://uat-usr.impacc.work/api/v2/login \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{ "citizen_id": "1234567890123", "password": "user-secret", "device_name": "v2-web" }'

# v2 SSO exchange
curl -X POST https://uat-usr.impacc.work/api/v2/sso/exchange \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{ "mToken": "0f3a…โทเค็น 64 ตัว…d9" }'

8.4 รหัสสถานะเพิ่มเติม (v2)

สถานะ ความหมาย (v2)
429 เรียกเกินอัตราที่กำหนด — รอแล้วลองใหม่
401 โทเค็นไม่มี/ไม่ถูกต้อง/หมดอายุ — login หรือแลกโทเค็นใหม่
422 ข้อมูลไม่ถูกต้อง, credential ผิด, บัญชีถูกปิด, หรือ mToken หมดอายุ/ถูกใช้แล้ว

9. API v3 — เข้ารหัส payload (opt-in)

v3 คือ ทุกอย่างใน v2 บวกการเข้ารหัส payload ของ response ให้เรียกใช้ภายใต้ prefix /api/v3 ทุก response ที่สำเร็จจะถูกส่งกลับเป็น envelope ที่เข้ารหัส แบบ hybrid ซึ่งถอดได้เฉพาะ client ที่เรียกเท่านั้น จึงปลอดภัยแม้ผ่านจุดที่ถอด TLS แล้ว (logging proxy, gateway ฯลฯ) ส่วน response ที่ผิดพลาด (4xx) ยังเป็น JSON ธรรมดา — v1 และ v2 ไม่เปลี่ยนแปลง

9.1 หลักการทำงาน

  1. client สร้าง คู่กุญแจ RSA แล้วลงทะเบียนกุญแจ สาธารณะ (public) → ได้ client_id
  2. client ส่ง X-Client-Id: <client_id> ในทุกคำขอ v3
  3. เซิร์ฟเวอร์เข้ารหัสแต่ละ response ด้วยกุญแจ AES-256-GCM ใหม่ แล้วห่อกุญแจนั้น ด้วย RSA public key ของ client (RSA-OAEP) ส่งกลับเป็น envelope
  4. client ถอดรหัส ด้วย private key เพื่อให้ได้ JSON แบบ v1/v2 ตามปกติ

private key ไม่เคยออกจากฝั่ง client หากไม่มี X-Client-Id ที่ถูกต้อง เซิร์ฟเวอร์ จะตอบ 400

9.2 Endpoint

Endpoint path v3 หมายเหตุ
ลงทะเบียนกุญแจ POST /api/v3/clients/register JSON ธรรมดา (เริ่มต้นใช้งาน)
เข้าสู่ระบบ POST /api/v3/login response เข้ารหัส
แลกโทเค็น SSO POST /api/v3/sso/exchange response เข้ารหัส
โปรไฟล์ GET /api/v3/profile response เข้ารหัส
สิทธิ์ GET /api/v3/permissions response เข้ารหัส
ออกจากระบบ POST /api/v3/logout response เข้ารหัส

9.3 รูปแบบ envelope

{
  "encrypted": true,
  "alg": "RSA-OAEP+A256GCM",
  "key": "<base64: RSA-OAEP(aes_key)>",
  "iv":  "<base64: nonce 12 ไบต์>",
  "tag": "<base64: GCM auth tag 16 ไบต์>",
  "ciphertext": "<base64: AES-256-GCM(plaintext_json)>"
}

การถอด = ใช้ private key ถอด key ด้วย RSA-OAEP → ได้กุญแจ AES แล้วถอด ciphertext ด้วย AES-256-GCM โดยใช้ iv+tag ผลลัพธ์คือ JSON เดียวกับ v1/v2

9.4 ตัวอย่างขั้นตอน

# 1. สร้างคู่กุญแจ
openssl genrsa -out client_priv.pem 2048
openssl rsa -in client_priv.pem -pubout -out client_pub.pem

# 2. ลงทะเบียน public key -> client_id
curl -X POST https://uat-usr.impacc.work/api/v3/clients/register \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg k "$(cat client_pub.pem)" '{name:"partner-app", public_key:$k}')"

# 3. เรียก endpoint v3 พร้อม X-Client-Id (response เป็น envelope)
curl -X POST https://uat-usr.impacc.work/api/v3/login \
  -H "Content-Type: application/json" -H "X-Client-Id: cli_..." \
  -d '{ "citizen_id":"1234567890123", "password":"user-secret" }'

ถอดรหัส — PHP

$env  = json_decode($responseBody, true);
$priv = openssl_pkey_get_private(file_get_contents('client_priv.pem'));
openssl_private_decrypt(base64_decode($env['key']), $aesKey, $priv, OPENSSL_PKCS1_OAEP_PADDING);
$json = openssl_decrypt(
    base64_decode($env['ciphertext']), 'aes-256-gcm', $aesKey, OPENSSL_RAW_DATA,
    base64_decode($env['iv']), base64_decode($env['tag'])
);
$data = json_decode($json, true);   // { token, expires_in, user, ... }

ถอดรหัส — Node.js

const crypto = require('crypto');
const env = JSON.parse(responseBody);
const aesKey = crypto.privateDecrypt(
  { key: fs.readFileSync('client_priv.pem'), padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
  Buffer.from(env.key, 'base64'));
const d = crypto.createDecipheriv('aes-256-gcm', aesKey, Buffer.from(env.iv, 'base64'));
d.setAuthTag(Buffer.from(env.tag, 'base64'));
const json = Buffer.concat([d.update(Buffer.from(env.ciphertext, 'base64')), d.final()]).toString('utf8');
const data = JSON.parse(json);

9.5 รหัสสถานะ (v3)

สถานะ ความหมาย
200 สำเร็จ — body เป็น envelope ที่เข้ารหัส (ต้องถอด)
400 ไม่มี X-Client-Id หรือไม่รู้จัก/ปิดใช้งาน
401 / 422 / 429 เหมือน v2 — ส่งกลับเป็น JSON ธรรมดา (ไม่เข้ารหัส error)

9.6 หมายเหตุ


Generated 2026-06-07 17:13 · ImpAcc User Profile API