A production-grade campus supermarket for BITS Pilani Goa with OTP-based authentication, JWT sessions, and Role-Based Access Control (RBAC).
JV Shetty Megamart is a full-stack PHP e-commerce platform built exclusively for
the BITS Pilani Goa campus community. It enforces campus-email login
(@goa.bits-pilani.ac.in), time-boxed store hours (4 – 9 PM IST), and a
multi-role permission model so that students, faculty, admins, and delivery
partners each see only what they are allowed to.
Key capabilities:
- Browse and search the product catalogue with category / price filters.
- Cart, checkout, order tracking, and cancellation workflows.
- Admin panel: order management, inventory control, analytics, role management.
- Delivery portal for fulfilment partners.
- OTP-verified login with JWT bearer tokens (no persistent passwords on the wire).
| Feature | Detail |
|---|---|
| OTP TTL | 10 minutes (600 s) — OTPs expire automatically |
| OTP rate limit | 3 requests per hour per email address (atomic Redis sliding window via Lua) |
| Brute-force lockout | 5 failed verification attempts → account locked for 1 hour |
| JWT session length | 30 minutes (1 800 s, JWT_TTL) |
| JWT algorithm | HS256 (JWT_ALGORITHM) |
| OTP hashing | bcrypt (password_hash / password_verify) |
| Campus restriction | Only @goa.bits-pilani.ac.in addresses are accepted |
| Security headers | CSP (nonce-based), X-Frame-Options: DENY, HSTS, X-Content-Type-Options, Referrer-Policy |
| Input sanitisation | NULL-byte stripping, control-char removal, 65 535-byte truncation |
| SQL injection guard | Prepared statements everywhere; request-level alerting via SecurityMiddleware |
JV-Shetty/
├── jv_shetty/ # Application root (Apache document root)
│ ├── auth/ # New OTP + JWT + RBAC auth system
│ │ ├── AuthService.php # Orchestration: OTP → email → JWT
│ │ ├── JWTHandler.php # Pure-PHP HS256 JWT (no external library)
│ │ ├── OTPManager.php # OTP lifecycle, rate-limiting, locking
│ │ ├── RBACManager.php # Permission map + enforcement helpers
│ │ ├── RedisClientInterface.php
│ │ ├── RedisFactory.php # Redis connection + adapter
│ │ └── middleware/
│ │ ├── JWTMiddleware.php # Bearer-token verification
│ │ ├── RBACMiddleware.php # JWT + role/permission gate
│ │ └── SecurityMiddleware.php # Security headers, input sanitise
│ ├── api/ # REST API endpoints
│ ├── config/ # App, DB, JWT, Redis config
│ ├── core/ # Legacy Auth (session), Validator, Response…
│ ├── models/ # Cart, Order, Product, User models
│ ├── admin/ # Admin panel pages
│ ├── tests/ # PHPUnit test suite
│ │ ├── phpunit.xml
│ │ ├── bootstrap.php
│ │ ├── OTPBoundaryTest.php # Boundary / unit tests (original)
│ │ ├── ExtendedCoverageTest.php # Extended coverage (new)
│ │ ├── IntegrationJourneyTest.php # End-to-end journey (new)
│ │ └── stubs/
│ │ ├── RedisStub.php
│ │ └── DatabaseStub.php
│ └── *.php # Customer-facing pages
└── README.md # This file
| Requirement | Version |
|---|---|
| PHP | ≥ 8.1 |
| MySQL | ≥ 8.0 |
| Redis | ≥ 6.0 |
| Apache | ≥ 2.4 (with mod_rewrite) |
| Composer | ≥ 2 (for PHPMailer) |
git clone https://github.com/Otto-Deviant1904/JV-Shetty.git
cd JV-Shetty/jv_shetty
composer installCopy .env.example to .env and fill in every value:
# ── MySQL ─────────────────────────────────────────────────────────────────────
DB_HOST=127.0.0.1
DB_NAME=jv_shetty_db
DB_USER=jvuser
DB_PASS=strongpassword
# ── Redis ─────────────────────────────────────────────────────────────────────
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASS=
REDIS_DB=0
# ── JWT ───────────────────────────────────────────────────────────────────────
# Must be at least 32 random characters — never commit the real value.
JWT_SECRET=replace-with-a-64-char-random-string
# ── SMTP (for OTP delivery) ───────────────────────────────────────────────────
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=you@gmail.com
SMTP_PASSWORD=app-password
SMTP_FROM_EMAIL=noreply@jvshetty.bitsgoa.ac.in
SMTP_FROM_NAME=JV Shetty Store
# ── App ───────────────────────────────────────────────────────────────────────
BASE_URL=http://localhost/jv_shetty
RATE_LIMIT_ENABLED=trueSecurity note —
JWT_SECRETis loaded byconfig/jwt.php. Set it via your web-server's environment (SetEnvin Apache,fastcgi_paramin nginx) and never hard-code it in source files.
# Base schema (products, users, roles, orders, etc.)
mysql -u jvuser -p jv_shetty_db < jv_shetty/database/schema.sql
# OTP auth tables (otp_codes + audit_log additions)
mysql -u jvuser -p jv_shetty_db < jv_shetty/database/migrations/002_otp_auth.sqlPoint the document root to jv_shetty/ and enable mod_rewrite:
<VirtualHost *:80>
DocumentRoot /var/www/html/JV-Shetty/jv_shetty
DirectoryIndex index.php
<Directory /var/www/html/JV-Shetty/jv_shetty>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>All API responses use Content-Type: application/json.
| Method | URL | Description |
|---|---|---|
POST |
/api/auth.php?action=request_otp |
Request a 6-digit OTP (campus email required) |
POST |
/api/auth.php?action=verify_otp |
Submit OTP → receive JWT |
POST |
/api/auth.php?action=logout |
Invalidate session |
GET |
/api/auth.php?action=me |
Return current authenticated user |
Request OTP
POST /api/auth.php?action=request_otp
{ "email": "f20230001@goa.bits-pilani.ac.in" }200 { "success": true, "message": "OTP sent to your email address." }
422 { "success": false, "message": "Only BITS Goa campus emails are allowed." }
429 { "success": false, "message": "Too many OTP requests. Please wait before requesting another OTP." }
423 { "success": false, "message": "Account is locked due to too many failed attempts." }Verify OTP
POST /api/auth.php?action=verify_otp
{ "email": "f20230001@goa.bits-pilani.ac.in", "otp": "482931" }200 { "success": true, "token": "eyJ...", "user": { "id": 1, "email": "...", "role": "student" } }
401 { "success": false, "message": "Invalid OTP. 3 attempt(s) remaining." }
410 { "success": false, "message": "OTP has expired. Please request a new one." }| Method | URL | Auth | Description |
|---|---|---|---|
GET |
/api/products.php |
— | List / search products |
GET |
/api/products.php?action=detail&id=X |
— | Product detail |
POST |
/api/products.php?action=create |
admin | Create product |
POST |
/api/products.php?action=update&id=X |
admin | Update product |
POST |
/api/products.php?action=delete&id=X |
admin | Delete product |
| Method | URL | Auth | Description |
|---|---|---|---|
GET |
/api/cart.php?action=get |
user | Get cart contents |
POST |
/api/cart.php?action=add |
user | Add item |
POST |
/api/cart.php?action=update |
user | Update quantity |
POST |
/api/cart.php?action=remove |
user | Remove item |
POST |
/api/cart.php?action=clear |
user | Clear cart |
| Method | URL | Auth | Description |
|---|---|---|---|
GET |
/api/orders.php |
user | List own orders |
POST |
/api/orders.php?action=create |
user | Place order |
GET |
/api/orders.php?action=detail&id=X |
user | Order detail |
POST |
/api/orders.php?action=update_status |
admin | Update order status |
POST |
/api/orders.php?action=cancel |
user | Cancel order |
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Or via the custom header:
X-Auth-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...| Role | Key Permissions |
|---|---|
super_admin |
Wildcard * — everything |
admin |
view/manage users, products, orders, reports, settings, audit logs |
faculty |
view products, place order, view own orders, manage profile |
student |
view products, place order, view own orders, manage profile |
customer |
view products, place order, view own orders, manage profile |
delivery_partner |
view assigned orders, update delivery status, manage profile |
// At the top of any protected endpoint:
// Any authenticated user
$claims = JWTMiddleware::handle();
// Admin or super_admin only
$claims = RBACMiddleware::handle(['admin', 'super_admin']);
// Require a specific permission
$claims = RBACMiddleware::requirePermission('manage_products');
// Non-aborting check (returns null on failure)
$claims = RBACMiddleware::check(['admin']);# Install PHPUnit (if not already via Composer)
cd jv_shetty
composer require --dev phpunit/phpunit ^8
# Run the full suite (no MySQL or Redis needed — SQLite + in-process stubs)
cd tests
phpunit --configuration phpunit.xmlExpected output:
PHPUnit 8.x.x
OTPBoundaryTest ........... (24 tests)
ExtendedCoverageTest ........... (55+ tests, fills AuthService / JWT /
OTPManager / RBAC / middleware gaps)
IntegrationJourneyTest ........... (full user journey + incompatibility docs)
OK (XX tests, YY assertions)
| File | Coverage |
|---|---|
OTPBoundaryTest.php |
OTP TTL boundaries, rate-limit boundaries, brute-force lockout, JWT expiry/tamper, RBAC basics, SecurityMiddleware detection |
ExtendedCoverageTest.php |
AuthService flows, all JWTHandler error paths (structure, alg, nbf, iss, aud), OTPManager state machine, full verifyOTP() paths, RBACManager edge cases, JWTMiddleware header extraction, RBACMiddleware check(), SecurityMiddleware nonce / jsonSafe / deep-sanitize |
IntegrationJourneyTest.php |
End-to-end: OTP request → verification → JWT → protected route → RBAC enforcement; rate-limit and brute-force scenarios; documented incompatibility between legacy session auth and JWTMiddleware |
The legacy PHP pages (api/cart.php, profile.php, checkout.php, etc.) use
Auth::requireAuth() which enforces PHP session authentication
($_SESSION['logged_in']). The new JWTMiddleware authenticates via
Bearer tokens stored in $_REQUEST['_jwt_claims'].
These two mechanisms are orthogonal. A user who authenticates via the OTP
flow and receives a JWT will be rejected by every legacy page that calls
Auth::requireAuth(), and vice-versa. Until both sides are unified behind a
single auth layer, pages that must support JWT-authenticated users must be
updated to check $_REQUEST['_jwt_claims'] in addition to (or instead of)
$_SESSION['logged_in'].
Files that need updating:
| File | Issue |
|---|---|
api/cart.php |
Calls Auth::requireAuth() → session redirect |
api/orders.php |
Calls Auth::requireAuth() → session redirect |
api/user.php |
Calls Auth::check() → session check |
api/wishlist.php |
Calls Auth::requireAuth() → session redirect |
profile.php |
Calls Auth::requireAuth() → session redirect |
cart.php |
Calls Auth::requireAuth() → session redirect |
checkout.php |
Calls Auth::requireAuth() → session redirect |
orders.php |
Calls Auth::requireAuth() → session redirect |
wishlist.php |
Calls Auth::requireAuth() → session redirect |
- Branch from
main. - Run the PHPUnit suite before opening a PR.
- New auth features must be covered by unit tests in
tests/. - Never commit secrets — use environment variables.
JV Shetty Megamart — Campus supermarket for BITS Pilani Goa.