# Technical documentation — crimemapping-webportal

Evidence date: 2026-10-18. Source: `git@bitbucket.org:overdrive/crimemapping-webportal.git`, `master` at `f8a20d4` (2024-02-05). Installed framework file `system/CodeIgniter.php` reports `CI_VERSION` `4.3.2`.

## What this repository is

A CodeIgniter 4 application named `codeigniter4/adminstarter` in `composer.json`, description "Admin Overdrive", homepage `https://www.overdrive.co.za`, license MIT. The README still describes the Admin Overdrive starter (auth, authorisation, registration, menu management that can create a controller and view). Crime-mapping screens were added on that starter.

Document root for the WAMP vhost in the README is `public/`, host name `crimeMappingWebportal.local`. `spark serve` is the other documented launch path. `app.baseURL` in the example `env` is left commented at `http://localhost:8080`. `Config/App.php` sets `$baseURL` to an empty string, so the environment value is what would override it once uncommented or copied into `.env`.

## Tree

```text
app/
  Config/           Routes, Filters, App, Database, Encryption, ...
  Controllers/      staff screens, Developers/, api*.php
  Database/         Migrations (2022-11-20 and 2022-11-25), Seeds
  Filters/          Authentication, Authorization, Cors
  Helpers/          menu, user access, logout
  Libraries/        JwtLibrary.php
  Models/
  Views/            layouts, common, settings, developers
public/             index.php, assets (DataTables, crimeMap.js)
tests/              CodeIgniter example tests only
writable/
bitbucket-pipelines.yml
composer.json
env                 example environment, not named .env
phpunit.xml.dist
sonar-project.properties
spark
```

`vendor/` is present in the clone used for this reading. `builds/` is a top-level directory from the issue's scan inventory; it was not used as product evidence.

Controllers that implement the product: `Welcome`, `Home`, `Users`, `UserRoles`, `Organisations`, `Locations`, `IncidentType`, `CrimeLog`, `CrimeLogExport`, `CrimeMap`, `CrimeMapGis`, `ActivityLog`, `Developers\MenuManagement`, `Developers\CRUDGenerator`, `apiUsers`, `apiCrimes`, `ApiIncidents`, `ApiOrganisations`, `Apigis`.

Auto-routing is enabled (`Routes.php` `$routes->setAutoRoute(true)`), so a public method is reachable as `/controller/method/argument` even when `Routes.php` does not list it. Explicit routes cover the login, registration, the main POST actions, and the five `resource` APIs.

## Runtime

| Item | Value | Evidence |
|---|---|---|
| Language | PHP `^8.0` | `composer.json`, README |
| Framework | CodeIgniter 4.3.2 | `vendor/.../system/CodeIgniter.php` |
| Database driver | MySQLi, port 3306, database `adminoverdrive`, user `root`, empty password | example `env` and `Config/Database.php` defaults |
| Test database | SQLite `:memory:`, prefix `db_` | `Config/Database.php` group `tests` |
| Sessions | `DatabaseHandler`, table `sessions`, cookie `ci_session`, match IP, regenerate and destroy | example `env` |
| Environment flag | `CI_ENVIRONMENT = development` in the example file | `env` |
| Charset | `utf8` / `utf8_general_ci` | `Config/Database.php` |

The example `env` also sets `encryption.key` to a `hex2bin:` value. That key is committed. It is not copied into this document. Production `.env` is not in the repository.

`GOOGLE_MAP_API_KEY` is read with `$_ENV` in `CrimeLog`, `CrimeLogExport`, `CrimeMap`, and `CrimeMapGis`. It is not declared in `env`.

There is no queue worker, scheduler, cache configuration change, or mail setup beyond the stock `Config/Email.php`. No `app/` code sends mail. `ForgotPassword` is empty.

## Routing and filters

Global `before` filters, in order: `cors`, `isLoggedIn`, `isGranted`.

| Filter | Except |
|---|---|
| `isLoggedIn` | `/`, `users`, `getLogin`, `register`, and `apiorganisations`, `apiusers`, `apicrimes`, `apiincidents`, `apigis` each with and without `/*` |
| `isGranted` | `/`, `getLogin`, `register`, `blocked`, `home`, `Welcome/*`, and the same API prefixes |

The declared login route is `POST GetLogin`. The exception string is `getLogin`. Whether the filter match is case-sensitive was not executed.

`toolbar` runs after every request, including API responses.

`Cors::before` sends `Access-Control-Allow-Origin: *` and exits on `OPTIONS`.

## Authentication

Staff: session, as in the PRD workflow W1. Passwords use PHP's default password hash.

API: `App\Libraries\JwtLibrary`.

- `generate_jwt` builds `base64url(header).base64url(payload).base64url(HMAC-SHA256)`.
- Default secret parameter is the literal `secret`.
- `is_jwt_valid` rejects an empty token, a payload whose `exp` is in the past, or a signature that does not match. Callers do not pass a different secret.
- Login sets `exp` to `time() + 60000`.
- The bearer is taken from `Authorization`, `HTTP_AUTHORIZATION`, or `apache_request_headers()`.

`users.token` is written on login and cleared on logout. Validation does not look up that column.

## Data model

Migrations under `app/Database/Migrations` create these tables. Column lists are the migration fields, not a live `DESCRIBE`.

| Table | Migration | Columns the migration creates |
|---|---|---|
| `users` | `2022-11-20-124016_Users.php` | id, role_id, organisation_id, name, surname, email, cellphone, password, note, active, created_by, created_at, updated_by, updated_at |
| `user_role` | `2022-11-20-125608_UserRole.php` | role table for id and role_name (see file) |
| `user_access` | `2022-11-20-125818_UserAccess.php` | role_id, menu_category_id, menu_id, submenu_id |
| `user_menu` | `2022-11-20-130307_UserMenu.php` | menu_category, title, url, icon, parent |
| `user_submenu` | `2022-11-20-130307_UserSubmenu.php` | submenu rows |
| `user_menu_category` | `2022-11-25-102709_UserMenuCategory.php` | category name |
| `user_submenu_category` | `2022-11-25-102709_UserSubMenuCategory.php` | present in the migration set |
| `sessions` | `2022-11-20-124435_Session.php` | CodeIgniter session table |
| `organisations` | `2022-11-20-124016_Organisations.php` | id, name, contact_number, address, town, suburb, parent, api_key, note, active, created_by, created_at, updated_by, updated_at |
| `crime_log` | `2022-11-20-124016_CrimeLog.php` | id, parent_id, user_id, organisation_id, report_date, coordinates_long, coordinates_lat, verified, verified_user_id, verified_date, note, active, created_by, created_at, updated_by, updated_at |
| `crime_incident_map` | `2022-11-20-124016_CrimIncidentMap.php` | id, crime_log_id, incident_type_id, note, active, created_by, created_at, updated_by, updated_at |
| `incident_type` | `2022-11-20-124016_IncidentType.php` | id, name, note, active, created_by, created_at, updated_by, updated_at |
| `image_log` | `2022-11-20-124016_ImageLog.php` | id, crime_log_id, name, path, type, size, date, note, active, created_by, created_at, updated_by, updated_at |
| `activity_log` | `2022-11-20-124016_ActivityLog.php` | id, user_id, ip_address, type, description, created_by, created_at, updated_by, updated_at |

### Used by PHP and not created by these migrations

| Object | Used for |
|---|---|
| `crime_log.coordinates_address` | Staff form, export, API create, GIS properties. Listed in `ApiCrimeModel::$allowedFields` |
| `users.token` | JWT storage. Listed in `ApiUserModel::$allowedFields` |
| `incident_type.attempted` | Map and incident picker filters |
| `incident_type.name_group` | Map marker label and `ORDER BY` |
| `incident_type.color` | Colour swatch on the map filter |
| `locations` | Municipal catalogue (`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` | organisation_id, location_id, active, audit columns |
| `organisations.status` | Written by `ApiOrganisations::create` and `update`. The migration column is `active`, not `status` |

There is no later migration in `app/Database/Migrations`. Either these objects were added outside Forge, or the screens fail on a database built only from the committed migrations. **Unknown / Requires Confirmation** which databases have been altered by hand.

Foreign keys are not declared in the migrations that were read. Deletes are application-level.

### Seeds

`php spark db:seed users` is the README command. `Seeds/Users.php` inserts one user and then calls `UserAccess`, `UserMenu`, `UserMenuCategory`, `UserRole`, and `Organisations`. Menu seed rows are the starter Dashboard / Users / Menu Management / CRUD Generator set. Crime menus are not seeded.

## HTTP API

Resource routes (`$routes->resource`) follow CodeIgniter's REST map: `GET` collection, `GET` one id, `POST` create, `PUT`/`PATCH` update, `DELETE`. Extra methods are auto-routed.

| Call | Token | Notes |
|---|---|---|
| `POST apiusers` | No | Creates role id 2. Duplicate cellphone returns a message and still goes through `respondCreated` |
| `POST apiusers/login` | No | JSON `cellphone`, `password`. Returns `id` and `token` |
| `GET` or `POST apiusers/getUser` | Bearer | User row for that token |
| `POST apiusers/logout` | Bearer value matched on `users.token` | Clears the token. Does not call `is_jwt_valid` first |
| `GET apicrimes` | Bearer | Calls a method `getUsers` that `ApiCrimeModel` does not define |
| `GET apicrimes/{id}` | Bearer | Crime plus incidents. `getCrimeImages` selects incident types, not `image_log` |
| `POST apicrimes` | Bearer | Creates the crime, incidents, and files. Success response overwrites the failure payload |
| `GET apiincidents`, `GET apiincidents/{id}` | Bearer | Incident type rows via `ApiIncidentModel` |
| `GET apiorganisations` | No | `findAll()`, ordered by id descending |
| `GET/POST/PUT/DELETE apiorganisations/{id}` | Bearer | Create success text says "Employee created successfully" |
| `POST apigis/authenticate` | No | JSON `vendor`, `api_key` |
| `apigis/getActiveTowns` | Bearer | Counts grouped by town |
| `apigis/getUserLocation` | Bearer | JSON `email` |
| `apigis/getTownCrimeLog` | Bearer | JSON `town`, optional `page`. 100 rows. Feature-shaped points |

The comment block in `Routes.php` that documents `POST /organisations/login` and CRUD on `/organisations` does not match these routes.

Staff JSON helpers (`getCrimeIncidents`, `getCrimeImages`, `getCrimeLogChildrenTable`, `getCrimeMapIncidents`, `getIncidentTypes`) take a JSON string as a method argument because of auto-routing, and they return `json_encode` strings. They sit behind the session filters when the first segment is `crimeLog` or `crimeMap`.

## Files and front end

Crime images from the API are written to a relative directory `assets/img/crime_images/{organisation_id}/{crime_id}/`. The working directory of the PHP process decides where that lands. **Unknown** whether production's cwd is `public/` or the project root.

Staff UI layout is `app/Views/layouts/main.php` with sidebar, header, and footer. Libraries loaded from the layout:

- Local `assets/js/jquery-3.5.1.js` and `jquery.dataTables.min.js`
- CDN jQuery 3.5.1 again
- DataTables 1.13.2 and Buttons 2.3.6 (html5, bootstrap5, print)

Google Maps is included with `libraries=places` on the map pages and on the crime log. MarkerClusterer is loaded from `cdn.rawgit.com`. The GIS page adds OpenLayers 7.4.0 and `main.js` from `https://sungis10.stb.sun.ac.za/crimestatic/`.

`common/home.php` renders no content inside the layout section.

## Deployment and quality tools

`bitbucket-pipelines.yml` uses image `python:3.5.7`. On branch `master` only:

1. SonarQube pipe `sonarsource/sonarqube-scan:1.0.0` with `SONAR_HOST_URL` and `SONAR_TOKEN`.
2. `apt-get` install of `zip`, `git`, `git-ftp`, then `git ftp push` to `ftp://$FTP_HOST`.

`sonar.projectKey` is `overdrive_crimemapping-webportal_AYZhi2TEtKPJNQrmj1QE`.

`sonarqube_scan.bat` is in the repository root (the issue inventory listed it). It was not executed.

No Dockerfile, no Terraform, and no web-server config besides the README vhost snippet. The vhost allows `Indexes`, `Includes`, `FollowSymLinks`, `MultiViews`, and `Require local`.

## Tests and local setup

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

`phpunit.xml.dist` bootstraps the framework test runner and covers `./app` except views and `Routes.php`. The suites that ship in `tests/` are the framework examples. They do not describe crime-log behaviour.

PHPUnit 9 is the dev requirement (`phpunit/phpunit: ^9.1`).

## Security notes found in the tree

These are observations for reviewers, not a pentest.

- CSRF filter is off. CORS origin is `*`.
- JWT secret is a fixed default. Logout does not invalidate a token that still has a valid signature and expiry.
- `POST apiusers` and `GET apiorganisations` are outside the token checks. The organisation payload includes `api_key` when the column exists.
- Public registration targets role id 1, and the posted field names do not match `createUser`.
- Sign-in ignores `active`.
- Several queries concatenate request data into SQL (`ApiGisModel` town filter, `CrimeMapModel` dates and id list, `OrganisationsModel::getLocation` town `LIKE`).
- API image directories are created mode `0777`.
- The example `env` commits an encryption key and an empty database password. The user and organisation seeds commit contact details and short secrets.
- `ApiCrimes::index` will error when hit, because `getUsers` is not on that model. `getCrimeImages` does not read `image_log`.

## Related repository

`crimemapping_website` (Silicon Ark directory of the same name) documents a later tree whose admin README uses this same vhost name. The relationship between the two repositories is **Unknown / Requires Confirmation**. Do not treat this file as a description of `crimemapping_website`.
