- Bcrypt (cost 12) in production, cost 4 under
phpunit.xmlfor fast tests. - Password rules: minimum 12 characters, mixed case, digit, symbol. The
uncompromised()haveibeenpwned check is enabled in production only — we don't want test runs to depend on outbound HTTP. - Login throttling: 5 failed attempts per minute per (email + IP). After
5 cumulative failures the account moves to
lockedfor 30 minutes. - TOTP 2FA via
pragmarx/google2fa-laravel. Recovery codes are encrypted at rest. Disabling 2FA requires the current password. - Every login attempt — success, failure, MFA required, locked — is written
to
login_histories. Successful logins are also written toaudit_logsviaaccount.login.
- Hand-rolled RBAC:
roles,permissions,permission_role,role_user. - Role assignments may be company-scoped via
role_user.company_id. Gate::beforeshort-circuits all checks forsystem_admin.- The
EnsureRoleandEnsurePermissionmiddleware are mounted on every authenticated controller route. Form Requests double-check the same permissions.
-
audit_logs.current_hash = sha256(prev_hash || canonical_json(payload) || secret). -
The previous hash is read inside a
SELECT … FOR UPDATEto avoid races. -
The runtime DB user must have no
UPDATEorDELETEonaudit_logs,activity_logs,login_histories, orapi_logs. Migrations run as a separate, more privileged user. This is the actual tamper barrier; the hash chain is the detection. -
php artisan audit:verify-chain(cron, daily 02:00 UTC) walks the chain end-to-end and reports the first row whose hash does not match. -
Tampering can be exhibited locally:
docker compose exec mysql mysql -uhr_assist -psecret hr_assist \ -e "UPDATE audit_logs SET action='TAMPERED' ORDER BY id LIMIT 1;" docker compose exec app php artisan audit:verify-chain # → "Audit chain broken — first bad row id: 1"
Standard HRs cannot record or terminate work experiences for another active HR at their own company. Only the Head HR (or a system admin) can. This prevents two Standard HRs from creating sham employment records for each other.
The Head HR's own work-experience at the company is created automatically by the system on appointment, so they never need to violate the self-block rule. On succession, the predecessor's open record is auto-terminated.
When a user claims their National ID, they upload a photo of the document.
- The file is stored under a private disk in a per-user directory.
- The admin reviews it via a server-side proxy with
Cache-Control: no-store. - On approve or reject, the file is deleted from disk. The
attachmentsrow stays for audit purposes (file_discarded = true,path = '__discarded__').
- All persistence goes through Eloquent (parameterised queries).
- Every write endpoint has a Form Request validator.
- File uploads compute and store a
sha256and use UUID-based paths; the original filename is kept only as metadata. - CSRF protection on every web POST. Sanctum bearer tokens for the API.
docker/nginx.conf ships with HSTS, X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, and a tight Permissions-Policy. CSP is intentionally
deferred until the Tailwind/Alpine CDN scripts are inlined or replaced with
Vite-built assets.
- Soft deletes on Person tables (
users,employees). national_id_verificationsandverification_requestsretain raw payloads for audit. A retention purger should be wired up before production (config/hr_assist.phpships with the policy slot).- Account deletion soft-deletes the user and removes the
employee_profilesbridge row. The canonicalemployeesrow and allwork_experiencesremain — the system is keyed by National ID, and HR records cannot be evaporated by an account holder.
APP_ENV=production,APP_DEBUG=false.- Force HTTPS at the reverse proxy.
URL::forceScheme('https')is on by default in production. - Strong, secret-manager-managed
AUDIT_HASH_SECRET. Don't rotate without a documented chain re-anchoring procedure. - DB grants: runtime user has no
UPDATE/DELETEon the four log tables. - Replace Tailwind / Alpine CDN tags with built assets and tighten CSP.
- Wire NRB credentials and run a smoke search before opening to traffic.
- Schedule
audit:verify-chainfailure alerts. - Enable
FEATURE_2FA_REQUIRED_ADMIN=true(default).