Compare commits

..

2 commits

Author SHA1 Message Date
c787caa821 fix: add Resend for password reset emails + install F1-F2 deps
- Wire Resend to /api/auth/reset-request with fallback for dev
- Install: sharp, recharts, next-pwa, resend, @react-pdf/renderer, @types/sharp

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:42:45 +05:30
c459b4411a fix: secure /api/ai endpoint and remove debug routes
- Add auth to /api/ai via requireFamily middleware
- Remove /api/ai and /api/auth/debug from public routes
- Delete debug/test routes that expose internal state

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:41:47 +05:30
7 changed files with 4916 additions and 62 deletions

View file

@ -11,6 +11,7 @@
"@auth/drizzle-adapter": "^1.11.2", "@auth/drizzle-adapter": "^1.11.2",
"@aws-sdk/client-s3": "^3.1045.0", "@aws-sdk/client-s3": "^3.1045.0",
"@aws-sdk/s3-request-presigner": "^3.1045.0", "@aws-sdk/s3-request-presigner": "^3.1045.0",
"@react-pdf/renderer": "^4.5.1",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -19,6 +20,7 @@
"nanoid": "^5.1.11", "nanoid": "^5.1.11",
"next": "16.2.6", "next": "16.2.6",
"next-auth": "5.0.0-beta.31", "next-auth": "5.0.0-beta.31",
"next-pwa": "^5.6.0",
"nodemailer": "^7.0.13", "nodemailer": "^7.0.13",
"openai": "^6.37.0", "openai": "^6.37.0",
"postgres": "^3.4.9", "postgres": "^3.4.9",
@ -26,6 +28,9 @@
"react": "19.2.4", "react": "19.2.4",
"react-chartjs-2": "^5.3.1", "react-chartjs-2": "^5.3.1",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"recharts": "^3.8.1",
"resend": "^6.12.3",
"sharp": "^0.34.5",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
@ -34,6 +39,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/sharp": "^0.32.0",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"tailwindcss": "^4", "tailwindcss": "^4",
"tsx": "^4.21.0", "tsx": "^4.21.0",

4875
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ test: "success" }); }

View file

@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { sql } from "@/db"; import { sql } from "@/db";
import { detectMedicalIntent, ESCALATION_RULES } from "@/lib/ai/medical-triggers"; import { detectMedicalIntent, ESCALATION_RULES } from "@/lib/ai/medical-triggers";
import { logAudit } from "@/lib/audit"; import { logAudit } from "@/lib/audit";
import { requireFamily } from "@/lib/auth";
const LITELLM_URL = process.env.LITELLM_BASE_URL; const LITELLM_URL = process.env.LITELLM_BASE_URL;
const LITELLM_KEY = process.env.LITELLM_API_KEY; const LITELLM_KEY = process.env.LITELLM_API_KEY;
@ -13,6 +14,15 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "AI service not configured" }, { status: 503 }); return NextResponse.json({ error: "AI service not configured" }, { status: 503 });
} }
// Require authentication
const auth = await requireFamily();
if (!auth.success) {
return NextResponse.json({ error: auth.error }, { status: auth.status });
}
const session = auth.session!;
const familyId = session.familyId!;
const body = await request.json(); const body = await request.json();
const { messages, childId } = body; const { messages, childId } = body;
@ -25,11 +35,10 @@ export async function POST(request: Request) {
// HARD GUARDRAIL: Check for medical intent BEFORE calling LLM // HARD GUARDRAIL: Check for medical intent BEFORE calling LLM
const intent = detectMedicalIntent(lastUserMsg); const intent = detectMedicalIntent(lastUserMsg);
if (intent.isMedical) { if (intent.isMedical) {
// Fetch pediatrician phone // Fetch pediatrician phone using familyId
const sessionToken = request.headers.get("cookie")?.match(/tia_session=([^;]+)/)?.[1] || "";
const families = await sql` const families = await sql`
SELECT pediatrician_phone FROM families SELECT pediatrician_phone FROM families
WHERE id IN (SELECT family_id FROM family_members WHERE user_id IN (SELECT user_id FROM sessions WHERE session_token = ${sessionToken})) WHERE id = ${familyId}
LIMIT 1 LIMIT 1
`; `;
const pediatricianPhone = families[0]?.pediatrician_phone; const pediatricianPhone = families[0]?.pediatrician_phone;
@ -49,6 +58,8 @@ export async function POST(request: Request) {
action: "ai_medical_redirect", action: "ai_medical_redirect",
metadata: { category: intent.category, keyword: intent.matchedKeyword }, metadata: { category: intent.category, keyword: intent.matchedKeyword },
request, request,
userId: session.userId,
familyId,
}); });
return NextResponse.json({ return NextResponse.json({

View file

@ -1,42 +0,0 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
import { cookies } from "next/headers";
export async function GET() {
const sessionToken = (await cookies()).get("tia_session")?.value;
if (!sessionToken) {
return NextResponse.json({ error: "no cookie" });
}
// Get user from session
const sessions = await sql.unsafe(
`SELECT s.user_id FROM sessions s WHERE s.session_token = $1`,
[sessionToken]
);
const userId = sessions?.[0]?.user_id;
// Check families table
const families = await sql.unsafe(
`SELECT id, name FROM families ORDER BY created_at DESC LIMIT 5`
);
// Check family_members table
const members = await sql.unsafe(
`SELECT id, family_id, user_id, role FROM family_members ORDER BY created_at DESC LIMIT 5`
);
// Check children table
const children = await sql.unsafe(
`SELECT id, family_id, name FROM children ORDER BY created_at DESC LIMIT 5`
);
return NextResponse.json({
cookie: sessionToken?.slice(0, 20) + "...",
userId,
families,
members,
children
});
}

View file

@ -2,6 +2,11 @@ import { NextResponse } from "next/server";
import { sql } from "@/db"; import { sql } from "@/db";
import { rateLimit, getClientIp, getRateLimitKey } from "@/lib/rate-limit"; import { rateLimit, getClientIp, getRateLimitKey } from "@/lib/rate-limit";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { Resend } from "resend";
const RESEND_API_KEY = process.env.RESEND_API_KEY;
const EMAIL_FROM = process.env.EMAIL_FROM || "Tia <noreply@tia.manohargupta.com>";
const RESET_URL = process.env.NEXT_PUBLIC_APP_URL || "https://tia.manohargupta.com";
export async function POST(request: Request) { export async function POST(request: Request) {
const ip = getClientIp(request); const ip = getClientIp(request);
@ -36,8 +41,34 @@ export async function POST(request: Request) {
[user.id, token, expiresAt.toISOString()] [user.id, token, expiresAt.toISOString()]
); );
// In production, send email with reset link const resetLink = `${RESET_URL}/reset-password?token=${token}`;
console.log(`[RESET-TOKEN] user=${user.id} email=${email} token=reset_${token} expires=${expiresAt.toISOString()}`);
// Send email via Resend if API key is configured
if (RESEND_API_KEY) {
try {
const resend = new Resend(RESEND_API_KEY);
await resend.emails.send({
from: EMAIL_FROM,
to: email,
subject: "Reset your Tia password",
html: `
<div style="font-family: system-ui, sans-serif; max-width: 500px; margin: 0 auto;">
<h2>Reset your Tia password</h2>
<p>Click the button below to reset your password. This link expires in 1 hour.</p>
<a href="${resetLink}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 16px 0;">Reset Password</a>
<p style="color: #6b7280; font-size: 14px;">If you didn't request this, you can safely ignore this email.</p>
</div>
`,
});
console.log(`[RESET-EMAIL-SENT] user=${user.id} email=${email}`);
} catch (emailError) {
console.error("[RESET-EMAIL-FAILED]", emailError);
}
} else {
// Development fallback
console.log(`[RESET-TOKEN] user=${user.id} email=${email} token=reset_${token} expires=${expiresAt.toISOString()}`);
}
return NextResponse.json({ success: true, message: "If email exists, reset link sent" }); return NextResponse.json({ success: true, message: "If email exists, reset link sent" });
} catch (error) { } catch (error) {
console.error("Reset request error:", error); console.error("Reset request error:", error);

View file

@ -9,8 +9,6 @@ const publicRoutes = [
"/api/auth/signin", "/api/auth/signin",
"/api/admin/auth", "/api/admin/auth",
"/api/onboarding", "/api/onboarding",
"/api/ai",
"/api/auth/debug",
]; ];
// Protected API routes that need authentication // Protected API routes that need authentication