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.

Base URL http://localhost:8080/api/admin

Format

JSON request & response bodies. Send Content-Type: application/json.

Auth

Bearer JWT in the Authorization header (except login).

Authorization

Role-Based Access Control. Each route requires a permission key.

Framework

CodeIgniter 4.7 · MySQL 8 · firebase/php-jwt.

Authentication

The API uses stateless JWT bearer tokens.

  1. Call POST /auth/login with email & password.
  2. Store the returned access_token.
  3. Send it on every subsequent request as a header:
    Authorization: Bearer <access_token>
  4. Tokens expire after expires_in seconds (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
CodeMeaning
200OK — request succeeded
201Created — resource created
401Unauthorized — missing / invalid / expired token
403Forbidden — authenticated but lacking the required permission
404Not Found — resource does not exist
422Unprocessable Entity — validation failed

Authentication Endpoints

POST/auth/loginAuthenticate & receive a JWT

Public — no token required

Request body
FieldTypeNotes
email requiredstringAdmin email
password requiredstringPlain 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"}'
GET/auth/meProfile & permissions of the logged-in admin

Requires authentication

Response 200
{
  "status": "success",
  "data": {
    "admin": { "id": 1, "name": "Super Admin", "is_super_admin": true, ... },
    "permissions": ["*"]
  }
}
POST/auth/change-passwordChange your own password

Requires authentication

Request body
FieldTypeNotes
current_password requiredstringExisting password
new_password requiredstringMin 8 characters
confirm_password requiredstringMust match new_password
POST/auth/logoutLog the logout event (client discards token)

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.

GET/admin-usersPaginated list

admin_users.view

Query parameters
ParamTypeNotes
page optionalintDefault 1
per_page optionalintDefault 15, max 100
search optionalstringMatches name, email or mobile
status optionalenumACTIVE · INACTIVE · BLOCKED
role_id optionalintFilter 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 }
}
GET/admin-users/{id}Single admin user

admin_users.view

POST/admin-usersCreate an admin user

admin_users.create

Request body
FieldTypeNotes
name requiredstring2–150 chars
email requiredstringUnique, valid email
password requiredstringMin 8 chars (hashed with bcrypt)
role_id requiredintMust reference an existing role
mobile optionalstringMax 20 chars
status optionalenumDefault ACTIVE
{
  "name": "Ravi Kumar",
  "email": "ravi@luvsweet.com",
  "mobile": "9888877766",
  "password": "Ravi@12345",
  "role_id": 2
}
PUT/admin-users/{id}Update (PATCH also supported)

admin_users.update

All fields optional — send only what changes. Include password to reset it (min 8 chars).

FieldType
name optionalstring
email optionalstring (unique)
mobile optionalstring
role_id optionalint
status optionalenum
password optionalstring (reset password)
PATCH/admin-users/{id}/statusActivate / deactivate / block

admin_users.update

Request body
{ "status": "BLOCKED" }   // ACTIVE | INACTIVE | BLOCKED

You cannot deactivate or block your own account.

DELETE/admin-users/{id}Delete an admin user

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.

GET/rolesAll roles with counts

roles.view

Optional ?search= filters by role name. Each role includes users_count and permissions_count.

GET/roles/{id}Role with its permissions

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"]
  }
}
POST/rolesCreate a role

roles.create

FieldTypeNotes
role_name requiredstringUnique
description optionalstring
is_active optional0 | 1Default 1
permission_ids optionalint[]Permissions to grant
{
  "role_name": "Order Manager",
  "description": "Handles orders & fulfilment",
  "permission_ids": [1, 21, 22]
}
PUT/roles/{id}Update (PATCH supported)

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.

PUT/roles/{id}/permissionsReplace a role's permission set

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.

DELETE/roles/{id}Delete a role

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.

GET/permissionsList all permissions

permissions.view

Add ?grouped=1 to receive permissions grouped by module_name — ideal for rendering a role-editor checklist.

GET/permissions/{id}Single permission

permissions.view

POST/permissionsCreate a permission

permissions.manage

FieldTypeNotes
permission_key requiredstringUnique, e.g. orders.refund
permission_name requiredstringDisplay label
module_name requiredstringGrouping module
PUT/permissions/{id}Update (PATCH supported)

permissions.manage

DELETE/permissions/{id}Delete a permission

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)

  1. Client sends the raw file to POST /media/upload as multipart/form-data.
  2. Server validates the file (type & size), uploads it to S3 under uploads/<folder>/YYYY/MM/<random>.ext, and returns the public url.
  3. Client then sends a normal JSON create/update request, passing that url in image_url / featured_image_url / images[].

2 · Direct multipart

  • Categories accept an image file 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.

POST/media/uploadUpload one or more files to S3

products.createproducts.updatecategories.createcategories.update (any one)

Content-Type: multipart/form-data.

Form fields
FieldTypeNotes
file optional*fileSingle file
files[] optional*file[]Multiple files
folder optionalstringOne 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.

GET/categoriesFlat list, or nested tree

categories.view

Query parameters
ParamTypeNotes
tree optional0 | 11 returns a nested tree with children[] (no pagination)
page / per_page optionalintDefault 1 / 20 (max 100)
search optionalstringMatches name or slug
is_active optional0 | 1
parent_id optionalintDirect children of a category
GET/categories/{id}Single category (+ counts)

categories.view

Includes children_count and products_count.

POST/categoriesCreate a category

categories.create

Accepts JSON (with image_url) or multipart/form-data with an image file (uploaded to S3 automatically).

FieldTypeNotes
name requiredstring2–150 chars
slug optionalstringAuto-generated from name if omitted
parent_id optionalintMust reference an existing category
description optionalstring
image_url optionalstringURL from /media/upload
image optionalfileDirect upload (multipart only)
sort_order optionalintDefault 0
is_active optional0 | 1Default 1
{
  "name": "Sugar Free",
  "parent_id": 1,
  "image_url": "https://<cloudfront>/uploads/categories/2026/06/ab.png",
  "sort_order": 5
}
PUT/categories/{id}Update (PATCH supported)

categories.update

All fields optional. Supports the same image direct-upload as create. A category cannot be set as its own parent.

DELETE/categories/{id}Delete a category

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
  1. Load dependencies first. Fetch categories via GET /categories?tree=1 so the admin can select category_id.
  2. 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.
  3. Upload images before final product save. When the admin selects images, call POST /media/upload with folder=products. Store each returned url in the draft. Use one URL as featured_image_url; use the rest in images[].
  4. Validate variants in the UI. Require at least one variant before final save. Each variant needs a unique sku, variant_name, and pack_size. The API also validates this and returns keyed errors like variant.0.sku.
  5. Submit once to create the product. Call POST /products with 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.
  6. 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?
ApproachBest forHow it works
Upload-firstNew product form / SPA admin UIUpload images with POST /media/upload, keep returned URLs in draft state, then send those URLs in POST /products.
Product image endpointExisting product edit screenAfter 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
  1. Load the full product using GET /products/{id}. This returns product fields, variants[], and images[].
  2. Save basic product/content/SEO changes with PUT/PATCH /products/{id}.
  3. Add, update, or delete variants using the variant endpoints. This avoids accidentally replacing the full variant list when only one row changes.
  4. Add or delete gallery images using the image endpoints. Deleting an image also deletes its S3 object.
GET/productsPaginated list

products.view

Query parameters
ParamTypeNotes
page / per_page optionalintDefault 1 / 15 (max 100)
search optionalstringMatches name or slug
category_id optionalint
is_active optional0 | 1
is_featured optional0 | 1

Each row includes category_name and variants_count. Pagination is returned in meta.

GET/products/{id}Full detail

products.view

Returns the product, its category_name, decoded nutrition_facts, the full variants[] array and the images[] gallery.

POST/productsCreate product (+ variants + images)

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 sectionPayload targetDatabase table affected
Basic infoTop-level fields like category_id, name, slug, is_activeproducts
Descriptions / SEOshort_description, description, ingredients, nutrition_facts, seo_title, seo_descriptionproducts
Featured imagefeatured_image_urlproducts
Variantsvariants[]product_variants
Gallery imagesimages[] with image_url, alt_text, sort_orderproduct_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
FieldTypeNotes
category_id requiredintExisting category
name requiredstring2–200 chars
slug optionalstringAuto-generated if omitted
short_description / description optionalstring
ingredients / how_to_use / terms_and_conditions optionalstring
nutrition_facts optionalobjectFree-form JSON, e.g. {"calories":0}
featured_image_url / how_to_use_video_url optionalstring
has_variants / is_featured / is_active optional0 | 1Defaults 1 / 0 / 1
seo_title / seo_description optionalstring
variants optionalobject[]See variant fields below
images optionalobject[]{ image_url, alt_text?, sort_order? }
Variant fields (per item in variants[])
FieldTypeNotes
sku requiredstringGlobally unique
variant_name requiredstringe.g. "250gm Pack"
pack_size requiredstringe.g. "250gm"
product_visibility optionalenumB2C · B2B · BOTH (default BOTH)
mrp / selling_price / gst_rate optionaldecimal
pack_weight_grams optionaldecimal
flavor optionalstring
price_includes_gst optional0 | 1
min_order_qty / max_order_qty / stock_qty / low_stock_alert_qty optionalint
is_active optional0 | 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.

PUT/products/{id}Update scalar fields (PATCH supported)

products.update

Updates only the product's own fields (any subset). Variants and gallery images are intentionally managed through their dedicated endpoints below.

PATCH/products/{id}/statusActivate / deactivate

products.update

{ "is_active": 0 }   // 0 = inactive, 1 = active
DELETE/products/{id}Delete product

products.delete

Variants and image rows cascade-delete in the database. All associated S3 objects (featured image + gallery) are removed too.

Variants

POST/products/{productId}/variantsAdd a variant

products.update

Body = a single variant object (same fields as in the create payload). SKU must be unique.

PUT/products/{productId}/variants/{variantId}Update a variant (PATCH supported)

products.update

Any subset of variant fields. The variant must belong to the given product.

DELETE/products/{productId}/variants/{variantId}Delete a variant

products.update

A product must keep at least one variant — deleting the last one returns 422.

Gallery Images

POST/products/{productId}/imagesUpload gallery images to S3

products.update

Content-Type: multipart/form-data. Files are uploaded to S3 and gallery rows are created with auto-incrementing sort_order.

FieldTypeNotes
files[] optional*file[]Multiple images
file optional*fileSingle image
alt_text optionalstringApplied 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"
DELETE/products/{productId}/images/{imageId}Delete a gallery image

products.update

Removes the gallery row and deletes the underlying S3 object.

Audit

GET/activity-logsAdmin activity audit trail

activity_logs.view

Every create / update / delete / status change and auth event is recorded automatically, including old & new values.

Query parameters
ParamTypeNotes
page / per_page optionalintDefault 1 / 20 (max 100)
module_name optionalstringe.g. admin_users
action_type optionalstringe.g. CREATE, DELETE
admin_user_id optionalintFilter 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.

ModulePermission Keys
dashboarddashboard.view
admin_users liveadmin_users.viewadmin_users.createadmin_users.updateadmin_users.delete
roles liveroles.viewroles.createroles.updateroles.delete
permissions livepermissions.viewpermissions.manage
activity_logs liveactivity_logs.view
catalog livecategories.viewcategories.createcategories.updatecategories.deleteproducts.viewproducts.createproducts.updateproducts.delete
ordersorders.vieworders.updateorders.cancel
couponscoupons.viewcoupons.createcoupons.updatecoupons.delete
customerscustomers.viewcustomers.update
enquiriesenquiries.viewenquiries.update
contentcontent.viewcontent.createcontent.updatecontent.deletetestimonials.manageclientele.manage
reportsreports.view