# documents module

Drop-in NestJS module for the **Staff Document Portal** — role-based document sharing with email OTP + password unlock and a full audit trail. Matches the exact conventions of your existing `audit-requests` / `audit-schedules` modules.

## What it does — matched to your HTML mockup

| Tab in `document-portal.html`     | Endpoints                                                                              |
| --------------------------------- | -------------------------------------------------------------------------------------- |
| ① **Admin — Upload & Assign**     | `POST /documents` (multipart, roles + security)                                        |
| ② **All Documents** (KPIs + list) | `GET /documents/analytics`, `GET /documents`                                           |
| ③ **Staff View** (my docs)        | `GET /documents` (auto-filtered by role), `POST /documents/:id/request-otp` → `/unlock` → `/view` or `/download` |
| ④ **Audit Trail**                 | `GET /documents/:id/access-log`, `GET /documents/:id/access-log/export`                |

## Folder structure

```
documents/
├── documents.module.ts
├── documents.controller.ts
├── dto/
│   ├── upload-document.dto.ts
│   ├── update-document.dto.ts
│   ├── list-documents.dto.ts
│   ├── unlock-document.dto.ts
│   └── list-access-log.dto.ts
├── entities/
│   ├── document.entity.ts
│   ├── document-role-assignment.entity.ts
│   ├── document-otp.entity.ts
│   └── document-access-log.entity.ts
├── services/
│   └── documents.service.ts
├── migrations/
│   └── 1724100000000-CreateDocumentPortalTables.ts
├── templates/
│   └── document-otp-email-template.ts
└── README.md
```

## Installation

### 1. Drop the folder into your project

Place under `src/documents/` (same level as your existing `audit-requests/`).

### 2. Install the extra dep

```bash
npm i bcrypt
npm i -D @types/bcrypt
```

`multer` and `class-validator` are already used by `audit-requests`, so nothing else to add.

### 3. Register the module in `AppModule`

```ts
import { DocumentsModule } from './documents/documents.module';

@Module({
  imports: [
    // ... your other modules
    AuditRequestsModule,
    DocumentsModule,   // ← add this line
  ],
})
export class AppModule {}
```

### 4. Run the migration

```bash
npm run typeorm migration:run
```

Creates:
- `documents`
- `document_role_assignments` (FK to `roles.id` — INT, matches your existing table)
- `document_otps`
- `document_access_log`

…and inserts one row into `modules` so the doc portal shows up in your generic module-driven UI.

### 5. Verify JWT + user id

Controller uses the same pattern as `audit-requests`:

```ts
const user = (req as any).user;
const id = user?.id ?? user?.userId ?? user?.sub;
```

If your JwtStrategy exposes user id under a different key, this line is the one to change.

## How the flow works

### Admin upload (POST /documents)

1. Multipart `file` lands on disk under `uploads/documents/`.
2. Service checks the caller has role_id = 1 (Super-admin).
3. In one transaction it inserts:
   - the `documents` row (bcrypts the password if provided)
   - one `document_role_assignments` row per selected role
4. Response includes the doc + its role assignments.

### Staff view (GET /documents)

The service reads the caller's role IDs from `user_roles`, then filters:

```sql
d.id IN (SELECT dra.document_id FROM document_role_assignments dra
         WHERE dra.role_id IN (:my_role_ids))
AND d.status = 'active'
AND (d.expiry_date IS NULL OR d.expiry_date >= CURDATE())
```

Admin skips this filter — sees everything, including archived.

### Unlock (POST /documents/:id/request-otp → /unlock → /view or /download)

```
1. Staff clicks "Open"
   → POST /documents/42/request-otp
   → service generates a 6-digit OTP, bcrypts it, stores in document_otps,
     emails the plain OTP to the user via MailsService,
     writes an 'otp_sent' row to document_access_log.

2. Staff types password + OTP into the modal
   → POST /documents/42/unlock  { password, otp }
   → service verifies both (with attempt-count + expiry checks),
     consumes the OTP row,
     issues a 15-minute opaque access token,
     returns { token, view_url, download_url }.

3. Frontend opens view_url
   → GET /documents/42/view?token=xxx
   → service verifies the token,
     writes a 'viewed' row to document_access_log,
     streams the file inline.

4. If allow_download = 1 and the user hits download_url
   → GET /documents/42/download?token=xxx
   → service verifies the token AND doc.allow_download,
     writes 'downloaded' to the log,
     streams the file as attachment.
```

Every failure (wrong password, wrong/expired OTP, no access, missing token) is logged with `otp_failed` / `password_failed` / `denied`. Nothing is silently dropped.

### Access tokens

Access tokens live in an in-memory Map for 15 minutes. That's fine for a single-node deployment. If you scale horizontally, replace `ACCESS_TOKENS` in `services/documents.service.ts` with a Redis client — the interface is trivial (`get / set / delete` with TTL).

## API endpoints

| Method | Path                                     | Role         | Action                                                       |
| ------ | ---------------------------------------- | ------------ | ------------------------------------------------------------ |
| POST   | `/documents`                             | Admin        | Upload + assign to roles (multipart)                         |
| GET    | `/documents`                             | Any          | List (admin sees all, staff sees only their roles')          |
| GET    | `/documents/analytics`                   | Admin        | Dashboard counts (total, active, opens, failed attempts)     |
| GET    | `/documents/:id`                         | Any w/ access| Metadata + role assignments                                  |
| PATCH  | `/documents/:id`                         | Admin        | Edit metadata / security / role reassignment                 |
| DELETE | `/documents/:id`                         | Admin        | Soft-archive (audit trail preserved)                         |
| POST   | `/documents/:id/request-otp`             | Staff w/ access | Sends OTP to user's email                                 |
| POST   | `/documents/:id/unlock`                  | Staff w/ access | Verifies password + OTP → issues access token            |
| GET    | `/documents/:id/view?token=...`          | Staff w/ token  | Streams file inline; logs `viewed`                       |
| GET    | `/documents/:id/download?token=...`      | Staff w/ token  | Streams as attachment; logs `downloaded` (403 if view-only) |
| GET    | `/documents/:id/access-log`              | Admin        | Paginated audit trail                                        |
| GET    | `/documents/:id/access-log/export`       | Admin        | CSV export                                                   |

## Security notes

- **Passwords are bcrypt-hashed** before hitting the DB. Empty password on edit clears it.
- **OTPs are bcrypt-hashed** — the plain code lives only in the user's inbox. Max 5 wrong attempts before that OTP is consumed.
- **Access tokens are opaque 24-byte random strings** with a 15-min TTL, single (user, doc) binding. Not a JWT — they can't be introspected client-side.
- **Downloads are gated by `allow_download`** on the doc; a valid token can still be rejected at the download step.
- **The audit log is append-only** and cascades only from `documents` (not from `users`) so removing a user does not delete their access history.
- **Uploader keeps access** to their own doc even if not admin (matches "assigned by Boss" behaviour in the UI).

## Frontend response shape

The list endpoint returns `{ data, meta: { total, page, limit, totalPages } }` — same as your `audit-schedules` / `audit-requests` endpoints, so your frontend pagination logic works unchanged.
