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)
This commit is contained in:
friday-bot 2026-05-27 16:49:10 -06:00
parent c8a9b7b1da
commit a164c1f956
58 changed files with 2866 additions and 1158 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.DS_Store
build/
node_modules/
server/dist/

View file

@ -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<T>(path: string, opts?: RequestInit): Promise<T> {
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 (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-md p-8 w-full max-w-sm">
<h1 className="text-2xl font-bold text-[#8C1D40] mb-6 text-center">MCDi Management</h1>
<form onSubmit={submit} className="space-y-4">
<input className="w-full border rounded px-3 py-2" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} required />
<input className="w-full border rounded px-3 py-2" type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} required />
{error && <p className="text-red-600 text-sm">{error}</p>}
<button className="w-full bg-[#8C1D40] text-white py-2 rounded hover:bg-[#6B1631] transition" disabled={loading}>
{loading ? 'Signing in…' : 'Sign In'}
</button>
</form>
</div>
</div>
);
};
// ── 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 }) => (
<button type={type} onClick={onClick} disabled={disabled} className={`${color} text-white text-sm px-3 py-1.5 rounded transition disabled:opacity-50`}>{children}</button>
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 }) => (
<button type={type} onClick={onClick} disabled={disabled}
className={`${color} text-white ${small ? 'text-xs px-2 py-1' : 'text-sm px-3 py-1.5'} rounded transition disabled:opacity-50`}>
{children}
</button>
);
const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
@ -87,6 +50,236 @@ const Textarea: React.FC<React.TextareaHTMLAttributes<HTMLTextAreaElement>> = (p
<textarea {...props} className={`w-full border rounded px-2 py-1.5 text-sm ${props.className ?? ''}`} />
);
// Image file picker with instant preview
const ImagePicker: React.FC<{
label: string;
currentSrc?: string;
onFile: (f: File | null) => void;
}> = ({ label, currentSrc, onFile }) => {
const [preview, setPreview] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
onFile(file);
if (file) {
const url = URL.createObjectURL(file);
setPreview(url);
} else {
setPreview(null);
}
};
const displaySrc = preview ?? currentSrc;
return (
<Field label={label}>
<div className="flex items-start gap-3">
{displaySrc && (
<img src={displaySrc} alt="preview" className="w-16 h-16 object-cover rounded border flex-shrink-0" />
)}
<div className="flex-1">
<Input type="file" accept="image/*" onChange={handleChange} />
{!preview && currentSrc && <p className="text-xs text-gray-400 mt-1">Current photo kept unless you choose a new one</p>}
</div>
</div>
</Field>
);
};
// Drag-and-drop list — wraps any array of items
function useDragReorder<T extends { _id: string }>(
items: T[],
setItems: React.Dispatch<React.SetStateAction<T[]>>,
saveOrder: (ids: string[]) => Promise<void>
) {
const dragIndex = useRef<number | null>(null);
const [dragOver, setDragOver] = useState<number | null>(null);
const onDragStart = (i: number) => { dragIndex.current = i; };
const onDragOver = (e: React.DragEvent, i: number) => { e.preventDefault(); setDragOver(i); };
const onDrop = async (i: number) => {
const from = dragIndex.current;
if (from === null || from === i) { setDragOver(null); return; }
const next = [...items];
const [moved] = next.splice(from, 1);
next.splice(i, 0, moved);
setItems(next);
setDragOver(null);
dragIndex.current = null;
await saveOrder(next.map(x => x._id));
};
const onDragEnd = () => setDragOver(null);
return { dragOver, onDragStart, onDragOver, onDrop, onDragEnd };
}
// ── Auth screens ──────────────────────────────────────────────────────────────
const Login: React.FC<{ onLogin: (token: string, user: User) => void; onTempToken: (t: string) => void; onForgot: () => void }> = ({ onLogin, onTempToken, onForgot }) => {
const [email, setEmail] = 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: { token?: string; user?: User; mustChangePassword?: boolean; tempToken?: string } = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}).then(r => r.json().then(d => { if (!r.ok) throw new Error(d.error ?? 'Login failed'); return d; }));
if (data.mustChangePassword && data.tempToken) {
onTempToken(data.tempToken);
} else if (data.token && data.user) {
onLogin(data.token, data.user);
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Login failed');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-md p-8 w-full max-w-sm">
<h1 className="text-2xl font-bold text-[#8C1D40] mb-2 text-center">MCDi Management</h1>
<p className="text-sm text-gray-500 text-center mb-6">Sign in with your board email</p>
<form onSubmit={submit} className="space-y-4">
<Input type="email" placeholder="Email address" value={email} onChange={e => setEmail(e.target.value)} required />
<Input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} required />
{error && <p className="text-red-600 text-sm">{error}</p>}
<button className="w-full bg-[#8C1D40] text-white py-2 rounded hover:bg-[#6B1631] transition" disabled={loading}>
{loading ? 'Signing in…' : 'Sign In'}
</button>
</form>
<button onClick={onForgot} className="w-full text-center text-sm text-[#8C1D40] hover:underline mt-4">
Forgot password / Set your password
</button>
</div>
</div>
);
};
const ForgotPassword: React.FC<{ onBack: () => void }> = ({ onBack }) => {
const [email, setEmail] = useState('');
const [sent, setSent] = useState(false);
const [loading, setLoading] = useState(false);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
await fetch('/api/auth/forgot-password', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }),
});
setSent(true); setLoading(false);
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-md p-8 w-full max-w-sm">
<h1 className="text-xl font-bold text-[#8C1D40] mb-4 text-center">Reset Password</h1>
{sent ? (
<div className="text-center">
<p className="text-green-700 mb-4">If that email is registered, a reset link has been sent. Check your inbox.</p>
<button onClick={onBack} className="text-sm text-[#8C1D40] hover:underline"> Back to sign in</button>
</div>
) : (
<form onSubmit={submit} className="space-y-4">
<p className="text-sm text-gray-500">Enter your board email address and we'll send you a link to set your password.</p>
<Input type="email" placeholder="Your board email" value={email} onChange={e => setEmail(e.target.value)} required />
<button className="w-full bg-[#8C1D40] text-white py-2 rounded hover:bg-[#6B1631] transition" disabled={loading}>
{loading ? 'Sending…' : 'Send Reset Link'}
</button>
<button type="button" onClick={onBack} className="w-full text-center text-sm text-gray-500 hover:underline"> Back to sign in</button>
</form>
)}
</div>
</div>
);
};
const ResetPassword: React.FC<{ token: string; onLogin: (token: string, user: User) => void }> = ({ token, onLogin }) => {
const [password, setPassword] = useState('');
const [confirm, setConfirm] = 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/reset-password', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password, confirmPassword: confirm }),
}).then(r => r.json().then(d => { if (!r.ok) throw new Error(d.error ?? 'Failed'); return d; }));
localStorage.setItem('mcdi_token', data.token);
window.history.replaceState({}, '', '/manage');
onLogin(data.token, data.user);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed');
} finally { setLoading(false); }
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-md p-8 w-full max-w-sm">
<h1 className="text-xl font-bold text-[#8C1D40] mb-4 text-center">Set Your Password</h1>
<form onSubmit={submit} className="space-y-4">
<Input type="password" placeholder="New password (min 8 chars)" value={password} onChange={e => setPassword(e.target.value)} required minLength={8} />
<Input type="password" placeholder="Confirm password" value={confirm} onChange={e => setConfirm(e.target.value)} required />
{error && <p className="text-red-600 text-sm">{error}</p>}
<button className="w-full bg-[#8C1D40] text-white py-2 rounded hover:bg-[#6B1631] transition" disabled={loading}>
{loading ? 'Saving…' : 'Set Password & Sign In'}
</button>
</form>
</div>
</div>
);
};
const ForceChangePassword: React.FC<{ tempToken: string; onLogin: (token: string, user: User) => void }> = ({ tempToken, onLogin }) => {
const [password, setPassword] = useState('');
const [confirm, setConfirm] = 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/change-password', {
method: 'POST',
headers: { Authorization: `Bearer ${tempToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ password, confirmPassword: confirm }),
}).then(r => r.json().then(d => { if (!r.ok) throw new Error(d.error ?? 'Failed'); return d; }));
localStorage.setItem('mcdi_token', data.token);
onLogin(data.token, data.user);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed');
} finally { setLoading(false); }
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-md p-8 w-full max-w-sm">
<h1 className="text-xl font-bold text-[#8C1D40] mb-2 text-center">Welcome to MCDi Management</h1>
<p className="text-sm text-gray-500 text-center mb-6">Please choose a password to get started.</p>
<form onSubmit={submit} className="space-y-4">
<Input type="password" placeholder="New password (min 8 chars)" value={password} onChange={e => setPassword(e.target.value)} required minLength={8} />
<Input type="password" placeholder="Confirm password" value={confirm} onChange={e => setConfirm(e.target.value)} required />
{error && <p className="text-red-600 text-sm">{error}</p>}
<button className="w-full bg-[#8C1D40] text-white py-2 rounded hover:bg-[#6B1631] transition" disabled={loading}>
{loading ? 'Saving…' : 'Set Password & Continue'}
</button>
</form>
</div>
</div>
);
};
// ── Board tab ─────────────────────────────────────────────────────────────────
const BoardTab: React.FC = () => {
@ -97,20 +290,25 @@ const BoardTab: React.FC = () => {
const load = useCallback(() => api<BoardMember[]>('/board').then(setMembers), []);
useEffect(() => { load(); }, [load]);
const saveOrder = useCallback(async (ids: string[]) => {
await api('/board/reorder', { method: 'PATCH', body: JSON.stringify({ ids }) });
}, []);
const drag = useDragReorder(members, setMembers, saveOrder);
const save = async (e: React.FormEvent) => {
e.preventDefault();
if (!editing) return;
const form = new FormData();
Object.entries(editing).forEach(([k, v]) => { if (k !== '_id' && v !== undefined) form.append(k, String(v)); });
if (imageFile) form.append('image', imageFile);
const token = getToken();
const url = editing._id ? `/api/board/${editing._id}` : '/api/board';
await fetch(url, { method: editing._id ? 'PUT' : 'POST', headers: { Authorization: `Bearer ${token}` }, body: form });
await fetch(url, { method: editing._id ? 'PUT' : 'POST', headers: { Authorization: `Bearer ${getToken()}` }, body: form });
setEditing(null); setImageFile(null); load();
};
const del = async (id: string) => {
if (!window.confirm('Remove this board member?')) return;
if (!window.confirm('Remove this board member? They will lose dashboard access.')) return;
await api(`/board/${id}`, { method: 'DELETE' });
load();
};
@ -118,35 +316,42 @@ const BoardTab: React.FC = () => {
return (
<div>
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-xl font-bold text-[#8C1D40]">Board Members</h2>
<Btn onClick={() => setEditing({ name:'', position:'', email:'', image:'', bio:'', order:0 })}>+ Add Member</Btn>
<p className="text-xs text-gray-500 mt-0.5">Drag cards to reorder. New members receive a welcome email to set their password.</p>
</div>
<Btn onClick={() => setEditing({ name:'', position:'', email:'', image:'', bio:'', order: members.length })}>+ Add Member</Btn>
</div>
{editing && (
<form onSubmit={save} className="bg-gray-50 border rounded p-4 mb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<Field label="Name"><Input required value={editing.name ?? ''} onChange={e => setEditing(p => ({ ...p, name: e.target.value }))} /></Field>
<Field label="Position"><Input required value={editing.position ?? ''} onChange={e => setEditing(p => ({ ...p, position: e.target.value }))} /></Field>
<Field label="Email"><Input required type="email" value={editing.email ?? ''} onChange={e => setEditing(p => ({ ...p, email: e.target.value }))} /></Field>
<Field label="Display Order"><Input type="number" value={editing.order ?? 0} onChange={e => setEditing(p => ({ ...p, order: +e.target.value }))} /></Field>
<Field label="Photo (upload new or keep existing)">
<Input type="file" accept="image/*" onChange={e => setImageFile(e.target.files?.[0] ?? null)} />
{editing.image && !imageFile && <p className="text-xs text-gray-500 mt-1">Current: {editing.image}</p>}
</Field>
<Field label="Email (used to log into dashboard)"><Input required type="email" value={editing.email ?? ''} onChange={e => setEditing(p => ({ ...p, email: e.target.value }))} /></Field>
<ImagePicker label="Photo" currentSrc={editing.image} onFile={setImageFile} />
<Field label="Bio" ><Textarea required rows={4} value={editing.bio ?? ''} onChange={e => setEditing(p => ({ ...p, bio: e.target.value }))} /></Field>
<div className="md:col-span-2 flex gap-2">
<Btn type="submit">Save</Btn>
<Btn type="submit">Save{!editing._id ? ' & Send Welcome Email' : ''}</Btn>
<Btn color="bg-gray-400 hover:bg-gray-500" onClick={() => { setEditing(null); setImageFile(null); }}>Cancel</Btn>
</div>
</form>
)}
<div className="space-y-2">
{members.map(m => (
<div key={m._id} className="flex items-center gap-3 bg-white border rounded p-3">
{members.map((m, i) => (
<div key={m._id}
draggable
onDragStart={() => drag.onDragStart(i)}
onDragOver={e => drag.onDragOver(e, i)}
onDrop={() => drag.onDrop(i)}
onDragEnd={drag.onDragEnd}
className={`flex items-center gap-3 bg-white border rounded p-3 cursor-grab active:cursor-grabbing transition-all ${drag.dragOver === i ? 'border-[#8C1D40] bg-[#8C1D40]/5' : ''}`}
>
<span className="text-gray-300 select-none"></span>
<img src={m.image} alt={m.name} className="w-12 h-12 rounded-full object-cover flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{m.name}</p>
<p className="text-sm text-gray-500">{m.position}</p>
<p className="text-sm text-gray-500">{m.position} · {m.email}</p>
</div>
<div className="flex gap-2 flex-shrink-0">
<Btn onClick={() => setEditing(m)}>Edit</Btn>
@ -232,19 +437,24 @@ const GalleryTab: React.FC = () => {
const [items, setItems] = useState<GalleryItem[]>([]);
const [imageFile, setImageFile] = useState<File | null>(null);
const [alt, setAlt] = useState('MCDi community event photo');
const [preview, setPreview] = useState<string | null>(null);
const load = useCallback(() => api<GalleryItem[]>('/gallery').then(setItems), []);
useEffect(() => { load(); }, [load]);
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
setImageFile(file);
setPreview(file ? URL.createObjectURL(file) : null);
};
const add = async (e: React.FormEvent) => {
e.preventDefault();
if (!imageFile) return;
const form = new FormData();
form.append('image', imageFile);
form.append('alt', alt);
const token = getToken();
await fetch('/api/gallery', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form });
setImageFile(null); setAlt('MCDi community event photo'); load();
form.append('image', imageFile); form.append('alt', alt);
await fetch('/api/gallery', { method: 'POST', headers: { Authorization: `Bearer ${getToken()}` }, body: form });
setImageFile(null); setPreview(null); setAlt('MCDi community event photo'); load();
};
const del = async (id: string) => {
@ -257,7 +467,12 @@ const GalleryTab: React.FC = () => {
<div>
<h2 className="text-xl font-bold text-[#8C1D40] mb-4">Gallery</h2>
<form onSubmit={add} className="bg-gray-50 border rounded p-4 mb-4 flex flex-wrap gap-3 items-end">
<Field label="Photo"><Input required type="file" accept="image/*" onChange={e => setImageFile(e.target.files?.[0] ?? null)} /></Field>
<div className="flex items-start gap-3">
{preview && <img src={preview} alt="preview" className="w-20 h-20 object-cover rounded border" />}
<Field label="Photo">
<Input required type="file" accept="image/*" onChange={handleFile} />
</Field>
</div>
<Field label="Alt text"><Input value={alt} onChange={e => setAlt(e.target.value)} /></Field>
<Btn type="submit" disabled={!imageFile}>Upload Photo</Btn>
</form>
@ -290,9 +505,8 @@ const SponsorsTab: React.FC = () => {
const form = new FormData();
Object.entries(editing).forEach(([k, v]) => { if (k !== '_id' && v !== undefined) form.append(k, String(v)); });
if (logoFile) form.append('logo', logoFile);
const token = getToken();
const url = editing._id ? `/api/sponsors/${editing._id}` : '/api/sponsors';
await fetch(url, { method: editing._id ? 'PUT' : 'POST', headers: { Authorization: `Bearer ${token}` }, body: form });
await fetch(url, { method: editing._id ? 'PUT' : 'POST', headers: { Authorization: `Bearer ${getToken()}` }, body: form });
setEditing(null); setLogoFile(null); load();
};
@ -306,17 +520,13 @@ const SponsorsTab: React.FC = () => {
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-[#8C1D40]">Sponsors</h2>
<Btn onClick={() => setEditing({ name:'', logoPath:'', description:'', websiteUrl:'', facebookUrl:'', instagramUrl:'', order:0 })}>+ Add Sponsor</Btn>
<Btn onClick={() => setEditing({ name:'', logoPath:'', description:'', websiteUrl:'', facebookUrl:'', instagramUrl:'', order: sponsors.length })}>+ Add Sponsor</Btn>
</div>
{editing && (
<form onSubmit={save} className="bg-gray-50 border rounded p-4 mb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<Field label="Name"><Input required value={editing.name ?? ''} onChange={e => setEditing(p => ({ ...p, name: e.target.value }))} /></Field>
<Field label="Display Order"><Input type="number" value={editing.order ?? 0} onChange={e => setEditing(p => ({ ...p, order: +e.target.value }))} /></Field>
<Field label="Logo (upload or keep existing)">
<Input type="file" accept="image/*" onChange={e => setLogoFile(e.target.files?.[0] ?? null)} />
{editing.logoPath && !logoFile && <p className="text-xs text-gray-500 mt-1">Current: {editing.logoPath}</p>}
</Field>
<ImagePicker label="Logo" currentSrc={editing.logoPath} onFile={setLogoFile} />
<Field label="Description"><Textarea required rows={3} value={editing.description ?? ''} onChange={e => setEditing(p => ({ ...p, description: e.target.value }))} /></Field>
<Field label="Website URL"><Input type="url" value={editing.websiteUrl ?? ''} onChange={e => setEditing(p => ({ ...p, websiteUrl: e.target.value }))} /></Field>
<Field label="Facebook URL"><Input type="url" value={editing.facebookUrl ?? ''} onChange={e => setEditing(p => ({ ...p, facebookUrl: e.target.value }))} /></Field>
@ -357,14 +567,19 @@ const MinutesTab: React.FC = () => {
const load = useCallback(() => api<Minute[]>('/minutes').then(setMinutes), []);
useEffect(() => { load(); }, [load]);
const saveOrder = useCallback(async (ids: string[]) => {
await api('/minutes/reorder', { method: 'PATCH', body: JSON.stringify({ ids }) });
}, []);
const drag = useDragReorder(minutes, setMinutes, saveOrder);
const add = async (e: React.FormEvent) => {
e.preventDefault();
if (!pdfFile) return;
const fd = new FormData();
Object.entries(form).forEach(([k, v]) => fd.append(k, v));
fd.append('file', pdfFile);
const token = getToken();
await fetch('/api/minutes', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: fd });
await fetch('/api/minutes', { method: 'POST', headers: { Authorization: `Bearer ${getToken()}` }, body: fd });
setPdfFile(null); setForm({ date:'', meetingType:'Board Meeting', location:'' }); load();
};
@ -376,7 +591,11 @@ const MinutesTab: React.FC = () => {
return (
<div>
<h2 className="text-xl font-bold text-[#8C1D40] mb-4">Meeting Minutes</h2>
<div className="mb-4">
<h2 className="text-xl font-bold text-[#8C1D40]">Meeting Minutes</h2>
<p className="text-xs text-gray-500 mt-0.5">Drag cards to reorder how they appear on the public site.</p>
</div>
<form onSubmit={add} className="bg-gray-50 border rounded p-4 mb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<Field label="Date (MM/DD/YYYY)"><Input required value={form.date} onChange={e => setForm(p => ({ ...p, date: e.target.value }))} placeholder="01/15/2026" /></Field>
<Field label="Meeting Type">
@ -394,12 +613,22 @@ const MinutesTab: React.FC = () => {
</form>
<div className="space-y-2">
{minutes.map(m => (
<div key={m._id} className="flex items-center justify-between bg-white border rounded p-3">
{minutes.map((m, i) => (
<div key={m._id}
draggable
onDragStart={() => drag.onDragStart(i)}
onDragOver={e => drag.onDragOver(e, i)}
onDrop={() => drag.onDrop(i)}
onDragEnd={drag.onDragEnd}
className={`flex items-center justify-between bg-white border rounded p-3 cursor-grab active:cursor-grabbing transition-all ${drag.dragOver === i ? 'border-[#8C1D40] bg-[#8C1D40]/5' : ''}`}
>
<div className="flex items-center gap-3">
<span className="text-gray-300 select-none"></span>
<div>
<p className="font-medium">{m.meetingType} {m.date}</p>
<p className="text-sm text-gray-500">{m.location}</p>
</div>
</div>
<div className="flex gap-2">
<a href={m.fileUrl} target="_blank" rel="noreferrer" className="bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-1.5 rounded transition">View</a>
<Btn color="bg-red-600 hover:bg-red-700" onClick={() => del(m._id)}>Delete</Btn>
@ -482,11 +711,7 @@ const MembersTab: React.FC = () => {
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-[#8C1D40] text-white">
<tr>
{['Name','Email','Type','Status','Joined',''].map(h => (
<th key={h} className="py-2 px-3 text-left">{h}</th>
))}
</tr>
<tr>{['Name','Email','Type','Status','Joined',''].map(h => <th key={h} className="py-2 px-3 text-left">{h}</th>)}</tr>
</thead>
<tbody>
{filtered.map(m => (
@ -524,7 +749,6 @@ const TABS: { id: Tab; label: string }[] = [
const Dashboard: React.FC<{ user: User; onLogout: () => void }> = ({ user, onLogout }) => {
const [tab, setTab] = useState<Tab>('board');
return (
<div className="min-h-screen bg-gray-100">
<header className="bg-[#8C1D40] text-white px-4 py-3 flex items-center justify-between">
@ -540,11 +764,8 @@ const Dashboard: React.FC<{ user: User; onLogout: () => void }> = ({ user, onLog
<div className="flex border-b bg-white px-4 gap-1 overflow-x-auto">
{TABS.map(t => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition whitespace-nowrap ${tab === t.id ? 'border-[#8C1D40] text-[#8C1D40]' : 'border-transparent text-gray-600 hover:text-gray-900'}`}
>
<button key={t.id} onClick={() => setTab(t.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition whitespace-nowrap ${tab === t.id ? 'border-[#8C1D40] text-[#8C1D40]' : 'border-transparent text-gray-600 hover:text-gray-900'}`}>
{t.label}
</button>
))}
@ -567,23 +788,34 @@ const Dashboard: React.FC<{ user: User; onLogout: () => void }> = ({ user, onLog
const Manage: React.FC = () => {
const [token, setToken] = useState<string | null>(getToken);
const [user, setUser] = useState<User | null>(null);
const [screen, setScreen] = useState<AuthScreen>('login');
const [tempToken, setTempToken] = useState<string | null>(null);
// Detect /manage/reset-password?token=xxx
const resetToken = new URLSearchParams(window.location.search).get('token');
const isResetPage = window.location.pathname.includes('/reset-password') && !!resetToken;
useEffect(() => {
if (!token) return;
api<User>('/auth/me').then(setUser).catch(() => { localStorage.removeItem('mcdi_token'); setToken(null); });
api<User>('/auth/me').then(setUser).catch(() => {
localStorage.removeItem('mcdi_token'); setToken(null);
});
}, [token]);
const handleLogin = (t: string, u: User) => {
localStorage.setItem('mcdi_token', t);
setToken(t); setUser(u);
setToken(t); setUser(u); setScreen('login'); setTempToken(null);
};
const handleLogout = () => {
localStorage.removeItem('mcdi_token');
setToken(null); setUser(null);
setToken(null); setUser(null); setScreen('login');
};
if (!token || !user) return <Login onLogin={handleLogin} />;
if (isResetPage) return <ResetPassword token={resetToken!} onLogin={handleLogin} />;
if (tempToken) return <ForceChangePassword tempToken={tempToken} onLogin={handleLogin} />;
if (screen === 'forgot') return <ForgotPassword onBack={() => setScreen('login')} />;
if (!token || !user) return <Login onLogin={handleLogin} onTempToken={t => { setTempToken(t); }} onForgot={() => setScreen('forgot')} />;
return <Dashboard user={user} onLogout={handleLogout} />;
};

View file

@ -35,15 +35,13 @@ const Minutes: React.FC = () => {
const sortedMinutes = [...minutesData].sort((a, b) => {
if (sortColumn === 'date') {
// Convert date strings to Date objects for proper comparison
const dateA = new Date(a.date.split('/').map((part, i) => i === 0 ? part : i === 1 ? part : part).join('/'));
const dateB = new Date(b.date.split('/').map((part, i) => i === 0 ? part : i === 1 ? part : part).join('/'));
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return sortDirection === 'asc' ? dateA.getTime() - dateB.getTime() : dateB.getTime() - dateA.getTime();
} else {
}
if (a[sortColumn] < b[sortColumn]) return sortDirection === 'asc' ? -1 : 1;
if (a[sortColumn] > b[sortColumn]) return sortDirection === 'asc' ? 1 : -1;
return 0;
}
});
const handleSort = (column: keyof MinuteEntry) => {

View file

@ -748,9 +748,15 @@ select {
.right-0 {
right: 0px;
}
.right-1 {
right: 0.25rem;
}
.right-2 {
right: 0.5rem;
}
.top-1 {
top: 0.25rem;
}
.top-2 {
top: 0.5rem;
}
@ -768,9 +774,18 @@ select {
margin-left: auto;
margin-right: auto;
}
.mb-12 {
margin-bottom: 3rem;
}
.mb-16 {
margin-bottom: 4rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}
@ -783,9 +798,18 @@ select {
.ml-1 {
margin-left: 0.25rem;
}
.mr-1 {
margin-right: 0.25rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.mt-0\.5 {
margin-top: 0.125rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@ -837,6 +861,9 @@ select {
.h-16 {
height: 4rem;
}
.h-20 {
height: 5rem;
}
.h-32 {
height: 8rem;
}
@ -879,6 +906,12 @@ select {
.w-12 {
width: 3rem;
}
.w-16 {
width: 4rem;
}
.w-20 {
width: 5rem;
}
.w-32 {
width: 8rem;
}
@ -897,21 +930,39 @@ select {
.w-full {
width: 100%;
}
.min-w-0 {
min-width: 0px;
}
.max-w-4xl {
max-width: 56rem;
}
.max-w-5xl {
max-width: 64rem;
}
.max-w-6xl {
max-width: 72rem;
}
.max-w-7xl {
max-width: 80rem;
}
.max-w-full {
max-width: 100%;
}
.max-w-md {
max-width: 28rem;
}
.max-w-sm {
max-width: 24rem;
}
.max-w-xs {
max-width: 20rem;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
@ -927,9 +978,16 @@ select {
.animate-spin {
animation: spin 1s linear infinite;
}
.cursor-grab {
cursor: grab;
}
.cursor-pointer {
cursor: pointer;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.resize {
resize: both;
}
@ -957,6 +1015,9 @@ select {
.items-start {
align-items: flex-start;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
@ -978,6 +1039,9 @@ select {
.gap-2 {
gap: 0.5rem;
}
.gap-3 {
gap: 0.75rem;
}
.gap-4 {
gap: 1rem;
}
@ -1002,6 +1066,11 @@ select {
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.space-y-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
@ -1024,9 +1093,15 @@ select {
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
.whitespace-pre-line {
white-space: pre-line;
}
.break-all {
word-break: break-all;
}
.rounded {
border-radius: 0.25rem;
}
@ -1054,6 +1129,9 @@ select {
.border-b-2 {
border-bottom-width: 2px;
}
.border-t {
border-top-width: 1px;
}
.border-t-2 {
border-top-width: 2px;
}
@ -1088,10 +1166,17 @@ select {
--tw-bg-opacity: 1;
background-color: rgb(140 29 64 / var(--tw-bg-opacity, 1));
}
.bg-\[\#8C1D40\]\/5 {
background-color: rgb(140 29 64 / 0.05);
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1));
}
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
@ -1100,10 +1185,29 @@ select {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.bg-white\/20 {
background-color: rgb(255 255 255 / 0.2);
}
.bg-yellow-400 {
--tw-bg-opacity: 1;
background-color: rgb(250 204 21 / var(--tw-bg-opacity, 1));
@ -1156,14 +1260,37 @@ select {
.p-6 {
padding: 1.5rem;
}
.p-8 {
padding: 2rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-1\.5 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.py-12 {
padding-top: 3rem;
padding-bottom: 3rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@ -1180,6 +1307,12 @@ select {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pt-4 {
padding-top: 1rem;
}
.text-left {
text-align: left;
}
@ -1231,6 +1364,9 @@ select {
.font-semibold {
font-weight: 600;
}
.capitalize {
text-transform: capitalize;
}
.leading-relaxed {
line-height: 1.625;
}
@ -1254,6 +1390,18 @@ select {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
}
.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
@ -1266,10 +1414,22 @@ select {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.text-green-700 {
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
}
.text-green-800 {
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@ -1288,6 +1448,12 @@ select {
.accent-yellow-300 {
accent-color: #fde047;
}
.opacity-0 {
opacity: 0;
}
.opacity-75 {
opacity: 0.75;
}
.opacity-80 {
opacity: 0.8;
}
@ -1323,11 +1489,21 @@ select {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-opacity {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-shadow {
transition-property: box-shadow;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@ -1640,11 +1816,35 @@ html {
background-color: rgb(107 22 49 / var(--tw-bg-opacity, 1));
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity, 1));
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity, 1));
}
.hover\:bg-white\/30:hover {
background-color: rgb(255 255 255 / 0.3);
}
.hover\:bg-yellow-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(253 224 71 / var(--tw-bg-opacity, 1));
@ -1698,12 +1898,28 @@ html {
text-decoration-line: none;
}
.hover\:opacity-100:hover {
opacity: 1;
}
.hover\:shadow-xl:hover {
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.active\:cursor-grabbing:active {
cursor: grabbing;
}
.disabled\:opacity-50:disabled {
opacity: 0.5;
}
.group:hover .group-hover\:opacity-100 {
opacity: 1;
}
@media (min-width: 640px) {
.sm\:mb-4 {
@ -1734,6 +1950,10 @@ html {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.sm\:gap-8 {
gap: 2rem;
}
@ -1774,6 +1994,10 @@ html {
@media (min-width: 768px) {
.md\:col-span-2 {
grid-column: span 2 / span 2;
}
.md\:mb-0 {
margin-bottom: 0px;
}
@ -1826,6 +2050,10 @@ html {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.md\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.md\:gap-8 {
gap: 2rem;
}
@ -1884,6 +2112,10 @@ html {
width: 50%;
}
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@ -1892,6 +2124,10 @@ html {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.lg\:flex-row {
flex-direction: row;
}

9
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,9 @@
services:
app:
networks:
- internal
networks:
caddy_network:
name: caddy_network_dev
driver: bridge

View file

@ -1,42 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = handler;
const nodemailer_1 = __importDefault(require("nodemailer"));
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
const transporter = nodemailer_1.default.createTransport({
host: 'smtp.gmail.com',
port: 2525,
secure: false,
auth: {
user: process.env.GOOGLE_EMAIL,
pass: process.env.GOOGLE_APP_PASSWORD,
},
});
async function handler(req, res) {
if (req.method === 'POST') {
const { name, email, subject, message } = req.body;
const mailOptions = {
from: process.env.GOOGLE_EMAIL,
to: 'contact@deafmissoula.org',
subject: `MCDi Contact Form: ${subject}`,
text: `Name: ${name}\nEmail: ${email}\n\nMessage:\n${message}`,
};
try {
await transporter.sendMail(mailOptions);
res.status(200).json({ message: 'Email sent successfully' });
}
catch (error) {
console.error('Detailed error sending email:', error);
res.status(500).json({ message: 'Error sending email', error: error instanceof Error ? error.message : 'Unknown error' });
}
}
else {
res.setHeader('Allow', ['POST']);
res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
}
//# sourceMappingURL=sendEmail.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"sendEmail.js","sourceRoot":"","sources":["../../src/api/sendEmail.ts"],"names":[],"mappings":";;;;;AAgBA,0BAsBC;AArCD,4DAAoC;AACpC,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,WAAW,GAAG,oBAAU,CAAC,eAAe,CAAC;IAC7C,IAAI,EAAE,gBAAgB;IACtB,IAAI,EAAE,IAAI;IACV,MAAM,EAAE,KAAK;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;QAC9B,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;KACtC;CACF,CAAC,CAAC;AAEY,KAAK,UAAU,OAAO,CAAC,GAAY,EAAE,GAAa;IAC/D,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;QAEnD,MAAM,WAAW,GAAG;YAClB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;YAC9B,EAAE,EAAE,0BAA0B;YAC9B,OAAO,EAAE,sBAAsB,OAAO,EAAE;YACxC,IAAI,EAAE,SAAS,IAAI,YAAY,KAAK,iBAAiB,OAAO,EAAE;SAC/D,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACxC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;YACtD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QAC5H,CAAC;IACH,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;QACjC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,MAAM,cAAc,EAAE,CAAC,CAAC;IACxE,CAAC;AACH,CAAC"}

15
server/dist/db.js vendored
View file

@ -1,15 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.connectDB = connectDB;
const mongoose_1 = __importDefault(require("mongoose"));
async function connectDB() {
const uri = process.env.MONGODB_URI;
if (!uri)
throw new Error('MONGODB_URI is not defined');
await mongoose_1.default.connect(uri);
console.log('MongoDB connected');
}
//# sourceMappingURL=db.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":";;;;;AAEA,8BAMC;AARD,wDAAgC;AAEzB,KAAK,UAAU,SAAS;IAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IACpC,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAExD,MAAM,kBAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACnC,CAAC"}

50
server/dist/index.js vendored
View file

@ -1,50 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv/config");
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const path_1 = __importDefault(require("path"));
const db_1 = require("./db");
const errorHandler_1 = require("./middleware/errorHandler");
const auth_1 = __importDefault(require("./routes/auth"));
const board_1 = __importDefault(require("./routes/board"));
const events_1 = __importDefault(require("./routes/events"));
const gallery_1 = __importDefault(require("./routes/gallery"));
const sponsors_1 = __importDefault(require("./routes/sponsors"));
const minutes_1 = __importDefault(require("./routes/minutes"));
const members_1 = __importDefault(require("./routes/members"));
const email_1 = __importDefault(require("./routes/email"));
const app = (0, express_1.default)();
const port = process.env.PORT ?? 801;
app.use((0, cors_1.default)());
app.use(express_1.default.json());
// Serve uploaded files
app.use('/uploads', express_1.default.static(path_1.default.join(__dirname, '../../uploads')));
// API routes
app.use('/api/auth', auth_1.default);
app.use('/api/board', board_1.default);
app.use('/api/events', events_1.default);
app.use('/api/gallery', gallery_1.default);
app.use('/api/sponsors', sponsors_1.default);
app.use('/api/minutes', minutes_1.default);
app.use('/api/members', members_1.default);
app.use('/api', email_1.default);
// Serve built React app
const clientBuild = path_1.default.join(__dirname, '../../client/build');
app.use(express_1.default.static(clientBuild));
app.use((_req, res) => {
res.sendFile(path_1.default.join(clientBuild, 'index.html'));
});
app.use(errorHandler_1.errorHandler);
(0, db_1.connectDB)()
.then(() => {
app.listen(port, () => console.log(`Server running on port ${port}`));
})
.catch((err) => {
console.error('Failed to connect to MongoDB:', err);
process.exit(1);
});
//# sourceMappingURL=index.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,yBAAuB;AACvB,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AACxB,6BAAiC;AACjC,4DAAyD;AACzD,yDAA0C;AAC1C,2DAA2C;AAC3C,6DAA4C;AAC5C,+DAA6C;AAC7C,iEAA8C;AAC9C,+DAA6C;AAC7C,+DAA6C;AAC7C,2DAA2C;AAE3C,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC;AAErC,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;AAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,uBAAuB;AACvB,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,iBAAO,CAAC,MAAM,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;AAE3E,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,WAAW,EAAM,cAAU,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAK,eAAW,CAAC,CAAC;AACtC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAI,gBAAW,CAAC,CAAC;AACtC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAG,iBAAa,CAAC,CAAC;AACxC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,kBAAa,CAAC,CAAC;AACxC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAG,iBAAY,CAAC,CAAC;AACvC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAG,iBAAY,CAAC,CAAC;AACvC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAW,eAAW,CAAC,CAAC;AAEtC,wBAAwB;AACxB,MAAM,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;AAC/D,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IACpB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;AACrD,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;AAEtB,IAAA,cAAS,GAAE;KACR,IAAI,CAAC,GAAG,EAAE;IACT,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC,CAAC;AACxE,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}

View file

@ -1,23 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireAuth = requireAuth;
const jose_1 = require("jose");
const secret = () => new TextEncoder().encode(process.env.JWT_SECRET ?? 'fallback-dev-secret');
async function requireAuth(req, res, next) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const token = header.slice(7);
const { payload } = await (0, jose_1.jwtVerify)(token, secret());
req.userId = payload.userId;
req.userRole = payload.role;
next();
}
catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
//# sourceMappingURL=auth.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":";;AAUA,kCAgBC;AAzBD,+BAAiC;AAOjC,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,qBAAqB,CAAC,CAAC;AAExF,KAAK,UAAU,WAAW,CAAC,GAAgB,EAAE,GAAa,EAAE,IAAkB;IACnF,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAChD,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAA,gBAAS,EAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QACrD,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,MAAgB,CAAC;QACtC,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC,IAAc,CAAC;QACtC,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}

View file

@ -1,20 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.errorHandler = errorHandler;
const zod_1 = require("zod");
function errorHandler(err, req, res, next) {
if (err instanceof zod_1.ZodError) {
res.status(400).json({ error: 'Validation error', details: err.flatten() });
return;
}
if (err instanceof Error) {
const status = err.status ?? 500;
const message = status < 500 ? err.message : 'Internal server error';
if (status >= 500)
console.error(err);
res.status(status).json({ error: message });
return;
}
res.status(500).json({ error: 'Internal server error' });
}
//# sourceMappingURL=errorHandler.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":";;AAGA,oCAoBC;AAtBD,6BAA+B;AAE/B,SAAgB,YAAY,CAC1B,GAAY,EACZ,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,IAAI,GAAG,YAAY,cAAQ,EAAE,CAAC;QAC5B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC5E,OAAO;IACT,CAAC;IAED,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAI,GAAmD,CAAC,MAAM,IAAI,GAAG,CAAC;QAClF,MAAM,OAAO,GAAG,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;QACrE,IAAI,MAAM,IAAI,GAAG;YAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACtC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5C,OAAO;IACT,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;AAC3D,CAAC"}

View file

@ -1,35 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.uploadMinutes = exports.uploadLogo = exports.uploadPhoto = exports.uploadHeadshot = void 0;
const multer_1 = __importDefault(require("multer"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const uploadsBase = path_1.default.join(__dirname, '../../../uploads');
function storageFor(subdir) {
return multer_1.default.diskStorage({
destination: (_req, _file, cb) => {
const dir = path_1.default.join(uploadsBase, subdir);
fs_1.default.mkdirSync(dir, { recursive: true });
cb(null, dir);
},
filename: (_req, file, cb) => {
const unique = `${Date.now()}-${Math.round(Math.random() * 1e6)}`;
cb(null, unique + path_1.default.extname(file.originalname));
},
});
}
const imageFilter = (_req, file, cb) => {
const allowed = /jpeg|jpg|png|gif|webp/;
cb(null, allowed.test(path_1.default.extname(file.originalname).toLowerCase()) && allowed.test(file.mimetype));
};
const pdfFilter = (_req, file, cb) => {
cb(null, file.mimetype === 'application/pdf' || path_1.default.extname(file.originalname).toLowerCase() === '.pdf');
};
exports.uploadHeadshot = (0, multer_1.default)({ storage: storageFor('headshots'), fileFilter: imageFilter, limits: { fileSize: 5 * 1024 * 1024 } });
exports.uploadPhoto = (0, multer_1.default)({ storage: storageFor('photos'), fileFilter: imageFilter, limits: { fileSize: 10 * 1024 * 1024 } });
exports.uploadLogo = (0, multer_1.default)({ storage: storageFor('sponsors'), fileFilter: imageFilter, limits: { fileSize: 5 * 1024 * 1024 } });
exports.uploadMinutes = (0, multer_1.default)({ storage: storageFor('minutes'), fileFilter: pdfFilter, limits: { fileSize: 20 * 1024 * 1024 } });
//# sourceMappingURL=upload.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/middleware/upload.ts"],"names":[],"mappings":";;;;;;AAAA,oDAA4B;AAC5B,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;AAE7D,SAAS,UAAU,CAAC,MAAc;IAChC,OAAO,gBAAM,CAAC,WAAW,CAAC;QACxB,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;YAC/B,MAAM,GAAG,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAC3C,YAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChB,CAAC;QACD,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;YAC3B,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC;YAClE,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;QACrD,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,WAAW,GAAiC,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;IACnE,MAAM,OAAO,GAAG,uBAAuB,CAAC;IACxC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AACvG,CAAC,CAAC;AAEF,MAAM,SAAS,GAAiC,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;IACjE,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,KAAK,iBAAiB,IAAI,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC;AAC5G,CAAC,CAAC;AAEW,QAAA,cAAc,GAAG,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AAC9H,QAAA,WAAW,GAAM,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAK,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AAC/H,QAAA,UAAU,GAAO,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC,EAAG,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;AAC9H,QAAA,aAAa,GAAI,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,EAAI,UAAU,EAAE,SAAS,EAAI,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC"}

View file

@ -1,47 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoardMember = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const BoardMemberSchema = new mongoose_1.Schema({
name: { type: String, required: true, trim: true },
position: { type: String, required: true, trim: true },
email: { type: String, required: true, trim: true, lowercase: true },
image: { type: String, required: true },
bio: { type: String, required: true },
order: { type: Number, default: 0 },
}, { timestamps: true });
exports.BoardMember = mongoose_1.default.model('BoardMember', BoardMemberSchema);
//# sourceMappingURL=BoardMember.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"BoardMember.js","sourceRoot":"","sources":["../../src/models/BoardMember.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AAWtD,MAAM,iBAAiB,GAAG,IAAI,iBAAM,CAAe;IACjD,IAAI,EAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACtD,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACtD,KAAK,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;IACvE,KAAK,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,GAAG,EAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,KAAK,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CACvC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,WAAW,GAAG,kBAAQ,CAAC,KAAK,CAAe,aAAa,EAAE,iBAAiB,CAAC,CAAC"}

View file

@ -1,49 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Event = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const EventSchema = new mongoose_1.Schema({
date: { type: String, required: true },
title: { type: String, required: true, trim: true },
description: { type: String, required: true, trim: true },
startTime: { type: String, required: true },
endTime: { type: String, required: true },
pointOfContact: { type: String, required: true, trim: true },
email: { type: String, required: true, trim: true, lowercase: true },
address: { type: String, required: true, trim: true },
}, { timestamps: true });
exports.Event = mongoose_1.default.model('Event', EventSchema);
//# sourceMappingURL=Event.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"Event.js","sourceRoot":"","sources":["../../src/models/Event.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AAatD,MAAM,WAAW,GAAG,IAAI,iBAAM,CAAS;IACrC,IAAI,EAAY,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAChD,KAAK,EAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC5D,WAAW,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC5D,SAAS,EAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAChD,OAAO,EAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAChD,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC5D,KAAK,EAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;IAC7E,OAAO,EAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;CAC7D,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,KAAK,GAAG,kBAAQ,CAAC,KAAK,CAAS,OAAO,EAAE,WAAW,CAAC,CAAC"}

View file

@ -1,44 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GalleryItem = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const GalleryItemSchema = new mongoose_1.Schema({
imageUrl: { type: String, required: true },
alt: { type: String, required: true, trim: true },
order: { type: Number, default: 0 },
}, { timestamps: true });
exports.GalleryItem = mongoose_1.default.model('GalleryItem', GalleryItemSchema);
//# sourceMappingURL=GalleryItem.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"GalleryItem.js","sourceRoot":"","sources":["../../src/models/GalleryItem.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AAQtD,MAAM,iBAAiB,GAAG,IAAI,iBAAM,CAAe;IACjD,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,GAAG,EAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACtD,KAAK,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CACvC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,WAAW,GAAG,kBAAQ,CAAC,KAAK,CAAe,aAAa,EAAE,iBAAiB,CAAC,CAAC"}

View file

@ -1,50 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Member = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const MemberSchema = new mongoose_1.Schema({
firstName: { type: String, required: true, trim: true },
lastName: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, trim: true, lowercase: true },
phone: { type: String },
address: { type: String },
membershipType: { type: String, enum: ['regular', 'honorary', 'board'], default: 'regular' },
status: { type: String, enum: ['active', 'inactive'], default: 'active' },
joinDate: { type: Date, required: true, default: Date.now },
notes: { type: String },
}, { timestamps: true });
exports.Member = mongoose_1.default.model('Member', MemberSchema);
//# sourceMappingURL=Member.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"Member.js","sourceRoot":"","sources":["../../src/models/Member.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AActD,MAAM,YAAY,GAAG,IAAI,iBAAM,CAAU;IACvC,SAAS,EAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC5D,QAAQ,EAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC5D,KAAK,EAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;IAC3F,KAAK,EAAW,EAAE,IAAI,EAAE,MAAM,EAAE;IAChC,OAAO,EAAS,EAAE,IAAI,EAAE,MAAM,EAAE;IAChC,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE;IAC5F,MAAM,EAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE;IACjF,QAAQ,EAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE;IACjE,KAAK,EAAW,EAAE,IAAI,EAAE,MAAM,EAAE;CACjC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,MAAM,GAAG,kBAAQ,CAAC,KAAK,CAAU,QAAQ,EAAE,YAAY,CAAC,CAAC"}

View file

@ -1,45 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Minute = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const MinuteSchema = new mongoose_1.Schema({
date: { type: String, required: true },
meetingType: { type: String, required: true, trim: true },
location: { type: String, required: true, trim: true },
fileUrl: { type: String, required: true },
}, { timestamps: true });
exports.Minute = mongoose_1.default.model('Minute', MinuteSchema);
//# sourceMappingURL=Minute.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"Minute.js","sourceRoot":"","sources":["../../src/models/Minute.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AAStD,MAAM,YAAY,GAAG,IAAI,iBAAM,CAAU;IACvC,IAAI,EAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC7C,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACzD,QAAQ,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACzD,OAAO,EAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC9C,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,MAAM,GAAG,kBAAQ,CAAC,KAAK,CAAU,QAAQ,EAAE,YAAY,CAAC,CAAC"}

View file

@ -1,48 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sponsor = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const SponsorSchema = new mongoose_1.Schema({
name: { type: String, required: true, trim: true },
logoPath: { type: String, required: true },
description: { type: String, required: true, trim: true },
websiteUrl: { type: String },
facebookUrl: { type: String },
instagramUrl: { type: String },
order: { type: Number, default: 0 },
}, { timestamps: true });
exports.Sponsor = mongoose_1.default.model('Sponsor', SponsorSchema);
//# sourceMappingURL=Sponsor.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"Sponsor.js","sourceRoot":"","sources":["../../src/models/Sponsor.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AAYtD,MAAM,aAAa,GAAG,IAAI,iBAAM,CAAW;IACzC,IAAI,EAAU,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC1D,QAAQ,EAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC9C,WAAW,EAAG,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IAC1D,UAAU,EAAI,EAAE,IAAI,EAAE,MAAM,EAAE;IAC9B,WAAW,EAAG,EAAE,IAAI,EAAE,MAAM,EAAE;IAC9B,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;IAC9B,KAAK,EAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAC3C,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,OAAO,GAAG,kBAAQ,CAAC,KAAK,CAAW,SAAS,EAAE,aAAa,CAAC,CAAC"}

View file

@ -1,46 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.User = void 0;
const mongoose_1 = __importStar(require("mongoose"));
const UserSchema = new mongoose_1.Schema({
username: { type: String, required: true, unique: true, trim: true },
password: { type: String, required: true },
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, trim: true, lowercase: true },
role: { type: String, enum: ['admin', 'board'], default: 'board' },
}, { timestamps: true });
exports.User = mongoose_1.default.model('User', UserSchema);
//# sourceMappingURL=User.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"User.js","sourceRoot":"","sources":["../../src/models/User.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AAWtD,MAAM,UAAU,GAAG,IAAI,iBAAM,CAAQ;IACnC,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACpE,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,IAAI,EAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;IACtD,KAAK,EAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;IACrF,IAAI,EAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE;CACvE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAEZ,QAAA,IAAI,GAAG,kBAAQ,CAAC,KAAK,CAAQ,MAAM,EAAE,UAAU,CAAC,CAAC"}

View file

@ -1,75 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const jose_1 = require("jose");
const argon2 = __importStar(require("argon2"));
const zod_1 = require("zod");
const User_1 = require("../models/User");
const auth_1 = require("../middleware/auth");
const router = (0, express_1.Router)();
const loginSchema = zod_1.z.object({
username: zod_1.z.string().min(1),
password: zod_1.z.string().min(1),
});
router.post('/login', async (req, res) => {
const { username, password } = loginSchema.parse(req.body);
const user = await User_1.User.findOne({ username });
if (!user) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const valid = await argon2.verify(user.password, password);
if (!valid) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const secret = new TextEncoder().encode(process.env.JWT_SECRET ?? 'fallback-dev-secret');
const token = await new jose_1.SignJWT({ userId: user.id, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(secret);
res.json({ token, user: { id: user.id, username: user.username, name: user.name, role: user.role } });
});
router.get('/me', auth_1.requireAuth, async (req, res) => {
const user = await User_1.User.findById(req.userId).select('-password');
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json(user);
});
exports.default = router;
//# sourceMappingURL=auth.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,+BAA+B;AAC/B,+CAAiC;AACjC,6BAAwB;AACxB,yCAAsC;AACtC,6CAA8D;AAE9D,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3B,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC5B,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACvC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAE3D,MAAM,IAAI,GAAG,MAAM,WAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;QACvD,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC3D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;QACvD,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,qBAAqB,CAAC,CAAC;IACzF,MAAM,KAAK,GAAG,MAAM,IAAI,cAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;SAClE,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;SACpC,iBAAiB,CAAC,IAAI,CAAC;SACvB,IAAI,CAAC,MAAM,CAAC,CAAC;IAEhB,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACxG,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAgB,EAAE,GAAG,EAAE,EAAE;IAC7D,MAAM,IAAI,GAAG,MAAM,WAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACjE,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAClD,OAAO;IACT,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,44 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const BoardMember_1 = require("../models/BoardMember");
const auth_1 = require("../middleware/auth");
const upload_1 = require("../middleware/upload");
const router = (0, express_1.Router)();
const boardSchema = zod_1.z.object({
name: zod_1.z.string().min(1),
position: zod_1.z.string().min(1),
email: zod_1.z.string().email(),
bio: zod_1.z.string().min(1),
order: zod_1.z.coerce.number().optional(),
image: zod_1.z.string().optional(),
});
router.get('/', async (_req, res) => {
const members = await BoardMember_1.BoardMember.find().sort({ order: 1, createdAt: 1 });
res.json(members);
});
router.post('/', auth_1.requireAuth, upload_1.uploadHeadshot.single('image'), async (req, res) => {
const data = boardSchema.parse(req.body);
const image = req.file ? `/uploads/headshots/${req.file.filename}` : (data.image ?? '');
const member = await BoardMember_1.BoardMember.create({ ...data, image });
res.status(201).json(member);
});
router.put('/:id', auth_1.requireAuth, upload_1.uploadHeadshot.single('image'), async (req, res) => {
const data = boardSchema.partial().parse(req.body);
const update = { ...data };
if (req.file)
update.image = `/uploads/headshots/${req.file.filename}`;
const member = await BoardMember_1.BoardMember.findByIdAndUpdate(req.params.id, update, { new: true });
if (!member) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(member);
});
router.delete('/:id', auth_1.requireAuth, async (req, res) => {
await BoardMember_1.BoardMember.findByIdAndDelete(req.params.id);
res.status(204).send();
});
exports.default = router;
//# sourceMappingURL=board.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"board.js","sourceRoot":"","sources":["../../src/routes/board.ts"],"names":[],"mappings":"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"}

View file

@ -1,34 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const nodemailer_1 = __importDefault(require("nodemailer"));
const router = (0, express_1.Router)();
const emailSchema = zod_1.z.object({
name: zod_1.z.string().min(1),
email: zod_1.z.string().email(),
message: zod_1.z.string().min(1),
});
router.post('/sendEmail', async (req, res) => {
const { name, email, message } = emailSchema.parse(req.body);
const transporter = nodemailer_1.default.createTransport({
service: 'gmail',
auth: {
user: process.env.GOOGLE_EMAIL,
pass: process.env.GOOGLE_APP_PASSWORD,
},
});
await transporter.sendMail({
from: process.env.GOOGLE_EMAIL,
to: process.env.CONTACT_EMAIL ?? process.env.GOOGLE_EMAIL,
subject: `MCDi Contact Form: ${name}`,
text: `From: ${name} <${email}>\n\n${message}`,
replyTo: email,
});
res.json({ success: true });
});
exports.default = router;
//# sourceMappingURL=email.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"email.js","sourceRoot":"","sources":["../../src/routes/email.ts"],"names":[],"mappings":";;;;;AAAA,qCAAiC;AACjC,6BAAwB;AACxB,4DAAoC;AAEpC,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3B,IAAI,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,KAAK,EAAI,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IAC3B,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3C,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAE7D,MAAM,WAAW,GAAG,oBAAU,CAAC,eAAe,CAAC;QAC7C,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE;YACJ,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;YAC9B,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;SACtC;KACF,CAAC,CAAC;IAEH,MAAM,WAAW,CAAC,QAAQ,CAAC;QACzB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;QAC9B,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY;QACzD,OAAO,EAAE,sBAAsB,IAAI,EAAE;QACrC,IAAI,EAAE,SAAS,IAAI,KAAK,KAAK,QAAQ,OAAO,EAAE;QAC9C,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,41 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const Event_1 = require("../models/Event");
const auth_1 = require("../middleware/auth");
const router = (0, express_1.Router)();
const eventSchema = zod_1.z.object({
date: zod_1.z.string().min(1),
title: zod_1.z.string().min(1),
description: zod_1.z.string().min(1),
startTime: zod_1.z.string().min(1),
endTime: zod_1.z.string().min(1),
pointOfContact: zod_1.z.string().min(1),
email: zod_1.z.string().email(),
address: zod_1.z.string().min(1),
});
router.get('/', async (_req, res) => {
const events = await Event_1.Event.find().sort({ date: 1 });
res.json(events);
});
router.post('/', auth_1.requireAuth, async (req, res) => {
const data = eventSchema.parse(req.body);
const event = await Event_1.Event.create(data);
res.status(201).json(event);
});
router.put('/:id', auth_1.requireAuth, async (req, res) => {
const data = eventSchema.partial().parse(req.body);
const event = await Event_1.Event.findByIdAndUpdate(req.params.id, data, { new: true });
if (!event) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(event);
});
router.delete('/:id', auth_1.requireAuth, async (req, res) => {
await Event_1.Event.findByIdAndDelete(req.params.id);
res.status(204).send();
});
exports.default = router;
//# sourceMappingURL=events.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/routes/events.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AACjC,6BAAwB;AACxB,2CAAwC;AACxC,6CAAiD;AAEjD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3B,IAAI,EAAY,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,KAAK,EAAW,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,WAAW,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,SAAS,EAAO,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,OAAO,EAAS,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,cAAc,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,KAAK,EAAW,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IAClC,OAAO,EAAS,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IAClC,MAAM,MAAM,GAAG,MAAM,aAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IACpD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,aAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACvC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACjD,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,MAAM,aAAK,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IAChF,IAAI,CAAC,KAAK,EAAE,CAAC;QAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACrE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACpD,MAAM,aAAK,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,37 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const GalleryItem_1 = require("../models/GalleryItem");
const auth_1 = require("../middleware/auth");
const upload_1 = require("../middleware/upload");
const router = (0, express_1.Router)();
router.get('/', async (_req, res) => {
const items = await GalleryItem_1.GalleryItem.find().sort({ order: 1, createdAt: 1 });
res.json(items);
});
router.post('/', auth_1.requireAuth, upload_1.uploadPhoto.single('image'), async (req, res) => {
const schema = zod_1.z.object({ alt: zod_1.z.string().min(1), order: zod_1.z.coerce.number().optional() });
const data = schema.parse(req.body);
const imageUrl = req.file
? `/uploads/photos/${req.file.filename}`
: zod_1.z.string().min(1).parse(req.body.imageUrl);
const item = await GalleryItem_1.GalleryItem.create({ ...data, imageUrl });
res.status(201).json(item);
});
router.put('/:id', auth_1.requireAuth, async (req, res) => {
const schema = zod_1.z.object({ alt: zod_1.z.string().optional(), order: zod_1.z.coerce.number().optional() });
const data = schema.parse(req.body);
const item = await GalleryItem_1.GalleryItem.findByIdAndUpdate(req.params.id, data, { new: true });
if (!item) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(item);
});
router.delete('/:id', auth_1.requireAuth, async (req, res) => {
await GalleryItem_1.GalleryItem.findByIdAndDelete(req.params.id);
res.status(204).send();
});
exports.default = router;
//# sourceMappingURL=gallery.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"gallery.js","sourceRoot":"","sources":["../../src/routes/gallery.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AACjC,6BAAwB;AACxB,uDAAoD;AACpD,6CAAiD;AACjD,iDAAmD;AAEnD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IAClC,MAAM,KAAK,GAAG,MAAM,yBAAW,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACxE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAW,EAAE,oBAAW,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAC5E,MAAM,MAAM,GAAG,OAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACzF,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI;QACvB,CAAC,CAAC,mBAAmB,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE;QACxC,CAAC,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,yBAAW,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACjD,MAAM,MAAM,GAAG,OAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC7F,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,yBAAW,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IACrF,IAAI,CAAC,IAAI,EAAE,CAAC;QAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACpE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACpD,MAAM,yBAAW,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,42 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const Member_1 = require("../models/Member");
const auth_1 = require("../middleware/auth");
const router = (0, express_1.Router)();
const memberSchema = zod_1.z.object({
firstName: zod_1.z.string().min(1),
lastName: zod_1.z.string().min(1),
email: zod_1.z.string().email(),
phone: zod_1.z.string().optional(),
address: zod_1.z.string().optional(),
membershipType: zod_1.z.enum(['regular', 'honorary', 'board']).optional(),
status: zod_1.z.enum(['active', 'inactive']).optional(),
joinDate: zod_1.z.string().optional(),
notes: zod_1.z.string().optional(),
});
router.get('/', auth_1.requireAuth, async (_req, res) => {
const members = await Member_1.Member.find().sort({ lastName: 1, firstName: 1 });
res.json(members);
});
router.post('/', auth_1.requireAuth, async (req, res) => {
const data = memberSchema.parse(req.body);
const member = await Member_1.Member.create(data);
res.status(201).json(member);
});
router.put('/:id', auth_1.requireAuth, async (req, res) => {
const data = memberSchema.partial().parse(req.body);
const member = await Member_1.Member.findByIdAndUpdate(req.params.id, data, { new: true });
if (!member) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(member);
});
router.delete('/:id', auth_1.requireAuth, async (req, res) => {
await Member_1.Member.findByIdAndDelete(req.params.id);
res.status(204).send();
});
exports.default = router;
//# sourceMappingURL=members.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"members.js","sourceRoot":"","sources":["../../src/routes/members.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AACjC,6BAAwB;AACxB,6CAA0C;AAC1C,6CAAiD;AAEjD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,YAAY,GAAG,OAAC,CAAC,MAAM,CAAC;IAC5B,SAAS,EAAO,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,QAAQ,EAAQ,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,KAAK,EAAW,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IAClC,KAAK,EAAW,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,OAAO,EAAS,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,cAAc,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnE,MAAM,EAAU,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzD,QAAQ,EAAQ,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,KAAK,EAAW,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,kBAAW,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,OAAO,GAAG,MAAM,eAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACxE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,MAAM,eAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACjD,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,MAAM,eAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACtE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACpD,MAAM,eAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,31 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const Minute_1 = require("../models/Minute");
const auth_1 = require("../middleware/auth");
const upload_1 = require("../middleware/upload");
const router = (0, express_1.Router)();
router.get('/', async (_req, res) => {
const minutes = await Minute_1.Minute.find().sort({ date: -1 });
res.json(minutes);
});
router.post('/', auth_1.requireAuth, upload_1.uploadMinutes.single('file'), async (req, res) => {
const schema = zod_1.z.object({
date: zod_1.z.string().min(1),
meetingType: zod_1.z.string().min(1),
location: zod_1.z.string().min(1),
});
const data = schema.parse(req.body);
const fileUrl = req.file
? `/uploads/minutes/${req.file.filename}`
: zod_1.z.string().min(1).parse(req.body.fileUrl);
const minute = await Minute_1.Minute.create({ ...data, fileUrl });
res.status(201).json(minute);
});
router.delete('/:id', auth_1.requireAuth, async (req, res) => {
await Minute_1.Minute.findByIdAndDelete(req.params.id);
res.status(204).send();
});
exports.default = router;
//# sourceMappingURL=minutes.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"minutes.js","sourceRoot":"","sources":["../../src/routes/minutes.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AACjC,6BAAwB;AACxB,6CAA0C;AAC1C,6CAAiD;AACjD,iDAAqD;AAErD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IAClC,MAAM,OAAO,GAAG,MAAM,eAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IACvD,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAW,EAAE,sBAAa,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IAC7E,MAAM,MAAM,GAAG,OAAC,CAAC,MAAM,CAAC;QACtB,IAAI,EAAS,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9B,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9B,QAAQ,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC/B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI;QACtB,CAAC,CAAC,oBAAoB,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE;QACzC,CAAC,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,eAAM,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACzD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACpD,MAAM,eAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}

View file

@ -1,45 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const zod_1 = require("zod");
const Sponsor_1 = require("../models/Sponsor");
const auth_1 = require("../middleware/auth");
const upload_1 = require("../middleware/upload");
const router = (0, express_1.Router)();
const sponsorSchema = zod_1.z.object({
name: zod_1.z.string().min(1),
description: zod_1.z.string().min(1),
websiteUrl: zod_1.z.string().url().optional().or(zod_1.z.literal('')),
facebookUrl: zod_1.z.string().url().optional().or(zod_1.z.literal('')),
instagramUrl: zod_1.z.string().url().optional().or(zod_1.z.literal('')),
order: zod_1.z.coerce.number().optional(),
logoPath: zod_1.z.string().optional(),
});
router.get('/', async (_req, res) => {
const sponsors = await Sponsor_1.Sponsor.find().sort({ order: 1, createdAt: 1 });
res.json(sponsors);
});
router.post('/', auth_1.requireAuth, upload_1.uploadLogo.single('logo'), async (req, res) => {
const data = sponsorSchema.parse(req.body);
const logoPath = req.file ? `/uploads/sponsors/${req.file.filename}` : (data.logoPath ?? '');
const sponsor = await Sponsor_1.Sponsor.create({ ...data, logoPath });
res.status(201).json(sponsor);
});
router.put('/:id', auth_1.requireAuth, upload_1.uploadLogo.single('logo'), async (req, res) => {
const data = sponsorSchema.partial().parse(req.body);
const update = { ...data };
if (req.file)
update.logoPath = `/uploads/sponsors/${req.file.filename}`;
const sponsor = await Sponsor_1.Sponsor.findByIdAndUpdate(req.params.id, update, { new: true });
if (!sponsor) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(sponsor);
});
router.delete('/:id', auth_1.requireAuth, async (req, res) => {
await Sponsor_1.Sponsor.findByIdAndDelete(req.params.id);
res.status(204).send();
});
exports.default = router;
//# sourceMappingURL=sponsors.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"sponsors.js","sourceRoot":"","sources":["../../src/routes/sponsors.ts"],"names":[],"mappings":"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"}

116
server/dist/seed.js vendored
View file

@ -1,116 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv/config");
const argon2 = __importStar(require("argon2"));
const db_1 = require("./db");
const User_1 = require("./models/User");
const BoardMember_1 = require("./models/BoardMember");
const Event_1 = require("./models/Event");
const GalleryItem_1 = require("./models/GalleryItem");
const Sponsor_1 = require("./models/Sponsor");
const Minute_1 = require("./models/Minute");
async function seed() {
await (0, db_1.connectDB)();
// Admin user
const existing = await User_1.User.findOne({ username: 'admin' });
if (!existing) {
const password = await argon2.hash(process.env.ADMIN_PASSWORD ?? 'changeme123!');
await User_1.User.create({ username: 'admin', password, name: 'MCDi Admin', email: 'admin@deafmissoula.org', role: 'admin' });
console.log('Created admin user');
}
// Board members
const boardCount = await BoardMember_1.BoardMember.countDocuments();
if (boardCount === 0) {
await BoardMember_1.BoardMember.insertMany([
{ name: 'Rita Brandborg', position: 'President', email: 'rita.brandborg@deafmissoula.org', image: '/headshots/rita.jpg', bio: "Meet Rita Brandborg, the proud mom of two tiny tornados (Levi and Josie) who keep her on her toes! When she's not wrangling her mini-me's or teaching Sign 102 at the University of Montana, you can find Rita snapping photos with her trusty camera or reeling in the big ones fly fishing. This Deaf momma is all about spreading joy, love, and a little bit of chaos wherever she goes! With a heart full of laughter and a mind full of stories, Rita is living life to the fullest - and loving every minute of it!", order: 0 },
{ name: 'Skyla Wilson', position: 'Vice President', email: 'skyla.wilson@deafmissoula.org', image: '/headshots/skyla.jpg', bio: "Meet Skyla Wilson, the river soul with a heart tuned to justice. She's a fierce advocate for the Deaf community, always ready to stand up, speak out (in her own way), and make space for voices that often go unheard. When she's not pushing for accessibility and equity, you'll find her floating peacefully down a river, soaking in the calm before diving back into the work. Skyla blends quiet power with unstoppable purpose—equal parts grace, grit, and a deep love for her community.", order: 1 },
{ name: 'Tessa Williams', position: 'Secretary', email: 'tessa.williams@deafmissoula.org', image: '/headshots/tessa.jpg', bio: "Meet Tessa Williams, the sparkplug of the University of Montana Social Worker's program. By day, she's a social work student with a heart of gold; by night, she's a sass-spewing machine who can take down anyone with her quick wit and sharp humor. When she's not advocating for Deaf rights or making her friends laugh, Tessa can be found sipping on a matcha latte with honey (her happy place). Don't mess with this tiny firecracker - she's got love for all, except maybe for those who can't keep up with her sass.", order: 2 },
{ name: 'Aubz M', position: 'Treasurer', email: 'aubz.m@deafmissoula.org', image: '/headshots/aubz.jpg', bio: "Aubz M is a shining star at the local veterinary hospital, where they're paving their way to become a certified vet tech. This animal whisperer's heart beats for creatures great and small, but it also swells with love for self-care and coziness. When they're not snuggling Kanga (their adorable pup), Aubz can be found soaking up wellness vibes or cracking jokes that'll leave you giggling. With a quick wit and thoughtful spirit, this Deaf rockstar is spreading kindness and compassion wherever they go!", order: 3 },
]);
console.log('Seeded board members');
}
// Events
const eventCount = await Event_1.Event.countDocuments();
if (eventCount === 0) {
await Event_1.Event.insertMany([
{ date: '2026-03-14', title: 'MCDi Board Meeting', description: 'Board Meeting at Funk It Coffee and Thrift', startTime: '10:00', endTime: '11:00', pointOfContact: 'Tessa Williams', email: 'tessa.williams@deafmissoula.org', address: '314 N 1st St. West, Missoula MT 59802' },
{ date: '2026-06-13', title: 'MCDi Board Meeting', description: 'Board Meeting', startTime: '09:00', endTime: '10:00', pointOfContact: 'Tessa Williams', email: 'tessa.williams@deafmissoula.org', address: 'Hybrid' },
{ date: '2026-09-12', title: 'MCDi Board Meeting', description: 'Board Meeting', startTime: '09:00', endTime: '10:00', pointOfContact: 'Tessa Williams', email: 'tessa.williams@deafmissoula.org', address: 'TBD' },
{ date: '2026-09-12', title: 'MCDi Annual Membership Meeting', description: 'Annual Membership Meeting', startTime: '10:00', endTime: '11:00', pointOfContact: 'Tessa Williams', email: 'tessa.williams@deafmissoula.org', address: 'TBD' },
]);
console.log('Seeded events');
}
// Gallery
const galleryCount = await GalleryItem_1.GalleryItem.countDocuments();
if (galleryCount === 0) {
const items = Array.from({ length: 18 }, (_, i) => ({
imageUrl: `/photos/${String(i + 1).padStart(4, '0')}.jpg`,
alt: 'MCDi community event photo',
order: i,
}));
await GalleryItem_1.GalleryItem.insertMany(items);
console.log('Seeded gallery');
}
// Sponsors
const sponsorCount = await Sponsor_1.Sponsor.countDocuments();
if (sponsorCount === 0) {
await Sponsor_1.Sponsor.insertMany([
{ name: 'Drum Coffee', logoPath: '/sponsors/drum-coffee-missoula.png', description: 'Drum Coffee partnered with MCDi to provide ASL lessons on coffee-related signology, enhancing their ability to serve Deaf community members. This initiative promotes inclusivity and improves communication in their bustling cafe environment.', websiteUrl: 'https://drumcoffeeroasting.com/', facebookUrl: 'https://m.facebook.com/drumcoffeemt', instagramUrl: 'https://www.instagram.com/drumcoffee/', order: 0 },
{ name: 'Imagine Nation Brewing', logoPath: '/sponsors/imagine-nation-brewing.png', description: 'Imagine Nation Brewing hosted a vibrant fundraising event, donating proceeds to MCDi. Their generous support not only raised funds but also raised awareness about the Deaf community, making a significant impact on our mission.', websiteUrl: 'https://imaginenationbrewing.com/', facebookUrl: 'https://m.facebook.com/ImagineNationBrewing', instagramUrl: 'https://www.instagram.com/imaginenationbrewingco', order: 1 },
{ name: 'GILD Brewing', logoPath: '/sponsors/gild-brewing.png', description: 'GILD Brewing organized a community-focused fundraiser for MCDi, combining craft beer tasting with Deaf culture education. Their event not only raised funds but also fostered a deeper understanding of Deaf culture, making a significant impact on our mission.', websiteUrl: 'https://www.gildbrewing.com/', facebookUrl: 'https://m.facebook.com/gildbrewing/', instagramUrl: 'https://www.instagram.com/gildbrewing/', order: 2 },
{ name: 'SBS Solar', logoPath: '/sponsors/sbs-solar.png', description: "SBS Solar supports MCDi's mission to serve the Deaf community in Missoula. Their commitment to renewable energy and community engagement aligns with our values of accessibility and sustainability.", websiteUrl: 'https://www.sbslink.com/', facebookUrl: 'https://www.facebook.com/sbssolar', instagramUrl: 'https://www.instagram.com/sbssolarmt/', order: 3 },
]);
console.log('Seeded sponsors');
}
// Minutes
const minuteCount = await Minute_1.Minute.countDocuments();
if (minuteCount === 0) {
await Minute_1.Minute.insertMany([
{ date: '04/12/2025', meetingType: 'Membership Meeting', location: 'The Break Coffee', fileUrl: '/minutes/minutes-04122025.pdf' },
{ date: '03/08/2025', meetingType: 'Board Meeting', location: 'The Break Espresso', fileUrl: '/minutes/minutes-03082025.pdf' },
{ date: '02/08/2025', meetingType: 'Board Meeting', location: 'Book Exchange - Liquid Planet', fileUrl: '/minutes/minutes-02082025.pdf' },
{ date: '01/11/2025', meetingType: 'Membership Meeting', location: 'Black Coffee Roasting Company', fileUrl: '/minutes/minutes-01112025.pdf' },
{ date: '10/17/2024', meetingType: 'Board Meeting', location: 'UC Market (University)', fileUrl: '/minutes/minutes-10172024.pdf' },
{ date: '09/14/2024', meetingType: 'Membership Meeting', location: 'Dog Wash Cafe', fileUrl: '/minutes/minutes-09142024.pdf' },
{ date: '08/06/2024', meetingType: 'Membership Meeting', location: 'The Break Espresso', fileUrl: '/minutes/minutes-08062024.pdf' },
]);
console.log('Seeded minutes');
}
console.log('Seed complete');
process.exit(0);
}
seed().catch((err) => { console.error(err); process.exit(1); });
//# sourceMappingURL=seed.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"}

2070
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"express-rate-limit": "^8.5.2",
"jose": "^5.10.0",
"mongoose": "^8.13.0",
"multer": "^1.4.5-lts.1",

View file

@ -7,15 +7,23 @@ export interface IBoardMember extends Document {
image: string;
bio: string;
order: number;
password?: string;
mustChangePassword: boolean;
resetToken?: string;
resetTokenExpiry?: Date;
}
const BoardMemberSchema = new Schema<IBoardMember>({
name: { type: String, required: true, trim: true },
position: { type: String, required: true, trim: true },
email: { type: String, required: true, trim: true, lowercase: true },
email: { type: String, required: true, unique: true, trim: true, lowercase: true },
image: { type: String, required: true },
bio: { type: String, required: true },
order: { type: Number, default: 0 },
password: { type: String, select: false },
mustChangePassword:{ type: Boolean, default: true },
resetToken: { type: String, select: false },
resetTokenExpiry: { type: Date, select: false },
}, { timestamps: true });
export const BoardMember = mongoose.model<IBoardMember>('BoardMember', BoardMemberSchema);

View file

@ -5,6 +5,7 @@ export interface IMinute extends Document {
meetingType: string;
location: string;
fileUrl: string;
order: number;
}
const MinuteSchema = new Schema<IMinute>({
@ -12,6 +13,7 @@ const MinuteSchema = new Schema<IMinute>({
meetingType: { type: String, required: true, trim: true },
location: { type: String, required: true, trim: true },
fileUrl: { type: String, required: true },
order: { type: Number, default: 0 },
}, { timestamps: true });
export const Minute = mongoose.model<IMinute>('Minute', MinuteSchema);

View file

@ -2,47 +2,161 @@ import { Router } from 'express';
import { SignJWT } from 'jose';
import * as argon2 from 'argon2';
import { z } from 'zod';
import { randomBytes } from 'crypto';
import rateLimit from 'express-rate-limit';
import nodemailer from 'nodemailer';
import { User } from '../models/User';
import { BoardMember } from '../models/BoardMember';
import { requireAuth, AuthRequest } from '../middleware/auth';
const router = Router();
const loginSchema = z.object({
username: z.string().min(1),
password: z.string().min(1),
const jwtSecret = () => new TextEncoder().encode(process.env.JWT_SECRET ?? 'fallback-dev-secret');
const appUrl = () => process.env.APP_URL ?? 'http://localhost:801';
const loginLimiter = rateLimit({
windowMs: 5 * 60 * 1000,
limit: 5,
standardHeaders: 'draft-8',
legacyHeaders: false,
message: { error: 'Too many login attempts. Please wait 5 minutes before trying again.' },
});
router.post('/login', async (req, res) => {
const { username, password } = loginSchema.parse(req.body);
const user = await User.findOne({ username });
if (!user) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const valid = await argon2.verify(user.password, password);
if (!valid) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const secret = new TextEncoder().encode(process.env.JWT_SECRET ?? 'fallback-dev-secret');
const token = await new SignJWT({ userId: user.id, role: user.role })
async function makeToken(payload: Record<string, unknown>) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(secret);
.sign(jwtSecret());
}
res.json({ token, user: { id: user.id, username: user.username, name: user.name, role: user.role } });
async function makeShortToken(payload: Record<string, unknown>) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.sign(jwtSecret());
}
async function sendMail(to: string, subject: string, html: string) {
if (!process.env.GOOGLE_EMAIL || !process.env.GOOGLE_APP_PASSWORD) return;
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: { user: process.env.GOOGLE_EMAIL, pass: process.env.GOOGLE_APP_PASSWORD },
});
await transporter.sendMail({ from: process.env.GOOGLE_EMAIL, to, subject, html });
}
router.get('/me', requireAuth, async (req: AuthRequest, res) => {
const user = await User.findById(req.userId).select('-password');
if (!user) {
res.status(404).json({ error: 'User not found' });
// POST /api/auth/login
router.post('/login', loginLimiter, async (req, res) => {
const { email, password } = z.object({ email: z.string().email(), password: z.string().min(1) }).parse(req.body);
// Check board member first
const member = await BoardMember.findOne({ email }).select('+password +mustChangePassword +resetToken +resetTokenExpiry');
if (member) {
if (!member.password) {
res.status(401).json({ error: 'No password set. Use "Forgot Password" to set your password.' });
return;
}
res.json(user);
const valid = await argon2.verify(member.password, password);
if (!valid) { res.status(401).json({ error: 'Invalid email or password.' }); return; }
if (member.mustChangePassword) {
const tempToken = await makeShortToken({ userId: member.id, role: 'board', mustChange: true });
res.json({ mustChangePassword: true, tempToken });
return;
}
const token = await makeToken({ userId: member.id, role: 'board' });
res.json({ token, user: { id: member.id, name: member.name, email: member.email, role: 'board' } });
return;
}
// Fall back to admin user (login by email)
const admin = await User.findOne({ email });
if (!admin) { res.status(401).json({ error: 'Invalid email or password.' }); return; }
const valid = await argon2.verify(admin.password, password);
if (!valid) { res.status(401).json({ error: 'Invalid email or password.' }); return; }
const token = await makeToken({ userId: admin.id, role: admin.role });
res.json({ token, user: { id: admin.id, name: admin.name, email: admin.email, role: admin.role } });
});
// GET /api/auth/me
router.get('/me', requireAuth, async (req: AuthRequest, res) => {
const member = await BoardMember.findById(req.userId);
if (member) { res.json({ id: member.id, name: member.name, email: member.email, role: 'board' }); return; }
const admin = await User.findById(req.userId).select('-password');
if (!admin) { res.status(404).json({ error: 'User not found' }); return; }
res.json(admin);
});
// POST /api/auth/change-password (forced change on first login)
router.post('/change-password', requireAuth, async (req: AuthRequest, res) => {
const { password, confirmPassword } = z.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
}).refine(d => d.password === d.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'] })
.parse(req.body);
const member = await BoardMember.findById(req.userId);
if (!member) { res.status(404).json({ error: 'Not found' }); return; }
member.password = await argon2.hash(password);
member.mustChangePassword = false;
await member.save();
const token = await makeToken({ userId: member.id, role: 'board' });
res.json({ token, user: { id: member.id, name: member.name, email: member.email, role: 'board' } });
});
// POST /api/auth/forgot-password
router.post('/forgot-password', loginLimiter, async (req, res) => {
const { email } = z.object({ email: z.string().email() }).parse(req.body);
const member = await BoardMember.findOne({ email }).select('+resetToken +resetTokenExpiry');
if (member) {
const token = randomBytes(32).toString('hex');
member.resetToken = token;
member.resetTokenExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
await member.save();
const link = `${appUrl()}/manage/reset-password?token=${token}`;
await sendMail(email, 'MCDi Management — Reset Your Password', `
<p>Hi ${member.name},</p>
<p>Click the link below to set your MCDi Management password. This link expires in 1 hour.</p>
<p><a href="${link}">${link}</a></p>
<p>If you did not request this, you can ignore this email.</p>
`).catch(err => console.error('Email send failed:', err));
}
// Always return 200 to avoid revealing whether email exists
res.json({ message: 'If that email is registered, a reset link has been sent.' });
});
// POST /api/auth/reset-password
router.post('/reset-password', async (req, res) => {
const { token, password, confirmPassword } = z.object({
token: z.string().min(1),
password: z.string().min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
}).refine(d => d.password === d.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'] })
.parse(req.body);
const member = await BoardMember.findOne({
resetToken: token,
resetTokenExpiry: { $gt: new Date() },
}).select('+resetToken +resetTokenExpiry');
if (!member) { res.status(400).json({ error: 'Invalid or expired reset link.' }); return; }
member.password = await argon2.hash(password);
member.mustChangePassword = false;
member.resetToken = undefined;
member.resetTokenExpiry = undefined;
await member.save();
const jwtToken = await makeToken({ userId: member.id, role: 'board' });
res.json({ token: jwtToken, user: { id: member.id, name: member.name, email: member.email, role: 'board' } });
});
export { sendMail };
export default router;

View file

@ -1,8 +1,10 @@
import { Router } from 'express';
import { z } from 'zod';
import { randomBytes } from 'crypto';
import { BoardMember } from '../models/BoardMember';
import { requireAuth } from '../middleware/auth';
import { uploadHeadshot } from '../middleware/upload';
import { sendMail } from './auth';
const router = Router();
@ -23,7 +25,23 @@ router.get('/', async (_req, res) => {
router.post('/', requireAuth, uploadHeadshot.single('image'), async (req, res) => {
const data = boardSchema.parse(req.body);
const image = req.file ? `/uploads/headshots/${req.file.filename}` : (data.image ?? '');
const member = await BoardMember.create({ ...data, image });
const member = await BoardMember.create({ ...data, image, mustChangePassword: true });
// Send welcome email (best-effort)
const token = randomBytes(32).toString('hex');
member.resetToken = token;
member.resetTokenExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days for welcome
await member.save();
const appUrl = process.env.APP_URL ?? 'http://localhost:801';
const link = `${appUrl}/manage/reset-password?token=${token}`;
sendMail(member.email, 'Welcome to MCDi Management', `
<p>Hi ${member.name},</p>
<p>You've been added to the MCDi Management system. Click the link below to set your password and get started.</p>
<p><a href="${link}">${link}</a></p>
<p>This link expires in 7 days.</p>
`).catch(err => console.error('Welcome email failed:', err));
res.status(201).json(member);
});
@ -37,6 +55,13 @@ router.put('/:id', requireAuth, uploadHeadshot.single('image'), async (req, res)
res.json(member);
});
// PATCH /api/board/reorder — body: { ids: [id1, id2, ...] } in desired order
router.patch('/reorder', requireAuth, async (req, res) => {
const { ids } = z.object({ ids: z.array(z.string()) }).parse(req.body);
await Promise.all(ids.map((id, index) => BoardMember.findByIdAndUpdate(id, { order: index })));
res.json({ ok: true });
});
router.delete('/:id', requireAuth, async (req, res) => {
await BoardMember.findByIdAndDelete(req.params.id);
res.status(204).send();

View file

@ -7,7 +7,7 @@ import { uploadMinutes } from '../middleware/upload';
const router = Router();
router.get('/', async (_req, res) => {
const minutes = await Minute.find().sort({ date: -1 });
const minutes = await Minute.find().sort({ order: 1, createdAt: -1 });
res.json(minutes);
});
@ -21,10 +21,20 @@ router.post('/', requireAuth, uploadMinutes.single('file'), async (req, res) =>
const fileUrl = req.file
? `/uploads/minutes/${req.file.filename}`
: z.string().min(1).parse(req.body.fileUrl);
const minute = await Minute.create({ ...data, fileUrl });
// Insert at top (order 0), shift everything else down
await Minute.updateMany({}, { $inc: { order: 1 } });
const minute = await Minute.create({ ...data, fileUrl, order: 0 });
res.status(201).json(minute);
});
// PATCH /api/minutes/reorder — body: { ids: [id1, id2, ...] } in desired order
router.patch('/reorder', requireAuth, async (req, res) => {
const { ids } = z.object({ ids: z.array(z.string()) }).parse(req.body);
await Promise.all(ids.map((id, index) => Minute.findByIdAndUpdate(id, { order: index })));
res.json({ ok: true });
});
router.delete('/:id', requireAuth, async (req, res) => {
await Minute.findByIdAndDelete(req.params.id);
res.status(204).send();

View file

@ -71,13 +71,13 @@ async function seed() {
const minuteCount = await Minute.countDocuments();
if (minuteCount === 0) {
await Minute.insertMany([
{ date: '04/12/2025', meetingType: 'Membership Meeting', location: 'The Break Coffee', fileUrl: '/minutes/minutes-04122025.pdf' },
{ date: '03/08/2025', meetingType: 'Board Meeting', location: 'The Break Espresso', fileUrl: '/minutes/minutes-03082025.pdf' },
{ date: '02/08/2025', meetingType: 'Board Meeting', location: 'Book Exchange - Liquid Planet', fileUrl: '/minutes/minutes-02082025.pdf' },
{ date: '01/11/2025', meetingType: 'Membership Meeting', location: 'Black Coffee Roasting Company', fileUrl: '/minutes/minutes-01112025.pdf' },
{ date: '10/17/2024', meetingType: 'Board Meeting', location: 'UC Market (University)', fileUrl: '/minutes/minutes-10172024.pdf' },
{ date: '09/14/2024', meetingType: 'Membership Meeting', location: 'Dog Wash Cafe', fileUrl: '/minutes/minutes-09142024.pdf' },
{ date: '08/06/2024', meetingType: 'Membership Meeting', location: 'The Break Espresso', fileUrl: '/minutes/minutes-08062024.pdf' },
{ date: '04/12/2025', meetingType: 'Membership Meeting', location: 'The Break Coffee', fileUrl: '/minutes/minutes-04122025.pdf', order: 0 },
{ date: '03/08/2025', meetingType: 'Board Meeting', location: 'The Break Espresso', fileUrl: '/minutes/minutes-03082025.pdf', order: 1 },
{ date: '02/08/2025', meetingType: 'Board Meeting', location: 'Book Exchange - Liquid Planet', fileUrl: '/minutes/minutes-02082025.pdf', order: 2 },
{ date: '01/11/2025', meetingType: 'Membership Meeting', location: 'Black Coffee Roasting Company', fileUrl: '/minutes/minutes-01112025.pdf', order: 3 },
{ date: '10/17/2024', meetingType: 'Board Meeting', location: 'UC Market (University)', fileUrl: '/minutes/minutes-10172024.pdf', order: 4 },
{ date: '09/14/2024', meetingType: 'Membership Meeting', location: 'Dog Wash Cafe', fileUrl: '/minutes/minutes-09142024.pdf', order: 5 },
{ date: '08/06/2024', meetingType: 'Membership Meeting', location: 'The Break Espresso', fileUrl: '/minutes/minutes-08062024.pdf', order: 6 },
]);
console.log('Seeded minutes');
}

View file

@ -10,3 +10,6 @@ JWT_SECRET=changeme-set-a-long-random-secret
# Admin dashboard default password (changed on first login recommended)
ADMIN_PASSWORD=changeme123!
# Public-facing URL for password reset emails
APP_URL=https://deafmissoula.org