Skip to content
Merged
158 changes: 158 additions & 0 deletions paystell-frontend/src/app/api/deposit/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { NextResponse, NextRequest } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { DepositRequest } from "@/lib/types/deposit";

import { depositStore } from "../deposit-store";

export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 1. Authentication check
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ message: "Authentication required" },
{ status: 401 }
);
}

const { id } = params;

// 2. Get deposit request
const deposit = depositStore.get(id);
if (!deposit) {
return NextResponse.json(
{ message: "Deposit request not found" },
{ status: 404 }
);
}

// 3. Check if user has access to this deposit
if (deposit.ownerId !== session.user.id) {
return NextResponse.json(
{ message: "Access denied" },
{ status: 403 }
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return NextResponse.json({
success: true,
deposit,
});
} catch (error: unknown) {
console.error("Deposit retrieval error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to retrieve deposit";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
}

export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 1. Authentication check
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ message: "Authentication required" },
{ status: 401 }
);
}

const { id } = params;
const updates = await request.json();

// 2. Get existing deposit request
const existingDeposit = depositStore.get(id);
if (!existingDeposit) {
return NextResponse.json(
{ message: "Deposit request not found" },
{ status: 404 }
);
}

// 3. Check if user has access to this deposit
if (existingDeposit.ownerId !== session.user.id) {
return NextResponse.json(
{ message: "Access denied" },
{ status: 403 }
);
}

// 4. Validate updates
const allowedUpdates = ["status", "transactionHash", "confirmedAt"];
const validUpdates: Partial<DepositRequest> = {};

for (const [key, value] of Object.entries(updates)) {
if (allowedUpdates.includes(key)) {
(validUpdates as Record<string, unknown>)[key] = value;
}
}

// 5. Update deposit request
const updatedDeposit = depositStore.update(id, validUpdates);

return NextResponse.json({
success: true,
deposit: updatedDeposit,
});
} catch (error: unknown) {
console.error("Deposit update error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to update deposit";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 1. Authentication check
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ message: "Authentication required" },
{ status: 401 }
);
}

const { id } = params;

// 2. Get existing deposit request
const existingDeposit = depositStore.get(id);
if (!existingDeposit) {
return NextResponse.json(
{ message: "Deposit request not found" },
{ status: 404 }
);
}

// 3. Check if user has access to this deposit
if (existingDeposit.ownerId !== session.user.id) {
return NextResponse.json(
{ message: "Access denied" },
{ status: 403 }
);
}

// 4. Delete deposit request
depositStore.delete(id);

return NextResponse.json({
success: true,
message: "Deposit request deleted",
});
} catch (error: unknown) {
console.error("Deposit deletion error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to delete deposit";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
}
39 changes: 39 additions & 0 deletions paystell-frontend/src/app/api/deposit/deposit-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DepositRequest } from "@/lib/types/deposit";

// Shared in-memory store for deposit requests
// In production, use a database
export const depositRequests = new Map<string, DepositRequest>();

// Helper functions for deposit management
export const depositStore = {
create: (id: string, deposit: DepositRequest) => {
depositRequests.set(id, deposit);
return deposit;
},

get: (id: string) => {
return depositRequests.get(id);
},

update: (id: string, updates: Partial<DepositRequest>) => {
const existing = depositRequests.get(id);
if (!existing) return null;

const updated = { ...existing, ...updates };
depositRequests.set(id, updated);
return updated;
},

delete: (id: string) => {
return depositRequests.delete(id);
},

getAll: () => {
return Array.from(depositRequests.values());
},

getByUser: (userId: string) => {
return Array.from(depositRequests.values())
.filter(deposit => deposit.ownerId === userId);
}
Comment on lines +35 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Filter deposits by owner id instead of the receiving address

Filtering with deposit.address === userId only works when you intentionally pollute the address field with the user id (which in turn breaks the rest of the flow). As soon as a real Stellar address is stored, the current user can no longer retrieve their deposits. Persist the authenticated id in a dedicated property (e.g. ownerId, added to DepositRequest) and filter on that instead so you can keep address for the blockchain destination.

   getByUser: (userId: string) => {
     return Array.from(depositRequests.values())
-      .filter(deposit => deposit.address === userId);
+      .filter(deposit => deposit.ownerId === userId);
   }
πŸ€– Prompt for AI Agents
In paystell-frontend/src/app/api/deposit/deposit-store.ts around lines 35 to 38,
the code filters deposits by comparing deposit.address to userId which is
incorrect; change the data model to include a dedicated ownerId property on
DepositRequest, update wherever DepositRequest instances are created to set
ownerId = authenticatedUser.id (leaving address for the blockchain destination),
and replace the filter to use deposit.ownerId === userId; also update the
DepositRequest type definition and any tests or callers that construct deposit
requests to populate ownerId.

};
200 changes: 200 additions & 0 deletions paystell-frontend/src/app/api/deposit/monitor/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { NextResponse, NextRequest } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { DepositMonitoringConfig } from "@/lib/types/deposit";
import { isValidStellarAddress } from "@/lib/deposit/deposit-utils";

// In-memory store for monitoring configurations per user
// In production, use a database
const monitoringConfigs = new Map<string, Map<string, DepositMonitoringConfig>>();

export async function POST(request: NextRequest) {
try {
// 1. Authentication check
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ message: "Authentication required" },
{ status: 401 }
);
}

const { address, asset, minAmount, maxAmount, memo } = await request.json();

// 2. Input validation
if (!address || !asset) {
return NextResponse.json(
{ message: "Address and asset are required" },
{ status: 400 }
);
}

// 3. Validate Stellar address
if (!isValidStellarAddress(address)) {
return NextResponse.json(
{ message: "Invalid Stellar address" },
{ status: 400 }
);
}

// 4. Validate asset
const supportedAssets = ["XLM", "USDC", "USDT"];
if (!supportedAssets.includes(asset)) {
return NextResponse.json(
{ message: "Unsupported asset" },
{ status: 400 }
);
}

// 5. Validate amounts if provided
if (minAmount && (isNaN(parseFloat(minAmount)) || parseFloat(minAmount) <= 0)) {
return NextResponse.json(
{ message: "Invalid minimum amount" },
{ status: 400 }
);
}

if (maxAmount && (isNaN(parseFloat(maxAmount)) || parseFloat(maxAmount) <= 0)) {
return NextResponse.json(
{ message: "Invalid maximum amount" },
{ status: 400 }
);
}

if (minAmount && maxAmount && parseFloat(minAmount) > parseFloat(maxAmount)) {
return NextResponse.json(
{ message: "Minimum amount cannot be greater than maximum amount" },
{ status: 400 }
);
}

// 6. Create monitoring configuration
const config: DepositMonitoringConfig = {
address,
asset,
minAmount: minAmount || undefined,
maxAmount: maxAmount || undefined,
memo: memo || undefined,
};

// 7. Store monitoring configuration per user
const userId = session.user.id;
const key = `${address}_${asset}`;

if (!monitoringConfigs.has(userId)) {
monitoringConfigs.set(userId, new Map());
}

monitoringConfigs.get(userId)!.set(key, config);

return NextResponse.json({
success: true,
config,
message: "Monitoring started",
});
} catch (error: unknown) {
console.error("Monitoring setup error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to setup monitoring";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
}

export async function GET(request: NextRequest) {
try {
// 1. Authentication check
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ message: "Authentication required" },
{ status: 401 }
);
}

const { searchParams } = new URL(request.url);
const address = searchParams.get("address");
const asset = searchParams.get("asset");

// 2. Get monitoring configurations for this user only
const userId = session.user.id;
const userConfigs = monitoringConfigs.get(userId) || new Map();
let configs = Array.from(userConfigs.values());

// 3. Filter by address if provided
if (address) {
configs = configs.filter(config => config.address === address);
}

// 4. Filter by asset if provided
if (asset) {
configs = configs.filter(config => config.asset === asset);
}

return NextResponse.json({
success: true,
configs,
total: configs.length,
});
} catch (error: unknown) {
console.error("Monitoring retrieval error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to retrieve monitoring configs";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
}

export async function DELETE(request: NextRequest) {
try {
// 1. Authentication check
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ message: "Authentication required" },
{ status: 401 }
);
}

const { searchParams } = new URL(request.url);
const address = searchParams.get("address");
const asset = searchParams.get("asset");

// 2. Input validation
if (!address || !asset) {
return NextResponse.json(
{ message: "Address and asset are required" },
{ status: 400 }
);
}

// 3. Remove monitoring configuration for this user only
const userId = session.user.id;
const key = `${address}_${asset}`;

const userConfigs = monitoringConfigs.get(userId);
if (!userConfigs) {
return NextResponse.json(
{ message: "Monitoring configuration not found" },
{ status: 404 }
);
}

const deleted = userConfigs.delete(key);

if (!deleted) {
return NextResponse.json(
{ message: "Monitoring configuration not found" },
{ status: 404 }
);
}

return NextResponse.json({
success: true,
message: "Monitoring stopped",
});
} catch (error: unknown) {
console.error("Monitoring removal error:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to remove monitoring";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
}
Loading
Loading