A pedagogical yet production-quality voting backend in native PHP 8.1+ and MySQL/InnoDB.
Demonstrates: SHA-256 chain hashing · ACID transactions · secure auth · PDO · clean MVC.
voting-system/
├── schema.sql ← MySQL DDL (run first)
├── bootstrap.php ← PDO singleton, autoloader, helpers
├── .htaccess ← Apache security headers + routing
│
├── config/
│ └── database.php ← DB constants (override via ENV)
│
├── public/
│ └── index.html ← Minimal SPA frontend
│
├── src/
│ ├── Repositories/ ← Data Access Layer (PDO only)
│ │ ├── UserRepository.php
│ │ ├── VoteRepository.php ← getLastHash, insertWithChain
│ │ └── LogRepository.php
│ │
│ ├── Services/ ← Business Logic Layer
│ │ ├── AuthService.php ← register, login (bcrypt cost=12)
│ │ ├── VoteService.php ← SHA-256 chain + TX insertion
│ │ └── AuditService.php ← Full chain verification
│ │
│ └── Controllers/ ← HTTP layer (thin dispatchers)
│ ├── AuthController.php
│ ├── VoteController.php
│ └── AuditController.php
│
└── api/ ← Entry points (bootstrap + delegate)
├── auth/
│ ├── register.php ← POST
│ ├── login.php ← POST
│ └── logout.php ← POST
├── vote/
│ ├── submit.php ← POST (auth required)
│ └── results.php ← GET (public)
└── audit/
├── chain.php ← GET (public)
└── logs.php ← GET (auth required)
mysql -u root -p < schema.sql# Either edit config/database.php, or set environment variables:
export DB_HOST=localhost
export DB_NAME=voting_system
export DB_USER=your_user
export DB_PASS=your_passwordApache (with mod_rewrite):
<VirtualHost *:80>
DocumentRoot /path/to/voting-system
AllowOverride All
</VirtualHost>PHP built-in server (development only):
cd voting-system
php -S localhost:8000 -t .
# Then open http://localhost:8000| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Register a new user |
| POST | /api/auth/login |
No | Login, receive session cookie |
| POST | /api/auth/logout |
No | Destroy session |
| POST | /api/vote/submit |
Yes | Cast vote (one per user) |
| GET | /api/vote/results |
No | Aggregated results |
| GET | /api/audit/chain |
No | Full chain + tamper detection |
| GET | /api/audit/logs |
Yes | Audit event log |
# Register
curl -X POST http://localhost:8000/api/auth/register.php \
-H 'Content-Type: application/json' \
-d '{"username":"alice","email":"alice@example.com","password":"secret123"}'
# Login
curl -c cookies.txt -X POST http://localhost:8000/api/auth/login.php \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"secret123"}'
# Vote
curl -b cookies.txt -X POST http://localhost:8000/api/vote/submit.php \
-H 'Content-Type: application/json' \
-d '{"choice":"candidate_a"}'
# Verify chain integrity
curl http://localhost:8000/api/audit/chain.phppassword_hash()with bcrypt cost=12password_verify()with constant-time comparison- Dummy hash for timing-safe login failures (prevents user enumeration)
session_regenerate_id(true)on loginHttpOnly,SameSite=Strict,Securecookie flags
UNIQUE(user_id)constraint invotestable (DB-level guarantee)- Backend
hasVoted()check inside the same transaction (application-level) - Both layers must fail for a duplicate vote to succeed — defence in depth
hash_n = SHA256( user_id | choice | timestamp | hash_{n-1} )
- Chain starts with a genesis hash (64 zeros) — same pattern as Bitcoin
AuditService::verifyChain()recomputes every hash from stored fields- Any field mutation (user_id, choice, timestamp, or previous_hash) breaks all subsequent hashes
UNIQUE(hash)prevents hash collision injection
- InnoDB engine for row-level locking and ACID transactions
- All queries via PDO prepared statements — zero string interpolation
PDO::ATTR_EMULATE_PREPARES = falseforces real prepared statements- Foreign key
votes.user_id → users.idwithON DELETE RESTRICT
- Security headers (CSP, X-Frame-Options, HSTS-ready, nosniff)
- Method validation on every endpoint
- No raw SQL errors exposed to clients
- Add a candidate: edit
VoteService::VALID_CHOICES - Admin role: add
rolecolumn tousers, gateaudit/logs.phponrole = 'admin' - Rate limiting: add IP-based throttle in
bootstrap.phpusing Redis or a DB table - HTTPS: uncomment
Strict-Transport-Securityin.htaccessand set'secure' => truein session params