Authentication
Hệ thống Boxme Scoring System API hỗ trợ 3 phương thức xác thực theo thứ tự ưu tiên:
| # | Phương thức | Header | Dùng cho | Ưu tiên |
|---|---|---|---|---|
| 1 | API Token | Authorization: Bearer blv_... hoặc X-API-Key: blv_... | Tích hợp hệ thống (S2S), Script, CI/CD | ⭐ Khuyên dùng |
| 2 | Supabase Session | Authorization: Bearer <jwt> | Frontend web app | Cho frontend |
| 3 | Legacy Header | X-User-Id: <id> | Hệ thống cũ | ⚠️ Sẽ bị gỡ |
Luồng xác thực (Authentication Flow)
flowchart TD
A[Request đến API] --> B{Có header Authorization hoặc X-API-Key?}
B -- "Bearer blv_..." --> C[Hash SHA-256 → Lookup api_tokens]
B -- "X-API-Key: blv_..." --> C
C --> D{Token tồn tại & is_active?}
D -- Không --> E["❌ 401 Token không hợp lệ"]
D -- Có --> F{Hết hạn?}
F -- Có --> G["❌ 401 Token đã hết hạn"]
F -- Không --> H["✅ Auth OK — set context"]
B -- Không có token --> I{Có X-User-Id?}
I -- Có --> J[Lookup users by ID]
J --> K{User active?}
K -- Có --> H
K -- Không --> L["❌ 401 User không tồn tại"]
I -- Không --> M{Bearer JWT (không phải blv_)?}
M -- Có --> N[Supabase getUser]
N --> O{Valid?}
O -- Có --> H
O -- Không --> P["❌ 401 Xác thực không hợp lệ"]
M -- Không --> P
1. API Token (Khuyên dùng cho Integration)
Phương thức an toàn nhất để tích hợp giữa các hệ thống. Token có tiền tố blv_ (52 ký tự) và được lưu dưới dạng hash SHA-256 — server không bao giờ lưu token gốc.
1.1. Tạo Token
Gọi API với quyền xác thực hiện tại (lần đầu có thể dùng X-User-Id):
# Tạo User Token
curl -X POST https://scoring.boxme.tech/api/tokens \
-H "Content-Type: application/json" \
-H "X-User-Id: 1" \
-d '{
"name": "WMS Integration",
"expires_in_days": 90
}'
Response (⚠️ Token chỉ hiển thị MỘT LẦN DUY NHẤT):
{
"success": true,
"id": 1,
"token": "blv_a1b2c3d4e5f6789012345678901234567890abcdef1234",
"prefix": "blv_a1b2",
"name": "WMS Integration",
"scope": "user",
"expires_at": "2026-05-27T07:00:00.000Z",
"message": "⚠️ Lưu lại token này ngay! Token chỉ hiển thị một lần duy nhất."
}
Hãy lưu token ngay lập tức vào biến môi trường hoặc secret manager. Sau khi response được trả về, không có cách nào lấy lại token gốc.
1.2. Sử dụng Token
Có 2 cách gửi token trong request:
# Cách 1: Authorization Bearer (Khuyên dùng — chuẩn OAuth 2)
curl https://scoring.boxme.tech/api/employees?status=active \
-H "Authorization: Bearer blv_a1b2c3d4e5f6..."
# Cách 2: X-API-Key header
curl https://scoring.boxme.tech/api/employees?status=active \
-H "X-API-Key: blv_a1b2c3d4e5f6..."
1.3. Master Token vs User Token
| Thuộc tính | User Token | Master Token |
|---|---|---|
| Scope | user | master |
| Quyền | Theo role của user tạo token | Full quyền (tương đương BOD) |
| Ai tạo được? | Mọi user đã xác thực | Chỉ user có role bod |
| user_id | Gắn với user cụ thể | NULL (system-wide) |
| Endpoint | POST /api/tokens | POST /api/tokens/master |
| Use case | Script cá nhân, integration riêng | System-to-system, CronJob toàn hệ thống |
# Tạo Master Token (chỉ BOD)
curl -X POST https://scoring.boxme.tech/api/tokens/master \
-H "Content-Type: application/json" \
-H "Authorization: Bearer blv_existing_bod_token" \
-d '{ "name": "CronJob Daily PPH Sync" }'
1.4. Quản lý Token
# Liệt kê token của bạn
curl https://scoring.boxme.tech/api/tokens \
-H "Authorization: Bearer blv_your_token"
# Thu hồi token (soft delete — set is_active = false)
curl -X DELETE https://scoring.boxme.tech/api/tokens/3 \
-H "Authorization: Bearer blv_your_token"
# Xác minh token hợp lệ (public endpoint — không cần auth header)
curl -X POST https://scoring.boxme.tech/api/tokens/verify \
-H "Content-Type: application/json" \
-d '{ "token": "blv_a1b2c3d4e5f6..." }'
Response /tokens/verify:
{
"valid": true,
"token_info": {
"id": 1,
"name": "WMS Integration",
"prefix": "blv_a1b2",
"scope": "user",
"user_id": 1,
"username": "admin",
"role": "bod",
"warehouse_id": null,
"expires_at": "2026-05-27T07:00:00.000Z"
}
}
2. Supabase Session (Frontend)
Dành cho ứng dụng frontend gọi API trực tiếp sau khi user đăng nhập qua Supabase Auth.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Bước 1: Đăng nhập
const { data } = await supabase.auth.signInWithPassword({
email: 'admin@boxme.asia',
password: '...'
})
// Bước 2: Lấy access token
const token = data.session.access_token
// Bước 3: Gọi API
const response = await fetch('https://scoring.boxme.tech/api/employees', {
headers: { 'Authorization': `Bearer ${token}` }
})
Middleware tự động xác thực JWT qua supabase.auth.getUser() và ánh xạ supabase_uid → users.id trong database.
3. Legacy Header (X-User-Id)
Phương thức đơn giản cho hệ thống cũ hoặc môi trường dev.
curl https://scoring.boxme.tech/api/employees \
-H "X-User-Id: 1"
Phương thức này không an toàn (không verificate danh tính thực) và sẽ bị gỡ trong phiên bản tương lai. Hãy chuyển sang API Token.
AuthContext (cho Developer Backend)
Sau khi middleware xác thực thành công, request handler nhận được AuthContext qua c.get('auth'):
interface AuthContext {
user_id: number // ID user trong bảng users
role: string // 'bod' | 'warehouse_manager' | 'warehouse_leader' | 'hr' | 'staff'
warehouse_id: number | null // null = toàn hệ thống (BOD/HR)
scope: 'user' | 'master' | 'session' | 'legacy'
token_id: number | null // ID trong bảng api_tokens (null nếu legacy/session)
}
Phân quyền theo role — sử dụng requireRole() middleware:
import { requireRole } from '../lib/auth-middleware'
// Chỉ BOD và HR mới được export payroll
app.get('/api/export/payroll', requireRole('bod', 'hr'), async (c) => {
const auth = c.get('auth')
// auth.user_id, auth.role, auth.warehouse_id sẵn sàng
})
Token có scope master sẽ bypass tất cả role check. Sử dụng cẩn thận cho CronJob và integration system-wide.
Ví dụ tích hợp End-to-End
Kịch bản: WMS đồng bộ PPH hàng ngày
# wms_sync.py — CronJob chạy lúc 1:00 AM mỗi ngày
import requests
import os
API_URL = "https://scoring.boxme.tech/api"
API_TOKEN = os.environ["BOXME_SCORING_TOKEN"] # blv_...
headers = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
# 1. Lấy danh sách nhân viên active
employees = requests.get(
f"{API_URL}/employees?status=active&limit=500",
headers=headers
).json()
# 2. Tính PPH từ WMS data
for emp in employees["data"]:
pph_data = calculate_pph_from_wms(emp["employee_code"])
# 3. Push violations tự động (nếu có)
if pph_data.get("violations"):
for v in pph_data["violations"]:
requests.post(f"{API_URL}/violations", headers=headers, json={
"employee_id": emp["id"],
"violation_type_id": v["type_id"],
"date": v["date"],
"penalty_points": v["points"],
"created_by": 1 # system user
})
Kịch bản: Webhook handler nhận token
// webhook-handler.js — Xác minh token trước khi xử lý webhook
app.post('/webhook/scoring', async (req, res) => {
const token = req.headers['x-api-key']
// Verify token với Scoring System
const verify = await fetch('https://scoring.boxme.tech/api/tokens/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
})
const result = await verify.json()
if (!result.valid) {
return res.status(401).json({ error: 'Invalid token' })
}
// Token hợp lệ — xử lý webhook
console.log(`Received from user: ${result.token_info.username}`)
})
Bảng mã lỗi Authentication
| HTTP Status | Error Message | Nguyên nhân | Cách xử lý |
|---|---|---|---|
401 | Token không hợp lệ hoặc đã bị thu hồi | Token sai, đã revoke, hoặc không tồn tại | Kiểm tra lại token, tạo token mới nếu cần |
401 | Token đã hết hạn | Token quá expires_at | Tạo token mới với POST /api/tokens |
401 | User không tồn tại hoặc đã bị vô hiệu hóa | User bị deactivate hoặc ID sai | Liên hệ admin để kích hoạt lại |
401 | Xác thực không hợp lệ | Không có header auth nào hợp lệ | Thêm header Authorization hoặc X-API-Key |
403 | Tài khoản đã bị vô hiệu hóa | Token hợp lệ nhưng user bị disable | Liên hệ admin |
403 | Bạn cần quyền ... để thực hiện thao tác này | Role không đủ quyền | Dùng token của user có role phù hợp hoặc master token |
Database Schema: api_tokens
CREATE TABLE api_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id), -- NULL cho master token
name TEXT NOT NULL, -- Tên mô tả token
token_hash TEXT NOT NULL UNIQUE, -- SHA-256 hash
token_prefix TEXT NOT NULL, -- 8 ký tự đầu (hiển thị)
scope TEXT DEFAULT 'user', -- 'user' | 'master'
permissions TEXT DEFAULT '{}', -- JSON permissions (reserved)
is_active BOOLEAN DEFAULT TRUE, -- FALSE = đã thu hồi
last_used_at DATETIME, -- Cập nhật mỗi lần sử dụng
expires_at DATETIME, -- NULL = không hết hạn
created_by INTEGER REFERENCES users(id),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Checklist tích hợp nhanh
- Tạo token qua
POST /api/tokens(hoặc/tokens/mastercho system token) - Lưu token vào Secret Manager / biến môi trường
- Gửi token qua
Authorization: Bearer blv_...trong mọi request - Xử lý HTTP 401 / 403 trong error handler
- Định kỳ rotate token (tạo mới → revoke cũ)