Enterprise-Grade Real-Time NFC Access Control, Hardware Simulation & Administrative Suite
Monorepo powered by Node.js, Express, MongoDB, React 19, Tailwind CSS v4, and DaisyUI 5.
The NFC-Based Secure Access Control System is a unified, enterprise-grade security solution designed for universities, corporate campuses, and restricted facilities. It provides end-to-end management of physical access control—from physical NFC card taps at reader stations to card lifecycle management, role-based authorization, time-window constraints, anti-passback anti-tailgating enforcement, and real-time security audit logging.
The system is structured as a modern monorepo comprising:
- Admin Web Dashboard (
frontend/admin-web): High-density management interface for security personnel to manage cardholders, monitor live taps, revoke cards, issue temporary access, and inspect audit logs. - Scanner Web Application (
frontend/scanner-web): Emulated physical hardware reader terminal interface deployed at access points. - Simulation Web Application (
frontend/simulation-web): Interactive tap simulator for testing physical NFC card taps under varying access scenarios. - Core Express API Backend (
backend): High-performance RESTful API implementing the security evaluation engine, anti-passback algorithm, card lifecycle state machine, and MongoDB persistence.
To allow evaluators to test every single flow end-to-end, run npm run seed to populate the database with pre-configured demo accounts:
# Seed database with complete test datasets
npm run seed| Role | Demo Email | Password | Card UID | Unlocked Application Features & Testing Flow |
|---|---|---|---|---|
| ** System Admin** | admin@university.edu.ng |
AdminPassword123! |
ADM88888 |
Full Enterprise Control: Live Overview Dashboard, Cardholder CRUD, Card Revocation & Replacement, Temporary Access Approvals, Role Permissions, Security Audit Log Inspection. |
| ** Academic Staff (Active)** | staff@university.edu.ng |
StaffPassword123! |
STF10001 |
Staff Portal: Personal Access History, Activity Timeline, Card Replacement Requests, Security Notifications, Temp Access Requests. |
| ** Academic Staff (Replacement & Temp Ticket)** | staff2@university.edu.ng |
StaffPassword123! |
STF10002 |
Advanced Staff Flow: Linked replacement history from old UID STF09999, pending Server Room access ticket (TKT-8849). |
| ** Student (Active)** | student@university.edu.ng |
StudentPassword123! |
STD20001 |
Student Portal: Limited Access History, Time-Window Access, Personal Tap Timeline. |
| ** Student (Suspended / Lost Card)** | student2@university.edu.ng |
StudentPassword123! |
STD20002 |
Security Failure Scenario: Card status suspended / lost. Simulating a tap with STD20002 triggers access denial and logs incident in Audit Logs. |
- Scenario 1 — Live NFC Tap & Verification:
- Open Tap Simulator (
simulation-webon port:5175). - Select Reader
RD-001 (Main Gate)and enter UIDSTF10001(Dr. Abdullahi Isa). Tap card. - Result: Access Granted logged instantly in real-time.
- Open Tap Simulator (
- Scenario 2 — Anti-Passback Violation:
- Tap
STF10001again atRD-001with directionentry. - Result: Access Denied (
anti_passback_violation) logged in Audit Logs.
- Tap
- Scenario 3 — Card Revocation & Instant Invalidation:
- Log into Admin Dashboard (
admin-webon port:5173) asadmin@university.edu.ng. - Navigate to Card Management and suspend card
STF10001. - Re-test tap in Tap Simulator ➔ Access immediately Denied.
- Log into Admin Dashboard (
- Scenario 4 — Temporary Access Request & Approval Workflow:
- Log in as
staff2@university.edu.ng(Dr. Amina Bello) and view submitted temporary access ticketTKT-8849. - Log in as Admin ➔ Navigate to Temporary Access ➔ Click Approve Ticket.
- Dr. Amina Bello's NFC access permissions update in real-time.
- Log in as
What makes this access control system stand out from generic CRUD applications:
Every NFC tap received at /api/access/tap passes through a non-bypassable 8-stage verification pipeline before granting entry:
[NFC Tap Request]
└─► 1. Reader Registration & Active Status Check
└─► 2. Card UID Existence Lookup
└─► 3. Card Status Verification (Must be 'Active')
└─► 4. Cardholder Account Status (Must be active & not suspended)
└─► 5. Role-Based Access Control (RBAC Check against Reader.allowedRoles)
└─► 6. Time-Window & Schedule Validation (Reader.accessSchedule)
└─► 7. State-Aware Anti-Passback Evaluation (Direction entry/exit sequence)
└─► 8. Immutable AccessLog Audit Record Commit
If any stage fails, the system immediately rejects access with an explicit denial code (invalid_reader, invalid_card, card_inactive, unauthorized_zone, outside_time_window, anti_passback_violation) and records the incident in the audit database.
To prevent tailgating, card sharing, and unauthorized entry duplication, the backend evaluates the user's historical directional state:
- If a cardholder's last recorded valid tap at a zone was an
entry, their next tap at that zone must be anexit. - Attempting an
entrytwice in succession without a matchingexittriggers an immediateanti_passback_violation, flagging a potential security breach.
Cards are modeled with a formal state machine (Active ➔ Lost | Stolen | Revoked | Replaced | Expired):
- Linked Replacement Chain: When issuing a replacement card, the old card transitions to
replacedand receives areplacedByreference to the new card, maintaining a complete, auditable lineage of card re-issuances over a cardholder's tenure. - Instant Revocation Propagation: Deactivating a user or flagging a card as lost/stolen instantly invalidates physical reader taps across all campus doors without requiring a reader reboot.
By maintaining admin-web, scanner-web, simulation-web, and backend within an npm workspaces monorepo:
- Developers can launch the entire ecosystem concurrently with a single command (
npm run dev). - Frontend applications share backend interface definitions, API client helpers, and styling design tokens.
- Admin Dashboard: Features quick period selectors (
24H,7D,30D), real-time metric cards with trend indicators, searchable activity feed with type filters (All,Taps,Admin), and administrative quick action shortcuts. - Interactive Audit Inspector: High-contrast, sticky-header table supporting multi-field instant search (UID, user, reader, door), result/role filter dropdowns, client-side pagination, Excel export (
xlsx), and an inline Log Inspection Modal for deep transaction analysis.
graph TB
subgraph Monorepo ["Monorepo Workspaces"]
subgraph Frontends ["Frontend Applications"]
ADMIN["Admin Web Dashboard\n:5173 / :3000"]
SCANNER["Scanner Web Terminal\n:5174"]
SIM["Tap Simulator\n:5175"]
end
subgraph Backend ["Express REST API :5000"]
MW["Middleware Layer\nJWT · RBAC · Rate Limit · Helmet"]
ROUTES["Route Controller Layer\nAuth · Cards · Users · Readers · Access"]
ENGINE["Security Verification Pipeline\nAnti-Passback · Schedule · State Machine"]
end
subgraph Database ["MongoDB Database"]
USERS[(Users)]
CARDS[(Cards)]
READERS[(Readers)]
LOGS[(AccessLogs)]
end
end
ADMIN & SCANNER & SIM -- "HTTP / REST API" --> MW
MW --> ROUTES
ROUTES --> ENGINE
ENGINE --> USERS & CARDS & READERS & LOGS
flowchart TD
A["NFC Card Tap Received\n{ uid, readerId, direction }"] --> B{"Reader Valid\n& Online?"}
B -- No --> Z1["DENIED: invalid_reader"]
B -- Yes --> C{"Card Found\nby UID?"}
C -- No --> Z2["DENIED: invalid_card"]
C -- Yes --> D{"Card Status\n== 'active'?"}
D -- No --> Z3["DENIED: card_inactive"]
D -- Yes --> E{"User Role Allowed\nfor Reader Zone?"}
E -- No --> Z4["DENIED: unauthorized_zone"]
E -- Yes --> F{"Tap Within\nTime Window?"}
F -- No --> Z5["DENIED: outside_time_window"]
F -- Yes --> G{"Anti-Passback\nDirection Valid?"}
G -- No --> Z6["DENIED: anti_passback_violation"]
G -- Yes --> H["GRANTED: Access Allowed"]
Z1 & Z2 & Z3 & Z4 & Z5 & Z6 & H --> LOG["Write Immutable AccessLog Record"]
stateDiagram-v2
[*] --> Active : Card Issued
Active --> Lost : Reported Lost
Active --> Stolen : Reported Stolen
Active --> Revoked : Admin Revoked / User Left
Active --> Replaced : Replacement Issued
Active --> Expired : Validity Period Ended
Lost --> Active : Card Recovered
Lost --> Replaced : Replacement Issued
Stolen --> Revoked : Permanently Disabled
Stolen --> Replaced : Replacement Issued
Replaced --> [*]
Revoked --> [*]
Expired --> [*]
nfc-access-control/
├── frontend/
│ ├── admin-web/ # React 19 + Vite + Tailwind CSS v4 + DaisyUI 5
│ │ ├── src/
│ │ │ ├── Api/ # Axios API service client layer
│ │ │ ├── component/ # Layout & Shared UI (DashboardLayout, Notification)
│ │ │ ├── pages/
│ │ │ │ ├── Auth/ # Login, Register, Password Reset
│ │ │ │ └── dashboard/admin/# Admin Dashboard, Audit Logs, Users, Cards, Roles
│ │ │ └── App.jsx # React Router setup
│ │ └── package.json
│ ├── scanner-web/ # Physical Reader Terminal Interface
│ └── simulation-web/ # NFC Tap Simulator Page
│
├── backend/ # Node.js + Express REST API (MVC Pattern)
│ ├── src/
│ │ ├── config/ # DB connection & Constants
│ │ ├── controllers/ # Auth, User, Card, Reader & Access Controllers
│ │ ├── middlewares/ # JWT Auth, RBAC Authorization, Rate Limiter, Error Handler
│ │ ├── models/ # Mongoose Schemas (User, Card, Reader, AccessLog)
│ │ ├── routes/ # Express API Route Definition
│ │ ├── services/ # Access Verification & Card State Machine Business Logic
│ │ └── server.js # Express Server Entry Point
│ └── package.json
│
├── ARCHITECTURE.md # System Architecture Blueprint
├── package.json # Monorepo root scripts & workspaces definition
└── README.md # Project Documentation
| Method | Endpoint | Auth | Role | Description |
|---|---|---|---|---|
POST |
/api/auth/login |
None | Public | Authenticate user & return JWT token |
POST |
/api/auth/register |
None | Public | Register new user account |
GET |
/get/profile |
JWT | Any | Get current user profile |
POST |
/api/access/tap |
Reader Payload | System | Core NFC Tap Verification Endpoint |
GET |
/access/logs |
JWT | Admin/Security | Fetch security audit logs |
GET |
/dashboard/stats |
JWT | Admin | Fetch aggregate security statistics |
GET |
/dashboard/recent-activity |
JWT | Admin | Fetch live recent tap and admin events |
GET |
/api/cards |
JWT | Admin | List all NFC card records |
POST |
/api/cards |
JWT | Admin | Register & issue new NFC card |
PUT |
/api/cards/:id/lost |
JWT | Admin | Mark card as lost |
PUT |
/api/cards/:id/stolen |
JWT | Admin | Mark card as stolen & revoke |
PUT |
/api/cards/:id/replace |
JWT | Admin | Issue replacement card & link reference |
GET |
/api/Admin/get/all-staff |
JWT | Admin | List all staff members |
- Node.js:
v18.xorv20.xhigher - npm:
v9.xor higher - MongoDB: Local MongoDB instance running on
mongodb://localhost:27017or MongoDB Atlas URI
git clone https://github.com/Yamuhammad01/NFC-Based-Secure-Access-Control-System.git
cd NFC-Based-Secure-Access-Control-System
# Install dependencies across all monorepo workspaces
npm installCreate a .env file inside the backend/ directory:
PORT=5000
MONGO_URI=mongodb://localhost:27017/nfc_access_control
JWT_SECRET=your_super_secret_jwt_key_here
NODE_ENV=development
# Optional — persistent profile-photo uploads (required on Vercel)
# CLOUDINARY_URL=cloudinary://<api_key>:<api_secret>@<cloud_name>Run all micro-frontends and backend concurrently using the monorepo root command:
npm run devThis launches:
- Backend REST API:
http://localhost:5000 - Admin Web Dashboard:
http://localhost:5173(or Vite assigned port) - Scanner Terminal:
http://localhost:5174 - NFC Tap Simulator:
http://localhost:5175
Note:
npm run devmust be run from the repository root, after a root-levelnpm install. Running it from a subfolder likefrontend/also works (npm walks up to the rootpackage.json), but only if the root devDependency is installed.If you see
'concurrently' is not recognized as an internal or external command, the root install never completed — fix it with:cd <repository-root> # the folder containing the root package.json npm install
# Build all workspaces for production
npm run build
# Run build specifically for admin frontend
npm --prefix frontend/admin-web run build
# Lint codebases
npm run lintProfile-photo uploads support two modes, and both can be verified locally — no deployment required:
cd backend
# Test whichever mode backend/.env currently selects
npm run verify:upload
# Force the Cloudinary path (requires CLOUDINARY_URL in backend/.env)
npm run verify:upload -- --cloud
# Force the local-disk fallback (no credentials needed)
npm run verify:upload -- --localEach run boots the real API on a spare port, logs in as a throwaway user, uploads a
real multipart image to /api/add/profilePhoto, then asserts that the stored photo is
publicly fetchable, that replacing it destroys the previous asset, and that the
database holds the newest value. Every test artifact (user, Cloudinary assets, local
files) is removed afterwards.
To exercise the cloud path locally, put real credentials in
backend/.env:CLOUDINARY_URL=cloudinary://<api_key>:<api_secret>@<cloud_name>(Cloudinary Dashboard → API Keys → "API environment variable").
This project is licensed under the MIT License.