deafmissoula-website/README.md

5.7 KiB

Missoula Council of the Deaf, Inc. (MCDi) Website

Official website for MCDi at 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)

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):

GOOGLE_EMAIL=system@deafgain.org
GOOGLE_APP_PASSWORD=...

Local Development (Preview)

# 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/

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:

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.