fix(security+trips): auth/ownership on all API routes; trip analytics in AUD
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.
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getTransactionById } from "@/lib/queries";
|
||||
import { getTransactionById, canAccessTransactions } from "@/lib/queries";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { queryRaw } from "@/lib/db";
|
||||
|
||||
const VALID_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"];
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const user = await getCurrentUser(req);
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
const { id } = await params;
|
||||
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
const txn = await getTransactionById(Number(id));
|
||||
if (!txn) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
return NextResponse.json(txn);
|
||||
@@ -19,8 +25,13 @@ export async function PATCH(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const user = await getCurrentUser(req);
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
const { id } = await params;
|
||||
const transactionId = Number(id);
|
||||
if (!(await canAccessTransactions(user.id, [transactionId]))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
const body = await req.json();
|
||||
|
||||
const { category, merchant_normalized, notes, transaction_type, my_share_percent, description, amount, transaction_date, trip_id } = body as {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { queryRaw } from "@/lib/db";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { canAccessTransactions } from "@/lib/queries";
|
||||
|
||||
interface SplitInput {
|
||||
participant_id: number;
|
||||
@@ -19,10 +21,15 @@ interface SplitRow {
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const user = await getCurrentUser(req);
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
const { id } = await params;
|
||||
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
const splits = await queryRaw<SplitRow>(
|
||||
`SELECT ts.*, p.name
|
||||
FROM transaction_splits ts
|
||||
@@ -38,8 +45,13 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const user = await getCurrentUser(req);
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
const { id } = await params;
|
||||
const transactionId = Number(id);
|
||||
if (!(await canAccessTransactions(user.id, [transactionId]))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
const { splits } = (await req.json()) as { splits: SplitInput[] };
|
||||
|
||||
if (!splits || !Array.isArray(splits) || splits.length === 0) {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRaw } from "@/lib/db";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
import { canAccessTransactions } from "@/lib/queries";
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const user = await getCurrentUser(req);
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
const { id } = await params;
|
||||
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
const { tag_id } = await req.json();
|
||||
if (!tag_id) return NextResponse.json({ error: "tag_id required" }, { status: 400 });
|
||||
await queryRaw(
|
||||
@@ -13,7 +20,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const user = await getCurrentUser(req);
|
||||
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||
const { id } = await params;
|
||||
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
const { tag_id } = await req.json();
|
||||
if (!tag_id) return NextResponse.json({ error: "tag_id required" }, { status: 400 });
|
||||
await queryRaw(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma, queryRaw } from "@/lib/db";
|
||||
import { assignTransactionsToTrip } from "@/lib/queries";
|
||||
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;
|
||||
@@ -17,6 +20,10 @@ export async function POST(req: NextRequest) {
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user