a5df05c3ce08678bba528e80e1adeb88dd8cce55
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a5df05c3ce |
Broaden gitignore to cover *.bak under seed/data/ (followup to 5f16855)
Commit 5f16855's message claimed it broadened .gitignore alongside the git rm --cached, but the .gitignore edit was left unstaged. This commit actually applies the change so future *.db.bak / *.bak files dropped into start9/*/seed/data/ stay out of version control. |
||
|
|
32b855f25b |
Untrack accidentally-committed seed/data/app.db.bak
The CSP-revert commit ba5a5d9 picked up start9/0.4/seed/data/app.db.bak because the gitignore only matched *.db, not *.db.bak. That file is the prior live snapshot (1.6MB of real workout history + bcrypt'd password hash) and must never be in version control. git rm --cached removes it from the index; the file stays on disk. .gitignore broadened to cover *.db.bak and *.bak under start9/*/seed/data/ so this can't recur. NOTE: the file is still in the previous commit. If this repo is ever pushed to a public remote, run `git filter-repo --path start9/0.4/seed/data/app.db.bak --invert-paths` (or BFG) to scrub history first. |
||
|
|
edeb1eb148 |
v1.0.0:2 — revert CSP nonces; restore inline-friendly CSP
v1.0.0:1 shipped a per-request nonce-based CSP via Next.js middleware. In production it produced a blank first paint: Next 14.2.x's bootstrap inline scripts weren't picking up the nonce reliably from the x-nonce request header, so the browser blocked them. This release reverts to the pre-experiment posture: - middleware.ts back to auth gating only (no nonce, no CSP). - next.config.js restores the static CSP with `'unsafe-inline'` allowed for script-src and style-src. Same headers (HSTS, Referrer-Policy, Permissions-Policy, frame-ancestors 'none', etc.) all stay. - New startos/versions/v1.0.0.2.ts with empty up/down migrations and a release note explaining the bug + revert. Promoted to `current` in the version graph; v1.0.0:1 moves to `other` so existing installs upgrade in place. No schema changes, no data migration. Existing v1.0.0:1 installs keep their /data. Re-attempt path documented in middleware.ts and next.config.js comments: future PR can revisit nonce CSP using Next's documented pattern verbatim (notably setting CSP on BOTH request headers and response headers — we only set it on response). |
||
|
|
990f5582b8 |
Typed Prisma queries, bcrypt native, CSP nonces, /api/me/import, more tests
Typed Prisma queries
- where: any in app/api/workouts/route.ts (GET + POST) and
lib/db/workouts.ts replaced with Prisma.WorkoutWhereInput +
Prisma.WorkoutCreateInput + Prisma.DateTimeFilter. Catches typos
at compile time and surfaces query shape directly in tooltips.
Workout import endpoint tests (tests/routes-import.test.ts)
- 7 tests covering /api/workouts/import/save: 401 unauthenticated,
empty workouts rejected, case-insensitive name matching against
existing exercises, new-exercise creation with isCustom=true and
type='other' default, explicit existingExerciseId honored over
name lookup, multiple workouts per call, sequential setNumber
per exercise per workout.
bcryptjs -> bcrypt (native)
- Roughly 10x faster than the pure-JS implementation under load —
login latency drops from ~250ms to ~25ms. Hash format is fully
cross-compatible with bcryptjs ($2a$ / $2b$ both verify), so
existing user passwords keep working without migration.
- Dockerfile builder stage adds python3 + make + g++ as a safety net
for native node-gyp compilation on alpine when prebuilt binaries
aren't available.
- Runner stage explicitly COPYs node_modules/bcrypt so the .node
binding is unambiguously present even if Next.js standalone
tracing somehow misses it.
- StartOS package's changeAdminCredentials.ts keeps bcryptjs (it's
bundled by ncc into a single JS file and runs only on the rare
admin action; native bcrypt would require shipping the .node
binding through ncc which it doesn't handle gracefully).
CSP nonces (middleware.ts + next.config.js)
- Per-request nonce generated in middleware. Forwarded to Next via
the x-nonce request header, which Next 13.4+ automatically stamps
onto its inline bootstrap scripts. CSP response header includes
`'nonce-${nonce}' 'strict-dynamic'`, dropping the previous
`'unsafe-inline'` from script-src.
- Static CSP removed from next.config.js (middleware-set headers
override static ones, so keeping both was redundant).
- Middleware matcher widened to all paths except static assets so
the CSP applies to every page response. Existing /main + /api
auth gating preserved.
- style-src keeps 'unsafe-inline' — Next/Tailwind still inject
critical inline <style>; tightening that requires hash-based
style-src or per-style nonce stamping (Next doesn't auto-do
either). Worth a follow-up if you want the cleanest possible CSP.
/api/me/import (mirror of /api/me/export)
- Accepts the same JSON shape /api/me/export emits (schema string
validated: only `proof-of-work-export@1` accepted today).
- mode: 'merge' (default) — adds imported rows; existing exercises
with matching names are NOT overwritten (the user's custom version
wins). All workout sets with a known exercise get rebound to the
user's actual exercise id via name lookup.
- mode: 'replace' — wipes the user's exercises/workouts/sets first,
then imports. Requires `confirm: "REPLACE"` in the body.
- Always scoped to the actor — never touches other users' data.
- Profile/admin flag/sessions/InstanceSettings deliberately not
imported (account identity stays put).
- 7 tests cover: 401, schema rejection, merge create+skip, replace
confirmation gate, replace wipes-then-imports, isolation across
users.
- ExportMyData component grew Import (merge) + Import (replace)
buttons with native browser confirm() before the destructive
replace.
Test suite now 81 tests across 9 files in ~2.6s.
|
||
|
|
54fa77f2eb |
Sessions UI, CSV parser tests, route tests, composite indexes, verify-db action
Per-user sessions UI (Settings -> Active sessions) - listMySessions returns the current user's still-valid sessions with last-8-char token suffix (UX hint) and an isCurrent flag (the authoritative "this device" marker). - revokeSession refuses if the target is the actor's current token — use Sign out for that flow. Per-row Revoke button on every other. - revokeAllOtherSessions = the previously-internal `deleteOtherSessions` helper exposed as a single button "Sign out other devices". - All gated to the actor's own userId (never lets a user touch another user's sessions). CSV parser refactor + tests - Extracted parseCSV, NAME_MAP, parseFloatMaybe, parseIntMaybe, getVariationNote, resolveExerciseName, parseDate from app/api/import/parse/route.ts to lib/csvParser.ts. Behavior byte-identical; route is now a thin wrapper that imports from the lib. - 18 tests covering: empty input, simple rows, lowercased headers, quoted-field commas, escaped double quotes, CRLF normalization, empty-line handling; numeric maybe-parsers; getVariationNote known patterns + null pass-through; ALL 27 NAME_MAP entries map to their canonical target; named CSV-shorthand examples; M/D/YYYY + ISO date parsing with noon-UTC anchoring (so US negative-offset zones still see the same calendar day). Workout + exercise CRUD route tests - New tests/routes-crud.test.ts: GET/POST /api/exercises, GET/POST /api/workouts. 401 on unauthenticated, per-user data isolation, query filtering, soft-delete exclusion, isCustom stamping, duplicate detection, type-driven inputFields defaults (cardio gets duration+calories), Zod validation rejection, set creation with weight/reps/rpe persisted, negative-reps rejected. - Helper builds NextRequest objects so the routes' nextUrl.searchParams access works. Composite indexes for hot query paths (schema.prisma + entrypoint) - Session: (userId, expiresAt) for "list my still-valid sessions" and per-user cleanup. - Workout: (userId, deletedAt, date) for the workout list query (filter by user + alive + date order). - SetLog: (workoutId, setNumber) for the always-ordered set fetch under each workout. - Existing single-column indexes kept; composites are additive. - Entrypoint runs CREATE INDEX IF NOT EXISTS so live snapshots pick up the new indexes on first boot after upgrade. verify-database StartOS action (start9/0.4/startos/actions/verifyDatabase.ts) - Read-only. Runs PRAGMA integrity_check + quick_check + row-count queries against /data/app.db, reports as a structured result. - allowedStatuses: only-running. Mounts the volume read-only. - Use after a StartOS Backup, after a host crash, or after a fresh sideload to confirm the data is sound before relying on it. Test suite now 67 tests across 7 files in ~2.4s. |
||
|
|
5de974edaf |
ESLint, server-action tests, export-my-data, enriched healthcheck, CHANGELOG
ESLint
- Pinned eslint@^8 + eslint-config-next@^14 to match Next 14's `next lint`.
ESLint 9's flat-config breaks `next lint` for legacy projects.
- .eslintrc.json extends next/core-web-vitals; ignores tests/, scripts/,
prisma/data/, .next/, node_modules.
- 7 pre-existing warnings surfaced (exhaustive-deps + alt-text + img tag
in user-written components). Left as warnings — pre-existing, not
breaking. CI runs lint; warnings don't fail the job.
Server action tests (tests/actions-admin.test.ts, tests/actions-auth.test.ts)
- Vitest setup file (tests/helpers/setup-actions.ts) sets DATABASE_URL
to a per-process temp SQLite DB and runs `prisma db push` BEFORE
lib/prisma instantiates its global PrismaClient. Tests then call the
real server actions against an isolated DB.
- vi.mock + vi.hoisted to mock @/lib/auth.getCurrentUser, next/headers
cookies+headers, next/navigation redirect, next/cache revalidatePath.
- Coverage:
- admin: setUserAdmin (Forbidden, promote, last-admin demote refused,
demote-with-other-admin allowed), deleteUser (last-admin guard,
self-delete refused, cascading delete to exercises + workouts),
adminResetPassword (hash-and-revoke, short-password rejected).
- auth flows: signupAction (closed by default, opens-and-creates,
mismatched confirm rejected, short pwd rejected, malformed email
rejected, no email-enumeration leak), changePasswordAction
(rotate-and-revoke-others, wrong current pwd rejected, no-op pwd
rejected), deleteMyAccountAction (phrase required, password required,
last-admin refused, success cascades + clears cookie + redirects).
- Total suite: 34 tests, ~2s.
Export my data (/api/me/export + Settings -> Export my data)
- Downloads a JSON dump of every workout/set/exercise/program tied to
the user. Excludes password hash and sessions. Filename includes
email + date. content-disposition: attachment, no-store cache.
- Exported shape matches the underlying tables 1:1 so a future "import
my data" flow can round-trip without ambiguity.
Enriched /api/health
- Now reports: database.connected, database.journalMode (and walEnabled
shortcut), users count, instanceSettings.signupsOpen, library.available
+ sizeBytes. Surfaces a `warnings` array if journal_mode != 'wal' but
doesn't fail the check (app still works without WAL — just unsafe for
online backups). Returns 503 only on hard DB failure.
CHANGELOG.md
- Single Unreleased section documenting everything that will ship as
v1.0.0:1 once the maintainer drops a fresh /data snapshot. Added /
Changed / Removed / Compat-notes sections.
|
||
|
|
65f4b7a7c7 |
Test suite (Vitest) + GitHub Actions CI
Test suite (proof-of-work/tests/)
- vitest 4 + @vitest/coverage-v8 added as devDeps. New scripts: test,
test:watch, test:coverage.
- vitest.config.ts: single-fork pool so DB-backed tests don't trample
each other on temp file paths. `@/` alias mirrors tsconfig.
- tests/helpers/db.ts: setupTestDb() spins up a fresh schema-only
SQLite file per test suite via `prisma db push --skip-generate`,
returns a scoped PrismaClient + cleanup that removes WAL/SHM
sidecars too.
- tests/rateLimit.test.ts: under-limit / over-limit / per-key
isolation / window-slides-and-allows-again. Plus tests for
clientIpFromHeaders header preference order.
- tests/auth-pure.test.ts: hashPassword roundtrips, salt-randomness
(same input, different hash), bcrypt format ($2 prefix).
- tests/library.test.ts: actually runs the runtime
ensureExerciseLibrary.cjs against a temp DB with two users — verifies
the full library lands for every user, idempotent across two runs,
and a user's own custom exercise with a colliding name is NOT
overwritten on subsequent ensure passes. This is the highest-stakes
test in the suite (covers the exact code path that runs on every
container boot).
12 tests, ~1.0s total.
GitHub Actions CI (.github/workflows/ci.yml)
- Two jobs running in parallel on push + PR to master/main:
- `app`: cd proof-of-work && npm ci && prisma validate && prisma
generate && tsc --noEmit && npm test
- `startos`: cd start9/0.4 && npm ci && npm run check (the
StartOS package's existing tsc --noEmit script)
- Both jobs use Node 20 with npm cache keyed off the package-lock.
|
||
|
|
d51400c2a9 |
Robustness: WAL mode, security headers, last-login, delete-my-account
SQLite WAL mode (start9/0.4/docker_entrypoint.sh) - Switches journal_mode to WAL on every boot. WAL persists in the DB header so this is effectively a one-shot but rerunning is harmless. - Crucial for the "background StartOS Backup while users are using the app" case: under the default rollback journal, a long backup can capture an inconsistent snapshot. WAL keeps readers and the writer from blocking each other. - synchronous=NORMAL paired with WAL: still crash-consistent at every checkpoint, ~10x faster than FULL. Security headers (proof-of-work/next.config.js) - Content-Security-Policy with frame-ancestors 'none', base-uri 'self', form-action 'self', object-src 'none'. Keeps 'unsafe-inline' for script/style because Next.js emits inline bootstrap; tightening to nonce-based CSP is a follow-up. - Strict-Transport-Security: max-age=31536000; includeSubDomains. - Referrer-Policy: strict-origin-when-cross-origin (don't leak workout IDs etc. to third-party sites). - Permissions-Policy: deny camera, mic, geolocation, USB, etc. across the board (none of those APIs are used today; explicit deny means vulnerability scanners have one less thing to flag). Last-login tracking - New User.lastLoginAt column. createSession stamps it inside the same transaction as the new Session row. - Compat ALTER in entrypoint adds the column to legacy snapshots. - Admin Users table now shows a relative-age cell (today / Nd ago / Nmo ago / Ny ago / "never" if the user hasn't signed in since the column was added). Hover reveals the exact ISO timestamp. Self-serve delete-my-account (Settings -> Danger Zone) - Requires both the user's current password AND typing the literal phrase "delete my account" (defense against a stolen-session attacker nuking the account in one click). - Refused for the last admin (instance can't be left with no admin — the user is told to promote someone first). - Cascades through Prisma onDelete: Cascade on every relation owned by User, so workouts, exercises, sessions, preferences all go in one shot. Session cookie cleared, redirected to /auth/login. |
||
|
|
a11639cc56 |
Self-serve password change, admin user management, login/signup rate limit
Per-user password change (Settings -> Change password)
- changePasswordAction verifies current password before rotating, blocks
same-as-current, requires 8+ chars and matching confirm.
- Always revokes every other session for the user via
deleteOtherSessions(userId, currentToken). If you're rotating because
you suspect compromise, the worst-case kicks the attacker off
immediately. UI surfaces how many sessions were revoked.
- ChangePasswordForm sits between SettingsForm and AdminInstanceSettings
on the existing settings page. Available to every user, no admin
privileges required.
Admin user management (/main/admin/users — admin only)
- New page lists every account: email, name, joined date, workout count,
role. Linked from the AdminInstanceSettings panel ("Manage users ->").
- Per-row actions: Promote/Demote (toggles isAdmin), Reset password
(inline 8+ char input), Delete (cascading delete via Prisma onDelete:
Cascade — workouts, exercises, sessions, preferences all go).
- Last-admin guard: setUserAdmin and deleteUser refuse if it would
leave 0 admins. Self-delete is blocked from the admin UI (preserves
the actor's session and forces them to use a "danger zone" flow they
set up explicitly elsewhere).
- adminResetPassword force-revokes ALL of the target user's sessions —
admin reset implies the old credential is no longer trusted.
- Server actions all do their own requireAdmin() gate (defense in depth
beyond the page-level redirect).
Rate limit on /auth/login + /auth/signup
- New lib/rateLimit.ts: tiny in-process sliding-window limiter, no deps.
Map<key, timestamps[]> with cutoff filtering on each call. Per Node
process — fine for the single-replica StartOS deploy shape.
- clientIpFromHeaders prefers x-forwarded-for (leftmost), falls back to
x-real-ip, then 'unknown' (acts as a global cap in dev).
- signup: 5 attempts per IP per 15min. Cuts off automated account
spraying without blocking legitimate household-member sign-ups.
- login: 10 attempts per IP per 15min. Slows credential stuffing while
giving typo-prone users headroom.
|
||
|
|
53d2bade5c |
Use crypto.randomBytes for session tokens; add deleteOtherSessions helper
Session tokens were derived from Math.random() + Date.now() — predictable enough that a determined attacker could brute-force or guess valid tokens for other users. Switch to crypto.randomBytes(32) (256 bits of CSPRNG output, hex-encoded), the standard for opaque bearer tokens. Also adds deleteOtherSessions(userId, keepToken) so the upcoming password-change flow can log a user out of every other device when they rotate their password. |
||
|
|
d9c4e6c4a0 |
Multi-user: self-serve sign-up gated by admin-toggleable flag
Schema - User.isAdmin: Boolean default false (Prisma) - New InstanceSettings singleton (id=1) holding signupsOpen flag Boot-time compat ALTERs (docker_entrypoint.sh) - Adds User.isAdmin column to legacy snapshots; auto-promotes the oldest user to admin if no admin exists yet, so workout-log -> proof-of-work cutover preserves admin functionality with no manual SQL. - Creates InstanceSettings table + singleton row (signupsOpen=0) for any snapshot that doesn't have it. App: sign-up flow - /auth/signup page: server component that reads InstanceSettings upfront. If sign-ups are closed it shows a closed-instance message and a back-to-sign-in link rather than a dead form. If open it renders SignupForm (client) which calls signupAction (server). - signupAction: re-checks the flag (defense in depth), validates email format / 8-char password / matching confirm, blocks duplicate-email enumeration with a generic error, creates the user with isAdmin=false, seeds default UserPreferences, ensures the curated exercise library for the new user (lib/library.ts upserts every entry), then issues a session cookie. - Login page now links to /auth/signup; old "Demo: admin@example.com / password" footer (which was wrong anyway) removed. App: admin in-app toggle - Settings page renders new AdminInstanceSettings component for admins only. Optimistic toggle posts to /api/admin/signups; error rollback on failure. - /api/admin/signups: GET returns current flag (any authed user, so the UI knows whether to show the sign-up CTA later); POST flips it (admin only). StartOS package action - toggle-signups: same setter as the in-app toggle, accessible from the StartOS UI without an admin login. Single boolean input. Asserts the read-back value matches what was written before reporting success. - changeAdminCredentials now keys the UPDATE on `WHERE isAdmin = 1 ORDER BY createdAt ASC LIMIT 1` (was: just ORDER BY createdAt) — correct under multi-user. Release notes / docs - v1.0.0:1 release notes expanded to call out multi-user as part of the cutover release (no separate version needed since this is the first proof-of-work release shipping to anyone). - Root README: short Multi-user section explaining both toggle paths and that new users get the curated library automatically. - README dev setup adds `npx prisma generate` step (required after schema changes for local dev). |
||
|
|
aa407b5f67 |
Rebrand to Proof of Work; multi-user 0.4 package with curated library sync
Repo cleanup - Add top-level .gitignore (was missing; node_modules, .next, *.s9pk, image.tar, seed/data/*.db, log files, etc.) and a root README. - Delete legacy start9/0.3.5/ package (StartOS 0.3.5 wrapper, no longer the deploy target). - Delete start9-example-packaging/ (template from another project). - Delete planning docs (START9_PACKAGING_LOG.md, VERSIONING.md, STARTOS_0.4_UPGRADE_PROMPT.md, ICON_FILES_INDEX.md, etc.) — info now lives in the deploy guide and code comments. - Drop the standalone Dockerfile, docker-compose.yml, ICON_*, and dev log/build artifacts from the app dir. - Drop the v0.1.0:18/19/20 version files (they belonged to the legacy workout-log package and don't apply to the new id). Rename + new package - Rename app dir workout-planner/ -> proof-of-work/. - Rename StartOS package id workout-log -> proof-of-work; the new id makes this a brand new StartOS service (clean cutover from the old one rather than in-place upgrade). - Reset version graph; v1.0.0:1 is the seeded cutover release. The Dockerfile bakes a one-time /data snapshot and docker_entrypoint.sh copies it into the new volume on truly-fresh first boot only (both /data/app.db missing AND /data/.seeded absent). - Move start9/0.4-migration/ -> start9/0.4/; the old start9/0.4/ stub is gone. Curated exercise library (multi-user-aware) - proof-of-work/prisma/exercises.seed.json is the canonical library shipped to every install (164 exercises today, dumped from the live snapshot). - proof-of-work/scripts/sync-library.cjs (npm run sync-library) refreshes the JSON from start9/0.4/seed/data/app.db after refresh_seed.sh. - proof-of-work/prisma/seed.ts now reads from the JSON instead of a hardcoded 52-exercise array; runs at Docker build time to seed the fallback DB and on first boot for fresh installs. - proof-of-work/prisma/ensureExerciseLibrary.cjs runs on every container boot (from docker_entrypoint.sh) and INSERT OR IGNOREs every library entry for every user, keyed on (userId, name). Library updates flow to existing installs on package upgrade; user-custom exercises (isCustom=true) and any colliding names are never overwritten; removed exercises stay on existing installs (additive-only). Deploy guide (start9/0.4/DEPLOY_040.md) - Rewritten end-to-end for the workout-log -> proof-of-work cutover: refresh_seed, sync-library, build, sideload, verify, rotate creds, stop the old service, then post-cutover cleanup release v1.0.0:2. |
||
|
|
1b64c45c52 | Initial commit for Start9 packaging |