inventory/
├── index.php # Entry point / login page
├── .env.example # Environment configuration template
├── .htaccess # Apache directory listing protection
├── schema.sql # Canonical database schema (14 tables)
├── Blueprint_Overview.html # Standalone offline documentation
│
├── includes/ # Core application engine
│ ├── load.php # Bootstrap: constants, session hardening, autoload
│ ├── config.php # .env loader → DB_HOST, DB_USER, DB_PASS, DB_NAME, APP_SECRET, SESSION_TIMEOUT_MINUTES
│ ├── database.php # MySqli_DB class (connection, query, prepared statements)
│ ├── session.php # Session class (login/logout, flash, idle timeout, is_secure_context)
│ ├── functions.php # CSRF, output escaping (h), redirect, validation, sanitize_input, helpers
│ ├── sql.php # Data access layer: CRUD, auth, audit(), can()/require_permission()
│ ├── sql_password_reset.php # Password-reset token: create/validate/consume (1h expiry, bcrypt-hashed)
│ ├── upload.php # Media class (image upload/validation for products & users)
│ └── formatcurrency.php # Currency display formatting
│
├── layouts/ # Shared UI shell
│ ├── header.php # HTML <head>, top nav bar, user dropdown, sidebar router
│ ├── footer.php # Closing tags, Bootstrap JS includes
│ ├── admin_menu.php # Sidebar menu for Admin (level 1)
│ ├── special_menu.php # Sidebar menu for Supervisor (level 2)
│ └── user_menu.php # Sidebar menu for User (level 3)
│
├── customers/ # Customer CRUD module
│ ├── customers.php # List all customers
│ ├── add_customer.php # Add new customer form+handler
│ └── edit_customer.php # Edit existing customer form+handler
│
├── products/ # Product & stock management
│ ├── products.php # Product list with category filter
│ ├── add_product.php # Add product form+handler
│ ├── edit_product.php # Edit product form+handler
│ ├── view_product.php # Product detail view
│ ├── product_search.php # Search products by name/SKU/description
│ ├── categories.php # Category list
│ ├── edit_category.php # Add/edit category form+handler
│ ├── add_stock.php # Add stock (increase quantity)
│ └── edit_stock.php # View stock history for product
│
├── sales/ # Order & sale processing
│ ├── sales.php # All sales listing
│ ├── add_order.php # Create new order
│ ├── edit_order.php # Edit existing order (add/remove line items)
│ ├── orders.php # All orders listing
│ ├── add_order_by_customer.php # Create order for specific customer
│ ├── add_sale_to_order.php # Add line item to existing order
│ ├── add_sale_by_sku.php # Add sale by SKU lookup
│ ├── add_sale_by_search.php # Add sale by product search
│ ├── edit_sale.php # Edit sale line item
│ ├── delete_sale.php # Delete sale line item
│ ├── sales_by_order.php # View all sales for an order
│ ├── sales_invoice.php # Printable invoice
│ └── order_picklist.php # Printable picklist
│
├── reports/ # Sales & stock reporting
│ ├── sales_report.php # Date-range sales report form
│ ├── sale_report_process.php # Sales report results (by date range)
│ ├── daily_sales.php # Daily sales breakdown (year/month)
│ ├── monthly_sales.php # Monthly sales breakdown (year)
│ ├── stock_report.php # Stock report form
│ └── stock_report_process.php # Stock report results
│
├── users/ # User & group administration
│ ├── index.php # Login form / user dashboard
│ ├── auth.php # Login handler (CSRF + rate limit + audit on success)
│ ├── logout.php # Session teardown (audits the logout)
│ ├── add_user.php # Add user form+handler
│ ├── edit_user.php # Edit user form+handler
│ ├── edit_account.php # Edit own account
│ ├── change_password.php # Change password form+handler
│ ├── forgot_password.php # Request password reset (token email/log)
│ ├── reset_password.php # Token-gated password reset form+handler
│ ├── profile.php # User profile view
│ ├── add_group.php # Add user group form+handler
│ ├── edit_group.php # Edit user group form+handler
│ ├── users.php # User list
│ ├── group.php # Group list
│ ├── delete_user.php # Soft-delete a user
│ ├── delete_group.php # Delete a user group
│ ├── settings.php # Admin-only app settings (currency code, etc.)
│ ├── log.php # Activity log viewer (raw page hits)
│ ├── audit.php # Audit log viewer (CRUD + login/logout events)
│ ├── permissions.php # Per-user / per-module RBAC override matrix (Admin)
│ ├── delete_log.php # Clear log entries
│ ├── delete_log_by_ip.php # Clear log entries by IP
│ ├── trash.php # Soft-delete trash UI (Admin only)
│ ├── restore.php # Restore a soft-deleted record
│ ├── purge.php # Permanently delete a soft-deleted record
│ ├── edit_category.php # Add/edit category form+handler (routes from admin menu)
│ ├── home.php # User home/dashboard page
│ ├── switch_org.php # Switch active organization (POST handler)
│ └── admin.php # Admin dashboard
│
├── orgs/ # Multi-tenant organization management
│ ├── orgs.php # Organization list (Admin only)
│ ├── edit_org.php # Create/edit organization form+handler
│ ├── update_org.php # Edit org POST handler
│ ├── delete_org.php # Soft-delete an org
│ ├── restore_org.php # Restore soft-deleted org
│ ├── add_member.php # Add user to org form+handler
│ ├── remove_member.php # Remove user from org
│ └── update_member.php # Update member role
│
├── migrations/ # Numbered UP/DOWN SQL migration files (001–024)
│ ├── 001–004 # Base schema: quantity INT, failed_logins, log FK, settings
│ ├── 005–009 # Soft-delete: deleted_at on users/customers/sales/orders/stock
│ ├── 010–012 # Multi-tenancy: orgs table, org_members, default org seed
│ ├── 013–018 # org_id columns on customers/products/categories/sales/orders/stock
│ ├── 019–020 # media org_id, settings org_id
│ ├── 021 # users.last_active_org_id for org switcher
│ ├── 022 # password_resets table (forgot-password tokens)
│ ├── 023 # audit_log table
│ └── 024 # permissions table (per-user RBAC overrides)
│
├── scripts/ # CLI utilities
│ ├── migrate.php # Incremental migration runner (applies pending *.up.sql via .env creds)
│ └── demo_seed.php # Idempotent demo data seeder (--clean to re-seed)
│
├── libs/ # Bundled frontend assets (no CDN)
│ ├── bootstrap/ # Bootstrap 5 CSS/JS
│ ├── icons/ # Bootstrap Icons SVG sprite (bi.svg)
│ ├── datepicker/ # Bootstrap Datepicker CSS/JS
│ ├── js/jquery.min.js # jQuery 3.x
│ └── css/main.css # Application styles (col-w-* classes, no inline styles)
│
├── uploads/ # User-uploaded media
│ ├── users/ # User profile images
│ └── products/ # Product images
│
├── vendor/ # Composer-managed dev dependencies (gitignored)
│ └── bin/phpunit # PHPUnit 11 test runner
│
└── tests/ # PHP test suites + Playwright UI tests
├── run.sh # Test runner
├── bootstrap.php # Test harness bootstrap (ob_start, HARNESS_ prefix)
├── CSRFTest.php # CSRF helpers (unit)
├── AuthTest.php # Authentication (integration)
├── CRUDTest.php # CRUD operations (integration)
├── SecurityHeadersTest.php # HTTP security headers (5 headers)
├── SessionTest.php # Idle timeout + is_secure_context() (unit)
├── SettingsTest.php # Settings class (integration)
├── SoftDeleteTest.php # Soft-delete lifecycle (integration)
├── TenancyTest.php # Multi-tenant data isolation (integration)
├── OrgManagementTest.php # Org CRUD + member management (integration)
├── PasswordResetTest.php # Token generation, expiry, single-use (integration)
├── PermissionsTest.php # can() / require_permission() + role defaults
└── ui/ # Playwright end-to-end specs
Every page request follows this sequence:
HTTP Request
│
▼
┌──────────────────────────────────────────────┐
│ 1. index.php / module page │
│ require_once 'includes/load.php' │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 2. includes/load.php (Bootstrap) │
│ • Define SITE_ROOT, LIB_PATH_INC │
│ • Session hardening (ini_set) │
│ • Autoload: config → functions → session │
│ → upload → database → sql → currency │
│ • Initialize CSRF token │
│ • Log user action (skip static assets) │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 3. includes/config.php │
│ • Resolve CONFIG_ROOT from file location │
│ • Parse .env → DB_HOST, DB_USER, DB_PASS, │
│ DB_NAME, APP_SECRET │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 4. includes/session.php │
│ • session_start() (with strict_mode) │
│ • Session class: login/logout/flash msg │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 5. includes/database.php │
│ • MySqli_DB constructor → db_connect() │
│ • $db global instance available │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 6. Module page (e.g., products/products.php) │
│ • page_require_level(N) — RBAC gate │
│ • Handle POST (CSRF verify) │
│ • Query data via sql.php helpers │
│ • Include layouts/header.php │
│ • Render HTML with h() escaping │
│ • Include layouts/footer.php │
└──────────────────────────────────────────────┘
| Level | Role | Description |
|---|---|---|
| 1 | Admin | Full access: all CRUD, user/group management, reports |
| 2 | Supervisor | Mid-tier: product/stock management, sales processing, reports |
| 3 | User | Limited: view products, basic sales entry |
-
Login —
authenticate($username, $password)insql.php:- Queries
userstable by username (prepared statement) - BCrypt:
password_verify($password, $stored_hash) - Legacy SHA1: detected by 40-char hex format, auto-rehashed to bcrypt
- On success:
$session->login($user_id)regenerates session ID (fixation protection) updateLastLogIn($user_id)records timestampaudit('users', 'login', $user_id)records the event inaudit_log
- Queries
-
Page-level authorization —
page_require_level($require_level)insql.php:- Checks
$session->isUserLoggedIn()→ redirect to login - Checks user
status(active/disabled) - Checks group
group_status(active/disabled) - Checks
$current_user['user_level'] <= $require_level(lower number = higher privilege) - On failure: flash message + redirect
- Checks
-
Per-module permissions —
can($module, $action)/require_permission($module, $action)insql.php:- Admins (level 1) bypass all checks (allow-all)
- Otherwise consults
permissionstable for an explicit row (user_id,module,action,allowed) - Falls back to role defaults: Supervisor →
view|create|edit, User →viewonly require_permission()redirects with a flash on denial
-
Session security (enforced in
session.phpbeforesession_start()):cookie_httponly = 1,cookie_samesite = 'Lax',use_strict_mode = 1cookie_securetoggled byis_secure_context()(HTTPS,X-Forwarded-Proto: https, or trusted local/private IPs)- Session ID regeneration on login (prevents session fixation)
- Idle timeout —
check_session_timeout()runs on every authenticated request;SESSION_TIMEOUT_MINUTESin.env(default 30) governs the inactivity window, after which the session is destroyed and the user is bounced to login
-
Password reset — self-service flow in
includes/sql_password_reset.php:users/forgot_password.phpaccepts username/email and creates a 64-char token (1-hour expiry, bcrypt-hashed in thepassword_resetstable)users/reset_password.phpvalidates the token and consumes it atomically (transaction: update password + mark token used)- The plaintext token is never logged (PR #52 follow-up); reset link emission is currently deferred (no SMTP wired)
-
Audit log —
audit($module, $action, $record_id, $summary)insql.php:- Records create / update / delete / login / logout events in the
audit_logtable with user, IP, and timestamp - Wrapped in try/catch so DB failures cannot white-screen login
- Viewable by Admins at Users → Audit Log (
users/audit.php)
- Records create / update / delete / login / logout events in the
-
CSRF protection — All POST forms include
csrf_field(), verified at handler entry. GET-based delete links usecsrf_url_param()+verify_get_csrf().
layouts/header.php conditionally includes the correct sidebar menu based on $user['user_level']:
- Level 1 →
admin_menu.php - Level 2 →
special_menu.php - Level 3 →
user_menu.php
Five business tables (users, customers, sales, orders, stock) have deleted_at TIMESTAMP and deleted_by INT for the soft-delete pattern. All 17 tables are fully merged into main.
| Table | Purpose | Soft-delete |
|---|---|---|
categories |
Product categories | No |
products |
Product catalog | No (gap — see gap-analysis.md) |
media |
Product/user images | No |
orders |
Sales orders | Yes |
sales |
Order line items | Yes |
stock |
Stock adjustments | Yes |
customers |
Customer directory | Yes |
users |
Login accounts | Yes |
user_groups |
RBAC group definitions | No |
log |
Page-hit activity trail | No (log.user_id → users.id ON DELETE SET NULL) |
failed_logins |
Login rate-limit tracking | No (pruned probabilistically) |
settings |
App config key/value | No |
orgs |
Organization registry | No |
org_members |
User ↔ org membership + role | No |
password_resets |
Forgot-password tokens (1h expiry, single-use) | No (FK to users ON DELETE CASCADE) |
audit_log |
CRUD + login/logout audit trail (user, module, action, IP) | No |
permissions |
Per-user / per-module RBAC overrides (allowed boolean) |
No |
Key relationships:
- products.category_id → categories.id (CASCADE)
- products.media_id → media.id
- sales.product_id → products.id (CASCADE)
- sales.order_id → orders.id
- stock.product_id → products.id
- users.user_level → user_groups.group_level (CASCADE)
- log.user_id → users.id ON DELETE SET NULL
- org_members.org_id → orgs.id (tenancy)
- org_members.user_id → users.id (tenancy)
Five tables use reversible soft-delete. The helpers live in includes/sql.php.
soft_delete_by_id($table, $id) Stamps deleted_at = NOW(), deleted_by = session user
restore_by_id($table, $id) Clears both columns (NOT NULL → NULL)
purge_by_id($table, $id) Hard DELETE — only allowed when deleted_at IS NOT NULL
find_by_id_with_deleted($table, $id) Bypasses filter (trash UI)
find_with_deleted($table) Returns all rows including soft-deleted
find_all() and find_by_id() automatically add WHERE deleted_at IS NULL when table_has_soft_delete() returns true. The probe result is cached per request in a static array, so there is exactly one SHOW COLUMNS query per table per request.
The trash UI (users/trash.php) is Admin-only (level 1). users/restore.php and users/purge.php handle the two actions.
| Component | Type | File | Purpose |
|---|---|---|---|
MySqli_DB |
Class | includes/database.php |
DB connection, raw query, prepared CRUD (prepare_query, prepare_select, prepare_select_one) |
Session |
Class | includes/session.php |
User login state, flash messaging, session ID regeneration, idle timeout |
Media |
Class | includes/upload.php |
File upload validation, image processing for users and products |
csrf_token() / verify_csrf() / verify_get_csrf() / csrf_url_param() |
Functions | includes/functions.php |
CSRF token generation, POST validation, and GET-based delete link protection |
h() |
Function | includes/functions.php |
HTML output escaping shorthand |
remove_junk() |
Function | includes/functions.php |
Output sanitization pipeline (strip tags, trim, htmlspecialchars) |
sanitize_input() |
Function | includes/functions.php |
Input sanitization for form POST values (trim, strip tags, length cap; default 2000 chars) |
is_secure_context() |
Function | includes/session.php |
Detects HTTPS / X-Forwarded-Proto / trusted local IPs so cookie_secure doesn't lock out non-HTTPS dev |
page_require_level() |
Function | includes/sql.php |
Coarse RBAC gate — all protected pages call this |
can() / require_permission() |
Functions | includes/sql.php |
Per-module RBAC (module, action) — Admin bypass, then permissions table, then role defaults |
audit() |
Function | includes/sql.php |
Records a row in audit_log (user, module, action, record_id, summary, IP); resilient (try/catch) |
authenticate() |
Function | includes/sql.php |
Username/password verification with legacy SHA1 migration |
create_reset_token() / validate_reset_token() / consume_reset_token() |
Functions | includes/sql_password_reset.php |
Password-reset token lifecycle (1h expiry, bcrypt-hashed, single-use; consume is transactional) |
find_by_id() / find_all() |
Functions | includes/sql.php |
Generic table CRUD helpers |