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

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

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


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://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://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://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://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://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://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://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://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://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 พร้อมระบุข้อมูลต่อไปนี้


Generated 2026-06-06 05:32 · ImpAcc User Profile API