# EsimLao Authentication API Documentation ## Overview API xác thực người dùng qua email với OTP (One-Time Password). URL_UAT : http://149.28.132.56:8360/ --- ## 1. Request OTP Gửi mã OTP đến email người dùng để xác thực đăng nhập. ### Endpoint ``` POST /apis/auth/request-otp ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "email": "user@example.com", "lang": "lo" // Default: "lo" / en } ``` ### Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | email | string | Yes | - | Email address của người dùng | | lang | string | No | "vi" | Ngôn ngữ email: `vi` (Tiếng Việt), `en` (English), `lo` (ລາວ) | ### Response Success (200) ```json { "errorCode": "0", "message": "", "data": { "email": "user@example.com", "expireInSeconds": 300 } } ``` ### Response Error (200) ```json // Email không được cung cấp { "errorCode": "-801", "message": "", "data": {} } // Lỗi hệ thống { "errorCode": "-6", "message": "", "data": {} } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | errorCode| string | "0" = Success, khác "0" = Error (xem Error Codes) | | message | string | Thông báo từ CONFIG table (theo ngôn ngữ) | | data.email | string | Email đã gửi OTP | | data.expireInSeconds | int | Thời gian OTP hết hạn (giây) | ### Notes - OTP gồm 6 chữ số - OTP có hiệu lực trong 5 phút - Mỗi lần request mới sẽ hủy các OTP cũ chưa sử dụng - Nếu email chưa tồn tại, hệ thống tự động tạo tài khoản mới --- ## 2. Verify OTP Xác thực mã OTP và hoàn tất đăng nhập. ### Endpoint ``` POST /apis/auth/verify-otp ``` ### Request Headers | Header | Value | Required | |--------|-------|----------| | Content-Type | application/json | Yes | ### Request Body ```json { "email": "user@example.com", "otpCode": "123456", "lang": "lo" // Default: "lo" / en } ``` ### Parameters | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | email | string | Yes | - | Email đã nhận OTP | | otpCode | string | Yes | - | Mã OTP 6 số | | lang | string | No | "lo" | Ngôn ngữ thông báo: `lo` (ລາວ), `en` (English) | ### Response Success (200) ```json { "errorCode": "0", "message": "", "data": { "userId": 12345, "email": "user@example.com", "fullName": "Nguyen Van A", "avatarUrl": "https://example.com/avatar.jpg", "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...", "expiresAt": "2024-12-30T10:00:00Z" } } ``` ### Response Error Cases ```json // Thiếu email hoặc OTP { "errorCode": "-801", "message": "", "data": {} } // OTP không hợp lệ { "errorCode": "-201", "message": "", "data": {} } // OTP đã được sử dụng { "errorCode": "-203", "message": "", "data": {} } // OTP đã hết hạn { "errorCode": "-202", "message": "", "data": {} } // Không tìm thấy người dùng { "errorCode": "-300", "message": "", "data": {} } // Lỗi hệ thống { "errorCode": "-6", "message": "", "data": {} } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | errorCode| string | "0" = Success, khác "0" = Error (xem Error Codes) | | message | string | Thông báo từ CONFIG table (theo ngôn ngữ) | | data.userId | int | ID người dùng | | data.email | string | Email người dùng | | data.fullName | string | Họ tên đầy đủ | | data.avatarUrl | string | URL ảnh đại diện (nullable) | | data.accessToken | string | JWT access token | | data.refreshToken | string | Refresh token để làm mới access token | | data.expiresAt | datetime | Thời điểm access token hết hạn | ### Notes - Access token có hiệu lực 24 giờ - Refresh token có hiệu lực 30 ngày - Mỗi lần đăng nhập thành công, các token cũ sẽ bị thu hồi --- ## Error Codes ### Success | errorCode| Constant | Description | |------|----------|-------------| | "0" | Success | Thành công (mọi request thành công đều trả về "0") | ### General Errors (-1 to -99) | errorCode| Constant | Description | |------|----------|-------------| | "-1" | Error | Lỗi chung | | "-6" | SystemError | Lỗi hệ thống | ### OTP Errors (-200 to -299) | errorCode| Constant | Description | |------|----------|-------------| | "-200" | OtpRequired | Yêu cầu OTP | | "-201" | OtpInvalid | OTP không hợp lệ | | "-202" | OtpExpired | OTP đã hết hạn | | "-203" | OtpAlreadyUsed | OTP đã được sử dụng | | "-204" | OtpMaxAttemptsExceeded | Vượt quá số lần thử | | "-205" | OtpSendFailed | Gửi OTP thất bại | | "-206" | OtpTooManyRequests | Request quá nhiều | ### User Errors (-300 to -399) | errorCode| Constant | Description | |------|----------|-------------| | "-300" | UserNotFound | Không tìm thấy người dùng | | "-304" | InvalidEmail | Email không hợp lệ | ### Validation Errors (-800 to -899) | errorCode| Constant | Description | |------|----------|-------------| | "-801" | RequiredFieldMissing | Thiếu trường bắt buộc | --- ## Authentication Sau khi đăng nhập thành công, sử dụng `accessToken` trong header cho các API yêu cầu xác thực: ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` --- ## Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client │ │ API │ │ Email │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ POST /request-otp │ │ │──────────────────>│ │ │ │ │ │ │ Generate OTP │ │ │ Save to DB │ │ │ Queue Email │ │ │ │ │ Response │ │ │<──────────────────│ │ │ │ │ │ │ Send OTP Email │ │ │──────────────────>│ │ │ │ │ │ │ OTP Email │<──────────────────────────────────────│ │ │ │ │ POST /verify-otp │ │ │──────────────────>│ │ │ │ │ │ │ Verify OTP │ │ │ Generate JWT │ │ │ │ │ Token Response │ │ │<──────────────────│ │ │ │ │ ``` --- ## Example Usage (cURL) ### Request OTP ```bash curl -X POST https://api.esimlao.com/apis/auth/request-otp \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "lang": "vi" }' ``` ### Verify OTP ```bash curl -X POST https://api.esimlao.com/apis/auth/verify-otp \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "otpCode": "123456" }' ``` ### Use Token ```bash curl -X GET https://api.esimlao.com/apis/user/profile \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` --- ## 3. Article Category Lấy danh sách danh mục bài viết. ### Endpoint ``` POST /apis/article/category ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "parentId": null } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ: "lo", "en" (có thể truyền qua header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại | | pageSize | int | 10 | Số item mỗi trang | | parentId | int? | null | ID danh mục cha (null = root) | ### Response Success ```json { "errorCode": "0", "message": "Success", "data": { "categories": [ { "id": 1, "categoryName": "Cẩm nang du lịch", "categorySlug": "cam-nang-du-lich", "description": "Mô tả...", "iconUrl": "/icons/travel.png", "parentId": null, "displayOrder": 1 } ], "pagination": { "pageNumber": 0, "pageSize": 10, "totalCount": 5, "totalPages": 1 } } } ``` ### Response Fields - categories[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | ARTICLE_CATEGORY.ID | ID danh mục bài viết (Primary Key) | | categoryName | string | ARTICLE_CATEGORY.CATEGORY_NAME
CATEGORY_NAME_EN
CATEGORY_NAME_LO | Tên danh mục (theo ngôn ngữ được chọn) | | categorySlug | string | ARTICLE_CATEGORY.CATEGORY_SLUG | URL-friendly slug cho danh mục | | description | string | ARTICLE_CATEGORY.DESCRIPTION
DESCRIPTION_EN
DESCRIPTION_LO | Mô tả chi tiết danh mục | | iconUrl | string | ARTICLE_CATEGORY.ICON_URL | Đường dẫn icon của danh mục | | parentId | int? | ARTICLE_CATEGORY.PARENT_ID | ID danh mục cha (null = danh mục gốc) | | displayOrder | int | ARTICLE_CATEGORY.DISPLAY_ORDER | Thứ tự hiển thị (số nhỏ hơn hiển thị trước) | ### Response Fields - pagination | Field | Type | Description | |-------|------|-------------| | pageNumber | int | Trang hiện tại (bắt đầu từ 0) | | pageSize | int | Số items trên mỗi trang | | totalCount | int | Tổng số items trong database | | totalPages | int | Tổng số trang (= ceiling(totalCount / pageSize)) | --- ## 4. Article Load Lấy danh sách bài viết hoặc chi tiết 1 bài viết. ### Endpoint ``` POST /apis/article/load ``` ### Request (danh sách) ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "categoryId": 1, "isFeatured": false } ``` ### Request (chi tiết) ```json { "lang": "lo", "slug": "huong-dan-cai-dat-esim" } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại | | pageSize | int | 10 | Số item mỗi trang | | categoryId | int? | null | Lọc theo danh mục | | isFeatured | bool? | null | Lọc bài viết nổi bật | | slug | string? | null | Slug để lấy chi tiết bài viết | ### Response Success (danh sách) ```json { "errorCode": "0", "message": "Success", "data": { "articles": [ { "id": 1, "title": "Hướng dẫn cài đặt eSIM", "slug": "huong-dan-cai-dat-esim", "summary": "Tóm tắt...", "thumbnailUrl": "/images/article1.jpg", "categoryId": 1, "viewCount": 150, "isFeatured": true, "isPinned": false, "publishedDate": "2024-12-25" } ], "pagination": {...} } } ``` ### Response Success (chi tiết) ```json { "errorCode": "0", "message": "Success", "data": { "article": { "id": 1, "title": "Hướng dẫn cài đặt eSIM", "slug": "huong-dan-cai-dat-esim", "summary": "Tóm tắt...", "content": "

Nội dung HTML...

", "thumbnailUrl": "/images/article1.jpg", "coverImageUrl": "/images/cover1.jpg", "metaDescription": "SEO description", "metaKeywords": "esim, laos", "categoryId": 1, "viewCount": 151, "isFeatured": true, "publishedDate": "2024-12-25", "createdDate": "2024-12-20" } } } ``` ### Response Fields - articles[] (List) | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | ARTICLE.ID | ID bài viết (Primary Key) | | title | string | ARTICLE.TITLE
TITLE_EN
TITLE_LO | Tiêu đề bài viết (theo ngôn ngữ) | | slug | string | ARTICLE.SLUG | URL-friendly slug (unique) | | summary | string | ARTICLE.SUMMARY
SUMMARY_EN
SUMMARY_LO | Tóm tắt ngắn gọn | | thumbnailUrl | string | ARTICLE.THUMBNAIL_URL | Ảnh thumbnail (dùng cho list) | | categoryId | int | ARTICLE.CATEGORY_ID | ID danh mục (FK → ARTICLE_CATEGORY) | | viewCount | int | ARTICLE.VIEW_COUNT | Số lượt xem | | isFeatured | bool | ARTICLE.IS_FEATURED | Bài viết nổi bật (true/false) | | isPinned | bool | ARTICLE.IS_PINNED | Bài viết được ghim (true/false) | | publishedDate | datetime | ARTICLE.PUBLISHED_DATE | Ngày xuất bản | ### Response Fields - article (Detail) | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | *Tất cả fields từ List + thêm:* | | | | | content | string | ARTICLE.CONTENT
CONTENT_EN
CONTENT_LO | Nội dung HTML đầy đủ | | coverImageUrl | string | ARTICLE.COVER_IMAGE_URL | Ảnh bìa (dùng cho detail page) | | metaDescription | string | ARTICLE.META_DESCRIPTION
META_DESCRIPTION_EN
META_DESCRIPTION_LO | SEO meta description | | metaKeywords | string | ARTICLE.META_KEYWORDS | SEO keywords (comma-separated) | | createdDate | datetime | ARTICLE.CREATED_DATE | Ngày tạo bài viết | --- ## 5. Banner Load Lấy danh sách banner. ### Endpoint ``` POST /apis/content/banner ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "position": "home" } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ (hoặc header Accept-Language) | | pageNumber | int | 0 | Trang hiện tại | | pageSize | int | 10 | Số item mỗi trang | | position | string? | null | Vị trí: "home", "sidebar"... | ### Response ```json { "errorCode": "0", "data": { "banners": [ { "id": 1, "title": "Banner Title", "subtitle": "Subtitle", "imageUrl": "/images/banner1.jpg", "imageMobileUrl": "/images/banner1_m.jpg", "linkUrl": "/promo", "linkTarget": "_blank", "position": "home", "displayOrder": 1 } ], "pagination": {...} } } ``` ### Response Fields - banners[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | BANNER.ID | ID banner (Primary Key) | | title | string | BANNER.TITLE
TITLE_EN
TITLE_LO | Tiêu đề banner (theo ngôn ngữ) | | subtitle | string | BANNER.SUBTITLE
SUBTITLE_EN
SUBTITLE_LO | Phụ đề banner | | imageUrl | string | BANNER.IMAGE_URL | Ảnh banner (desktop) | | imageMobileUrl | string | BANNER.IMAGE_MOBILE_URL | Ảnh banner (mobile) | | linkUrl | string | BANNER.LINK_URL | URL đích khi click banner | | linkTarget | string | BANNER.LINK_TARGET | Target (_self, _blank,...) | | position | string | BANNER.POSITION | Vị trí hiển thị (home, category,...) | | displayOrder | int | BANNER.DISPLAY_ORDER | Thứ tự hiển thị | **Lọc tự động**: Chỉ trả về banners với `STATUS = 1` và trong khoảng `START_DATE ≤ now ≤ END_DATE` --- ## 6. Customer Review Load Lấy đánh giá của khách hàng. ### Endpoint ``` POST /apis/content/review ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "isFeatured": true } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ | | pageNumber | int | 0 | Trang | | pageSize | int | 10 | Số item | | isFeatured | bool? | null | Lọc review nổi bật | ### Response ```json { "errorCode": "0", "data": { "reviews": [ { "id": 1, "customerName": "Nguyen Van A", "avatarUrl": "/avatars/user1.jpg", "rating": 1, "reviewContent": "Dich vu rat tot...", "destination": "Vientiane, Laos", "isFeatured": true, "createdDate": "2024-12-25" } ], "pagination": {...} } } ``` ### Response Fields - reviews[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | CUSTOMER_REVIEW.ID | ID đánh giá (Primary Key) | | customerName | string | CUSTOMER_REVIEW.CUSTOMER_NAME | Tên khách hàng | | avatarUrl | string | CUSTOMER_REVIEW.AVATAR_URL | Ảnh đại diện (nullable) | | rating | int | CUSTOMER_REVIEW.RATING | Số sao (1-5) | | reviewContent | string | CUSTOMER_REVIEW.REVIEW_CONTENT
REVIEW_CONTENT_EN
REVIEW_CONTENT_LO | Nội dung đánh giá (theo ngôn ngữ) | | destination | string | CUSTOMER_REVIEW.DESTINATION
DESTINATION_EN
DESTINATION_LO | Địa điểm du lịch | | isFeatured | bool | CUSTOMER_REVIEW.IS_FEATURED | Review nổi bật | | createdDate | datetime | CUSTOMER_REVIEW.CREATED_DATE | Ngày tạo review | **Lọc tự động**: Chỉ trả về reviews với `STATUS = 1` (đã duyệt) --- ## 6.1 Customer Review Create Khách hàng gửi đánh giá (chờ duyệt). ### Endpoint ``` POST /apis/content/review/create ``` ### Request ```json { "lang": "lo", "customerName": "Nguyen Van A", "reviewContent": "Dich vu rat tot, toi rat hai long!", "destination": "Vientiane, Laos", "rating": 5 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | lang | string | No | Ngôn ngữ | | customerName | string | Yes | Tên khách hàng | | reviewContent | string | Yes | Nội dung đánh giá | | destination | string | No | Địa điểm | | rating | int | No | Đánh giá (1-5) | ### Response Success ```json { "errorCode": "0", "message": "Review submitted successfully", "data": { "reviewId": 123 } } ``` ### Note - Review mới sẽ có `Status = false` (chờ admin duyệt) --- ## 7. FAQ Category Load Lấy danh mục FAQ. ### Endpoint ``` POST /apis/content/faq-category ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10 } ``` ### Response ```json { "errorCode": "0", "data": { "categories": [ { "id": 1, "categoryName": "Cài đặt eSIM", "categorySlug": "cai-dat-esim", "description": "Hướng dẫn cài đặt", "iconUrl": "/icons/setup.png", "displayOrder": 1 } ], "pagination": {...} } } ``` ### Response Fields - categories[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | FAQ_CATEGORY.ID | ID danh mục FAQ (Primary Key) | | categoryName | string | FAQ_CATEGORY.CATEGORY_NAME
CATEGORY_NAME_EN
CATEGORY_NAME_LO | Tên danh mục FAQ | | categorySlug | string | FAQ_CATEGORY.CATEGORY_SLUG | URL-friendly slug | | description | string | FAQ_CATEGORY.DESCRIPTION
DESCRIPTION_EN
DESCRIPTION_LO | Mô tả danh mục | | iconUrl | string | FAQ_CATEGORY.ICON_URL | Icon của danh mục | | displayOrder | int | FAQ_CATEGORY.DISPLAY_ORDER | Thứ tự hiển thị | --- ## 8. FAQ Load Lấy danh sách câu hỏi thường gặp. ### Endpoint ``` POST /apis/content/faq ``` ### Request ```json { "lang": "lo", "pageNumber": 0, "pageSize": 10, "categoryId": 1, "isFeatured": false } ``` | Field | Type | Default | Description | |-------|------|---------|-------------| | lang | string | "lo" | Ngôn ngữ | | pageNumber | int | 0 | Trang | | pageSize | int | 10 | Số item | | categoryId | int? | null | Lọc theo danh mục | | isFeatured | bool? | null | Lọc FAQ nổi bật | ### Response ```json { "errorCode": "0", "data": { "faqs": [ { "id": 1, "question": "Làm sao để cài đặt eSIM?", "answer": "

Hướng dẫn chi tiết...

", "categoryId": 1, "viewCount": 100, "isFeatured": true } ], "pagination": {...} } } ``` ### Response Fields - faqs[] | Field | Type | DB Column | Description | |-------|------|-----------|-------------| | id | int | FAQ.ID | ID câu hỏi (Primary Key) | | question | string | FAQ.QUESTION
QUESTION_EN
QUESTION_LO | Câu hỏi (theo ngôn ngữ) | | answer | string | FAQ.ANSWER
ANSWER_EN
ANSWER_LO | Câu trả lời (HTML format) | | categoryId | int | FAQ.CATEGORY_ID | ID danh mục (FK → FAQ_CATEGORY) | | viewCount | int | FAQ.VIEW_COUNT | Số lượt xem | | isFeatured | bool | FAQ.IS_FEATURED | FAQ nổi bật | **Lọc tự động**: Chỉ trả về FAQs với `STATUS = 1` ---