Ten routes accepted requests with no getCurrentUser check (transactions/[id], bulk, splits, tags-on-tx, splits/settle, statements/[id], tags, tags/[id], merchants, participants/[id]/balance), and by-id routes did no ownership check at all — any participant could read or modify another's data. Adds canAccessTransactions() (owner via statement/direct, or split participant), applies it to every transaction-scoped route, owner-scopes statements/[id], and rescopes splits/settle in raw SQL so settlement only touches splits the caller is party to. Also: all trip analytics now sum COALESCE(amount_aud, amount) instead of raw amount, matching every other analytics query — trip totals previously added foreign-currency amounts to AUD ones unit-less. And rules apply_split no longer delete+reinserts splits (which reset settled flags on every run) — it upserts share_percent and removes only participants no longer in the rule.
96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma, queryRaw } from "@/lib/db";
|
|
import { assignTransactionsToTrip, canAccessTransactions } from "@/lib/queries";
|
|
import { getCurrentUser } from "@/lib/auth";
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const user = await getCurrentUser(req);
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
|
const body = await req.json();
|
|
const { action, ids, category, merchant_normalized, splits, tag_id } = body as {
|
|
action: string;
|
|
ids: number[];
|
|
category?: string;
|
|
merchant_normalized?: string;
|
|
splits?: { participant_id: number; share_percent: number }[];
|
|
tag_id?: number;
|
|
};
|
|
|
|
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
return NextResponse.json({ error: "ids required" }, { status: 400 });
|
|
}
|
|
|
|
if (!(await canAccessTransactions(user.id, ids.map(Number)))) {
|
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
}
|
|
|
|
if (action === "categorize" && category) {
|
|
const ops = ids.map((id) =>
|
|
prisma.transaction_overrides.upsert({
|
|
where: { transaction_id: id },
|
|
update: { category_override: category, updated_at: new Date() },
|
|
create: { transaction_id: id, category_override: category },
|
|
})
|
|
);
|
|
await prisma.$transaction(ops);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if (action === "normalize" && merchant_normalized) {
|
|
const ops = ids.map((id) =>
|
|
prisma.transaction_overrides.upsert({
|
|
where: { transaction_id: id },
|
|
update: { merchant_normalized, updated_at: new Date() },
|
|
create: { transaction_id: id, merchant_normalized },
|
|
})
|
|
);
|
|
await prisma.$transaction(ops);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if (action === "split" && Array.isArray(splits) && splits.length > 0) {
|
|
const total = splits.reduce((s, x) => s + x.share_percent, 0);
|
|
if (Math.abs(total - 100) > 0.01) {
|
|
return NextResponse.json({ error: "Shares must sum to 100%" }, { status: 400 });
|
|
}
|
|
await prisma.$transaction(
|
|
ids.flatMap((id) => [
|
|
prisma.transaction_splits.deleteMany({ where: { transaction_id: id } }),
|
|
prisma.transaction_splits.createMany({
|
|
data: splits.map((s) => ({
|
|
transaction_id: id,
|
|
participant_id: s.participant_id,
|
|
share_percent: s.share_percent,
|
|
})),
|
|
}),
|
|
])
|
|
);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if ((action === "tag" || action === "untag") && tag_id) {
|
|
if (action === "tag") {
|
|
for (const id of ids) {
|
|
await queryRaw(
|
|
`INSERT INTO transaction_tags (transaction_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
[id, tag_id]
|
|
);
|
|
}
|
|
} else {
|
|
await queryRaw(
|
|
`DELETE FROM transaction_tags WHERE transaction_id = ANY($1::int[]) AND tag_id = $2`,
|
|
[ids, tag_id]
|
|
);
|
|
}
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
if (action === "assign_trip") {
|
|
const { trip_id } = body as { ids: number[]; trip_id: number | null };
|
|
await assignTransactionsToTrip(trip_id, ids);
|
|
return NextResponse.json({ updated: ids.length });
|
|
}
|
|
|
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
}
|