# PRD — unity-api

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/unity-api.git`, `master` at `eeafec9` (2019-08-30).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `routes/api.php`, `routes/web.php`, the controllers under `app/Http/Controllers/API/`, `app/User.php`, `app/Product.php`, `config/auth.php`, `app/Http/Kernel.php`, the three migrations, and `db/unity.sql` (schema and trigger text only). Provenance is labelled. Row data from the dump — names, addresses, phone numbers, message text, password hashes, and token values — is omitted.

`README.md` is the upstream Laravel framework README. It is not a requirements document for this service.

## Product purpose

**Inferred.** A Laravel HTTP API, repository name `unity-api`, that registers a user, logs that user in, returns a Passport personal access token, and lets the token holder manage `products` rows.

**Implemented.** The routes in `routes/api.php` and the models `App\User` and `App\Product`.

**Also in the repository, not wired to those routes.** `db/unity.sql` is a phpMyAdmin dump of MySQL database `unity` (generated 2019-08-30). It holds a CRM / helpdesk / HR schema. No PHP class selects or writes those tables.

**Documented.** Nothing in-repo states a product purpose. Commit subjects are "API first commit" and "Including new db".

**Unknown / Requires Confirmation:** the product owner, the caller application, whether this API was ever deployed, and whether the SQL dump was the database the API was supposed to serve.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| Anonymous HTTP caller | `POST /api/register`, `POST /api/v1/register`, `POST /api/v1/login` have no `auth` middleware | **Implemented** |
| Caller holding a Passport access token | `auth:api` guard driver is `passport` (`config/auth.php`). Product routes, `POST /api/v1/getUser`, and `GET /api/user` use `auth:api` | **Implemented**. How the caller obtained a client id is the Passport install, recorded in the dump's `oauth_*` tables. Client secret values are omitted |
| `users` row | Created by both register actions. Four rows are in the dump, `email_verified_at` null. Hashes omitted | **Implemented** as the API user store |
| `sysuser` row | Staff table inside the SQL dump, with flags `blnLineManager`, `blnSalesPerson`, `blnStaff`, `blnTicketOwner`, `blnActive` | **Present in the dump only**. No PHP model reads it |
| Person opening `GET /` | `routes/web.php` returns `welcome` | **Implemented**. Stock Laravel welcome view |
| Person using the stock web auth controllers | `app/Http/Controllers/Auth/*` exist. `routes/web.php` does not register them. Their `$redirectTo` is `/home`, and no `/home` route exists | **Present, not routed** |

No role, group, or ownership check is applied on the API. Any valid Passport token can list, create, update, and delete every product.

`User` implements `MustVerifyEmail`. The `verified` middleware is registered in the kernel and is not attached to any route. API register does not set `email_verified_at`. The dump's four API users have `email_verified_at` null.

## Systems the product talks to

1. MySQL, through Laravel's database config. `.env.example` names the database `homestead`. The dump names the database `unity`. `config/database.php` defaults the database to `forge` when the env var is missing. The connection a deployment used is **Unknown / Requires Confirmation**.
2. Laravel Passport 7.3.2 (`composer.lock`). `AuthServiceProvider` calls `Passport::routes()` with no arguments. Token creation uses `$user->createToken(...)`.
3. Browser CORS. Three mechanisms are configured at once: global `Spatie\Cors\Cors` (`config/cors.php`), route middleware `cors` on the v1 login and register routes (`app/Http/Middleware/Cors.php`), and `Header set Access-Control-Allow-Origin "*"` in the repository root `.htaccess`.
4. Optional services listed only as empty env keys: Redis, SMTP (Mailtrap in `.env.example`), AWS S3, Pusher, Mailgun, Postmark, SES, SparkPost, Stripe (`config/services.php`, `.env.example`). No application code calls them.

No queue worker, scheduler entry, or monitoring vendor is configured by this application. `QUEUE_CONNECTION` in `.env.example` is `sync`. `app/Console/Kernel.php` has no schedule. The SQL dump has a `syscronjob` table and triggers that insert rows into it. Nothing in the PHP reads that table.

There is no Bitbucket Pipelines file.

## Major workflows

Routes below include the `/api` prefix applied by `app/Providers/RouteServiceProvider.php`. Passport's own routes are not in `routes/api.php`; see WF-8.

### WF-1 — Register (envelope response)

**Implemented** in `app/Http/Controllers/API/RegisterController.php`, `POST /api/register`. No auth middleware.

1. Validate `name` required, `email` required and email, `password` required, `c_password` required and `same:password`.
2. On failure, `BaseController::sendError('Validation Error.', $errors)`. The default HTTP status in `sendError` is **404**, not 422.
3. On success, `bcrypt` the password, `User::create` with the request input (only `name`, `email`, `password` are fillable), create a Passport personal access token named `MyApp`.
4. Response status 200, body `{ success: true, data: { token, name }, message: "User register successfully." }`.

`email` is not validated as unique. The `users.email` unique index will reject a duplicate. That failure is not caught in the controller. The HTTP status is then Laravel's exception rendering, which depends on `APP_DEBUG`. `.env.example` sets `APP_DEBUG=true`. `config/app.php` defaults debug to false when the env var is absent.

This action does not fire `Illuminate\Auth\Events\Registered`. The listener `SendEmailVerificationNotification` in `EventServiceProvider` therefore does not run for this path.

### WF-2 — Register (token-only response)

**Implemented** in `app/Http/Controllers/API/AuthController.php`, `POST /api/v1/register`. Also runs the custom `cors` middleware. The class namespace is `App\Http\Controllers\Api` while the directory is `API`. On a case-sensitive filesystem the Composer PSR-4 path for that namespace does not match the directory name. Whether production was case-sensitive is **Unknown / Requires Confirmation**.

1. Same four fields as WF-1.
2. On failure, status **401** and body `{ error: <validator errors> }`.
3. On success, bcrypt, `User::create`, Passport token named `AppName`.
4. Response status 200, body `{ success: { token } }`. The user's name is not returned. This shape is not the `BaseController` envelope.

Same unique-email gap as WF-1. Same absence of the `Registered` event.

### WF-3 — Login

**Implemented** in `AuthController::login`, `POST /api/v1/login`, custom `cors` middleware, no auth middleware.

1. `Auth::attempt` with `email` and `password` from the request helper (not a type-hinted `Request`).
2. Failure: status 401, body `{ error: "Unauthorised" }`. No field-level validation. Missing fields fail the attempt the same way as a wrong password.
3. Success: Passport token named `AppName`, status 200, body `{ success: { token } }`.

`MustVerifyEmail` does not block this attempt, because the `verified` middleware is not on the route.

### WF-4 — Current user

Two implemented reads:

| Route | Handler | Response |
|---|---|---|
| `POST /api/v1/getUser` | `AuthController::getUser` | Status 200, `{ success: <user> }`. `password` and `remember_token` are hidden on the model |
| `GET /api/user` | Closure in `routes/api.php` | The user object from `$request->user()`, Laravel's default JSON conversion of the model |

Both require `auth:api`. A missing or rejected token is Passport / Laravel's unauthenticated response. This application does not customise that response (`app/Exceptions/Handler.php` calls the parent).

### WF-5 — Product list, read, create, update, delete

**Implemented** in `ProductController`, group middleware `auth:api`. `Route::resource('products', ...)` is used, not `Route::apiResource`. In Laravel 5.8 a full resource also registers `create` and `edit`. Those two methods are not defined on the controller.

| Method and path | Action | Behaviour |
|---|---|---|
| `GET /api/products` | `index` | `Product::all()`. Status 200 envelope, message "Products retrieved successfully." |
| `POST /api/products` | `store` | `name` and `detail` required. Failure: `sendError` status **404**, message "Validation Error." Success: `Product::create`, status 200, message "Product created successfully." |
| `GET /api/products/create` | not defined | Framework method-not-found response. **Inferred** from the resource route plus the missing method |
| `GET /api/products/{id}` | `show` | `Product::find($id)`. Null: `sendError('Product not found.')` status **404**. Otherwise status 200 |
| `GET /api/products/{id}/edit` | not defined | Same as `create` |
| `PUT` or `PATCH /api/products/{product}` | `update` | Route-model binding `Product $product`. Missing id is Laravel's binding 404, before the controller. `name` and `detail` required (both, even for a partial patch). Assigns those two fields and `save()`. Status 200, message "Product updated successfully." |
| `DELETE /api/products/{product}` | `destroy` | Binding, then `$product->delete()`. Status 200 with the deleted row's array, message "Product deleted successfully." |

No pagination. No filter. No per-user ownership. `name` is not unique.

`show` treats a missing id as a JSON 404 from `sendError`. `update` and `destroy` use implicit binding, so a missing id never reaches that JSON helper.

### WF-6 — Welcome page

**Implemented.** `GET /` returns view `welcome`. Feature test `tests/Feature/ExampleTest.php` asserts status 200 for `/`. It does not call the API.

### WF-7 — Web registration, login, password reset, email verification

**Not routed.** Controllers exist under `app/Http/Controllers/Auth/`. `RegisterController` there would require `name`, `email` unique on `users`, `password` min 8 and `confirmed`, and would hash with `Hash::make`. That class is unreachable from `routes/web.php`. Password reset config (`config/auth.php`) sets token expiry to 60 minutes and table `password_resets`. No route posts to it.

`User` implements `MustVerifyEmail`, and `Registered` is mapped to `SendEmailVerificationNotification`. The API register methods do not dispatch `Registered`. The web register controller would, and it is not routed. Net effect in the running route table: verification mail is not sent.

### WF-8 — Passport OAuth routes

**Implemented** as `Passport::routes()` in `app/Providers/AuthServiceProvider.php`, called with no callback and no options. The path list is not copied into this repository. It is the default route set of locked `laravel/passport` v7.3.2.

The dump's `migrations` table shows Passport's five migrations applied as batch 2 (`oauth_auth_codes`, `oauth_access_tokens`, `oauth_refresh_tokens`, `oauth_clients`, `oauth_personal_access_clients`). Those PHP files are not in `database/migrations`. The dump also contains `oauth_*` row data. Secrets and token ids are omitted.

The API login and register actions do not call `/oauth/token`. They call `createToken`, which writes a personal access token.

## Functional requirements

### FR-001 — Register a user and return a token

Requirement: An unauthenticated caller can create a `users` row and receive a Passport personal access token.

Who: anonymous HTTP caller.

Trigger: `POST /api/register` or `POST /api/v1/register`.

Inputs: `name`, `email`, `password`, `c_password`.

Expected behaviour: password stored as bcrypt; token returned; `c_password` not stored (not fillable).

Outputs: two different JSON shapes, WF-1 and WF-2.

Validation: the four rules above. No unique check, no minimum password length, on these two routes.

Error conditions: validation failure is 404 on `/api/register` and 401 on `/api/v1/register`. Duplicate email is an unhandled database exception.

Dependencies: Passport personal access client (present as tables in the dump; client secrets omitted).

Evidence: `RegisterController.php`, `AuthController.php`, `User.php`, `2014_10_12_000000_create_users_table.php`.

Implementation: Implemented.

Confidence: High for the two response contracts. Low for which contract a caller was supposed to use.

### FR-002 — Log in

Requirement: A caller with an email and password receives a Passport personal access token.

Who: anonymous HTTP caller.

Trigger: `POST /api/v1/login`.

Inputs: `email`, `password`.

Expected behaviour: `Auth::attempt`. Success returns `{ success: { token } }` status 200, token name `AppName`.

Error conditions: any failed attempt returns 401 `{ error: "Unauthorised" }`.

Evidence: `AuthController::login`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Read the authenticated user

Requirement: A token holder can read their user record, without the password hash.

Who: Passport-authenticated caller.

Trigger: `POST /api/v1/getUser` or `GET /api/user`.

Outputs: two shapes (WF-4). Hidden attributes: `password`, `remember_token`.

Evidence: `AuthController::getUser`, `routes/api.php`, `User.php`.

Implementation: Implemented.

Confidence: High.

### FR-004 — Manage products

Requirement: A token holder can list, create, read, update, and delete products. Each product has `name` and `detail`.

Who: any Passport-authenticated caller. No narrower role exists.

Trigger: the resource routes in WF-5.

Validation: `name` and `detail` required on create and on update.

Error conditions: validation and missing `show` id use HTTP 404 via `sendError`. Missing id on update/delete follows route-model binding.

Evidence: `ProductController.php`, `Product.php`, `2019_07_13_213211_create_products_table.php`.

Implementation: Implemented. `create` and `edit` routes exist without methods.

Confidence: High.

### FR-005 — Throttle the API group

Requirement: Routes loaded from `routes/api.php` pass the `api` middleware group, which is `throttle:60,1` and `bindings`.

Evidence: `app/Http/Kernel.php`, `RouteServiceProvider`.

Implementation: Implemented for `/api/*`. Whether Passport's `/oauth` routes share that throttle is the package default, not set in this kernel. **Unknown / Requires Confirmation** against the locked package if a deployment depends on it.

Confidence: High for `/api/*`.

### FR-006 — CORS for browser callers

Requirement: Cross-origin calls are allowed.

Implemented behaviour, three layers:

- Spatie default profile: origins `*`, methods POST GET OPTIONS PUT PATCH DELETE, headers `*`, `allow_credentials` true, max age 86400 seconds, forbidden body `{ message: "Forbidden (cors).", status: 403 }`.
- Custom middleware on v1 login and register only: `Access-Control-Allow-Origin: *`, methods `GET, POST, PATCH, PUT, DELETE, OPTIONS`. The `Access-Control-Allow-Headers` line is commented out.
- Root `.htaccess`: `Access-Control-Allow-Origin: *` when `mod_headers` is available.

`allow_credentials: true` together with origin `*` is what the config file sets. Browsers reject that combination. That outcome is **Inferred** from the config values, not from a captured browser session.

Evidence: `config/cors.php`, `app/Http/Middleware/Cors.php`, `app/Http/Kernel.php`, `.htaccess`.

Implementation: Implemented as configuration.

Confidence: High for the configured headers. Medium for the browser result.

### FR-007 — Expose the Unity CRM through this API

The dump contains the CRM schema. The PHP API does not read or write it.

Implementation: Not implemented.

Confidence: High that it is absent. Whether it was required is **Unknown / Requires Confirmation**.

## Important constraints

- PHP `^7.1.3`, Laravel `5.8.28`, Passport `7.3.2`.
- API responses for products and `POST /api/register` use the `BaseController` envelope. Login, v1 register, and `getUser` do not.
- Validation failures on the envelope routes use HTTP 404.
- `users.email` is unique in the database. The API validators do not check uniqueness.
- Password policy on the routed register actions is "required" only. The unrouted web `RegisterController` requires min 8 and `confirmed`.
- Timezone in `config/app.php` is `UTC`.
- No authorisation policy class exists. `AuthServiceProvider` maps `'App\Model'` to `'App\Policies\ModelPolicy'`. That policy class is not in the repository.

## Open product questions

- Which register route is the supported contract, WF-1 or WF-2?
- Was `db/unity.sql` meant to be the application database, a reference copy, or an accidental commit?
- Is a caller other than "anyone with a token" allowed to change products?
- Is email verification required? The model says yes; the routes do not enforce it.
- Is this repository still in service, or replaced by another Unity API repository?
