| Component | Version | Purpose |
|---|---|---|
| PHP | 8.x | Application language — procedural style with classes for DB, Session, and Media |
| MariaDB | 10.x | Relational database — 17 tables, InnoDB engine, foreign key constraints with CASCADE |
| Apache | 2.x | Web server with .htaccess directory protection and PHP module |
| Component | Version | Location | Purpose |
|---|---|---|---|
| Bootstrap | 5.x | libs/bootstrap/ |
CSS framework for responsive layout, forms, tables, alerts, modals |
| Bootstrap Icons | 1.x | libs/icons/ |
SVG icon sprite — referenced via <use href="...bi.svg#icon-name"> |
| jQuery | 3.x | libs/js/jquery.min.js |
DOM manipulation, AJAX for product/customer autocomplete search |
| Datepicker | 1.x | libs/datepicker/ |
Bootstrap Datepicker for sales report date-range selection |
| Main CSS | — | libs/css/main.css |
Custom application styles (sidebar, header, page layout) |
- All CSS, JS, fonts, and icons are bundled in the repository
- No external CDN URLs in any
<link>or<script>tag - Works on fully air-gapped networks — no internet required
- jQuery (minified) is the only JavaScript framework; the rest is vanilla JS
| Feature | Implementation | File |
|---|---|---|
| Password hashing | password_hash(PASSWORD_BCRYPT) with auto-upgrade of legacy SHA1 on login; password_needs_rehash() on every login |
includes/sql.php (authenticate()) |
| SQL injection prevention | prepare_query() / prepare_select() with bound ? parameters throughout |
includes/database.php |
| CSRF protection (POST) | Per-session token via random_bytes(32), verified with hash_equals() in verify_csrf() |
includes/functions.php |
| CSRF protection (GET deletes) | csrf_url_param() on delete links; verify_get_csrf() in handler |
includes/functions.php |
| Session hardening | httponly, samesite=Lax, strict_mode, secure when is_secure_context() is true — set before session_start() |
includes/session.php |
| Session fixation | session_regenerate_id(true) on every login |
includes/session.php |
| Session idle timeout | check_session_timeout() destroys idle sessions after SESSION_TIMEOUT_MINUTES (default 30) |
includes/session.php, .env |
| XSS prevention | h() (htmlspecialchars ENT_QUOTES wrapper) on all dynamic output |
includes/functions.php |
| Output sanitization | remove_junk() pipeline: nl2br → trim → stripslashes → strip_tags → htmlspecialchars |
includes/functions.php |
| Input sanitization | sanitize_input($str, $max_length=2000) for POST values: trim + strip tags + length cap |
includes/functions.php |
| CSP headers | default-src 'self', script-src 'self', style-src 'self', frame-ancestors 'none' — no unsafe-inline/eval; emitted on every authenticated response |
layouts/header.php |
| Security headers | X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (camera/mic/geo off) |
layouts/header.php |
| Login rate limiting | 5 attempts per IP per 15-min window via failed_logins table; auto-cleared on success |
includes/sql.php, users/auth.php |
| Password complexity | validate_password(): min 8 chars, requires letter + digit, common-password denylist |
includes/functions.php |
| Password reset (self-service) | Token-based: 64-char random, bcrypt-hashed in password_resets, 1-hour expiry, single-use (atomic consume) |
includes/sql_password_reset.php, migration 022 |
| Per-module permissions (RBAC) | can($module, $action) / require_permission() — Admin bypass, then permissions table override, then role defaults |
includes/sql.php, migration 024 |
| Audit log | audit($module, $action, $record_id, $summary) records CRUD + login/logout to audit_log with user/IP |
includes/sql.php, migration 023, users/audit.php (viewer) |
| Soft-delete | Reversible delete on 5 tables; hard DELETE requires explicit purge by Admin | includes/sql.php, migrations 005–009 |
| Multi-tenant isolation | All queries scoped to $_SESSION['active_org_id']; join_product_table() / find_all() auto-filter by org_id; HARNESS_ORG used in tests |
includes/sql.php, migrations 010–021 |
| Directory listing | .htaccess blocks indexing in includes/, uploads/, and project root |
.htaccess files |
| Activity logging | All page requests logged with user_id, IP, action, timestamp | includes/sql.php (logAction()) |
| Tool | Version | Purpose | Config |
|---|---|---|---|
| Composer | 2.x | PHP dependency manager — dev dependencies only (not used at runtime) | composer.json |
| PHPUnit | 11.x | Unit and integration test runner | tests/ — invoked via tests/run.sh or vendor/bin/phpunit |
| Tool | Purpose | Config |
|---|---|---|
| GitHub Actions | php -l lint + full test suite on push/PR |
.github/workflows/ci.yml |
| Pre-commit hook | php -l on staged PHP files, blocks commit on syntax error |
.githooks/pre-commit (opt-in: bash scripts/install-hooks.sh) |
| Test suite | Two runners: legacy harness (test() / check(), 7 suites via tests/run.sh) + PHPUnit (CSRF, Session, PasswordReset, Permissions, Health, Backup, LogRotate, InfraSmoke). Playwright UI tests run separately. All use HARNESS_ data isolation. |
tests/run.sh, phpunit.xml |
- Primary: Raspberry Pi 5 running Apache + MariaDB (Raspberry Pi OS)
- Also compatible: Any Debian/Ubuntu LAMP stack
- Memory: Minimal — typical PHP memory_limit of 128M is sufficient
- Storage: Proportional to product images and sales history; database starts under 1MB
Configuration is via .env file in the project root (parsed by includes/config.php):
DB_HOST=localhost
DB_USER=root
DB_PASS=
DB_NAME=inventory
APP_SECRET=<random 64-char hex>
SESSION_TIMEOUT_MINUTES=30
APP_SECRET— used for CSRF token derivation (generate withopenssl rand -hex 32)SESSION_TIMEOUT_MINUTES— idle timeout enforced bycheck_session_timeout()(default 30; set to0to disable for dev).envis git-ignored;.env.exampleis the committed template- Constants defined:
DB_HOST,DB_USER,DB_PASS,DB_NAME,APP_SECRET,SESSION_TIMEOUT - Currency code is stored in the
settingsDB table (key:currency_code, defaultUSD). Change it in the app via Settings (Admin-only).LOGIN_MAX_ATTEMPTSandLOGIN_WINDOW_SECONDScan be overridden by defining them beforesql.phploads.