A full-stack web application where users can rate stores (1–5 stars).
Supports three roles with role-isolated dashboards: System Administrator, Normal User, and Store Owner.
| Layer | Technology |
|---|---|
| Backend | Node.js, Express.js, Prisma ORM |
| Database | PostgreSQL 14+ |
| Auth | JWT (24 h expiry, role claim) |
| Frontend | React 19, Vite, Tailwind CSS |
| Testing | Node built-in test runner (backend), Vitest (frontend) |
| Linting | ESLint (both sides) |
| CI | GitHub Actions |
/
├── backend/
│ ├── prisma/
│ │ ├── schema.prisma # User, Store, Rating models
│ │ ├── migrations/ # Versioned DB migrations
│ │ └── seed.js # Demo data seeder
│ ├── src/
│ │ ├── config/env.js # Startup env validation
│ │ ├── lib/prisma.js # Prisma singleton
│ │ ├── middleware/
│ │ │ ├── auth.js # JWT authentication
│ │ │ ├── checkRole.js # Role-based access guard
│ │ │ └── errorHandler.js # Global error handler
│ │ ├── routes/
│ │ │ ├── auth.routes.js # /api/v1/auth
│ │ │ ├── admin.routes.js # /api/v1/admin
│ │ │ ├── stores.routes.js # /api/v1/stores
│ │ │ ├── ratings.routes.js # /api/v1/ratings
│ │ │ └── storeOwner.routes.js # /api/v1/store-owner
│ │ └── utils/
│ │ ├── validation.js # Name/email/password/address validators
│ │ ├── pagination.js # parsePagination, parseSort, paginatedResponse
│ │ └── storeHelpers.js # avgRating calculation helpers
│ └── tests/
│ ├── validation.test.js
│ └── storeHelpers.test.js
├── frontend/
│ └── src/
│ ├── components/
│ │ ├── ErrorBanner.jsx
│ │ ├── Layout.jsx
│ │ ├── LoadingSpinner.jsx
│ │ ├── Pagination.jsx
│ │ ├── ProtectedRoute.jsx
│ │ ├── SortableTable.jsx
│ │ ├── StarRating.jsx
│ │ └── StatCard.jsx
│ ├── context/AuthContext.jsx
│ ├── hooks/useListQuery.js
│ ├── pages/
│ │ ├── admin/
│ │ │ ├── AdminDashboardPage.jsx
│ │ │ ├── AdminAddStorePage.jsx
│ │ │ ├── AdminAddUserPage.jsx
│ │ │ ├── AdminStoreListPage.jsx
│ │ │ ├── AdminUserListPage.jsx
│ │ │ └── AdminUserDetailPage.jsx
│ │ ├── ChangePasswordPage.jsx
│ │ ├── LoginPage.jsx
│ │ ├── SignupPage.jsx
│ │ ├── StoreOwnerDashboardPage.jsx
│ │ └── UserStoreBrowsePage.jsx
│ ├── services/api.js
│ └── utils/
│ ├── formatRole.js
│ └── validation.js
├── .github/workflows/ci.yml # CI: lint + test + build on push/PR
├── docker-compose.yml
└── package.json # Root workspace scripts
- Node.js 18+
- PostgreSQL 14+ (or Docker)
- npm 9+
docker compose up -dOr point DATABASE_URL at an existing PostgreSQL instance.
cd backend
cp .env.example .env
# Edit .env — set DATABASE_URL, JWT_SECRET, and CORS_ORIGIN at minimum
npm install
npx prisma migrate deploy
npx prisma db seed
npm run devAPI runs at http://localhost:5000.
cd frontend
cp .env.example .env
npm install
npm run devApp runs at http://localhost:3000. All /api/* requests are proxied to the backend.
npm install
npm run dev| Role | Password | |
|---|---|---|
| Admin | admin@store-rating.com |
Admin@123456 |
| Store Owner | owner@store-rating.com |
DemoUser@123456 |
| Normal User | user@store-rating.com |
DemoUser@123456 |
The seed also creates one demo store owned by the Store Owner and one rating submitted by the Normal User.
Re-running the seed is safe — it upserts all records.
| Variable | Required | Description | Default |
|---|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string | — |
JWT_SECRET |
✅ | Secret used to sign JWTs — change in production | — |
PORT |
API server port | 5000 |
|
CORS_ORIGIN |
Allowed frontend origin for CORS | http://localhost:3000 |
|
NODE_ENV |
Set to production for safe error logging |
development |
Security: The server will refuse to start in production if
JWT_SECRETstill contains the placeholder valuechange-in-production.
| Variable | Required | Description | Default |
|---|---|---|---|
VITE_API_URL |
Backend API base URL | /api/v1 |
All endpoints are versioned under /api/v1.
All errors use a consistent shape:
{ "error": { "message": "...", "field": "optional-field-name" } }Validation errors include a nested errors map:
{ "error": { "message": "Validation failed", "errors": { "name": "...", "email": "..." } } }Rate-limited: 100 requests per 15 minutes per IP.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/signup |
Public | Register a new Normal User |
POST |
/login |
Public | Login for any role — returns JWT + user |
GET |
/me |
Any | Validate session and return current user |
PUT |
/change-password |
Any | Change password (validates new password rules) |
| Method | Path | Description |
|---|---|---|
GET |
/stats |
Live counts: total users, stores, ratings |
GET |
/stores |
Paginated, sortable, filterable store list |
POST |
/stores |
Create a store and assign owner |
GET |
/users |
Paginated, sortable, filterable user list |
POST |
/users |
Create a user with any role |
GET |
/users/:id |
User detail; includes avg store rating for STORE_OWNER |
GET |
/store-owners |
List all STORE_OWNER users (for the owner dropdown) |
Query params for list endpoints: page, limit, sortBy, order (asc/desc), plus field filters (name, email, address, role).
| Method | Path | Description |
|---|---|---|
GET |
/stores |
Browse all stores with avg rating and own rating; supports name + address search |
POST |
/ratings |
Submit or update a rating (1–5) — upserts, never duplicates |
| Method | Path | Description |
|---|---|---|
GET |
/dashboard |
Returns owned stores, list of raters (name, email, value), and overall avg rating |
| Role | Capabilities |
|---|---|
ADMIN |
Dashboard stats; create/list/filter/sort all users and stores; view user detail with store avg rating |
NORMAL_USER |
Browse stores; search by name/address; submit or modify ratings |
STORE_OWNER |
View all users who rated their store(s); see overall avg rating |
Role routing is enforced on both backend (middleware) and frontend (ProtectedRoute). Authenticated users visiting /login or /signup are redirected to their role's home page.
Enforced independently on both frontend and backend:
| Field | Rule |
|---|---|
| Name | 20–60 characters |
| Address | Max 400 characters |
| Password | 8–16 characters, at least one uppercase letter and one special character |
| Standard email format |
- Passwords hashed with
bcrypt(10 rounds) — never stored or returned in plaintext - JWTs expire after 24 hours; invalid/expired tokens return
401 - Role middleware returns
403(not500) for wrong-role access - Auth routes are rate-limited (100 req / 15 min per IP), including
/change-password - CORS restricted to
CORS_ORIGINenv variable - Prisma parameterized queries — no raw string interpolation
JWT_SECRETplaceholder detected and rejected at startup in productionconsole.errorlogs full errors in development; terse summary only in production
# From root — runs backend + frontend tests
npm test
# Backend only (Node built-in test runner)
cd backend && npm test
# Frontend only (Vitest)
cd frontend && npm testGitHub Actions (.github/workflows/ci.yml) runs on every push and pull request:
- Backend:
npm test+npm run lint - Frontend:
npm test+npm run lint+npm run build
| Script | Description |
|---|---|
npm run dev |
Start backend and frontend concurrently |
npm test |
Run all tests |
npm run lint |
Lint both backend and frontend |
npm run build |
Production frontend build |
| Script | Description |
|---|---|
npm run dev |
Start with nodemon |
npm test |
Run unit tests |
npm run lint |
ESLint check |
npm run db:migrate |
Run Prisma migrations |
npm run db:seed |
Seed demo data |
npm run db:generate |
Regenerate Prisma client |
| Script | Description |
|---|---|
npm run dev |
Start Vite dev server (port 3000) |
npm test |
Run Vitest tests |
npm run lint |
ESLint check |
npm run build |
Production build |