Fast to run. Easy to read. Tiny PHP API + React client for employee vacation requests with a manager dashboard.
- 1) Quick start
- 2) What’s inside
- 3) Configuration
- 4) API Overview (session-based)
- 5) Frontend (React + Vite)
- 6) Example requests (cURL)
- 7) Security notes
- 8) Troubleshooting
- 9) Code style & comments
- 10) License
- 11) Credits
# requirements
php -v # PHP 8.4+
composer -V # Composer
node -v # Node 18+ recommended
npm -v
# install deps
composer install
npm installphp bin/migrate.php- Applies
schema.sqland seeds a manager iffusersis empty:- Email:
manager@example.com - Password:
pass - Role:
manager - Random
employee_code
- Email:
Change this credential immediately.
# Terminal A — PHP API (http://localhost:8000)
php -S localhost:8000 -t public# Terminal B — Vite dev (http://localhost:5173)
npm run devCORS allows
http://localhost:5173with credentials (cookies).
public/
index.php # Minimal API front controller (sessions, CORS, routes)
src/
db.php # PDO bootstrap (App\DB::pdo) + tiny legacy helpers
bin/
migrate.php # One-shot schema apply + seed
schema.sql # Tables: users, vacation_requests, etc.
frontend/
src/
App.jsx # Router (Login / Register / Employee / Manager)
api.js # Fetch wrapper + typed endpoints
pages/
Login.jsx
Register.jsx
EmployeeHome.jsx # My requests, create modal, filters, pagination
ManagerHome.jsx # Tabs: Requests & Users (approve/reject, CRUD)
components/
Brand.jsx # Branding stub (logo/title)
Paths can vary slightly; code assumes these conventions.
- Default SQLite:
var/app.sqlite(project root). - Override via
DB_DSN:
# .env (optional; loaded by bin/migrate.php if phpdotenv is installed)
DB_DSN=sqlite:/absolute/path/to/app.sqlite
# MySQL:
# DB_DSN="mysql:host=127.0.0.1;port=3306;dbname=vacay;charset=utf8mb4"
# Postgres:
# DB_DSN="pgsql:host=127.0.0.1;port=5432;dbname=vacay"
src/db.php PDO options:
PDO::ATTR_ERRMODE = EXCEPTIONPDO::ATTR_DEFAULT_FETCH_MODE = FETCH_ASSOC
SQLite note (legacy db()): PRAGMA foreign_keys = ON.
- Allowed origin:
http://localhost:5173 - Credentials enabled
- Cookie:
httponly=true,samesite=Lax
Production tips
- Serve FE + API under the same origin (best).
- If cross-origin, set exact
Access-Control-Allow-Origin, enable HTTPS,Securecookies.
Base during dev: http://localhost:8000
JSON requests require Content-Type: application/json.
Errors: { "error": "Message" }.
- POST
/login→{email,password}→ sets session, returns{ id, name, email, role, employee_code } - POST
/logout→{ ok: true } - GET
/me→ current user or 401
- POST
/register→{ name, email, password } - Validates: name, valid email, password ≥ 6, unique email
- Creates
employee+ generatedemployee_code - 201
{ ok: true, employee_code: "123-456-789" }
- GET
/me/requests→ list of own requests - POST
/me/requests→{ date_from:"YYYY-MM-DD", date_to:"YYYY-MM-DD", reason }→ 201{ id }
- GET
/admin/users→ list{ id, name, email, role, employee_code, created_at } - POST
/admin/users→{ name, email, password, role?, employee_code? }→ 201{ id, employee_code } - PUT
/admin/users/{id}→ partial{ name?, email?, password? }→{ ok: true } - DELETE
/admin/users/{id}→ 204 No Content (expects FK cascade)
- GET
/admin/requests→ all requests joined with user info - POST
/admin/requests/{id}/approve - POST
/admin/requests/{id}/reject→{ ok: true }
-
src/api.js:- Base:
/api(server strips/apiinpublic/index.php) credentials: "include"for session cookies- Throws
Error(message)on non-2xx (surfaces{error})
- Base:
-
Pages:
Login.jsx→api.login→ redirect:manager→/manager- else →
/employee
Register.jsx→ public sign-up (UI role radio is ignored by backend)EmployeeHome.jsx→ list/filter/paginate + create modalManagerHome.jsx→ tabs:- Requests: approve/reject
- Users: list, create (modal), edit (modal), delete
Frontend dependencies (install first):
Before running the app, install the frontend dependencies and React Router:
# from the frontend folder (adjust if your src/ is elsewhere)
npm i
# install React Router (routing for /login, /register, /employee, /manager)
npm i react-router-domDev:
npm run dev
# open http://localhost:5173Build:
npm run build# Login (store cookies)
curl -i -c cookie.txt -H "Content-Type: application/json" -d '{"email":"manager@example.com","password":"pass"}' http://localhost:8000/login
# Me (authenticated)
curl -b cookie.txt http://localhost:8000/me
# Create employee (manager)
curl -b cookie.txt -H "Content-Type: application/json" -d '{"name":"Alice","email":"alice@example.com","password":"secret"}' http://localhost:8000/admin/users
# List all requests (manager)
curl -b cookie.txt http://localhost:8000/admin/requests
# Approve a request (manager)
curl -b cookie.txt -X POST http://localhost:8000/admin/requests/42/approve- Passwords:
PASSWORD_DEFAULT(argon/bcrypt per PHP build) - Sessions: HttpOnly,
SameSite=Lax(useSecureunder HTTPS) - CORS: dev locked to
http://localhost:5173 - Errors:
display_errors=0, PHP errors → exceptions → JSON 500 - AuthZ:
require_auth()for employee,require_manager()for admin - Validation: email via
FILTER_VALIDATE_EMAIL, password length, uniqueness (409)
Hardening (prod)
- Rate limit
/login, brute-force protection - CSRF protection if cross-origin
- Rotate seed credentials; enforce strong passwords
- HTTPS-only; HSTS; audit admin actions
- CORS: use FE at
http://localhost:5173and API athttp://localhost:8000, or configure a Vite proxy - Session not sticking: ensure browser allows cookies (cross-origin in dev); or proxy API through Vite
- “DB not initialized”: run
php bin/migrate.php, checkDB_DSN - 409 email in use: use another email or delete the user (manager)
- 204 on DELETE: expected (empty body)
- SQLite locks: avoid concurrent writers; consider MySQL/Postgres
- Top-of-file headers for PHP + functions/
ifcomments - PHP formatted PSR-12 without behavior changes
- React components documented with JSDoc + inline guard comments
Unlicensed/internal by default. Add a license if distributing.
Author: Christos Polimatidis
Date: 2025-11-01