# Reviewer Paper Access — Phase 1 Discovery Report

**Date:** 2026-04-11
**Scope:** Discovery only. No code changes, no solutions proposed.
**Problem:** When a Reviewer opens an assigned paper, there is no viewer, no download, no way to read the manuscript. Reviewers are rating blindly.
**Project root:** `/Users/umeshpratapsingh/Documents/Shodh-Sanchayan/ShodhShanchayanCodeBase`

---

## Section 1 — Project Structure & Tech Stack

### Top-level layout
```
ShodhShanchayanCodeBase/
├── docs/                         ← existing docs folder
├── shodh-sanchayan-api/          ← BACKEND (Spring Boot)
│   ├── docker/                   ← docker assets
│   ├── docker-compose.yml        ← PostgreSQL 16 (host port 5433), MinIO
│   ├── pom.xml
│   ├── src/main/java/com/shodh/sanchayan/
│   ├── src/main/resources/
│   │   ├── application.yml
│   │   └── db/migration/         ← Flyway V1–V6
│   └── uploads/                  ← local file storage root (./uploads/manuscripts/...)
└── shodh-sanchayan-ui/           ← FRONTEND (Next.js)
    ├── package.json
    ├── next.config.js
    ├── tailwind.config.ts
    └── src/
        ├── app/                  ← App Router routes
        ├── components/
        ├── lib/
        ├── types/
        └── styles/
```

### Backend
- **Framework:** Spring Boot `3.3.5`
- **Language:** Java `21`
- **Build:** Maven (`pom.xml`)
- **Key deps:**
  - `spring-boot-starter-web`, `spring-boot-starter-data-jpa`, `spring-boot-starter-validation`
  - `spring-boot-starter-security` + `spring-boot-starter-oauth2-client`
  - `jjwt-api / jjwt-impl / jjwt-jackson` `0.12.6`
  - `flyway-core` + `flyway-database-postgresql` `10.21.0`
  - `aws-java-sdk-s3` `2.29.26` (used by MinIO/S3 storage impl)
  - `razorpay-java` `1.4.8`, `mapstruct` `1.6.3`, `springdoc-openapi` `2.3.0`
  - **PDF / document libs:** `flying-saucer-pdf-openpdf 9.1.22`, `openpdf 2.0.3`, `poi-ooxml 5.2.5`
  - **No PDFBox, no Apache Tika, no metadata-stripping library**

### Frontend
- **Framework:** Next.js `14.2.20` (App Router), React `18.3.1`, TypeScript `5.7.2`
- **Package manager:** npm (`package-lock.json`)
- **Key deps:**
  - State: `zustand 5.0.2`
  - Data: `@tanstack/react-query 5.62.0`, `axios 1.7.9`
  - Forms: `react-hook-form 7.54.0` + `zod 3.24.1`
  - UI: Radix UI primitives, `sonner`, `recharts`, `tailwindcss 3.4.17`
  - **PDF rendering:** `react-pdf 9.2.1` — **declared, never imported anywhere in `src/`**
  - No `pdfjs-dist`, no `@react-pdf/renderer`

### Database
- **Engine:** PostgreSQL 16 (Alpine), containerized via `docker-compose.yml`, exposed on host port `5433`, container port `5432`
- **Schema:** `shodh_sanchayan`, user `shodh`
- **ORM:** Spring Data JPA / Hibernate, `ddl-auto: validate` (Flyway owns the schema)
- **Migration tool:** Flyway `10.21.0`
- **Migration files** in `src/main/resources/db/migration/`:
  - `V1__base_schema.sql` — enums + all tables
  - `V2__cms_add_enum_values.sql`
  - `V3__cms_expanded_sections.sql`
  - `V4__add_must_change_password.sql`
  - `V5__magazine_quarter_length.sql`
  - `V6__magazine_paper_pdf_key.sql`

### File storage
- Interface: `service/StorageService.java` — `upload / uploadBytes / download / delete / getPresignedUrl`
- Two implementations selected by `app.storage.type`:
  - `local` (default) → `LocalStorageServiceImpl` writes to `./uploads/{folder}/{UUID}_{filename}`
  - `minio` → `StorageServiceImpl` writes to S3/MinIO bucket `shodh-manuscripts`
- MinIO container is present in `docker-compose.yml` but default is `local`.

### File processing libraries already present
- Flying Saucer + OpenPDF — used for magazine PDF generation.
- Apache POI — DOCX text extraction.
- **No PDF viewer, no metadata sanitizer, no PDF redaction, no Tika, no PDFBox.**

### Gaps
- No document-rendering or metadata-stripping library in either backend or frontend.
- `react-pdf` is installed but unused — dead dependency.

### Risks
- Adding a PDF viewer later will require either wiring up `react-pdf` (needs workers/CDN setup) or serving raw PDFs to `<iframe>` / `<object>` (needs proper Content-Disposition + auth).

---

## Section 2 — File Storage

### Where files land
- **Author submits** a paper → `POST /papers` (multipart: `data` JSON part + `manuscript` file part), handled in `controller/api/PaperController.java`:
  ```java
  @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  @PreAuthorize("hasRole('AUTHOR')")
  public ResponseEntity<PaperDetailResponse> submit(
      @Valid @RequestPart("data") SubmitPaperRequest request,
      @RequestPart("manuscript") MultipartFile manuscript) {
      UUID userId = SecurityUtils.getCurrentUserId();
      return ResponseEntity.ok(paperService.submit(userId, request, manuscript));
  }
  ```
- **Persistence** in `service/impl/PaperServiceImpl.java`:
  ```java
  if (manuscript != null && !manuscript.isEmpty()) {
      String manuscriptKey = storageService.upload("manuscripts", referenceNo, manuscript);
      paper.setManuscriptKey(manuscriptKey);
      paper.setManuscriptName(manuscript.getOriginalFilename());
      paper.setManuscriptSize(manuscript.getSize());
  }
  ```
- **Local path pattern:** `./uploads/manuscripts/{UUID}_{referenceNo}` (the `referenceNo` is used as the filename argument, not the original filename; `LocalStorageServiceImpl` then prepends a UUID).
- **S3/MinIO key pattern:** `manuscripts/{UUID}_{referenceNo}`
- **Path-traversal guard:** `LocalStorageServiceImpl` validates that the resolved target `startsWith(rootDir)`.

### Accepted file formats
- **No code-level file-type validation** anywhere in the controller or service — no `if (!contentType.equals("application/pdf"))` check.
- Only Spring multipart limits enforce anything:
  ```yaml
  spring.servlet.multipart:
    max-file-size: 25MB
    max-request-size: 30MB
  ```
- CMS labels ("PDF, DOCX, LaTeX") are informational only.

### Entity & columns
- Table `papers` (from `V1__base_schema.sql`) stores:
  - `manuscript_key VARCHAR(500)` — storage key returned by `StorageService.upload(...)`
  - `manuscript_name VARCHAR(255)` — original filename
  - `manuscript_size BIGINT` — size in bytes
- Mapped on `entity/Paper.java`:
  ```java
  @Column(name = "manuscript_key")  private String manuscriptKey;
  @Column(name = "manuscript_name") private String manuscriptName;
  @Column(name = "manuscript_size") private Long manuscriptSize;
  ```

### File versioning
- `paper_revisions` table + `entity/PaperRevision.java` tracks revisions with its own `manuscript_key`, `manuscript_name`, `version`, `change_notes`, `uploaded_at`.
- Endpoint: `POST /papers/{id}/revisions` (`@PreAuthorize("hasRole('AUTHOR')")`).

### Gaps
- No checksum / content hash column — cannot detect tampering or duplicate uploads.
- No explicit content-type / extension validation.
- No antivirus scan step.
- No metadata stripping / sanitization.

### Risks
- Uploaded PDFs retain all embedded metadata (author, producer, title, comments) → **any reviewer who gets the file can read the author's identity directly from PDF properties**, breaking double-blind review by construction.
- A malicious author could upload any file type up to 25 MB (executables, archives, etc.) — nothing blocks it.

---

## Section 3 — Database Schema

### users
```sql
CREATE TABLE users (
    id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email                 VARCHAR(255) NOT NULL UNIQUE,
    phone                 VARCHAR(20)  UNIQUE,
    password_hash         VARCHAR(255),
    name_hi               VARCHAR(255) NOT NULL,
    name_en               VARCHAR(255) NOT NULL,
    role                  user_role    NOT NULL DEFAULT 'AUTHOR',
    status                user_status  NOT NULL DEFAULT 'ACTIVE',
    auth_provider         auth_provider NOT NULL DEFAULT 'LOCAL',
    oauth_id              VARCHAR(255),
    avatar_url            VARCHAR(500),
    designation           VARCHAR(255),
    institution           VARCHAR(255),
    department            VARCHAR(255),
    orcid_id              VARCHAR(50),
    bio_hi                TEXT,
    bio_en                TEXT,
    email_verified        BOOLEAN NOT NULL DEFAULT false,
    phone_verified        BOOLEAN NOT NULL DEFAULT false,
    invited_by            UUID REFERENCES users(id),
    must_change_password  BOOLEAN NOT NULL DEFAULT false,  -- added in V4
    last_login_at         TIMESTAMPTZ,
    created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- enum user_role values: 'AUTHOR', 'REVIEWER', 'ADMIN'
```

### papers
```sql
CREATE TABLE papers (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    reference_no        VARCHAR(50) NOT NULL UNIQUE,
    title_hi            TEXT NOT NULL,
    title_en            TEXT NOT NULL,
    abstract_hi         TEXT,
    abstract_en         TEXT,
    keywords            TEXT[],
    category_id         BIGINT REFERENCES categories(id),
    subcategory_id      BIGINT REFERENCES categories(id),
    status              paper_status NOT NULL DEFAULT 'DRAFT',
    submitted_by        UUID NOT NULL REFERENCES users(id),
    manuscript_key      VARCHAR(500),
    manuscript_name     VARCHAR(255),
    manuscript_size     BIGINT,
    doi                 VARCHAR(255),
    volume              INT,
    issue               INT,
    page_start          INT,
    page_end            INT,
    citation_count      INT NOT NULL DEFAULT 0,
    download_count      INT NOT NULL DEFAULT 0,
    plagiarism_score    NUMERIC(5,2),
    plagiarism_report   JSONB,
    required_reviewers  INT NOT NULL DEFAULT 2,
    admin_notes         TEXT,
    submitted_at        TIMESTAMPTZ,
    accepted_at         TIMESTAMPTZ,
    published_at        TIMESTAMPTZ,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### reviews (this is also the reviewer-assignment table)
```sql
CREATE TABLE reviews (
    id                   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    paper_id             UUID NOT NULL REFERENCES papers(id),
    reviewer_id          UUID NOT NULL REFERENCES users(id),
    status               review_status NOT NULL DEFAULT 'PENDING',
    originality_score    INT,   -- 1..10
    methodology_score    INT,
    clarity_score        INT,
    relevance_score      INT,
    references_score     INT,
    overall_score        NUMERIC(4,2),
    recommendation       review_recommendation, -- ACCEPT / MINOR_REVISION / MAJOR_REVISION / REJECT
    comments_to_author   TEXT,
    confidential_notes   TEXT,
    assigned_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    deadline_at          TIMESTAMPTZ,
    started_at           TIMESTAMPTZ,
    completed_at         TIMESTAMPTZ,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (paper_id, reviewer_id)
);
```
→ **There is no separate `reviewer_assignments` table.** The `reviews` row IS the assignment. A reviewer becomes "assigned" the moment an admin inserts a `reviews` row via `POST /admin/papers/{id}/assign`.

### paper_coauthors (file-adjacent — stores author PII)
```sql
CREATE TABLE paper_coauthors (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    paper_id          UUID NOT NULL REFERENCES papers(id) ON DELETE CASCADE,
    user_id           UUID REFERENCES users(id),         -- nullable for external authors
    name_hi           VARCHAR(255) NOT NULL,
    name_en           VARCHAR(255) NOT NULL,
    email             VARCHAR(255),
    institution       VARCHAR(255),
    author_order      INT NOT NULL DEFAULT 1,
    is_corresponding  BOOLEAN NOT NULL DEFAULT false,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### paper_revisions
```sql
CREATE TABLE paper_revisions (
    id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    paper_id         UUID NOT NULL REFERENCES papers(id) ON DELETE CASCADE,
    version          INT NOT NULL DEFAULT 1,
    manuscript_key   VARCHAR(500) NOT NULL,
    manuscript_name  VARCHAR(255),
    change_notes     TEXT,
    uploaded_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### Entity classes (verbatim excerpts)

**`entity/User.java`** — fields above mapped with Lombok; `@Enumerated(EnumType.STRING) private UserRole role`.
**`enums/UserRole.java`:** `public enum UserRole { AUTHOR, REVIEWER, ADMIN }`.
**`entity/Paper.java`** — includes `manuscriptKey`, `manuscriptName`, `manuscriptSize`, `@OneToMany List<PaperCoauthor> coauthors`, `@OneToMany List<PaperRevision> revisions`, `@OneToMany List<Review> reviews`.
**`entity/Review.java`** — `@Table(name = "reviews", uniqueConstraints = @UniqueConstraint(columnNames = {"paper_id", "reviewer_id"}))`, 5 score integers, `overallScore BigDecimal`, recommendation enum, comment/notes fields, timestamps.
**`entity/PaperCoauthor.java`** — name/email/institution/order/corresponding.
**`entity/PaperRevision.java`** — own `manuscriptKey`/`manuscriptName`/`version`/`changeNotes`/`uploadedAt`.

### Gaps
- No dedicated assignment table → attempting to track "accepted assignment" vs "declined" is squeezed into `review_status` enum (PENDING / IN_PROGRESS / COMPLETED / DECLINED / EXPIRED).
- No per-access audit table for "reviewer X downloaded paper Y".
- No content-hash/integrity column on `papers` or `paper_revisions`.

### Risks
- The same `reviews` row carries both "is this reviewer allowed to see the paper" and "review scores" — no separation of concerns, so any future "can reviewer see file" check has to look up the reviews table directly.
- `paper_coauthors` stores PII (`email`, `institution`, `name_*`) that absolutely must not leak to reviewers.

---

## Section 4 — Existing API Endpoints

### Paper / Submission — `controller/api/PaperController.java` (base `/papers`)

| Method | Path | Handler | Auth | File bytes? |
|---|---|---|---|---|
| GET  | `/papers` | `myPapers(Pageable)` | `@PreAuthorize("hasRole('AUTHOR')")` | no |
| **GET**  | **`/papers/{id}`** | **`getById(UUID)`** | **NONE — no `@PreAuthorize`** | no (but returns `PaperDetailResponse` incl. coauthor PII) |
| POST | `/papers` | `submit(@RequestPart data, @RequestPart manuscript)` | `@PreAuthorize("hasRole('AUTHOR')")` | **accepts** multipart file |
| POST | `/papers/{id}/revisions` | `uploadRevision(...)` | `@PreAuthorize("hasRole('AUTHOR')")` | **accepts** multipart file |
| PUT  | `/papers/{id}` | `updatePaper(...)` | `@PreAuthorize("hasRole('AUTHOR')")` | no |
| GET  | `/papers/search` | `search(String q, Pageable)` | public | no |

### Review — `controller/api/ReviewController.java` (base `/reviewer/reviews`)

| Method | Path | Handler | Auth | File bytes? |
|---|---|---|---|---|
| GET  | `/reviewer/reviews/pending`   | `pending()`                 | `hasAnyRole('REVIEWER','ADMIN')` | no |
| GET  | `/reviewer/reviews/completed` | `completed()`               | `hasAnyRole('REVIEWER','ADMIN')` | no |
| POST | `/reviewer/reviews/{id}/submit`  | `submitReview(UUID, SubmitReviewRequest)` | `hasAnyRole('REVIEWER','ADMIN')` | no |
| POST | `/reviewer/reviews/{id}/decline` | `decline(UUID)`             | `hasAnyRole('REVIEWER','ADMIN')` | no |

### Admin Papers — `controller/admin/AdminPaperController.java` (base `/admin/papers`)

| Method | Path | Handler | Auth |
|---|---|---|---|
| GET  | `/admin/papers`                       | `list(PaperStatus, Pageable)`       | `hasRole('ADMIN')` |
| POST | `/admin/papers/{id}/assign`           | `assignReviewer(UUID, Map body)`    | `hasRole('ADMIN')` |
| GET  | `/admin/papers/{id}/assigned-reviewers` | `getAssignedReviewers(UUID)`      | `hasRole('ADMIN')` |
| POST | `/admin/papers/{id}/publish`          | `publish(UUID)`                     | `hasRole('ADMIN')` |
| POST | `/admin/papers/{id}/request-revision` | `requestRevision(UUID, Map body)`   | `hasRole('ADMIN')` |
| POST | `/admin/papers/{id}/reject`           | `reject(UUID, Map body)`            | `hasRole('ADMIN')` |

### File access endpoints (author flow / reviewer flow)
- **None exist for manuscripts.** There is no `GET /papers/{id}/manuscript`, no `/download`, no streaming controller, no FileController / UploadController / DownloadController class for author manuscripts.
- The **only file-bytes endpoints in the system** are the magazine ones (`MagazineController`):
  - `GET /magazines/{id}/pdf` — public, returns magazine shell PDF `byte[]`
  - `GET /magazines/{id}/papers/{paperOrder}/pdf` — public, returns individual paper PDF `byte[]`
  - These serve a separately-rendered magazine PDF, not the author's original manuscript.

### Gaps
- **There is no endpoint whatsoever that returns manuscript bytes to a reviewer.**
- `GET /papers/{id}` has no authorization.
- No endpoint accepts a `manuscriptKey` and streams the file back.
- No signed-URL endpoint for reviewers.

### Risks
- Reviewers cannot read the paper through any API — they are literally scoring based on paper title + reference number (whatever the Review DTO exposes).
- `GET /papers/{id}` is an IDOR waiting to be exploited: any authenticated user (and depending on `SecurityConfig`, any request at all that slips past the filter) can fetch any paper's details, including coauthor PII.

---

## Section 5 — Frontend — Reviewer Review Page

### Route & file path
- **Route:** `/reviewer/review/{reviewId}`
- **File:** `src/app/(dashboard)/reviewer/review/[id]/page.tsx`

### API calls on mount
- `useQuery(["reviewer-pending"], reviewApi.pending)` → `GET /reviewer/reviews/pending`
- That's it. The page then does `pendingReviews?.find(r => r.id === reviewId)` to pick out the current assignment from the list.
- **There is no call to `paperApi.get(paperId)`, no call to any manuscript endpoint.**

### Data fields displayed
- `review.paperTitleEn || review.paperTitleHi`
- `review.paperReferenceNo`
- `review.deadlineAt`
- Form inputs:
  - 5 score sliders (1–10): `originalityScore`, `methodologyScore`, `clarityScore`, `relevanceScore`, `referencesScore`
  - Recommendation buttons: `ACCEPT / MINOR_REVISION / MAJOR_REVISION / REJECT`
  - `commentsToAuthor` (required textarea)
  - `confidentialNotes` (optional textarea)
- Submit button → `reviewApi.submit(reviewId, {...scores, recommendation, commentsToAuthor, confidentialNotes})`

### References to paper file / viewer / download
- **None.** No `<iframe>`, `<embed>`, `<object>`, no `react-pdf` import, no download `<a>`, no manuscriptKey reference, no `manuscript*` field used anywhere on this page.
- Repo-wide grep confirms: `react-pdf` has zero imports anywhere under `src/`.
- `manuscript` references across the codebase:
  - `src/types/index.ts` — `manuscriptName?: string` on `PaperDetail`
  - `src/app/(dashboard)/papers/[id]/page.tsx` — displays `paper.manuscriptName` (author/admin view only, not reviewer)
  - `src/app/(dashboard)/submit/page.tsx` — `fd.append("manuscript", file)` in the author submit handler
  - `src/app/(dashboard)/papers/[id]/page.tsx` — `fd.append("manuscript", ...)` in revision upload
  - i18n — the string "Please upload manuscript"
- **No reviewer-facing code path references the manuscript file in any way.**

### Reviewer dashboard (for context)
- `src/app/(dashboard)/reviewer/page.tsx` — tabs `pending` / `completed`, uses `reviewApi.pending()` and `reviewApi.completed()`, lists paper titles + reference numbers + deadlines. No file links.

### Gaps
- No UI affordance at all to read the paper.
- The review page pulls the review record by re-fetching the full pending list and filtering in JS — there is no dedicated "review detail" endpoint, and therefore no natural place where a manuscript URL would be returned.

### Risks
- Reviewers submitting ratings without reading the paper invalidates the review process.
- `react-pdf` is dead weight (~2 MB worker) shipped to clients for nothing.

---

## Section 6 — Authentication & Authorization

### Security configuration
- `config/security/SecurityConfig.java`:
  - `@EnableMethodSecurity` (enables `@PreAuthorize`)
  - Session: `STATELESS`
  - CSRF: disabled
  - `authorizeHttpRequests`:
    - `permitAll()`: `/auth/login`, `/auth/register`, `/auth/otp/**`, `/public/**`, swagger, `/actuator/health`, `GET /papers/published/**`, `GET /magazines/**`, `GET /categories/**`, `GET /cms/**`
    - `/admin/**` → `hasRole('ADMIN')`
    - `/reviewer/**` → `hasAnyRole('REVIEWER','ADMIN')`
    - everything else → `authenticated()`
  - Adds `JwtAuthenticationFilter` before `UsernamePasswordAuthenticationFilter`
  - BCrypt password encoder
- JWT: HS256, 24h access / 7d refresh (`app.jwt.expiration-ms`).
- `JwtAuthenticationFilter` reads `Authorization: Bearer ...`, loads `UserDetails`, sets `SecurityContextHolder`.

### Role names
`AUTHOR`, `REVIEWER`, `ADMIN` (enum `UserRole`; Spring prefixes with `ROLE_` implicitly when using `hasRole(...)`)

### How current user is extracted in controllers
- Via `util/SecurityUtils.java`:
  ```java
  public static UUID getCurrentUserId() {
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      if (auth != null && auth.getPrincipal() instanceof CustomUserPrincipal principal) {
          return principal.getUserId();
      }
      throw new BusinessException("User not authenticated");
  }
  ```
- Controllers call `SecurityUtils.getCurrentUserId()`; a `CustomUserPrincipal` wraps the user's UUID + email.

### "Reviewer assigned to this paper" check
- **Exists only at review-submit time**, in `service/impl/ReviewServiceImpl.submitReview(...)`:
  ```java
  Review review = reviewRepository.findById(reviewId).orElseThrow(...);
  if (!review.getReviewer().getId().equals(reviewerId)) {
      throw new BusinessException("Only the assigned reviewer can submit this review");
  }
  ```
- That check is on a **review id** (not a paper id), and only inside the submit mutation.
- **There is no helper, no predicate, no `@PreAuthorize("@reviewAuth.canAccessPaper(#id)")`, and no endpoint that takes a `paperId` and enforces "the caller has a row in `reviews` for this paper".**

### Gaps
- No method-level authorization on `GET /papers/{id}`.
- No authorization helper named like `ReviewAccessService` / `PaperAccessPolicy` — each controller method performs (or skips) its own checks inline.
- No audit logging of who called what.

### Risks
- Any authenticated user can enumerate papers via `GET /papers/{id}` and read coauthor PII.
- When a file-serving endpoint is eventually added, developers will have no pre-built authorization helper and are likely to either copy the inline check pattern (error-prone) or forget the check entirely.

---

## Section 7 — Double-Blind Status

### Does submission collect author PII?
- **Yes.** `SubmitPaperRequest` accepts `coauthors` with `nameHi`, `nameEn`, `email`, `institution`. The submitting user is recorded on `papers.submitted_by`, and a join through `users` exposes their full profile. All of this is stored in `paper_coauthors` and `users`.

### Reviewer-facing API responses
- `ReviewResponse` DTO (returned from `/reviewer/reviews/pending` and `/completed`):
  ```java
  private UUID id;
  private UUID paperId;
  private String paperTitleHi;
  private String paperTitleEn;
  private String paperReferenceNo;
  private ReviewStatus status;
  // 5 score ints, overallScore, recommendation, commentsToAuthor,
  // assignedAt, deadlineAt, completedAt
  ```
  **No author fields.** Good — this DTO is double-blind-clean.
- **But:** `GET /papers/{id}` returns `PaperDetailResponse`, which DOES include:
  ```java
  private List<CoauthorResponse> coauthors;
  // CoauthorResponse: nameHi, nameEn, email, institution, authorOrder, corresponding
  ```
  and this endpoint has **no `@PreAuthorize`** — any authenticated user (reviewer included) can call it and unmask the authors.

### Reviewer-facing frontend
- The reviewer review page and dashboard render only `paperTitleHi/En` and `paperReferenceNo` from the `Review` DTO — correct, no author identity displayed.
- No code path on the reviewer side fetches `GET /papers/{id}`; the leak is only reachable if a reviewer (or anyone) hits the endpoint directly (browser devtools / curl).

### Metadata stripping on uploaded files
- **Absent.** No code in `PaperServiceImpl`, `LocalStorageServiceImpl`, `StorageServiceImpl`, or anywhere else opens the uploaded PDF to strip `/Author`, `/Title`, `/Creator`, `/Producer` metadata or XMP.
- The project ships Flying Saucer + OpenPDF + POI (all generation/extraction libraries) but **no sanitization library** (no PDFBox, no Tika, no pdf-lib analog).
- Uploaded manuscripts are stored byte-identical to what the author uploaded.

### Gaps
- Blinding is enforced only in one DTO (`ReviewResponse`) and one page (`reviewer/review/[id]`).
- The `/papers/{id}` hole + lack of metadata stripping makes blinding trivially defeatable by any technical reviewer.

### Risks
- **Double-blind is nominal, not real.** Two independent attack paths:
  1. Call `GET /papers/{id}` directly from browser devtools.
  2. Once a file endpoint exists (or if a reviewer obtains the file by other means), read `/Author` from the PDF metadata.
- Compliance/ethics risk if the platform publicly advertises double-blind peer review.

---

## Section 7 (wrap) — Root cause summary

**Reviewers cannot access paper content because the feature is simply not implemented end-to-end:**

1. **Backend:** No endpoint exists that streams a manuscript's bytes to the caller. `LocalStorageServiceImpl.download(key)` exists at the service layer, but no controller calls it for manuscripts. The only file-bytes endpoints (`/magazines/.../pdf`) serve a separately-rendered magazine PDF and are public.
2. **Frontend:** The reviewer review page makes exactly one API call (`reviewApi.pending()`), reads only title + reference + deadline from the response, and renders only a scoring form. There is no PDF viewer, no iframe, no download button, no reference to `manuscriptKey` or `react-pdf`.
3. **API client:** `paperApi` has no `downloadManuscript` / `getFile` / binary-response method.
4. **Authorization layer:** There is no helper that maps "reviewer X is assigned to paper Y" — the only check (`review.reviewer.id == caller`) happens at submit time and takes a `reviewId`, not a `paperId`.
5. **Pre-existing PII leak:** `GET /papers/{id}` is unguarded and returns coauthor name/email/institution, so even if a file endpoint is added, the surrounding metadata surface already defeats double-blind.
6. **No metadata stripping:** Even if a file endpoint is added and properly authorized, the PDF itself will still identify its author from embedded metadata.

**What exists (and can be built on):**
- `reviews` table already ties `(paper_id, reviewer_id)` uniquely → authorization is trivially computable.
- `StorageService.download(key)` and `getPresignedUrl(key, ...)` already exist on the interface and both impls.
- `ReviewResponse.paperId` is already returned → frontend has a paper id to use against a future endpoint.
- `@EnableMethodSecurity` + `@PreAuthorize` wiring is in place for declarative authorization.

**HARD STOP — no solutions proposed.**
