Initial commit — Grow baby tracker

Next.js 16 App Router, Prisma + PostgreSQL, NextAuth v5 JWT.

Features: dashboard quick-log, timeline, growth charts (WHO percentiles),
stats, journal/notes, doctor notes, milestones, vaccinations, settings,
superadmin panel. Mobile-first with sidebar nav + bottom nav + quick-add FAB.
Dark mode, PWA push notifications, multi-family invite system.

Docker: multi-stage Dockerfile + docker-compose with postgres service.
This commit is contained in:
2026-06-13 01:52:46 +02:00
commit cd16c354c0
94 changed files with 14354 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
+30
View File
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { sendEmail, resetPasswordEmail } from "@/lib/email";
export async function POST(req: Request) {
const { email } = await req.json();
if (!email) return NextResponse.json({ error: "Email requis" }, { status: 400 });
const user = await prisma.user.findUnique({ where: { email } });
// Always return success to prevent user enumeration
if (!user) return NextResponse.json({ ok: true });
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
const reset = await prisma.passwordResetToken.create({
data: { userId: user.id, expiresAt },
});
const origin = process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000";
const resetUrl = `${origin}/auth/reset-password?token=${reset.token}`;
try {
const { html, text } = resetPasswordEmail(user.name, resetUrl);
await sendEmail({ to: user.email, subject: "Réinitialisation de votre mot de passe — Grow", html, text });
} catch (err) {
console.error("Email send failed:", err);
// Still succeed silently to avoid leaking info
}
return NextResponse.json({ ok: true });
}
+23
View File
@@ -0,0 +1,23 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";
export async function POST(req: Request) {
const { token, password } = await req.json();
if (!token || !password || password.length < 8) {
return NextResponse.json({ error: "Données invalides" }, { status: 400 });
}
const reset = await prisma.passwordResetToken.findUnique({ where: { token } });
if (!reset || reset.used || reset.expiresAt < new Date()) {
return NextResponse.json({ error: "Lien expiré ou invalide" }, { status: 400 });
}
const hashed = await bcrypt.hash(password, 12);
await Promise.all([
prisma.user.update({ where: { id: reset.userId }, data: { password: hashed } }),
prisma.passwordResetToken.update({ where: { id: reset.id }, data: { used: true } }),
]);
return NextResponse.json({ ok: true });
}