Skip to content

Commit 7e4c9a6

Browse files
fix: issues 553, 554, 555, 556 (#563)
Co-authored-by: Emmanuel Chukwunyere <emmanuelanalaba@gmail.com>
1 parent 55918a7 commit 7e4c9a6

24 files changed

Lines changed: 1858 additions & 68 deletions

File tree

next.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ const nextConfig = {
1818
},
1919
async headers() {
2020
return [
21+
{
22+
// Applied to every route; the more specific /embed/:id rule below
23+
// overrides X-Frame-Options for that one path.
24+
source: "/:path*",
25+
headers: [
26+
{ key: "X-Frame-Options", value: "DENY" },
27+
{ key: "X-Content-Type-Options", value: "nosniff" },
28+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
29+
{
30+
key: "Strict-Transport-Security",
31+
value: "max-age=63072000; includeSubDomains; preload",
32+
},
33+
{
34+
key: "Permissions-Policy",
35+
value: "camera=(), microphone=(), geolocation=(), payment=(self)",
36+
},
37+
],
38+
},
2139
{
2240
source: "/sw.js",
2341
headers: [

src/app/api/folders/[id]/route.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { FolderNameSchema, deleteFolder, getFolder, renameFolder } from "@/lib/folders";
3+
4+
/** PATCH /api/folders/:id — rename a folder. */
5+
export async function PATCH(
6+
request: NextRequest,
7+
{ params }: { params: { id: string } }
8+
) {
9+
try {
10+
if (!getFolder(params.id)) {
11+
return NextResponse.json({ error: "Folder not found" }, { status: 404 });
12+
}
13+
14+
const rawBody = await request.json();
15+
const parsed = FolderNameSchema.safeParse(rawBody);
16+
17+
if (!parsed.success) {
18+
return NextResponse.json(
19+
{
20+
error: "Invalid folder payload — expected { name: string }",
21+
details: parsed.error.issues,
22+
},
23+
{ status: 422 }
24+
);
25+
}
26+
27+
const folder = renameFolder(params.id, parsed.data.name);
28+
return NextResponse.json({ folder }, { status: 200 });
29+
} catch (error) {
30+
console.error("Folder rename error:", error);
31+
return NextResponse.json(
32+
{
33+
error: "Failed to rename folder",
34+
details: error instanceof Error ? error.message : String(error),
35+
},
36+
{ status: 500 }
37+
);
38+
}
39+
}
40+
41+
/** DELETE /api/folders/:id — delete a folder and clear its membership. */
42+
export async function DELETE(
43+
_request: NextRequest,
44+
{ params }: { params: { id: string } }
45+
) {
46+
try {
47+
const removed = deleteFolder(params.id);
48+
if (!removed) {
49+
return NextResponse.json({ error: "Folder not found" }, { status: 404 });
50+
}
51+
return NextResponse.json({ success: true }, { status: 200 });
52+
} catch (error) {
53+
console.error("Folder delete error:", error);
54+
return NextResponse.json(
55+
{
56+
error: "Failed to delete folder",
57+
details: error instanceof Error ? error.message : String(error),
58+
},
59+
{ status: 500 }
60+
);
61+
}
62+
}

src/app/api/folders/route.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import {
3+
FolderNameSchema,
4+
MAX_FOLDERS,
5+
createFolder,
6+
getMembershipMap,
7+
listFolders,
8+
} from "@/lib/folders";
9+
10+
/**
11+
* GET /api/folders — every folder, plus the invoiceId → folderIds membership map.
12+
*
13+
* One request serves both the sidebar (the folder list) and list filtering
14+
* (the membership map), so the folders page doesn't fan out a request per invoice.
15+
*/
16+
export async function GET(_request: NextRequest) {
17+
try {
18+
return NextResponse.json(
19+
{ folders: listFolders(), byInvoice: getMembershipMap() },
20+
{ status: 200 }
21+
);
22+
} catch (error) {
23+
console.error("Folder list error:", error);
24+
return NextResponse.json(
25+
{
26+
error: "Failed to list folders",
27+
details: error instanceof Error ? error.message : String(error),
28+
},
29+
{ status: 500 }
30+
);
31+
}
32+
}
33+
34+
/** POST /api/folders — create a new named folder. */
35+
export async function POST(request: NextRequest) {
36+
try {
37+
if (listFolders().length >= MAX_FOLDERS) {
38+
return NextResponse.json({ error: `Cannot exceed ${MAX_FOLDERS} folders` }, { status: 422 });
39+
}
40+
41+
const rawBody = await request.json();
42+
const parsed = FolderNameSchema.safeParse(rawBody);
43+
44+
if (!parsed.success) {
45+
return NextResponse.json(
46+
{
47+
error: "Invalid folder payload — expected { name: string }",
48+
details: parsed.error.issues,
49+
},
50+
{ status: 422 }
51+
);
52+
}
53+
54+
const folder = createFolder(parsed.data.name);
55+
return NextResponse.json({ folder }, { status: 201 });
56+
} catch (error) {
57+
console.error("Folder create error:", error);
58+
return NextResponse.json(
59+
{
60+
error: "Failed to create folder",
61+
details: error instanceof Error ? error.message : String(error),
62+
},
63+
{ status: 500 }
64+
);
65+
}
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { FolderMembershipSchema, getFoldersForInvoice, setFoldersForInvoice } from "@/lib/folders";
3+
4+
/** GET /api/invoices/:id/folders — folder ids this invoice currently belongs to. */
5+
export async function GET(
6+
_request: NextRequest,
7+
{ params }: { params: { id: string } }
8+
) {
9+
try {
10+
return NextResponse.json(
11+
{ invoiceId: params.id, folderIds: getFoldersForInvoice(params.id) },
12+
{ status: 200 }
13+
);
14+
} catch (error) {
15+
console.error("Invoice folder fetch error:", error);
16+
return NextResponse.json(
17+
{
18+
error: "Failed to fetch invoice folders",
19+
details: error instanceof Error ? error.message : String(error),
20+
},
21+
{ status: 500 }
22+
);
23+
}
24+
}
25+
26+
/**
27+
* PATCH /api/invoices/:id/folders — replace the invoice's folder membership.
28+
*
29+
* The client sends the full desired list (both adds and removes go through
30+
* here), matching `PATCH /api/invoices/:id/tags`.
31+
*/
32+
export async function PATCH(
33+
request: NextRequest,
34+
{ params }: { params: { id: string } }
35+
) {
36+
try {
37+
const rawBody = await request.json();
38+
const parsed = FolderMembershipSchema.safeParse(rawBody);
39+
40+
if (!parsed.success) {
41+
return NextResponse.json(
42+
{
43+
error: "Invalid payload — expected { folderIds: string[] }",
44+
details: parsed.error.issues,
45+
},
46+
{ status: 422 }
47+
);
48+
}
49+
50+
const folderIds = setFoldersForInvoice(params.id, parsed.data.folderIds);
51+
52+
return NextResponse.json(
53+
{ success: true, invoiceId: params.id, folderIds },
54+
{ status: 200 }
55+
);
56+
} catch (error) {
57+
console.error("Invoice folder save error:", error);
58+
return NextResponse.json(
59+
{
60+
error: "Failed to save invoice folders",
61+
details: error instanceof Error ? error.message : String(error),
62+
},
63+
{ status: 500 }
64+
);
65+
}
66+
}

src/app/api/invoices/route.ts

Lines changed: 9 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { splitClient } from "@/lib/stellar";
3-
4-
const PAGE_SIZE = 20;
2+
import { fetchInvoicesForAddress } from "@/lib/stellar/horizonServer";
53

64
/**
75
* GET /api/invoices?cursor=<id>&limit=20&publicKey=<address>&q=<query>
@@ -19,9 +17,9 @@ const PAGE_SIZE = 20;
1917
export async function GET(request: NextRequest) {
2018
const { searchParams } = request.nextUrl;
2119
const publicKey = searchParams.get("publicKey");
22-
const cursorParam = searchParams.get("cursor");
20+
const cursor = searchParams.get("cursor");
2321
const limitParam = searchParams.get("limit");
24-
const q = searchParams.get("q")?.trim().toLowerCase() || "";
22+
const q = searchParams.get("q") ?? undefined;
2523

2624
if (!publicKey) {
2725
return NextResponse.json(
@@ -30,66 +28,12 @@ export async function GET(request: NextRequest) {
3028
);
3129
}
3230

33-
const limit = Math.min(
34-
Math.max(1, parseInt(limitParam ?? String(PAGE_SIZE), 10) || PAGE_SIZE),
35-
50,
36-
);
37-
38-
// Determine starting invoice id (cursor is the last id we already returned)
39-
const startId = cursorParam ? parseInt(cursorParam, 10) + 1 : 1;
40-
41-
const results = [];
42-
let lastCheckedId = startId - 1;
43-
44-
for (let id = startId; results.length < limit; id++) {
45-
// Safety cap — don't scan more than limit*10 ids in a single request
46-
if (id > startId + limit * 10) break;
47-
48-
lastCheckedId = id;
49-
50-
try {
51-
const inv = await splitClient.getInvoice(String(id));
52-
const mine =
53-
inv.creator === publicKey ||
54-
inv.recipients.some((r) => r.address === publicKey);
55-
56-
if (mine) {
57-
if (q) {
58-
const memo = (inv as any).memo as string | undefined;
59-
const matchesQuery =
60-
(inv.title || "").toLowerCase().startsWith(q) ||
61-
(memo || "").toLowerCase().startsWith(q);
62-
if (matchesQuery) {
63-
results.push(inv);
64-
}
65-
} else {
66-
results.push(inv);
67-
}
68-
}
69-
} catch {
70-
// splitClient throws when invoice id does not exist — treat as end of list
71-
return NextResponse.json(
72-
{ invoices: results, nextCursor: null },
73-
{
74-
headers: {
75-
"Cache-Control": "private, no-store",
76-
},
77-
},
78-
);
79-
}
80-
}
81-
82-
// If we filled the page we don't know yet whether there are more — return the
83-
// id of the last invoice we fetched so the client can continue from there.
84-
const nextCursor =
85-
results.length === limit ? String(results[results.length - 1].id) : null;
31+
const limit = limitParam ? parseInt(limitParam, 10) : undefined;
32+
const result = await fetchInvoicesForAddress(publicKey, { cursor, limit, q });
8633

87-
return NextResponse.json(
88-
{ invoices: results, nextCursor },
89-
{
90-
headers: {
91-
"Cache-Control": "private, no-store",
92-
},
34+
return NextResponse.json(result, {
35+
headers: {
36+
"Cache-Control": "private, no-store",
9337
},
94-
);
38+
});
9539
}

0 commit comments

Comments
 (0)