Commit Graph

60 Commits

Author SHA1 Message Date
Ryan Moon
2cc8f24535 Add Phase 7: grade history and session-plan item linking
- New tables: lesson_plan_item_grade_history (append-only), lesson_session_plan_item
- Grading an item updates current_grade_value and creates immutable history record
- Grading a not_started item auto-transitions it to in_progress
- Linking items to a session also auto-transitions not_started items
- Link operation is idempotent — re-linking same items produces no duplicates
- Endpoints: POST/GET /lesson-plan-items/:id/grades, GET /lesson-plan-items/:id/grade-history
- Endpoints: POST/GET /lesson-sessions/:id/plan-items
- 8 new integration tests
2026-03-30 10:33:21 -05:00
Ryan Moon
5cd2d05983 Add Phase 4b: instructor blocked dates, store closures, and substitute instructors
- New tables: instructor_blocked_date, store_closure (migration 0034)
- substitute_instructor_id column added to lesson_session
- Session generation skips blocked instructor dates and store closure periods
- Substitute assignment validates sub is not blocked and has no conflicting slot
- Routes: POST/GET/DELETE /instructors/:id/blocked-dates, POST/GET/DELETE /store-closures
- 15 new integration tests covering blocked dates, store closures, and sub validation
2026-03-30 10:29:13 -05:00
Ryan Moon
aae5a022a8 Add lessons Phase 6: lesson plans with curriculum tracking
Structured lesson plans with nested sections and items per enrollment.
Deep create in one request, one-active-per-enrollment constraint,
auto-set startedDate/masteredDate on status transitions, progress %
calculation (skipped items excluded). 8 new tests (84 total).
2026-03-30 09:40:41 -05:00
Ryan Moon
31f661ff4f Add lessons Phase 5: grading scales with nested levels
Custom grading scales with ordered levels (value, label, numeric score,
color). Supports one-default-per-store constraint, deep create with
nested levels, lookup endpoint for dropdowns, and search/pagination.
12 new tests (76 total lessons tests).
2026-03-30 09:36:48 -05:00
Ryan Moon
73360cd478 Add lessons Phase 4: lesson sessions with hybrid calendar generation
Individual lesson occurrences generated from schedule slot patterns.
Idempotent session generation with configurable rolling window.
Post-lesson notes workflow with auto-set notesCompletedAt. Status
tracking (scheduled/attended/missed/makeup/cancelled) and date/time
filtering. 13 new tests (64 total lessons tests).
2026-03-30 09:29:03 -05:00
Ryan Moon
93405af3b2 Add lessons Phase 3: enrollments with capacity and time conflict checks
Links members to schedule slots via enrollments. Enforces max_students
capacity on slots and prevents members from double-booking the same
day/time. Supports status transitions and filtering. 11 new tests
(51 total lessons tests).
2026-03-30 09:23:43 -05:00
Ryan Moon
f777ce5184 Add lessons Phase 2: schedule slots with conflict detection
Recurring weekly time slots linking instructors to lesson types.
Includes day/time overlap detection, instructor and day-of-week
filtering, and 17 new integration tests (40 total lessons tests).
2026-03-30 09:20:03 -05:00
Ryan Moon
5dbe837c08 Add lessons domain Phase 1: instructor and lesson type entities
Foundation tables for the lessons module with full CRUD, pagination,
search, and sorting. Includes migration, Drizzle schema, Zod validation,
services, routes, and 23 integration tests.
2026-03-30 09:17:32 -05:00
Ryan Moon
7176c1471e Add music store seed preset and repair data reset script
- music-store-seed.ts: 52 templates covering strings, brass, woodwinds,
  guitar, plus music-specific tickets and a school band batch
- reset-repairs.ts: clears all repair data for switching between presets
- New scripts: bun run db:seed-music, bun run db:seed-reset-repairs
2026-03-30 08:52:09 -05:00
Ryan Moon
701e15ea6d Remove DB-dependent unit tests, use api-tests for integration testing
All tests in __tests__/ were hitting the database directly. Integration
testing is handled by the api-tests/ suite instead.
2026-03-30 08:52:01 -05:00
Ryan Moon
9400828f62 Rename Forte to LunarFront, generalize for any small business
Rebrand from Forte (music-store-specific) to LunarFront (any small business):
- Package namespace @forte/* → @lunarfront/*
- Database forte/forte_test → lunarfront/lunarfront_test
- Docker containers, volumes, connection strings
- UI branding, localStorage keys, test emails
- All documentation and planning docs

Generalize music-specific terminology:
- instrumentDescription → itemDescription
- instrumentCount → itemCount
- instrumentType → itemCategory (on service templates)
- New migration 0027_generalize_terminology for column renames
- Seed data updated with generic examples
- RBAC descriptions updated
2026-03-30 08:51:54 -05:00
Ryan Moon
535446696c Include accessLevel in vault categories list response 2026-03-30 07:36:23 -05:00
Ryan Moon
1510133074 Seed default roles on startup alongside permissions 2026-03-30 07:16:25 -05:00
Ryan Moon
328b4a1f7b Fix dev seed for single-company schema, sync RBAC on startup
- Remove all company_id references from dev-seed.ts (removed in 0021)
- seedPermissions now syncs role-permission assignments for system roles
  when new permissions are added (e.g., vault.view assigned to admin)
- Fix enum migration: use text cast workaround for PostgreSQL's
  "unsafe use of new enum value" error on fresh DB creation
2026-03-30 07:10:20 -05:00
Ryan Moon
e346e072b8 Add module management system for enabling/disabling features
Stores can enable/disable feature modules from Settings. When disabled,
nav links are hidden and API routes return 403. Designed as the
foundation for future license-based gating (licensed + enabled flags).

Core modules (Accounts, Members, Users, Roles, Settings) are always on.

- module_config table with slug, name, description, licensed, enabled
- In-memory cache for fast per-request module checks
- requireModule middleware wraps route groups in main.ts
- Settings page Modules card with toggle switches
- Sidebar hides nav links for disabled modules
- Default modules seeded: inventory, pos, repairs, rentals, lessons,
  files, vault, email, reports
2026-03-30 06:52:27 -05:00
Ryan Moon
7246587955 Add vault secret manager backend with AES-256-GCM encryption
Secrets are encrypted at rest in the database. The derived encryption
key is held in memory only — on reboot, an authorized user must enter
the master password to unlock. Admins can also manually lock the vault.

- vault_config, vault_category, vault_category_permission, vault_entry tables
- AES-256-GCM encryption with PBKDF2-derived key + per-entry IV
- Master password initialize/unlock/lock/change lifecycle
- Category CRUD with role/user permission model (view/edit/admin)
- Entry CRUD with reveal endpoint (POST to avoid caching)
- Secret values never returned in list/detail responses
- vault.view/edit/admin RBAC permissions seeded
- 19 API integration tests covering full lifecycle
2026-03-30 06:11:33 -05:00
Ryan Moon
748ea59c80 Harden storage permissions and WebDAV security
Permission service:
- Add hasAccess() with explicit minLevel param, deprecate canAccess()
- Cycle protection + depth limit (50) on all parent traversal
- Pick highest access level across multiple roles (was using first match)
- isPublic only grants view on directly requested folder, not inherited
- Sanitize file extension from content-type
- Clean up orphaned traverse perms when removing permissions
- Add getPermissionById() for authz checks on permission deletion

Storage routes:
- All write ops require edit via hasAccess() — traverse can no longer
  create folders, upload files, rename, toggle isPublic, or delete
- Permission delete requires admin access on the folder
- Permission list requires admin access on the folder
- Folder children listing filtered by user access
- File search results filtered by user access (was returning all)
- Signed URL requires view (was using canAccess which allows traverse)

WebDAV:
- 100MB upload size limit (was unbounded — OOM risk)
- PROPFIND root filters folders by user access (was listing all)
- COPY uses hasAccess('view') not canAccess (traverse bypass)
- All writes use hasAccess('edit') consistently
- MKCOL at root requires files.delete permission
- Lock ownership enforced on UNLOCK (was allowing any user)
- Lock conflict check on LOCK (423 if locked by another user)
- Lock enforcement on PUT and DELETE (423 if locked by another)
- Max 100 locks per user, periodic expired lock cleanup
- Path traversal protection: reject .. and null bytes in segments
- Brute-force protection: 10 failed attempts per IP, 5min lockout
2026-03-29 18:21:19 -05:00
Ryan Moon
f998b16a3f Add traverse access level for folder navigation without file access
When a permission is set on a nested folder, traverse is automatically
granted on all ancestor folders so users can navigate to it. Traverse
only shows subfolders in listings — files are hidden. This prevents
orphaned permissions where a user has access to a nested folder but
can't reach it.

Hierarchy: traverse < view < edit < admin
2026-03-29 18:04:24 -05:00
Ryan Moon
51ca2ca683 Add folder permissions UI and WebDAV protocol support
Permissions UI:
- FolderPermissionsDialog component with public/private toggle,
  role/user permission management, and access level badges
- Integrated into file manager toolbar (visible for folder admins)
- Backend returns accessLevel in folder detail endpoint

WebDAV server:
- Full WebDAV protocol at /webdav/ with Basic Auth (existing credentials)
- PROPFIND, GET, PUT, DELETE, MKCOL, COPY, MOVE, LOCK/UNLOCK support
- Permission-checked against existing folder access model
- In-memory lock stubs for Windows client compatibility
- 22 API integration tests covering all operations

Also fixes canAccess to check folder creator (was missing).
2026-03-29 17:38:57 -05:00
Ryan Moon
f9bf1c9bff Add rectangular logo upload to settings, support SVG content type
Settings page now shows a rectangular upload area for the store logo
instead of circular avatar. Uses authenticated image fetching with
blob URL cleanup. Accepts SVG in addition to JPEG/PNG/WebP. SVG
added to file serve content type map. Simplified to single logo
image (used on PDFs, sidebar, and login).
2026-03-29 16:27:02 -05:00
Ryan Moon
8d75586f8b Add store logo and app icon uploads to settings page
AvatarUpload component now supports custom category and placeholder
icon props. Settings page shows two upload circles: Store Logo (for
PDFs/invoices, uses ImageIcon placeholder) and App Icon (for sidebar/
login, uses Building placeholder). Added 'company' to allowed file
entity types.
2026-03-29 16:14:08 -05:00
Ryan Moon
653fff6ce2 Add store settings page with location management
Company table gains address and logo_file_id columns. New store
settings API: GET/PATCH /store for company info, full CRUD for
/locations. Settings page shows store name, phone, email, address,
timezone with inline edit. Location cards with add/edit/delete.
Settings link in admin sidebar. Fixes leftover company_id on
location table and seed files.
2026-03-29 15:56:02 -05:00
Ryan Moon
0f6cc104d2 Add shared file storage with folder tree, permissions, and file manager UI
New document hub for centralized file storage — replaces scattered
drives and USB sticks for non-technical SMBs. Three new tables:
storage_folder (nested hierarchy), storage_folder_permission (role
and user-level access control), storage_file.

Backend: folder CRUD with nested paths, file upload/download via
signed URLs, permission checks (view/edit/admin with inheritance
from parent folders), public/private toggle, breadcrumb navigation,
file search.

Frontend: two-panel file manager — collapsible folder tree on left,
icon grid view on right. Folder icons by type, file size display,
upload button, context menu for download/delete. Breadcrumb nav.
Files sidebar link added.
2026-03-29 15:31:20 -05:00
Ryan Moon
d36c6f7135 Remove multi-tenant company_id scoping from entire codebase
Drop company_id column from all 22 domain tables via migration.
Remove companyId from JWT payload, auth plugins, all service method
signatures (~215 occurrences), all route handlers (~105 occurrences),
test runner, test suites, and frontend auth store/types.

The company table stays as store settings (name, timezone). Tenant
isolation in a SaaS deployment would be at the database level (one
DB per customer) not the application level.

All 107 API tests pass. Zero TSC errors across all packages.
2026-03-29 14:58:33 -05:00
Ryan Moon
55f8591cf1 Add dev seed data, batch status progress bar, pre-fill account from batch
Dev seed script creates 8 accounts, 8 members, 16 repair templates,
6 repair tickets in various statuses, and a school batch with 5
instruments. Run with bun run db:seed-dev.

Batch detail page now has a status progress bar matching the ticket
detail pattern. Add Repair from batch pre-fills the account and
contact name. New repair form reads accountId and contactName from
search params when linked from a batch.
2026-03-29 14:04:02 -05:00
Ryan Moon
72d0ff0a33 Fix security and quality issues from code review
Critical: Add company scoping to line item update/delete and note
delete via ownership verification through ticket join. Add companyId
validation to signed URL file serving. High: Paginate notes list
endpoint with search and sort support. Fix blob URL memory leaks in
AuthImage components with proper cleanup on unmount. Improve photo
upload error handling — count failures and show specific error count
instead of silently clearing form.
2026-03-29 12:16:17 -05:00
Ryan Moon
21ef7e7059 Expand repair tests to 43 cases, fix default status to new
Comprehensive test coverage for repairs: full status lifecycle (new →
picked_up), in_transit branch, pending_parts round-trip, delivered
alternate ending, reopen cancelled, validation errors, search by
instrument, filter by status and isBatch, notes CRUD with visibility
and status capture, service templates CRUD with soft-delete, signed
URL generation and access. Migration to set column default to new.
107 total API tests passing.
2026-03-29 11:56:06 -05:00
Ryan Moon
b0379052d6 Add 'new' status as default, in_transit becomes branch state
New tickets start as 'new' (just created, not yet examined). In Transit
is now a branch status off New for school pickups and shipped instruments.
Intake means the instrument has been physically received and documented.
Status progress bar, labels, filters, and default status all updated.
Removed debug logging from file upload endpoint.
2026-03-29 11:45:41 -05:00
Ryan Moon
ba94adb8d7 Fix DELETE sending empty JSON body, add repair_note to allowed file entity types
API client no longer sets Content-Type: application/json on requests
without a body (fixes DELETE 400 errors). Added repair_note to the
allowed entityTypes whitelist for file uploads so photos can be
attached to repair notes.
2026-03-29 11:23:21 -05:00
Ryan Moon
bf2091365d Add signed URLs for file access, fix unauthorized file viewing
New /files/signed-url/:id endpoint generates a 15-minute JWT-signed URL
for any file. New /files/s/* endpoint serves files using the token from
the query string without requiring auth headers. This allows files to
open in new browser tabs without authentication issues. Photos and
documents in repair tickets now use signed URLs when clicked.
2026-03-29 11:02:24 -05:00
Ryan Moon
7eac03f6c2 Add repair notes journal with running feed, visibility, and status tagging
New repair_note table for timestamped journal entries on tickets. Each
note captures author, content, visibility (internal or customer-facing),
and the ticket status at time of writing. Notes display as a running
feed on the ticket detail page with newest first. Internal notes have
a lock icon, customer-visible notes highlighted in blue. Supports add
and delete with appropriate permission gating.
2026-03-29 10:27:39 -05:00
Ryan Moon
01cff80f2b Add repair list filters, template management page, and backend filter support
Repairs list now has a filter panel with status (defaults to active only),
condition, batch/individual toggle, and date range filters for intake and
promised dates. Added Batch column to the repairs table. Backend list
endpoint accepts filter query params for status, condition, dates, and
batch membership. Template management page (admin only) with CRUD for
common repair services (rehair, string change, etc.) with instrument
type, size, and default pricing. Sidebar updated with Repair Templates
link gated on repairs.admin permission.
2026-03-29 10:13:38 -05:00
Ryan Moon
7d55fbe7ef Add repair ticket detail improvements and intake estimate builder
Status progress bar component with visual step indicator, in_transit
status for instruments being transported to shop. Ticket detail page
reworked with inline edit form, reopen for cancelled tickets, photos
grouped by repair phase (intake/in_progress/completed). Intake form
now supports building estimates with template picker and manual line
items that carry over to the ticket. Service template API client and
types added for template search in line item dialogs.
2026-03-29 09:56:28 -05:00
Ryan Moon
f17bbff02c Add repairs domain with tickets, line items, batches, and service templates
Full-stack implementation of instrument repair tracking: DB schema with
repair_ticket, repair_line_item, repair_batch, and repair_service_template
tables. Backend services and routes with pagination/search/sort. 20 API
tests covering CRUD, status workflow, line items, and batch operations.
Admin frontend with ticket list, detail with status progression, line item
management, batch list/detail with approval workflow, and new ticket form
with searchable account picker and intake photo uploads.
2026-03-29 09:12:40 -05:00
Ryan Moon
b9f78639e2 Add paginated users/roles, user status, frontend permissions, profile pictures, identifier file storage
- Users page: paginated, searchable, sortable with inline roles (no N+1)
- Roles page: paginated, searchable, sortable + /roles/all for dropdowns
- User is_active field with migration, PATCH toggle, auth check (disabled=401)
- Frontend permission checks: auth store loads permissions, sidebar/buttons conditional
- Profile pictures via file storage for users and members, avatar component
- Identifier images use file storage API instead of base64
- Fix TypeScript errors across admin UI
- 64 API tests passing (10 new)
2026-03-29 08:16:34 -05:00
Ryan Moon
92371ff228 Add RBAC tests, wiki docs, reset token to 1 hour
12 RBAC API tests: permission denial for no-role users, viewer read-only,
sales associate can create but not delete, technician scoped access,
instructor inventory denied, admin full access, permission inheritance
(admin implies edit+view), system role undeletable, custom role lifecycle.

Wiki articles for Users & Roles and Profile settings.
Reset password link expires in 1 hour instead of 24.
2026-03-28 18:11:32 -05:00
Ryan Moon
7dea20e818 Add user profile page, password change, reset links, auto-seed RBAC
Backend:
- POST /v1/auth/change-password (current user)
- POST /v1/auth/reset-password/:userId (admin generates 24h signed link)
- POST /v1/auth/reset-password (token-based reset, no auth required)
- GET/PATCH /v1/auth/me (profile read/update)
- Auto-seed system permissions on server startup

Frontend:
- Profile page with name edit, password change, theme/color settings
- Sidebar user link goes to profile page (replaces dropdown)
- Users page: "Reset Password Link" in kebab (copies to clipboard)
- Sign out button below profile link
2026-03-28 17:59:55 -05:00
Ryan Moon
58bf54a251 Add roles and users admin UI with role management API
Backend:
- GET /v1/users (list company users)
- GET/POST/PATCH/DELETE /v1/roles (role CRUD with permissions)
- GET/POST/DELETE /v1/users/:userId/roles (role assignment)
- GET /v1/me/permissions (current user's effective permissions)

Frontend:
- Roles list page with kebab menu (edit permissions, delete custom)
- Role detail page with grouped permission checkboxes and inheritance note
- New role page with auto-generated slug
- Users list page showing assigned roles per user
- Manage Roles dialog for adding/removing roles per user
- Sidebar: Admin section with Users, Roles, Help links
2026-03-28 17:16:53 -05:00
Ryan Moon
4a1fc608f0 Implement RBAC with permissions, roles, and route guards
- permission, role, role_permission, user_role_assignment tables
- 42 system permissions across 13 domains
- 6 default roles: Admin, Manager, Sales Associate, Technician, Instructor, Viewer
- Permission inheritance: admin implies edit implies view
- requirePermission() Fastify decorator on ALL routes
- System permissions and roles seeded per company
- Test helpers and API test runner seed RBAC data
- All 42 API tests pass with permissions enforced
2026-03-28 17:00:42 -05:00
Ryan Moon
e0493814f7 Add audit logging for sensitive operations
Structured logging with request ID correlation throughout:
- Auth: register, login success, login failure (warn level)
- Accounts: soft-delete
- Members: move between accounts
- Tax exemptions: approve (info), revoke (warn with reason)
- Files: upload, delete (already had logging)

All logs include userId, entityId, and contextual data for debugging.
4xx errors logged as warn, 5xx as error.
2026-03-28 16:23:20 -05:00
Ryan Moon
e44d461de1 Fix MEDIUM security issues, add logging and request timeout
- Password minimum increased from 8 to 12 characters
- CORS configurable via CORS_ORIGINS env var (comma-separated whitelist)
- Pagination empty string q param handled via preprocess
- Request timeout set to 30 seconds
- Log file output via LOG_FILE env var (stdout + file in production)
- Pino-pretty in development, JSON to stdout + file in production
2026-03-28 16:14:05 -05:00
Ryan Moon
e65175ef19 Fix security issues: path traversal, typed errors, file validation
- Fix path traversal in file serve endpoint (validate company prefix, block ..)
- Add typed error classes: ValidationError, NotFoundError, ForbiddenError,
  ConflictError, StorageError
- Global error handler catches AppError subclasses with correct status codes
- 4xx logged as warn, 5xx as error with request ID
- File upload validates entityType whitelist, UUID format, category pattern
- Remove fragile string-matching error handling from routes
- Services throw typed errors instead of plain Error
- Health endpoint documented as intentionally public
2026-03-28 16:03:45 -05:00
Ryan Moon
760e995ae3 Implement file storage layer with local provider, upload/download API, tests
- StorageProvider interface with LocalProvider (S3 placeholder)
- File table with entity_type/entity_id references, content type, path
- POST /v1/files (multipart upload), GET /v1/files (list by entity),
  GET /v1/files/:id (metadata), GET /v1/files/serve/* (content),
  DELETE /v1/files/:id
- member_identifier drops base64 columns, uses file_id FKs
- File validation: type whitelist, size limits, per-entity max
- Fastify storage plugin injects provider into app
- 6 API tests for upload, list, get, delete, validation
- Test runner kills stale port before starting backend
2026-03-28 15:29:06 -05:00
Ryan Moon
ce560cc568 Add comprehensive account and member API tests
12 new tests: search by email/phone, pagination params, sort order,
billing mode, scoped member list, isMinor from DOB, DOB override,
isMinor recalculation on update, 404 for missing member. Total: 36.
2026-03-28 12:57:57 -05:00
Ryan Moon
f47bfdbcdb Add standalone API test runner with accounts and members suites
Custom test framework that starts the backend, creates a test DB, runs
migrations, and hits real HTTP endpoints. Supports --suite and --tag
filtering. 24 tests covering account CRUD, member inheritance, state
normalization, move, search, and auto-generated numbers. Run with
bun run api-test.
2026-03-28 12:52:04 -05:00
Ryan Moon
b9e984cfa3 Add member address, state normalization, account inheritance, fix member form
- Address field on member table (jsonb, same format as account)
- Members inherit email, phone, address from account when not provided
- State normalization: "Texas" → "TX", "california" → "CA" via shared util
- Member form drops zodResolver to fix optional field validation flashing
- Account name auto-format: "First Last - Account"
- US state lookup with full name + code support
2026-03-28 12:31:02 -05:00
Ryan Moon
c7b460c0bf Add member identifiers table for ID documents (DL, passport, school ID)
member_identifier table with type, value, issuing authority, expiry,
front/back image storage (base64 in Postgres), primary flag. CRUD
endpoints under /members/:memberId/identifiers. Zod schemas with
constrained type enum.
2026-03-28 09:38:01 -05:00
Ryan Moon
727275af59 Add integration tests for members list, move, auto-numbers, primary member, isMinor flag 2026-03-28 09:18:32 -05:00
Ryan Moon
8ea3b8dffb Add auto-generated account numbers and member numbers
6-digit random numbers generated on create, unique per company. Member
number column added to member table. Both displayed in UI tables.
2026-03-28 09:15:27 -05:00
Ryan Moon
572af05a3f Add top-level members list, primary member on account, member move, combined create flows
- GET /v1/members with search across all members (includes account name)
- POST /members/:id/move with optional accountId (creates new account if omitted)
- primary_member_id on account table, auto-set when first member added
- isMinor flag on member create (manual override when no DOB provided)
- Account search now includes member names
- New account form includes primary contact fields, auto-generates name
- Members page in sidebar with global search
2026-03-28 09:08:06 -05:00