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:
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const [families, babies, events, growthLogs, journalNotes, milestones] = await Promise.all([
|
||||
prisma.family.findMany({
|
||||
include: { users: { select: { id: true, name: true, email: true, role: true, isSuperAdmin: true, createdAt: true } } },
|
||||
}),
|
||||
prisma.baby.findMany(),
|
||||
prisma.event.findMany({ orderBy: { createdAt: "desc" }, take: 10000 }),
|
||||
prisma.growthLog.findMany(),
|
||||
prisma.journalNote.findMany(),
|
||||
prisma.milestone.findMany(),
|
||||
]);
|
||||
|
||||
const payload = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
families,
|
||||
babies,
|
||||
events,
|
||||
growthLogs,
|
||||
journalNotes,
|
||||
milestones,
|
||||
};
|
||||
|
||||
const filename = `grow-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
|
||||
return new NextResponse(JSON.stringify(payload, null, 2), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
const ALLOWED_KEYS = [
|
||||
"email_provider",
|
||||
"mailgun_api_key",
|
||||
"mailgun_domain",
|
||||
"mailgun_from",
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_user",
|
||||
"smtp_pass",
|
||||
"smtp_from",
|
||||
"pushover_app_token",
|
||||
"allow_registration",
|
||||
"maintenance_mode",
|
||||
];
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const configs = await prisma.systemConfig.findMany();
|
||||
const result: Record<string, string> = {};
|
||||
for (const c of configs) result[c.key] = c.value;
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const body: Record<string, string> = await req.json();
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(body)
|
||||
.filter(([key]) => ALLOWED_KEYS.includes(key))
|
||||
.map(([key, value]) =>
|
||||
prisma.systemConfig.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const families = await prisma.family.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
inviteCode: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
users: true,
|
||||
babies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(families);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
function generateInviteCode(): string {
|
||||
return crypto.randomUUID().replace(/-/g, "").slice(0, 25);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const inviteCode = generateInviteCode();
|
||||
|
||||
await prisma.family.update({
|
||||
where: { id },
|
||||
data: { inviteCode },
|
||||
});
|
||||
|
||||
return NextResponse.json({ inviteCode });
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const [userCount, familyCount, babyCount, eventCount, growthLogCount] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.family.count(),
|
||||
prisma.baby.count(),
|
||||
prisma.event.count(),
|
||||
prisma.growthLog.count(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({ userCount, familyCount, babyCount, eventCount, growthLogCount });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sendEmail } from "@/lib/email";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
if (!session.user?.email) {
|
||||
return NextResponse.json({ error: "Email de l'administrateur introuvable" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: session.user!.email as string,
|
||||
subject: "Test email — Grow",
|
||||
html: "<p>La configuration email fonctionne correctement.</p>",
|
||||
text: "La configuration email fonctionne correctement.",
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Erreur inconnue";
|
||||
return NextResponse.json({ error: message }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user!.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const [appTokenRow, user] = await Promise.all([
|
||||
prisma.systemConfig.findUnique({ where: { key: "pushover_app_token" } }),
|
||||
prisma.user.findUnique({ where: { id: session.user!.id }, select: { pushoverUserKey: true } }),
|
||||
]);
|
||||
|
||||
const appToken = appTokenRow?.value ?? process.env.PUSHOVER_APP_TOKEN ?? "";
|
||||
const userKey = user?.pushoverUserKey ?? "";
|
||||
|
||||
if (!appToken) {
|
||||
return NextResponse.json({ error: "Token application Pushover non configuré" }, { status: 400 });
|
||||
}
|
||||
if (!userKey) {
|
||||
return NextResponse.json({ error: "Clé utilisateur Pushover non configurée" }, { status: 400 });
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("token", appToken);
|
||||
formData.append("user", userKey);
|
||||
formData.append("title", "Grow — Test de notification");
|
||||
formData.append("message", "La configuration Pushover fonctionne correctement.");
|
||||
|
||||
const res = await fetch("https://api.pushover.net/1/messages.json", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
return NextResponse.json({ error: `Erreur Pushover : ${text}` }, { status: 502 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
if (id === session.user?.id) {
|
||||
return NextResponse.json({ error: "Impossible de vous déconnecter vous-même" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { forcedLogoutAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
const VALID_ROLES = ["PARENT", "READONLY", "DOCTOR"] as const;
|
||||
type ValidRole = typeof VALID_ROLES[number];
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
const body: { role?: string } = await req.json();
|
||||
|
||||
if (!body.role || !(VALID_ROLES as readonly string[]).includes(body.role)) {
|
||||
return NextResponse.json({ error: "Rôle invalide" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { role: body.role as ValidRole },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
if (id === session.user?.id) {
|
||||
return NextResponse.json({ error: "Impossible de supprimer votre propre compte" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.user.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
isSuperAdmin: true,
|
||||
familyId: true,
|
||||
family: { select: { name: true } },
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(users);
|
||||
}
|
||||
Reference in New Issue
Block a user