Design baseline · 24 SEP 2026 · 09 / 23
TDD · Login & quản lý Zalo
Current behavior · Dashboard authentication, phiên Zalo, QR, nhóm và transport.
Ranh giới và chủ sở hữu
Dashboard Express sở hữu xác thực người vận hành và BFF. TypeScript gateway sở hữu vòng đời session Zalo, credentials, listener, nhóm và gửi tin. Quarkus nhận event qua internal API để xử lý nghiệp vụ. Request internal gateway yêu cầu X-Internal-Token; browser đi qua BFF đã xác thực bằng session của ứng dụng.
POST /api/auth/login → cookie zalo_sessionĐiều kiện trước
- Dashboard có tenant logistics active; đăng nhập bằng tài khoản tenant hoặc admin hợp lệ. Tài khoản mặc định dùng
APP_LOGIN_PASSWORD; admin dùngAPP_ADMIN_PASSWORD. - Gateway cần
INTERNAL_TOKEN, kết nối Postgres và thư mục bí mật ghi được trongZALO_CREDENTIALS_DIR. Khi tạo session mới, gateway cần đăng ký scope với Quarkus. - Login QR mới tạo được khi session tồn tại và chưa CONNECTED. Đọc nhóm/gửi tin yêu cầu session đã kết nối.
- Session tự reconnect sau restart chỉ khi có credentials hợp lệ và
connect_on_start=true; logout đặt chính sách này false.
Contract xác thực ứng dụng
| Endpoint | Request / response quan sát từ source | Lỗi và biên |
|---|---|---|
POST /api/auth/login | {username,password}; trả {success:true,user}, đặt cookie zalo_session httpOnly, SameSite=Strict, Secure khi COOKIE_SECURE=true, TTL 30 ngày. | Thiếu trường: 400; sai thông tin: 401; sau 5 lần sai, request tiếp theo trong cửa sổ 2 phút trả 429 + Retry-After. Bộ đếm theo IP là in-memory. |
GET /api/auth/me | Đọc bearer token hoặc cookie; trả user đã sanitize. | Token không hợp lệ/hết hạn: 401. |
POST /api/auth/logout | Xóa token session, đóng websocket cùng token, xóa cookie. | Trả success kể cả khi không có session token. |
GET /api/sessions | BFF lấy session của tenant xác thực từ gateway; sau đó đọc status snapshot từng session. | Gateway không cấu hình/không kết nối: lỗi được proxy về; snapshot lỗi có thể rơi về metadata/status fallback. |
POST /api/users/{tenantId}/sessions | Body chỉ nhận tên hiển thị (1–120 ký tự); tenant suy ra từ principal, admin có thể chọn tenant theo kiểm tra BFF. | Sai tenant: 404; tên không hợp lệ: 400; gateway/core lỗi được proxy. |
POST /api/sessions/{id}/login/qr | BFF gọi internal gateway với tenant lấy từ session principal; gateway trả nhanh trạng thái QR_GENERATING. | Chưa chọn session khi nhiều session trong allowlist: 409; chưa có facade: 503; lỗi upstream: 502. |
Contract internal gateway cho QR/session
| Route | Hành vi / response | Lỗi chính |
|---|---|---|
GET /internal/v1/channel-sessions?tenantId=… | Yêu cầu internal token; liệt kê runtime session theo tenant. | 401 thiếu/sai token; 400 thiếu tenantId; 404 tenant ngoài active allowlist. |
POST /internal/v1/channel-sessions | Body {tenantId,name}; tạo sess_<uuid>, lưu metadata connect_on_start=false, đăng ký channel scope Quarkus, trả 201 + session. | 400 body sai; 403 tenant không được phép; 502 registration lỗi; 503 lifecycle/session creation lỗi. Nếu registration lỗi, session vừa tạo được soft-delete. |
POST /{id}/login/qr | Bắt đầu login bất đồng bộ; trả success và status hiện tại (thường QR_GENERATING, có thể đã tiến QR_WAITING nếu callback tới nhanh). | 400 thiếu tenant; 409 session đang connected; 404 không tìm thấy session; 503 lifecycle chưa nối. |
GET /{id}/status?tenantId=… | Trạng thái, profile, ảnh QR dạng data URI và scanned user (nếu có). Không trả cookie/IMEI/token login. | 400 thiếu tenant; 404 session không tồn tại/xóa; 503 lifecycle chưa nối. |
POST /{id}/login/qr/abort | Hủy QR, xóa QR state, status DISCONNECTED; connect_on_start=false. | 404 session không có; 503 lifecycle chưa nối. |
POST /{id}/logout | Dừng listener, flush album queue, đặt DISCONNECTED và connect_on_start=false; giữ file credentials. | 404 session không có; 503 lifecycle chưa nối. |
DELETE /{id} | Logout rồi soft-delete metadata thành tombstone DELETED. | 404 không tồn tại/đã xóa; credentials không bị xóa. |
GET /{id}/groups?tenantId=…&sessionId=… | ID path và query phải khớp; gọi getAllGroups() rồi getGroupInfo(); trả groupId/name/desc/member/avatar/time. | 400 scope fields sai; 404 tenant/session không được cấu hình; 502 lỗi provider hoặc session chưa connected. |
Luồng QR và trạng thái
Gateway chỉ giữ pollable QR state trong memory. Callback QR generated chuyển QR_WAITING và thêm data URI; scanned chuyển QR_SCANNED và thêm displayName/avatar; expired xóa QR và về DISCONNECTED; declined đặt ERROR và xóa QR. Khi login promise hoàn tất, adapter trích IMEI/cookie/user-agent/language từ SDK context, ghi file an toàn, lấy profile nếu được, đặt CONNECTED và gắn listener. Lỗi khi hoàn tất đặt ERROR.
| Từ trạng thái | Sự kiện | Tới trạng thái / side effect |
|---|---|---|
| DISCONNECTED / ERROR | Bắt đầu login QR | QR_GENERATING; tắt connect_on_start; QR cũ bị xóa. |
| QR_GENERATING | SDK phát QRCodeGenerated | QR_WAITING + data URI được hiển thị. |
| QR_WAITING | SDK báo QRCodeScanned | QR_SCANNED + scannedUser nếu QR state còn tồn tại. |
| QR_WAITING / QR_SCANNED | SDK báo hết hạn | DISCONNECTED, qrState=null. |
| QR_WAITING / QR_SCANNED | SDK bị từ chối | ERROR, qrState=null. |
| QR_SCANNED | SDK login resolve và ghi secret thành công | CONNECTED; lưu profile best-effort; connect_on_start=true; listener bắt đầu. |
| QR_* | Abort | DISCONNECTED; callback abort; generation tăng để callback cũ không ghi đè. |
| CONNECTED | Logout | DISCONNECTED; dừng listener; giữ credentials; connect_on_start=false. |
Bảo vệ credentials và tenant scope
- Credentials được xác thực trường tối thiểu (imei, userAgent, cookie), ghi file tạm exclusive, fsync, atomic rename; thư mục 0700, file 0600. Đường dẫn secret là
tenantId/sessionId/credentials.json; cả hai segment validate và symlink bị từ chối khi đọc/ghi. - Session registry lưu metadata/status, không giữ credential payload. Legacy metadata chỉ ánh xạ tenant/session; credentialsPath trong file legacy bị bỏ qua. Flat secret cũ chỉ được đọc nếu session ID xuất hiện đúng một lần trong registry/metadata; ID trùng tenant sẽ fail closed cho tới khi có secret tenant-scoped.
- Initial session import được lọc theo ACTIVE_TENANT_ID, ACTIVE_SESSION_SCOPES và tùy chọn selected tenant/session. Dynamic create kiểm tra active tenant và register scope với core.
- Gateway endpoint là internal, yêu cầu token. App API yêu cầu login; tenant suy ra server-side. Admin tenant override cần tenant logistics active. Với session CRUD, BFF forward tenant đã suy ra thay vì tin tenant trong client body.
Transport lân cận: inbound, album, outbound
- Zalo message listener bỏ qua message tự gửi, chuẩn hóa chat.photo và nội dung text thành ChannelEvent gồm tenant/session/thread/sender/media/timestamp.
- Ảnh liền nhau cùng sender/thread được gom thành một album event theo quiet window; gateway inbox commit trước rồi forward Quarkus. Event ID và channel message ID dedupe trong scope. Core 502/503 được retry có giới hạn; event đã commit được worker replay sau outage/restart; payload conflict trả 409.
- Outbound internal command validate text/media/HTTPS URL, tối đa 12 ảnh; idempotency theo tenant+session+commandId. Gateway ghi SENDING trước adapter send, COMPLETED khi có kết quả; lỗi/khởi động lại lúc SENDING thành UNKNOWN và không tự gửi lại.
- Image downloader giới hạn domain qua MEDIA_PROXY_HOSTS, bytes/ảnh theo giới hạn SDK và kiểm tra ảnh trước upload. Gửi text + album có thể thành nhiều tin Zalo; kết quả không chắc chắn cần đối soát history, không retry tự động.
TDD scenarios
| ID | Scenario / expected observable behavior | Evidence status |
|---|---|---|
| ZL-01 | Login app thành công đặt cookie an toàn; /me nhận user đã sanitize. | Có test/source: app auth source; gateway suite không bao phủ auth app đầy đủ. |
| ZL-02 | Login thiếu username/password → 400; 5 lần sai → 401; request sai tiếp theo trong cửa sổ → 429 và Retry-After. | Có logic source; cần test tích hợp app để xác nhận boundary/rate-limit expiry. |
| ZL-03 | Không cookie/bearer hoặc token hết hạn: protected /api trả 401; logout invalidates token và clears cookie. | Source-derived; scenario test đề xuất nếu chưa có test route. |
| ZL-04 | Tạo session thành công tạo sess UUID, đăng ký core scope; registration 403/5xx làm session tombstone và không để session dùng được. | Có test gateway lifecycle phần 201/tenant error; cần test rollback registration lỗi. |
| ZL-05 | Unauthenticated internal call → 401; tenant inactive → 403 create / 404 list; tenant B không thể đọc session tenant A. | Có test source: gateway/tests/gateway.test.ts lifecycle/scope. |
| ZL-06 | QR start trả nhanh QR_GENERATING; poll thấy QR_WAITING image; scan hiện QR_SCANNED; success CONNECTED. | Có test source: gateway/tests/zalo-lifecycle.test.ts mock QR lifecycle. |
| ZL-07 | Status tuyệt đối không chứa QR token/code, cookie, IMEI hay user-agent; credentials file mode 0600, dirs 0700. | Có test source: lifecycle và session-secrets. |
| ZL-08 | Abort/expire/decline/logout/delete theo status transitions; logout giữ secret + connect_on_start=false; delete là tombstone. | Có test source: lifecycle; expiry/decline branches cần kiểm chứng thêm. |
| ZL-09 | Gateway restart khi QR chưa xong reconcile status về DISCONNECTED; bắt đầu QR mới hoạt động. | Có logic source ở registry initialize; cần integration test restart DB + gateway. |
| ZL-10 | Connected session lấy group inventory; mismatch path/query session → 400; cross-tenant/not-connected → không trả nhóm. | Có test source cho scope/auth; cần adapter test cho provider failure/not connected. |
| ZL-11 | Inbound album giữ thứ tự ảnh và tạo đúng một event trong debounce window; gửi ảnh đơn thì event media có một phần tử. | Có test source: gateway/tests/zalo-image-album.test.ts, zalo-media.test.ts. |
| ZL-12 | Inbound DB down không trả ack QUEUED; Java outage sau commit trả queued rồi replay theo thứ tự session. | Có test source: inbound-store.test.ts. |
| ZL-13 | Outbound duplicate cùng payload không gọi adapter lần nữa; command ID reuse payload khác → 409; outcome UNKNOWN không tự replay. | Có test source: gateway/tests/gateway.test.ts + command-store behaviors. |
| ZL-14 | Media HTTP, non-allowlisted host, quá số lượng/bytes hoặc URL sai protocol bị từ chối trước adapter upload. | Có test source: gateway/tests/zalo-media.test.ts; bổ sung boundary tests theo cấu hình thực tế. |
| ZL-15 | Hai tenant import cùng session ID nhưng chỉ có flat legacy secret. | Không dùng chung credential; flat legacy secret bị từ chối, mỗi tenant cần secret scoped riêng. Test source: gateway/tests/session-config.test.ts; trạng thái chạy suite được ghi sau lượt kiểm tra. |
Kết quả chạy test · 24 Sep 2026
| Bộ kiểm tra | Kết quả | Phạm vi được chứng minh |
|---|---|---|
bun run app:typecheck | PASS | TypeScript cho Dashboard/BFF. |
bun run app:test | PASS · 40/40 | Bộ app, gồm 9Router cookie retry và workflow BFF. Chưa có kiểm thử end-to-end login app với Zalo thật. |
bun test gateway/tests/*.test.ts | PASS · 31/31 | Gateway session/QR, secret scope, album, inbox và outbound bằng mock/fake. |
Các kết quả này xác nhận contract trong code/test harness; chúng không thay thế login QR bằng tài khoản thật, đọc inventory group thật hoặc kiểm tra quyền gửi tại Zalo.
Khoảng trống và không được suy diễn
- Zalo session/transport dùng chung với vertical slice Role → Task → Skill cho UC-01; nó chưa phải runtime logistics tổng quát. Việc tạo session không tương đương tạo role hoặc workflow instance.
- Không có QR resume sau restart; không có self-service credential deletion khi soft-delete; tombstone giữ file credentials để ngăn session cũ tự hồi sinh.
- Status CONNECTED phụ thuộc listener callback/provider lifecycle. Đọc `/health` hoặc thấy QR login thành công không chứng minh group inventory, ảnh inbound, model inference hay outbound đều sẵn sàng.
- Albums được gom heuristic theo cửa sổ; ảnh đến ngoài cửa sổ có thể thành event khác. Event listener emitter không đợi async persistence callback, nên burst ngắn có thể buffer trong memory; Postgres outage tại đúng thời điểm commit có thể mất callback delivery.
- Tests trong source dùng mock/in-memory cho phần lớn gateway contract; chúng không xác nhận tài khoản Zalo thực, quyền room thật, chính sách rate-limit nhà cung cấp hay độ tin cậy mạng production.
Source of truth để bảo trì
backend/dashboard-bff/auth.ts; route/BFF trong backend/dashboard-bff/server.ts; gateway/server.ts; gateway/adapters/zalo.ts; gateway/session-store.ts; gateway/session-config.ts; gateway/session-secrets.ts; gateway/adapters/zalo-image-album.ts; gateway/adapters/zalo-media.ts; gateway/adapters/zalo-media-upload.ts; gateway/inbound-store.ts; gateway/command-store.ts. Test source: gateway/tests/{gateway,zalo-lifecycle,session-config,session-secrets,zalo-image-album,zalo-media,inbound-store}.test.ts. App login/BFF boundary and real Zalo account/group permissions still need runtime acceptance beyond these gateway tests.