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