Compare commits

...

55 commits
v1.0 ... main

Author SHA1 Message Date
friday-bot
041fb91182 Add unique visitor total and CSV export to document access
Surfaces distinct visitors for the selected range as a headline figure, and
lets the current view be exported for use in filings without retyping.

Unique visitors are counted across the range as a whole rather than summed
per document, so one person pulling five filings counts once. Where visitor
IPs were never recorded the CSV leaves the cell blank instead of writing 0,
which would otherwise read as "nobody downloaded this".
2026-07-30 13:49:10 -06:00
friday-bot
04bd71aaaa Add document access statistics to admin dashboard
Records every successful document download and surfaces the counts in the
admin area, so the docket owner can see which filings are being pulled and
how often.

- document_accesses table + DocumentAccess model
- DocumentController@download records each successful download with IP and
  user agent
- documents:import-access-log backfills history from an archived nginx log
- Dashboard gains a 60-day downloads card and a most-requested list
- New Document Access page with a date-range filter and per-day trend

Client IPs were only captured from 2026-07-30, when the reverse proxy chain
started forwarding X-Forwarded-For correctly. Rows imported from the old
nginx log therefore have no IP, and the UI labels those periods as download
counts rather than unique visitors instead of showing a misleading number.
2026-07-30 13:14:01 -06:00
13e7d23e0a Add Copy Link button to PDF viewer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 12:43:59 -06:00
8bc2a45e39 Update Gmail app password for email notifications 2026-01-11 09:53:00 -07:00
b85187c92b Fix PostgreSQL sequence issue after data import
- Added fixSequences() method to V1DataMigrationSeeder
- Automatically resets sequences for docket_entries, documents, and subscriptions
- Prevents 'duplicate key value violates unique constraint' errors
- Fixes issue where PDF uploads were failing due to out-of-sync sequences
2026-01-11 09:38:54 -07:00
ecb2322ac6 Update memory bank: DNS switchover complete, email notifications enabled, iPad fix deployed 2025-12-19 22:38:36 -07:00
81aaa22875 Enable email notifications to all active subscribers 2025-12-19 22:26:02 -07:00
d15a2fcb30 Add comprehensive DNS switchover instructions for v2.0 deployment 2025-12-19 22:10:21 -07:00
10b3f92cdd Change port from 8080 to 806 to match v1 configuration 2025-12-19 22:09:32 -07:00
8243ee1a92 Update .env for DNS switchover - change URLs to mad-lawsuit.org 2025-12-19 22:07:46 -07:00
159ce11eb8 Fix iPad PDF scroll issue - detect iOS and open in new tab 2025-12-19 17:44:14 -07:00
400678f4af Update memory bank with Phase 4.1 enhancements
- PDF upload fix (nginx permissions)
- Unsubscribe confirmation page
- Date format consistency (American MM/DD/YYYY)
- Removed pagination from docket entries admin page
- All features deployed and tested
2025-12-18 12:02:15 -07:00
b48b51ab6e Remove pagination from admin docket entries - show all on one page 2025-12-18 11:54:05 -07:00
1f3bf22553 Fix subscriber date format to American MM/DD/YYYY 2025-12-18 11:47:27 -07:00
e2930c6ce9 Fix CSRF token in unsubscribe page 2025-12-18 11:41:41 -07:00
474df2aa61 Add unsubscribe confirmation page
- Created Unsubscribe.vue with matching site styling
- Added showUnsubscribe method to SubscriptionController
- Updated routes: GET /unsubscribe/{token} shows confirmation page
- POST /api/unsubscribe/{token} processes unsubscribe
- Updated email template to use new unsubscribe route
- Prevents accidental unsubscribes with confirmation dialog
2025-12-18 11:33:47 -07:00
80b5adf3f4 Fix nginx parent directory permissions
- chmod 755 on /var/lib/nginx to allow www-data to traverse
- Parent directory was 750 (drwxr-x---) owned by nginx:nginx
- www-data couldn't access tmp subdirectory despite owning it
- This fixes the Permission denied error on PDF uploads
2025-12-18 10:52:23 -07:00
a81f26ce88 Fix nginx temp directory permissions in Dockerfile
- Create nginx temp directories during build
- Set proper ownership (www-data) and permissions (775)
- Fixes 'Permission denied' error on PDF uploads
2025-12-18 10:49:12 -07:00
b2b061b57a Add comprehensive debug logging for PDF upload
- Added detailed logging at each step of upload process
- Logs file info, validation, storage, and database creation
- Catches and logs full exception details with stack trace
- Will help diagnose 500 error on PDF upload
2025-12-18 10:40:37 -07:00
6f66e0987a Change email notifications to manual trigger
- Removed automatic email sending on docket entry creation
- Added sendNotification() method to DocketEntryController
- Added route for manual notification trigger
- Added 'Send Notification' button to Show page
- Email now only sends when admin explicitly clicks button
- Still in testing mode (chris@deafgain.org only)
2025-12-18 10:38:07 -07:00
2f9670caf9 Add PostgreSQL sequence fix script 2025-12-18 10:31:54 -07:00
9687b9b9be Add email notification system for new docket entries
- Created NewDocketEntryNotification mailable class
- Added professional HTML email template
- Updated DocketEntryController to send notifications on entry creation
- TESTING MODE: Only sends to chris@deafgain.org
- Configured SMTP with Gmail (system@deafgain.org)
- Added logging for email delivery tracking
- Production code commented out for safety during testing
2025-12-18 10:21:31 -07:00
2393f50b25 Update Memory Bank with bug fixes and configuration changes 2025-12-18 10:01:22 -07:00
4bfe443633 Increase file upload limit to 500MB for large court documents 2025-12-18 10:00:04 -07:00
94fe847c22 Update README with comprehensive project documentation 2025-12-18 09:55:19 -07:00
0187c2d822 Fix subscribers page - use items() instead of through() for pagination 2025-12-18 09:46:06 -07:00
9ad058339a Add mobile detection - open PDFs in new tab on screens < 768px 2025-12-18 08:56:36 -07:00
7d792fe635 Simplify PDF viewer to match v1.0 - remove wrapper div for mobile scrolling 2025-12-18 08:48:53 -07:00
0273caca03 Fix mobile PDF viewer scrolling - wrap iframe in scrollable container 2025-12-18 08:36:42 -07:00
73b0e7ae9e Fix mobile/tablet PDF viewer - enable viewing in all orientations with scrolling 2025-12-18 08:32:59 -07:00
7b409eba81 Match v1 background colors exactly (#6E6362 and rgba(57,64,83,0.95)) 2025-12-17 22:06:47 -07:00
d2d60f6e8b Add TrustProxies middleware to fix HTTPS detection behind Caddy 2025-12-17 22:00:12 -07:00
7258b35dda Fix AdminSeeder to use correct bcrypt hash format 2025-12-17 21:53:00 -07:00
047f81b73f Add ASSET_URL to fix mixed content issues 2025-12-17 21:50:13 -07:00
1b63d00560 Remove password mutator that was causing double-hashing 2025-12-17 21:38:46 -07:00
539df42dc5 Remove test route, set APP_DEBUG=false, fix storage volume mount to preserve permissions 2025-12-17 21:27:48 -07:00
a3c1ffd3b3 Add simple test route to debug 500 errors 2025-12-17 21:21:50 -07:00
f2183392e2 Change APP_ENV back to production (keep DEBUG=true) 2025-12-17 21:19:59 -07:00
ed43ef5622 Enable debug mode: Change APP_ENV to local for better error visibility 2025-12-17 21:14:16 -07:00
d36aa2a47d Change SESSION_DOMAIN to null for IP-based access 2025-12-17 21:07:59 -07:00
d104bf5471 Fix AdminSeeder to insert password hash directly without model mutator 2025-12-17 21:02:56 -07:00
f203bb03eb Fix DatabaseSeeder to call specific seeders instead of using factories 2025-12-17 20:55:57 -07:00
5d9997d3a6 Update AdminSeeder to use Eliza's credentials from v1.0 2025-12-17 20:52:04 -07:00
9772e4cb83 Fix .dockerignore to include .env file in Docker build 2025-12-17 20:45:39 -07:00
9492a3b08f Add .env file to repository for deployment 2025-12-17 20:38:07 -07:00
d7517442d6 Fix PHP-FPM command in supervisord
- Change php-fpm8.3 to php-fpm (Alpine uses generic command)
- Fixes 502 Bad Gateway error
2025-12-17 20:16:31 -07:00
55a5165d20 Fix V1DataMigrationSeeder for PostgreSQL
- Remove PRAGMA statements (SQLite-specific)
- PostgreSQL handles foreign keys automatically
2025-12-17 20:04:47 -07:00
1b8679965e Fix Dockerfile: Create supervisor log directory
- Add mkdir -p /var/log/supervisor before starting supervisor
- Fixes container restart loop
2025-12-17 19:55:52 -07:00
790fe8a48a Fix TypeScript errors in Home.vue
- Add type assertions for event.target in mouseover/mouseout handlers
- Fixes vue-tsc compilation errors during Docker build
2025-12-17 19:50:28 -07:00
7583712730 Fix Dockerfile: Use npm install instead of npm ci
- package-lock.json not in repo (.gitignore)
- npm ci requires existing lock file
- npm install will generate it during build
2025-12-17 19:45:08 -07:00
90550d34b7 Fix Dockerfile: Remove composer.lock dependency
- composer.lock is in .gitignore
- Let composer generate it during build
- Allows Docker build to succeed without local dependencies
2025-12-17 19:41:50 -07:00
b7125cf2b7 Phase 4: Production deployment configuration
- Add Docker configuration (Dockerfile, docker-compose.yml)
- Add Nginx and Supervisor configuration
- Add deployment documentation (DEPLOYMENT.md)
- Complete Phase 3 data migration (63 entries, 63 docs, 28 subs)
- Add responsive PDF viewer component
- Fix date format to American (MM/DD/YYYY)
- Update typography to match v1.0
- Add admin dashboard with full CRUD operations
- Configure PostgreSQL with secure password
- Connect to caddy_network for reverse proxy
- Ready for production deployment
2025-12-17 19:12:32 -07:00
844dcf3b73 Phase 2 partial: Admin authentication foundation
Authentication System Complete (50% of Phase 2):
- Created Admin AuthController with login/logout
- Created AdminAuth middleware for session-based auth
- Registered middleware in bootstrap/app.php
- Configured all admin routes with protection
- Created 4 admin controller files (Dashboard, DocketEntry, Document, Subscriber)

Session-based authentication:
- Stores admin_id and admin_username in session
- Middleware checks for admin_id presence
- Separate from Laravel Breeze User auth
- Uses AdminUser model with auto-hashing passwords

Routes configured:
- Public: GET/POST /admin/login
- Protected: /admin/dashboard, /admin/docket-entries (CRUD), /admin/documents, /admin/subscribers

Remaining work (50%):
- Implement controller logic (7 methods for DocketEntry, etc.)
- Create 9 Vue admin pages (Login, Dashboard, CRUD forms)
- Create AdminSeeder for default admin user
- Configure file storage for PDF uploads
- Test all admin features

See cline_docs/phase2_progress.md for detailed next steps
2025-12-17 15:58:32 -07:00
06a620406c Phase 1: Email subscription and document download features
- Created SubscriptionController with subscribe/unsubscribe endpoints
- Added subscription API routes (POST /api/subscribe, GET /api/unsubscribe/{token})
- Updated Vue Home component with working subscription form
- Created DocumentController with download endpoint
- Added document download route (GET /api/documents/{id}/download)
- Updated Vue component to link PDF buttons to download route
- Created DocketSeeder with 5 sample docket entries and documents
- Seeded database with test data for development

Features working:
- Email subscription with validation and duplicate checking
- Subscription reactivation for previously unsubscribed emails
- Document download functionality (ready for actual PDFs)
- Sample data for testing UI

Next: Admin dashboard for CRUD operations
2025-12-17 15:51:47 -07:00
80657cc209 v2.0: Laravel + Inertia + Vue foundation complete
- Replaced Next.js + Express + React stack with Laravel + Inertia + Vue
- Created database migrations for docket_entries, documents, subscriptions, admin_users
- Built Eloquent models with relationships (DocketEntry, Document, Subscription, AdminUser)
- Implemented HomeController with Inertia.js integration
- Created Vue home page component matching v1.0 design exactly
- Installed Laravel Breeze for authentication scaffolding
- Configured Vite 7 for Vue 3 + TypeScript compilation
- Updated .gitignore for Laravel project structure
- Tested locally - website rendering correctly with empty database
- All v1.0 Next.js/Express files removed, replaced with Laravel structure

Technology Stack:
- Backend: Laravel 12.43.1, PHP 8.3.28, Eloquent ORM
- Frontend: Vue 3, TypeScript, Inertia.js, Tailwind CSS 3.x
- Build: Vite 7.x, Composer 2.9.2
- Database: SQLite (dev), PostgreSQL (production)

Next steps: Email subscription API, document downloads, admin dashboard
2025-12-17 15:44:26 -07:00
4930 changed files with 14148 additions and 408892 deletions

68
.dockerignore Normal file
View file

@ -0,0 +1,68 @@
# Git
.git
.gitignore
.gitattributes
# Environment files
.env.backup
.env.example
# Node modules (will be installed in container)
node_modules/
npm-debug.log
yarn-error.log
# Composer vendor (will be installed in container)
vendor/
# Build artifacts
/public/build
/public/hot
/public/storage
# Storage (will be mounted as volume)
/storage/*.key
/storage/pail
/bootstrap/cache/*
# Database
/database/*.sqlite
/database/*.sqlite-journal
*.db
*.db-shm
*.db-wal
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Testing
/tests/
phpunit.xml
.phpunit.result.cache
# Documentation
README.md
PHASE3_COMPLETION_INSTRUCTIONS.md
DATA_COMPARISON_REPORT.md
cline_docs/
# Scripts
parse_sql_to_seeder.py
parse_sql_to_seeder_v2.py
update_seeder.py
update_seeder_v2.py
check_v1_data.js
seeder_data.txt
seeder_data_full.txt
# Temporary files
/tmp/
*.log

View file

@ -1,15 +1,18 @@
root = true root = true
[*] [*]
indent_style = tab
end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true end_of_line = lf
insert_final_newline = true indent_size = 4
[{package.json,*.yml}]
indent_style = space indent_style = space
indent_size = 2 insert_final_newline = true
trim_trailing_whitespace = true
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

71
.env
View file

@ -1,10 +1,67 @@
# Production Environment Configuration APP_NAME="MAD Lawsuit v2.0"
APP_ENV=production
APP_KEY=base64:dUzoSQvI3yjuaq3Sj6XMMpHw/1v/zPFRzwO6WzeADaQ=
APP_DEBUG=false
APP_TIMEZONE=UTC
APP_URL=https://mad-lawsuit.org
# Database Configuration APP_LOCALE=en
DB_PASSWORD=h3LvCItcZKcyRdsmeH1X+nVams7RAf89 APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
# JWT Configuration APP_MAINTENANCE_DRIVER=file
JWT_SECRET=ut02ymAdoHdrWAPgPsX25PMuhgqO54/Tow5JvgRdWeM= # APP_MAINTENANCE_STORE=database
# Email Configuration (Keep existing working values) PHP_CLI_SERVER_WORKERS=4
GOOGLE_APP_PASSWORD=ojvlysraxwjriwzy
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=mad_lawsuit_v2
DB_USERNAME=mad_user
DB_PASSWORD=mK9#vL2$pQ7@wN4&xR8!zT6^yU3*bH5
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=system@deafgain.org
MAIL_PASSWORD=idjcxxwzvloorfuk
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="system@deafgain.org"
MAIL_FROM_NAME="MAD Lawsuit Court Docket"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
ASSET_URL=https://mad-lawsuit.org

65
.env.example Normal file
View file

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

View file

@ -1,16 +0,0 @@
# Production Environment Configuration
# Copy this file and set your actual production values
# Database Configuration
DB_PASSWORD=your_secure_database_password_here
# JWT Configuration (Generate a secure random string)
JWT_SECRET=your_secure_jwt_secret_here_minimum_32_characters
# Email Configuration (Keep existing working values)
GOOGLE_APP_PASSWORD=ojvlysraxwjriwzy
# Optional: Override other settings if needed
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# GOOGLE_EMAIL=system@deafgain.org

11
.gitattributes vendored Normal file
View file

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

89
.gitignore vendored
View file

@ -1,47 +1,68 @@
# macOS # Laravel & PHP
*.log
.DS_Store .DS_Store
.DS_Store? .env.backup
._* .phpactor.json
.Spotlight-V100 .phpunit.result.cache
.Trashes /.fleet
ehthumbs.db /.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/vendor
Homestead.json
Homestead.yaml
Thumbs.db Thumbs.db
# Node modules # Node & NPM
node_modules/ /node_modules
npm-debug.log* /public/build
yarn-debug.log* /public/hot
yarn-error.log*
# Environment variables # Storage & Uploads
.env.local /public/storage
.env.development.local /storage/*.key
.env.test.local /storage/pail
.env.production.local /storage/app/public/*
!/storage/app/public/.gitkeep
# Logs # Database
logs /database/*.sqlite
*.log /database/*.sqlite-journal
*.db
*.db-shm
*.db-wal
# Runtime data # Build artifacts
pids /public/hot
*.pid /public/storage
*.seed /storage/*.key
*.pid.lock /bootstrap/cache/*
!/bootstrap/cache/.gitkeep
# Coverage directory used by tools like istanbul
coverage/
# Build outputs
dist/
build/
.next/
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
*.swo *.swo
# OS generated files
*~ *~
# OS
.DS_Store
Thumbs.db
# Composer
/vendor/
composer.lock
# NPM/Yarn
node_modules/
npm-debug.log
yarn-error.log
package-lock.json
pnpm-lock.yaml
# TypeScript
*.tsbuildinfo

View file

@ -1,143 +0,0 @@
# Caddyfile for Eliza Kragh v. MAD Court Docket Website
# This file should be placed in the Caddy configuration directory on the remote server
# Replace with your actual domain
docket.example.com {
# Enable automatic HTTPS
tls {
# Use Let's Encrypt for SSL certificates
# email admin@example.com
}
# Security headers
header {
# Security headers for legal document website
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
# Remove server information
-Server
-X-Powered-By
}
# Rate limiting for API endpoints
rate_limit {
zone api {
key {remote_host}
events 100
window 1m
}
zone auth {
key {remote_host}
events 10
window 1m
}
}
# API routes with rate limiting
@api path /api/*
rate_limit @api api
@auth path /api/auth/*
rate_limit @auth auth
# Reverse proxy to frontend container
reverse_proxy frontend:3000 {
# Health check
health_uri /
health_interval 30s
health_timeout 10s
# Load balancing (if multiple instances)
lb_policy round_robin
# Headers for proper forwarding
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
}
# Logging for legal compliance
log {
output file /var/log/caddy/docket-access.log {
roll_size 100mb
roll_keep 10
roll_keep_for 720h
}
format json
level INFO
}
# Error handling
handle_errors {
@5xx expression {http.error.status_code} >= 500
respond @5xx "Service temporarily unavailable" 503
@4xx expression {http.error.status_code} >= 400 && {http.error.status_code} < 500
respond @4xx "Page not found" 404
}
}
# Admin subdomain (optional - for enhanced security)
admin.docket.example.com {
tls {
# email admin@example.com
}
# Additional security for admin interface
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
}
# Stricter rate limiting for admin
rate_limit {
zone admin {
key {remote_host}
events 20
window 1m
}
}
rate_limit admin
# Only allow admin routes
@admin path /admin*
handle @admin {
reverse_proxy frontend:3000 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
# Redirect non-admin requests to main site
redir https://docket.example.com{uri} permanent
log {
output file /var/log/caddy/admin-access.log {
roll_size 50mb
roll_keep 20
roll_keep_for 2160h # 90 days
}
format json
level INFO
}
}
# HTTP to HTTPS redirect
http://docket.example.com {
redir https://docket.example.com{uri} permanent
}
http://admin.docket.example.com {
redir https://admin.docket.example.com{uri} permanent
}

141
Caddyfile.remote Normal file
View file

@ -0,0 +1,141 @@
{
admin 0.0.0.0:2019
email chris@sigd.net
}
# Internal Services
gitea.sigd.net {
reverse_proxy 10.4.0.156:3000
}
statistics.deafgain.org {
reverse_proxy matomo:80
}
archives.sigd.net {
reverse_proxy 10.4.0.159:8001
}
upload.sigd.net {
handle /api/* {
uri strip_prefix /api
reverse_proxy 10.4.0.159:3334
}
handle {
reverse_proxy 10.4.0.159:4174
}
}
preview.sigd.net {
reverse_proxy 10.4.0.83:3000
}
droppy.sigd.net {
reverse_proxy 10.4.0.159:6969
}
requests.sigd.net {
reverse_proxy 10.4.0.157:5055
}
documents.deafgain.org {
reverse_proxy 10.4.0.156:8001
}
documents.sigd.net {
reverse_proxy 10.4.0.159:8001
}
doculens.deafgain.org {
reverse_proxy 10.4.0.156:5015
}
firewall.sigd.net {
reverse_proxy 10.4.0.1:9080
}
# Public Websites (on this VM - 10.4.0.205)
# Deaf Summit Website
deafsummit.org {
# Route API requests to backend
handle /api/* {
reverse_proxy localhost:808
}
# Route everything else to frontend
handle {
reverse_proxy localhost:807
}
}
www.deafsummit.org {
redir https://deafsummit.org{uri} permanent
}
# DeafMissoula.org
www.deafmissoula.org {
redir https://deafmissoula.org{uri} permanent
}
deafmissoula.org {
reverse_proxy 172.18.0.7:801
}
# ChrisHaulmark.com
www.chrishaulmark.com {
redir https://chrishaulmark.com{uri} permanent
}
chrishaulmark.com {
reverse_proxy chrishaulmark-website-web-1:803
}
# DeafGain.org
www.deafgain.org {
redir https://deafgain.org{uri} permanent
}
deafgain.org {
reverse_proxy 172.18.0.4:804
}
# FinLion domains
finlion.com {
reverse_proxy 172.18.0.8:805
}
www.finlion.com {
redir https://finlion.com{uri} permanent
}
finlion.net, www.finlion.net {
redir https://finlion.com{uri} permanent
}
finlion.org, www.finlion.org {
redir https://finlion.com{uri} permanent
}
# MAD Lawsuit
www.mad-lawsuit.org {
redir https://mad-lawsuit.org{uri} permanent
}
mad-lawsuit.org {
reverse_proxy 172.18.0.6:806
}
files.mad-lawsuit.org {
reverse_proxy 172.18.0.5:901
}
# MAD Lawsuit V2 (Preview)
v2.mad-lawsuit.org {
reverse_proxy localhost:8080
}
www.v2.mad-lawsuit.org {
redir https://v2.mad-lawsuit.org{uri} permanent
}

198
DATA_COMPARISON_REPORT.md Normal file
View file

@ -0,0 +1,198 @@
# v1.0 vs v2.0 Data Migration Comparison Report
**Date**: December 17, 2025
**Migration Status**: Partial Success (51% of entries, 100% of documents and subscriptions)
## Summary
| Data Type | v1.0 Production | v2.0 Imported | Status | Percentage |
|-----------|----------------|---------------|--------|------------|
| **Docket Entries** | 63 | 32 | ⚠️ Partial | 51% |
| **Documents** | 63 | 63 | ✅ Complete | 100% |
| **Subscriptions** | 28 | 28 | ✅ Complete | 100% |
## Detailed Analysis
### ✅ Successfully Imported (100%)
1. **Documents (63/63)**
- All PDF files correctly linked to entries
- File paths updated from `/app/uploads/` to `documents/`
- All UUIDs preserved
- File sizes and metadata intact
2. **Subscriptions (28/28)**
- All email addresses imported
- Active/inactive status preserved
- Unsubscribe tokens maintained
- Timestamps accurate
### ⚠️ Partially Imported (51%)
**Docket Entries (32/63)**
**What Was Imported:**
- 32 entries successfully migrated
- All imported entries verified with correct:
- IDs (preserved from v1.0)
- Dates
- Titles
- Summaries
- Notes
- Timestamps
**Sample Imported Entries:**
- ID 72: Order Granting Motion to Extend Time (2025-10-29)
- ID 71: Motion to Extend Time for Filing Reply Briefs (2025-10-28)
- ID 67: Order Granting Motion to Strike (2025-10-23)
- ID 65: Certificate of Service (2025-10-21)
- ID 54: Defendant's Motion for Summary Judgment (2025-10-06)
**What Was NOT Imported:**
- 31 entries missing (49%)
- These entries have multi-line INSERT statements in the SQL dump
- Long summaries caused line breaks in the SQL file
- Python parser (`parse_sql_to_seeder.py`) only captured single-line INSERTs
## Root Cause
The Python parser script uses a simple line-by-line regex approach:
```python
# Current implementation
if line.startswith('INSERT INTO public.docket_entries'):
# Parse single line only
```
**Problem**: Many entries have summaries with 500+ characters that span multiple lines in the SQL dump, causing the parser to miss them.
**Example of missed entry:**
```sql
INSERT INTO public.docket_entries (id, date, title, summary, ...) VALUES (
1,
'2025-05-07',
'Some Title',
'This is a very long summary that spans
multiple lines in the SQL dump file
and therefore was not captured by the parser',
...
);
```
## Impact Assessment
### Critical Data
- ✅ **All documents preserved** - Every PDF file is accounted for
- ✅ **All subscribers preserved** - Complete email list maintained
- ⚠️ **51% of entries** - Significant but not complete
### Functional Impact
- **Public Website**: Will display 32 entries instead of 63
- **Admin Dashboard**: Shows accurate count (32 entries, 63 docs, 28 subs)
- **Document Downloads**: All 63 PDFs accessible and working
- **Email Subscriptions**: All 28 subscribers can receive notifications
### Data Integrity
- ✅ No data corruption
- ✅ All imported data is accurate
- ✅ Relationships intact (entries → documents)
- ✅ IDs preserved (no conflicts)
## Recommendations
### Option 1: Import Remaining 31 Entries (Recommended)
**Fix the parser to handle multi-line INSERTs:**
```python
# Improved parser approach
import re
def parse_multiline_inserts(sql_file):
with open(sql_file, 'r') as f:
content = f.read()
# Match complete INSERT statements (including multi-line)
pattern = r"INSERT INTO public\.docket_entries.*?VALUES\s*\((.*?)\);"
matches = re.findall(pattern, content, re.DOTALL)
return matches
```
**Steps:**
1. Update `parse_sql_to_seeder.py` with multi-line support
2. Re-run parser: `python3 parse_sql_to_seeder.py /tmp/v1-data.sql > seeder_data_full.txt`
3. Update seeder with new arrays
4. Clear database: `php artisan migrate:fresh --seed --seeder=AdminSeeder`
5. Re-run migration: `php artisan db:seed --class=V1DataMigrationSeeder`
6. Verify: Should show 63 entries
**Time Estimate**: 30-60 minutes
### Option 2: Direct SQL Import
**Import the SQL dump directly into PostgreSQL:**
```bash
# On production server
psql -U postgres -d mad_lawsuit_v2 < /tmp/v1-data.sql
```
**Pros:**
- Guaranteed 100% data import
- Faster than fixing parser
**Cons:**
- Requires PostgreSQL setup first
- May need schema adjustments
- Bypasses Laravel migrations
### Option 3: Accept Partial Import
**Keep current 32 entries and add remaining manually:**
**Pros:**
- No additional work needed
- 51% coverage may be acceptable for testing
**Cons:**
- Missing 31 historical entries
- Incomplete migration
- Not suitable for production
## Current Status
### What's Working ✅
- v2.0 application fully functional
- Admin dashboard operational
- Document uploads/downloads working
- All 32 imported entries display correctly
- All 63 documents accessible
- All 28 subscribers active
### What's Missing ⚠️
- 31 docket entries from v1.0 production
- These entries exist in `/tmp/v1-data.sql` but weren't parsed
## Next Steps
**Recommended Action**: Implement Option 1 (Fix Parser)
1. Update `parse_sql_to_seeder.py` with multi-line INSERT support
2. Re-generate seeder arrays with all 63 entries
3. Clear and re-import data
4. Verify 100% migration success
**Alternative**: If time-constrained, proceed to Phase 4 with current 32 entries and fix later.
## Files Reference
- **SQL Dump**: `/tmp/v1-data.sql` (contains all 63 entries)
- **Parser Script**: `parse_sql_to_seeder.py` (needs multi-line support)
- **Generated Arrays**: `seeder_data.txt` (32 entries currently)
- **Seeder**: `database/seeders/V1DataMigrationSeeder.php` (ready for update)
- **PDF Files**: `storage/app/public/documents/` (all 69 files present)
## Conclusion
The data migration is **functionally successful** with 100% of documents and subscriptions imported. The 51% entry import rate is due to a parser limitation, not data loss. All missing entries are recoverable from the SQL dump file. The system is operational and ready for Phase 4, with the option to complete the remaining 31 entries at any time.

420
DEPLOYMENT.md Normal file
View file

@ -0,0 +1,420 @@
# MAD Lawsuit v2.0 - Production Deployment Guide
## Overview
This guide covers deploying the Laravel + Vue + Inertia application to production using Docker.
**Production URL**: https://v2.mad-lawsuit.org
**Server**: 10.4.0.205 (public-websites)
**Directory**: `~/websites/v2.mad-lawsuit.org/`
---
## Prerequisites
### On Local Machine
- Git repository with all changes committed
- `.env.production` file ready (NOT in Git)
- All PDF files in `storage/app/public/documents/`
### On Production Server
- Docker and Docker Compose installed
- PostgreSQL container running (or use existing v1.0 database)
- Caddy reverse proxy configured
- SSH access: `ssh chaulmark@10.4.0.205`
---
## Deployment Steps
### 1. Prepare Local Repository
```bash
# Ensure all changes are committed
cd /Users/chaulmark/Eliza-MAD-docket-website
git status
# Commit any pending changes
git add .
git commit -m "Phase 4: Production deployment configuration"
# Tag the release
git tag -a v2.0 -m "MAD Lawsuit v2.0 - Laravel + Vue + Inertia"
# Push to Gitea
git push origin main
git push origin v2.0
```
### 2. Transfer Sensitive Files via SCP
**IMPORTANT**: `.env.production` is NOT in Git for security.
```bash
# Copy .env.production to server
scp .env.production chaulmark@10.4.0.205:~/websites/v2.mad-lawsuit.org/.env
# Copy PDF files (if not already on server)
scp -r storage/app/public/documents/*.pdf chaulmark@10.4.0.205:~/websites/v2.mad-lawsuit.org/storage/app/public/documents/
```
### 3. SSH to Production Server
```bash
ssh chaulmark@10.4.0.205
```
### 4. Clone/Pull Repository
```bash
# If first deployment
cd ~/websites
git clone https://gitea.sigd.net/chaulmark/mad-lawsuit.git v2.mad-lawsuit.org
cd v2.mad-lawsuit.org
git checkout v2.0
# If updating existing deployment
cd ~/websites/v2.mad-lawsuit.org
git fetch --all
git checkout v2.0
git pull origin v2.0
```
### 5. Configure Environment
```bash
# Verify .env file exists (transferred via SCP)
ls -la .env
# Generate application key
docker compose run --rm app php artisan key:generate
# Update .env with generated key
nano .env
# Copy the APP_KEY value from output above
```
### 6. Build and Start Docker Containers
```bash
# Build the Docker image
docker compose build
# Start containers
docker compose up -d
# Check container status
docker compose ps
docker compose logs -f app
```
### 7. Run Database Migrations
```bash
# Run migrations
docker compose exec app php artisan migrate --force
# Seed admin user
docker compose exec app php artisan db:seed --class=AdminSeeder
# Import v1.0 data
docker compose exec app php artisan db:seed --class=V1DataMigrationSeeder
# Create storage symlink
docker compose exec app php artisan storage:link
```
### 8. Verify Application
```bash
# Check if app is running
curl http://localhost:8080
# Check database connection
docker compose exec app php artisan tinker
>>> DB::connection()->getPdo();
>>> \App\Models\DocketEntry::count();
>>> exit
```
---
## Caddy Configuration
### Add to Caddyfile
**File**: `/home/chaulmark/docker/caddy/config/Caddyfile`
```caddyfile
# MAD Lawsuit v2.0
v2.mad-lawsuit.org {
reverse_proxy 172.18.0.X:8080
encode gzip
log {
output file /var/log/caddy/v2-mad-lawsuit.log
}
}
```
**Note**: Replace `172.18.0.X` with the actual container IP on the caddy_network.
### Find Container IP
```bash
# Get container IP
docker inspect mad-lawsuit-v2 | grep IPAddress
# Or use docker network inspect
docker network inspect caddy_network | grep -A 5 mad-lawsuit-v2
```
### Reload Caddy
```bash
# Reload Caddy configuration
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
```
---
## Database Configuration
### Option 1: Use Existing v1.0 PostgreSQL
Update `.env`:
```env
DB_CONNECTION=pgsql
DB_HOST=mad-lawsuit-db # v1.0 container name
DB_PORT=5432
DB_DATABASE=docket_db
DB_USERNAME=docket_user
DB_PASSWORD=<v1.0_password>
```
### Option 2: Use New PostgreSQL Container
The `docker-compose.yml` includes a PostgreSQL container:
```env
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=mad_lawsuit_v2
DB_USERNAME=mad_user
DB_PASSWORD=<secure_password>
```
---
## Post-Deployment Verification
### 1. Test Public Website
```bash
# From local machine
curl https://v2.mad-lawsuit.org
```
Or visit in browser: https://v2.mad-lawsuit.org
**Check**:
- [ ] Home page loads
- [ ] Docket entries display
- [ ] PDF downloads work
- [ ] Email subscription form works
### 2. Test Admin Dashboard
Visit: https://v2.mad-lawsuit.org/admin/login
**Credentials**:
- Username: `admin`
- Password: `password` (change after first login)
**Check**:
- [ ] Login works
- [ ] Dashboard shows correct statistics
- [ ] Can view docket entries
- [ ] Can create new entry
- [ ] Can upload PDF
- [ ] Can view subscribers
### 3. Check Logs
```bash
# Application logs
docker compose logs -f app
# Nginx logs
docker compose exec app tail -f /var/log/nginx/access.log
docker compose exec app tail -f /var/log/nginx/error.log
# Caddy logs
docker exec caddy tail -f /var/log/caddy/v2-mad-lawsuit.log
```
---
## Troubleshooting
### Container Won't Start
```bash
# Check logs
docker compose logs app
# Check if port 8080 is available
netstat -tuln | grep 8080
# Rebuild container
docker compose down
docker compose build --no-cache
docker compose up -d
```
### Database Connection Failed
```bash
# Check PostgreSQL container
docker compose ps postgres
# Test connection
docker compose exec app php artisan tinker
>>> DB::connection()->getPdo();
```
### Permission Errors
```bash
# Fix storage permissions
docker compose exec app chown -R www-data:www-data /var/www/html/storage
docker compose exec app chmod -R 775 /var/www/html/storage
```
### PDF Files Not Found
```bash
# Check if files exist
docker compose exec app ls -la /var/www/html/storage/app/public/documents/
# Recreate storage symlink
docker compose exec app php artisan storage:link
```
### Caddy Not Routing
```bash
# Check Caddy logs
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
# Reload Caddy
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# Check container IP
docker inspect mad-lawsuit-v2 | grep IPAddress
```
---
## Maintenance Commands
### Update Application
```bash
# Pull latest code
cd ~/websites/v2.mad-lawsuit.org
git pull origin main
# Rebuild and restart
docker compose down
docker compose build
docker compose up -d
# Run migrations
docker compose exec app php artisan migrate --force
# Clear caches
docker compose exec app php artisan cache:clear
docker compose exec app php artisan config:clear
docker compose exec app php artisan view:clear
```
### Backup Database
```bash
# Backup PostgreSQL
docker compose exec postgres pg_dump -U mad_user mad_lawsuit_v2 > backup-$(date +%Y%m%d).sql
# Or use v1.0 database
docker exec mad-lawsuit-db pg_dump -U docket_user docket_db > backup-$(date +%Y%m%d).sql
```
### View Logs
```bash
# All logs
docker compose logs -f
# Specific service
docker compose logs -f app
docker compose logs -f postgres
# Last 100 lines
docker compose logs --tail=100 app
```
### Restart Services
```bash
# Restart all
docker compose restart
# Restart specific service
docker compose restart app
```
---
## Security Checklist
- [ ] `.env` file has secure `APP_KEY`
- [ ] Database password is strong and unique
- [ ] Admin password changed from default
- [ ] `APP_DEBUG=false` in production
- [ ] SSL certificates active (via Caddy)
- [ ] File permissions correct (775 for storage)
- [ ] `.env` file NOT in Git repository
- [ ] Firewall rules configured (if applicable)
---
## Rollback Plan
If v2.0 has issues, v1.0 remains operational:
1. **Keep v1.0 running** at https://mad-lawsuit.org
2. **Test v2.0** at https://v2.mad-lawsuit.org
3. **Switch DNS** only after thorough testing
4. **Rollback**: Simply revert Caddy configuration to point to v1.0
---
## Next Steps After Deployment
1. **Test thoroughly** on v2.mad-lawsuit.org
2. **Implement email notifications** (Phase 4.2)
3. **Monitor logs** for errors
4. **Update DNS** to switch from v1.0 to v2.0
5. **Decommission v1.0** after 48 hours of stable v2.0
---
## Support
**Repository**: https://gitea.sigd.net/chaulmark/mad-lawsuit
**Server**: 10.4.0.205 (public-websites)
**Contact**: chaulmark@sigd.net
---
*Last Updated: December 17, 2025*

220
DEPLOYMENT_EMAIL_TESTING.md Normal file
View file

@ -0,0 +1,220 @@
# Email Notification System - Deployment & Testing Guide
## Overview
Email notification system has been implemented for the MAD Lawsuit v2.0 website. When a new docket entry is created via the admin dashboard, an email notification is automatically sent to subscribers.
## Testing Mode Configuration
**IMPORTANT**: The system is currently in TESTING MODE and will ONLY send emails to `chris@deafgain.org`.
### Testing Mode Details
- **Test Recipient**: chris@deafgain.org
- **Purpose**: Verify email functionality without spamming all 28 production subscribers
- **Location**: `app/Http/Controllers/Admin/DocketEntryController.php` (line ~155)
### Production Code (Currently Commented Out)
```php
// Production code (commented out for testing):
// $subscribers = Subscription::where('is_active', true)->get();
// foreach ($subscribers as $subscriber) {
// Mail::to($subscriber->email)->send(
// new NewDocketEntryNotification($entry, $subscriber->unsubscribe_token)
// );
// }
```
## Deployment Steps
### 1. SSH to Production Server
```bash
ssh chaulmark@10.4.0.205
```
### 2. Navigate to Project Directory
```bash
cd ~/websites/v2.mad-lawsuit.org
```
### 3. Pull Latest Code
```bash
git pull origin main
```
### 4. Rebuild Docker Container
```bash
docker compose down
docker compose up -d --build
```
### 5. Verify Container Status
```bash
docker compose ps
docker compose logs -f app
```
### 6. Clear Laravel Caches
```bash
docker exec mad-lawsuit-v2 php artisan config:clear
docker exec mad-lawsuit-v2 php artisan cache:clear
docker exec mad-lawsuit-v2 php artisan view:clear
```
## Testing the Email System
### Test Procedure
1. **Login to Admin Dashboard**
- URL: https://v2.mad-lawsuit.org/admin/login
- Username: eliza
- Password: AXEoZWk2AMAD0naw
2. **Create a Test Docket Entry**
- Navigate to "Docket Entries" → "Create New Entry"
- Fill in the form:
- Date: Today's date
- Title: "TEST - Email Notification System"
- Summary: "This is a test entry to verify email notifications are working correctly."
- Notes: (optional)
- Click "Create Docket Entry"
3. **Verify Email Delivery**
- Check chris@deafgain.org inbox
- Look for email with subject: "New Court Filing: TEST - Email Notification System"
- Verify email formatting and content
4. **Check Logs**
```bash
docker exec mad-lawsuit-v2 tail -f storage/logs/laravel.log
```
- Look for log entries:
- "Sending email notification for new docket entry"
- "Email notification sent successfully"
### Expected Email Content
- **Subject**: New Court Filing: [Entry Title]
- **From**: MAD Lawsuit Court Docket <system@deafgain.org>
- **To**: chris@deafgain.org
- **Content**:
- Professional header with case name
- Filing date, title, and summary
- "View Full Docket" button linking to https://v2.mad-lawsuit.org
- Unsubscribe link (placeholder during testing)
## SMTP Configuration
### Gmail SMTP Settings (Already Configured in .env)
```env
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=system@deafgain.org
MAIL_PASSWORD=ojvlysraxwjriwzy
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="system@deafgain.org"
MAIL_FROM_NAME="MAD Lawsuit Court Docket"
```
### Gmail App Password
- **Email**: system@deafgain.org
- **App Password**: ojvlysraxwjriwzy (16-character Google App Password)
- **Note**: This is NOT the regular Gmail password
## Switching to Production Mode
### When Ready to Send to All Subscribers
1. **Edit DocketEntryController.php**
```bash
# On production server
cd ~/websites/v2.mad-lawsuit.org
nano app/Http/Controllers/Admin/DocketEntryController.php
```
2. **Update sendEmailNotifications Method** (around line 155)
- Comment out the testing code:
```php
// TESTING MODE (comment out when ready for production)
// $testEmail = 'chris@deafgain.org';
// $testToken = 'test-unsubscribe-token';
// Mail::to($testEmail)->send(new NewDocketEntryNotification($entry, $testToken));
```
- Uncomment the production code:
```php
// Production code:
$subscribers = Subscription::where('is_active', true)->get();
foreach ($subscribers as $subscriber) {
Mail::to($subscriber->email)->send(
new NewDocketEntryNotification($entry, $subscriber->unsubscribe_token)
);
}
```
3. **Commit and Deploy**
```bash
git add app/Http/Controllers/Admin/DocketEntryController.php
git commit -m "Switch email notifications to production mode - send to all subscribers"
git push origin main
# On production server
git pull origin main
docker compose down
docker compose up -d --build
docker exec mad-lawsuit-v2 php artisan config:clear
```
## Troubleshooting
### Email Not Received
1. **Check Laravel Logs**
```bash
docker exec mad-lawsuit-v2 tail -100 storage/logs/laravel.log
```
2. **Check Gmail SMTP Connection**
```bash
docker exec mad-lawsuit-v2 php artisan tinker
# In tinker:
Mail::raw('Test email', function($msg) {
$msg->to('chris@deafgain.org')->subject('Test');
});
```
3. **Verify .env Configuration**
```bash
docker exec mad-lawsuit-v2 php artisan config:show mail
```
### Common Issues
- **Authentication Failed**: Verify Gmail App Password is correct
- **Connection Timeout**: Check firewall rules for port 587
- **SSL/TLS Errors**: Ensure MAIL_ENCRYPTION=tls (not ssl)
## Files Modified
### New Files Created
1. `app/Mail/NewDocketEntryNotification.php` - Mailable class
2. `resources/views/emails/new-docket-entry.blade.php` - HTML email template
### Modified Files
1. `app/Http/Controllers/Admin/DocketEntryController.php` - Added email sending logic
2. `.env` - Updated SMTP configuration
## Current Status
- ✅ Email notification system implemented
- ✅ Professional HTML email template created
- ✅ SMTP configured with Gmail
- ✅ Testing mode active (chris@deafgain.org only)
- ✅ Code committed and pushed to repository
- ⏳ Awaiting deployment to production server
- ⏳ Awaiting email functionality testing
## Next Steps
1. Deploy to production server (10.4.0.205)
2. Test email delivery to chris@deafgain.org
3. Verify email formatting and content
4. Once confirmed working, switch to production mode
5. Update documentation with test results
---
**Last Updated**: December 18, 2025 at 10:21 AM MST
**Status**: Ready for deployment and testing
**Git Commit**: 9687b9b9

View file

@ -1,510 +0,0 @@
# Eliza Kragh v. MAD Court Docket Website - Design Specification
## Project Overview
A public-facing website to display court documents and filings for the ongoing lawsuit: **Eliza Kragh v. Montana Association of the Deaf**. The system provides public transparency for court proceedings while allowing administrative management of documents and user notifications.
## Technical Architecture
### System Components
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Next.js 15.4 │ │ Express.js 6 │ │ PostgreSQL 17.5 │
│ Frontend │◄──►│ Backend API │◄──►│ Database │
│ (Port 3000) │ │ (Port 3001) │ │ (Port 5432) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ┌─────────────────┐ │
│ │ Redis │ │
└──────────────►│ Cache/Session │◄─────────────┘
│ (Port 6379) │
└─────────────────┘
┌─────────────────┐
│ File System │
│ /uploads/ │
└─────────────────┘
```
**Technology Stack (June 2025 Latest Versions):**
- **Frontend**: Next.js 15.4.0 with React 19, TypeScript 6.0, Tailwind CSS
- **Backend**: Node.js 22 LTS, Express.js 6.0, TypeScript 6.0, Prisma 5.0
- **Database**: PostgreSQL 17.5 with Redis caching
- **Infrastructure**: Docker Engine 28.1.1, Nginx reverse proxy
## Database Schema
### Tables Structure
```sql
-- Docket Entries (main court filings)
CREATE TABLE docket_entries (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
document_type VARCHAR(100) NOT NULL,
summary TEXT NOT NULL,
court_notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Documents (PDFs associated with entries)
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
docket_entry_id INTEGER REFERENCES docket_entries(id) ON DELETE CASCADE,
original_filename VARCHAR(255) NOT NULL,
stored_filename VARCHAR(255) NOT NULL UNIQUE,
file_path VARCHAR(500) NOT NULL,
title VARCHAR(255) NOT NULL,
summary TEXT,
notes TEXT,
is_primary BOOLEAN DEFAULT FALSE,
file_size INTEGER NOT NULL,
display_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Email Subscriptions
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
unsubscribe_token VARCHAR(255) UNIQUE
);
-- Admin Users
CREATE TABLE admin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
);
-- Indexes for performance
CREATE INDEX idx_docket_entries_date ON docket_entries(date DESC);
CREATE INDEX idx_documents_docket_entry ON documents(docket_entry_id);
CREATE INDEX idx_documents_display_order ON documents(docket_entry_id, display_order);
CREATE INDEX idx_subscriptions_active ON subscriptions(is_active);
```
## API Endpoints
### Public Endpoints
```
GET /api/docket-entries
Response: Array of docket entries with associated documents
{
"entries": [
{
"id": 1,
"date": "2024-12-15",
"document_type": "Motion to Dismiss",
"summary": "Defendant's motion...",
"court_notes": "Motion filed within deadline...",
"documents": [
{
"id": 1,
"title": "Main Document",
"summary": "Primary motion document",
"notes": "Filed by defendant's counsel",
"is_primary": true,
"display_order": 0
}
]
}
]
}
GET /api/documents/:id/download
Response: PDF file stream
Headers: Content-Type: application/pdf
POST /api/subscribe
Body: { "email": "user@example.com" }
Response: { "success": true, "message": "Subscribed successfully" }
GET /api/unsubscribe/:token
Response: Unsubscribe confirmation page
```
### Admin Endpoints (Protected)
```
POST /api/auth/login
Body: { "username": "admin", "password": "password" }
Response: { "token": "jwt-token", "expires": "timestamp" }
POST /api/docket-entries
Body: {
"date": "2024-12-15",
"document_type": "Motion",
"summary": "Entry summary",
"court_notes": "Court notes",
"documents": [
{
"title": "Main Document",
"summary": "Document summary",
"notes": "Document notes",
"is_primary": true,
"file": "base64-encoded-pdf"
}
]
}
PUT /api/docket-entries/:id
Body: Updated entry data
Response: Updated entry object
DELETE /api/docket-entries/:id
Response: { "success": true }
POST /api/documents
Body: FormData with file and metadata
Response: Created document object
PUT /api/documents/:id
Body: Updated document metadata
Response: Updated document object
PUT /api/documents/:id/replace
Body: FormData with new file
Response: Updated document object
DELETE /api/documents/:id
Response: { "success": true }
```
## File Management System
### UUID-Based Naming Scheme
```
Format: {entry-id}_{document-type}_{uuid}.pdf
Examples:
001_main_a7b3c9d2-4e5f-6789-abcd-ef0123456789.pdf
001_exhibit_b8c4d0e3-5f60-789a-bcde-f01234567890.pdf
002_main_c9d5e1f4-6071-890b-cdef-012345678901.pdf
```
### Directory Structure
```
uploads/
├── 2024/
│ ├── 12/
│ │ ├── 001_main_a7b3c9d2-4e5f-6789-abcd-ef0123456789.pdf
│ │ ├── 001_exhibit_b8c4d0e3-5f60-789a-bcde-f01234567890.pdf
│ │ └── 002_main_c9d5e1f4-6071-890b-cdef-012345678901.pdf
│ └── 11/
│ └── [previous month files]
└── 2025/
└── [future files]
```
### File Upload Process
1. Validate file type (PDF only) and size (max 10MB)
2. Generate UUID for filename
3. Create year/month directory structure
4. Save file with UUID-based name
5. Store metadata in database
6. Return success response with document ID
## Frontend Components
### Public Interface Components
```
components/
├── Layout/
│ ├── Header.tsx # Site header with title and subscribe button
│ ├── Footer.tsx # Site footer
│ └── Layout.tsx # Main layout wrapper
├── Docket/
│ ├── DocketList.tsx # List of all docket entries
│ ├── DocketEntry.tsx # Individual entry display
│ ├── DocumentList.tsx # Documents within an entry
│ └── DocumentViewer.tsx # PDF viewing modal
├── Subscription/
│ ├── SubscribeForm.tsx # Email subscription form
│ └── SubscribeModal.tsx # Subscription confirmation modal
└── Common/
├── LoadingSpinner.tsx # Loading states
├── ErrorMessage.tsx # Error display
└── Button.tsx # Reusable button component
```
### Admin Interface Components
```
components/admin/
├── Auth/
│ └── LoginForm.tsx # Admin login form
├── Dashboard/
│ ├── Dashboard.tsx # Main admin dashboard
│ ├── EntryList.tsx # Manage existing entries
│ └── Stats.tsx # Basic statistics
├── Entry/
│ ├── CreateEntry.tsx # New docket entry form
│ ├── EditEntry.tsx # Edit existing entry
│ └── DeleteEntry.tsx # Delete confirmation
├── Document/
│ ├── DocumentUpload.tsx # Multi-file upload component
│ ├── DocumentEdit.tsx # Edit document metadata
│ └── DocumentReplace.tsx # Replace PDF file
└── Layout/
├── AdminLayout.tsx # Admin layout wrapper
└── AdminNav.tsx # Admin navigation
```
## User Interface Layouts
### Public Docket Page
```
┌─────────────────────────────────────────────────────────────┐
│ ELIZA KRAGH v. MAD DOCKET │
│ [Subscribe] │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Case Information │
│ ───────────────── │
│ Case: Eliza Kragh v. Montana Association of the Deaf │
│ Status: Ongoing │
│ Last Updated: December 15, 2024 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Docket Entries │
│ ───────────────── │
│ [Date] | [Document Type] | [Summary] | [View] │
│ ──────────────────────────────────────────────────────── │
│ 12/15/24 | Motion to Dismiss | Defendant's motion... |[+] │
│ └─ 📄 Main Document │
│ └─ 📄 Exhibit A - Financial Records │
│ └─ 📄 Exhibit B - Email Correspondence │
│ ──────────────────────────────────────────────────────── │
│ 12/10/24 | Complaint | Plaintiff's initial filing... |[+] │
│ └─ 📄 Main Document │
│ └─ 📄 Exhibit 1 - Contract Agreement │
│ ──────────────────────────────────────────────────────── │
│ [More entries...] │
└─────────────────────────────────────────────────────────────┘
```
### Admin Dashboard
```
┌─────────────────────────────────────────────────────────────┐
│ ADMIN DASHBOARD [Logout] │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ [+ New Docket Entry] │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Manage Existing Entries │
│ ────────────────────── │
│ 12/15/24 | Motion to Dismiss | [Edit] [Delete] │
│ └─ 📄 Main Document [Edit] [Replace] [Del] │
│ └─ 📄 Exhibit A [Edit] [Replace] [Del] │
│ └─ 📄 Exhibit B [Edit] [Replace] [Del] │
│ └─ [+ Add Document to Entry] │
│ ──────────────────────────────────────────────────────── │
│ 12/10/24 | Complaint | [Edit] [Delete] │
│ └─ 📄 Main Document [Edit] [Replace] [Del] │
│ └─ 📄 Exhibit 1 [Edit] [Replace] [Del] │
│ └─ [+ Add Document to Entry] │
└─────────────────────────────────────────────────────────────┘
```
## Docker Configuration
### docker-compose.yml
```yaml
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://backend:3001/api
depends_on:
- backend
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- DATABASE_URL=postgresql://docket_user:docket_pass@postgres:5432/docket_db
- REDIS_URL=redis://redis:6379
- JWT_SECRET=your-secret-key
- UPLOAD_DIR=/app/uploads
volumes:
- uploads:/app/uploads
depends_on:
- postgres
- redis
postgres:
image: postgres:15
environment:
- POSTGRES_DB=docket_db
- POSTGRES_USER=docket_user
- POSTGRES_PASSWORD=docket_pass
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
depends_on:
- frontend
- backend
volumes:
postgres_data:
uploads:
```
## Security Implementation
### Authentication & Authorization
- JWT tokens for admin authentication
- Token expiration and refresh mechanism
- Protected routes middleware
- Role-based access control
### File Security
- Files served through API endpoints only
- No direct file system access
- File type validation (PDF only)
- File size limits (10MB max)
- UUID-based naming prevents enumeration
### Input Validation
- All API inputs validated with Joi/Zod
- SQL injection prevention with Prisma ORM
- XSS protection with input sanitization
- CSRF protection for admin forms
### Rate Limiting
- API endpoint rate limiting
- File upload rate limiting
- Login attempt limiting
- Email subscription rate limiting
## Email Notification System
### Subscription Process
1. User enters email on public site
2. Email validated and stored in database
3. Confirmation email sent with unsubscribe token
4. User added to active subscribers list
### Notification Trigger
1. Admin uploads new docket entry
2. System checks for active subscribers
3. Email notification queued for each subscriber
4. Background job processes email queue
5. Emails sent with entry summary and link
### Email Template
```
Subject: New Filing in Eliza Kragh v. Montana Association of the Deaf
Dear Subscriber,
A new document has been filed in the case of Eliza Kragh v. Montana Association of the Deaf:
Date: December 15, 2024
Document Type: Motion to Dismiss
Summary: Defendant's motion to dismiss based on lack of jurisdiction...
View the complete filing and all documents at:
https://docket-site.com
To unsubscribe from these notifications, click here:
https://docket-site.com/unsubscribe/[token]
Best regards,
Court Docket Notification System
```
## Performance Optimization
### Frontend Optimization
- Next.js static generation for public pages
- Image optimization for any graphics
- Code splitting and lazy loading
- PDF.js worker for document rendering
- Caching strategies for API responses
### Backend Optimization
- Database query optimization with indexes
- Redis caching for frequently accessed data
- File streaming for PDF downloads
- Connection pooling for database
- Compression middleware for API responses
### Infrastructure Optimization
- Nginx reverse proxy with caching
- Gzip compression for static assets
- CDN integration for file serving
- Database connection pooling
- Redis session storage
## Deployment Strategy
### Development Environment
- Docker Compose for local development
- Hot reloading for both frontend and backend
- Development database with sample data
- Email testing with local SMTP server
### Production Environment
- Docker Swarm or Kubernetes deployment
- SSL/TLS certificates with Let's Encrypt
- Production database with backups
- Real SMTP service for email notifications
- Monitoring and logging with ELK stack
### CI/CD Pipeline
- GitHub Actions for automated testing
- Docker image building and pushing
- Automated deployment to staging
- Manual approval for production deployment
- Database migration automation
## Testing Strategy
### Unit Testing
- Backend API endpoints with Jest
- Frontend components with React Testing Library
- Database operations with test database
- File upload functionality testing
### Integration Testing
- End-to-end user workflows with Cypress
- API integration testing
- Email notification testing
- File serving and download testing
### Security Testing
- Authentication and authorization testing
- Input validation testing
- File upload security testing
- SQL injection prevention testing
This comprehensive design specification serves as the complete technical reference for implementing the Eliza Kragh v. MAD court docket website.

View file

@ -0,0 +1,224 @@
# DNS Switchover Instructions - MAD Lawsuit v2.0
## Overview
This document provides step-by-step instructions to switch `mad-lawsuit.org` from v1.0 (Next.js) to v2.0 (Laravel).
## Pre-Switchover Checklist
- ✅ v2.0 fully tested at https://v2.mad-lawsuit.org
- ✅ All data migrated (63 entries, 63 documents, 28 subscribers)
- ✅ Admin dashboard functional
- ✅ PDF uploads working
- ✅ iOS/iPad PDF viewing fixed
- ✅ .env updated with mad-lawsuit.org URLs
- ✅ docker-compose.yml updated to use port 806
- ✅ Caddyfile updated on server
## Deployment Steps
### 1. SSH to Production Server
```bash
ssh chaulmark@10.4.0.205
```
### 2. Stop v1.0 Containers (Old mad-lawsuit.org)
```bash
cd ~/websites/mad-lawsuit.org
docker compose down
```
### 3. Pull Latest v2.0 Code
```bash
cd ~/websites/v2.mad-lawsuit.org
git pull origin main
```
### 4. Rebuild v2.0 Container with New Port
```bash
docker compose down
docker compose up -d --build
```
### 5. Verify Container is Running on Port 806
```bash
docker compose ps
# Should show mad-lawsuit-v2 running on 0.0.0.0:806->80/tcp
```
### 6. Clear Laravel Cache
```bash
docker exec mad-lawsuit-v2 php artisan config:clear
docker exec mad-lawsuit-v2 php artisan cache:clear
docker exec mad-lawsuit-v2 php artisan route:clear
```
### 7. Reload Caddy Configuration
```bash
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
```
### 8. Test the Switchover
```bash
# Test from command line
curl -I https://mad-lawsuit.org
# Should return HTTP/2 200
# Should show Laravel headers
```
### 9. Browser Testing
Open https://mad-lawsuit.org in multiple browsers:
- ✅ Desktop Chrome/Firefox/Safari
- ✅ Mobile Chrome/Safari
- ✅ iPad Safari (test PDF viewing)
### 10. Verify All Functionality
- [ ] Home page loads correctly
- [ ] Docket entries display properly
- [ ] PDF documents open correctly
- [ ] iPad PDF viewing works (opens in new tab)
- [ ] Admin login works: https://mad-lawsuit.org/admin/login
- [ ] Admin dashboard accessible
- [ ] Can create/edit/delete docket entries
- [ ] Can upload PDFs
- [ ] Email subscription works
## Configuration Details
### Port Mapping
- **v1.0 (Old)**: Port 806 (now stopped)
- **v2.0 (New)**: Port 806 (same port, different container)
### Caddyfile Configuration
```caddyfile
# MAD Lawsuit V2 (Production)
www.mad-lawsuit.org {
redir https://mad-lawsuit.org{uri} permanent
}
mad-lawsuit.org {
reverse_proxy 172.18.0.6:806
}
# Old v2 subdomain redirects to main domain
v2.mad-lawsuit.org {
redir https://mad-lawsuit.org{uri} permanent
}
www.v2.mad-lawsuit.org {
redir https://mad-lawsuit.org{uri} permanent
}
```
### Docker Network
- **Network**: `caddy_network` (external)
- **Container IP**: 172.18.0.6
- **Container Port**: 80 (internal)
- **Host Port**: 806 (external)
## Rollback Plan (If Needed)
If issues arise, you can quickly rollback to v1.0:
### 1. Stop v2.0 Container
```bash
cd ~/websites/v2.mad-lawsuit.org
docker compose down
```
### 2. Restore Original Caddyfile
```bash
# Download backup Caddyfile (if you made one)
# Or manually edit to point back to v1.0
```
### 3. Start v1.0 Containers
```bash
cd ~/websites/mad-lawsuit.org
docker compose up -d
```
### 4. Reload Caddy
```bash
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
```
## Post-Switchover Tasks
### Immediate (Within 24 hours)
- [ ] Monitor error logs: `docker logs mad-lawsuit-v2 -f`
- [ ] Check database connections
- [ ] Verify email notifications work
- [ ] Test all admin functions
### Within 1 Week
- [ ] Remove v1.0 containers permanently
- [ ] Clean up old v1.0 files (optional backup first)
- [ ] Update documentation
- [ ] Notify Eliza of successful switchover
### Optional Enhancements
- [ ] Set up automated backups
- [ ] Configure monitoring/alerts
- [ ] Implement email notifications for new entries
- [ ] Add Redis caching for performance
## Troubleshooting
### Issue: Site not loading
**Solution**: Check container status
```bash
docker compose ps
docker logs mad-lawsuit-v2 --tail 50
```
### Issue: 502 Bad Gateway
**Solution**: Verify Caddy can reach container
```bash
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
docker network inspect caddy_network
```
### Issue: Database connection errors
**Solution**: Check PostgreSQL container
```bash
docker compose ps postgres
docker logs mad-lawsuit-v2-db --tail 50
```
### Issue: PDFs not loading
**Solution**: Check file permissions and storage
```bash
docker exec mad-lawsuit-v2 ls -la storage/app/public/documents
docker exec mad-lawsuit-v2 php artisan storage:link
```
## Important Notes
1. **SSL Certificates**: Caddy automatically handles SSL for mad-lawsuit.org (Let's Encrypt)
2. **Database**: PostgreSQL data persists in Docker volume `postgres_data`
3. **PDF Files**: Stored in `./storage/app/public/documents` (bind mount)
4. **No Downtime**: Switchover should be nearly instant (< 1 minute)
5. **DNS**: No DNS changes needed - same domain, different backend
## Success Criteria
✅ https://mad-lawsuit.org loads v2.0 application
✅ All 63 docket entries visible
✅ All 63 PDFs accessible
✅ Admin dashboard functional
✅ iPad PDF viewing works correctly
✅ No errors in logs
✅ SSL certificate valid
## Contact
If issues arise:
- Check logs: `docker logs mad-lawsuit-v2 -f`
- Review this document
- Rollback if necessary (see Rollback Plan above)
---
**Last Updated**: December 19, 2025
**Version**: 2.0
**Status**: Ready for deployment

81
Dockerfile Normal file
View file

@ -0,0 +1,81 @@
# Multi-stage build for Laravel + Vue/Inertia application
FROM php:8.3-fpm-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libzip-dev \
zip \
unzip \
postgresql-dev \
nodejs \
npm \
nginx \
supervisor
# Install PHP extensions
RUN docker-php-ext-install pdo pdo_pgsql pgsql zip gd
# Configure PHP for large file uploads
RUN echo "upload_max_filesize = 550M" >> /usr/local/etc/php/conf.d/uploads.ini && \
echo "post_max_size = 550M" >> /usr/local/etc/php/conf.d/uploads.ini && \
echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini && \
echo "max_input_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini && \
echo "memory_limit = 512M" >> /usr/local/etc/php/conf.d/uploads.ini
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy composer files
COPY composer.json ./
# Install PHP dependencies (generate composer.lock if missing)
RUN composer install --no-dev --optimize-autoloader --no-scripts --no-interaction
# Copy package files
COPY package.json ./
# Install Node dependencies (generate package-lock.json if missing)
RUN npm install --legacy-peer-deps
# Copy application files
COPY . .
# Build frontend assets
RUN npm run build
# Generate optimized autoload files
RUN composer dump-autoload --optimize
# Create log directory for supervisor
RUN mkdir -p /var/log/supervisor
# Create nginx temp directories and set permissions
RUN mkdir -p /var/lib/nginx/tmp/client_body \
/var/lib/nginx/tmp/proxy \
/var/lib/nginx/tmp/fastcgi \
/var/lib/nginx/tmp/uwsgi \
/var/lib/nginx/tmp/scgi
# Set permissions - fix parent directory first, then subdirectories
RUN chmod 755 /var/lib/nginx && \
chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache /var/lib/nginx/tmp && \
chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache /var/lib/nginx/tmp
# Copy nginx configuration
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/default.conf /etc/nginx/http.d/default.conf
# Copy supervisor configuration
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose port 80
EXPOSE 80
# Start supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -0,0 +1,239 @@
# Phase 3 Data Migration - Completion Instructions
## Current Status
- ✅ Phase 1: Public website COMPLETE
- ✅ Phase 2: Admin dashboard COMPLETE
- ⏳ Phase 3: Data migration 95% COMPLETE
- ✅ SQL dump exported from v1.0 production
- ✅ 69 PDF files copied to `storage/app/public/documents/`
- ✅ V1DataMigrationSeeder created and tested
- ✅ Python parser script created and run
- ✅ Generated PHP arrays in `seeder_data.txt`
- ⏳ **REMAINING**: Update seeder with generated arrays and run import
## Files Ready
1. **`seeder_data.txt`** - Contains parsed PHP arrays (32 entries, 63 docs, 28 subs)
2. **`database/seeders/V1DataMigrationSeeder.php`** - Seeder file to update
3. **`parse_sql_to_seeder.py`** - Parser script (if needed to re-run)
4. **`/tmp/v1-data.sql`** - Original PostgreSQL dump
## What Needs to Be Done
### Step 1: Extract Arrays from seeder_data.txt
The file contains three sections:
```
================================================================================
DOCKET ENTRIES ARRAY:
================================================================================
$entries = [
['id' => 12, 'date' => '2025-06-06', ...],
['id' => 5, 'date' => '2025-05-07', ...],
...
];
================================================================================
DOCUMENTS ARRAY:
================================================================================
$documents = [
['id' => 4, 'docket_entry_id' => 1, ...],
...
];
================================================================================
SUBSCRIPTIONS ARRAY:
================================================================================
$subscriptions = [
['id' => 1, 'email' => 'chris@sigd.net', ...],
...
];
```
### Step 2: Update V1DataMigrationSeeder.php
**File**: `database/seeders/V1DataMigrationSeeder.php`
**Replace three methods:**
1. **importDocketEntries()** - Replace the `$entries` array (lines ~60-70)
2. **importDocuments()** - Replace the `$documents` array (lines ~80-90)
3. **importSubscriptions()** - Replace the `$subscriptions` array (lines ~100-110)
**Using replace_in_file tool:**
```php
// For importDocketEntries():
------- SEARCH
private function importDocketEntries(): void
{
$entries = [
// ... existing sample data ...
];
=======
private function importDocketEntries(): void
{
// PASTE THE $entries ARRAY FROM seeder_data.txt HERE
+++++++ REPLACE
// For importDocuments():
------- SEARCH
private function importDocuments(): void
{
$documents = [
// ... existing sample data ...
];
=======
private function importDocuments(): void
{
// PASTE THE $documents ARRAY FROM seeder_data.txt HERE
+++++++ REPLACE
// For importSubscriptions():
------- SEARCH
private function importSubscriptions(): void
{
$subscriptions = [
// ... existing sample data ...
];
=======
private function importSubscriptions(): void
{
// PASTE THE $subscriptions ARRAY FROM seeder_data.txt HERE
+++++++ REPLACE
```
### Step 3: Run the Seeder
```bash
php artisan db:seed --class=V1DataMigrationSeeder
```
**Expected Output:**
```
INFO Seeding database.
Imported 32 docket entries
Imported 63 documents
Imported 28 subscriptions
✅ v1.0 data migration complete!
- Docket Entries: 32
- Documents: 63
- Subscriptions: 28
```
### Step 4: Verify in Admin Dashboard
1. Open: http://127.0.0.1:8000/admin/login
2. Login: admin / password
3. Check dashboard shows:
- **32 entries** (increased from 10)
- **63 documents** (increased from 2)
- **28 subscribers** (increased from 2)
4. Click "Manage Entries" to see all 32 production entries
5. Click on an entry to verify documents are linked
## Important Notes
### Why Only 32 Entries (Not 63)?
The Python parser only captured single-line INSERT statements. Some entries in the SQL dump span multiple lines due to long summaries. This is acceptable because:
1. **32 entries is 3x more than the 10 test entries**
2. **All 63 documents are included** (every PDF is linked)
3. **All 28 subscribers are included** (complete email list)
4. **The remaining 31 entries can be added later** if needed
### File Paths
The documents array uses `/app/uploads/` paths from v1.0. These need to be updated to `documents/` for v2.0:
**Option A: Update in seeder before import**
- Find/replace `/app/uploads/` with `documents/` in the `$documents` array
**Option B: Update after import**
- Run SQL: `UPDATE documents SET file_path = REPLACE(file_path, '/app/uploads/', 'documents/');`
### PDF Files
All 69 PDF files are already in: `storage/app/public/documents/`
The storage symlink is configured: `php artisan storage:link` (already done)
## Troubleshooting
### If Seeder Fails
**Check for syntax errors:**
```bash
php -l database/seeders/V1DataMigrationSeeder.php
```
**Common issues:**
- Missing comma between array elements
- Unescaped quotes in strings
- Mismatched brackets
### If Documents Don't Show
**Check file_path column:**
```bash
php artisan tinker
>>> Document::first()->file_path
```
Should be: `documents/UUID.pdf` (not `/app/uploads/UUID.pdf`)
**Fix if needed:**
```bash
php artisan tinker
>>> DB::table('documents')->update(['file_path' => DB::raw("REPLACE(file_path, '/app/uploads/', 'documents/')")]);
```
### If You Need All 63 Entries
**Option 1: Fix the parser**
- Update `parse_sql_to_seeder.py` to handle multi-line INSERT statements
- Re-run: `python3 parse_sql_to_seeder.py /tmp/v1-data.sql > seeder_data_full.txt`
**Option 2: Manual SQL import**
- Import the SQL dump directly into PostgreSQL
- Export as CSV
- Create seeder from CSV
## Next Steps After Completion
Once Phase 3 is complete:
### Phase 4: Production Deployment
1. **Configure PostgreSQL** for production
2. **Implement email notifications** (SMTP)
3. **Deploy to v2.mad-lawsuit.org** subdomain
4. **Final testing** and verification
5. **Switch DNS** from v1.0 to v2.0
6. **Decommission v1.0**
## Files to Keep
- `seeder_data.txt` - Generated arrays (backup)
- `parse_sql_to_seeder.py` - Parser script (for future use)
- `/tmp/v1-data.sql` - Original SQL dump (backup)
- `storage/app/public/documents/*.pdf` - All PDF files (69 files)
## Success Criteria
✅ Phase 3 is complete when:
- Seeder runs without errors
- Dashboard shows 32+ entries
- Dashboard shows 63 documents
- Dashboard shows 28 subscribers
- All entries display correctly
- Documents download successfully
- Subscriber list is accurate
---
**Estimated Time**: 15-30 minutes
**Difficulty**: Easy (copy/paste + run command)
**Risk**: Low (can re-run seeder if issues occur)

439
README.md
View file

@ -1,278 +1,279 @@
# Elizabeth Kragh v. Montana Association of the Deaf - Court Docket Website # MAD Lawsuit Court Docket Website
A public-facing website to display court documents and filings for the ongoing lawsuit: **Elizabeth Kragh v. Montana Association of the Deaf**. The system provides public transparency for court proceedings while allowing administrative management of documents and user notifications. A public-facing website for displaying court documents and filings for the lawsuit: **Elizabeth Kragh v. Montana Association of the Deaf**.
## 🌐 Live Website **Live Site**: https://v2.mad-lawsuit.org
**Admin Dashboard**: https://v2.mad-lawsuit.org/admin/login
- **Public Access**: https://mad-lawsuit.org ## 📋 Project Overview
- **Admin Panel**: https://mad-lawsuit.org/admin
- **Status**: LIVE AND OPERATIONAL ✅
## 🏗️ Architecture This website provides public access to court documents from an ongoing lawsuit in Montana state court. Since the Montana court system requires a licensed attorney electronic account for access, this platform ensures transparency by making all case filings, motions, exhibits, and court decisions publicly accessible.
- **Frontend**: Next.js 15.4.0 with React 19, TypeScript 6.0, Tailwind CSS ### Purpose
- **Backend**: Node.js 22 LTS, Express.js 6.0, TypeScript 6.0, Prisma 5.0
- **Database**: PostgreSQL 17.5 with Redis caching
- **Infrastructure**: Docker Engine 28.1.1, Nginx reverse proxy
## 🚀 Quick Start - **Public Transparency**: Makes court proceedings accessible to anyone interested
- **Document Organization**: Centralized, chronological display of all case filings
- **Real-time Updates**: Email subscription system for case updates
- **Professional Presentation**: Clean interface with document summaries and metadata
### Target Audience
- General public following the case
- Legal community (attorneys, paralegals, law students)
- Disability rights advocacy groups
- Media and journalists
- Researchers studying the case
## 🛠️ Technology Stack
### Backend
- **Framework**: Laravel 12.43.1
- **Language**: PHP 8.3.28
- **Database**: PostgreSQL 16
- **ORM**: Eloquent
- **Authentication**: Laravel Sanctum + Session
### Frontend
- **Framework**: Vue 3 with TypeScript
- **Routing**: Inertia.js (SSR-like experience)
- **Styling**: Tailwind CSS 3.x
- **Build Tool**: Vite 7.x
- **Components**: Vue Single File Components (SFC)
### Infrastructure
- **Containerization**: Docker with Alpine Linux
- **Web Server**: nginx 1.28.0
- **PHP**: PHP-FPM 8.3.28
- **Reverse Proxy**: Caddy (with automatic SSL)
- **Deployment**: Production server at 10.4.0.205
## ✨ Features
### Public Website
- **Court Docket Display**: Chronological list of all court filings
- **Document Viewer**:
- Desktop: Modal PDF viewer with download option
- Mobile: Opens PDFs in new tab (optimized for iOS Safari)
- **Email Subscriptions**: Users can subscribe for case updates
- **Responsive Design**: Optimized for desktop, tablet, and mobile
- **Case Information**: Status, last updated date, and case details
### Admin Dashboard
- **Authentication**: Secure session-based login
- **Docket Entry Management**: Full CRUD operations
- Create new entries with date, title, and summary
- Edit existing entries
- Delete entries (cascades to associated documents)
- View entry details
- **Document Management**:
- Upload PDF documents (500MB max)
- UUID-based file naming for security
- Multiple documents per docket entry
- Delete documents
- **Subscriber Management**:
- View all subscribers (paginated, 50 per page)
- Deactivate subscribers
- Statistics dashboard (total, active, inactive)
- **Dashboard Statistics**: Quick overview of entries, documents, and subscribers
## 🚀 Getting Started
### Prerequisites ### Prerequisites
- PHP 8.3+
- Composer
- Node.js 18+ and npm
- PostgreSQL 16+
- Docker (for production deployment)
- Node.js 22 LTS ### Local Development Setup
- Docker Engine 28.1.1 & Docker Compose
- pnpm (recommended package manager)
### Development Setup
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone <repository-url> git clone https://gitea.sigd.net/chaulmark/mad-lawsuit.git
cd eliza-mad-docket-website cd mad-lawsuit
``` ```
2. **Start the development environment** 2. **Install PHP dependencies**
```bash ```bash
docker-compose up -d composer install
``` ```
3. **Install dependencies** 3. **Install JavaScript dependencies**
```bash ```bash
# Backend npm install
cd backend
pnpm install
# Frontend
cd ../frontend
pnpm install
``` ```
4. **Set up the database** 4. **Configure environment**
```bash ```bash
cd backend cp .env.example .env
pnpm prisma migrate dev php artisan key:generate
pnpm prisma generate
``` ```
5. **Start development servers** 5. **Configure database**
Edit `.env` with your PostgreSQL credentials:
```env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=mad_lawsuit
DB_USERNAME=your_username
DB_PASSWORD=your_password
```
6. **Run migrations**
```bash ```bash
# Backend (in backend directory) php artisan migrate
pnpm dev
# Frontend (in frontend directory)
pnpm dev
``` ```
6. **Access the application** 7. **Seed admin user**
- Frontend: http://localhost:3000 ```bash
- Backend API: http://localhost:3001 php artisan db:seed --class=AdminSeeder
- Database: localhost:5432 ```
- Redis: localhost:6379
8. **Create storage symlink**
```bash
php artisan storage:link
```
9. **Start development servers**
```bash
# Terminal 1: Laravel development server
php artisan serve
# Terminal 2: Vite development server
npm run dev
```
10. **Access the application**
- Public site: http://localhost:8000
- Admin dashboard: http://localhost:8000/admin/login
- Default credentials: See `database/seeders/AdminSeeder.php`
### Production Deployment
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed production deployment instructions using Docker.
## 📁 Project Structure ## 📁 Project Structure
``` ```
eliza-mad-docket/ ├── app/
├── backend/ # Express.js API │ ├── Http/
│ ├── src/ │ │ ├── Controllers/
│ │ ├── routes/ # API routes │ │ │ ├── Admin/ # Admin dashboard controllers
│ │ ├── middleware/ # Express middleware │ │ │ ├── HomeController.php
│ │ ├── utils/ # Utility functions │ │ │ └── SubscriptionController.php
│ │ └── index.ts # Main server file │ │ └── Middleware/
│ ├── prisma/ # Database schema & migrations │ │ └── AdminAuth.php # Admin authentication middleware
│ ├── Dockerfile # Backend container │ └── Models/
│ └── package.json │ ├── AdminUser.php
├── frontend/ # Next.js application │ ├── DocketEntry.php
│ ├── src/ │ ├── Document.php
│ │ ├── app/ # Next.js 13+ app directory │ └── Subscription.php
│ │ ├── components/ # React components ├── database/
│ │ └── lib/ # Utility libraries │ ├── migrations/ # Database schema
│ ├── Dockerfile # Frontend container │ └── seeders/
│ └── package.json │ ├── AdminSeeder.php
├── cline_docs/ # Memory Bank documentation │ └── V1DataMigrationSeeder.php
├── docker-compose.yml # Container orchestration ├── resources/
└── DESIGN_SPECIFICATION.md # Complete technical spec │ ├── js/
│ │ ├── Components/
│ │ │ └── PDFViewer.vue # PDF modal viewer
│ │ ├── Pages/
│ │ │ ├── Home.vue # Public homepage
│ │ │ └── Admin/ # Admin dashboard pages
│ │ └── app.ts
│ └── css/
│ └── app.css
├── routes/
│ └── web.php # Application routes
├── docker/ # Docker configuration
├── cline_docs/ # Project documentation
└── storage/
└── app/
└── public/
└── documents/ # Uploaded PDF files
``` ```
## 🔧 Environment Variables ## 🔐 Security
### Backend (.env) - **Admin Authentication**: Session-based authentication separate from Laravel Breeze
```env - **File Security**: UUID-based file naming prevents direct access guessing
DATABASE_URL=postgresql://docket_user:docket_pass@localhost:5432/docket_db - **CSRF Protection**: Laravel's built-in CSRF protection on all forms
REDIS_URL=redis://localhost:6379 - **SQL Injection**: Eloquent ORM prevents SQL injection
JWT_SECRET=your-secret-key-change-in-production - **XSS Protection**: Vue.js automatic escaping
JWT_EXPIRES_IN=24h - **HTTPS**: Automatic SSL via Caddy in production
UPLOAD_DIR=/app/uploads
MAX_FILE_SIZE=10485760
NODE_ENV=development
```
### Frontend (.env.local) ## 📊 Database Schema
```env
NEXT_PUBLIC_API_URL=http://localhost:3001/api
```
## 🗄️ Database Schema ### Tables
- **docket_entries**: Court filings with date, title, summary, notes
- **documents**: PDF files linked to docket entries
- **subscriptions**: Email subscribers with active/inactive status
- **admin_users**: Admin authentication
The application uses PostgreSQL with the following main tables: ### Relationships
- One docket entry has many documents
- **docket_entries**: Main court filings - Documents cascade delete when entry is deleted
- **documents**: PDF files associated with entries - Subscriptions are soft-deleted (is_active flag)
- **subscriptions**: Email subscriptions for notifications
- **admin_users**: Administrative users
## 🔐 Security Features
- JWT authentication for admin access
- File access control through API endpoints
- UUID-based file naming for security
- Input validation and sanitization
- Rate limiting on API endpoints
- CORS protection
## 📄 File Management
- **Storage**: Local filesystem with Docker volumes
- **Naming**: UUID-based scheme: `{entry-id}_{document-type}_{uuid}.pdf`
- **Structure**: Organized by year/month directories
- **Security**: Files served only through API endpoints
- **Limits**: 10MB maximum file size, PDF only
## 🚀 Deployment
### Production Deployment
For production deployment with Portainer and Caddy, see the complete [DEPLOYMENT.md](./DEPLOYMENT.md) guide.
**Quick Production Setup:**
1. **Prepare environment**
```bash
cp .env.prod .env
# Edit .env with secure values
```
2. **Deploy with production compose**
```bash
docker-compose -f docker-compose.prod.yml up -d
```
3. **Set up admin user for Elizabeth**
```bash
docker exec -it backend_container node setup-admin-fixed.js
```
## 🔐 Admin Access
### Current Production Credentials
- **URL**: https://mad-lawsuit.org/admin
- **Username**: `eliza`
- **Password**: `AXEoZWk2AMAD0naw`
- **Status**: Active and functional ✅
**⚠️ Security Note**: These credentials were generated during production deployment. Change them immediately after first login for enhanced security.
### Development Setup
1. **Build containers**
```bash
docker-compose build
```
2. **Run development stack**
```bash
docker-compose up -d
```
3. **Run database migrations**
```bash
docker-compose exec backend pnpm prisma migrate deploy
```
### Environment-specific configurations
- **Development**: Hot reloading, detailed logging, demo credentials
- **Production**: Optimized builds, security headers, SSL/TLS, secure credentials
## 🧪 Testing ## 🧪 Testing
```bash ```bash
# Backend tests # Run PHP tests
cd backend php artisan test
pnpm test
# Frontend tests # Run with coverage
cd frontend php artisan test --coverage
pnpm test
# Type checking
pnpm type-check
``` ```
## 📊 API Endpoints ## 📝 API Endpoints
### Public Endpoints ### Public Routes
- `GET /api/docket-entries` - Get all docket entries with documents - `GET /` - Homepage with docket entries
- `GET /api/documents/:id/download` - Download PDF file - `POST /api/subscribe` - Email subscription
- `POST /api/subscribe` - Subscribe to email notifications - `GET /api/documents/{id}/download` - Download PDF
- `GET /api/unsubscribe/:token` - Unsubscribe from notifications
### Admin Endpoints (Protected) ### Admin Routes (requires authentication)
- `POST /api/auth/login` - Admin authentication - `GET /admin/login` - Admin login page
- `POST /api/docket-entries` - Create new docket entry - `POST /admin/login` - Process login
- `PUT /api/docket-entries/:id` - Update docket entry - `POST /admin/logout` - Logout
- `DELETE /api/docket-entries/:id` - Delete docket entry - `GET /admin/dashboard` - Admin dashboard
- `POST /api/documents` - Upload document - Resource routes for:
- `PUT /api/documents/:id` - Update document metadata - `/admin/docket-entries` - Docket entry management
- `DELETE /api/documents/:id` - Delete document - `/admin/documents` - Document management
- `/admin/subscribers` - Subscriber management
## 🎨 UI Features ## 🐛 Known Issues & Solutions
### Public Interface ### Mobile PDF Viewing
- Clean, responsive design - **Issue**: iOS Safari renders PDFs in iframes as single-page images
- Document viewing with PDF.js - **Solution**: On screens < 768px, PDFs open in a new tab instead of modal viewer
- Email subscription system
- Mobile-friendly layout
- Accessibility compliant
### Admin Interface ### Admin Subscribers Page
- Secure login system - **Fixed**: Changed pagination from `->through()` to `->items()` to properly pass array to Vue
- Multi-file upload support
- Document management (CRUD operations)
- File replacement capability
- Metadata editing
## 📧 Email Notifications
- Automatic notifications for new filings
- Unsubscribe functionality
- HTML email templates
- Background job processing
## 🔍 Monitoring & Logging
- Structured logging with Winston
- Request/response logging
- Error tracking and reporting
- Health check endpoints
- Performance monitoring
## 🤝 Contributing ## 🤝 Contributing
1. Fork the repository This is a private project for a specific lawsuit. For questions or issues, contact the repository owner.
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## 📝 License ## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details. Proprietary - All rights reserved.
## 👥 Credits
- **Development**: Built with Laravel, Vue.js, and Inertia.js
- **Design**: DeafGain LLC (http://deafgain.org)
- **Hosting**: Self-hosted on dedicated infrastructure
## 📞 Support ## 📞 Support
For questions or issues, please contact the development team or create an issue in the repository. For technical issues or questions:
- Repository: https://gitea.sigd.net/chaulmark/mad-lawsuit
- Production URL: https://v2.mad-lawsuit.org
--- ---
**Note**: This is a public transparency tool for court proceedings. All uploaded documents become publicly accessible through this website. **Version**: 2.0
**Last Updated**: December 2025
**Status**: Production Ready ✅

View file

@ -0,0 +1,118 @@
<?php
namespace App\Console\Commands;
use App\Models\Document;
use App\Models\DocumentAccess;
use Carbon\Carbon;
use Illuminate\Console\Command;
class ImportDocumentAccessLog extends Command
{
protected $signature = 'documents:import-access-log
{file : Path to an nginx access log in NCSA combined format}
{--source=nginx-backfill : Value stored in the source column}
{--dry-run : Parse and report without writing anything}';
protected $description = 'Backfill document_accesses from an archived nginx access log';
/**
* Matches e.g.
* 172.18.0.1 - - [23/May/2026:18:50:19 +0000] "GET /api/documents/83/download HTTP/1.1" 200 12345 "-" "Mozilla/5.0 ..."
*
* Only status 200 counts as an access: 499 means the client aborted the
* transfer, and 4xx/5xx never delivered the file.
*/
private const LINE = '#^\S+ \S+ \S+ \[([^\]]+)\] "GET /api/documents/(\d+)/download [^"]*" 200 \S+ "[^"]*" "([^"]*)"#';
public function handle(): int
{
$file = $this->argument('file');
if (! is_readable($file)) {
$this->error("Cannot read file: {$file}");
return self::FAILURE;
}
$source = $this->option('source');
$dryRun = $this->option('dry-run');
// Only import accesses for documents that still exist; the FK would
// reject the rest and a deleted document has no dashboard row anyway.
$knownIds = Document::pluck('id')->flip();
$handle = fopen($file, 'r');
$rows = [];
$parsed = $skippedUnknown = $unmatched = 0;
while (($line = fgets($handle)) !== false) {
if (! preg_match(self::LINE, $line, $m)) {
// Most lines are ordinary page views, not downloads.
if (str_contains($line, '/download')) {
$unmatched++;
}
continue;
}
[, $timestamp, $documentId, $userAgent] = $m;
if (! $knownIds->has((int) $documentId)) {
$skippedUnknown++;
continue;
}
$rows[] = [
'document_id' => (int) $documentId,
'accessed_at' => Carbon::createFromFormat('d/M/Y:H:i:s O', $timestamp)->utc(),
'ip_address' => null, // never recorded by the old log format
'user_agent' => $userAgent,
'source' => $source,
'created_at' => now(),
'updated_at' => now(),
];
$parsed++;
}
fclose($handle);
$this->info("Parsed {$parsed} successful downloads from {$file}");
if ($skippedUnknown > 0) {
$this->warn("Skipped {$skippedUnknown} referring to documents that no longer exist");
}
if ($unmatched > 0) {
$this->warn("Skipped {$unmatched} download lines that were not status 200 (aborted or failed)");
}
if ($dryRun) {
$this->comment('Dry run: nothing written.');
return self::SUCCESS;
}
// Idempotency: a re-run must not double-count. Every row from a given
// log carries the same source, so clearing that source first makes the
// import safely repeatable.
$existing = DocumentAccess::where('source', $source)->count();
if ($existing > 0) {
if (! $this->confirm("{$existing} rows with source '{$source}' already exist. Replace them?", true)) {
$this->comment('Aborted; nothing written.');
return self::SUCCESS;
}
DocumentAccess::where('source', $source)->delete();
}
foreach (array_chunk($rows, 500) as $chunk) {
DocumentAccess::insert($chunk);
}
$this->info('Imported '.count($rows)." rows with source '{$source}'.");
return self::SUCCESS;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AdminUser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Session;
use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Http\RedirectResponse;
class AuthController extends Controller
{
/**
* Show the admin login form.
*/
public function showLogin(): Response
{
return Inertia::render('Admin/Login');
}
/**
* Handle admin login.
*/
public function login(Request $request): RedirectResponse
{
$request->validate([
'username' => 'required|string',
'password' => 'required|string',
]);
$admin = AdminUser::where('username', $request->username)->first();
if (!$admin || !Hash::check($request->password, $admin->password)) {
return back()->withErrors([
'username' => 'The provided credentials do not match our records.',
])->onlyInput('username');
}
// Store admin ID in session
Session::put('admin_id', $admin->id);
Session::put('admin_username', $admin->username);
return redirect()->route('admin.dashboard');
}
/**
* Handle admin logout.
*/
public function logout(Request $request): RedirectResponse
{
Session::forget('admin_id');
Session::forget('admin_username');
Session::flush();
return redirect()->route('admin.login');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DocketEntry;
use App\Models\Document;
use App\Models\DocumentAccess;
use App\Models\Subscription;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
/**
* Display the admin dashboard with statistics and recent entries.
*/
public function index(): Response
{
$stats = [
'total_entries' => DocketEntry::count(),
'total_documents' => Document::count(),
'total_subscribers' => Subscription::where('is_active', true)->count(),
'recent_entries' => DocketEntry::with('documents')
->latest('date')
->take(5)
->get()
->map(function ($entry) {
return [
'id' => $entry->id,
'date' => $entry->date->format('m/d/Y'),
'title' => $entry->title,
'summary' => $entry->summary,
'documents_count' => $entry->documents->count(),
];
}),
];
$stats += $this->documentAccessSummary();
return Inertia::render('Admin/Dashboard', $stats);
}
/**
* Headline document-access numbers for the last 60 days, plus the most
* requested documents. Crawlers are excluded so the figures reflect people.
*/
private function documentAccessSummary(): array
{
$since = Carbon::now()->subDays(60)->startOfDay();
return [
'downloads_60d' => DocumentAccess::excludingBots()
->where('accessed_at', '>=', $since)
->count(),
'top_documents_60d' => DocumentAccess::query()
->excludingBots()
->join('documents', 'documents.id', '=', 'document_accesses.document_id')
->where('document_accesses.accessed_at', '>=', $since)
->groupBy('documents.id', 'documents.title')
->orderByDesc('downloads')
->take(5)
->get([
'documents.id',
'documents.title',
DB::raw('count(*) as downloads'),
])
->map(fn ($row) => [
'id' => $row->id,
'title' => $row->title,
'downloads' => (int) $row->downloads,
]),
];
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DocketEntry;
use App\Models\Subscription;
use App\Mail\NewDocketEntryNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Http\RedirectResponse;
class DocketEntryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): Response
{
$entries = DocketEntry::with('documents')
->orderBy('date', 'desc')
->get();
return Inertia::render('Admin/DocketEntries/Index', [
'entries' => $entries->map(fn ($entry) => [
'id' => $entry->id,
'date' => $entry->date->format('m/d/Y'),
'title' => $entry->title,
'summary' => $entry->summary,
'documents_count' => $entry->documents->count(),
])->toArray(),
]);
}
/**
* Show the form for creating a new resource.
*/
public function create(): Response
{
return Inertia::render('Admin/DocketEntries/Create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'date' => 'required|date',
'title' => 'required|string|max:500',
'summary' => 'required|string',
'notes' => 'nullable|string',
]);
$entry = DocketEntry::create($validated);
return redirect()->route('admin.docket-entries.show', $entry->id)
->with('success', 'Docket entry created successfully.');
}
/**
* Display the specified resource.
*/
public function show(DocketEntry $docketEntry): Response
{
$docketEntry->load('documents');
return Inertia::render('Admin/DocketEntries/Show', [
'entry' => [
'id' => $docketEntry->id,
'date' => $docketEntry->date->format('m/d/Y'),
'title' => $docketEntry->title,
'summary' => $docketEntry->summary,
'notes' => $docketEntry->notes,
'documents' => $docketEntry->documents->map(fn ($doc) => [
'id' => $doc->id,
'title' => $doc->title,
'original_filename' => $doc->original_filename,
'file_size' => $doc->file_size,
'summary' => $doc->summary,
'display_order' => $doc->display_order,
]),
],
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(DocketEntry $docketEntry): Response
{
return Inertia::render('Admin/DocketEntries/Edit', [
'entry' => [
'id' => $docketEntry->id,
'date' => $docketEntry->date->format('Y-m-d'),
'title' => $docketEntry->title,
'summary' => $docketEntry->summary,
'notes' => $docketEntry->notes,
],
]);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, DocketEntry $docketEntry): RedirectResponse
{
$validated = $request->validate([
'date' => 'required|date',
'title' => 'required|string|max:500',
'summary' => 'required|string',
'notes' => 'nullable|string',
]);
$docketEntry->update($validated);
return redirect()->route('admin.docket-entries.show', $docketEntry->id)
->with('success', 'Docket entry updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(DocketEntry $docketEntry): RedirectResponse
{
$docketEntry->delete();
return redirect()->route('admin.docket-entries.index')
->with('success', 'Docket entry deleted successfully.');
}
/**
* Send email notification to subscribers for this docket entry.
*/
public function sendNotification(DocketEntry $docketEntry): RedirectResponse
{
$this->sendEmailNotifications($docketEntry);
return redirect()->route('admin.docket-entries.show', $docketEntry->id)
->with('success', 'Email notifications sent successfully to subscribers.');
}
/**
* Send email notifications to all active subscribers.
*/
private function sendEmailNotifications(DocketEntry $entry): void
{
try {
// Get all active subscribers
$subscribers = Subscription::where('is_active', true)->get();
if ($subscribers->isEmpty()) {
Log::info('No active subscribers to notify', [
'entry_id' => $entry->id,
]);
return;
}
Log::info('Sending email notifications for new docket entry', [
'entry_id' => $entry->id,
'entry_title' => $entry->title,
'subscriber_count' => $subscribers->count(),
]);
$successCount = 0;
$failureCount = 0;
foreach ($subscribers as $subscriber) {
try {
Mail::to($subscriber->email)->send(
new NewDocketEntryNotification($entry, $subscriber->unsubscribe_token)
);
$successCount++;
Log::debug('Email sent to subscriber', [
'entry_id' => $entry->id,
'subscriber_email' => $subscriber->email,
]);
} catch (\Exception $e) {
$failureCount++;
Log::error('Failed to send email to subscriber', [
'entry_id' => $entry->id,
'subscriber_email' => $subscriber->email,
'error' => $e->getMessage(),
]);
}
}
Log::info('Email notification batch complete', [
'entry_id' => $entry->id,
'success_count' => $successCount,
'failure_count' => $failureCount,
'total_subscribers' => $subscribers->count(),
]);
} catch (\Exception $e) {
Log::error('Failed to send email notifications', [
'entry_id' => $entry->id,
'error' => $e->getMessage(),
]);
// Don't throw exception - we don't want email failures to break the entry creation
}
}
}

View file

@ -0,0 +1,199 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DocumentAccess;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class DocumentAccessController extends Controller
{
/**
* The date real client IPs started being recorded. Before this, the nginx
* log stored only the docker gateway address, so unique-visitor counts are
* not available for earlier periods and must not be implied.
*/
private const IP_DATA_STARTS = '2026-07-30';
/**
* Per-document access counts over a selectable date range.
*/
public function index(Request $request): Response
{
$validated = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'include_bots' => ['nullable', 'boolean'],
]);
$from = isset($validated['from'])
? Carbon::parse($validated['from'])->startOfDay()
: Carbon::now()->subDays(60)->startOfDay();
$to = isset($validated['to'])
? Carbon::parse($validated['to'])->endOfDay()
: Carbon::now()->endOfDay();
$includeBots = (bool) ($validated['include_bots'] ?? false);
$documents = $this->documentCounts($from, $to, $includeBots);
return Inertia::render('Admin/DocumentAccess', [
'documents' => $documents,
'daily' => $this->dailyCounts($from, $to, $includeBots),
'filters' => [
'from' => $from->toDateString(),
'to' => $to->toDateString(),
'include_bots' => $includeBots,
],
'totals' => [
'downloads' => $documents->sum('downloads'),
'documents_touched' => $documents->count(),
// Distinct across the whole range, not the sum of the per-document
// column: one person pulling five documents is one visitor.
'unique_visitors' => $this->uniqueVisitors($from, $to, $includeBots),
],
// Drives the caveat shown in the UI.
'ip_data_starts' => self::IP_DATA_STARTS,
'range_predates_ip_data' => $from->lt(Carbon::parse(self::IP_DATA_STARTS)),
]);
}
/**
* Export the current view as CSV, so figures can be cited or filed
* without retyping them.
*/
public function export(Request $request): StreamedResponse
{
$validated = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'include_bots' => ['nullable', 'boolean'],
]);
$from = isset($validated['from'])
? Carbon::parse($validated['from'])->startOfDay()
: Carbon::now()->subDays(60)->startOfDay();
$to = isset($validated['to'])
? Carbon::parse($validated['to'])->endOfDay()
: Carbon::now()->endOfDay();
$includeBots = (bool) ($validated['include_bots'] ?? false);
$documents = $this->documentCounts($from, $to, $includeBots);
$ipDataStarts = Carbon::parse(self::IP_DATA_STARTS);
$filename = sprintf(
'document-access-%s-to-%s.csv',
$from->toDateString(),
$to->toDateString()
);
return response()->streamDownload(function () use ($documents, $from, $ipDataStarts) {
$out = fopen('php://output', 'w');
fputcsv($out, ['Document', 'Downloads', 'Unique visitors', 'Last accessed']);
foreach ($documents as $row) {
fputcsv($out, [
$row['title'],
$row['downloads'],
// Blank rather than 0 where the figure is unknowable, so the
// spreadsheet cannot be read as "nobody downloaded this".
$row['unique_visitors'] === 0 ? '' : $row['unique_visitors'],
$row['last_accessed'],
]);
}
if ($from->lt($ipDataStarts)) {
fputcsv($out, []);
fputcsv($out, [
'Note: visitor IP addresses were only recorded from '
.$ipDataStarts->toDateString()
.'. Unique visitors are blank for earlier periods.',
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
/**
* Distinct visitors across the range as a whole.
*/
private function uniqueVisitors(Carbon $from, Carbon $to, bool $includeBots): int
{
$query = DocumentAccess::query()
->whereBetween('accessed_at', [$from, $to])
->whereNotNull('ip_address');
if (! $includeBots) {
$query->excludingBots();
}
return (int) $query->distinct()->count('ip_address');
}
/**
* Download counts per document, plus unique visitors where the underlying
* rows actually carry an IP.
*/
private function documentCounts(Carbon $from, Carbon $to, bool $includeBots)
{
$query = DocumentAccess::query()
->join('documents', 'documents.id', '=', 'document_accesses.document_id')
->whereBetween('document_accesses.accessed_at', [$from, $to]);
if (! $includeBots) {
$query->excludingBots();
}
return $query
->groupBy('documents.id', 'documents.title')
->orderByDesc('downloads')
->get([
'documents.id',
'documents.title',
DB::raw('count(*) as downloads'),
DB::raw('count(distinct document_accesses.ip_address) as unique_visitors'),
DB::raw('max(document_accesses.accessed_at) as last_accessed'),
])
->map(fn ($row) => [
'id' => $row->id,
'title' => $row->title,
'downloads' => (int) $row->downloads,
'unique_visitors' => (int) $row->unique_visitors,
'last_accessed' => Carbon::parse($row->last_accessed)->format('m/d/Y'),
]);
}
/**
* Downloads per day, for the trend chart.
*/
private function dailyCounts(Carbon $from, Carbon $to, bool $includeBots)
{
$query = DocumentAccess::query()
->whereBetween('accessed_at', [$from, $to]);
if (! $includeBots) {
$query->excludingBots();
}
return $query
->groupBy('day')
->orderBy('day')
->get([
DB::raw('date(accessed_at) as day'),
DB::raw('count(*) as downloads'),
])
->map(fn ($row) => [
'day' => Carbon::parse($row->day)->format('m/d'),
'downloads' => (int) $row->downloads,
]);
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DocketEntry;
use App\Models\Document;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class DocumentController extends Controller
{
/**
* Store a newly uploaded document.
*/
public function store(Request $request, DocketEntry $docketEntry): RedirectResponse
{
try {
\Log::info('=== PDF UPLOAD DEBUG START ===');
\Log::info('Docket Entry ID: ' . $docketEntry->id);
\Log::info('Request has file: ' . ($request->hasFile('file') ? 'YES' : 'NO'));
if ($request->hasFile('file')) {
$file = $request->file('file');
\Log::info('File original name: ' . $file->getClientOriginalName());
\Log::info('File size: ' . $file->getSize());
\Log::info('File mime type: ' . $file->getMimeType());
\Log::info('File is valid: ' . ($file->isValid() ? 'YES' : 'NO'));
}
$validated = $request->validate([
'file' => 'required|file|mimes:pdf|max:512000', // 500MB max
'title' => 'required|string|max:255',
'summary' => 'nullable|string',
]);
\Log::info('Validation passed');
// Get the uploaded file
$file = $request->file('file');
// Generate unique filename
$storedFilename = Str::uuid() . '.pdf';
\Log::info('Generated filename: ' . $storedFilename);
// Store file in storage/app/public/documents
\Log::info('Attempting to store file...');
$filePath = $file->storeAs('documents', $storedFilename, 'public');
\Log::info('File stored at: ' . $filePath);
// Get the highest display order for this entry
$maxOrder = $docketEntry->documents()->max('display_order') ?? -1;
\Log::info('Max display order: ' . $maxOrder);
// Create document record
\Log::info('Creating document record...');
$document = Document::create([
'docket_entry_id' => $docketEntry->id,
'title' => $validated['title'],
'original_filename' => $file->getClientOriginalName(),
'stored_filename' => $storedFilename,
'file_path' => $filePath,
'file_size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'summary' => $validated['summary'] ?? null,
'display_order' => $maxOrder + 1,
]);
\Log::info('Document created with ID: ' . $document->id);
\Log::info('=== PDF UPLOAD DEBUG END (SUCCESS) ===');
return redirect()->route('admin.docket-entries.show', $docketEntry->id)
->with('success', 'Document uploaded successfully.');
} catch (\Exception $e) {
\Log::error('=== PDF UPLOAD ERROR ===');
\Log::error('Error message: ' . $e->getMessage());
\Log::error('Error file: ' . $e->getFile());
\Log::error('Error line: ' . $e->getLine());
\Log::error('Stack trace: ' . $e->getTraceAsString());
\Log::error('=== PDF UPLOAD DEBUG END (ERROR) ===');
return redirect()->route('admin.docket-entries.show', $docketEntry->id)
->with('error', 'Upload failed: ' . $e->getMessage());
}
}
/**
* Remove the specified document.
*/
public function destroy(Document $document): RedirectResponse
{
$docketEntryId = $document->docket_entry_id;
// Delete the file from storage
if (Storage::disk('public')->exists($document->file_path)) {
Storage::disk('public')->delete($document->file_path);
}
// Delete the database record
$document->delete();
return redirect()->route('admin.docket-entries.show', $docketEntryId)
->with('success', 'Document deleted successfully.');
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Subscription;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
class SubscriberController extends Controller
{
/**
* Display a listing of subscribers.
*/
public function index(): Response
{
$subscribers = Subscription::orderBy('created_at', 'desc')
->paginate(50);
return Inertia::render('Admin/Subscribers/Index', [
'subscribers' => $subscribers->items(),
'pagination' => [
'current_page' => $subscribers->currentPage(),
'last_page' => $subscribers->lastPage(),
'per_page' => $subscribers->perPage(),
'total' => $subscribers->total(),
],
'stats' => [
'total' => Subscription::count(),
'active' => Subscription::where('is_active', true)->count(),
'inactive' => Subscription::where('is_active', false)->count(),
],
]);
}
/**
* Remove the specified subscriber.
*/
public function destroy(Subscription $subscription): RedirectResponse
{
// Soft delete by setting is_active to false
$subscription->update(['is_active' => false]);
return redirect()->route('admin.subscribers.index')
->with('success', 'Subscriber deactivated successfully.');
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): Response
{
return Inertia::render('Auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use App\Models\DocumentAccess;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class DocumentController extends Controller
{
/**
* Download a document by ID.
*/
public function download(Request $request, int $id): StreamedResponse
{
$document = Document::findOrFail($id);
// Files are stored in public disk (storage/app/public/documents/)
// Check if file exists
if (!Storage::disk('public')->exists($document->file_path)) {
abort(404, 'Document file not found.');
}
// Record the access before streaming, so the dashboard reflects it.
// Only successful downloads reach this point, which matches how the
// historical nginx rows were filtered (status 200 only).
DocumentAccess::create([
'document_id' => $document->id,
'accessed_at' => now(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'source' => 'live',
]);
// Stream the file to the browser
return Storage::disk('public')->download(
$document->file_path,
$document->original_filename,
[
'Content-Type' => $document->mime_type,
'Content-Disposition' => 'inline; filename="' . $document->original_filename . '"',
]
);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use App\Models\DocketEntry;
use Inertia\Inertia;
use Inertia\Response;
class HomeController extends Controller
{
/**
* Display the home page with all docket entries.
*/
public function index(): Response
{
$docketEntries = DocketEntry::with('documents')
->orderBy('date', 'desc')
->get()
->map(fn ($entry) => [
'id' => $entry->id,
'date' => $entry->date->format('m/d/Y'),
'title' => $entry->title,
'summary' => $entry->summary,
'documents' => $entry->documents->map(fn ($doc) => [
'id' => $doc->id,
'title' => $doc->title,
'file_path' => $doc->file_path,
]),
]);
return Inertia::render('Home', [
'docketEntries' => $docketEntries,
'caseInfo' => [
'title' => 'Elizabeth Kragh v. Montana Association of the Deaf',
'status' => 'Active Litigation',
'lastUpdated' => $docketEntries->first()['date'] ?? 'No entries yet',
],
]);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use Inertia\Response;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): Response
{
return Inertia::render('Profile/Edit', [
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
'status' => session('status'),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers;
use App\Models\Subscription;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
class SubscriptionController extends Controller
{
/**
* Subscribe an email address to notifications.
*/
public function subscribe(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => 'required|email|max:255',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Please provide a valid email address.',
'errors' => $validator->errors(),
], 422);
}
$email = $request->input('email');
// Check if email already exists
$existing = Subscription::where('email', $email)->first();
if ($existing) {
if ($existing->is_active) {
return response()->json([
'success' => false,
'message' => 'This email is already subscribed to notifications.',
], 409);
} else {
// Reactivate subscription
$existing->is_active = true;
$existing->save();
return response()->json([
'success' => true,
'message' => 'Your subscription has been reactivated! You will receive email notifications for new docket entries.',
]);
}
}
// Create new subscription
Subscription::create([
'email' => $email,
'is_active' => true,
]);
return response()->json([
'success' => true,
'message' => 'Successfully subscribed! You will receive email notifications for new docket entries.',
]);
}
/**
* Show unsubscribe confirmation page.
*/
public function showUnsubscribe(string $token)
{
$subscription = Subscription::where('unsubscribe_token', $token)->first();
if (!$subscription) {
abort(404, 'Invalid unsubscribe token.');
}
return inertia('Unsubscribe', [
'token' => $token,
'email' => $subscription->email,
]);
}
/**
* Process unsubscribe request.
*/
public function unsubscribe(Request $request, string $token): JsonResponse
{
$subscription = Subscription::where('unsubscribe_token', $token)->first();
if (!$subscription) {
return response()->json([
'success' => false,
'message' => 'Invalid unsubscribe token.',
], 404);
}
$subscription->is_active = false;
$subscription->save();
return response()->json([
'success' => true,
'message' => 'You have been successfully unsubscribed from notifications.',
]);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Symfony\Component\HttpFoundation\Response;
class AdminAuth
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!Session::has('admin_id')) {
return redirect()->route('admin.login');
}
return $next($request);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies = '*';
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace App\Mail;
use App\Models\DocketEntry;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class NewDocketEntryNotification extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
public DocketEntry $entry,
public string $unsubscribeToken
) {
//
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'New Court Filing: ' . $this->entry->title,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.new-docket-entry',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

27
app/Models/AdminUser.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
class AdminUser extends Model
{
protected $fillable = [
'username',
'password',
];
protected $hidden = [
'password',
];
/**
* Verify password.
*/
public function verifyPassword($password): bool
{
return Hash::check($password, $this->password);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class DocketEntry extends Model
{
protected $fillable = [
'date',
'title',
'summary',
'notes',
];
protected $casts = [
'date' => 'date',
];
/**
* Get the documents for the docket entry.
*/
public function documents(): HasMany
{
return $this->hasMany(Document::class)->orderBy('display_order');
}
}

35
app/Models/Document.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Document extends Model
{
protected $fillable = [
'docket_entry_id',
'title',
'original_filename',
'stored_filename',
'file_path',
'file_size',
'mime_type',
'summary',
'notes',
'display_order',
];
protected $casts = [
'file_size' => 'integer',
'display_order' => 'integer',
];
/**
* Get the docket entry that owns the document.
*/
public function docketEntry(): BelongsTo
{
return $this->belongsTo(DocketEntry::class);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DocumentAccess extends Model
{
protected $table = 'document_accesses';
protected $fillable = [
'document_id',
'accessed_at',
'ip_address',
'user_agent',
'source',
];
protected $casts = [
'accessed_at' => 'datetime',
];
/**
* Get the document that was accessed.
*/
public function document(): BelongsTo
{
return $this->belongsTo(Document::class);
}
/**
* Requests whose user agent looks like a crawler rather than a person.
* Kept as a query-time filter rather than a write-time one so the raw
* record stays complete and the definition can be changed later.
*/
public function scopeExcludingBots($query)
{
foreach (['%bot%', '%crawl%', '%spider%', '%slurp%'] as $pattern) {
$query->where(function ($q) use ($pattern) {
$q->whereNull('user_agent')
->orWhere('user_agent', 'not ilike', $pattern);
});
}
return $query;
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model
{
protected $fillable = [
'email',
'is_active',
'unsubscribe_token',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Generate a unique unsubscribe token.
*/
protected static function boot()
{
parent::boot();
static::creating(function ($subscription) {
if (empty($subscription->unsubscribe_token)) {
$subscription->unsubscribe_token = Str::random(64);
}
});
}
}

48
app/Models/User.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Vite::prefetch(concurrency: 3);
}
}

18
artisan Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

View file

@ -1,28 +0,0 @@
# Database
DATABASE_URL=postgresql://docket_user:docket_pass@localhost:5432/docket_db
REDIS_URL=redis://localhost:6379
# Email Configuration (Gmail Smuggler)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=2525
SMTP_SECURE=false
SMTP_USER=system@deafgain.org
SMTP_PASS=ojvlysraxwjriwzy
FROM_EMAIL=system@deafgain.org
RECIPIENT_EMAIL=eliza@deafgain.org
GOOGLE_EMAIL=system@deafgain.org
GOOGLE_APP_PASSWORD=ojvlysraxwjriwzy
# Authentication
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRES_IN=24h
# File Upload
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760
# Environment
NODE_ENV=development
PORT=3001
LOG_LEVEL=info
FRONTEND_URL=https://mad-lawsuit.org

View file

@ -1,50 +0,0 @@
# Use Node.js 22 Alpine image to match package.json engines requirement
FROM node:22-alpine
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache openssl curl python3 make g++
# Install pnpm
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml* ./
# Install all dependencies
RUN pnpm install --frozen-lockfile
# Copy Prisma schema
COPY prisma ./prisma/
# Generate Prisma client
RUN pnpm prisma generate
# Copy source code and build
COPY . .
RUN pnpm build
# Remove dev dependencies to reduce image size
ENV CI=true
RUN pnpm prune --prod
# Generate Prisma client after prune (prisma CLI now available in production deps)
RUN pnpm prisma generate
# Create uploads directory
RUN mkdir -p /app/uploads
# Set environment variables
ENV NODE_ENV=production
ENV PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3001/api/health || exit 1
# Start the application
CMD ["pnpm", "start"]

130
backend/dist/index.js vendored
View file

@ -1,130 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.redis = exports.prisma = void 0;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const helmet_1 = __importDefault(require("helmet"));
const compression_1 = __importDefault(require("compression"));
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
const dotenv_1 = __importDefault(require("dotenv"));
const client_1 = require("@prisma/client");
const adapter_pg_1 = require("@prisma/adapter-pg");
const pg_1 = __importDefault(require("pg"));
const redis_1 = require("redis");
const logger_1 = require("./utils/logger");
const errorHandler_1 = require("./middleware/errorHandler");
const requestLogger_1 = require("./middleware/requestLogger");
const auth_1 = __importDefault(require("./routes/auth"));
const docket_1 = __importDefault(require("./routes/docket"));
const documents_1 = __importDefault(require("./routes/documents"));
const subscriptions_1 = __importDefault(require("./routes/subscriptions"));
const health_1 = __importDefault(require("./routes/health"));
const notifications_1 = __importDefault(require("./routes/notifications"));
dotenv_1.default.config();
const app = (0, express_1.default)();
const PORT = process.env.PORT || 3001;
app.set('trust proxy', true);
const connectionString = process.env.DATABASE_URL;
const pool = new pg_1.default.Pool({ connectionString });
const adapter = new adapter_pg_1.PrismaPg(pool);
exports.prisma = new client_1.PrismaClient({
adapter,
log: ['query', 'info', 'warn', 'error'],
});
exports.redis = (0, redis_1.createClient)({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
exports.redis.connect().catch((error) => {
logger_1.logger.error('Redis connection failed:', error);
});
exports.redis.on('error', (error) => {
logger_1.logger.error('Redis error:', error);
});
exports.redis.on('connect', () => {
logger_1.logger.info('Connected to Redis');
});
app.use((0, helmet_1.default)({
crossOriginResourcePolicy: { policy: "cross-origin" }
}));
app.use((0, cors_1.default)({
origin: process.env.NODE_ENV === 'production'
? [
process.env.FRONTEND_URL,
'https://mad-lawsuit.org',
'https://files.mad-lawsuit.org',
'http://frontend:806',
'http://localhost:806'
]
: [
'http://localhost:3000',
'http://localhost:806',
'http://frontend:806',
'http://frontend:3000',
'https://mad-lawsuit.org',
'https://files.mad-lawsuit.org'
],
credentials: true,
}));
const limiter = (0, express_rate_limit_1.default)({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return false;
},
keyGenerator: (req) => {
return req.ip || 'unknown';
},
});
app.use('/api', limiter);
const authLimiter = (0, express_rate_limit_1.default)({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many authentication attempts, please try again later.',
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
return req.ip || 'unknown';
},
});
app.use('/api/auth', authLimiter);
app.use((0, compression_1.default)());
app.use(express_1.default.json({ limit: '10mb' }));
app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' }));
app.use(requestLogger_1.requestLogger);
app.use('/api/health', health_1.default);
app.use('/api/auth', auth_1.default);
app.use('/api/docket-entries', docket_1.default);
app.use('/api/documents', documents_1.default);
app.use('/api/subscriptions', subscriptions_1.default);
app.use('/api/notifications', notifications_1.default);
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Route not found',
});
});
app.use(errorHandler_1.errorHandler);
process.on('SIGTERM', async () => {
logger_1.logger.info('SIGTERM received, shutting down gracefully');
await exports.prisma.$disconnect();
await exports.redis.quit();
process.exit(0);
});
process.on('SIGINT', async () => {
logger_1.logger.info('SIGINT received, shutting down gracefully');
await exports.prisma.$disconnect();
await exports.redis.quit();
process.exit(0);
});
app.listen(PORT, () => {
logger_1.logger.info(`Server running on port ${PORT}`);
logger_1.logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
exports.default = app;
//# sourceMappingURL=index.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,oDAA4B;AAC5B,8DAAsC;AACtC,4EAA2C;AAC3C,oDAA4B;AAC5B,2CAA8C;AAC9C,mDAA8C;AAC9C,4CAAoB;AACpB,iCAAqC;AACrC,2CAAwC;AACxC,4DAAyD;AACzD,8DAA2D;AAG3D,yDAAuC;AACvC,6DAA2C;AAC3C,mEAAgD;AAChD,2EAAwD;AACxD,6DAA2C;AAC3C,2EAAwD;AAGxD,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAGtC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;AAG7B,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAClD,MAAM,IAAI,GAAG,IAAI,YAAE,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAC/C,MAAM,OAAO,GAAG,IAAI,qBAAQ,CAAC,IAAI,CAAC,CAAC;AAEtB,QAAA,MAAM,GAAG,IAAI,qBAAY,CAAC;IACrC,OAAO;IACP,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC;CACxC,CAAC,CAAC;AAGU,QAAA,KAAK,GAAG,IAAA,oBAAY,EAAC;IAChC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,wBAAwB;CACvD,CAAC,CAAC;AAGH,aAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC9B,eAAM,CAAC,KAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,aAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;IAC1B,eAAM,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC,CAAC,CAAC;AAEH,aAAK,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,eAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AACpC,CAAC,CAAC,CAAC;AAGH,GAAG,CAAC,GAAG,CAAC,IAAA,gBAAM,EAAC;IACb,yBAAyB,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE;CACtD,CAAC,CAAC,CAAC;AAGJ,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,EAAC;IACX,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAC3C,CAAC,CAAC;YACE,OAAO,CAAC,GAAG,CAAC,YAAY;YACxB,yBAAyB;YACzB,+BAA+B;YAC/B,qBAAqB;YACrB,sBAAsB;SACvB;QACH,CAAC,CAAC;YACE,uBAAuB;YACvB,sBAAsB;YACtB,qBAAqB;YACrB,sBAAsB;YACtB,yBAAyB;YACzB,+BAA+B;SAChC;IACL,WAAW,EAAE,IAAI;CAClB,CAAC,CAAC,CAAC;AAGJ,MAAM,OAAO,GAAG,IAAA,4BAAS,EAAC;IACxB,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,GAAG,EAAE,GAAG;IACR,OAAO,EAAE,yDAAyD;IAClE,eAAe,EAAE,IAAI;IACrB,aAAa,EAAE,KAAK;IAEpB,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE;QAEZ,OAAO,KAAK,CAAC;IACf,CAAC;IACD,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE;QAEpB,OAAO,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC;IAC7B,CAAC;CACF,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAGzB,MAAM,WAAW,GAAG,IAAA,4BAAS,EAAC;IAC5B,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACxB,GAAG,EAAE,CAAC;IACN,OAAO,EAAE,2DAA2D;IACpE,eAAe,EAAE,IAAI;IACrB,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE;QACpB,OAAO,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC;IAC7B,CAAC;CACF,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAGlC,GAAG,CAAC,GAAG,CAAC,IAAA,qBAAW,GAAE,CAAC,CAAC;AACvB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AACzC,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;AAG/D,GAAG,CAAC,GAAG,CAAC,6BAAa,CAAC,CAAC;AAGvB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,gBAAY,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,gBAAY,CAAC,CAAC;AAC7C,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,mBAAc,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,oBAAoB,EAAE,uBAAkB,CAAC,CAAC;AAClD,GAAG,CAAC,GAAG,CAAC,oBAAoB,EAAE,uBAAkB,CAAC,CAAC;AAGlD,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IACxB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,iBAAiB;KAC3B,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAGH,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;AAGtB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;IAC/B,eAAM,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IAG1D,MAAM,cAAM,CAAC,WAAW,EAAE,CAAC;IAG3B,MAAM,aAAK,CAAC,IAAI,EAAE,CAAC;IAEnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IAC9B,eAAM,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;IAGzD,MAAM,cAAM,CAAC,WAAW,EAAE,CAAC;IAG3B,MAAM,aAAK,CAAC,IAAI,EAAE,CAAC;IAEnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAGH,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,eAAM,CAAC,IAAI,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;IAC9C,eAAM,CAAC,IAAI,CAAC,gBAAgB,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,EAAE,CAAC,CAAC;AACvE,CAAC,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC"}

View file

@ -1,50 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authenticateToken = void 0;
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const index_1 = require("../index");
const authenticateToken = async (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required',
});
}
try {
const jwtSecret = process.env['JWT_SECRET'];
if (!jwtSecret) {
return res.status(500).json({
success: false,
message: 'Server configuration error',
});
}
const decoded = jsonwebtoken_1.default.verify(token, jwtSecret);
const user = await index_1.prisma.adminUser.findUnique({
where: { id: decoded.userId },
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token - user not found',
});
}
req.user = {
userId: decoded.userId,
username: decoded.username,
};
next();
}
catch (error) {
return res.status(403).json({
success: false,
message: 'Invalid or expired token',
});
}
};
exports.authenticateToken = authenticateToken;
//# sourceMappingURL=auth.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":";;;;;;AACA,gEAA+B;AAC/B,oCAAkC;AAS3B,MAAM,iBAAiB,GAAG,KAAK,EACpC,GAAyB,EACzB,GAAa,EACb,IAAkB,EAClB,EAAE;IACF,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,UAAU,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,uBAAuB;SACjC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC5C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,4BAA4B;aACtC,CAAC,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAQ,CAAC;QAGpD,MAAM,IAAI,GAAG,MAAM,cAAM,CAAC,SAAS,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE;SAC9B,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,gCAAgC;aAC1C,CAAC,CAAC;QACL,CAAC;QAED,GAAG,CAAC,IAAI,GAAG;YACT,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC;QAEF,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,0BAA0B;SACpC,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC;AAlDW,QAAA,iBAAiB,qBAkD5B"}

View file

@ -1,37 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.asyncHandler = exports.createError = exports.errorHandler = void 0;
const logger_1 = require("../utils/logger");
const errorHandler = (error, req, res, next) => {
const statusCode = error.statusCode || 500;
const message = error.message || 'Internal Server Error';
logger_1.logger.error('Error occurred:', {
error: error.message,
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
});
const isDevelopment = process.env.NODE_ENV === 'development';
res.status(statusCode).json({
success: false,
message: isDevelopment ? message : 'Something went wrong',
...(isDevelopment && { stack: error.stack }),
});
};
exports.errorHandler = errorHandler;
const createError = (message, statusCode = 500) => {
const error = new Error(message);
error.statusCode = statusCode;
error.isOperational = true;
return error;
};
exports.createError = createError;
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
exports.asyncHandler = asyncHandler;
//# sourceMappingURL=errorHandler.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":";;;AACA,4CAAyC;AAOlC,MAAM,YAAY,GAAG,CAC1B,KAAe,EACf,GAAY,EACZ,GAAa,EACb,IAAkB,EACZ,EAAE;IACR,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,GAAG,CAAC;IAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,uBAAuB,CAAC;IAGzD,eAAM,CAAC,KAAK,CAAC,iBAAiB,EAAE;QAC9B,KAAK,EAAE,KAAK,CAAC,OAAO;QACpB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;KACjC,CAAC,CAAC;IAGH,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,CAAC;IAE7D,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;QAC1B,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB;QACzD,GAAG,CAAC,aAAa,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KAC7C,CAAC,CAAC;AACL,CAAC,CAAC;AA3BW,QAAA,YAAY,gBA2BvB;AAEK,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,aAAqB,GAAG,EAAY,EAAE;IACjF,MAAM,KAAK,GAAa,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3C,KAAK,CAAC,UAAU,GAAG,UAAU,CAAC;IAC9B,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC;IAC3B,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AALW,QAAA,WAAW,eAKtB;AAEK,MAAM,YAAY,GAAG,CAAC,EAAY,EAAE,EAAE;IAC3C,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC,CAAC;AACJ,CAAC,CAAC;AAJW,QAAA,YAAY,gBAIvB"}

View file

@ -1,28 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requestLogger = void 0;
const logger_1 = require("../utils/logger");
const requestLogger = (req, res, next) => {
const start = Date.now();
logger_1.logger.info('Incoming request', {
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.get('User-Agent'),
});
const originalEnd = res.end;
res.end = function (chunk, encoding) {
const duration = Date.now() - start;
logger_1.logger.info('Request completed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
});
return originalEnd.call(this, chunk, encoding);
};
next();
};
exports.requestLogger = requestLogger;
//# sourceMappingURL=requestLogger.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"requestLogger.js","sourceRoot":"","sources":["../../src/middleware/requestLogger.ts"],"names":[],"mappings":";;;AACA,4CAAyC;AAElC,MAAM,aAAa,GAAG,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;IACrF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAGzB,eAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE;QAC9B,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;KACjC,CAAC,CAAC;IAGH,MAAM,WAAW,GAAG,GAAG,CAAC,GAAG,CAAC;IAC5B,GAAG,CAAC,GAAG,GAAG,UAAS,KAAW,EAAE,QAAc;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QAEpC,eAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE;YAC/B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,QAAQ,EAAE,GAAG,QAAQ,IAAI;YACzB,EAAE,EAAE,GAAG,CAAC,EAAE;SACX,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAQ,CAAC;IAET,IAAI,EAAE,CAAC;AACT,CAAC,CAAC;AA5BW,QAAA,aAAa,iBA4BxB"}

View file

@ -1,85 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const bcryptjs_1 = __importDefault(require("bcryptjs"));
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const index_1 = require("../index");
const errorHandler_1 = require("../middleware/errorHandler");
const auth_1 = require("../middleware/auth");
const joi_1 = __importDefault(require("joi"));
const router = (0, express_1.Router)();
const loginSchema = joi_1.default.object({
username: joi_1.default.string().required(),
password: joi_1.default.string().required(),
});
router.post('/login', (0, errorHandler_1.asyncHandler)(async (req, res) => {
const { error, value } = loginSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const { username, password } = value;
const admin = await index_1.prisma.adminUser.findUnique({
where: { username },
});
if (!admin) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
const isValidPassword = await bcryptjs_1.default.compare(password, admin.passwordHash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
await index_1.prisma.adminUser.update({
where: { id: admin.id },
data: { lastLogin: new Date() },
});
const jwtSecret = process.env['JWT_SECRET'];
const jwtExpiresIn = process.env['JWT_EXPIRES_IN'] || '24h';
if (!jwtSecret) {
return res.status(500).json({
success: false,
message: 'Server configuration error',
});
}
const token = jsonwebtoken_1.default.sign({
userId: admin.id,
username: admin.username
}, jwtSecret, {
expiresIn: jwtExpiresIn
});
res.json({
success: true,
message: 'Login successful',
token,
user: {
id: admin.id,
username: admin.username,
},
});
}));
router.get('/verify', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
res.json({
success: true,
message: 'Token is valid',
user: req.user,
});
}));
router.post('/logout', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
res.json({
success: true,
message: 'Logged out successfully',
});
}));
exports.default = router;
//# sourceMappingURL=auth.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":";;;;;AAAA,qCAAoD;AACpD,wDAA8B;AAC9B,gEAA+B;AAC/B,oCAAkC;AAClC,6DAA0D;AAC1D,6CAA6E;AAC7E,8CAAsB;AAEtB,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAGxB,MAAM,WAAW,GAAG,aAAG,CAAC,MAAM,CAAC;IAC7B,QAAQ,EAAE,aAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,QAAQ,EAAE,aAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAGH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACvE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAExD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;SAClC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAGrC,MAAM,KAAK,GAAG,MAAM,cAAM,CAAC,SAAS,CAAC,UAAU,CAAC;QAC9C,KAAK,EAAE,EAAE,QAAQ,EAAE;KACpB,CAAC,CAAC;IAEH,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,qBAAqB;SAC/B,CAAC,CAAC;IACL,CAAC;IAGD,MAAM,eAAe,GAAG,MAAM,kBAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAE3E,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,qBAAqB;SAC/B,CAAC,CAAC;IACL,CAAC;IAGD,MAAM,cAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAC5B,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE;QACvB,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE;KAChC,CAAC,CAAC;IAGH,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC;IAE5D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,4BAA4B;SACtC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,KAAK,GAAG,sBAAG,CAAC,IAAI,CACpB;QACE,MAAM,EAAE,KAAK,CAAC,EAAE;QAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,EACD,SAAS,EACT;QACE,SAAS,EAAE,YAAY;KACL,CACrB,CAAC;IAEF,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,kBAAkB;QAC3B,KAAK;QACL,IAAI,EAAE;YACJ,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB;KACF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC;AAGJ,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,wBAAiB,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAyB,EAAE,GAAa,EAAE,EAAE;IACvG,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,gBAAgB;QACzB,IAAI,EAAE,GAAG,CAAC,IAAI;KACf,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC;AAGJ,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,wBAAiB,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAyB,EAAE,GAAa,EAAE,EAAE;IACxG,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,yBAAyB;KACnC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC;AAEJ,kBAAe,MAAM,CAAC"}

View file

@ -1,190 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const index_1 = require("../index");
const errorHandler_1 = require("../middleware/errorHandler");
const auth_1 = require("../middleware/auth");
const joi_1 = __importDefault(require("joi"));
const router = (0, express_1.Router)();
function normalizeDate(dateInput) {
if (typeof dateInput === 'string') {
const [year, month, day] = dateInput.split('-');
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
if (dateInput instanceof Date) {
const year = dateInput.getFullYear();
const month = dateInput.getMonth();
const day = dateInput.getDate();
return new Date(year, month, day);
}
return dateInput;
}
const createDocketEntrySchema = joi_1.default.object({
date: joi_1.default.date().required(),
title: joi_1.default.string().required().max(500),
summary: joi_1.default.string().required(),
notes: joi_1.default.string().optional().allow(''),
});
const updateDocketEntrySchema = joi_1.default.object({
date: joi_1.default.date().optional(),
title: joi_1.default.string().optional().max(500),
summary: joi_1.default.string().optional(),
notes: joi_1.default.string().optional().allow(''),
});
router.get('/', (0, errorHandler_1.asyncHandler)(async (req, res) => {
const entries = await index_1.prisma.docketEntry.findMany({
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
orderBy: {
date: 'asc',
},
});
res.json({
success: true,
entries,
});
}));
router.get('/:id', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const entryId = parseInt(req.params.id);
if (isNaN(entryId)) {
return res.status(400).json({
success: false,
message: 'Invalid entry ID',
});
}
const entry = await index_1.prisma.docketEntry.findUnique({
where: { id: entryId },
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
});
if (!entry) {
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
res.json({
success: true,
entry,
});
}));
router.post('/', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const { error, value } = createDocketEntrySchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const normalizedData = { ...value };
if (value.date) {
normalizedData.date = normalizeDate(value.date);
}
const entry = await index_1.prisma.docketEntry.create({
data: normalizedData,
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
});
res.status(201).json({
success: true,
message: 'Docket entry created successfully',
entry,
});
}));
router.put('/:id', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const entryId = parseInt(req.params.id);
if (isNaN(entryId)) {
return res.status(400).json({
success: false,
message: 'Invalid entry ID',
});
}
const { error, value } = updateDocketEntrySchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const normalizedData = { ...value };
if (value.date) {
normalizedData.date = normalizeDate(value.date);
}
try {
const entry = await index_1.prisma.docketEntry.update({
where: { id: entryId },
data: normalizedData,
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
});
res.json({
success: true,
message: 'Docket entry updated successfully',
entry,
});
}
catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
throw error;
}
}));
router.delete('/:id', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const entryId = parseInt(req.params.id);
if (isNaN(entryId)) {
return res.status(400).json({
success: false,
message: 'Invalid entry ID',
});
}
try {
await index_1.prisma.document.deleteMany({
where: { docketEntryId: entryId },
});
await index_1.prisma.docketEntry.delete({
where: { id: entryId },
});
res.json({
success: true,
message: 'Docket entry deleted successfully',
});
}
catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
throw error;
}
}));
exports.default = router;
//# sourceMappingURL=docket.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,231 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const index_1 = require("../index");
const errorHandler_1 = require("../middleware/errorHandler");
const auth_1 = require("../middleware/auth");
const multer_1 = __importDefault(require("multer"));
const uuid_1 = require("uuid");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const joi_1 = __importDefault(require("joi"));
const router = (0, express_1.Router)();
const storage = multer_1.default.diskStorage({
destination: (req, file, cb) => {
const uploadDir = process.env['UPLOAD_DIR'] || './uploads';
if (!fs_1.default.existsSync(uploadDir)) {
fs_1.default.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uuid = (0, uuid_1.v4)();
const extension = path_1.default.extname(file.originalname);
cb(null, `${uuid}${extension}`);
},
});
const upload = (0, multer_1.default)({
storage,
limits: {
fileSize: parseInt(process.env['MAX_FILE_SIZE'] || '262144000'),
},
fileFilter: (req, file, cb) => {
const allowedMimeTypes = [
'application/pdf',
'application/x-pdf',
'application/acrobat',
'applications/vnd.pdf',
'text/pdf',
'text/x-pdf'
];
const isValidPDF = allowedMimeTypes.includes(file.mimetype) ||
file.originalname.toLowerCase().endsWith('.pdf');
console.log('Upload attempt:', {
mimetype: file.mimetype,
filename: file.originalname,
isValid: isValidPDF
});
if (isValidPDF) {
cb(null, true);
}
else {
cb(new Error(`Only PDF files are allowed. Received: ${file.mimetype} for file: ${file.originalname}`));
}
},
});
const createDocumentSchema = joi_1.default.object({
docketEntryId: joi_1.default.number().integer().required(),
title: joi_1.default.string().required().max(500),
summary: joi_1.default.string().optional().allow(''),
notes: joi_1.default.string().optional().allow(''),
displayOrder: joi_1.default.number().integer().min(0).optional(),
});
const updateDocumentSchema = joi_1.default.object({
title: joi_1.default.string().optional().max(500),
summary: joi_1.default.string().optional().allow(''),
notes: joi_1.default.string().optional().allow(''),
displayOrder: joi_1.default.number().integer().min(0).optional(),
});
router.get('/:id/download', (0, errorHandler_1.asyncHandler)(async (req, res) => {
const documentId = parseInt(req.params['id'] || '');
if (isNaN(documentId)) {
return res.status(400).json({
success: false,
message: 'Invalid document ID',
});
}
const document = await index_1.prisma.document.findUnique({
where: { id: documentId },
});
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
const filePath = document.filePath;
if (!fs_1.default.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'File not found on server',
});
}
res.setHeader('Content-Type', document.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${document.originalFilename}"`);
const fileStream = fs_1.default.createReadStream(filePath);
fileStream.pipe(res);
}));
router.post('/upload', auth_1.authenticateToken, upload.single('file'), (0, errorHandler_1.asyncHandler)(async (req, res) => {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded',
});
}
const { error, value } = createDocumentSchema.validate(req.body);
if (error) {
fs_1.default.unlinkSync(req.file.path);
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const { docketEntryId, title, summary, notes, displayOrder } = value;
const docketEntry = await index_1.prisma.docketEntry.findUnique({
where: { id: docketEntryId },
});
if (!docketEntry) {
fs_1.default.unlinkSync(req.file.path);
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
let finalDisplayOrder = displayOrder;
if (finalDisplayOrder === undefined) {
const lastDocument = await index_1.prisma.document.findFirst({
where: { docketEntryId },
orderBy: { displayOrder: 'desc' },
});
finalDisplayOrder = lastDocument ? lastDocument.displayOrder + 1 : 0;
}
const document = await index_1.prisma.document.create({
data: {
docketEntryId,
title,
originalFilename: req.file.originalname,
storedFilename: req.file.filename,
filePath: req.file.path,
fileSize: req.file.size,
mimeType: req.file.mimetype,
summary: summary || '',
notes: notes || '',
displayOrder: finalDisplayOrder,
},
});
res.status(201).json({
success: true,
message: 'Document uploaded successfully',
document,
});
}));
router.put('/:id', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const documentId = parseInt(req.params['id'] || '');
if (isNaN(documentId)) {
return res.status(400).json({
success: false,
message: 'Invalid document ID',
});
}
const { error, value } = updateDocumentSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
try {
const document = await index_1.prisma.document.update({
where: { id: documentId },
data: value,
});
res.json({
success: true,
message: 'Document updated successfully',
document,
});
}
catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
throw error;
}
}));
router.delete('/:id', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const documentId = parseInt(req.params['id'] || '');
if (isNaN(documentId)) {
return res.status(400).json({
success: false,
message: 'Invalid document ID',
});
}
try {
const document = await index_1.prisma.document.findUnique({
where: { id: documentId },
});
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
if (fs_1.default.existsSync(document.filePath)) {
fs_1.default.unlinkSync(document.filePath);
}
await index_1.prisma.document.delete({
where: { id: documentId },
});
res.json({
success: true,
message: 'Document deleted successfully',
});
}
catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
throw error;
}
}));
exports.default = router;
//# sourceMappingURL=documents.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,34 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const index_1 = require("../index");
const index_2 = require("../index");
const router = (0, express_1.Router)();
router.get('/', async (req, res) => {
try {
await index_1.prisma.$queryRaw `SELECT 1`;
await index_2.redis.ping();
res.json({
success: true,
message: 'API is healthy',
timestamp: new Date().toISOString(),
services: {
database: 'connected',
redis: 'connected',
},
});
}
catch (error) {
res.status(503).json({
success: false,
message: 'Service unavailable',
timestamp: new Date().toISOString(),
services: {
database: 'disconnected',
redis: 'disconnected',
},
});
}
});
exports.default = router;
//# sourceMappingURL=health.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/routes/health.ts"],"names":[],"mappings":";;AAAA,qCAAoD;AACpD,oCAAkC;AAClC,oCAAiC;AAEjC,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IACpD,IAAI,CAAC;QAEH,MAAM,cAAM,CAAC,SAAS,CAAA,UAAU,CAAC;QAGjC,MAAM,aAAK,CAAC,IAAI,EAAE,CAAC;QAEnB,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,gBAAgB;YACzB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,QAAQ,EAAE;gBACR,QAAQ,EAAE,WAAW;gBACrB,KAAK,EAAE,WAAW;aACnB;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,qBAAqB;YAC9B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,QAAQ,EAAE;gBACR,QAAQ,EAAE,cAAc;gBACxB,KAAK,EAAE,cAAc;aACtB;SACF,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,104 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const index_1 = require("../index");
const errorHandler_1 = require("../middleware/errorHandler");
const auth_1 = require("../middleware/auth");
const joi_1 = __importDefault(require("joi"));
const uuid_1 = require("uuid");
const router = (0, express_1.Router)();
const subscribeSchema = joi_1.default.object({
email: joi_1.default.string().email().required(),
});
router.post('/subscribe', (0, errorHandler_1.asyncHandler)(async (req, res) => {
const { error, value } = subscribeSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const { email } = value;
const existing = await index_1.prisma.subscription.findUnique({
where: { email },
});
if (existing) {
if (existing.isActive) {
return res.status(400).json({
success: false,
message: 'Email is already subscribed',
});
}
else {
await index_1.prisma.subscription.update({
where: { email },
data: { isActive: true },
});
}
}
else {
await index_1.prisma.subscription.create({
data: {
email,
unsubscribeToken: (0, uuid_1.v4)(),
},
});
}
res.json({
success: true,
message: 'Successfully subscribed to notifications',
});
}));
router.get('/', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const subscribers = await index_1.prisma.subscription.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
isActive: true,
createdAt: true,
},
});
const stats = {
total: subscribers.length,
active: subscribers.filter(sub => sub.isActive).length,
inactive: subscribers.filter(sub => !sub.isActive).length,
};
res.json({
success: true,
subscribers,
stats,
});
}));
router.put('/:id/toggle', auth_1.authenticateToken, (0, errorHandler_1.asyncHandler)(async (req, res) => {
const subscriberId = parseInt(req.params.id);
if (isNaN(subscriberId)) {
return res.status(400).json({
success: false,
message: 'Invalid subscriber ID',
});
}
const subscriber = await index_1.prisma.subscription.findUnique({
where: { id: subscriberId },
});
if (!subscriber) {
return res.status(404).json({
success: false,
message: 'Subscriber not found',
});
}
const updated = await index_1.prisma.subscription.update({
where: { id: subscriberId },
data: { isActive: !subscriber.isActive },
});
res.json({
success: true,
message: `Subscriber ${updated.isActive ? 'activated' : 'deactivated'} successfully`,
subscriber: updated,
});
}));
exports.default = router;
//# sourceMappingURL=subscriptions.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"subscriptions.js","sourceRoot":"","sources":["../../src/routes/subscriptions.ts"],"names":[],"mappings":";;;;;AAAA,qCAAoD;AACpD,oCAAkC;AAClC,6DAA0D;AAC1D,6CAA6E;AAC7E,8CAAsB;AACtB,+BAAoC;AAEpC,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAGxB,MAAM,eAAe,GAAG,aAAG,CAAC,MAAM,CAAC;IACjC,KAAK,EAAE,aAAG,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAGH,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;IAC3E,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAE5D,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;SAClC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IAGxB,MAAM,QAAQ,GAAG,MAAM,cAAM,CAAC,YAAY,CAAC,UAAU,CAAC;QACpD,KAAK,EAAE,EAAE,KAAK,EAAE;KACjB,CAAC,CAAC;IAEH,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACtB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,6BAA6B;aACvC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YAEN,MAAM,cAAM,CAAC,YAAY,CAAC,MAAM,CAAC;gBAC/B,KAAK,EAAE,EAAE,KAAK,EAAE;gBAChB,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aACzB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;SAAM,CAAC;QAEN,MAAM,cAAM,CAAC,YAAY,CAAC,MAAM,CAAC;YAC/B,IAAI,EAAE;gBACJ,KAAK;gBACL,gBAAgB,EAAE,IAAA,SAAM,GAAE;aAC3B;SACF,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,0CAA0C;KACpD,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC;AAGJ,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,wBAAiB,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAyB,EAAE,GAAa,EAAE,EAAE;IACjG,MAAM,WAAW,GAAG,MAAM,cAAM,CAAC,YAAY,CAAC,QAAQ,CAAC;QACrD,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;QAC9B,MAAM,EAAE;YACN,EAAE,EAAE,IAAI;YACR,KAAK,EAAE,IAAI;YACX,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;SAChB;KACF,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG;QACZ,KAAK,EAAE,WAAW,CAAC,MAAM;QACzB,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM;QACtD,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM;KAC1D,CAAC;IAEF,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,WAAW;QACX,KAAK;KACN,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC;AAGJ,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,wBAAiB,EAAE,IAAA,2BAAY,EAAC,KAAK,EAAE,GAAyB,EAAE,GAAa,EAAE,EAAE;IAC3G,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAE7C,IAAI,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;QACxB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,uBAAuB;SACjC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,cAAM,CAAC,YAAY,CAAC,UAAU,CAAC;QACtD,KAAK,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE;KAC5B,CAAC,CAAC;IAEH,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,sBAAsB;SAChC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,cAAM,CAAC,YAAY,CAAC,MAAM,CAAC;QAC/C,KAAK,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE;QAC3B,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE;KACzC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,cAAc,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,eAAe;QACpF,UAAU,EAAE,OAAO;KACpB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC;AAEJ,kBAAe,MAAM,CAAC"}

View file

@ -1,240 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.emailService = exports.EmailService = void 0;
const logger_1 = require("../utils/logger");
const nodemailer_1 = __importDefault(require("nodemailer"));
class EmailService {
static instance;
transporter;
constructor() {
this.transporter = nodemailer_1.default.createTransport({
host: process.env['SMTP_HOST'] || 'smtp.gmail.com',
port: parseInt(process.env['SMTP_PORT'] || '587'),
secure: process.env['SMTP_SECURE'] === 'true',
auth: {
user: process.env['GOOGLE_EMAIL'],
pass: process.env['GOOGLE_APP_PASSWORD'],
},
});
this.verifyConnection();
}
static getInstance() {
if (!EmailService.instance) {
EmailService.instance = new EmailService();
}
return EmailService.instance;
}
async verifyConnection() {
try {
await this.transporter.verify();
logger_1.logger.info('SMTP connection verified successfully');
}
catch (error) {
logger_1.logger.error('SMTP connection failed:', error);
}
}
async sendEmail(options) {
try {
const mailOptions = {
from: {
name: 'Court Docket System',
address: process.env['GOOGLE_EMAIL'] || 'system@deafgain.org',
},
to: options.to,
subject: options.subject,
html: options.html,
};
if (options.bcc && options.bcc.length > 0) {
mailOptions.bcc = options.bcc;
}
const result = await this.transporter.sendMail(mailOptions);
logger_1.logger.info('Email sent successfully:', {
to: options.to,
bcc: options.bcc ? `${options.bcc.length} recipients` : 'none',
subject: options.subject,
messageId: result.messageId,
});
return true;
}
catch (error) {
logger_1.logger.error('Failed to send email:', error);
return false;
}
}
async notifySubscribersOfNewEntry(entryTitle, entryDate, entrySummary) {
try {
const { prisma } = await Promise.resolve().then(() => __importStar(require('../index')));
const subscribers = await prisma.subscription.findMany({
where: { isActive: true },
});
if (subscribers.length === 0) {
logger_1.logger.info('No active subscribers to notify');
return;
}
const subject = `New Court Filing: ${entryTitle}`;
const html = this.generateNotificationEmail(entryTitle, entryDate, entrySummary);
const subscriberEmails = subscribers.map((subscriber) => subscriber.email);
const success = await this.sendEmail({
to: process.env['GOOGLE_EMAIL'] || 'system@deafgain.org',
bcc: subscriberEmails,
subject,
html,
});
if (success) {
logger_1.logger.info(`Email notification sent successfully to ${subscriberEmails.length} subscribers via BCC`);
}
else {
logger_1.logger.error(`Failed to send BCC email notification to ${subscriberEmails.length} subscribers`);
}
}
catch (error) {
logger_1.logger.error('Failed to notify subscribers:', error);
}
}
generateNotificationEmail(title, date, summary) {
const formattedDate = new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Court Filing Notification</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #1f2937;
color: white;
padding: 20px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.content {
background-color: #f9fafb;
padding: 30px;
border-radius: 0 0 8px 8px;
border: 1px solid #e5e7eb;
}
.filing-info {
background-color: white;
padding: 20px;
border-radius: 6px;
margin: 20px 0;
border-left: 4px solid #3b82f6;
}
.date {
color: #6b7280;
font-size: 14px;
margin-bottom: 10px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #1f2937;
margin-bottom: 15px;
}
.summary {
color: #4b5563;
line-height: 1.5;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #6b7280;
text-align: center;
}
.button {
display: inline-block;
background-color: #3b82f6;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="header">
<h1>Eliza Kragh v. Montana Association of the Deaf</h1>
<p>New Court Filing Notification</p>
</div>
<div class="content">
<p>A new filing has been added to the court docket:</p>
<div class="filing-info">
<div class="date">${formattedDate}</div>
<div class="title">${title}</div>
<div class="summary">${summary}</div>
</div>
<p>You can view the complete docket and any associated documents by visiting the <a href="https://mad-lawsuit.org/" style="color: #3b82f6; text-decoration: none;">website</a>:</p>
<a href="https://mad-lawsuit.org/" class="button">
View Court Docket
</a>
<div class="footer">
<p>You are receiving this email because you subscribed to notifications for this case.</p>
<p>This is an automated notification from the court docket system.</p>
<p>To unsubscribe, please send an email to eliza.kragh@gmail.com</p>
</div>
</div>
</body>
</html>
`;
}
}
exports.EmailService = EmailService;
exports.emailService = EmailService.getInstance();
//# sourceMappingURL=emailService.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"emailService.js","sourceRoot":"","sources":["../../src/services/emailService.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,4CAAyC;AACzC,4DAAoC;AASpC,MAAa,YAAY;IACf,MAAM,CAAC,QAAQ,CAAe;IAC9B,WAAW,CAAyB;IAE5C;QAEE,IAAI,CAAC,WAAW,GAAG,oBAAU,CAAC,eAAe,CAAC;YAC5C,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,gBAAgB;YAClD,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;YACjD,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,MAAM;YAC7C,IAAI,EAAE;gBACJ,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;gBACjC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;aACzC;SACF,CAAC,CAAC;QAGH,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAC1B,CAAC;IAEM,MAAM,CAAC,WAAW;QACvB,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;YAC3B,YAAY,CAAC,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;QAC7C,CAAC;QACD,OAAO,YAAY,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAEO,KAAK,CAAC,gBAAgB;QAC5B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;YAChC,eAAM,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,eAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAqB;QACnC,IAAI,CAAC;YACH,MAAM,WAAW,GAAQ;gBACvB,IAAI,EAAE;oBACJ,IAAI,EAAE,qBAAqB;oBAC3B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,qBAAqB;iBAC9D;gBACD,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI,EAAE,OAAO,CAAC,IAAI;aACnB,CAAC;YAGF,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1C,WAAW,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;YAChC,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YAE5D,eAAM,CAAC,IAAI,CAAC,0BAA0B,EAAE;gBACtC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,aAAa,CAAC,CAAC,CAAC,MAAM;gBAC9D,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,SAAS,EAAE,MAAM,CAAC,SAAS;aAC5B,CAAC,CAAC;YAEH,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,eAAM,CAAC,KAAK,CAAC,uBAAuB,EAAE,KAAK,CAAC,CAAC;YAC7C,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,2BAA2B,CAAC,UAAkB,EAAE,SAAiB,EAAE,YAAoB;QAC3F,IAAI,CAAC;YAEH,MAAM,EAAE,MAAM,EAAE,GAAG,wDAAa,UAAU,GAAC,CAAC;YAC5C,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC;gBACrD,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aAC1B,CAAC,CAAC;YAEH,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC7B,eAAM,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;gBAC/C,OAAO;YACT,CAAC;YAED,MAAM,OAAO,GAAG,qBAAqB,UAAU,EAAE,CAAC;YAClD,MAAM,IAAI,GAAG,IAAI,CAAC,yBAAyB,CAAC,UAAU,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;YACjF,MAAM,gBAAgB,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,UAAe,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAGhF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC;gBACnC,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,qBAAqB;gBACxD,GAAG,EAAE,gBAAgB;gBACrB,OAAO;gBACP,IAAI;aACL,CAAC,CAAC;YAEH,IAAI,OAAO,EAAE,CAAC;gBACZ,eAAM,CAAC,IAAI,CAAC,2CAA2C,gBAAgB,CAAC,MAAM,sBAAsB,CAAC,CAAC;YACxG,CAAC;iBAAM,CAAC;gBACN,eAAM,CAAC,KAAK,CAAC,4CAA4C,gBAAgB,CAAC,MAAM,cAAc,CAAC,CAAC;YAClG,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,eAAM,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAEO,yBAAyB,CAAC,KAAa,EAAE,IAAY,EAAE,OAAe;QAC5E,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,OAAO,EAAE;YAC/D,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,MAAM;YACb,GAAG,EAAE,SAAS;SACf,CAAC,CAAC;QAEH,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAgFqB,aAAa;iCACZ,KAAK;mCACH,OAAO;;;;;;;;;;;;;;;;;KAiBrC,CAAC;IACJ,CAAC;CACF;AApND,oCAoNC;AAEY,QAAA,YAAY,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC"}

View file

@ -1,33 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.logger = void 0;
const winston_1 = __importDefault(require("winston"));
const logFormat = winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.errors({ stack: true }), winston_1.default.format.json());
exports.logger = winston_1.default.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: { service: 'docket-api' },
transports: [
new winston_1.default.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880,
maxFiles: 5,
}),
new winston_1.default.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880,
maxFiles: 5,
}),
],
});
if (process.env.NODE_ENV !== 'production') {
exports.logger.add(new winston_1.default.transports.Console({
format: winston_1.default.format.combine(winston_1.default.format.colorize(), winston_1.default.format.simple())
}));
}
exports.default = exports.logger;
//# sourceMappingURL=logger.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":";;;;;;AAAA,sDAA8B;AAE9B,MAAM,SAAS,GAAG,iBAAO,CAAC,MAAM,CAAC,OAAO,CACtC,iBAAO,CAAC,MAAM,CAAC,SAAS,EAAE,EAC1B,iBAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EACtC,iBAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CACtB,CAAC;AAEW,QAAA,MAAM,GAAG,iBAAO,CAAC,YAAY,CAAC;IACzC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;IACtC,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE;IACtC,UAAU,EAAE;QACV,IAAI,iBAAO,CAAC,UAAU,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,gBAAgB;YAC1B,KAAK,EAAE,OAAO;YACd,OAAO,EAAE,OAAO;YAChB,QAAQ,EAAE,CAAC;SACZ,CAAC;QACF,IAAI,iBAAO,CAAC,UAAU,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,mBAAmB;YAC7B,OAAO,EAAE,OAAO;YAChB,QAAQ,EAAE,CAAC;SACZ,CAAC;KACH;CACF,CAAC,CAAC;AAGH,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IAC1C,cAAM,CAAC,GAAG,CAAC,IAAI,iBAAO,CAAC,UAAU,CAAC,OAAO,CAAC;QACxC,MAAM,EAAE,iBAAO,CAAC,MAAM,CAAC,OAAO,CAC5B,iBAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,EACzB,iBAAO,CAAC,MAAM,CAAC,MAAM,EAAE,CACxB;KACF,CAAC,CAAC,CAAC;AACN,CAAC;AAED,kBAAe,cAAM,CAAC"}

View file

@ -1,809 +0,0 @@
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:13:09.977Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:13:09.980Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:13:09.984Z"}
{"ip":"::ffff:127.0.0.1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:13:57.178Z","url":"/","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"}
{"duration":"2ms","ip":"::ffff:127.0.0.1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:13:57.180Z","url":"/"}
{"ip":"::ffff:127.0.0.1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:13:57.204Z","url":"/favicon.ico","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"}
{"duration":"1ms","ip":"::ffff:127.0.0.1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:13:57.205Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:14:07.942Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:14:07.944Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:15:56.009Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:15:56.010Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:15:56.012Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:20:19.862Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:20:19.863Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:20:19.866Z"}
{"ip":"::ffff:127.0.0.1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:21:02.794Z","url":"/","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::ffff:127.0.0.1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:21:02.796Z","url":"/"}
{"ip":"::ffff:127.0.0.1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:21:02.815Z","url":"/favicon.ico","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"1ms","ip":"::ffff:127.0.0.1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:21:02.816Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:21:14.796Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:21:14.799Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:21:25.211Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:21:25.212Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:21:25.214Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:21:37.049Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:21:37.066Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:24:18.294Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:24:18.295Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:28:23.634Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:28:23.636Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:28:23.637Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:30:45.172Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:30:45.174Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:30:45.175Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:31:04.950Z","url":"/api/docket","userAgent":"curl/8.7.1"}
{"duration":"0ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:31:04.950Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:31:07.099Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"19ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:31:07.119Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:31:14.621Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:31:14.625Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T21:31:23.756Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"249ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:31:24.004Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T21:31:35.716Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"0ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:31:35.716Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T21:31:54.377Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T21:31:54.799Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T21:31:54.801Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T21:31:54.802Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T21:32:04.251Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"21ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:32:04.272Z","url":"/subscribe"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:36:37.203Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:36:37.209Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:36:44.997Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":404,"timestamp":"2025-06-24T21:36:44.997Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T21:36:52.430Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":400,"timestamp":"2025-06-24T21:36:52.434Z","url":"/subscribe"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:43:55.707Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T21:43:55.725Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T21:44:26.328Z","url":"/api/auth/login","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"266ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T21:44:26.593Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:44:26.803Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T21:44:26.806Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:44:26.809Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T21:44:26.812Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T21:45:50.519Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T21:45:50.523Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:41:20.733Z","url":"/api/auth/login","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"268ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:41:21.001Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:41:21.120Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T22:41:21.127Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:41:21.130Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:41:21.132Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:43:07.980Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}
{"error":"Only PDF files are allowed","ip":"::1","level":"error","message":"Error occurred:","method":"POST","service":"docket-api","stack":"Error: Only PDF files are allowed\n at fileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/src/routes/documents.ts:39:10)\n at wrappedFileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/index.js:44:7)\n at Multipart.<anonymous> (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/lib/make-middleware.js:109:7)\n at Multipart.emit (node:events:507:28)\n at HeaderParser.cb (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:358:14)\n at HeaderParser.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:162:20)\n at SBMH.ssCb [as _cb] (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:394:37)\n at feed (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:219:14)\n at SBMH.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:104:16)\n at Multipart._write (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:567:19)","timestamp":"2025-06-24T22:43:07.987Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":500,"timestamp":"2025-06-24T22:43:07.987Z","url":"/api/documents/upload"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T22:44:10.109Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T22:44:10.520Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T22:44:10.522Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T22:44:10.524Z"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T22:44:23.926Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T22:44:24.304Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T22:44:24.306Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T22:44:24.308Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:44:33.165Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"250ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:44:33.415Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:44:33.425Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"html":"\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"...","level":"info","message":"Email would be sent:","service":"docket-api","subject":"New Court Filing: Discovery Motion Filed","timestamp":"2025-06-24T22:44:33.447Z","to":"test@example.com"}
{"level":"info","message":"Email notifications sent: 1 successful, 0 failed","service":"docket-api","timestamp":"2025-06-24T22:44:33.548Z"}
{"duration":"123ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":201,"timestamp":"2025-06-24T22:44:33.548Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:45:35.068Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:45:35.075Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:45:38.010Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T22:45:38.014Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:47:42.071Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:42.076Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:47:42.084Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:42.087Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:42.099Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:42.105Z","url":"/subscribe"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:42.112Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":400,"timestamp":"2025-06-24T22:47:42.114Z","url":"/subscribe"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:42.122Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"238ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:42.360Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:47:42.374Z","url":"/api/auth/verify","userAgent":"curl/8.7.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:42.376Z","url":"/verify"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:47:42.385Z","url":"/api/docket-entries/1","userAgent":"curl/8.7.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:42.391Z","url":"/1"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:42.402Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"html":"\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"...","level":"info","message":"Email would be sent:","service":"docket-api","subject":"New Court Filing: Automated Test Entry - 17:47:42","timestamp":"2025-06-24T22:47:42.412Z","to":"test@example.com"}
{"html":"\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"...","level":"info","message":"Email would be sent:","service":"docket-api","subject":"New Court Filing: Automated Test Entry - 17:47:42","timestamp":"2025-06-24T22:47:42.412Z","to":"test1750805262@example.com"}
{"level":"info","message":"Email notifications sent: 2 successful, 0 failed","service":"docket-api","timestamp":"2025-06-24T22:47:42.513Z"}
{"duration":"112ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":201,"timestamp":"2025-06-24T22:47:42.514Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:43.537Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}
{"duration":"0ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":401,"timestamp":"2025-06-24T22:47:43.537Z","url":"/upload"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:43.550Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}
{"error":"Only PDF files are allowed","ip":"::1","level":"error","message":"Error occurred:","method":"POST","service":"docket-api","stack":"Error: Only PDF files are allowed\n at fileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/src/routes/documents.ts:39:10)\n at wrappedFileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/index.js:44:7)\n at Multipart.<anonymous> (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/lib/make-middleware.js:109:7)\n at Multipart.emit (node:events:507:28)\n at HeaderParser.cb (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:358:14)\n at HeaderParser.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:162:20)\n at SBMH.ssCb [as _cb] (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:394:37)\n at feed (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:219:14)\n at SBMH.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:104:16)\n at Multipart._write (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:567:19)","timestamp":"2025-06-24T22:47:43.556Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":500,"timestamp":"2025-06-24T22:47:43.556Z","url":"/api/documents/upload"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:47:43.570Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"231ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":401,"timestamp":"2025-06-24T22:47:43.801Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:47:43.809Z","url":"/api/auth/verify","userAgent":"curl/8.7.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":403,"timestamp":"2025-06-24T22:47:43.810Z","url":"/verify"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:47:43.817Z","url":"/api/health","userAgent":"curl/8.7.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:47:43.822Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-24T22:49:20.164Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-24T22:49:32.273Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-24T22:49:32.275Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-24T22:49:32.278Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-24T22:49:32.777Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:49:42.212Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"248ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:49:42.460Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:49:42.470Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"level":"info","message":"Email sent successfully:","messageId":"<ae446ddd-3cdd-afc2-3b7e-5cd4819c08ae@deafgain.org>","service":"docket-api","subject":"New Court Filing: Real Email Test - Settlement Conference Scheduled","timestamp":"2025-06-24T22:49:43.978Z","to":"test1750805262@example.com"}
{"level":"info","message":"Email sent successfully:","messageId":"<e570d07d-b26e-2726-8924-e383604cf0b4@deafgain.org>","service":"docket-api","subject":"New Court Filing: Real Email Test - Settlement Conference Scheduled","timestamp":"2025-06-24T22:49:44.022Z","to":"test@example.com"}
{"level":"info","message":"Email notifications sent: 2 successful, 0 failed","service":"docket-api","timestamp":"2025-06-24T22:49:44.024Z"}
{"duration":"1555ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":201,"timestamp":"2025-06-24T22:49:44.025Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:51:06.241Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:51:06.247Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:51:50.975Z","url":"/api/subscriptions/subscribe","userAgent":"curl/8.7.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:51:50.982Z","url":"/subscribe"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:52:01.178Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"229ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:52:01.407Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-24T22:52:01.415Z","url":"/api/docket-entries","userAgent":"curl/8.7.1"}
{"level":"info","message":"Email sent successfully:","messageId":"<2d07ac41-147a-1249-5fde-395dd03b18f5@deafgain.org>","service":"docket-api","subject":"New Court Filing: Email Test for Chris - Motion for Summary Judgment Filed","timestamp":"2025-06-24T22:52:02.630Z","to":"test@example.com"}
{"level":"info","message":"Email sent successfully:","messageId":"<87a974f7-0735-ef00-8f83-bae865916422@deafgain.org>","service":"docket-api","subject":"New Court Filing: Email Test for Chris - Motion for Summary Judgment Filed","timestamp":"2025-06-24T22:52:02.631Z","to":"test1750805262@example.com"}
{"level":"info","message":"Email sent successfully:","messageId":"<3e6a094c-9cb0-0e95-b5cd-9de97df11e33@deafgain.org>","service":"docket-api","subject":"New Court Filing: Email Test for Chris - Motion for Summary Judgment Filed","timestamp":"2025-06-24T22:52:04.474Z","to":"chris@sigd.net"}
{"level":"info","message":"Email notifications sent: 3 successful, 0 failed","service":"docket-api","timestamp":"2025-06-24T22:52:04.475Z"}
{"duration":"3061ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":201,"timestamp":"2025-06-24T22:52:04.476Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T22:58:48.528Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"19ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-24T22:58:48.547Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-24T23:01:13.428Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-24T23:01:13.432Z","url":"/"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T13:38:31.483Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T13:38:31.485Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T13:38:31.489Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T13:38:31.965Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T13:38:50.881Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"21ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T13:38:50.902Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:00:26.370Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"12ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:00:26.382Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:01:04.654Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:01:04.658Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:06:46.367Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"17ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:06:46.384Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T14:06:52.974Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T14:06:53.436Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T14:06:53.437Z"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T14:06:53.439Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T14:06:53.440Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T14:07:10.448Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T14:07:10.449Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T14:07:10.452Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T14:07:10.927Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:07:14.281Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"19ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:07:14.300Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:09:11.796Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:09:11.801Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:16:36.010Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"17ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:16:36.027Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:19:59.753Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:19:59.756Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:20:26.607Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:20:26.612Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:21:36.853Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"17ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:21:36.870Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:29:05.803Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:29:05.821Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:29:22.453Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:29:22.458Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:32:10.797Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:32:10.801Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T14:38:43.507Z","url":"/api/auth/login","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"266ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T14:38:43.773Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:38:44.159Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:38:44.165Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:38:44.167Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T14:38:44.170Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:39:39.010Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:39:39.014Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:40:55.724Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:40:55.729Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:41:42.699Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:41:42.702Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:42:29.563Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:42:29.567Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:42:42.320Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:42:42.323Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:42:42.326Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:42:42.328Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:42:49.326Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:42:49.330Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:42:49.333Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T14:42:49.336Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:44:45.354Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:44:45.362Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"24ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:44:45.378Z","url":"/"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T14:44:45.380Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:49:49.872Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"16ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:49:49.888Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:50:39.092Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:50:39.096Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:50:39.101Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T14:50:39.103Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T14:56:27.090Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"17ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T14:56:27.107Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:00:01.999Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:00:02.003Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:00:17.453Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:00:17.457Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:00:27.417Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:00:27.420Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:00:27.422Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:00:27.425Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:00:29.988Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:00:29.991Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:00:29.994Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:00:29.997Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T15:00:53.837Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T15:00:53.837Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T15:01:10.606Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T15:01:10.607Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T15:01:10.609Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T15:01:11.462Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:15.919Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"19ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:15.938Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:19.275Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:19.279Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:19.284Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:01:19.288Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:20.890Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:20.893Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:20.897Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:20.900Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:22.797Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:22.800Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:22.805Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:22.807Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:24.249Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:24.253Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:01:24.258Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:01:24.261Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:55:43.733Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:55:43.751Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:55:54.282Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:55:54.285Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:55:54.289Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:55:54.291Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:55:56.034Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:55:56.037Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:55:56.040Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:55:56.043Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"PUT","service":"docket-api","timestamp":"2025-06-25T15:56:15.070Z","url":"/api/docket-entries/8","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"14ms","ip":"::1","level":"info","message":"Request completed","method":"PUT","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:56:15.084Z","url":"/8"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:56:15.089Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:56:15.091Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:56:32.552Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:56:32.554Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"PUT","service":"docket-api","timestamp":"2025-06-25T15:57:01.882Z","url":"/api/docket-entries/8","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"15ms","ip":"::1","level":"info","message":"Request completed","method":"PUT","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:57:01.897Z","url":"/8"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:57:01.906Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:57:01.909Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:57:06.435Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:57:06.438Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"PUT","service":"docket-api","timestamp":"2025-06-25T15:57:14.812Z","url":"/api/docket-entries/8","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"10ms","ip":"::1","level":"info","message":"Request completed","method":"PUT","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:57:14.822Z","url":"/8"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:57:14.828Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T15:57:14.831Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:57:18.727Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:57:18.731Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:57:26.740Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:57:26.743Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T15:57:26.747Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T15:57:26.749Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T15:58:15.130Z","url":"/api/documents/upload","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T15:58:52.409Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T15:58:52.410Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T15:58:52.413Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T15:58:52.891Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T15:59:55.718Z","url":"/api/documents/upload","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T16:00:54.943Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T16:00:54.945Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T16:00:54.947Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T16:00:55.507Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T16:01:00.397Z","url":"/api/documents/upload","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"42ms","ip":"::1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":201,"timestamp":"2025-06-25T16:01:00.439Z","url":"/upload"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:01:07.043Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:01:07.049Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:01:16.880Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"12ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:01:16.892Z","url":"/1/download"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:01:45.528Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:01:45.532Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T16:10:56.757Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T16:10:56.758Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T16:10:56.760Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T16:10:57.292Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:11:12.710Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:11:12.713Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T16:12:30.150Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T16:12:30.151Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T16:12:30.153Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T16:12:30.611Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:12:44.316Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"}
{"duration":"19ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:12:44.335Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:12:50.408Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:12:50.412Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:12:54.677Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"10ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:12:54.687Z","url":"/1/download"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:13:14.780Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:13:14.785Z","url":"/1/download"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:13:34.059Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:13:34.065Z","url":"/1/download"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:14:03.596Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:14:03.600Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:14:03.604Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:14:03.607Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"PUT","service":"docket-api","timestamp":"2025-06-25T16:14:19.020Z","url":"/api/docket-entries/8","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"PUT","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:14:19.038Z","url":"/8"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:14:19.047Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:14:19.050Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:14:22.721Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:14:22.724Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"PUT","service":"docket-api","timestamp":"2025-06-25T16:14:38.542Z","url":"/api/docket-entries/8","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"12ms","ip":"::1","level":"info","message":"Request completed","method":"PUT","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:14:38.554Z","url":"/8"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:14:38.562Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:14:38.565Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:14:41.134Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:14:41.137Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:18:12.936Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"23ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:18:12.959Z","url":"/1/download"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:18:25.293Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:18:25.296Z","url":"/1/download"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:18:41.211Z","url":"/api/documents/1/download","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:18:41.216Z","url":"/1/download"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:25:03.885Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:25:03.903Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:25:07.631Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:25:07.635Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:25:35.042Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:25:35.046Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"PUT","service":"docket-api","timestamp":"2025-06-25T16:27:04.052Z","url":"/api/docket-entries/8","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"12ms","ip":"::1","level":"info","message":"Request completed","method":"PUT","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:27:04.064Z","url":"/8"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:27:04.071Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:27:04.074Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:27:07.884Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:27:07.887Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:30:38.837Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"17ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:30:38.854Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:32:16.783Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:32:16.788Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:34:58.611Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:34:58.614Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:35:48.523Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:35:48.524Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"22ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:35:48.545Z","url":"/"}
{"duration":"21ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:35:48.545Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:36:40.857Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:36:40.861Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:36:40.864Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:36:40.866Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:37:58.921Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:37:58.924Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:37:58.928Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:37:58.931Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:38:04.598Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:38:04.602Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:38:04.605Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:38:04.607Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:38:06.105Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:38:06.108Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:38:06.113Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:38:06.115Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:39:38.668Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:39:38.675Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:39:38.677Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:39:38.680Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:39:40.218Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:39:40.223Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:39:41.858Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:39:41.861Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:39:41.868Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:39:41.870Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:08.199Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:08.204Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:08.207Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:40:08.210Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:38.268Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:38.272Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:39.922Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:39.923Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:39.926Z","url":"/"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:40:39.927Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:44.249Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:44.252Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:44.255Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:44.257Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:48.683Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:48.693Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"29ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:48.713Z","url":"/"}
{"duration":"20ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:40:48.713Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:50.184Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:50.188Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:40:50.191Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:40:50.194Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:41:00.830Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:41:00.833Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:44:17.842Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:44:17.846Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:44:19.428Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:44:19.432Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:44:19.435Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:44:19.437Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:44:22.130Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:44:22.134Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:44:22.138Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:44:22.141Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:45:18.485Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:45:18.489Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:45:18.497Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:45:18.500Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:45:21.632Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:45:21.635Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:45:21.639Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:45:21.641Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:51:08.564Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"16ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:51:08.580Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:51:30.090Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:51:30.092Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T16:51:31.610Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T16:51:31.612Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T16:51:31.614Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T16:51:32.064Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:51:40.787Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"19ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:51:40.806Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:52:11.625Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:52:11.630Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:53:15.847Z"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:53:15.849Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T16:53:24.138Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T16:53:24.139Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T16:53:24.141Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T16:53:24.600Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:53:27.326Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:53:27.345Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:53:36.575Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T16:53:36.579Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:56:11.828Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:56:11.832Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:56:27.948Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:56:27.952Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T16:57:40.062Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T16:57:40.066Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T16:58:15.851Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T16:58:16.348Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T16:58:16.349Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T16:58:16.352Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T16:58:16.926Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T17:00:36.490Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T17:00:36.491Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T17:00:36.493Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T17:00:38.052Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:01:08.516Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"18ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T17:01:08.534Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:01:59.122Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:01:59.126Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:02:23.225Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:02:23.230Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:02:23.236Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T17:02:23.239Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:02:26.892Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:02:26.896Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:02:26.902Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T17:02:26.905Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:09:39.693Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"14ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:09:39.707Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:11:13.511Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:11:13.516Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:11:25.325Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:11:25.329Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:11:25.332Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:11:25.335Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:11:28.796Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:11:28.801Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:11:28.804Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:11:28.808Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:15:43.200Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"17ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:15:43.217Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:16:08.337Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:16:08.341Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T17:17:58.237Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T17:17:58.639Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T17:17:58.640Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T17:17:58.643Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T17:17:59.241Z"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T17:18:52.571Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T17:18:52.961Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T17:18:52.963Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T17:18:52.965Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T17:18:53.437Z"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T17:19:09.018Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T17:19:09.401Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T17:19:09.402Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T17:19:09.405Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T17:19:09.897Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:20:39.310Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"22ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:20:39.331Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:20:41.966Z","url":"/api/subscriptions","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"8ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T17:20:41.974Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:20:41.977Z","url":"/api/subscriptions","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T17:20:41.981Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:21:15.553Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T17:21:15.553Z","url":"/api/docket-entries","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":304,"timestamp":"2025-06-25T17:21:15.557Z","url":"/"}
{"duration":"16ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T17:21:15.569Z","url":"/"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T17:31:17.501Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T17:45:56.359Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T17:45:56.360Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T17:45:56.366Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T17:47:42.726Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T17:47:42.727Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T17:47:42.734Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T18:04:15.731Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T18:04:15.734Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T18:04:15.751Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T18:04:16.426Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:04:18.874Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"121ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:04:18.995Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:04:49.082Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:04:49.089Z","url":"/"}
{"ip":"::ffff:172.18.0.1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:05:01.969Z","url":"/api/health","userAgent":"curl/8.7.1"}
{"duration":"10ms","ip":"::ffff:172.18.0.1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:05:01.978Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:05:19.158Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:05:19.163Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:05:49.240Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:05:49.245Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:06:19.334Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:06:19.341Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:06:49.431Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"8ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:06:49.439Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:07:19.528Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:07:19.535Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:07:49.628Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"8ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:07:49.637Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:08:19.720Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:08:19.726Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:08:49.821Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:08:49.825Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:09:19.913Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"41ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:09:19.954Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:09:50.020Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:09:50.024Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:10:20.087Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:10:20.092Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:10:50.143Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:10:50.148Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:11:20.225Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:11:20.231Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:11:50.322Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:11:50.328Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:12:20.401Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:12:20.408Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:12:50.466Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:12:50.471Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:13:20.568Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:13:20.574Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:13:50.669Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:13:50.675Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:14:20.753Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"46ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:14:20.799Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:14:50.887Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:14:50.891Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:15:20.997Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:15:21.001Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:15:51.087Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:15:51.089Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:16:21.182Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:16:21.189Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:16:51.279Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:16:51.282Z","url":"/"}
{"ip":"::ffff:172.18.0.1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:16:53.682Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"3943ms","ip":"::ffff:172.18.0.1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:16:57.626Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:17:21.368Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:17:21.372Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:17:51.429Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:17:51.431Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:18:21.494Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:18:21.496Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:18:51.589Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:18:51.591Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:19:21.684Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"39ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:19:21.723Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:19:51.814Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:19:51.817Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:20:21.896Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:20:21.897Z","url":"/"}
{"ip":"::ffff:172.18.0.1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:20:36.608Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"3889ms","ip":"::ffff:172.18.0.1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:20:40.497Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:20:51.979Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:20:51.981Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:21:22.051Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:21:22.053Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:21:52.115Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:21:52.117Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:22:22.202Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:22:22.204Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:22:52.320Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:22:52.322Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:23:22.390Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:23:22.392Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:23:52.474Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:23:52.475Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:24:22.566Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"40ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:24:22.606Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:24:52.696Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:24:52.699Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:25:22.789Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:25:22.792Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:25:52.888Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:25:52.890Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:26:22.971Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:26:22.973Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:26:53.064Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:26:53.066Z","url":"/"}
{"ip":"::ffff:172.18.0.1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:27:17.986Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"duration":"3908ms","ip":"::ffff:172.18.0.1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:27:21.894Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:27:23.157Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:27:23.159Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:27:53.254Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:27:53.257Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:28:23.322Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:28:23.323Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:28:53.429Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:28:53.431Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:29:23.501Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"37ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:29:23.538Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:29:53.622Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:29:53.625Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:30:23.709Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:30:23.711Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:30:53.799Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:30:53.801Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:31:23.884Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:31:23.886Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:31:53.967Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:31:53.969Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:32:24.053Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:32:24.055Z","url":"/"}
{"ip":"::ffff:172.18.0.5","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:32:41.817Z","url":"/api/auth/login"}
{"duration":"3933ms","ip":"::ffff:172.18.0.5","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:32:45.750Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:32:54.142Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:32:54.143Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:33:24.203Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:33:24.206Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:33:54.285Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:33:54.287Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:34:24.358Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"36ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:34:24.394Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:34:54.485Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:34:54.488Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:35:24.553Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:35:24.554Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:35:54.623Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:35:54.625Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:36:24.688Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:36:24.690Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:36:54.778Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:36:54.781Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:37:24.870Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:37:24.873Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:37:54.966Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:37:54.968Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:38:25.064Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:38:25.066Z","url":"/"}
{"ip":"::ffff:172.18.0.5","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:38:50.489Z","url":"/api/auth/login","userAgent":"curl/8.14.1"}
{"duration":"3949ms","ip":"::ffff:172.18.0.5","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:38:54.438Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:38:55.165Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:38:55.167Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:39:25.255Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"39ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:39:25.294Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:39:55.387Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:39:55.389Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:40:25.477Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:40:25.480Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:40:55.555Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:40:55.557Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:41:25.646Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:41:25.648Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:41:55.720Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:41:55.723Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:42:25.806Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:42:25.807Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:42:55.895Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:42:55.896Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:43:25.990Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:43:25.993Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:43:56.084Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:43:56.086Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:44:26.148Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"34ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:44:26.182Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:44:56.251Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:44:56.254Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:45:26.322Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:45:26.324Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:45:56.418Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:45:56.420Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:46:26.513Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:46:26.514Z","url":"/"}
{"ip":"::ffff:172.18.0.5","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:46:38.766Z","url":"/api/auth/login","userAgent":"node"}
{"duration":"3924ms","ip":"::ffff:172.18.0.5","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:46:42.690Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:46:56.586Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:46:56.588Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:47:26.677Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"12ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:47:26.689Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:47:56.755Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:47:56.758Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:48:26.824Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:48:26.826Z","url":"/"}
{"ip":"::ffff:172.18.0.1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:48:55.788Z","url":"/api/auth/login","userAgent":"curl/8.7.1"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:48:57.020Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"308ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:48:57.329Z","url":"/"}
{"duration":"3958ms","ip":"::ffff:172.18.0.1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:48:59.746Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:49:27.422Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"46ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:49:27.468Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:49:57.539Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:49:57.542Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:50:27.623Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:50:27.625Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:50:57.714Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:50:57.718Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:51:27.808Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:51:27.809Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:51:57.862Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"1ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:51:57.863Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:52:27.952Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:52:27.954Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:52:58.038Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:52:58.040Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:53:28.130Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:53:28.132Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:53:58.224Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:53:58.226Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:54:28.315Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"37ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:54:28.353Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:54:58.434Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"3ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:54:58.437Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:55:28.521Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"2ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:55:28.523Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T18:55:47.633Z"}
{"level":"info","message":"Server running on port 3001","service":"docket-api","timestamp":"2025-06-25T18:56:03.229Z"}
{"level":"info","message":"Environment: development","service":"docket-api","timestamp":"2025-06-25T18:56:03.232Z"}
{"level":"info","message":"Connected to Redis","service":"docket-api","timestamp":"2025-06-25T18:56:03.250Z"}
{"level":"info","message":"SMTP connection verified successfully","service":"docket-api","timestamp":"2025-06-25T18:56:03.817Z"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:56:06.438Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"121ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:56:06.559Z","url":"/"}
{"ip":"::ffff:172.19.0.5","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:56:25.990Z","url":"/api/health"}
{"duration":"6ms","ip":"::ffff:172.19.0.5","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:56:25.995Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:56:36.639Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"8ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:56:36.647Z","url":"/"}
{"ip":"::ffff:172.19.0.5","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:56:44.561Z","url":"/api/auth/login"}
{"duration":"3900ms","ip":"::ffff:172.19.0.5","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:56:48.461Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:57:06.716Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:57:06.722Z","url":"/"}
{"ip":"::ffff:172.19.0.1","level":"info","message":"Incoming request","method":"POST","service":"docket-api","timestamp":"2025-06-25T18:57:22.523Z","url":"/api/auth/login","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"}
{"duration":"3981ms","ip":"::ffff:172.19.0.1","level":"info","message":"Request completed","method":"POST","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:57:26.505Z","url":"/login"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:57:36.810Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"13ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:57:36.824Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:58:06.907Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:58:06.912Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:58:37.002Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:58:37.005Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:59:07.101Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"4ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:59:07.106Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T18:59:37.200Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"5ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T18:59:37.205Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:00:07.300Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:00:07.306Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:00:37.403Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:00:37.409Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:01:07.498Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"45ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:01:07.542Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:01:37.603Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:01:37.609Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:02:07.688Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"7ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:02:07.696Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:02:37.793Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"8ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:02:37.801Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:03:07.892Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"6ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:03:07.898Z","url":"/"}
{"ip":"::1","level":"info","message":"Incoming request","method":"GET","service":"docket-api","timestamp":"2025-06-25T19:03:37.989Z","url":"/api/health","userAgent":"curl/8.12.1"}
{"duration":"8ms","ip":"::1","level":"info","message":"Request completed","method":"GET","service":"docket-api","statusCode":200,"timestamp":"2025-06-25T19:03:37.996Z","url":"/"}
{"level":"info","message":"SIGTERM received, shutting down gracefully","service":"docket-api","timestamp":"2025-06-25T19:03:54.773Z"}

View file

@ -1,2 +0,0 @@
{"error":"Only PDF files are allowed","ip":"::1","level":"error","message":"Error occurred:","method":"POST","service":"docket-api","stack":"Error: Only PDF files are allowed\n at fileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/src/routes/documents.ts:39:10)\n at wrappedFileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/index.js:44:7)\n at Multipart.<anonymous> (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/lib/make-middleware.js:109:7)\n at Multipart.emit (node:events:507:28)\n at HeaderParser.cb (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:358:14)\n at HeaderParser.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:162:20)\n at SBMH.ssCb [as _cb] (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:394:37)\n at feed (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:219:14)\n at SBMH.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:104:16)\n at Multipart._write (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:567:19)","timestamp":"2025-06-24T22:43:07.987Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}
{"error":"Only PDF files are allowed","ip":"::1","level":"error","message":"Error occurred:","method":"POST","service":"docket-api","stack":"Error: Only PDF files are allowed\n at fileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/src/routes/documents.ts:39:10)\n at wrappedFileFilter (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/index.js:44:7)\n at Multipart.<anonymous> (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/multer@1.4.5-lts.2/node_modules/multer/lib/make-middleware.js:109:7)\n at Multipart.emit (node:events:507:28)\n at HeaderParser.cb (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:358:14)\n at HeaderParser.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:162:20)\n at SBMH.ssCb [as _cb] (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:394:37)\n at feed (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:219:14)\n at SBMH.push (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/streamsearch@1.1.0/node_modules/streamsearch/lib/sbmh.js:104:16)\n at Multipart._write (/Users/chaulmark/Eliza-MAD-docket-website/backend/node_modules/.pnpm/busboy@1.6.0/node_modules/busboy/lib/types/multipart.js:567:19)","timestamp":"2025-06-24T22:47:43.556Z","url":"/api/documents/upload","userAgent":"curl/8.7.1"}

View file

@ -1,60 +0,0 @@
{
"name": "eliza-mad-docket-backend",
"version": "1.0.0",
"description": "Backend API for Eliza Kragh v. MAD court docket website",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:studio": "prisma studio",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix"
},
"dependencies": {
"@prisma/adapter-pg": "^7.1.0",
"@prisma/client": "^7.1.0",
"bcryptjs": "^3.0.3",
"compression": "^1.8.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"helmet": "^8.1.0",
"joi": "^18.0.2",
"jsonwebtoken": "^9.0.3",
"multer": "^2.0.2",
"nodemailer": "^7.0.11",
"pg": "^8.16.3",
"prisma": "^7.1.0",
"redis": "^5.10.0",
"uuid": "^13.0.0",
"winston": "^3.19.0"
},
"devDependencies": {
"@types/bcryptjs": "^3.0.0",
"@types/compression": "^1.8.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^25.0.2",
"@types/nodemailer": "^7.0.4",
"@types/uuid": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"eslint": "^9.39.2",
"jest": "^30.2.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22.0.0"
}
}

6809
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +0,0 @@
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});

View file

@ -1,80 +0,0 @@
-- CreateTable
CREATE TABLE "docket_entries" (
"id" SERIAL NOT NULL,
"date" DATE NOT NULL,
"document_type" VARCHAR(100) NOT NULL,
"summary" TEXT NOT NULL,
"court_notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "docket_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "documents" (
"id" SERIAL NOT NULL,
"docket_entry_id" INTEGER NOT NULL,
"original_filename" VARCHAR(255) NOT NULL,
"stored_filename" VARCHAR(255) NOT NULL,
"file_path" VARCHAR(500) NOT NULL,
"title" VARCHAR(255) NOT NULL,
"summary" TEXT,
"notes" TEXT,
"is_primary" BOOLEAN NOT NULL DEFAULT false,
"file_size" INTEGER NOT NULL,
"display_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "subscriptions" (
"id" SERIAL NOT NULL,
"email" VARCHAR(255) NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"unsubscribe_token" VARCHAR(255) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "admin_users" (
"id" SERIAL NOT NULL,
"username" VARCHAR(50) NOT NULL,
"password_hash" VARCHAR(255) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_login" TIMESTAMP(3),
CONSTRAINT "admin_users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "idx_docket_entries_date" ON "docket_entries"("date" DESC);
-- CreateIndex
CREATE UNIQUE INDEX "documents_stored_filename_key" ON "documents"("stored_filename");
-- CreateIndex
CREATE INDEX "idx_documents_docket_entry" ON "documents"("docket_entry_id");
-- CreateIndex
CREATE INDEX "idx_documents_display_order" ON "documents"("docket_entry_id", "display_order");
-- CreateIndex
CREATE UNIQUE INDEX "subscriptions_email_key" ON "subscriptions"("email");
-- CreateIndex
CREATE UNIQUE INDEX "subscriptions_unsubscribe_token_key" ON "subscriptions"("unsubscribe_token");
-- CreateIndex
CREATE INDEX "idx_subscriptions_active" ON "subscriptions"("is_active");
-- CreateIndex
CREATE UNIQUE INDEX "admin_users_username_key" ON "admin_users"("username");
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_docket_entry_id_fkey" FOREIGN KEY ("docket_entry_id") REFERENCES "docket_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,20 +0,0 @@
/*
Warnings:
- You are about to drop the column `court_notes` on the `docket_entries` table. All the data in the column will be lost.
- You are about to drop the column `document_type` on the `docket_entries` table. All the data in the column will be lost.
- You are about to drop the column `is_primary` on the `documents` table. All the data in the column will be lost.
- Added the required column `title` to the `docket_entries` table without a default value. This is not possible if the table is not empty.
- Added the required column `mime_type` to the `documents` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "docket_entries" DROP COLUMN "court_notes",
DROP COLUMN "document_type",
ADD COLUMN "notes" TEXT,
ADD COLUMN "title" VARCHAR(500) NOT NULL;
-- AlterTable
ALTER TABLE "documents" DROP COLUMN "is_primary",
ADD COLUMN "mime_type" VARCHAR(100) NOT NULL,
ALTER COLUMN "title" SET DATA TYPE VARCHAR(500);

View file

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View file

@ -1,70 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
model DocketEntry {
id Int @id @default(autoincrement())
date DateTime @db.Date
title String @db.VarChar(500)
summary String @db.Text
notes String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
documents Document[]
@@map("docket_entries")
@@index([date(sort: Desc)], name: "idx_docket_entries_date")
}
model Document {
id Int @id @default(autoincrement())
docketEntryId Int @map("docket_entry_id")
title String @db.VarChar(500)
originalFilename String @map("original_filename") @db.VarChar(255)
storedFilename String @unique @map("stored_filename") @db.VarChar(255)
filePath String @map("file_path") @db.VarChar(500)
fileSize Int @map("file_size")
mimeType String @map("mime_type") @db.VarChar(100)
summary String? @db.Text
notes String? @db.Text
displayOrder Int @default(0) @map("display_order")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
docketEntry DocketEntry @relation(fields: [docketEntryId], references: [id], onDelete: Cascade)
@@map("documents")
@@index([docketEntryId], name: "idx_documents_docket_entry")
@@index([docketEntryId, displayOrder], name: "idx_documents_display_order")
}
model Subscription {
id Int @id @default(autoincrement())
email String @unique @db.VarChar(255)
isActive Boolean @default(true) @map("is_active")
unsubscribeToken String @unique @map("unsubscribe_token") @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at")
@@map("subscriptions")
@@index([isActive], name: "idx_subscriptions_active")
}
model AdminUser {
id Int @id @default(autoincrement())
username String @unique @db.VarChar(50)
passwordHash String @map("password_hash") @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at")
lastLogin DateTime? @map("last_login")
@@map("admin_users")
}

View file

@ -1,63 +0,0 @@
#!/usr/bin/env node
const bcrypt = require('bcryptjs');
const { PrismaClient } = require('@prisma/client');
const crypto = require('crypto');
const prisma = new PrismaClient();
async function setupAdmin() {
try {
console.log('🔧 Setting up admin user for production...');
// Generate secure credentials
const username = 'eliza';
const password = crypto.randomBytes(12).toString('base64').replace(/[+/=]/g, '').substring(0, 16);
console.log('\n📋 Generated Credentials:');
console.log('Username:', username);
console.log('Password:', password);
console.log('\n⚠ IMPORTANT: Save these credentials securely!');
// Hash the password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Check if admin user already exists
const existingAdmin = await prisma.adminUser.findUnique({
where: { username }
});
if (existingAdmin) {
// Update existing admin
await prisma.adminUser.update({
where: { username },
data: {
passwordHash
}
});
console.log('\n✅ Admin user updated successfully!');
} else {
// Create new admin user
await prisma.adminUser.create({
data: {
username,
passwordHash
}
});
console.log('\n✅ Admin user created successfully!');
}
console.log('\n🚀 Production setup complete!');
console.log('Eliza can now log in with the credentials above.');
} catch (error) {
console.error('❌ Error setting up admin user:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Run the setup
setupAdmin();

View file

@ -1,66 +0,0 @@
#!/usr/bin/env node
const bcrypt = require('bcryptjs');
const { PrismaClient } = require('@prisma/client');
const crypto = require('crypto');
const prisma = new PrismaClient();
async function setupAdmin() {
try {
console.log('🔧 Setting up admin user for production...');
// Generate secure credentials
const username = 'eliza';
const password = crypto.randomBytes(12).toString('base64').replace(/[+/=]/g, '').substring(0, 16);
console.log('\n📋 Generated Credentials:');
console.log('Username:', username);
console.log('Password:', password);
console.log('\n⚠ IMPORTANT: Save these credentials securely!');
// Hash the password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Check if admin user already exists
const existingAdmin = await prisma.adminUser.findUnique({
where: { username }
});
if (existingAdmin) {
// Update existing admin
await prisma.adminUser.update({
where: { username },
data: {
passwordHash,
updatedAt: new Date()
}
});
console.log('\n✅ Admin user updated successfully!');
} else {
// Create new admin user
await prisma.adminUser.create({
data: {
username,
passwordHash,
createdAt: new Date(),
updatedAt: new Date()
}
});
console.log('\n✅ Admin user created successfully!');
}
console.log('\n🚀 Production setup complete!');
console.log('Eliza can now log in with the credentials above.');
} catch (error) {
console.error('❌ Error setting up admin user:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Run the setup
setupAdmin();

View file

@ -1,178 +0,0 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import dotenv from 'dotenv';
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
import { createClient } from 'redis';
import { logger } from './utils/logger';
import { errorHandler } from './middleware/errorHandler';
import { requestLogger } from './middleware/requestLogger';
// Import routes
import authRoutes from './routes/auth';
import docketRoutes from './routes/docket';
import documentRoutes from './routes/documents';
import subscriptionRoutes from './routes/subscriptions';
import healthRoutes from './routes/health';
import notificationRoutes from './routes/notifications';
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001;
// Trust proxy when behind reverse proxy (Caddy)
app.set('trust proxy', true);
// Initialize Prisma client with PostgreSQL adapter
const connectionString = process.env.DATABASE_URL;
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
export const prisma = new PrismaClient({
adapter,
log: ['query', 'info', 'warn', 'error'],
});
// Initialize Redis client
export const redis = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
// Connect to Redis
redis.connect().catch((error) => {
logger.error('Redis connection failed:', error);
});
redis.on('error', (error) => {
logger.error('Redis error:', error);
});
redis.on('connect', () => {
logger.info('Connected to Redis');
});
// Security middleware
app.use(helmet({
crossOriginResourcePolicy: { policy: "cross-origin" }
}));
// CORS configuration
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? [
process.env.FRONTEND_URL,
'https://mad-lawsuit.org',
'https://files.mad-lawsuit.org',
'http://frontend:806',
'http://localhost:806'
]
: [
'http://localhost:3000',
'http://localhost:806',
'http://frontend:806',
'http://frontend:3000',
'https://mad-lawsuit.org',
'https://files.mad-lawsuit.org'
],
credentials: true,
}));
// Rate limiting with proper proxy configuration
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
// Skip rate limiting validation errors in production behind proxy
skip: (req) => {
// Skip rate limiting if we can't determine the real IP
return false;
},
keyGenerator: (req) => {
// Use X-Forwarded-For header if available, otherwise fall back to req.ip
return req.ip || 'unknown';
},
});
app.use('/api', limiter);
// Stricter rate limiting for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: 'Too many authentication attempts, please try again later.',
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
return req.ip || 'unknown';
},
});
app.use('/api/auth', authLimiter);
// Body parsing middleware
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Request logging
app.use(requestLogger);
// Routes
app.use('/api/health', healthRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/docket-entries', docketRoutes);
app.use('/api/documents', documentRoutes);
app.use('/api/subscriptions', subscriptionRoutes);
app.use('/api/notifications', notificationRoutes);
// 404 handler - Express 5 compatible
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'Route not found',
});
});
// Error handling middleware (must be last)
app.use(errorHandler);
// Graceful shutdown
process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully');
// Close database connection
await prisma.$disconnect();
// Close Redis connection
await redis.quit();
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('SIGINT received, shutting down gracefully');
// Close database connection
await prisma.$disconnect();
// Close Redis connection
await redis.quit();
process.exit(0);
});
// Start server
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
export default app;

View file

@ -1,62 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { prisma } from '../index';
export interface AuthenticatedRequest extends Request {
user?: {
userId: number;
username: string;
};
}
export const authenticateToken = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required',
});
}
try {
const jwtSecret = process.env['JWT_SECRET'];
if (!jwtSecret) {
return res.status(500).json({
success: false,
message: 'Server configuration error',
});
}
const decoded = jwt.verify(token, jwtSecret) as any;
// Verify user still exists
const user = await prisma.adminUser.findUnique({
where: { id: decoded.userId },
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token - user not found',
});
}
req.user = {
userId: decoded.userId,
username: decoded.username,
};
next();
} catch (error) {
return res.status(403).json({
success: false,
message: 'Invalid or expired token',
});
}
};

View file

@ -1,49 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger';
export interface AppError extends Error {
statusCode?: number;
isOperational?: boolean;
}
export const errorHandler = (
error: AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
const statusCode = error.statusCode || 500;
const message = error.message || 'Internal Server Error';
// Log error details
logger.error('Error occurred:', {
error: error.message,
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
});
// Don't leak error details in production
const isDevelopment = process.env.NODE_ENV === 'development';
res.status(statusCode).json({
success: false,
message: isDevelopment ? message : 'Something went wrong',
...(isDevelopment && { stack: error.stack }),
});
};
export const createError = (message: string, statusCode: number = 500): AppError => {
const error: AppError = new Error(message);
error.statusCode = statusCode;
error.isOperational = true;
return error;
};
export const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};

View file

@ -1,32 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger';
export const requestLogger = (req: Request, res: Response, next: NextFunction): void => {
const start = Date.now();
// Log request
logger.info('Incoming request', {
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.get('User-Agent'),
});
// Override res.end to log response
const originalEnd = res.end;
res.end = function(chunk?: any, encoding?: any): Response {
const duration = Date.now() - start;
logger.info('Request completed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
});
return originalEnd.call(this, chunk, encoding);
} as any;
next();
};

View file

@ -1,108 +0,0 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { prisma } from '../index';
import { asyncHandler } from '../middleware/errorHandler';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import Joi from 'joi';
const router = Router();
// Validation schemas
const loginSchema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
});
// Login endpoint
router.post('/login', asyncHandler(async (req: Request, res: Response) => {
const { error, value } = loginSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const { username, password } = value;
// Find admin user
const admin = await prisma.adminUser.findUnique({
where: { username },
});
if (!admin) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, admin.passwordHash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
// Update last login
await prisma.adminUser.update({
where: { id: admin.id },
data: { lastLogin: new Date() },
});
// Generate JWT token
const jwtSecret = process.env['JWT_SECRET'];
const jwtExpiresIn = process.env['JWT_EXPIRES_IN'] || '24h';
if (!jwtSecret) {
return res.status(500).json({
success: false,
message: 'Server configuration error',
});
}
const token = jwt.sign(
{
userId: admin.id,
username: admin.username
},
jwtSecret,
{
expiresIn: jwtExpiresIn
} as jwt.SignOptions
);
res.json({
success: true,
message: 'Login successful',
token,
user: {
id: admin.id,
username: admin.username,
},
});
}));
// Verify token endpoint
router.get('/verify', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
res.json({
success: true,
message: 'Token is valid',
user: req.user,
});
}));
// Logout endpoint (client-side token removal, but we can log it)
router.post('/logout', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
res.json({
success: true,
message: 'Logged out successfully',
});
}));
export default router;

View file

@ -1,228 +0,0 @@
import { Router, Request, Response } from 'express';
import { prisma } from '../index';
import { asyncHandler } from '../middleware/errorHandler';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import { emailService } from '../services/emailService';
import Joi from 'joi';
const router = Router();
// Helper function to normalize date input to avoid timezone issues
function normalizeDate(dateInput: any): Date {
if (typeof dateInput === 'string') {
// If it's a string in YYYY-MM-DD format, create date in local time
const [year, month, day] = dateInput.split('-');
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
if (dateInput instanceof Date) {
// If it's already a Date object, extract date parts and recreate to avoid timezone issues
const year = dateInput.getFullYear();
const month = dateInput.getMonth();
const day = dateInput.getDate();
return new Date(year, month, day);
}
return dateInput;
}
// Validation schemas
const createDocketEntrySchema = Joi.object({
date: Joi.date().required(),
title: Joi.string().required().max(500),
summary: Joi.string().required(),
notes: Joi.string().optional().allow(''),
});
const updateDocketEntrySchema = Joi.object({
date: Joi.date().optional(),
title: Joi.string().optional().max(500),
summary: Joi.string().optional(),
notes: Joi.string().optional().allow(''),
});
// Public endpoint - Get all docket entries with documents
router.get('/', asyncHandler(async (req: Request, res: Response) => {
const entries = await prisma.docketEntry.findMany({
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
orderBy: {
date: 'asc',
},
});
res.json({
success: true,
entries,
});
}));
// Admin endpoint - Get single docket entry
router.get('/:id', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const entryId = parseInt(req.params.id);
if (isNaN(entryId)) {
return res.status(400).json({
success: false,
message: 'Invalid entry ID',
});
}
const entry = await prisma.docketEntry.findUnique({
where: { id: entryId },
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
});
if (!entry) {
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
res.json({
success: true,
entry,
});
}));
// Admin endpoint - Create new docket entry
router.post('/', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const { error, value } = createDocketEntrySchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
// Normalize date to avoid timezone issues
const normalizedData = { ...value };
if (value.date) {
normalizedData.date = normalizeDate(value.date);
}
const entry = await prisma.docketEntry.create({
data: normalizedData,
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
});
// Email notifications are now manual-only via the green mail button
// Automatic notifications disabled per user request
res.status(201).json({
success: true,
message: 'Docket entry created successfully',
entry,
});
}));
// Admin endpoint - Update docket entry
router.put('/:id', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const entryId = parseInt(req.params.id);
if (isNaN(entryId)) {
return res.status(400).json({
success: false,
message: 'Invalid entry ID',
});
}
const { error, value } = updateDocketEntrySchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
// Normalize date to avoid timezone issues
const normalizedData = { ...value };
if (value.date) {
normalizedData.date = normalizeDate(value.date);
}
try {
const entry = await prisma.docketEntry.update({
where: { id: entryId },
data: normalizedData,
include: {
documents: {
orderBy: {
displayOrder: 'asc',
},
},
},
});
res.json({
success: true,
message: 'Docket entry updated successfully',
entry,
});
} catch (error: any) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
throw error;
}
}));
// Admin endpoint - Delete docket entry
router.delete('/:id', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const entryId = parseInt(req.params.id);
if (isNaN(entryId)) {
return res.status(400).json({
success: false,
message: 'Invalid entry ID',
});
}
try {
// Delete associated documents first (cascade should handle this, but being explicit)
await prisma.document.deleteMany({
where: { docketEntryId: entryId },
});
// Delete the docket entry
await prisma.docketEntry.delete({
where: { id: entryId },
});
res.json({
success: true,
message: 'Docket entry deleted successfully',
});
} catch (error: any) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
throw error;
}
}));
export default router;

View file

@ -1,276 +0,0 @@
import { Router, Request, Response } from 'express';
import { prisma } from '../index';
import { asyncHandler } from '../middleware/errorHandler';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
import Joi from 'joi';
const router = Router();
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = process.env['UPLOAD_DIR'] || './uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uuid = uuidv4();
const extension = path.extname(file.originalname);
cb(null, `${uuid}${extension}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: parseInt(process.env['MAX_FILE_SIZE'] || '262144000'), // 250MB default
},
fileFilter: (req, file, cb) => {
// Allow PDF files with multiple possible MIME types
const allowedMimeTypes = [
'application/pdf',
'application/x-pdf',
'application/acrobat',
'applications/vnd.pdf',
'text/pdf',
'text/x-pdf'
];
// Check MIME type or file extension
const isValidPDF = allowedMimeTypes.includes(file.mimetype) ||
file.originalname.toLowerCase().endsWith('.pdf');
// Log for debugging
console.log('Upload attempt:', {
mimetype: file.mimetype,
filename: file.originalname,
isValid: isValidPDF
});
if (isValidPDF) {
cb(null, true);
} else {
cb(new Error(`Only PDF files are allowed. Received: ${file.mimetype} for file: ${file.originalname}`));
}
},
});
// Validation schemas
const createDocumentSchema = Joi.object({
docketEntryId: Joi.number().integer().required(),
title: Joi.string().required().max(500),
summary: Joi.string().optional().allow(''),
notes: Joi.string().optional().allow(''),
displayOrder: Joi.number().integer().min(0).optional(),
});
const updateDocumentSchema = Joi.object({
title: Joi.string().optional().max(500),
summary: Joi.string().optional().allow(''),
notes: Joi.string().optional().allow(''),
displayOrder: Joi.number().integer().min(0).optional(),
});
// Public endpoint - Download document
router.get('/:id/download', asyncHandler(async (req: Request, res: Response) => {
const documentId = parseInt(req.params['id'] || '');
if (isNaN(documentId)) {
return res.status(400).json({
success: false,
message: 'Invalid document ID',
});
}
const document = await prisma.document.findUnique({
where: { id: documentId },
});
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
const filePath = document.filePath;
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'File not found on server',
});
}
res.setHeader('Content-Type', document.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${document.originalFilename}"`);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
}));
// Admin endpoint - Upload document
router.post('/upload', authenticateToken, upload.single('file'), asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded',
});
}
const { error, value } = createDocumentSchema.validate(req.body);
if (error) {
// Clean up uploaded file if validation fails
fs.unlinkSync(req.file.path);
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const { docketEntryId, title, summary, notes, displayOrder } = value;
// Verify docket entry exists
const docketEntry = await prisma.docketEntry.findUnique({
where: { id: docketEntryId },
});
if (!docketEntry) {
// Clean up uploaded file
fs.unlinkSync(req.file.path);
return res.status(404).json({
success: false,
message: 'Docket entry not found',
});
}
// Get next display order if not provided
let finalDisplayOrder = displayOrder;
if (finalDisplayOrder === undefined) {
const lastDocument = await prisma.document.findFirst({
where: { docketEntryId },
orderBy: { displayOrder: 'desc' },
});
finalDisplayOrder = lastDocument ? lastDocument.displayOrder + 1 : 0;
}
const document = await prisma.document.create({
data: {
docketEntryId,
title,
originalFilename: req.file.originalname,
storedFilename: req.file.filename,
filePath: req.file.path,
fileSize: req.file.size,
mimeType: req.file.mimetype,
summary: summary || '',
notes: notes || '',
displayOrder: finalDisplayOrder,
},
});
res.status(201).json({
success: true,
message: 'Document uploaded successfully',
document,
});
}));
// Admin endpoint - Update document metadata
router.put('/:id', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const documentId = parseInt(req.params['id'] || '');
if (isNaN(documentId)) {
return res.status(400).json({
success: false,
message: 'Invalid document ID',
});
}
const { error, value } = updateDocumentSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
try {
const document = await prisma.document.update({
where: { id: documentId },
data: value,
});
res.json({
success: true,
message: 'Document updated successfully',
document,
});
} catch (error: any) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
throw error;
}
}));
// Admin endpoint - Delete document
router.delete('/:id', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const documentId = parseInt(req.params['id'] || '');
if (isNaN(documentId)) {
return res.status(400).json({
success: false,
message: 'Invalid document ID',
});
}
try {
const document = await prisma.document.findUnique({
where: { id: documentId },
});
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
// Delete the file from filesystem
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
// Delete from database
await prisma.document.delete({
where: { id: documentId },
});
res.json({
success: true,
message: 'Document deleted successfully',
});
} catch (error: any) {
if (error.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'Document not found',
});
}
throw error;
}
}));
export default router;

View file

@ -1,37 +0,0 @@
import { Router, Request, Response } from 'express';
import { prisma } from '../index';
import { redis } from '../index';
const router = Router();
router.get('/', async (req: Request, res: Response) => {
try {
// Check database connection
await prisma.$queryRaw`SELECT 1`;
// Check Redis connection
await redis.ping();
res.json({
success: true,
message: 'API is healthy',
timestamp: new Date().toISOString(),
services: {
database: 'connected',
redis: 'connected',
},
});
} catch (error) {
res.status(503).json({
success: false,
message: 'Service unavailable',
timestamp: new Date().toISOString(),
services: {
database: 'disconnected',
redis: 'disconnected',
},
});
}
});
export default router;

View file

@ -1,59 +0,0 @@
import { Router, Response } from 'express';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import { emailService } from '../services/emailService';
import { logger } from '../utils/logger';
const router = Router();
// Manual email notification endpoint
router.post('/send/:docketEntryId', authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
try {
const { docketEntryId } = req.params;
// Import prisma here to avoid circular dependency
const { prisma } = await import('../index');
// Get the docket entry with details
const docketEntry = await prisma.docketEntry.findUnique({
where: { id: parseInt(docketEntryId) },
include: {
documents: {
orderBy: { displayOrder: 'asc' }
}
}
});
if (!docketEntry) {
return res.status(404).json({
success: false,
message: 'Docket entry not found'
});
}
// Send the email notification using the BCC functionality
await emailService.notifySubscribersOfNewEntry(
docketEntry.title,
docketEntry.date.toISOString(),
docketEntry.summary
);
logger.info(`Manual email notification sent for docket entry: ${docketEntry.title}`, {
docketEntryId: docketEntry.id,
adminUser: req.user?.username
});
res.json({
success: true,
message: 'Email notification sent successfully to all subscribers'
});
} catch (error) {
logger.error('Failed to send manual email notification:', error);
res.status(500).json({
success: false,
message: 'Failed to send email notification'
});
}
});
export default router;

View file

@ -1,121 +0,0 @@
import { Router, Request, Response } from 'express';
import { prisma } from '../index';
import { asyncHandler } from '../middleware/errorHandler';
import { authenticateToken, AuthenticatedRequest } from '../middleware/auth';
import Joi from 'joi';
import { v4 as uuidv4 } from 'uuid';
const router = Router();
// Validation schemas
const subscribeSchema = Joi.object({
email: Joi.string().email().required(),
});
// Subscribe to notifications
router.post('/subscribe', asyncHandler(async (req: Request, res: Response) => {
const { error, value } = subscribeSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message,
});
}
const { email } = value;
// Check if already subscribed
const existing = await prisma.subscription.findUnique({
where: { email },
});
if (existing) {
if (existing.isActive) {
return res.status(400).json({
success: false,
message: 'Email is already subscribed',
});
} else {
// Reactivate subscription
await prisma.subscription.update({
where: { email },
data: { isActive: true },
});
}
} else {
// Create new subscription
await prisma.subscription.create({
data: {
email,
unsubscribeToken: uuidv4(),
},
});
}
res.json({
success: true,
message: 'Successfully subscribed to notifications',
});
}));
// Admin endpoint - Get all subscribers
router.get('/', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const subscribers = await prisma.subscription.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
isActive: true,
createdAt: true,
},
});
const stats = {
total: subscribers.length,
active: subscribers.filter(sub => sub.isActive).length,
inactive: subscribers.filter(sub => !sub.isActive).length,
};
res.json({
success: true,
subscribers,
stats,
});
}));
// Admin endpoint - Toggle subscriber status
router.put('/:id/toggle', authenticateToken, asyncHandler(async (req: AuthenticatedRequest, res: Response) => {
const subscriberId = parseInt(req.params.id);
if (isNaN(subscriberId)) {
return res.status(400).json({
success: false,
message: 'Invalid subscriber ID',
});
}
const subscriber = await prisma.subscription.findUnique({
where: { id: subscriberId },
});
if (!subscriber) {
return res.status(404).json({
success: false,
message: 'Subscriber not found',
});
}
const updated = await prisma.subscription.update({
where: { id: subscriberId },
data: { isActive: !subscriber.isActive },
});
res.json({
success: true,
message: `Subscriber ${updated.isActive ? 'activated' : 'deactivated'} successfully`,
subscriber: updated,
});
}));
export default router;

View file

@ -1,225 +0,0 @@
import { logger } from '../utils/logger';
import nodemailer from 'nodemailer';
interface EmailOptions {
to: string;
bcc?: string[];
subject: string;
html: string;
}
export class EmailService {
private static instance: EmailService;
private transporter: nodemailer.Transporter;
private constructor() {
// Initialize Gmail SMTP transporter
this.transporter = nodemailer.createTransport({
host: process.env['SMTP_HOST'] || 'smtp.gmail.com',
port: parseInt(process.env['SMTP_PORT'] || '587'),
secure: process.env['SMTP_SECURE'] === 'true', // false for 587, true for 465
auth: {
user: process.env['GOOGLE_EMAIL'],
pass: process.env['GOOGLE_APP_PASSWORD'],
},
});
// Verify SMTP connection
this.verifyConnection();
}
public static getInstance(): EmailService {
if (!EmailService.instance) {
EmailService.instance = new EmailService();
}
return EmailService.instance;
}
private async verifyConnection(): Promise<void> {
try {
await this.transporter.verify();
logger.info('SMTP connection verified successfully');
} catch (error) {
logger.error('SMTP connection failed:', error);
}
}
async sendEmail(options: EmailOptions): Promise<boolean> {
try {
const mailOptions: any = {
from: {
name: 'Court Docket System',
address: process.env['GOOGLE_EMAIL'] || 'system@deafgain.org',
},
to: options.to,
subject: options.subject,
html: options.html,
};
// Add BCC if provided
if (options.bcc && options.bcc.length > 0) {
mailOptions.bcc = options.bcc;
}
const result = await this.transporter.sendMail(mailOptions);
logger.info('Email sent successfully:', {
to: options.to,
bcc: options.bcc ? `${options.bcc.length} recipients` : 'none',
subject: options.subject,
messageId: result.messageId,
});
return true;
} catch (error) {
logger.error('Failed to send email:', error);
return false;
}
}
async notifySubscribersOfNewEntry(entryTitle: string, entryDate: string, entrySummary: string): Promise<void> {
try {
// Get all active subscribers
const { prisma } = await import('../index');
const subscribers = await prisma.subscription.findMany({
where: { isActive: true },
});
if (subscribers.length === 0) {
logger.info('No active subscribers to notify');
return;
}
const subject = `New Court Filing: ${entryTitle}`;
const html = this.generateNotificationEmail(entryTitle, entryDate, entrySummary);
const subscriberEmails = subscribers.map((subscriber: any) => subscriber.email);
// Send single email with all subscribers as BCC
const success = await this.sendEmail({
to: process.env['GOOGLE_EMAIL'] || 'system@deafgain.org',
bcc: subscriberEmails,
subject,
html,
});
if (success) {
logger.info(`Email notification sent successfully to ${subscriberEmails.length} subscribers via BCC`);
} else {
logger.error(`Failed to send BCC email notification to ${subscriberEmails.length} subscribers`);
}
} catch (error) {
logger.error('Failed to notify subscribers:', error);
}
}
private generateNotificationEmail(title: string, date: string, summary: string): string {
const formattedDate = new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Court Filing Notification</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #1f2937;
color: white;
padding: 20px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.content {
background-color: #f9fafb;
padding: 30px;
border-radius: 0 0 8px 8px;
border: 1px solid #e5e7eb;
}
.filing-info {
background-color: white;
padding: 20px;
border-radius: 6px;
margin: 20px 0;
border-left: 4px solid #3b82f6;
}
.date {
color: #6b7280;
font-size: 14px;
margin-bottom: 10px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #1f2937;
margin-bottom: 15px;
}
.summary {
color: #4b5563;
line-height: 1.5;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #6b7280;
text-align: center;
}
.button {
display: inline-block;
background-color: #3b82f6;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="header">
<h1>Eliza Kragh v. Montana Association of the Deaf</h1>
<p>New Court Filing Notification</p>
</div>
<div class="content">
<p>A new filing has been added to the court docket:</p>
<div class="filing-info">
<div class="date">${formattedDate}</div>
<div class="title">${title}</div>
<div class="summary">${summary}</div>
</div>
<p>You can view the complete docket and any associated documents by visiting the <a href="https://mad-lawsuit.org/" style="color: #3b82f6; text-decoration: none;">website</a>:</p>
<a href="https://mad-lawsuit.org/" class="button">
View Court Docket
</a>
<div class="footer">
<p>You are receiving this email because you subscribed to notifications for this case.</p>
<p>This is an automated notification from the court docket system.</p>
<p>To unsubscribe, please send an email to eliza.kragh@gmail.com</p>
</div>
</div>
</body>
</html>
`;
}
}
export const emailService = EmailService.getInstance();

View file

@ -1,38 +0,0 @@
import winston from 'winston';
const logFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: { service: 'docket-api' },
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
],
});
// If we're not in production, log to the console as well
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
export default logger;

View file

@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": false,
"noImplicitReturns": false,
"noImplicitThis": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"exactOptionalPropertyTypes": false,
"noImplicitOverride": false,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": false,
"allowUnusedLabels": true,
"allowUnreachableCode": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}

Some files were not shown because too many files have changed in this diff Show more