Admin API Documentation
RESTful, JWT-secured endpoints for managing the LuvSweet admin panel — admin users, roles & permissions, the product catalog (categories, products, variants & S3 image uploads) and the activity audit trail.
Overview
All endpoints return JSON and are prefixed with /api/admin.
http://localhost:8080/api/admin
Format
Content-Type: application/json.Auth
Authorization header (except login).Authorization
Framework
Authentication
The API uses stateless JWT bearer tokens.
- Call
POST /auth/loginwith email & password. - Store the returned
access_token. - Send it on every subsequent request as a header:
Authorization: Bearer <access_token> - Tokens expire after
expires_inseconds (default 8 hours). Re-login to obtain a new one.
Super Admin. The seeded Super Admin role bypasses all permission checks — it implicitly has every permission (returned as ["*"]).
Default super admin credentials (configurable in .env, change them immediately in production):
Email: superadmin@luvsweet.com · Password: SuperAdmin@123
Conventions & Errors
Every response follows a consistent envelope.
Success envelope
"status": "success",
"message": "Human readable message",
"data": { ... }, // object, array or omitted
"meta": { ... } // pagination, only on list endpoints
Error envelope
"status": "error",
"message": "What went wrong",
"errors": { "field": "reason" } // only on validation errors (422)
HTTP status codes
| Code | Meaning |
|---|---|
200 | OK — request succeeded |
201 | Created — resource created |
401 | Unauthorized — missing / invalid / expired token |
403 | Forbidden — authenticated but lacking the required permission |
404 | Not Found — resource does not exist |
422 | Unprocessable Entity — validation failed |
Authentication Endpoints
Public — no token required
Request body
| Field | Type | Notes |
|---|---|---|
email required | string | Admin email |
password required | string | Plain text password |
{
"email": "superadmin@luvsweet.com",
"password": "SuperAdmin@123"
}
Response 200
{
"status": "success",
"message": "Login successful.",
"data": {
"token": {
"access_token": "eyJ0eXAiOiJKV1Qi...",
"token_type": "Bearer",
"expires_in": 28800,
"expires_at": "2026-06-10T16:12:03+00:00"
},
"admin": {
"id": 1, "name": "Super Admin",
"email": "superadmin@luvsweet.com",
"role_id": 1, "role_name": "Super Admin",
"is_super_admin": true
},
"permissions": ["*"]
}
}
cURL
curl -X POST http://localhost:8080/api/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"superadmin@luvsweet.com","password":"SuperAdmin@123"}'
Requires authentication
Response 200
{
"status": "success",
"data": {
"admin": { "id": 1, "name": "Super Admin", "is_super_admin": true, ... },
"permissions": ["*"]
}
}
Requires authentication
Request body
| Field | Type | Notes |
|---|---|---|
current_password required | string | Existing password |
new_password required | string | Min 8 characters |
confirm_password required | string | Must match new_password |
Requires authentication
JWTs are stateless — there is no server-side session to destroy. The client should delete the stored token. This endpoint only records the logout in the activity log.
Admin Users
Manage the accounts that can log into the admin panel.
admin_users.view
Query parameters
| Param | Type | Notes |
|---|---|---|
page optional | int | Default 1 |
per_page optional | int | Default 15, max 100 |
search optional | string | Matches name, email or mobile |
status optional | enum | ACTIVE · INACTIVE · BLOCKED |
role_id optional | int | Filter by role |
Response 200 (with pagination meta)
{
"status": "success",
"data": [ { "id": 1, "name": "Super Admin", "role_name": "Super Admin", ... } ],
"meta": { "current_page": 1, "per_page": 15, "total": 1, "total_pages": 1 }
}
admin_users.view
admin_users.create
Request body
| Field | Type | Notes |
|---|---|---|
name required | string | 2–150 chars |
email required | string | Unique, valid email |
password required | string | Min 8 chars (hashed with bcrypt) |
role_id required | int | Must reference an existing role |
mobile optional | string | Max 20 chars |
status optional | enum | Default ACTIVE |
{
"name": "Ravi Kumar",
"email": "ravi@luvsweet.com",
"mobile": "9888877766",
"password": "Ravi@12345",
"role_id": 2
}
admin_users.update
All fields optional — send only what changes. Include password to reset it (min 8 chars).
| Field | Type |
|---|---|
name optional | string |
email optional | string (unique) |
mobile optional | string |
role_id optional | int |
status optional | enum |
password optional | string (reset password) |
admin_users.update
Request body
{ "status": "BLOCKED" } // ACTIVE | INACTIVE | BLOCKED
You cannot deactivate or block your own account.
admin_users.delete
You cannot delete your own account, nor the only remaining Super Admin.
Roles
Roles bundle a set of permissions. Admin users are assigned exactly one role.
roles.view
Optional ?search= filters by role name. Each role includes users_count and permissions_count.
roles.view
{
"data": {
"id": 2, "role_name": "Order Manager", "is_active": "1",
"users_count": 3,
"permission_ids": [1, 21, 22],
"permission_keys": ["dashboard.view", "orders.view", "orders.update"]
}
}
roles.create
| Field | Type | Notes |
|---|---|---|
role_name required | string | Unique |
description optional | string | |
is_active optional | 0 | 1 | Default 1 |
permission_ids optional | int[] | Permissions to grant |
{
"role_name": "Order Manager",
"description": "Handles orders & fulfilment",
"permission_ids": [1, 21, 22]
}
roles.update
Accepts role_name, description, is_active and/or permission_ids (replaces the set when provided).
The Super Admin role is protected and cannot be modified or deleted.
roles.update
{ "permission_ids": [1, 5, 6, 21] }
All listed ids must exist or the request fails with 422. The previous set is fully replaced.
roles.delete
A role with admin users still assigned cannot be deleted — reassign them first.
Permissions
The catalog of permission keys that roles can grant. Usually static, but fully manageable.
permissions.view
Add ?grouped=1 to receive permissions grouped by module_name — ideal for rendering a role-editor checklist.
permissions.view
permissions.manage
| Field | Type | Notes |
|---|---|---|
permission_key required | string | Unique, e.g. orders.refund |
permission_name required | string | Display label |
module_name required | string | Grouping module |
permissions.manage
permissions.manage
Media & Image Uploads
Images are stored in AWS S3 and served through CloudFront. The API never stores binary files in the database — only the resulting public URL is persisted against a category or product.
How image handling works
There are two ways to attach an image, both ending with a CloudFront URL saved in the DB:
1 · Upload-first (recommended for SPAs)
- Client sends the raw file to
POST /media/uploadasmultipart/form-data. - Server validates the file (type & size), uploads it to S3 under
uploads/<folder>/YYYY/MM/<random>.ext, and returns the publicurl. - Client then sends a normal JSON create/update request, passing that
urlinimage_url/featured_image_url/images[].
2 · Direct multipart
- Categories accept an
imagefile directly on create/update (multipart). - Product gallery images are uploaded directly via
POST /products/{id}/images.
On delete of a product, a product image, or a category, the corresponding S3 object(s) are removed automatically so storage never leaks.
Storage layout. Object key = uploads/{folder}/{year}/{month}/{32-hex}.{ext}. Public URL = AWS_CF_PATH + key. Configure credentials via AWS_REGION, AWS_ACCESS_ID, AWS_SECRET_KEY, AWS_BUCKET, AWS_CF_PATH in .env.
Constraints. Allowed types: jpeg, png, webp, gif. Max size: 5 MB per file. Invalid files are rejected with 422; if S3 is not configured the API responds 503.
products.createproducts.updatecategories.createcategories.update (any one)
Content-Type: multipart/form-data.
Form fields
| Field | Type | Notes |
|---|---|---|
file optional* | file | Single file |
files[] optional* | file[] | Multiple files |
folder optional | string | One of products, categories, library, testimonials, clientele, misc (default misc) |
* Provide at least one of file or files[].
Response 201
{
"status": "success",
"message": "File(s) uploaded successfully.",
"data": {
"folder": "products",
"files": [
{
"url": "https://<cloudfront>/uploads/products/2026/06/ab12...png",
"key": "uploads/products/2026/06/ab12...png",
"original_name": "hero.png",
"size": 20480, "mime": "image/png"
}
]
}
}
cURL
curl -X POST http://localhost:8080/api/admin/media/upload \
-H "Authorization: Bearer <token>" \
-F "folder=products" \
-F "file=@/path/to/hero.png"
Categories
Product categories support a self-referential hierarchy via parent_id (a category can have sub-categories). Slugs are auto-generated & de-duplicated from the name when not supplied.
categories.view
Query parameters
| Param | Type | Notes |
|---|---|---|
tree optional | 0 | 1 | 1 returns a nested tree with children[] (no pagination) |
page / per_page optional | int | Default 1 / 20 (max 100) |
search optional | string | Matches name or slug |
is_active optional | 0 | 1 | |
parent_id optional | int | Direct children of a category |
categories.view
Includes children_count and products_count.
categories.create
Accepts JSON (with image_url) or multipart/form-data with an image file (uploaded to S3 automatically).
| Field | Type | Notes |
|---|---|---|
name required | string | 2–150 chars |
slug optional | string | Auto-generated from name if omitted |
parent_id optional | int | Must reference an existing category |
description optional | string | |
image_url optional | string | URL from /media/upload |
image optional | file | Direct upload (multipart only) |
sort_order optional | int | Default 0 |
is_active optional | 0 | 1 | Default 1 |
{
"name": "Sugar Free",
"parent_id": 1,
"image_url": "https://<cloudfront>/uploads/categories/2026/06/ab.png",
"sort_order": 5
}
categories.update
All fields optional. Supports the same image direct-upload as create. A category cannot be set as its own parent.
categories.delete
Blocked with 422 if the category has sub-categories or products. Its S3 image is deleted on success.
Products
A product belongs to one category and has one or more variants (SKU / pack-size / price / stock) plus an optional image gallery. nutrition_facts is a free-form JSON object.
How to integrate product creation in the admin panel
A product screen usually has multiple UI sections: basic product information, SEO/content, pricing & inventory variants, and images. Treat them as one draft in the frontend, then submit them in the correct order.
Recommended add-product flow
- Load dependencies first. Fetch categories via
GET /categories?tree=1so the admin can selectcategory_id. - Build a local product draft. Keep product fields,
variants[], featured image URL, and gallery image URLs in local form state. Do not create database rows while the admin is still editing the draft. - Upload images before final product save. When the admin selects images, call
POST /media/uploadwithfolder=products. Store each returnedurlin the draft. Use one URL asfeatured_image_url; use the rest inimages[]. - Validate variants in the UI. Require at least one variant before final save. Each variant needs a unique
sku,variant_name, andpack_size. The API also validates this and returns keyed errors likevariant.0.sku. - Submit once to create the product. Call
POST /productswith product fields +variants[]+images[]. The API wraps product, variant, and image row creation in a DB transaction, so if any nested record fails, no partial product is saved. - Redirect to edit/details. Use the returned product payload or call
GET /products/{id}to render the final saved state.
Frontend draft structure
{
"product": {
"category_id": 1,
"name": "LuvSweet Stevia Drops",
"short_description": "Zero calorie sweetener drops",
"featured_image_url": "https://<cloudfront>/uploads/products/.../hero.png"
},
"variants": [
{ "sku": "LS-STV-30ML", "variant_name": "30ml", "pack_size": "30ml", "selling_price": 149 }
],
"gallery_images": [
{ "image_url": "https://<cloudfront>/uploads/products/.../front.png", "alt_text": "Front pack shot", "sort_order": 0 }
]
}
Which image approach should I use?
| Approach | Best for | How it works |
|---|---|---|
| Upload-first | New product form / SPA admin UI | Upload images with POST /media/upload, keep returned URLs in draft state, then send those URLs in POST /products. |
| Product image endpoint | Existing product edit screen | After product exists, upload gallery files directly to POST /products/{id}/images. The API uploads to S3 and immediately creates product_images rows. |
Edit-product flow
- Load the full product using
GET /products/{id}. This returns product fields,variants[], andimages[]. - Save basic product/content/SEO changes with
PUT/PATCH /products/{id}. - Add, update, or delete variants using the variant endpoints. This avoids accidentally replacing the full variant list when only one row changes.
- Add or delete gallery images using the image endpoints. Deleting an image also deletes its S3 object.
products.view
Query parameters
| Param | Type | Notes |
|---|---|---|
page / per_page optional | int | Default 1 / 15 (max 100) |
search optional | string | Matches name or slug |
category_id optional | int | |
is_active optional | 0 | 1 | |
is_featured optional | 0 | 1 |
Each row includes category_name and variants_count. Pagination is returned in meta.
products.view
Returns the product, its category_name, decoded nutrition_facts, the full variants[] array and the images[] gallery.
products.create
Integration summary. Use this endpoint as the final "Save Product" call after the admin has completed all tabs/sections in the UI. The request is JSON only; upload binary images first via /media/upload, then pass the returned URLs here.
Recommended UI sections: Basic Info -> Descriptions / SEO -> Variants -> Images -> Review & Save.
Entity mapping
| UI section | Payload target | Database table affected |
|---|---|---|
| Basic info | Top-level fields like category_id, name, slug, is_active | products |
| Descriptions / SEO | short_description, description, ingredients, nutrition_facts, seo_title, seo_description | products |
| Featured image | featured_image_url | products |
| Variants | variants[] | product_variants |
| Gallery images | images[] with image_url, alt_text, sort_order | product_images |
Important: POST /products does not accept file uploads directly. If the form has image files, upload them first and replace the files in your form state with the returned CloudFront URLs before sending this JSON payload.
Product fields
| Field | Type | Notes |
|---|---|---|
category_id required | int | Existing category |
name required | string | 2–200 chars |
slug optional | string | Auto-generated if omitted |
short_description / description optional | string | |
ingredients / how_to_use / terms_and_conditions optional | string | |
nutrition_facts optional | object | Free-form JSON, e.g. {"calories":0} |
featured_image_url / how_to_use_video_url optional | string | |
has_variants / is_featured / is_active optional | 0 | 1 | Defaults 1 / 0 / 1 |
seo_title / seo_description optional | string | |
variants optional | object[] | See variant fields below |
images optional | object[] | { image_url, alt_text?, sort_order? } |
Variant fields (per item in variants[])
| Field | Type | Notes |
|---|---|---|
sku required | string | Globally unique |
variant_name required | string | e.g. "250gm Pack" |
pack_size required | string | e.g. "250gm" |
product_visibility optional | enum | B2C · B2B · BOTH (default BOTH) |
mrp / selling_price / gst_rate optional | decimal | |
pack_weight_grams optional | decimal | |
flavor optional | string | |
price_includes_gst optional | 0 | 1 | |
min_order_qty / max_order_qty / stock_qty / low_stock_alert_qty optional | int | |
is_active optional | 0 | 1 |
{
"category_id": 1,
"name": "LuvSweet Stevia Drops",
"nutrition_facts": { "calories": 0, "sugar": "0g" },
"is_featured": 1,
"featured_image_url": "https://<cloudfront>/uploads/products/2026/06/hero.png",
"variants": [
{ "sku": "LS-STV-30ML", "variant_name": "30ml", "pack_size": "30ml",
"mrp": 199, "selling_price": 149, "gst_rate": 5, "stock_qty": 50, "product_visibility": "B2C" }
],
"images": [
{ "image_url": "https://<cloudfront>/uploads/products/2026/06/g1.png", "alt_text": "front" }
]
}
The whole operation is transactional. Variant SKUs are checked for duplicates both within the request and against the database; errors come back keyed as variant.0.sku, etc.
products.update
Updates only the product's own fields (any subset). Variants and gallery images are intentionally managed through their dedicated endpoints below.
products.update
{ "is_active": 0 } // 0 = inactive, 1 = active
products.delete
Variants and image rows cascade-delete in the database. All associated S3 objects (featured image + gallery) are removed too.
Variants
products.update
Body = a single variant object (same fields as in the create payload). SKU must be unique.
products.update
Any subset of variant fields. The variant must belong to the given product.
products.update
A product must keep at least one variant — deleting the last one returns 422.
Gallery Images
products.update
Content-Type: multipart/form-data. Files are uploaded to S3 and gallery rows are created with auto-incrementing sort_order.
| Field | Type | Notes |
|---|---|---|
files[] optional* | file[] | Multiple images |
file optional* | file | Single image |
alt_text optional | string | Applied to the uploaded image(s) |
* At least one of files[] / file is required.
curl -X POST http://localhost:8080/api/admin/products/1/images \
-H "Authorization: Bearer <token>" \
-F "alt_text=hero shot" \
-F "files[]=@front.png" -F "files[]=@back.png"
products.update
Removes the gallery row and deletes the underlying S3 object.
Audit
activity_logs.view
Every create / update / delete / status change and auth event is recorded automatically, including old & new values.
Query parameters
| Param | Type | Notes |
|---|---|---|
page / per_page optional | int | Default 1 / 20 (max 100) |
module_name optional | string | e.g. admin_users |
action_type optional | string | e.g. CREATE, DELETE |
admin_user_id optional | int | Filter by actor |
Permission Catalog
Seeded permission keys. The Super Admin role holds all of them. Keys marked live are enforced by the endpoints documented above; the rest are reserved for upcoming modules.
| Module | Permission Keys |
|---|---|
dashboard | dashboard.view |
admin_users live | admin_users.viewadmin_users.createadmin_users.updateadmin_users.delete |
roles live | roles.viewroles.createroles.updateroles.delete |
permissions live | permissions.viewpermissions.manage |
activity_logs live | activity_logs.view |
catalog live | categories.viewcategories.createcategories.updatecategories.deleteproducts.viewproducts.createproducts.updateproducts.delete |
orders | orders.vieworders.updateorders.cancel |
coupons | coupons.viewcoupons.createcoupons.updatecoupons.delete |
customers | customers.viewcustomers.update |
enquiries | enquiries.viewenquiries.update |
content | content.viewcontent.createcontent.updatecontent.deletetestimonials.manageclientele.manage |
reports | reports.view |