# Stage 8 — Project Sharing, Collaboration, and Public Links

## Status

Implementation in progress as of 2026-09-24. The additive schema, centralized owner/editor/viewer authorization, direct sharing APIs, revocable public-link APIs, dashboard sharing workflow, read-only collaborator experience, sanitized public preview, bilingual UI, and automated regression coverage are implemented on `main`.

Backend automated acceptance currently passes 62 tests (one environment-dependent isolated-renderer test skipped), the dedicated sharing suite passes 42 assertions, and the frontend build plus 40 Node tests pass. Desktop/mobile browser acceptance, separate-session revocation checks, production-like backup/migration rehearsal, native Ubuntu verification, and Moodle SCORM 1.2 acceptance remain required before this stage is marked complete.

This Stage 8 document applies to the `main` product line. The experimental H5P work remains isolated on its separate branch and is not part of this release.

Stage 8 must be delivered through additive migrations and backward-compatible authorization. Existing project owners, source PowerPoint files, rendered slides, layout corrections, fonts, questions, question groups, media, course flow, learner settings, generated packages, archives, and private storage paths must remain unchanged.

## Summary

Allow a project owner to collaborate with existing DocuDeck users and publish a controlled public preview link.

An owner will be able to:

- Share a project directly with an existing user as **Can view** or **Can edit**.
- Change or revoke a collaborator's permission at any time.
- Create, rotate, expire, disable, and revoke public preview links.
- Optionally allow an existing generated SCORM package to be downloaded through a share.
- See who owns a project, who can access it, and which permission applies.

Shared users will see accessible projects in a dedicated **Shared with me** dashboard view. Editors can change course content but cannot manage ownership, sharing, archiving, restoration, or deletion. Viewers receive a clearly read-only experience.

Anonymous public links are view-only. Anonymous editing is intentionally prohibited. An edit invitation link must require authentication and resolve to an existing account before it grants editor membership.

## Goals

- Provide understandable owner, editor, and viewer roles.
- Share directly with any existing user by email without exposing the user directory.
- Support revocable public view links without making private storage public.
- Keep concurrent edits protected by the existing revision checks and project operation locks.
- Preserve every existing project and its current owner without a destructive backfill.
- Make production deployment observable, reversible at the code level, and backed up before schema changes.

## Non-goals

- Anonymous editing.
- Multiple owners or ownership transfer.
- Sharing folders or all future projects from one account.
- Real-time cursors, presence, chat, comments, or Google Docs-style operational transforms.
- Publishing the original PowerPoint, uploaded fonts, internal validation reports, or private source media through a public link.
- Copying a shared project into another account. A future stage may add an explicit safe-copy workflow.
- Changing the permissions inside an already exported SCORM ZIP. Exported packages remain independent files.

## 1. Access Model

The existing `conversions.user_id` remains the single project owner. Stage 8 does not rewrite it and does not infer ownership from a share record.

### Roles

| Capability | Owner | Editor | Viewer | Anonymous public viewer |
| --- | ---: | ---: | ---: | ---: |
| Open project and learner preview | Yes | Yes | Yes | Yes, while link is valid |
| View slides, questions, media, and settings | Yes | Yes | Yes | Sanitized learner preview only |
| Edit course content | Yes | Yes | No | No |
| Upload files or fonts | Yes | Yes | No | No |
| Render slides or generate a package | Yes | Yes | No | No |
| Download generated SCORM | Yes | Yes | Only when explicitly allowed | Only when explicitly allowed |
| Rename project | Yes | Yes | No | No |
| Manage collaborators and links | Yes | No | No | No |
| Archive, restore, or permanently delete | Yes | No | No | No |
| Transfer ownership | Deferred | No | No | No |

Owner-only actions must remain owner-only even when an editor calls the endpoint directly. Hiding buttons in the frontend is not authorization.

### Archived and deleting projects

- Only the owner can archive, restore, or delete a project.
- An archived project is read-only for collaborators until its owner restores it.
- Public links to archived projects return a neutral unavailable response rather than exposing archive state.
- Once `deleting_at` is set, all collaborator and public access stops immediately.
- Project deletion continues to remove project-owned files and rows. Share records and public links cascade with the project and never keep deleted files alive.

## 2. Additive Data Model

### Direct project shares

Add `conversion_shares`:

- UUID primary key.
- `conversion_id`, foreign key to `conversions`, cascade on project deletion.
- `user_id`, foreign key to `users`, cascade when the collaborator account is deleted.
- `permission`: `viewer` or `editor`.
- `allow_package_download`, default false.
- `created_by`, nullable user foreign key for audit attribution.
- `revoked_at`, nullable timestamp.
- Created and updated timestamps.
- Unique key on `(conversion_id, user_id)` so revoking and re-sharing reuses the same membership row instead of creating ambiguous active duplicates.
- Indexes for `(user_id, revoked_at, updated_at)` and `(conversion_id, revoked_at)`.

The owner must never receive a share row for their own project. A share row never changes `conversions.user_id`.

### Public links

Add `conversion_share_links`:

- UUID `public_id`, used as the non-secret lookup identifier.
- `conversion_id`, foreign key to `conversions`, cascade on project deletion.
- `token_hash`, SHA-256 hash of a cryptographically random secret; never store the raw secret.
- `permission`, fixed to `viewer` in Stage 8.
- `allow_package_download`, default false.
- Optional `expires_at`.
- Optional owner-facing label such as “Client review”.
- `created_by` user foreign key.
- `revoked_at`, nullable timestamp.
- `last_accessed_at`, nullable timestamp, and a bounded access counter for operational visibility.
- Created and updated timestamps.
- Unique index on `token_hash` and indexes for project, expiry, and revocation checks.

The public URL uses a two-part identifier. The frontend route contains the public UUID and keeps the secret in the URL fragment, for example:

```text
https://app.example.com/shared/{public-id}#{secret}
```

Fragments are not sent in HTTP request URLs or ordinary access logs. The frontend sends the secret in a dedicated request header. The API hashes it and compares it using constant-time comparison.

The raw secret is returned only when a link is created or rotated. If it is lost, the owner rotates the link and copies the new URL. Rotation invalidates the old secret immediately, except for already issued short-lived asset URLs until their maximum five-minute expiry.

### Optional audit events

Add an append-only `conversion_share_events` table if operational audit history is required in the first release. It may record share creation, permission change, revocation, link creation, link rotation, and link revocation. It must not store raw public-link secrets.

Audit history is not a substitute for the active access tables and must not prevent project deletion.

### Migration safety

- Create new tables and indexes only.
- Do not update, delete, re-key, or backfill existing conversion rows.
- Do not change existing project UUIDs, `user_id`, access-token hashes, revisions, status, or storage paths.
- Existing projects have no collaborators and no public links after migration, so their behavior remains owner-only.
- The migration `down()` may drop only Stage 8 tables. Production rollback should normally keep harmless additive tables and roll application code forward rather than dropping collaboration history.

## 3. Central Authorization

Replace owner-only checks with one central project-access service or Laravel policy that resolves:

- `owner`
- `editor`
- `viewer`
- no access

Every project controller, nested resource controller, job-dispatch endpoint, source/package download, and signed-asset URL generator must use that policy.

Required abilities:

- `view`
- `edit`
- `generate`
- `downloadPackage`
- `manageSharing`
- `archive`
- `restore`
- `delete`

The current `ConversionWriteLock` must become permission-aware while retaining its row lock and rollback behavior:

- Safe GET requests require `view`.
- Mutating authoring requests require `edit`.
- Owner-only project lifecycle and sharing requests use their dedicated abilities.
- The middleware must still reject writes while a project is deleting, rendering, or packaging as appropriate.

Nested records must continue to be verified against the parent conversion. Having access to one conversion must never authorize a slide, question, group, font, media item, caption, layout preview, or course-flow row owned by another conversion.

Unauthorized private-project requests should continue returning `404` to avoid confirming project existence. Authenticated users with viewer access who attempt a mutation should receive `403` with a stable `read_only_project` error code so the UI can explain the restriction.

## 4. Sharing With Existing Users

### Owner workflow

Add a **Share** action to each owned project card and the authoring workspace header.

The sharing dialog contains:

- Project name and owner.
- Existing collaborators with avatar/initial, display name, masked email, permission, and package-download status.
- Email field for an existing DocuDeck account.
- Permission selector: **Can view** or **Can edit**.
- Optional **Allow SCORM download** toggle.
- Update and Remove actions.
- Separate Public links section.

The backend accepts one exact normalized email address. It must not provide a searchable user directory or reveal whether arbitrary emails are registered outside this owner-authorized action. A successful share returns the collaborator summary. A missing account returns a neutral, actionable message such as “No DocuDeck account can receive this share yet.” Email invitations to unregistered people are deferred.

Rules:

- Owners cannot share a project with themselves.
- Re-sharing a revoked membership reactivates it safely.
- Permission changes are atomic and immediately effective.
- Revocation does not delete or alter project content.
- Editors cannot add other collaborators or create public links.
- Removing the last editor has no effect on ownership.
- A collaborator cannot archive, delete, or transfer the project.

### Shared-user workflow

Add dashboard filters:

- **My projects**
- **Shared with me**
- **Archived** for owned archived projects

Shared cards show:

- Owner name.
- Permission badge: **Can edit** or **View only**.
- Package-download availability.
- Last project update.
- **Open editor** for editors or **Open preview** for viewers.
- **Leave project** action, which revokes only that user's membership.

Owned and shared records must be paginated and searched server-side. The API must not duplicate a project if a corrupt legacy state somehow contains a self-share.

## 5. Read-only Experience

Viewer mode must be a deliberate interface, not an editable screen whose controls merely fail later.

- Show a persistent **View only** banner with the owner name.
- Disable or omit every mutation control, upload surface, drag handle, autosave action, package-generation button, archive action, and destructive menu.
- Do not start autosave timers.
- Do not send revision-changing requests.
- Allow navigation through course structure and full learner preview.
- If package download is disabled, do not expose the package URL or render a disabled link that reveals it.
- If access is revoked while the project is open, the next request returns `403/404`, local unsaved editor state is not submitted, and the UI returns safely to the dashboard.

Editors use the normal authoring workspace. Existing document revision checks remain the lost-update boundary. When another editor saves first, stale saves return `409`; the UI must retain local work and offer reload/retry rather than silently replacing it.

Real-time collaborative merging is deferred. The UI should state when a project changed in another session and may display the last editor from audit metadata when available.

## 6. Public Preview Links

### Link management

Owners may create more than one labelled link for the same project so access can be revoked independently.

Controls:

- Copy newly created link.
- Optional expiry: never, 24 hours, 7 days, 30 days, or a specific future date.
- Optional existing-package download.
- Rotate secret.
- Disable/re-enable.
- Revoke permanently.
- Display created time, expiry, last access, and status without displaying the stored secret.

### Public viewer

The public route presents the learner-facing full-course preview and basic project title. It does not expose the dashboard, authoring editor, collaborator list, account email addresses, source filename, internal errors, revisions, validation reports, private paths, or owner-only settings.

Public preview behavior:

- Read-only and no autosave.
- Uses the same learner rendering as Preview Full SCORM.
- Does not write SCORM learner state to the project database.
- May keep temporary progress in browser memory/session storage only.
- Does not trigger conversion, rendering, media processing, or package generation.
- Returns a neutral unavailable page for invalid, expired, revoked, archived, deleting, or unfinished projects.
- Reflects the latest successfully saved project state. A failed edit or package generation never destroys the last valid public preview assets.

Public endpoints must be rate-limited. Responses use `Cache-Control: private, no-store`, a restrictive Content Security Policy, `Referrer-Policy: no-referrer`, and frame controls consistent with the intended preview. Public media endpoints must support safe byte-range requests without exposing filesystem paths.

## 7. Assets and Download Safety

Existing files stay under `storage/app/private/conversions/{conversion}`.

- Never create public storage symlinks for project sources or generated assets.
- Authenticated owner/editor/viewer asset URLs are issued only after access authorization.
- Public preview asset URLs are short-lived, signed, and bound to the public link and conversion.
- Link revocation prevents new signed URLs immediately.
- Package download checks the current share/link permission at request time; possession of a project UUID is insufficient.
- Original PowerPoint, uploaded fonts, layout sources, and intermediate render files are owner/editor-only and are never available to public viewers.
- Use safe `Content-Disposition`, MIME allowlists, and `X-Content-Type-Options: nosniff`.

## 8. APIs

### Authenticated project access

- Extend `GET /api/v1/projects` with `scope=owned|shared` while preserving the current default as owned projects.
- Extend project summaries additively with `access_role`, `owner`, `can_edit`, `can_manage_sharing`, and `can_download_package`.
- Keep existing response fields and URLs unchanged.

### Collaborators

- `GET /api/v1/projects/{conversion}/shares`
- `POST /api/v1/projects/{conversion}/shares`
- `PATCH /api/v1/projects/{conversion}/shares/{share}`
- `DELETE /api/v1/projects/{conversion}/shares/{share}`
- `DELETE /api/v1/projects/{conversion}/shares/me` to leave a shared project

All collaborator-management endpoints are owner-only, revision-safe, and validate that the share belongs to the conversion in the route.

### Public links

- `GET /api/v1/projects/{conversion}/share-links`
- `POST /api/v1/projects/{conversion}/share-links`
- `PATCH /api/v1/projects/{conversion}/share-links/{link}`
- `POST /api/v1/projects/{conversion}/share-links/{link}/rotate`
- `DELETE /api/v1/projects/{conversion}/share-links/{link}`
- `GET /api/v1/public/projects/{publicId}` using the share secret header
- Public preview asset and optional package endpoints scoped to the same validated link

Public endpoints return only a dedicated sanitized presenter contract. They must not reuse the full authenticated authoring document response without an explicit field allowlist.

### Error contract

Use stable error codes in addition to localized messages:

- `project_not_found`
- `read_only_project`
- `share_recipient_not_found`
- `share_already_exists`
- `share_link_expired`
- `share_link_revoked`
- `project_unavailable`
- `revision_conflict`

## 9. Notifications and Localization

- Add English and Arabic/RTL copy for every sharing state and permission.
- A newly shared existing user sees the project immediately on the dashboard.
- Email notification is optional for the first release, but if enabled it must contain no raw project assets and must use the configured frontend origin.
- Permission labels must use words, not color alone.
- Dialogs, menus, link controls, and read-only previews must support keyboard operation, focus return, mobile layout, and screen-reader announcements.

## 10. Safe Production Deployment

### Feature flag

Introduce `PROJECT_SHARING_ENABLED=false` by default.

Deployment order:

1. Record the current production commit and verify the working tree is clean.
2. Back up the database, production `.env`, and `storage/app/private`; verify that the backup archives can be listed/read.
3. Deploy backend and frontend from one reviewed release commit.
4. Run the additive migrations with sharing still disabled.
5. Clear caches and restart workers so all processes use the new authorization code.
6. Smoke-test an existing owner and an existing old project before enabling sharing.
7. Enable sharing, clear configuration cache, and test one viewer, one editor, one revoked share, and one public link.
8. Keep the previous frontend build and previous release commit available for code rollback.

Never during an update:

- Replace production `.env` with `.env.example`.
- Generate a new `APP_KEY`.
- Run `migrate:fresh`, `db:wipe`, table truncation, or a database reset.
- Delete or recreate `storage/app/private`.
- Make private storage web-accessible to fix a permissions problem.
- Roll back the Stage 8 migration after real shares exist unless a reviewed backup/restore plan explicitly preserves those rows.

### Rollback behavior

Because Stage 8 uses additive tables and starts behind a feature flag:

- Disable `PROJECT_SHARING_ENABLED` first if a production issue appears.
- Existing owners continue using their projects through the owner path.
- Leave the additive tables in place while deploying corrected code so memberships and links are not lost.
- A code rollback must be tested against the database with the additional tables present.
- Database restore is a last resort and must restore the database and matching private-storage snapshot from the same deployment point.

## 11. Test Plan

### Migration and old-data safety

- Migrate a database containing Stage 1–7 projects and compare row counts, ownership, revisions, hashes, paths, package references, and file inventories before and after.
- Verify an existing owner can open, edit, render, package, download, archive, restore, and delete exactly as before.
- Verify a project with no share rows remains private.
- Verify migration retry/idempotence and rollback behavior in a disposable database.
- Verify project deletion cascades only Stage 8 access metadata plus the already-owned project data.

### Authorization

- Test owner, editor, viewer, revoked collaborator, unrelated authenticated user, expired link, revoked link, malformed token, and anonymous visitor against every project route.
- Test nested-resource ownership for slides, questions, groups, media, fonts, layouts, captions, and course flow.
- Confirm editors cannot manage shares, archive, restore, delete, or transfer ownership.
- Confirm viewers cannot mutate through direct API calls.
- Confirm unauthorized responses do not reveal private project existence.

### Collaboration and concurrency

- Share by normalized existing-user email as viewer and editor.
- Change permission, revoke, re-share, and leave project.
- Test simultaneous editor saves and every document's `409` revision behavior.
- Test sharing changes while rendering, packaging, archiving, or deleting.
- Test owner deletion and collaborator account deletion.
- Verify a failed editor save preserves local work and the last valid generated package.

### Public links

- Create, copy, open, expire, disable, re-enable, rotate, and revoke links.
- Verify a rotated old token fails and newly issued asset URLs expire quickly.
- Verify public responses never contain source paths, emails, internal errors, or authoring-only data.
- Verify optional package download independently from preview access.
- Test rate limiting, timing-safe token checks, headers, media byte ranges, mobile, Arabic/RTL, and keyboard navigation.

### Regression and production acceptance

- Run all backend and frontend suites.
- Build the production frontend.
- Run one existing-project and one new-project smoke test with sharing disabled, then enabled.
- Browser-test owner, editor, viewer, and anonymous public preview in separate sessions.
- Verify queue workers continue processing conversion, media, and layout-preview jobs.
- Generate and test a replacement SCORM package in Moodle 1.2; sharing must not change the package or learner-state contract.

## 12. Acceptance Criteria

Stage 8 is complete only when:

- Existing projects and private files remain intact after a production-like migration.
- Direct viewer/editor sharing works for existing users.
- Owner-only controls are enforced server-side.
- Public links are revocable, optionally expiring, and anonymous view-only.
- Viewer mode makes no write requests.
- Concurrent editor conflicts preserve unsaved work.
- Revocation is effective without deleting project data.
- Production backup, migration, smoke-test, feature-flag, and rollback procedures have been rehearsed.
- Automated regressions pass and browser acceptance covers desktop, mobile, English, and Arabic/RTL.

## Assumptions and Defaults

- `conversions.user_id` remains the single owner.
- Direct shares target existing DocuDeck accounts only.
- Direct permission defaults to **Can view**.
- Public links default to no expiry and package download disabled; the UI must make both choices visible before creation.
- Anonymous public access is view-only.
- Edit invitation links require authentication and become an explicit direct share before editing.
- Editors may generate packages but may not manage project lifecycle or sharing.
- Viewers and public visitors cannot access the original PowerPoint, fonts, or private intermediate files.
- Existing project URLs, IDs, revisions, storage locations, generated packages, and learner-state formats remain unchanged.
