Skip to content

Commit 3ee9957

Browse files
MJigahunrealtim-techKingsman-99
authored
fix: issues 557, 558, 559, 560 (#564)
Co-authored-by: unrealtim-tech <jigah4thjuly@gmail.com> Co-authored-by: Emmanuel Chukwunyere <emmanuelanalaba@gmail.com>
1 parent 7e4c9a6 commit 3ee9957

50 files changed

Lines changed: 1086 additions & 47 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ NEXT_PUBLIC_STELLAR_NETWORK=testnet
22
NEXT_PUBLIC_CONTRACT_ID=YOUR_CONTRACT_ID_HERE
33
NEXT_PUBLIC_RPC_URL=https://soroban-testnet.stellar.org
44
API_KEY_SIGNING_SECRET=replace-with-a-long-random-server-secret
5+
CSRF_SECRET=replace-with-a-long-random-server-secret
56

67
# Generate with: npx web-push generate-vapid-keys
78
NEXT_PUBLIC_VAPID_PUBLIC_KEY=replace-with-vapid-public-key

src/app/api/api-keys/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { type ApiKeyScope } from "@/lib/apiKeys";
33
import { generateSignedApiKey } from "@/lib/signedApiKeys";
4+
import { assertCsrf } from "@/lib/middleware/csrfMiddleware";
45

56
function isScope(value: unknown): value is ApiKeyScope {
67
return value === "read" || value === "write";
78
}
89

910
export async function POST(request: NextRequest) {
11+
const csrfError = await assertCsrf(request);
12+
if (csrfError) return csrfError;
13+
1014
const body = await request.json().catch(() => null);
1115
const scope = body?.scope;
1216

src/app/api/csrf/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { NextResponse } from "next/server";
2+
import { generateCsrfToken } from "@/lib/csrf";
3+
4+
/** GET /api/csrf — issues a fresh CSRF token, valid for 60 minutes. */
5+
export async function GET() {
6+
const { token, expiresAt } = await generateCsrfToken();
7+
return NextResponse.json({ token, expiresAt });
8+
}

src/app/api/dev/faucet/route.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { StrKey } from "@stellar/stellar-sdk";
3+
import { assertCsrf } from "@/lib/middleware/csrfMiddleware";
4+
5+
const HORIZON_URL =
6+
process.env.NEXT_PUBLIC_HORIZON_URL ??
7+
(process.env.NEXT_PUBLIC_STELLAR_NETWORK === "mainnet"
8+
? "https://horizon.stellar.org"
9+
: "https://horizon-testnet.stellar.org");
10+
11+
const FRIENDBOT_URL = "https://friendbot.stellar.org";
12+
13+
/**
14+
* Dev-only proxy for the Stellar testnet Friendbot. Never available in
15+
* production — guarded both by NODE_ENV and by the app only rendering the
16+
* widget that calls this route on testnet.
17+
*
18+
* POST /api/dev/faucet { publicKey: string }
19+
*/
20+
export async function POST(request: NextRequest) {
21+
if (process.env.NODE_ENV === "production" || process.env.NEXT_PUBLIC_STELLAR_NETWORK === "mainnet") {
22+
return NextResponse.json({ error: "Faucet is not available in this environment" }, { status: 403 });
23+
}
24+
25+
const csrfError = await assertCsrf(request);
26+
if (csrfError) return csrfError;
27+
28+
let publicKey: unknown;
29+
try {
30+
({ publicKey } = await request.json());
31+
} catch {
32+
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
33+
}
34+
35+
if (typeof publicKey !== "string" || !StrKey.isValidEd25519PublicKey(publicKey)) {
36+
return NextResponse.json({ error: "A valid Stellar public key is required" }, { status: 400 });
37+
}
38+
39+
const friendbotResponse = await fetch(`${FRIENDBOT_URL}?addr=${encodeURIComponent(publicKey)}`);
40+
41+
if (!friendbotResponse.ok) {
42+
let detail: string | undefined;
43+
try {
44+
const body = await friendbotResponse.json();
45+
detail = body?.detail;
46+
} catch {
47+
// Friendbot didn't return JSON — fall through with no detail.
48+
}
49+
50+
if (friendbotResponse.status === 400) {
51+
// Friendbot returns 400 when the account already exists / is already funded.
52+
return NextResponse.json(
53+
{ alreadyFunded: true, message: detail ?? "This account is already funded." },
54+
{ status: 200 }
55+
);
56+
}
57+
58+
return NextResponse.json(
59+
{ error: detail ?? "Friendbot request failed" },
60+
{ status: 502 }
61+
);
62+
}
63+
64+
const account = await fetch(`${HORIZON_URL}/accounts/${publicKey}`).then((res) =>
65+
res.ok ? res.json() : null
66+
);
67+
68+
const nativeBalance = account?.balances?.find((b: any) => b.asset_type === "native");
69+
const xlm = nativeBalance ? (parseFloat(nativeBalance.balance) || 0).toFixed(7) : null;
70+
71+
return NextResponse.json({ funded: true, xlm });
72+
}

src/app/api/invoices/[id]/address-change-requests/[requestId]/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import type { AddressChangeRequestStatus } from '@/types/addressChangeRequest';
3+
import { assertCsrf } from '@/lib/middleware/csrfMiddleware';
34

45
// Import the store (in real implementation, would use database)
56
// For now using a simple in-memory approach (would be shared across routes)
@@ -9,6 +10,9 @@ export async function PATCH(
910
request: NextRequest,
1011
{ params }: { params: { id: string; requestId: string } }
1112
) {
13+
const csrfError = await assertCsrf(request);
14+
if (csrfError) return csrfError;
15+
1216
try {
1317
const { id: invoiceId, requestId } = params;
1418
const body = await request.json();

src/app/api/invoices/[id]/address-change-requests/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import type { AddressChangeRequest, AddressChangeRequestStatus } from '@/types/addressChangeRequest';
3+
import { assertCsrf } from '@/lib/middleware/csrfMiddleware';
34

45
// In-memory store for address change requests
56
// In production, this would use a database
@@ -41,6 +42,9 @@ export async function POST(
4142
request: NextRequest,
4243
{ params }: { params: { id: string } }
4344
) {
45+
const csrfError = await assertCsrf(request);
46+
if (csrfError) return csrfError;
47+
4448
try {
4549
const invoiceId = params.id;
4650
const body = await request.json();

src/app/api/invoices/[id]/comments/[commentId]/reactions/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { getComment, isAllowedEmoji, toggleReaction, ALLOWED_EMOJIS } from "@/lib/commentStore";
3+
import { assertCsrf } from "@/lib/middleware/csrfMiddleware";
34

45
export async function POST(
56
request: NextRequest,
67
{ params }: { params: { id: string; commentId: string } }
78
) {
9+
const csrfError = await assertCsrf(request);
10+
if (csrfError) return csrfError;
11+
812
const body = await request.json().catch(() => null);
913
const emoji = body?.emoji;
1014
const reactorId = body?.reactorId;

src/app/api/invoices/[id]/comments/[commentId]/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { deleteComment, getComment } from "@/lib/commentStore";
33
import { getSplitClient } from "@/lib/stellar";
4+
import { assertCsrf } from "@/lib/middleware/csrfMiddleware";
45

56
export async function DELETE(
67
request: NextRequest,
78
{ params }: { params: { id: string; commentId: string } }
89
) {
10+
const csrfError = await assertCsrf(request);
11+
if (csrfError) return csrfError;
12+
913
const body = await request.json().catch(() => null);
1014
const requesterAddress = body?.requesterAddress;
1115
const coCreatorWritePermission = body?.coCreatorWritePermission === true;

src/app/api/invoices/[id]/comments/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
reactorEmojis,
77
type StoredComment,
88
} from "@/lib/commentStore";
9+
import { assertCsrf } from "@/lib/middleware/csrfMiddleware";
910

1011
function serialise(comment: StoredComment, reactorId: string | null) {
1112
return {
@@ -32,6 +33,9 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
3233
}
3334

3435
export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
36+
const csrfError = await assertCsrf(request);
37+
if (csrfError) return csrfError;
38+
3539
const body = await request.json().catch(() => null);
3640
const authorAddress = body?.authorAddress;
3741
const text = typeof body?.text === "string" ? body.text.trim() : "";

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { splitClient } from "@/lib/stellar";
33
import { safeParseSplitMeta, type SplitMetaInput } from "@/lib/splitMetaSchema";
4+
import { assertCsrf } from "@/lib/middleware/csrfMiddleware";
45

56
interface SplitMetaStore {
67
[invoiceId: string]: SplitMetaInput;
@@ -12,6 +13,9 @@ export async function PATCH(
1213
request: NextRequest,
1314
{ params }: { params: { id: string } }
1415
) {
16+
const csrfError = await assertCsrf(request);
17+
if (csrfError) return csrfError;
18+
1519
try {
1620
const invoiceId = params.id;
1721
const walletPublicKey = request.headers.get("x-wallet-public-key");

0 commit comments

Comments
 (0)