Audience: end users, customer support Difficulty: intermediate
What this covers
The signed link a project customer receives to approve or reject a time entry. Used when an organization bills time to a customer's project and wants the customer to sign off before the time is invoiced.
URL pattern
https://solavel.com/finance/customer/time-approvals/{timeEntry}/{token}
Three endpoints (defined in solavel-finance/routes/web.php, named
customer.time-approvals.*):
| URL | Method | Purpose |
|---|---|---|
/customer/time-approvals/{timeEntry}/{token} |
GET | View the time entry |
/customer/time-approvals/{timeEntry}/{token}/approve |
POST | Approve |
/customer/time-approvals/{timeEntry}/{token}/reject |
POST | Reject |
Controller: App\Http\Controllers\Solabooks\Portal\TimeApprovalController.
Throttle: 30,1 (30 requests per minute per IP). No middleware-level
token verification — the controller's guardSignature() method asserts
the URL signature on every action.
How it works
The link is a Laravel signed URL. It is created by
App\Services\Projects\CustomerTimeApprovalService using
URL::signedRoute(...) (or URL::temporarySignedRoute(...)) which
appends a query-string signature derived from app.key. The token in
the path is not a DB row — it is a path parameter that participates in
the signature.
When the customer clicks:
- Laravel's signed-URL validator runs. If the signature is missing or wrong, the request is rejected.
- If the signature is valid but expired, the controller calls
guardSignature()which renders a branded "link expired" view without mutating any state. - If the signature is valid and unexpired, the requested action runs:
- GET — show the entry's date, hours, project, narrative.
- POST approve — sets the time entry status to approved.
- POST reject — sets it to rejected. Optionally records a reason.
The approval/reject decision is final; the link cannot be re-used to flip the answer. To change the decision, staff must reset the time entry's approval state from the project page.
Expiry
The link's expiry is set when it is generated, in
CustomerTimeApprovalService. The exact window was not extracted in
the Phase 1 audit (it is a configurable parameter on signedRoute /
temporarySignedRoute). Confirm by reading the service or checking
the issued URL's expires= query string.
If the customer opens an expired link the controller renders the view
finance.portal.time-approvals.expired (response()->view(...)) —
nothing in the database changes.
When the link is sent
The link is issued by the staff user from the project's time-tracker view, when:
- The time entry has status
submitted(the project member has finished it). tracker.time_approvalandprojects.time_approvalfeatures are enabled.- The project's customer has an email on file.
The customer email contains: project name, period, total hours, the URL.
Permissions
- Project member submits time —
timesheets.create. - Staff sends customer link — internal, no permission check at the link-generation step (the project member can issue it for their own entries).
- Customer approval — token-only; no Solavel account.
- Manager can override customer's decision —
timesheets.approve.