chore: remove agent/claude/planning files from repo and update .gitignore (#320)
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
This commit is contained in:
@@ -1,19 +0,0 @@
|
|||||||
## 2024-05-24 - Lightbox ARIA Attributes
|
|
||||||
**Learning:** Adding `role="dialog"`, `aria-modal="true"`, and `aria-label` to custom modal/lightbox overlays ensures screen readers announce them properly instead of just falling back to reading inner content without context. Additionally, toggles that show/hide panels (like the EXIF info button) must use `aria-expanded` and link to the panel via `aria-controls` for proper screen reader communication.
|
|
||||||
**Action:** When building or modifying custom overlays or toggle buttons in the future, always verify that ARIA attributes are set correctly to match the visual behavior.
|
|
||||||
|
|
||||||
## 2024-05-25 - SVG Icon Accessibility
|
|
||||||
**Learning:** Adding `aria-hidden="true"` to decorative `<svg>` icons within interactive elements (like `<button>` or `<a>`) that already have an `aria-label` is crucial. Otherwise, screen readers may read the raw vector nodes or fall back to confusing announcements in addition to the label. Similarly, when adding text to visual spinners, use `role="alert"` and `aria-live="polite"` on the container while hiding the SVG graphic.
|
|
||||||
**Action:** When adding or modifying interactive elements with icon graphics, always ensure the graphic is explicitly hidden from screen readers using `aria-hidden="true"` if a text alternative (`aria-label`) is provided.
|
|
||||||
|
|
||||||
## 2024-05-26 - Interactive Card ARIA Labels
|
|
||||||
**Learning:** When building interactive card components that contain multiple pieces of text or badges (like titles and photo counts), screen readers may announce them in a fragmented or confusing way.
|
|
||||||
**Action:** Always apply a comprehensive `aria-label` to the parent link/container that encompasses all meaningful visual data (e.g., titles and item counts). Use `aria-hidden="true"` on redundant text or visual child elements, and set `alt=""` on decorative images to consolidate screen reader announcements into a single, accurate interaction point.
|
|
||||||
|
|
||||||
## 2025-05-07 - Skip-to-Content Navigation
|
|
||||||
**Learning:** Keyboard users and screen readers need a way to bypass repetitive header navigation links. Without a visually hidden skip link that becomes visible on focus, these users must tab through every nav item before reaching the main content on every page load.
|
|
||||||
**Action:** Always include a "Skip to content" link as the first focusable element in the DOM, anchored to the main content container. Ensure the main container has `tabIndex={-1}` so it can programmatically receive focus without interrupting normal tab flow.
|
|
||||||
|
|
||||||
## 2024-06-18 - Dynamic Numeric Counters ARIA
|
|
||||||
**Learning:** When displaying dynamic numeric text changes during navigation (like a lightbox image counter), the visual presentation (e.g., "1 / 5") does not provide sufficient context for screen reader users and might not be announced automatically.
|
|
||||||
**Action:** Wrap the counter container with `aria-live="polite"` and `aria-atomic="true"`. Use `.sr-only` to provide a context-rich screen reader string (e.g., "Photo 1 of 5") and hide the visual short-hand using `aria-hidden="true"`.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
description: Update documentation when adding user configuration settings
|
|
||||||
---
|
|
||||||
|
|
||||||
Wenn du neuen Code hinzufügst, der dem Benutzer erlaubt, neue Einstellungen zu machen (beispielsweise durch Anpassungen an der App oder der Schema-Datei für Eigenschaften):
|
|
||||||
|
|
||||||
1. **Beispiel-Konfiguration aktualisieren:**
|
|
||||||
Öffne die Datei `content/gallery.yaml.example` (bzw. `content/settings.yaml.example` falls zutreffend) und ergänze dort das neue Feature als auskommentiertes oder fiktives Anwendungsbeispiel, damit User verstehen, was sie dort eintragen können.
|
|
||||||
|
|
||||||
2. **Dokumentation aktualisieren:**
|
|
||||||
Öffne die Datei `docs/gallery-config.md` (oder vergleichbare Dokus wie `docs/settings-config.md`) und füge eine Erläuterung für die neuen Parameter, ihre Datentypen und deren Wirkung auf die UI hinzu. Gebe wenn möglich Vorher-/Nachher- oder Code-Beispiele an.
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
---
|
|
||||||
name: run-immich-folio
|
|
||||||
description: Build, run, and drive immich-folio. Use when asked to start the app, run its dev server, take a screenshot of the portfolio, interact with the running gallery, or test immich-folio.
|
|
||||||
---
|
|
||||||
|
|
||||||
Immich Folio is a Next.js 16 (App Router) photography portfolio that proxies images from an Immich server. Drive it by starting the Next.js dev server and running `.claude/skills/run-immich-folio/driver.mjs` from the repo root — the script uses Playwright's bundled Chromium to navigate the gallery, verify the health endpoint, and capture screenshots.
|
|
||||||
|
|
||||||
All paths below are relative to the repo root (`immich-folio/`).
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Node 20+ and npm are sufficient — no extra system packages needed.
|
|
||||||
|
|
||||||
Playwright's Chromium is bundled in `node_modules` but must be installed once:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
node_modules/.bin/playwright install chromium
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Copy the example files if `content/gallery.yaml` or `.env.local` don't exist yet:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.local.example .env.local
|
|
||||||
# edit .env.local: set IMMICH_API_URL and IMMICH_API_KEY
|
|
||||||
|
|
||||||
cp content/gallery.yaml.example content/gallery.yaml
|
|
||||||
# edit gallery.yaml: paste Immich album UUIDs
|
|
||||||
```
|
|
||||||
|
|
||||||
Required env vars (`.env.local`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
IMMICH_API_URL=http://your-immich:2283 # base URL of your Immich instance
|
|
||||||
IMMICH_API_KEY=your-api-key-here # from Immich → Admin → API Keys
|
|
||||||
AUTH_SECRET=a-long-random-secret # required in production; auto-generated in dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run (agent path)
|
|
||||||
|
|
||||||
Start the dev server in the background, then run the driver:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev &> /tmp/immich-folio-dev.log &
|
|
||||||
echo $! > /tmp/immich-folio.pid
|
|
||||||
|
|
||||||
# Wait until the health endpoint answers
|
|
||||||
for i in {1..30}; do
|
|
||||||
curl -sf http://localhost:3001/api/health >/dev/null && echo "Ready" && break
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Port note:** If port 3000 is taken, Next.js auto-assigns the next free port (3001, 3002, …). Check the log: `grep "Local:" /tmp/immich-folio-dev.log`
|
|
||||||
|
|
||||||
Run the driver (from repo root):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node .claude/skills/run-immich-folio/driver.mjs http://localhost:3001
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output:
|
|
||||||
|
|
||||||
```
|
|
||||||
[1/4] Homepage...
|
|
||||||
title: Ralfos Portfolio
|
|
||||||
→ /tmp/shots/01-homepage.png
|
|
||||||
[2/4] Subpage: /deutschland
|
|
||||||
→ /tmp/shots/02-subpage.png
|
|
||||||
[3/4] Album: /deutschland/an-der-ostsee
|
|
||||||
→ /tmp/shots/03-album-detail.png
|
|
||||||
[4/4] API health...
|
|
||||||
{"status":"ok","immich":"connected","uptime":...}
|
|
||||||
|
|
||||||
✓ All checks passed. Screenshots in /tmp/shots/
|
|
||||||
```
|
|
||||||
|
|
||||||
Screenshots land in `/tmp/shots/`. Pass a different URL as the first argument to target any running instance:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node .claude/skills/run-immich-folio/driver.mjs http://localhost:3002
|
|
||||||
```
|
|
||||||
|
|
||||||
Stop the dev server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kill $(cat /tmp/immich-folio.pid)
|
|
||||||
# or: pkill -f "next dev"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run (human path)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev # → http://localhost:3001 (or next free port). Ctrl-C to stop.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unit tests (Vitest, 66 tests across 7 suites — runs in ~250ms)
|
|
||||||
npm run test:unit
|
|
||||||
|
|
||||||
# Type-check
|
|
||||||
npx tsc --noEmit
|
|
||||||
|
|
||||||
# E2E (Playwright, starts its own dev server automatically)
|
|
||||||
npm run test:e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gotchas
|
|
||||||
|
|
||||||
- **Port auto-selection** — if 3000 is occupied, Next.js picks 3001 silently. The driver defaults to 3001; override with the first argument if needed. Check `grep "Local:" /tmp/immich-folio-dev.log` to find the actual port.
|
|
||||||
|
|
||||||
- **Use `page.goto()` not `.click()` for album navigation** — Next.js RSC streaming causes `.click()` + `waitForLoadState('networkidle')` to capture the previous page's DOM before the new route has streamed in. Navigate by href directly.
|
|
||||||
|
|
||||||
- **`eval()` console error in dev** — React emits `eval() is not supported in this environment` in dev mode due to the CSP middleware injecting a nonce. This is dev-only noise; production is unaffected. The driver filters it out.
|
|
||||||
|
|
||||||
- **`⚠ The "middleware" file convention is deprecated`** — Next.js 16 warns about `middleware.ts` being renamed to `proxy.ts`. Harmless; the app still works.
|
|
||||||
|
|
||||||
- **Immich must be reachable** — all pages are `dynamic = 'force-dynamic'` (no SSG). If `IMMICH_API_URL` is unreachable the health endpoint returns `{"immich":"error"}` and album pages return 404. The driver will fail at step 4.
|
|
||||||
|
|
||||||
- **`content/gallery.yaml` must have at least one album or subpage** — `getConfig()` throws at startup if the file is empty or has no albums. If you only have the example file, fill in real UUIDs first.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- **`Cannot find package '@playwright/test'`**: Run the driver from the repo root (`immich-folio/`), not from `/tmp` or another directory. `@playwright/test` is in `node_modules/` here.
|
|
||||||
|
|
||||||
- **Health returns 404 (returns HTML from a different app)**: Port 3000 is another app. Check the actual port: `grep "Local:" /tmp/immich-folio-dev.log` and pass it to the driver.
|
|
||||||
|
|
||||||
- **`EADDRINUSE` on restart**: The previous dev server is still running. `pkill -f "next dev"` before relaunching.
|
|
||||||
|
|
||||||
- **`gallery.yaml must define at least one album or subpage`**: Your `content/gallery.yaml` is empty or only has comments. Add at least one real Immich album UUID under `albums:` or `subpages:`.
|
|
||||||
|
|
||||||
- **Security warning about `AUTH_SECRET`**: Set `AUTH_SECRET` in `.env.local`. In development the app generates a temporary one per session (cookies break on restart). In production it hard-fails if unset.
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* Immich Folio smoke driver.
|
|
||||||
*
|
|
||||||
* Usage (run from repo root):
|
|
||||||
* node .claude/skills/run-immich-folio/driver.mjs [BASE_URL]
|
|
||||||
*
|
|
||||||
* BASE_URL defaults to http://localhost:3001.
|
|
||||||
* Screenshots land in /tmp/shots/.
|
|
||||||
*
|
|
||||||
* Exits 0 on success, 1 on any failure.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { chromium } from '@playwright/test';
|
|
||||||
import { mkdirSync } from 'fs';
|
|
||||||
|
|
||||||
const BASE = process.argv[2] ?? process.env.BASE_URL ?? 'http://localhost:3001';
|
|
||||||
const SHOTS = '/tmp/shots';
|
|
||||||
mkdirSync(SHOTS, { recursive: true });
|
|
||||||
|
|
||||||
let browser;
|
|
||||||
try {
|
|
||||||
browser = await chromium.launch({ args: ['--no-sandbox'] });
|
|
||||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
||||||
const page = await ctx.newPage();
|
|
||||||
|
|
||||||
const consoleErrors = [];
|
|
||||||
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
|
||||||
|
|
||||||
// ── 1. Homepage ──────────────────────────────────────────────
|
|
||||||
console.log('[1/4] Homepage...');
|
|
||||||
await page.goto(`${BASE}/`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const title = await page.title();
|
|
||||||
if (!title) throw new Error('Homepage has no title');
|
|
||||||
console.log(` title: ${title}`);
|
|
||||||
await page.screenshot({ path: `${SHOTS}/01-homepage.png` });
|
|
||||||
console.log(` → ${SHOTS}/01-homepage.png`);
|
|
||||||
|
|
||||||
// ── 2. First nav subpage ─────────────────────────────────────
|
|
||||||
// Grab the first subpage link from the header nav (skip "Home" and "About")
|
|
||||||
const firstSubpageHref = await page.$eval(
|
|
||||||
'.header__nav a:not([href="/"]):not([href="/about"]):not([href="/map"])',
|
|
||||||
el => el.getAttribute('href'),
|
|
||||||
);
|
|
||||||
if (!firstSubpageHref) throw new Error('No subpage nav link found');
|
|
||||||
console.log(`[2/4] Subpage: ${firstSubpageHref}`);
|
|
||||||
await page.goto(`${BASE}${firstSubpageHref}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.screenshot({ path: `${SHOTS}/02-subpage.png` });
|
|
||||||
console.log(` → ${SHOTS}/02-subpage.png`);
|
|
||||||
|
|
||||||
// ── 3. Album detail ──────────────────────────────────────────
|
|
||||||
// Album cards on a subpage grid use class "subpage-grid__item".
|
|
||||||
// Navigate by href (not click) — click + networkidle can race with RSC streaming.
|
|
||||||
const albumCard = page.locator('a.subpage-grid__item').first();
|
|
||||||
const albumHref = await albumCard.getAttribute('href').catch(() => null);
|
|
||||||
if (albumHref) {
|
|
||||||
console.log(`[3/4] Album: ${albumHref}`);
|
|
||||||
await page.goto(`${BASE}${albumHref}`);
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.screenshot({ path: `${SHOTS}/03-album-detail.png` });
|
|
||||||
console.log(` → ${SHOTS}/03-album-detail.png`);
|
|
||||||
} else {
|
|
||||||
// Subpage had only one album — we're already on the album detail
|
|
||||||
console.log('[3/4] Already on album detail (single-album subpage)');
|
|
||||||
await page.screenshot({ path: `${SHOTS}/03-album-detail.png` });
|
|
||||||
console.log(` → ${SHOTS}/03-album-detail.png`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 4. Health endpoint ───────────────────────────────────────
|
|
||||||
console.log('[4/4] API health...');
|
|
||||||
const health = await page.evaluate(async (base) => {
|
|
||||||
const r = await fetch(`${base}/api/health`);
|
|
||||||
return r.json();
|
|
||||||
}, BASE);
|
|
||||||
console.log(` ${JSON.stringify(health)}`);
|
|
||||||
if (health.status !== 'ok') throw new Error(`Health check failed: ${JSON.stringify(health)}`);
|
|
||||||
|
|
||||||
// ── Summary ───────────────────────────────────────────────────
|
|
||||||
const appErrors = consoleErrors.filter(e => !e.includes('eval() is not supported'));
|
|
||||||
if (appErrors.length > 0) {
|
|
||||||
console.warn(`\n⚠ Console errors (${appErrors.length}):`);
|
|
||||||
appErrors.forEach(e => console.warn(' ', e));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n✓ All checks passed. Screenshots in ${SHOTS}/`);
|
|
||||||
} finally {
|
|
||||||
await browser?.close();
|
|
||||||
}
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,6 +45,9 @@ Thumbs.db
|
|||||||
# ── Agent & Dev Tools ────────────────────────────────
|
# ── Agent & Dev Tools ────────────────────────────────
|
||||||
.agent/
|
.agent/
|
||||||
.Jules/
|
.Jules/
|
||||||
|
.jules/
|
||||||
|
.claude/
|
||||||
|
.planning/
|
||||||
|
|
||||||
# ── One-off Scripts (Root) ───────────────────────────
|
# ── One-off Scripts (Root) ───────────────────────────
|
||||||
/check_albums.js
|
/check_albums.js
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
## YYYY-MM-DD - [Memoize grid item re-rendering]
|
|
||||||
|
|
||||||
**Learning:** `useMemo` can significantly boost the performance of long list or grid components (like `PhotoGrid` component) that otherwise gets re-rendered on simple interactions like navigating the image lightbox. Running standard formatting `pnpm format` applies huge changes across entire directories, so it's generally best to format and clean up ONLY modified files to avoid huge diff pollution in code review.
|
|
||||||
**Action:** Be extremely cautious of running format/lint commands at the root codebase; specify only modified directories or files, or avoid formatting if it cascades globally. Focus on targeted memoization where state changes dictate expensive array mapping operations.
|
|
||||||
|
|
||||||
## 2024-05-28 - [Deduplicate concurrent API requests via Promise caching]
|
|
||||||
|
|
||||||
**Learning:** When using `Promise.all` to fetch dependent data in parallel (e.g., getting multiple subpages and standalone albums that internally call a shared `getAlbums` function), the internal cache might not be populated in time. This leads to redundant concurrent network requests to the upstream server.
|
|
||||||
**Action:** Implement Promise deduplication (request coalescing) by caching the pending Promise itself (e.g., in a `this.pendingPromise` class field) rather than just the final result. Return the pending Promise to subsequent callers until it resolves, ensuring only one network request is made.
|
|
||||||
## 2024-05-29 - [Buffer inheritance optimization]
|
|
||||||
|
|
||||||
**Learning:** `Buffer` inherits from `Uint8Array` in Node.js. Passing a `Buffer` into `new Uint8Array(...)` creates a completely new `ArrayBuffer` and copies all elements, which is an O(N) memory allocation and copy.
|
|
||||||
**Action:** When decoding base64 strings or similar byte streams in Node.js where a `Uint8Array` is expected, return `Buffer.from(data, 'base64')` directly instead of wrapping it.
|
|
||||||
## 2026-04-24 - [OOM prevention through promise chunking]
|
|
||||||
**Learning:** When fetching unbounded large collections (like photo albums and their assets), using concurrent `Promise.all` over the entire array can cause Node.js Out of Memory (OOM) crashes.
|
|
||||||
**Action:** Instead of `Promise.all(array.map(...))`, use a chunked data fetching approach (e.g., `Promise.all` within a `for` loop processing chunks of e.g. 10 items) to balance memory constraints with concurrent network speed.
|
|
||||||
|
|
||||||
## 2024-05-30 - [O(1) Memory Bucketing for Coordinate Aggregation]
|
|
||||||
**Learning:** When bucketing or grouping large datasets (e.g., thousands of map coordinates into geographical clusters), array allocations (`[].push()`) followed by `reduce` operations create an O(N) memory overhead per bucket and expensive post-processing loops.
|
|
||||||
**Action:** Replace memory-heavy array collections with running statistical accumulators (e.g., `latSum`, `lngSum`, `count`) directly on the bucket. This reduces bucket memory from O(N) to O(1).
|
|
||||||
## 2024-05-29 - Memoize repeated auth checks in Map API
|
|
||||||
**Learning:** When filtering map locations, the API performs redundant `isAuthenticated` checks for albums within the same subpage slug. Since each check involves cryptographic operations (HMAC calculations), doing this repeatedly in a nested filter loop severely impacts performance for galleries with many geotagged photos.
|
|
||||||
**Action:** Use a request-scoped `Map` to memoize the results of `isAuthenticated` calls within the route handler, preventing redundant cryptographic operations for the same subpage context.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
## 2024-05-18 - Unprotected Dynamic Image Generation Endpoints
|
|
||||||
|
|
||||||
**Vulnerability:** The `/api/og` route generates an `ImageResponse` dynamically using `next/og`. Because this operation is computationally expensive and memory-intensive, unauthenticated endpoints lacking rate limiting act as prominent Denial of Service (DoS) vectors. Attackers can flood the endpoint with varying parameters, forcing the server to continually spawn compute-heavy tasks until resources are exhausted and the instance crashes or latency spikes to unacceptable levels.
|
|
||||||
|
|
||||||
**Learning:** Next.js dynamic endpoints that do not hit external upstream APIs or databases (like image generation with `next/og`) are often overlooked for rate limiting. Rate limiting is just as critical for protecting local compute resources as it is for protecting downstream API quotas or databases. Any route performing on-the-fly heavy processing must implement throttling.
|
|
||||||
|
|
||||||
**Prevention:** Apply the shared `checkRateLimit` utility (from `@/lib/rate-limit`) to all computationally expensive endpoints (such as `next/og` usage), even if they do not explicitly query external services. Ensure `getConfig().rateLimitRpm` is passed as the threshold to maintain configurable global protection.
|
|
||||||
## 2024-05-18 - [Preventing Timing Attacks on Variable-Length Secrets]
|
|
||||||
**Vulnerability:** Comparing variable-length plaintext passwords using strict equality (`===`) or `crypto.timingSafeEqual` with unequal lengths, which leaks length information and enables timing attacks.
|
|
||||||
**Learning:** When comparing variable-length secrets (like plaintext fallback passwords), both values must be hashed to a fixed-length algorithm (like SHA-256) first to ensure identical input lengths for the secure comparison, preventing side-channel leaks.
|
|
||||||
**Prevention:** Always hash variable-length secrets to a fixed length before passing them to `crypto.timingSafeEqual`.
|
|
||||||
## 2024-05-18 - Rate Limit Store Flooding and Cache Eviction Vulnerability
|
|
||||||
**Vulnerability:** The rate limiter implemented an LRU/FIFO map to track API request limits, evicting the oldest tracked IP when maximum capacity was reached. An attacker could flood the store with randomly spoofed IP headers, continuously evicting legitimate users' IP addresses. This allowed the attacker to effectively bypass their own rate limit and subject the upstream server to DoS.
|
|
||||||
**Learning:** Evicting entries from a security control store (like a rate limit map) under pressure turns the limit into a revolving door if an attacker controls the keys (e.g., via IP spoofing headers).
|
|
||||||
**Prevention:** Rather than evicting old entries when an in-memory rate limit store is full, the correct security posture is to fail closed (or block new keys) until entries naturally expire, preserving tracking on existing keys.
|
|
||||||
## 2026-04-23 - Prevent Global DoS in Rate Limiter via FIFO Eviction
|
|
||||||
**Vulnerability:** The in-memory rate limiter implemented a hard ceiling (`MAX_STORE_ENTRIES`) and rejected all new IPs when reached, creating a trivial Global Denial of Service (DoS) vulnerability where an attacker could spam spoofed IPs to fill the bounded Map and permanently block all legitimate traffic.
|
|
||||||
**Learning:** "Failing securely" by denying unknown requests during capacity limits is incorrect for rate limiters. Bounded Maps must actively evict older entries rather than rejecting new ones, otherwise the security mechanism itself becomes the vector for DoS.
|
|
||||||
**Prevention:** In-memory rate limiters using bounded Maps must implement a FIFO (First-In-First-Out) or LRU (Least Recently Used) eviction policy (e.g., deleting `store.keys().next().value`) to make room for new IPs when maximum capacity is reached.
|
|
||||||
## 2024-05-18 - Incorrect Scrypt Hash Generation in Plaintext Fallback
|
|
||||||
**Vulnerability:** The plaintext password fallback logic incorrectly generated a recommended `scrypt` hash using the SHA256 hashed password rather than the raw `storedPassword`. This leads to generating an invalid `scrypt` hash that would fail authentication if the user updated their configuration with it. Furthermore, hashing variable-length secrets with SHA256 before `crypto.timingSafeEqual` prevents timing attacks, but using a non-keyed hash like SHA256 may leave it vulnerable. HMAC-SHA256 keyed with a secret provides better security.
|
|
||||||
**Learning:** When generating a secure hash recommendation for users migrating from plaintext, ensure the raw input is used, not an intermediate hash intended for timing-safe comparison.
|
|
||||||
**Prevention:** Generate the `scrypt` hash directly from the raw secret string (`storedPassword`). Always use HMAC keyed with a secret for hashing variable-length passwords prior to constant-time comparison.
|
|
||||||
## 2024-05-30 - Prevent Stored XSS via Content-Type Validation on Proxied Assets
|
|
||||||
**Vulnerability:** Asset proxy routes (`/api/image/*`, `/api/video/*`) directly served the upstream `Content-Type` header without validation. Since these API routes are explicitly excluded from the Next.js Content Security Policy (CSP) matcher, returning types like `image/svg+xml` or `text/html` would execute scripts directly in the user's browser, leading to Stored XSS.
|
|
||||||
**Learning:** Endpoints that serve user-controlled content and are not protected by global CSP headers require strict MIME-type validation. An attacker could upload an SVG containing malicious scripts and retrieve it via these proxy endpoints to bypass standard security controls.
|
|
||||||
**Prevention:** Always sanitize the `Content-Type` header on proxy endpoints. First convert it to lowercase, remap required generic types (like `application/octet-stream` to `image/jpeg`), and finally enforce a strict positive allowlist (`startsWith('image/')` and `!includes('svg')`) before falling back to `application/octet-stream`.
|
|
||||||
8
.planning/ui-reviews/.gitignore
vendored
8
.planning/ui-reviews/.gitignore
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Screenshot files — never commit binary assets
|
|
||||||
*.png
|
|
||||||
*.webp
|
|
||||||
*.jpg
|
|
||||||
*.jpeg
|
|
||||||
*.gif
|
|
||||||
*.bmp
|
|
||||||
*.tiff
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Phase Standalone — UI Review
|
|
||||||
|
|
||||||
**Audited:** 2024
|
|
||||||
**Baseline:** Abstract 6-Pillar Standards
|
|
||||||
**Screenshots:** Not captured (code-only audit)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pillar Scores
|
|
||||||
|
|
||||||
| Pillar | Score | Key Finding |
|
|
||||||
| -------------------- | ----- | ------------------------------------------------------------------------ |
|
|
||||||
| 1. Copywriting | 3/4 | Error states use generic 'Something went wrong'. |
|
|
||||||
| 2. Visuals | 3/4 | Standard component structures, but icon accessibility could be improved. |
|
|
||||||
| 3. Color | 2/4 | Heavy use of hardcoded colors in `DevToolbar.tsx`. |
|
|
||||||
| 4. Typography | 3/4 | Uses custom CSS styles rather than utility classes; mostly consistent. |
|
|
||||||
| 5. Spacing | 3/4 | Uses custom CSS styles; no major arbitrary inline spacing found. |
|
|
||||||
| 6. Experience Design | 3/4 | Good coverage of loading/error states in MapView and PasswordGate. |
|
|
||||||
|
|
||||||
**Overall: 17/24**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Top 3 Priority Fixes
|
|
||||||
|
|
||||||
1. **Refactor Colors in DevToolbar** — Hardcoded hex values break theme consistency and dark mode — Extract colors to CSS variables in the theme layer.
|
|
||||||
2. **Improve Error Copy in PasswordGate** — Generic "Something went wrong" frustrates users — Replace with specific, actionable messaging like "Incorrect password, please try again."
|
|
||||||
3. **Enhance MapView Loading State** — Simple boolean loading state causes layout shifts — Implement a skeleton loader or a dedicated map placeholder to improve perceived performance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detailed Findings
|
|
||||||
|
|
||||||
### Pillar 1: Copywriting (3/4)
|
|
||||||
|
|
||||||
- **PasswordGate.tsx**: Uses standard CTA ('Enter', 'Verifying...') but error state falls back to generic "Something went wrong" at line 40.
|
|
||||||
|
|
||||||
### Pillar 2: Visuals (3/4)
|
|
||||||
|
|
||||||
- Components use semantic HTML for the most part. SVG icons in `ThemeToggle`, `Lightbox`, and `Footer` lack robust `aria-label` alternatives for screen readers in some instances.
|
|
||||||
|
|
||||||
### Pillar 3: Color (2/4)
|
|
||||||
|
|
||||||
- **DevToolbar.tsx**: Contains over 20 distinct hardcoded hex values (clues: `#e60012`, `#e4e4e7`, `#166534`, `#ff6b35`). This violates the single-source-of-truth theme approach.
|
|
||||||
- **Lightbox / SetupScreen**: Minimal hardcoded values used for fallback (`#000`, `#ffffff`).
|
|
||||||
|
|
||||||
### Pillar 4: Typography (3/4)
|
|
||||||
|
|
||||||
- Exclusively uses CSS module classes rather than utility variables.
|
|
||||||
- Some inline font sizes exist (e.g., `fontSize: 10` in DevToolbar).
|
|
||||||
|
|
||||||
### Pillar 5: Spacing (3/4)
|
|
||||||
|
|
||||||
- Relies cleanly on CSS modules.
|
|
||||||
- Only a few inline spacing overrides (e.g., `marginTop: 6` in DevToolbar).
|
|
||||||
|
|
||||||
### Pillar 6: Experience Design (3/4)
|
|
||||||
|
|
||||||
- **PasswordGate.tsx**: Correctly disables the submit button and indicates processing state (`loading ? 'Verifying...' : 'Enter'`).
|
|
||||||
- **MapView.tsx**: Handles API mapping errors gracefully (`error && <p>{error}</p>`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Audited
|
|
||||||
|
|
||||||
- `app/api/auth/route.ts`
|
|
||||||
- `app/api/health/route.ts`
|
|
||||||
- `components/PasswordGate.tsx`
|
|
||||||
- `components/MapView.tsx`
|
|
||||||
- `components/Lightbox.tsx`
|
|
||||||
- `components/DevToolbar.tsx`
|
|
||||||
- `components/ThemeToggle.tsx`
|
|
||||||
- `components/Footer.tsx`
|
|
||||||
Reference in New Issue
Block a user