Admin Portal
CivicPulse ships with a dedicated admin portal at /admin. It is completely separate from the public app β different route, different authentication, different JWT secret. Regular users cannot access it.
Overviewβ
| Capability | Description |
|---|---|
| Feature Flags | Toggle auth, navigation, media features without a redeploy |
| Site Content | Edit app title, tagline, contact email, GitHub URL |
| Donation Config | Set UPI ID, upload QR code image, enable/disable donation section |
| Donor Wall | Add/edit/remove donors, refresh X profile pictures |
| Ward / Officials Data | CRUD table of ward-level government officials with CSV import/export and public submission queue |
| Admin Management | Superadmin can invite admins, disable accounts, reset roles |
Architectureβ
/admin β AdminLogin.jsx (no auth required)
/admin/dashboard/flags β FlagsView.jsx (auth required)
/admin/dashboard/content β ContentView.jsx (auth required)
/admin/dashboard/donation β DonationView.jsx (auth required)
/admin/dashboard/admins β AdminsView.jsx (superadmin only)
/admin/dashboard/ward-data β WardDataView.jsx (auth required)
The admin portal is a completely separate Vite bundle loaded from admin/index.html. It shares no JavaScript with the main app and uses BrowserRouter basename="/admin". The only entry point from the public app is a small, unstyled link at the bottom of the sign-in card.
All admin API calls go to /api/admin/* and require a Bearer token issued specifically for admin sessions. This token is stored in sessionStorage (cleared on tab close) and uses a separate secret (ADMIN_JWT_SECRET) from the public user JWT.
First-time Setup β Seed Superadminβ
Before any admin can log in, you must create the first superadmin account by running the seed script once after your backend is deployed.
1. Set environment variablesβ
ADMIN_JWT_SECRET=<64-char hex β generate below>
SUPERADMIN_EMAIL=you@example.com
SUPERADMIN_PASSWORD=<min 12 chars, upper+lower+digit>
SUPERADMIN_NAME=Your Name
Generate a strong secret:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
2. Run the seed scriptβ
cd backend
node scripts/seed-superadmin.js
Expected output:
β
Superadmin created: you@example.com (ROLE: superadmin)
The script is idempotent β if the email already exists it prints a warning and exits cleanly.
3. Log inβ
Navigate to https://civicpulse.in/admin and log in with your superadmin credentials.
Authentication Flowβ
POST /api/admin/auth/login
body: { email, password }
β 200: { token, admin: { adminId, name, email, role } }
β 401: Wrong credentials
β 403: Account deactivated
- Tokens expire in 8 hours
- Admin login is rate-limited: 10 attempts per 15 minutes per IP
- Constant-time bcrypt comparison prevents timing attacks even when the email does not exist
No-Access Screenβ
If a non-admin visits /admin and attempts to log in, the response is 401. Regular users see:
You do not have access to the admin portal.
This is a client-side "No Access" card β it does not expose whether the email exists.
Rolesβ
| Role | Capabilities |
|---|---|
admin | Feature flags, site content, donation config, donor management |
superadmin | All admin capabilities + manage other admin accounts |
Feature Flagsβ
Feature flags let you toggle functionality for all users without a code deployment.
Available Flagsβ
| Flag Key | Default | Description |
|---|---|---|
enableGoogleAuth | true | Show Google sign-in button |
enableEmailAuth | true | Show email/password sign-in |
enableIssueReporting | true | Allow citizens to file new issues |
enableBottomNav | true | Show mobile bottom navigation bar |
enableReportButton | true | Show "Report Issue" CTA on landing |
enableMediaUpload | true | Allow photo/video attachments on issues |
enableGeoTagging | true | Attach GPS coordinates to issue reports |
enableExifExtraction | true | Auto-extract EXIF metadata from photos |
Text Overridesβ
From the Flags view you can also update copy:
| Key | Description |
|---|---|
appTitle | Primary app name (e.g. "CivicPulse") |
appTagline | Hero subtitle |
appSubtitle | Supporting text under tagline |
reportBtnLabel | Label on the main CTA button |
landingHero | Short hero eyebrow text |
Flags are persisted to DynamoDB (civicpulse-site-config table, key "flags") and cached at the CDN level for 60 seconds.
Site Contentβ
Edit static content from Content in the admin sidebar:
| Field | Max Length | Description |
|---|---|---|
| App Title | 60 | Displayed in <title> and landing hero |
| Tagline | 120 | Hero subtitle |
| Subtitle | 200 | Supporting hero text |
| Contact Email | 120 | Used in footer / contact links |
| GitHub URL | 200 | Link in Collaborate section |
Donationsβ
Configuring the Donation Sectionβ
- Navigate to Donation in the admin sidebar.
- Upload a QR code image (PNG/JPG, β€2 MB) β stored as base64 in DynamoDB.
- Enter your UPI ID (e.g.
civicpulse@upi). - Toggle Enable donation section on.
- Click Save config.
The section appears on the landing page automatically once enabled.
Managing the Donor Wallβ
Each donor entry has:
| Field | Description |
|---|---|
| Name | Donor display name |
| Amount | Donation amount (INR) |
| X Handle | Twitter/X username (without @) |
| Message | Optional thank-you message |
| Show Name | Whether to display this donor publicly |
| Show Profile | Whether to show their X profile picture |
When Show Profile is enabled and an X handle is provided, CivicPulse fetches the profile picture from the Twitter API v2 at 400Γ400 resolution and caches it in DynamoDB. Click Refresh pic to re-fetch.
Ward / Officials Dataβ
The Ward Data view manages a lookup table of government officials organised by city, pincode, ward, and department. This powers the issue assignment feature in the main app β when a citizen views an issue, the AssignmentPanel fetches the relevant officials and provides one-click deep-links to contact them via X, WhatsApp, or email.
Storageβ
Data is stored in backend/data/ward-officials.json (flat JSON, no DynamoDB dependency):
{
"officials": [
{
"id": "uuid",
"city": "Mumbai",
"pincode": "400001",
"ward": "A Ward",
"category": "Roads",
"agency": "MCGM",
"officialName": "...",
"designation": "...",
"email": "...",
"whatsapp": "...",
"twitter": "...",
"verified": true,
"createdAt": "ISO"
}
],
"submissions": [...]
}
Three Tabsβ
| Tab | Description |
|---|---|
| Officials | CRUD table β add/edit/delete entries, toggle verified status, filter by city / category / verified |
| Submissions | Review crowd-sourced submissions from the public form β approve or reject with one click |
| Import CSV | Paste or upload a CSV (semicolon or comma separated) to bulk-import officials |
CSV Formatβ
city;pincode;ward;category;agency;officialName;designation;email;whatsapp;twitter
Mumbai;400001;A Ward;Roads;MCGM;Rahul Verma;Road Inspector;r.verma@mcgm.gov.in;919876543210;@rahul_mcgm
Public Endpointsβ
| Method | Path | Description |
|---|---|---|
GET | /api/ward-lookup | ?city=X&pincode=X&category=X β returns verified officials for issue assignment |
POST | /api/ward-officials/submit | Public crowd-sourced submission (rate-limited: 10/hr per IP) |
Admin Endpoints (Bearer required)β
| Method | Path | Description |
|---|---|---|
GET | /api/admin/ward-data | List all officials (with optional filters) |
POST | /api/admin/ward-data | Add a new official |
PUT | /api/admin/ward-data/:id | Update official |
DELETE | /api/admin/ward-data/:id | Delete official |
GET | /api/admin/ward-data/export.csv | Download all officials as CSV |
POST | /api/admin/ward-data/import | Bulk import via CSV text in body |
GET | /api/admin/ward-data/submissions | List pending submissions |
POST | /api/admin/ward-data/submissions/:id/approve | Approve and promote a submission to verified |
POST | /api/admin/ward-data/submissions/:id/reject | Reject a submission |
Admin Management (Superadmin Only)β
From the Admins view, a superadmin can:
- Invite a new admin with name, email, password, and role
- Enable / Disable accounts (disabled admins get
403on login) - Delete admin accounts (cannot delete your own account)
Password Requirementsβ
Admin passwords must:
- Be at least 12 characters
- Contain at least one uppercase letter
- Contain at least one lowercase letter
- Contain at least one digit
DynamoDB Tablesβ
Three new tables are required for the admin portal:
civicpulse-adminsβ
| Attribute | Type | Description |
|---|---|---|
adminId | String (PK) | UUID with ADM# prefix |
email | String | Unique login email |
passwordHash | String | bcrypt hash (cost 12) |
name | String | Display name |
role | String | admin or superadmin |
active | Boolean | Account enabled/disabled |
createdAt | String | ISO 8601 timestamp |
civicpulse-site-configβ
| Attribute | Type | Description |
|---|---|---|
configKey | String (PK) | e.g. flags, app-config, donation-config |
value | Map | JSON-serialisable config object |
updatedAt | String | ISO 8601 timestamp |
civicpulse-donorsβ
| Attribute | Type | Description |
|---|---|---|
donorId | String (PK) | UUID with DON# prefix |
name | String | Donor name |
amount | Number | Donation amount |
xHandle | String (opt.) | Twitter handle |
imageUrl | String (opt.) | Cached profile image URL |
showName | Boolean | Show publicly |
showProfile | Boolean | Show X profile picture |
message | String (opt.) | Donor message |
createdAt | String | ISO 8601 timestamp |
Create Tables (AWS CLI)β
for TABLE in civicpulse-admins civicpulse-site-config civicpulse-donors; do
aws dynamodb create-table \
--table-name $TABLE \
--attribute-definitions AttributeName=PK,AttributeType=S \
--key-schema AttributeName=PK,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ap-south-1
done
Each table uses a single string primary key named
PKwhich maps toadminId,configKey, anddonorIdrespectively in the application layer.
API Referenceβ
Authβ
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/admin/auth/login | None | Issue admin JWT |
POST | /api/admin/auth/logout | Bearer | Invalidate session (client-side) |
GET | /api/admin/auth/me | Bearer | Current admin profile |
Feature Flagsβ
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/flags | Bearer | Get all flags + text overrides |
PUT | /api/admin/flags | Bearer | Save flags + text overrides |
GET | /api/flags | None | Public flags endpoint (60s CDN cache) |
Site Configβ
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/config | Bearer | Get site content config |
PUT | /api/admin/config | Bearer | Update site content config |
GET | /api/admin/donation-config | Bearer | Get donation settings |
PUT | /api/admin/donation-config | Bearer | Update donation settings |
GET | /api/donation-config/public | None | Public donation config (safe fields only) |
Donorsβ
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/donors | Bearer | List all donors (admin view) |
POST | /api/admin/donors | Bearer | Add a new donor |
PUT | /api/admin/donors/:id | Bearer | Update donor |
DELETE | /api/admin/donors/:id | Bearer | Delete donor |
POST | /api/admin/donors/:id/refresh-pic | Bearer | Re-fetch X profile picture |
GET | /api/donors/public | None | Public donor list (filtered, 300s cache) |
Admin Managementβ
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/admins | Superadmin | List all admins |
POST | /api/admin/admins | Superadmin | Create new admin |
PATCH | /api/admin/admins/:id/active | Superadmin | Enable/disable admin |
DELETE | /api/admin/admins/:id | Superadmin | Delete admin |
Security Checklistβ
- Admin JWT uses a separate secret (
ADMIN_JWT_SECRET) from user JWT - Tokens stored in
sessionStorageβ cleared on browser/tab close - Login rate-limited: 10 req / 15 min per IP
- Timing-safe login: dummy bcrypt compare when email not found
- Passwords hashed with bcrypt at cost factor 12
- Superadmin-only endpoints enforce role check in addition to auth check
- Admin cannot delete their own account
- Public endpoints expose no sensitive data (QR image stripped, amounts hidden)