88 lines
4 KiB
Markdown
88 lines
4 KiB
Markdown
# System Patterns
|
|
|
|
## Architecture
|
|
|
|
Single-container Express app serves both the React SPA and the REST API. MongoDB runs in a separate container on an internal Docker network.
|
|
|
|
```
|
|
Browser
|
|
└─► Caddy (caddy_network)
|
|
└─► deafmissoulaorg-app-1:801
|
|
├─ GET /api/* → Express routes
|
|
├─ GET /uploads/* → multer upload volume
|
|
├─ GET /photos/*, /videos/*, etc. → client/build/ static assets
|
|
└─ GET * → client/build/index.html (SPA catch-all)
|
|
└─► deafmissoulaorg-mongodb-1:27017 (internal network only)
|
|
```
|
|
|
|
## Data Flow
|
|
|
|
All frontend data is fetched from the API at runtime — nothing is hardcoded in components. Each component does a `useEffect` → `fetch('/api/...')` → `useState` pattern.
|
|
|
|
## File Organization
|
|
|
|
```
|
|
client/src/components/
|
|
├── Manage.tsx # /manage dashboard (auth + all admin tabs)
|
|
├── App.tsx # Routing: /manage/* outside Header/Footer
|
|
├── Home.tsx # Landing page (fetches events)
|
|
├── Calendar.tsx # Events calendar (fetches events)
|
|
├── Meet-Board.tsx # Board member profiles (fetches /api/board)
|
|
├── Gallery.tsx # Photo gallery (fetches /api/gallery)
|
|
├── VideoGallery.tsx # Video gallery (hardcoded paths in client/public/videos/)
|
|
├── Minutes.tsx # Meeting minutes table (fetches /api/minutes)
|
|
├── Sponsors.tsx # Sponsors (fetches /api/sponsors)
|
|
├── Bylaws.tsx # Static PDF links (hardcoded paths in client/public/)
|
|
└── Resources.tsx # Montana/National Deaf resources directory (static)
|
|
|
|
server/src/
|
|
├── models/ # Mongoose schemas
|
|
│ ├── BoardMember.ts # Includes auth fields (password, mustChangePassword, resetToken)
|
|
│ ├── Event.ts
|
|
│ ├── GalleryItem.ts
|
|
│ ├── Minute.ts # Has 'order' field for drag-and-drop sorting
|
|
│ ├── Sponsor.ts
|
|
│ ├── Member.ts
|
|
│ └── User.ts # Admin-only account (not BoardMember)
|
|
├── routes/
|
|
│ ├── auth.ts # Login, forgot/reset password, change password
|
|
│ ├── board.ts # CRUD + reorder + welcome email on create
|
|
│ ├── minutes.ts # CRUD + reorder (new entries auto-insert at top)
|
|
│ ├── events.ts
|
|
│ ├── gallery.ts
|
|
│ ├── sponsors.ts
|
|
│ ├── members.ts
|
|
│ └── email.ts # Contact form
|
|
├── middleware/
|
|
│ ├── auth.ts # JWT verification, exports AuthRequest
|
|
│ ├── upload.ts # Multer config for headshots/photos/sponsors/minutes
|
|
│ └── errorHandler.ts # ZodError → 400, others → 500
|
|
├── db.ts # mongoose.connect()
|
|
├── index.ts # Express app setup, static serving, route mounting
|
|
└── seed.ts # One-time data population (idempotent — skips if data exists)
|
|
```
|
|
|
|
## Dashboard (`/manage`) Patterns
|
|
|
|
- **Route security:** `/manage` is not linked from public nav (obscurity) + JWT required for all write operations
|
|
- **Login:** Checks BoardMember by email first, then User (admin fallback)
|
|
- **Drag-and-drop:** `useDragReorder<T>` custom hook using HTML5 drag API + `useRef` for drag index tracking; calls `PATCH /api/[resource]/reorder` with `{ ids: string[] }` on drop
|
|
- **Image preview:** `URL.createObjectURL(file)` in `ImagePicker` component before upload
|
|
- **Forced password change:** `mustChangePassword: true` → server issues `tempToken` (1-hour) → client shows change-password form before dashboard loads
|
|
|
|
## Deployment Pattern
|
|
|
|
1. Code changes pushed to Gitea (`git push origin main`)
|
|
2. SSH to production server (`ssh chaulmark@10.4.0.205`)
|
|
3. `git pull origin main && docker compose down && docker compose up -d --build`
|
|
|
|
No Portainer. No webhooks. Manual pull + rebuild.
|
|
|
|
## Rollback Pattern
|
|
|
|
`v1` tag in Git points to the last hardcoded version (no database):
|
|
```bash
|
|
git checkout v1
|
|
docker compose down
|
|
docker compose up -d --build
|
|
```
|