# Note Tracker — Backend

Multi-tenant SaaS backend for tracking client requests, issues, and action items.
**Laravel 12 · PHP 8.3 · MySQL 8 · Redis · Sanctum · S3-compatible storage.**

Internal teams manage notes under projects; external clients view shared
(global) notes and reply via secure one-time links — no account required.

---

## Quick start (Docker)

```bash
cp .env.example .env
docker compose up -d --build

# Install dependencies, generate key, migrate + seed the demo dataset
docker compose exec app composer install
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate --seed
```

Services:

| Service   | URL / Port                    | Notes                              |
|-----------|-------------------------------|------------------------------------|
| API       | http://localhost:8000         | nginx → php-fpm                    |
| MySQL 8   | localhost:3306                | db/user: `notetracker` / `secret`  |
| Redis     | localhost:6379                | queues + cache                     |
| Mailhog   | http://localhost:8025         | SMTP catch-all UI (port 1025)      |
| MinIO     | http://localhost:9001         | S3 API on :9000; console login `notetracker` / `notetracker-secret` |
| Queue     | —                             | supervisord → `queue:work redis --queue=attachments,emails,default` |
| ClamAV    | internal port 3310             | fail-closed malware scanning for private attachments |
| Scheduler | —                             | `php artisan schedule:work` (runs `share-tokens:clean` daily, configured in `bootstrap/app.php`) |

### Private attachment storage and malware scanning

Note attachments are stored on the disk named by **`ATTACHMENTS_DISK`**.
Host development defaults to the private `local` disk at
`storage/app/private`, so uploads work without Docker. Docker Compose
overrides the disk to `s3`, targeting the bundled **MinIO** container; the
one-shot `minio-create-bucket` service creates the
`notetracker` bucket automatically on `docker compose up` (credentials and
endpoint are prewired in `.env.example`: `AWS_ENDPOINT=http://minio:9000`,
`AWS_USE_PATH_STYLE_ENDPOINT=true`). Files land under the per-tenant prefix
`org-{id}/notes/{note_id}/`. Production may set `ATTACHMENTS_DISK=s3` with
its own S3-compatible credentials and endpoint.

Every upload first lands under `quarantine/org-{id}/notes/{note_id}` and is
unavailable for preview/download until the `attachments` queue receives a
clean result from `clamd`. Infected payloads are deleted; scanner failures
remain quarantined and fail closed. After deploying the scan-state migration,
queue existing files with `php artisan attachments:scan-pending`. Production
must provide ClamAV and an active worker for the configured attachment queue.

### Production Ubuntu: attachment scanning

The production host needs PHP 8.3+, Redis, `clamd`, and a continuously running
Laravel worker. The application talks to ClamAV over its `INSTREAM` TCP
protocol; keep that port bound to loopback because it has no authentication or
transport encryption.

Install and enable the required services:

```bash
sudo apt update
sudo apt install -y clamav clamav-daemon redis-server supervisor
sudo systemctl enable --now redis-server supervisor clamav-daemon
```

In `/etc/clamav/clamd.conf`, enable a loopback-only TCP listener and allow a
stream slightly larger than the application's 50 MB upload limit:

```ini
TCPSocket 3310
TCPAddr 127.0.0.1
StreamMaxLength 55M
```

Restart ClamAV and verify that port 3310 is listening locally:

```bash
sudo systemctl restart clamav-daemon
sudo systemctl status clamav-daemon
sudo ss -ltnp | grep 3310
```

Set the following values in the production `.env` alongside the real
production URL, database, mail, and attachment-storage credentials:

```dotenv
APP_ENV=production
APP_DEBUG=false

QUEUE_CONNECTION=redis
ATTACHMENT_SCAN_QUEUE_CONNECTION=redis
ATTACHMENT_SCAN_QUEUE=attachments

REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

CLAMAV_HOST=127.0.0.1
CLAMAV_PORT=3310
CLAMAV_TIMEOUT=60
UPLOAD_MAX_KB=51200
```

If ClamAV runs in a separate container, set `CLAMAV_HOST` to its private
service/DNS name instead of `127.0.0.1`. Do not publish port 3310 to the public
network.

Create `/etc/supervisor/conf.d/notetracker-attachments.conf` (adjust the path
and service user to match the deployment):

```ini
[program:notetracker-attachments]
process_name=%(program_name)s_%(process_num)02d
directory=/var/www/notetracker/note-tracker-backend
command=/usr/bin/php artisan queue:work redis --queue=attachments --sleep=3 --tries=3 --timeout=75 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/notetracker/note-tracker-backend/storage/logs/attachment-worker.log
stopwaitsecs=90
```

Load the worker and refresh Laravel's cached configuration:

```bash
cd /var/www/notetracker/note-tracker-backend
php artisan optimize:clear
php artisan migrate --force
php artisan optimize

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start 'notetracker-attachments:*'
sudo supervisorctl status
```

After each deployment, restart the long-lived worker so it loads the new code
and configuration:

```bash
php artisan queue:restart
```

To recover attachments already stuck in `pending` or `error`, dispatch them
again after ClamAV and the worker are healthy:

```bash
php artisan attachments:scan-pending --chunk=200
php artisan queue:failed
tail -f storage/logs/attachment-worker.log storage/logs/laravel.log
```

Once a scan succeeds, the attachment changes from `pending` to `clean`, the UI
shows **Ready**, and preview/download becomes available. See the
[Laravel queue documentation](https://laravel.com/docs/12.x/queues) and
[ClamAV clamd protocol documentation](https://docs.clamav.net/manual/Usage/ClamdProtocol.html)
for operational details.

## Demo credentials

All users have password **`password`**. Organization members automatically use
the organization assigned to their account. Super admins start in platform
administration and select an organization before entering its workspace.
| Email                 | Role        | Organization          |
|-----------------------|-------------|-----------------------|
| admin@notetracker.app | super_admin | — (platform)          |
| alice@acme.test       | org_admin   | Acme Corp (`acme`)    |
| eddie@acme.test       | editor      | Acme Corp             |
| vera@acme.test        | viewer      | Acme Corp             |
| gina@globex.test      | org_admin   | Globex (`globex`)     |
| evan@globex.test      | editor      | Globex                |
| henry@helios.test     | org_admin   | Helios Energy (suspended) |
| ivy@helios.test       | editor      | Helios Energy (suspended) |

Seeded demo data: 3 organizations, 7 projects, 24 notes with full timelines
(incl. external replies), per-org statuses (Need Update / Closed / Need CR /
Need Tech), project share links, pending note requests, 20+ activity rows.

**Demo one-time update link:** `GET/POST /api/public/update/demo-update-token-482916`
with verification code **482916** (also printed by the seeder). A second,
already-used token exists to demonstrate the `409` "already replied" path.

## Architecture

```
Client (React SPA) ──► Nginx ──► PHP-FPM (Laravel 12)
                                   ├── ResolveTenantContext → binds "currentOrganization"
                                   ├── BelongsToOrganization global scope (tenant isolation)
                                   ├── Sanctum token auth (roles: super_admin|org_admin|editor|viewer)
                                   ├── MySQL 8 (InnoDB/utf8mb4, soft deletes on notes+projects)
                                   ├── File cache + synchronous queues
                                   └── S3 disk (attachments under org-{id}/notes/{note_id}/)
```

- **Tenancy**: account-based on one application host. Members use `user.organization_id`;
  super admins select with `X-Organization-ID`. `ResolveTenantContext` binds
  `currentOrganization`, and global scopes filter every selected workspace.
- **Suspended tenants** are read-only: `EnsureNotSuspended` returns 403 on mutations.
- **Audit**: every mutation writes an `activity_logs` row via `App\Support\Activity::log()`.
- **Status changes**: `Note::changeStatus($status, $actor)` runs in a transaction
  (update + `status_change` timeline entry + activity log) and fires
  `App\Events\NoteStatusChanged` for follower notifications.

### Key contracts (consumed by api-core / flows modules)

- Container: `currentOrganization` → `?App\Models\Organization`; helper `current_org()`.
- Roles on `users.role`: `super_admin | org_admin | editor | viewer`
  (`User::isSuperAdmin()`, `hasRole()`, `canEditNotes()`, `canManageOrganization()`).
- Middleware aliases: `tenant`, `tenant-member`, `not-suspended`, `role:super_admin|org_admin,...`.
- `Note::isGlobal()`, `Note::changeStatus()`, `Note::followers()`.
- `ShareToken::isValid()` (not revoked/expired, single-use for update requests),
  `markUsed()`, `revoke()`, `isProjectView()`, `isUpdateRequest()`.
- `Project::globalNotes()`, `Organization::isSuspended()`.
- Config: `config/notetracker.php` (token TTLs, upload whitelist,
  email queue name, default statuses).

## API summary

Base prefix `/api` (see SPEC §5 for full details):

- **Auth**: `POST /auth/login|logout|refresh`, `GET /auth/me`
- **Super admin** (`/admin`, `role:super_admin`): organizations CRUD, stats, suspend/activate
- **Projects**: CRUD + `POST/DELETE /projects/{id}/share`
- **Notes**: CRUD, `POST /notes/{id}/status`, `/request-update`, `/reply`, `GET /notes/{id}/timeline`
- **Attachments**: `POST /notes/{id}/attachments`, `GET /attachments/{id}/download`
- **Users / Statuses / Pending requests / Notifications**: tenant-scoped management endpoints
- **Public** (`/api/public`, unauthenticated, throttled): `GET /p/{token}`,
  `POST /p/{token}/request-note`, `GET|POST /update/{token}`

## Module ownership (build order)

1. **foundation** (this stage): skeleton, config, docker, migrations, models,
   traits, middleware, factories, seeders, `Activity` helper.
2. **api-core**: form requests, resources, policies, authenticated controllers/routes.
3. **flows**: services (ShareToken/UpdateRequest/Attachment/Stats), jobs,
   notifications, mailables, public controllers (`routes/api-public.php`),
   `share-tokens:clean` console command.

## Testing

Reference PHPUnit feature tests live in `tests/Feature` (status-change and
tenant-scoping contracts). Run inside the container:

```bash
docker compose exec app php artisan test
```

