From a164c1f9569bb4a2db7193f2fc345720917ad8dd Mon Sep 17 00:00:00 2001 From: friday-bot Date: Wed, 27 May 2026 16:49:10 -0600 Subject: [PATCH] Auth, drag-and-drop, image previews, rate limiting - Login by email; board members are dashboard users (full access) - Forced password change on first login; forgot/reset password via email - Auto-send welcome email with set-password link when board member created - 5 attempts / 5 min rate limit on login endpoint - Board + Minutes tabs: drag-and-drop card reordering, persisted to DB - PATCH /api/board/reorder and /api/minutes/reorder endpoints - New minutes auto-insert at top (order 0) - ImagePicker: inline preview thumbnail before upload (board, gallery, sponsors) - Fix Minutes public sort: proper date parsing instead of broken string compare - Remove server/dist/ from git tracking (.gitignore) --- .gitignore | 1 + client/src/components/Manage.tsx | 444 ++++- client/src/components/Minutes.tsx | 12 +- client/src/tailwind.css | 236 +++ docker-compose.dev.yml | 9 + server/dist/api/sendEmail.js | 42 - server/dist/api/sendEmail.js.map | 1 - server/dist/db.js | 15 - server/dist/db.js.map | 1 - server/dist/index.js | 50 - server/dist/index.js.map | 1 - server/dist/middleware/auth.js | 23 - server/dist/middleware/auth.js.map | 1 - server/dist/middleware/errorHandler.js | 20 - server/dist/middleware/errorHandler.js.map | 1 - server/dist/middleware/upload.js | 35 - server/dist/middleware/upload.js.map | 1 - server/dist/models/BoardMember.js | 47 - server/dist/models/BoardMember.js.map | 1 - server/dist/models/Event.js | 49 - server/dist/models/Event.js.map | 1 - server/dist/models/GalleryItem.js | 44 - server/dist/models/GalleryItem.js.map | 1 - server/dist/models/Member.js | 50 - server/dist/models/Member.js.map | 1 - server/dist/models/Minute.js | 45 - server/dist/models/Minute.js.map | 1 - server/dist/models/Sponsor.js | 48 - server/dist/models/Sponsor.js.map | 1 - server/dist/models/User.js | 46 - server/dist/models/User.js.map | 1 - server/dist/routes/auth.js | 75 - server/dist/routes/auth.js.map | 1 - server/dist/routes/board.js | 44 - server/dist/routes/board.js.map | 1 - server/dist/routes/email.js | 34 - server/dist/routes/email.js.map | 1 - server/dist/routes/events.js | 41 - server/dist/routes/events.js.map | 1 - server/dist/routes/gallery.js | 37 - server/dist/routes/gallery.js.map | 1 - server/dist/routes/members.js | 42 - server/dist/routes/members.js.map | 1 - server/dist/routes/minutes.js | 31 - server/dist/routes/minutes.js.map | 1 - server/dist/routes/sponsors.js | 45 - server/dist/routes/sponsors.js.map | 1 - server/dist/seed.js | 116 -- server/dist/seed.js.map | 1 - server/package-lock.json | 2070 ++++++++++++++++++++ server/package.json | 1 + server/src/models/BoardMember.ts | 20 +- server/src/models/Minute.ts | 2 + server/src/routes/auth.ts | 170 +- server/src/routes/board.ts | 27 +- server/src/routes/minutes.ts | 14 +- server/src/seed.ts | 14 +- stack.env | 3 + 58 files changed, 2866 insertions(+), 1158 deletions(-) create mode 100644 docker-compose.dev.yml delete mode 100644 server/dist/api/sendEmail.js delete mode 100644 server/dist/api/sendEmail.js.map delete mode 100644 server/dist/db.js delete mode 100644 server/dist/db.js.map delete mode 100644 server/dist/index.js delete mode 100644 server/dist/index.js.map delete mode 100644 server/dist/middleware/auth.js delete mode 100644 server/dist/middleware/auth.js.map delete mode 100644 server/dist/middleware/errorHandler.js delete mode 100644 server/dist/middleware/errorHandler.js.map delete mode 100644 server/dist/middleware/upload.js delete mode 100644 server/dist/middleware/upload.js.map delete mode 100644 server/dist/models/BoardMember.js delete mode 100644 server/dist/models/BoardMember.js.map delete mode 100644 server/dist/models/Event.js delete mode 100644 server/dist/models/Event.js.map delete mode 100644 server/dist/models/GalleryItem.js delete mode 100644 server/dist/models/GalleryItem.js.map delete mode 100644 server/dist/models/Member.js delete mode 100644 server/dist/models/Member.js.map delete mode 100644 server/dist/models/Minute.js delete mode 100644 server/dist/models/Minute.js.map delete mode 100644 server/dist/models/Sponsor.js delete mode 100644 server/dist/models/Sponsor.js.map delete mode 100644 server/dist/models/User.js delete mode 100644 server/dist/models/User.js.map delete mode 100644 server/dist/routes/auth.js delete mode 100644 server/dist/routes/auth.js.map delete mode 100644 server/dist/routes/board.js delete mode 100644 server/dist/routes/board.js.map delete mode 100644 server/dist/routes/email.js delete mode 100644 server/dist/routes/email.js.map delete mode 100644 server/dist/routes/events.js delete mode 100644 server/dist/routes/events.js.map delete mode 100644 server/dist/routes/gallery.js delete mode 100644 server/dist/routes/gallery.js.map delete mode 100644 server/dist/routes/members.js delete mode 100644 server/dist/routes/members.js.map delete mode 100644 server/dist/routes/minutes.js delete mode 100644 server/dist/routes/minutes.js.map delete mode 100644 server/dist/routes/sponsors.js delete mode 100644 server/dist/routes/sponsors.js.map delete mode 100644 server/dist/seed.js delete mode 100644 server/dist/seed.js.map create mode 100644 server/package-lock.json diff --git a/.gitignore b/.gitignore index ea90594..a4af855 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store build/ node_modules/ +server/dist/ diff --git a/client/src/components/Manage.tsx b/client/src/components/Manage.tsx index e378d09..072fdd5 100644 --- a/client/src/components/Manage.tsx +++ b/client/src/components/Manage.tsx @@ -1,16 +1,17 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; // ── Types ──────────────────────────────────────────────────────────────────── -interface User { id: string; username: string; name: string; role: string; } +interface User { id: string; name: string; email: string; role: string; } interface BoardMember { _id: string; name: string; position: string; email: string; image: string; bio: string; order: number; } interface Event { _id: string; date: string; title: string; description: string; startTime: string; endTime: string; pointOfContact: string; email: string; address: string; } interface GalleryItem { _id: string; imageUrl: string; alt: string; order: number; } interface Sponsor { _id: string; name: string; logoPath: string; description: string; websiteUrl?: string; facebookUrl?: string; instagramUrl?: string; order: number; } -interface Minute { _id: string; date: string; meetingType: string; location: string; fileUrl: string; } +interface Minute { _id: string; date: string; meetingType: string; location: string; fileUrl: string; order: number; } interface Member { _id: string; firstName: string; lastName: string; email: string; phone?: string; address?: string; membershipType: string; status: string; joinDate: string; notes?: string; } type Tab = 'board' | 'events' | 'gallery' | 'sponsors' | 'minutes' | 'members'; +type AuthScreen = 'login' | 'forgot' | 'reset' | 'changePassword'; // ── Auth helpers ───────────────────────────────────────────────────────────── @@ -24,52 +25,14 @@ async function api(path: string, opts?: RequestInit): Promise { return res.json(); } -// ── Login screen ───────────────────────────────────────────────────────────── - -const Login: React.FC<{ onLogin: (token: string, user: User) => void }> = ({ onLogin }) => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - const submit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); setError(''); - try { - const data = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }), - }).then(r => { if (!r.ok) throw new Error('Invalid credentials'); return r.json(); }); - onLogin(data.token, data.user); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : 'Login failed'); - } finally { - setLoading(false); - } - }; - - return ( -
-
-

MCDi Management

-
- setUsername(e.target.value)} required /> - setPassword(e.target.value)} required /> - {error &&

{error}

} - -
-
-
- ); -}; - // ── Shared UI ───────────────────────────────────────────────────────────────── -const Btn: React.FC<{ onClick?: () => void; type?: 'button'|'submit'; color?: string; disabled?: boolean; children: React.ReactNode }> = ({ onClick, type = 'button', color = 'bg-[#8C1D40] hover:bg-[#6B1631]', disabled, children }) => ( - +const Btn: React.FC<{ onClick?: () => void; type?: 'button'|'submit'; color?: string; disabled?: boolean; small?: boolean; children: React.ReactNode }> = + ({ onClick, type = 'button', color = 'bg-[#8C1D40] hover:bg-[#6B1631]', disabled, small, children }) => ( + ); const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( @@ -87,6 +50,236 @@ const Textarea: React.FC> = (p