deafmissoula-website/README.md

145 lines
5.7 KiB
Markdown

# Missoula Council of the Deaf, Inc. (MCDi) Website
Official website for MCDi at [deafmissoula.org](https://deafmissoula.org). Built on a full-stack TypeScript architecture with a MongoDB backend and a React frontend.
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Frontend | React 19, TypeScript 5.8, Tailwind CSS, Framer Motion |
| Backend | Node.js 22 LTS, Express 5.1, TypeScript 5.8 |
| Database | MongoDB 7.0 (Mongoose 8.x ODM) |
| Auth | JWT (`jose` 5.x), `argon2` password hashing |
| File uploads | `multer` 1.4.5-lts |
| Email | `nodemailer` (Google Workspace SMTP) |
| Containerization | Docker, Docker Compose |
| Reverse proxy | Caddy (on production server) |
## Repository Structure
```
deafmissoula-website/
├── client/ # React frontend (Create React App)
│ ├── public/ # Static assets (photos, videos, PDFs, headshots)
│ └── src/
│ └── components/ # React components
├── server/ # Express backend
│ └── src/
│ ├── models/ # Mongoose models
│ ├── routes/ # API route handlers
│ ├── middleware/ # Auth, upload, error handling
│ ├── index.ts # Server entry point
│ ├── db.ts # MongoDB connection
│ └── seed.ts # Initial data seed script
├── docker-compose.yml # Production compose (2 containers: app + mongodb)
├── docker-compose.dev.yml # Dev override (no caddy_network requirement)
├── Dockerfile # Multi-stage build: client build + server compile
└── stack.env # Environment variables (committed — Gitea is internal-only)
```
## API Routes
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/auth/login` | — | Email + password login (rate limited: 5/5 min) |
| POST | `/api/auth/forgot-password` | — | Send password reset email |
| POST | `/api/auth/reset-password` | — | Set new password via reset token |
| POST | `/api/auth/change-password` | temp JWT | Forced password change on first login |
| GET | `/api/auth/me` | JWT | Current user info |
| GET/POST/PUT/DELETE | `/api/board` | JWT (write) | Board members |
| PATCH | `/api/board/reorder` | JWT | Drag-and-drop reorder |
| GET/POST/PUT/DELETE | `/api/events` | JWT (write) | Calendar events |
| GET/POST/PUT/DELETE | `/api/gallery` | JWT (write) | Photo gallery items |
| GET/POST/PUT/DELETE | `/api/minutes` | JWT (write) | Meeting minutes |
| PATCH | `/api/minutes/reorder` | JWT | Drag-and-drop reorder |
| GET/POST/PUT/DELETE | `/api/sponsors` | JWT (write) | Sponsors |
| GET/POST/PUT/DELETE | `/api/members` | JWT | Member directory |
| POST | `/api/email` | — | Contact form submission |
## Dashboard
Available at `/manage` (intentionally not `/admin`). Board members log in with their `@deafmissoula.org` email address.
**First login flow:** Board members are created with `mustChangePassword: true`. On first login they receive a temp JWT and are immediately redirected to set their own password.
**Forgot password:** Sends a 1-hour reset link to the member's email via Google Workspace SMTP.
**New board member:** When added via the dashboard, a welcome email is automatically sent with a 7-day set-password link.
## Environment Variables (`stack.env`)
```env
CACHEBUST=... # Increment to bust Docker layer cache
MONGO_INITDB_ROOT_USERNAME=mcdi
MONGO_INITDB_ROOT_PASSWORD=...
MONGO_INITDB_DATABASE=mcdi
MONGODB_URI=mongodb://mcdi:...@mongodb:27017/mcdi?authSource=admin
JWT_SECRET=...
ADMIN_PASSWORD=... # Initial password for admin@deafmissoula.org
APP_URL=https://deafmissoula.org # Used in password reset email links
```
Google email credentials live in `.env` (not committed):
```env
GOOGLE_EMAIL=system@deafgain.org
GOOGLE_APP_PASSWORD=...
```
## Local Development (Preview)
```bash
# Start MongoDB separately (or use docker compose)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# Seed the database (first run only)
cd server && npm run seed
# Dev servers
cd client && npm start # React dev server on :3000
cd server && npm run dev # Express with tsx watch on :801
```
## Production Deployment
Production server: `10.4.0.205` (WireGuard VPN required)
Site path: `/home/chaulmark/websites/deafmissoula.org/`
```bash
ssh chaulmark@10.4.0.205
cd ~/websites/deafmissoula.org
git pull origin main
docker compose down
docker compose up -d --build
docker exec deafmissoulaorg-app-1 node dist/seed.js # First deploy only
```
**Note on Caddy:** The app container joins the external `caddy_network`, so Caddy can route to it by container name:
```
deafmissoula.org {
reverse_proxy deafmissoulaorg-app-1:801
}
```
If you see a 502 after a rebuild, check that the container name hasn't changed (`docker ps`).
## Rollback to v1
The last hardcoded (no-database) version is tagged `v1` in Git:
```bash
ssh chaulmark@10.4.0.205
cd ~/websites/deafmissoula.org
git checkout v1
docker compose down
docker compose up -d --build
```
v1 had no MongoDB container — `docker compose down` will cleanly remove the v2 stack. The MongoDB data volume (`mcdi-mongo-data`) is preserved unless you explicitly run `docker compose down -v`.
## Docker Volumes
| Volume | Purpose |
|--------|---------|
| `mcdi-mongo-data` | MongoDB data (persists across rebuilds) |
| `mcdi-uploads-data` | User-uploaded files via dashboard (`/uploads/`) |
Static assets baked into the image (photos, videos, PDFs from `client/public/`) are served directly from `client/build/` at their original paths (e.g. `/photos/0001.jpg`, `/minutes/minutes-04122025.pdf`). New uploads from the dashboard go to `/uploads/headshots/`, `/uploads/photos/`, etc.