Frederic CabassutandClaude Opus 5 a6f86ec1f0 fix(wu-002): refuse password resets that could never work
Second slice of issue #77. PATCH /api/admin/users/[id] accepted
{ newPassword } for any account and returned 200, but resetPassword()
writes only passwordHash while verifyPassword() matches only rows whose
authProvider is 'password'. So for an OAuth-only account (the
"OAuth-only" checkbox stores 'none') or anyone who has signed in via
OAuth (authProvider becomes the provider name), the endpoint wrote a
hash that could never be used to sign in, returned success, and fired a
"your password was reset" email. It lied.

The endpoint already guards both other self-affecting operations
server-side — "Cannot change your own role" and "Cannot delete your own
account" — but a newPassword targeting the requesting admin succeeded.

Two early returns in the newPassword branch, ordered length -> self ->
eligibility so the existing length 400 keeps precedence. Both are placed
before the resetPassword() call and before the email block: that
ordering is what guarantees no unusable hash is written and no
misleading email is sent, and it is a property of control flow rather
than of an assertion, because neither is observable from a test.
passwordHash never leaves the server (toUser omits it) and
isEmailConfigured() is false under e2e, so an assertion either way would
pass for a broken implementation too.

Eligibility comes from canResetPassword() rather than an inline
comparison, so the endpoint and the settings UI cannot drift; the 400
message comes from passwordResetBlockedReason() so the API reason and
the UI explanation are the same string.

DoD items verified:
- [x] 1. eligible user other than self still 200s, new password works
- [x] 2. authProvider 'none' returns 400 naming the reason
- [x] 3. positive control: an eligible user CAN log in after a reset
- [x] 4. targeting own id returns 400 naming the reason
- [x] 5. sub-8-char password still 400s for length, beating eligibility
- [x] 6. both guards precede the write and the email (diff-verified)
- [x] 7. role, revokeSessions and DELETE byte-unchanged (15+/0-)
- [x] 8. eligibility imported from wu-001, not re-implemented
- [x] 9. declarative Given/When/Then, distinct victim emails throughout

Verified independently by the orchestrator: pnpm check 0 errors /
14 pre-existing warnings; pnpm test:unit 306 passed / 26 files;
pnpm build && pnpm test:e2e admin-users.spec.ts 14 passed (7 auth-setup
+ 3 pre-existing + 4 new). The reviewer additionally reverted the
endpoint to its pre-change form and confirmed both guard scenarios fail
with "Expected: 400, Received: 200", so they are genuine regression
tests rather than assertions of behaviour that already held.

Reviewed-by: adversarial-review (PASS)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RbqJcFofZSbhbGwUwZP3wU
2026-08-18 12:31:12 +02:00
2026-08-05 08:57:08 +00:00
2026-08-05 08:57:08 +00:00
2026-03-20 22:58:39 +01:00

CI License: MIT Version Docker

Crumbs logo

Crumbs by Bretzel

A self-hostable, offline-first note-taking app that respects your privacy. Part of the Bretzel app universe.

Crumbs notes grid view

Install

docker compose up -d

Open http://localhost:3000 and create your admin account on first visit.

With SSO (optional)

# docker-compose.yml
services:
  crumbs:
    image: ghcr.io/bretzel-app/crumbs:latest
    ports:
      - "3000:3000"
    volumes:
      - crumbs-data:/data
    environment:
      - ORIGIN=https://notes.example.com
      - AUTH_OIDC_ISSUER=https://authentik.example.com/application/o/crumbs/
      - AUTH_OIDC_CLIENT_ID=your-client-id
      - AUTH_OIDC_CLIENT_SECRET=your-client-secret
      - AUTH_OIDC_DISPLAY_NAME=Authentik
    restart: unless-stopped

volumes:
  crumbs-data:

Google and GitHub OAuth are also supported. See docs/AUTH.md for all providers and setup guides.

With Email Notifications (optional)

Add SMTP settings to enable email notifications (share alerts, welcome emails, security alerts):

environment:
  - SMTP_HOST=smtp.example.com
  - SMTP_PORT=587
  - SMTP_USER=your-username
  - SMTP_PASS=your-password
  - SMTP_FROM=Crumbs <noreply@example.com>

Auto-enabled when SMTP_HOST is set. Users can opt out in Settings > Preferences. See docs/DEPLOYMENT.md for details.

Manual (Node.js)

git clone <repo-url> crumbs && cd crumbs
pnpm install
pnpm build
DATABASE_URL=./data/crumbs.db ORIGIN=http://localhost:3000 node build

Development

pnpm install
pnpm dev

Run make help to see all available commands, or use pnpm directly:

pnpm test          # Unit + E2E tests
pnpm check         # Type checking
pnpm build         # Production build

Features

  • Rich notes with Markdown, checklists, image attachments, and 12 color themes
  • Organize with #tags, pinning, archive, and trash
  • Full-text search across titles, content, and tags
  • Note version history — browse and restore previous versions
  • Share notes with other users on the same instance
  • Email notifications for shares, security alerts, and account events (optional SMTP)
  • PWA — installable, works offline via IndexedDB + LWW CRDT sync
  • MCP server — let AI assistants (Claude Code, etc.) manage your notes
  • Multi-user auth (Argon2) with optional OAuth/SSO (Google, GitHub, OIDC)
  • Docker deployment with a single command

See docs/FEATURES.md for detailed feature documentation.

Tech Stack

Layer Technology
Framework SvelteKit 2 (Svelte 5 runes)
Language TypeScript (strict)
UI Tailwind CSS 4
Database SQLite (better-sqlite3) + Drizzle ORM
Client DB IndexedDB (idb)
Sync LWW CRDTs
Auth Argon2 + session cookies
Testing Vitest + Playwright
Container Docker (multi-stage)
CI/CD GitHub Actions

Documentation

Document Description
Features Detailed feature list and behavior
Architecture Local-first sync design, tech rationale, DB schema
Auth Password auth, OAuth/SSO setup (Authentik, Keycloak, etc.)
Deployment Docker, Node.js, reverse proxy, backups, env vars
API REST API reference (auto-generated)
Contributing How to contribute
Security Vulnerability reporting policy
Changelog Release history

CI/CD

  • CI — lint, type check, unit tests, E2E tests, Docker build on every push/PR
  • Release — builds and pushes Docker image on v* tags

Configure registry via GitHub Secrets: REGISTRY_URL, REGISTRY_USER, REGISTRY_TOKEN.

License

MIT

Languages
TypeScript 68.5%
Svelte 25.1%
HTML 2.7%
CSS 2.7%
Shell 0.5%
Other 0.5%