Solavel Solavel Docs

Project Shares

docs/customer-portal/project-shares.md

Audience: end users, customer support Difficulty: intermediate

What this covers

The two URLs that let an external party (typically a project customer or a stakeholder) view a shared snapshot of a project, no login required: /shared/projects/{token} and the shorter alias /p/projects/{token}.


URL patterns

Two prefixes, identical handlers:

https://solavel.com/finance/shared/projects/{token}
https://solavel.com/finance/shared/projects/{token}/print
https://solavel.com/finance/p/projects/{token}
https://solavel.com/finance/p/projects/{token}/print

Defined in solavel-finance/routes/web.php (named projects.public-shares.*). Both prefixes route to App\Http\Controllers\Projects\ProjectPublicShareController.

URL Method Purpose
/{token} GET View the share — landing page with project summary
/{token}/print GET Printable / PDF-friendly view

/p/projects/ is the short URL used in emails and SMS where character count matters; /shared/projects/ is the full path. They are interchangeable.


Auth & throttle

Middleware: throttle:portal-public only. No middleware-level token verification — the controller validates the token itself before rendering. This is inconsistent with the /i/{token} portal-link flow, which has a dedicated middleware. The behaviour is acceptable but worth noting if you are debugging an unexpected 200 response.


How a share is created

A staff user opens a project and clicks Share. The controller issues a token (bin2hex(random_bytes(...))-style) and stores its hash in the project share table. The URL is presented for copy/paste, or emailed.

The exact share table schema and any expiry behaviour were not extracted in the Phase 1 audit — confirm by reading Projects/ProjectPublicShareController and the relevant migration. Two design choices are likely:

  • Token-only, no expiry. The share lives until staff revokes it.
  • Token + expiry on the share row, validated in the controller.

What we do know:

  • The route layer enforces only the throttle. Anything else is in the controller.
  • The print sub-route works on the same token.

What the customer sees

Typical content (based on the controller's view name patterns):

  • Project name, status, dates, customer.
  • Milestones / deliverables.
  • Tasks and their status — read-only.
  • Optionally: time entries, budget vs actuals, attachments.

The page has no edit affordances. It is read-only by design.


When to use which

You want… Use
Send a customer a single document (invoice / quote) secure-document-links
Send a customer a quote they can accept/decline public-quote-links
Have a customer approve a time entry customer-time-approvals
Show a customer the shape of an entire project Project share (this page)
Give a customer ongoing self-serve access to all their docs overview (account-based portal)

Revocation

To kill a share, open the project, Shares tab (or the equivalent in the project sidebar), and click Revoke on the row. The link returns 404 immediately on next request. Token hashes are not deleted — just flagged.


Security notes

  • Anyone with the URL can view. Treat it like a password.
  • Tokens are long enough that brute-force is not a realistic concern.
  • The page is not indexed by search engines (it should serve noindex headers — confirm in the controller).
  • Throttle bucket is shared with /i/{token} (portal-public) so a burst of requests against one customer-facing surface affects the other on the same IP.

Related

Source: docs/customer-portal/project-shares.md ← All documentation