# Technical documentation — crimemapping_website

Evidence date: 2026-10-18. Source: `master` at `8c3c372f82a07c3b808cf75a64d2e2f49eca502a`.

## Repository layout

```text
crimemapping_website/
  index.html, index.old.html, email.php, .htaccess
  assets/                  marketing css, js, images
  org/                     saved WordPress coming-soon page
  app.crimemapping.co.za/  Create React App production build
  admin.crimemapping.co.za/
    app/                   CI4 application
    public/                CI4 front controller
    vendor/                Composer install, committed
    env                    environment sample, committed
    composer.json, spark, phpunit.xml.dist
    bitbucket-pipelines.yml
    sonar-project.properties
  bitbucket-pipelines.yml  root pipeline
  sonar-project.properties root Sonar key
```

`vendor/` is tracked. A fresh `composer install` is not required to read the tree. Windows checkouts of this repository fail on long `vendor/` paths unless Git `core.longpaths` is enabled. That is an observation from cloning this revision, not a project setting.

## Admin application

CodeIgniter 4. `composer.json` requires PHP `^8.0`, `codeigniter4/framework` `^4`, `aws/aws-sdk-php` `^3.254`, `mpdf/mpdf` `^8.1`, `phpoffice/phpspreadsheet` `^1.25`. Dev requirements are Faker, vfsStream, and PHPUnit `^9.1`. No `use` of the AWS SDK, mPDF, or PhpSpreadsheet was found under `app/`.

README server extensions: intl, mbstring, mysqlnd, json, xml, and libcurl when the HTTP client is used.

`app/Config/Routes.php`:

- Default controller `Home`, default method `index`, auto-route true.
- Explicit routes for login, register, users, crime log, incident types, organisations, activity log, locations, menu management, and the CRUD generator.
- `$routes->resource` for `apiorganisations`, `apiusers`, `apicrimes`, `apiincidents`, `apigis`.

Controllers live in `app/Controllers` without a `Controller` suffix (`CrimeLog.php`, `Apiusers.php`). Views are PHP under `app/Views`. Models use the query builder more often than CI4 entity validation. `allowedFields` on the API models is not a complete picture of columns the admin models insert.

`App.php`: `baseURL` is an empty string in config (the `env` file sets `app.baseURL` to `http://localhost:8080`). `indexPage` remains `index.php`. `appTimezone` is `America/Chicago`. Session expiration is 7200 seconds.

The committed `env` (not dotted) sets `CI_ENVIRONMENT` to `development`, the database session handler, MySQLi, hostname `localhost`, database `adminoverdrive`, username `root`, and an empty password. It also contains an `encryption.key`. That key is not copied into this document. There is a separate `app.crimemapping.co.za/.env` with `PUBLIC_URL` and `REACT_APP_PUBLIC_API_URL`. A Google API key is present in that file and is not copied here. Admin maps read `$_ENV['GOOGLE_MAP_API_KEY']`. That variable was not in the admin `env` keys that were listed.

README local vhost: `crimeMappingWebportal.local` with document root `public/` under a WAMP path `c:/wamp/www/crimemapping-webportal/public`.

## Data

Migrations under `app/Database/Migrations` are all dated 2022-11-20 except two menu-category files dated 2022-11-25. They create:

| Table | Columns the migration creates |
|---|---|
| `users` | id, role_id, organisation_id, name, surname, email, cellphone, password, note, active, created_by, created_at, updated_by, updated_at |
| `user_role` | id and role fields in `2022-11-20-125608_UserRole.php` |
| `user_access` | role and menu permission ids |
| `user_menu`, `user_submenu`, `user_menu_category`, `user_submenu_category` | navigation |
| `sessions` | CI session store. Primary key is declared on both `id` and `ip_address` |
| `organisations` | name, contact_number, address, town, suburb, parent, api_key, note, active, audit columns |
| `incident_type` | name, note, active, audit columns |
| `crime_log` | parent_id, user_id, organisation_id, report_date, coordinates_long, coordinates_lat, verified, verified_user_id, verified_date, note, active, audit columns |
| `crime_incident_map` | crime_log_id, incident_type_id, note, active, audit columns |
| `image_log` | crime_log_id, name, path, type, size, date, plus further columns in the same migration |
| `activity_log` | user_id, ip_address, type, description, and audit columns in that migration |

The PHP on `8c3c372` also reads or writes data the migrations do not create:

| Used by code | Not in the migrations that were read |
|---|---|
| `crime_log.coordinates_address` | column |
| `users.token` | column |
| `incident_type.attempted`, `name_group`, `color` | columns |
| `locations` | table. Fields written: `sp_code`, `sp_name`, `mp_code`, `mp_name`, `mn_code`, `mn_name`, `mn_mdb`, `mn_type`, `dc_mn`, `dc_name`, `dc_mdb`, `shape_length`, `shape_area`, `active`, audit columns |
| `org_location_map` | table. Columns used: `organisation_id`, `location_id` |

Provenance: **Implemented** usage against **Unknown** live DDL. A database created only from these migrations will not match the current models.

Seed order from `Seeds/Users.php`: insert a user, then call `UserAccess`, `UserMenu`, `UserMenuCategory`, `UserRole`, `Organisations`. The user row is inserted before the role and organisation rows it references. Provenance: **Implemented** seeder order.

`created_by` on several admin inserts is `session()->get('role_id')`, not the user id (`UserModel::createUser`, `OrganisationsModel`, `IncidentTypeModel`, `LocationsModel`). Crime-log create uses the user id. Provenance: **Implemented**. The two meanings of `created_by` disagree.

## Auth

Web: PHP session. Filter aliases `isLoggedIn` (`Authentication`) and `isGranted` (`Authorization`) are global, with the exceptions in `app/Config/Filters.php`. CSRF is commented out. CORS is global and allows origin `*`.

API: `JwtLibrary` builds HMAC-SHA256 tokens. `is_jwt_valid` checks `exp` and the signature. The secret parameter defaults inside the library. Tokens are also stored on `users.token`, so logout can clear the row, but `is_jwt_valid` does not compare the bearer token to the stored row. A token remains valid until `exp` even after logout, for any caller that only uses `is_jwt_valid`. Provenance: **Implemented**.

`app/Helpers/app_auth_helper.php` defines a `logout()` function that references `$this`. It is not a class. No controller call of that function was found.

## Files and images

`Apicrimes::create` writes uploads to `assets/img/crime_images/{organisation_id}/{crime_id}/` relative to the PHP process, creates the directory at mode `0777`, and inserts `image_log`. Admin crime create does not upload files. Admin crime edit can list images.

## Mobile build

`app.crimemapping.co.za` is a CRA build: `asset-manifest.json`, `static/js/main.f6fb2877.js`, `static/css/main.57bcb42b.css`, `service-worker.js`, `manifest.json`. `index.html` title is "Crime Mapping Org". It registers `serviceWorker.js` (different hyphenation from the file that is present). Routes and API hosts are listed in `features.md`. There is no `package.json` and no `src/`.

## Public site

`index.html` is hand-written HTML with local CSS under `assets/`. `email.php` is a three-line redirect. `.htaccess` is an HTTPS rewrite only. It does not front the CodeIgniter app. The admin app is a separate directory with its own `public/` document root.

`org/index.html` is a saved Themify/WordPress page (theme version strings 5.6.7 and 5.7.4, WordPress 6.1.1 in the inline script). Assets point at both local files and `https://crimemapping.co.za/wp-content/...`. Provenance: **Implemented** static snapshot.

## Pipeline and analysis

Root `bitbucket-pipelines.yml`: image `python:3.5.7`, branch `master` only. The SonarQube step is commented out. The active step installs zip, git, and git-ftp, then `git ftp push` to `ftp://$FTP_HOST/public_html`. An init line is commented out. Header comments say "Silicon Overdive" and describe a full FTP push on each commit.

`admin.crimemapping.co.za/bitbucket-pipelines.yml`: same image. On `master` it runs the SonarQube pipe (`sonarsource/sonarqube-scan:1.0.0`, full clone) and then `git ftp push` to `ftp://$FTP_HOST` with no `/public_html` suffix.

Sonar project keys differ:

- Root `sonar-project.properties`: `overdrive_crimemapping_website_AYbFPecl_7nCiPvwlqwx`
- Admin `sonar-project.properties`: `overdrive_crimemapping-webportal_AYZhi2TEtKPJNQrmj1QE`

Which file a pipeline uses depends on the working directory of that step. The root Sonar step does not run. Provenance: **Implemented**.

## Tests and local run

`composer.json` script `test` runs PHPUnit. `tests/` contains CodeIgniter examples (`HealthTest`, example session test, example database test) and support stubs. No test names a crime, user, or API behaviour.

README run path: copy `env` to `.env`, `php spark db:create`, `php spark migrate`, `php spark db:seed users`, `php spark key:generate`, `php spark serve`.

## Security notes visible in the tree

These are observations for review, not a pentest.

- JWT HMAC secret is hardcoded in `JwtLibrary.php`.
- CORS origin is `*`. CSRF filter is off. Auto-routing is on.
- `ApiOrganisations::index` has no bearer check.
- `Apicrimes::create` creates upload directories mode `0777`.
- `env` is committed with `CI_ENVIRONMENT = development` and an encryption key. The app `.env` is committed with a Google API key.
- Seeders contain a short literal password and a short `api_key`.
- Several SQL fragments concatenate request values (`CrimeMapModel` dates and id lists, `ApiGisModel` town, `CrimeLogModel` id lists). Provenance: **Implemented**.

## Background work

No queue, cron, or worker was found. Pipelines are the only automated job in the repository. Activity-log writes from a scheduler were not found.

## Monitoring

Matomo on the marketing page, site id 4, host `analytics.medmin.co.za`. No application log configuration beyond CodeIgniter defaults was reviewed. SonarQube is configured for the admin pipeline.
