Website POST routes
Phạm vi account và customer
Mỗi website có account độc lập. Cùng email có thể đăng ký ở nhiều website với mật khẩu/profile riêng; email đăng nhập chỉ unique trong website. Không tự đăng nhập hoặc chia sẻ lịch sử đơn hàng giữa các website.
Đăng ký chỉ tạo account và membership. Checkout đã đăng nhập dùng customer liên kết bằng account ID; chưa có thì tạo customer riêng, kể cả trùng email. Customer email không unique. Guest không nhận hồ sơ hay lịch sử cũ theo email/số điện thoại.
Hiện chỉ hỗ trợ email + mật khẩu. Backend có registry phương thức đăng ký/xác thực để mở rộng; OTP/OAuth và tạo account từ miniapp chưa được bật. Không gửi workspace_id, application_id hoặc customer_id để chọn chủ sở hữu từ form.
Danh sách đầy đủ các route submit của website theo quy luật /_post/{nhóm}/{hành động}. Dùng chung cho HTML form và AJAX; không phải API quản trị hoặc REST API mobile.
Hướng dẫn dựng website · Object auth · Route reference
Quy ước chung
- Tất cả route trong bảng dùng POST, cùng origin website. GET/HEAD vào action trả 405, Allow: POST. Namespace /_post/ dành cho backend, không dùng làm trang theme.
- Không còn route cũ /_account/*, /_form/* hoặc /forms/{form}/submit; không alias và không redirect POST cũ.
- HTML gửi application/x-www-form-urlencoded với _token. AJAX gửi Accept: application/json. JSON gửi Content-Type: application/json và X-CSRF-Token lấy từ HTML; không cần API bootstrap token.
- CSRF dùng chung cho contact/account/cart. Backend xác định workspace/application/store/channel từ context, kiểm tra origin, phiên và quyền riêng. Không gửi account_id/customer_id/workspace_id để chọn chủ dữ liệu.
- Account/commerce body tối đa 16 KiB; key lạ, field trùng hoặc token mâu thuẫn bị từ chối. Không gửi token qua query, không ghi cookie/mật khẩu vào log.
- Account/commerce lỗi hiện trả JSON với HTTP status tương ứng kể cả native form. Không tự retry POST; tải lại trang nếu csrf_rejected.
<form method="post" action="/_post/profile/update">
<input type="hidden" name="_token" value="{{ csrf_token }}">
<input name="display_name" value="{{ auth.user.display_name }}" required>
<input name="phone" value="{{ auth.user.phone }}">
<button>Lưu</button>
</form>Toàn bộ route hiện tại
| POST route | Dữ liệu và tác dụng | JSON / HTML thành công |
|---|---|---|
| /_post/auth/register | email, password, display_name; password_confirm tùy chọn. Đăng ký email + mật khẩu theo policy website; không tự login. | 202 {message}; HTML 303 /login?registered=1. |
| /_post/auth/login | email, password. Xác thực và tạo cookie phiên; thu hồi phiên cũ của trình duyệt. | 200 {authenticated:true, csrf_token}; HTML 303 /profile. |
| /_post/auth/logout | Không có field nghiệp vụ. Thu hồi phiên hiện tại, xóa cookie. Không dùng GET để logout. | 200 {authenticated:false, csrf_token}; HTML 303 /login. |
| /_post/profile/update | Cần đăng nhập. display_name bắt buộc, phone tùy chọn. Không sửa email/mật khẩu/quyền hoặc custom fields tại route này. | 200 profile gồm display_name, phone, email, email_verified; HTML 303 /profile?saved=1. |
| /_post/forms/{form-id}/submit | _token và các field.key của form đã published, thuộc phạm vi ứng dụng. {form-id} có thể là ID/key được resource resolver hỗ trợ. Ưu tiên form.action_url từ theme engine. | 200 {ok:true,message,submission_id}; HTML 303 về trang nguồn hoặc redirect_url của form. |
| /_post/cart/get | Body {}. Lấy giỏ của session trình duyệt, cấp session nếu chưa có. Guest dùng được; không bắt buộc gọi trước add. | 200 CartView. Luôn JSON, kể cả form. |
| /_post/cart/add | variant_id, quantity (số nguyên 1–999). Cộng quantity vào số lượng hiện có; tổng không vượt 999. Backend khóa session khi cập nhật, không cần FE đọc rồi tự cộng. | 200 CartView mới; HTML 303 /cart. |
| /_post/cart/update | variant_id, quantity (1–999). Đặt số lượng tuyệt đối, không cộng. Có thể tạo dòng nếu chưa có; muốn xóa dùng remove. | 200 CartView mới; HTML 303 /cart. |
| /_post/cart/remove | variant_id. Xóa dòng sản phẩm; không gửi quantity. Backend tự xác định giỏ từ cookie session, không nhận UUID giỏ. | 200 CartView mới; HTML 303 /cart. |
| /_post/checkout/quote | shipping_address. Tính giá, thuế, vận chuyển và fingerprint trên backend. Không tạo đơn hay thu tiền. | 200 CartQuote; luôn JSON. |
| /_post/checkout/place-order | shipping_address, quote_fingerprint, idempotency_key, note tùy chọn. Kiểm tra lại báo giá/tồn kho, tạo đơn COD. | 200 {ID,Number,Currency,GrandTotalAmount}; HTML account: 303 /my-orders/{ID}; guest nhận JSON receipt, chưa có trang lịch sử đơn guest. |
| /_post/website/access | password và next (đường dẫn nội bộ), _token. Mở khóa website private bằng mật khẩu website; không tạo account hay đăng nhập thành viên. | Hiện nhận HTML form, thành công 303 về next; sai mật khẩu render lại trang bảo vệ kèm thông báo. Không trả JSON. |
Account và điều kiện sử dụng
Register/login không yêu cầu đã đăng nhập; website cần bật email_password và policy tương ứng. Mật khẩu 12–72 bytes, email tối đa 254 bytes. Register trả thông báo chung để tránh dò email tồn tại; 202 không bảo đảm tài khoản mới đã được tạo. display_name tối đa 160 ký tự; phone tối đa 32 bytes, cho phép rỗng.
Profile cần phiên external account của website, không dùng tài khoản Console. Cart/checkout dùng session trình duyệt và hỗ trợ guest. Account policy không cần bật để guest dùng giỏ; HTTPS vẫn bắt buộc trừ cấu hình HTTP test rõ ràng. Giỏ không tự đồng bộ giữa thiết bị. Logout có thể gọi khi không có phiên để xóa cookie.
Commerce cần store/channel active và các capability commerce.cart.write, commerce.customer.self, commerce.checkout.create; catalog đọc qua commerce.catalog.read. Chỉ sản phẩm published/public, variant active và được phép trên channel mới dùng được. COD phải được bật; không tự fallback qua cổng thanh toán khác.
Gửi contact form
Route forms không yêu cầu account policy. Gửi URL-encoded hoặc multipart form, không gửi JSON body. AJAX dùng URLSearchParams/FormData và Accept: application/json. Field được validate theo cấu hình, lưu vào submission; field lạ không phải cấu hình không được lưu. Route này chưa hỗ trợ upload file vào submission.
{% set contact = forms.contact %}
<form method="post" action="{{ contact.action_url }}">
<input type="hidden" name="_token" value="{{ csrf_token }}">
<input name="email" type="email" required>
<textarea name="message"></textarea>
<button>Gửi</button>
</form>Tên email/message chỉ là ví dụ: thay bằng field.key của form. Validation lỗi trả 400 {ok:false,message,missing,errors} cho AJAX; native form dùng flash lỗi/old input và 303. Form không tồn tại, không published hoặc không khả dụng trong scope trả 404.
Xem object cart để render giỏ bằng Lingo. Giỏ lưu Redis phía server, cookie chỉ chứa mã session ngẫu nhiên HttpOnly/Secure trên HTTPS, hết hạn cố định sau 30 ngày. Restart backend không mất giỏ; nhiều node dùng chung Redis, không cần sticky routing. Redis cần persistence để giữ dữ liệu khi Redis restart. Không ghi bảng carts/cart_items; dữ liệu đơn hàng vẫn lưu DB. Giới hạn 100 dòng, 999 đơn vị/biến thể.
Ví dụ AJAX thêm giỏ
// Layout: <meta name="csrf-token" content="{{ csrf_token }}">
const response = await fetch("/_post/cart/add", {
method: "POST", credentials: "same-origin", cache: "no-store",
headers: {"Accept":"application/json", "Content-Type":"application/json",
"X-CSRF-Token":document.querySelector('meta[name="csrf-token"]').content},
body: JSON.stringify({variant_id: variant.id, quantity: 1})
});
const result = await response.json();
if (!response.ok) throw new Error(result.error?.message || "Không thể cập nhật giỏ");
// Không tự retry add khi timeout: kiểm tra lại cart/get trước.Địa chỉ, báo giá và đặt hàng
shipping_address gồm recipient_name, phone, address_line, ward, district, city, province, postal_code, country_code. Khi place-order, recipient_name/phone/address_line bắt buộc, country_code là mã hai ký tự (ví dụ VN). JSON gửi object; native form dùng shipping_address[recipient_name], shipping_address[phone]…
CartView gồm Currency, Status, Items, Subtotal, ItemCount; không có ID. CartQuote gồm Fingerprint, Currency, Items, SubtotalAmount, DiscountAmount, ShippingAmount, TaxAmount, GrandTotalAmount, ExpiresAt. Tên field response phân biệt hoa/thường; không tự đổi sang snake_case. Số tiền là minor units theo currency.
Gửi quote.Fingerprint vào quote_fingerprint khi đặt hàng. idempotency_key phải ổn định khi thử lại cùng yêu cầu; giữ nguyên payload nếu timeout/5xx, không tạo key mới tùy tiện. quote_changed yêu cầu lấy quote mới và cho khách xác nhận. Backend không tin giá, tổng tiền, trạng thái thanh toán từ client.
Lỗi và bảo mật
Account/commerce trả {error:{code,message}}. 401 unauthenticated; 403 forbidden/origin_rejected/csrf_rejected; 404 not_found; 409 quote_changed, checkout_conflict, payment_unavailable hoặc commerce_unavailable; 422 invalid_input; 429 rate_limited (Retry-After); 503 temporarily_unavailable. Không tự replay thao tác khi CSRF hết hạn.
HTTPS trong production; cookie host-only, HttpOnly; CSRF và dữ liệu riêng tư không cache CDN/service worker. Sau logout xóa DOM riêng tư; khi quay lại qua bfcache tải lại trang để xác thực.
Đọc dữ liệu không qua POST action
Profile dùng auth.check/auth.user. Sản phẩm dùng products.get/find/first/paginate/count/exists. Đơn hàng dùng orders.get/find/first/paginate/count/exists; tracking nằm trong dữ liệu đơn hàng, không có route POST tracking riêng. Không có route session/profile GET cũ, không có route apply-discount, OTP, reset-password hoặc payment callback trong danh sách này.
Checklist
Thử register → login → profile → cart/get → add/update/remove → quote → place-order → orders → logout. Kiểm tra sai token/origin, account khác, GET action 405, route cũ 404 và retry checkout không tạo đơn trùng. Không gửi đơn thật khi thử nghiệm.