Skip to content

Commit d707a1a

Browse files
feat: real-time sync, push notifications, multi-member accounts + security hardening
- **Partie 1 — WebSocket real-time sync**: Live updates across all 9 pages (tasks, shopping, calendar, budget, family, recipes, meal planning, planning, dashboard). Server-side broadcast utility (broadcaster.ts) + WebSocketContext with exponential backoff and heartbeat. - **Partie 2 — Push notifications**: web-push VAPID integration, node-cron reminder scheduler (30min/1h before appointments), service worker (sw.js, injectManifest mode), useNotifications hook, NotificationBell component with unread badge and dropdown. - **Partie 3 — Multi-member accounts**: Family invite system — owners generate a 64-char token link (/join?invite=TOKEN), new users register with it or existing users join via the Join page. JWT now carries { userId, ownerId } so all data queries transparently scope to the family owner. Family page shows shared accounts list, invite link generator, kick member (owner) and leave family (member) actions. DB migration adds family_owner_id and family_invites table. - **Critical — SQL injection (dataTransfer.ts)**: Column names from import body now validated against SAFE_IDENTIFIER regex before use in queries. - **High — WebSocket auth bypass (index.ts + WebSocketContext)**: Client now sends JWT token instead of plain userId; server verifies before registering the connection (4001 Unauthorized on failure). - **Medium — Missing HTTP security headers (app.ts)**: Added helmet() (CSP, X-Frame-Options, HSTS, X-Content-Type-Options, Referrer-Policy, etc.). - **Medium — No request size limit (app.ts)**: express.json() and urlencoded() capped at 1 MB to prevent DoS via oversized payloads. - **Low — bcrypt cost factor 10 → 12 (auth.ts)**: Aligned with current OWASP recommendation. - **Low — Silent DB password fallback (db.ts)**: Clear warning logged when POSTGRES_PASSWORD is not set instead of silently using the default. - Proxmox LXC install script (scripts/proxmox-lxc-install.sh) - Update script (scripts/update.sh)
1 parent 0ef99d4 commit d707a1a

53 files changed

Lines changed: 2472 additions & 105 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client/public/OpenFamily.png

61.4 KB
Loading

client/public/apple-touch-icon.png

12.2 KB
Loading

client/public/favicon-16x16.png

11 Bytes
Loading

client/public/favicon-32x32.png

466 Bytes
Loading

client/public/icon-192.png

13.3 KB
Loading

client/public/icon-512.png

63.3 KB
Loading

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import MealPlanning from './pages/MealPlanning';
1212
import Budget from './pages/Budget';
1313
import Family from './pages/Family';
1414
import Settings from './pages/Settings';
15+
import Join from './pages/Join';
1516

1617
function App() {
1718
const { isAuthenticated, loading } = useAuth();
@@ -44,6 +45,7 @@ function App() {
4445
<Route path="/budget" element={<Budget />} />
4546
<Route path="/family" element={<Family />} />
4647
<Route path="/settings" element={<Settings />} />
48+
<Route path="/join" element={<Join />} />
4749
<Route path="*" element={<Navigate to="/" replace />} />
4850
</Routes>
4951
</Layout>

client/src/components/layout/Layout.tsx

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from 'lucide-react';
2323
import { cn } from '../../lib/utils';
2424
import { Button } from '../ui/Button';
25+
import { NotificationBell } from '../ui/NotificationBell';
2526

2627
interface LayoutProps {
2728
children: ReactNode;
@@ -103,9 +104,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
103104
<div className="flex h-full flex-col">
104105
<div className="flex h-20 items-center justify-between border-b border-border px-6">
105106
<Link to="/" className="flex items-center gap-3" onClick={closeMenus}>
106-
<div className="flex h-9 w-9 items-center justify-center rounded-card bg-primary text-primary-foreground shadow-surface">
107-
<Users className="h-5 w-5" />
108-
</div>
107+
<img src="/OpenFamily.png" alt="OpenFamily" className="h-9 w-9 object-contain" />
109108
<span className="text-lg font-semibold tracking-tight">OpenFamily</span>
110109
</Link>
111110
<button
@@ -207,28 +206,31 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
207206
</div>
208207
</div>
209208

210-
<div className="hidden items-center gap-2 lg:flex">
211-
<Button
212-
variant="secondary"
213-
size="icon"
214-
onClick={toggleTheme}
215-
aria-label="Changer le theme"
216-
>
217-
{actualTheme === 'dark' ? (
218-
<Sun className="h-4 w-4" />
219-
) : (
220-
<Moon className="h-4 w-4" />
221-
)}
222-
</Button>
223-
<Button
224-
variant="ghost"
225-
size="icon"
226-
onClick={logout}
227-
aria-label="Se deconnecter"
228-
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
229-
>
230-
<LogOut className="h-4 w-4" />
231-
</Button>
209+
<div className="flex items-center gap-2">
210+
<NotificationBell />
211+
<div className="hidden items-center gap-2 lg:flex">
212+
<Button
213+
variant="secondary"
214+
size="icon"
215+
onClick={toggleTheme}
216+
aria-label="Changer le theme"
217+
>
218+
{actualTheme === 'dark' ? (
219+
<Sun className="h-4 w-4" />
220+
) : (
221+
<Moon className="h-4 w-4" />
222+
)}
223+
</Button>
224+
<Button
225+
variant="ghost"
226+
size="icon"
227+
onClick={logout}
228+
aria-label="Se deconnecter"
229+
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
230+
>
231+
<LogOut className="h-4 w-4" />
232+
</Button>
233+
</div>
232234
</div>
233235
</div>
234236
</header>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { Bell, BellOff, CheckCheck } from 'lucide-react';
3+
import { Link } from 'react-router-dom';
4+
import { cn } from '../../lib/utils';
5+
import { useNotifications, AppNotification } from '../../hooks/useNotifications';
6+
import { Button } from './Button';
7+
8+
function timeAgo(dateStr: string): string {
9+
const diff = Date.now() - new Date(dateStr).getTime();
10+
const minutes = Math.floor(diff / 60000);
11+
if (minutes < 1) return 'à l\'instant';
12+
if (minutes < 60) return `il y a ${minutes} min`;
13+
const hours = Math.floor(minutes / 60);
14+
if (hours < 24) return `il y a ${hours}h`;
15+
return `il y a ${Math.floor(hours / 24)}j`;
16+
}
17+
18+
export const NotificationBell: React.FC = () => {
19+
const {
20+
isSupported,
21+
unreadCount,
22+
notifications,
23+
fetchNotifications,
24+
markAsRead,
25+
markAllAsRead,
26+
} = useNotifications();
27+
28+
const [open, setOpen] = useState(false);
29+
const ref = useRef<HTMLDivElement>(null);
30+
31+
// Close on outside click
32+
useEffect(() => {
33+
const handler = (e: MouseEvent) => {
34+
if (ref.current && !ref.current.contains(e.target as Node)) {
35+
setOpen(false);
36+
}
37+
};
38+
document.addEventListener('mousedown', handler);
39+
return () => document.removeEventListener('mousedown', handler);
40+
}, []);
41+
42+
const handleOpen = () => {
43+
setOpen((o) => {
44+
if (!o) void fetchNotifications();
45+
return !o;
46+
});
47+
};
48+
49+
const handleMarkAsRead = (n: AppNotification) => {
50+
if (!n.is_read) void markAsRead(n.id);
51+
};
52+
53+
if (!isSupported) return null;
54+
55+
return (
56+
<div ref={ref} className="relative">
57+
<Button
58+
variant="secondary"
59+
size="icon"
60+
onClick={handleOpen}
61+
aria-label="Notifications"
62+
className="relative"
63+
>
64+
<Bell className="h-4 w-4" />
65+
{unreadCount > 0 && (
66+
<span className="absolute -right-1 -top-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-bold text-destructive-foreground">
67+
{unreadCount > 99 ? '99+' : unreadCount}
68+
</span>
69+
)}
70+
</Button>
71+
72+
{open && (
73+
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-card border border-border bg-card shadow-surface-hover">
74+
{/* Header */}
75+
<div className="flex items-center justify-between border-b border-border px-4 py-3">
76+
<span className="text-caption font-semibold text-foreground">Notifications</span>
77+
{unreadCount > 0 && (
78+
<button
79+
type="button"
80+
onClick={() => void markAllAsRead()}
81+
className="flex items-center gap-1 text-micro text-primary hover:text-primary/80"
82+
>
83+
<CheckCheck className="h-3.5 w-3.5" />
84+
Tout marquer lu
85+
</button>
86+
)}
87+
</div>
88+
89+
{/* Notification list */}
90+
<div className="max-h-80 overflow-y-auto">
91+
{notifications.length === 0 ? (
92+
<div className="flex flex-col items-center gap-2 px-4 py-8 text-center text-muted-foreground">
93+
<BellOff className="h-8 w-8 opacity-40" />
94+
<p className="text-micro">Aucune notification</p>
95+
</div>
96+
) : (
97+
notifications.slice(0, 10).map((n) => (
98+
<button
99+
key={n.id}
100+
type="button"
101+
onClick={() => handleMarkAsRead(n)}
102+
className={cn(
103+
'flex w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-2',
104+
!n.is_read && 'bg-primary-soft/30'
105+
)}
106+
>
107+
{!n.is_read && (
108+
<span className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-primary" />
109+
)}
110+
<div className={cn('flex-1 min-w-0', n.is_read && 'ml-5')}>
111+
<p className="truncate text-micro font-medium text-foreground">
112+
{n.title}
113+
</p>
114+
<p className="mt-0.5 line-clamp-2 text-micro text-muted-foreground">
115+
{n.message}
116+
</p>
117+
<p className="mt-1 text-micro text-muted-foreground/60">
118+
{timeAgo(n.created_at)}
119+
</p>
120+
</div>
121+
</button>
122+
))
123+
)}
124+
</div>
125+
126+
{/* Footer */}
127+
<div className="border-t border-border px-4 py-2">
128+
<Link
129+
to="/settings"
130+
onClick={() => setOpen(false)}
131+
className="block text-center text-micro text-primary hover:text-primary/80"
132+
>
133+
Gérer les notifications →
134+
</Link>
135+
</div>
136+
</div>
137+
)}
138+
</div>
139+
);
140+
};

client/src/contexts/AuthContext.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ interface User {
55
id: string;
66
email: string;
77
name: string;
8+
is_owner?: boolean;
89
}
910

1011
interface AuthContextType {
1112
user: User | null;
1213
loading: boolean;
1314
login: (email: string, password: string) => Promise<void>;
14-
register: (email: string, password: string, name: string) => Promise<void>;
15+
register: (email: string, password: string, name: string, inviteToken?: string) => Promise<void>;
16+
joinFamily: (inviteToken: string) => Promise<void>;
17+
leaveFamily: () => Promise<void>;
1518
logout: () => void;
1619
isAuthenticated: boolean;
1720
}
@@ -88,11 +91,26 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
8891
}
8992
};
9093

91-
const register = async (email: string, password: string, name: string) => {
92-
const response = await api.register(email, password, name);
94+
const register = async (email: string, password: string, name: string, inviteToken?: string) => {
95+
const response = await api.register(email, password, name, inviteToken);
96+
if (response.success && response.user) {
97+
setUser(response.user);
98+
localStorage.setItem('user', JSON.stringify(response.user));
99+
}
100+
};
101+
102+
const joinFamily = async (inviteToken: string) => {
103+
const response = await api.joinFamily(inviteToken);
104+
if (response.success && response.user) {
105+
setUser(response.user);
106+
localStorage.setItem('user', JSON.stringify(response.user));
107+
}
108+
};
109+
110+
const leaveFamily = async () => {
111+
const response = await api.leaveFamily();
93112
if (response.success && response.user) {
94113
setUser(response.user);
95-
// Also store in localStorage for persistence
96114
localStorage.setItem('user', JSON.stringify(response.user));
97115
}
98116
};
@@ -110,6 +128,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
110128
loading,
111129
login,
112130
register,
131+
joinFamily,
132+
leaveFamily,
113133
logout,
114134
isAuthenticated: !!user,
115135
}}

0 commit comments

Comments
 (0)