Skip to main content

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​

CapabilityDescription
Feature FlagsToggle auth, navigation, media features without a redeploy
Site ContentEdit app title, tagline, contact email, GitHub URL
Donation ConfigSet UPI ID, upload QR code image, enable/disable donation section
Donor WallAdd/edit/remove donors, refresh X profile pictures
Ward / Officials DataCRUD table of ward-level government officials with CSV import/export and public submission queue
Admin ManagementSuperadmin 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​

RoleCapabilities
adminFeature flags, site content, donation config, donor management
superadminAll admin capabilities + manage other admin accounts

Feature Flags​

Feature flags let you toggle functionality for all users without a code deployment.

Available Flags​

Flag KeyDefaultDescription
enableGoogleAuthtrueShow Google sign-in button
enableEmailAuthtrueShow email/password sign-in
enableIssueReportingtrueAllow citizens to file new issues
enableBottomNavtrueShow mobile bottom navigation bar
enableReportButtontrueShow "Report Issue" CTA on landing
enableMediaUploadtrueAllow photo/video attachments on issues
enableGeoTaggingtrueAttach GPS coordinates to issue reports
enableExifExtractiontrueAuto-extract EXIF metadata from photos

Text Overrides​

From the Flags view you can also update copy:

KeyDescription
appTitlePrimary app name (e.g. "CivicPulse")
appTaglineHero subtitle
appSubtitleSupporting text under tagline
reportBtnLabelLabel on the main CTA button
landingHeroShort 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:

FieldMax LengthDescription
App Title60Displayed in <title> and landing hero
Tagline120Hero subtitle
Subtitle200Supporting hero text
Contact Email120Used in footer / contact links
GitHub URL200Link in Collaborate section

Donations​

Configuring the Donation Section​

  1. Navigate to Donation in the admin sidebar.
  2. Upload a QR code image (PNG/JPG, ≀2 MB) β€” stored as base64 in DynamoDB.
  3. Enter your UPI ID (e.g. civicpulse@upi).
  4. Toggle Enable donation section on.
  5. Click Save config.

The section appears on the landing page automatically once enabled.

Managing the Donor Wall​

Each donor entry has:

FieldDescription
NameDonor display name
AmountDonation amount (INR)
X HandleTwitter/X username (without @)
MessageOptional thank-you message
Show NameWhether to display this donor publicly
Show ProfileWhether 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​

TabDescription
OfficialsCRUD table β€” add/edit/delete entries, toggle verified status, filter by city / category / verified
SubmissionsReview crowd-sourced submissions from the public form β€” approve or reject with one click
Import CSVPaste 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​

MethodPathDescription
GET/api/ward-lookup?city=X&pincode=X&category=X β€” returns verified officials for issue assignment
POST/api/ward-officials/submitPublic crowd-sourced submission (rate-limited: 10/hr per IP)

Admin Endpoints (Bearer required)​

MethodPathDescription
GET/api/admin/ward-dataList all officials (with optional filters)
POST/api/admin/ward-dataAdd a new official
PUT/api/admin/ward-data/:idUpdate official
DELETE/api/admin/ward-data/:idDelete official
GET/api/admin/ward-data/export.csvDownload all officials as CSV
POST/api/admin/ward-data/importBulk import via CSV text in body
GET/api/admin/ward-data/submissionsList pending submissions
POST/api/admin/ward-data/submissions/:id/approveApprove and promote a submission to verified
POST/api/admin/ward-data/submissions/:id/rejectReject 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 403 on 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​

AttributeTypeDescription
adminIdString (PK)UUID with ADM# prefix
emailStringUnique login email
passwordHashStringbcrypt hash (cost 12)
nameStringDisplay name
roleStringadmin or superadmin
activeBooleanAccount enabled/disabled
createdAtStringISO 8601 timestamp

civicpulse-site-config​

AttributeTypeDescription
configKeyString (PK)e.g. flags, app-config, donation-config
valueMapJSON-serialisable config object
updatedAtStringISO 8601 timestamp

civicpulse-donors​

AttributeTypeDescription
donorIdString (PK)UUID with DON# prefix
nameStringDonor name
amountNumberDonation amount
xHandleString (opt.)Twitter handle
imageUrlString (opt.)Cached profile image URL
showNameBooleanShow publicly
showProfileBooleanShow X profile picture
messageString (opt.)Donor message
createdAtStringISO 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 PK which maps to adminId, configKey, and donorId respectively in the application layer.


API Reference​

Auth​

MethodPathAuthDescription
POST/api/admin/auth/loginNoneIssue admin JWT
POST/api/admin/auth/logoutBearerInvalidate session (client-side)
GET/api/admin/auth/meBearerCurrent admin profile

Feature Flags​

MethodPathAuthDescription
GET/api/admin/flagsBearerGet all flags + text overrides
PUT/api/admin/flagsBearerSave flags + text overrides
GET/api/flagsNonePublic flags endpoint (60s CDN cache)

Site Config​

MethodPathAuthDescription
GET/api/admin/configBearerGet site content config
PUT/api/admin/configBearerUpdate site content config
GET/api/admin/donation-configBearerGet donation settings
PUT/api/admin/donation-configBearerUpdate donation settings
GET/api/donation-config/publicNonePublic donation config (safe fields only)

Donors​

MethodPathAuthDescription
GET/api/admin/donorsBearerList all donors (admin view)
POST/api/admin/donorsBearerAdd a new donor
PUT/api/admin/donors/:idBearerUpdate donor
DELETE/api/admin/donors/:idBearerDelete donor
POST/api/admin/donors/:id/refresh-picBearerRe-fetch X profile picture
GET/api/donors/publicNonePublic donor list (filtered, 300s cache)

Admin Management​

MethodPathAuthDescription
GET/api/admin/adminsSuperadminList all admins
POST/api/admin/adminsSuperadminCreate new admin
PATCH/api/admin/admins/:id/activeSuperadminEnable/disable admin
DELETE/api/admin/admins/:idSuperadminDelete 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)