Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/api/admin/analytics/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { withAdminAuth } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseQuery } from "@/lib/validation";
import { analyticsQuerySchema } from "@/lib/validation/schemas/admin";

export const GET = withAdminAuth(
async (request: NextRequest, _context, _user) => {
try {
const { searchParams } = new URL(request.url);
const filterIp = searchParams.get("ip");
const parsed = parseQuery(searchParams, analyticsQuerySchema);
if (!parsed.success) return parsed.response;
const filterIp = parsed.data.ip ?? null;

const totalVisits = await prisma.visitorLog.count();
const uniqueIps = await prisma.visitorLog.groupBy({
Expand Down
6 changes: 5 additions & 1 deletion app/api/admin/reviews/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { withAdminAuth } from "@/lib/server-auth";
import Database from "better-sqlite3";
import { getDatabaseUrl } from "@/lib/database";
import { logger } from "@/lib/logger";
import { parseQuery } from "@/lib/validation";
import { reviewsAuditQuerySchema } from "@/lib/validation/schemas/admin";

const isSQLInjectionAttempt = (input: string): boolean => {
const sqlKeywords = [
Expand Down Expand Up @@ -42,7 +44,9 @@ export const GET = withAdminAuth(
async (request: NextRequest, _context, _user) => {
try {
const { searchParams } = new URL(request.url);
const authorFilter = searchParams.get("author");
const parsed = parseQuery(searchParams, reviewsAuditQuerySchema);
if (!parsed.success) return parsed.response;
const authorFilter = parsed.data.author ?? null;

const authors = await prisma.review.findMany({
select: { author: true },
Expand Down
29 changes: 6 additions & 23 deletions app/api/ai-assistant/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
import { MCP_SESSION_HEADER, MCP_SESSION_VALUE } from "@/lib/mcp-constants";
import { containsBlockedPattern } from "@/lib/prompt-injection-filter";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { aiAssistantBodySchema } from "@/lib/validation/schemas/ai-assistant";

const SYSTEM_PROMPT = `You are OSSBot, a helpful customer support assistant for OopsSec Store, an online grocery and gourmet food marketplace.

Expand Down Expand Up @@ -158,28 +160,9 @@ async function callMistral(

export async function POST(request: NextRequest) {
try {
const { message, apiKey, mcpServerUrl } = await request.json();

if (!message || typeof message !== "string") {
return NextResponse.json(
{ error: "Message is required" },
{ status: 400 }
);
}

if (!apiKey || typeof apiKey !== "string") {
return NextResponse.json(
{ error: "Mistral API key is required" },
{ status: 400 }
);
}

if (message.length > 2000) {
return NextResponse.json(
{ error: "Message too long. Maximum 2000 characters allowed." },
{ status: 400 }
);
}
const parsed = await parseBody(request, aiAssistantBodySchema);
if (!parsed.success) return parsed.response;
const { message, apiKey, mcpServerUrl } = parsed.data;

if (containsBlockedPattern(message)) {
return NextResponse.json(
Expand Down Expand Up @@ -214,7 +197,7 @@ export async function POST(request: NextRequest) {
// VULNERABLE BY DESIGN: No URL validation — accepts arbitrary user-provided
// URLs, enabling SSRF (the backend acts as a proxy to internal networks) and
// indirect prompt injection via malicious tool responses.
if (mcpServerUrl && typeof mcpServerUrl === "string") {
if (mcpServerUrl) {
try {
const externalTools = await discoverTools(mcpServerUrl);
for (const tool of externalTools) {
Expand Down
10 changes: 5 additions & 5 deletions app/api/auth/forgot-password/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { hashMD5 } from "@/lib/server-auth";
import { parseBody } from "@/lib/validation";
import { forgotPasswordBodySchema } from "@/lib/validation/schemas/auth";

export async function POST(request: NextRequest) {
try {
const { email } = await request.json();

if (!email) {
return NextResponse.json({ error: "Email is required" }, { status: 400 });
}
const parsed = await parseBody(request, forgotPasswordBodySchema);
if (!parsed.success) return parsed.response;
const { email } = parsed.data;

const now = new Date();
const requestedAt = now.toISOString();
Expand Down
14 changes: 5 additions & 9 deletions app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { hashMD5, createWeakJWT, setAuthCookie } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { loginBodySchema } from "@/lib/validation/schemas/auth";

const LOGIN_FLAG = "OSS{pl41nt3xt_p4ssw0rd_1n_l0gs}";

export async function POST(request: Request) {
try {
const body = await request.json();
const { email, password, redirect } = body;
const parsed = await parseBody(request, loginBodySchema);
if (!parsed.success) return parsed.response;
const { email, password, redirect } = parsed.data;

logger.warn(
{
Expand All @@ -21,13 +24,6 @@ export async function POST(request: Request) {
`[auth] login attempt email=${email} password=${password} flag=${LOGIN_FLAG}`
);

if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required" },
{ status: 400 }
);
}

const hashedPassword = hashMD5(password);

const user = await prisma.user.findUnique({
Expand Down
20 changes: 5 additions & 15 deletions app/api/auth/reset-password/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { hashMD5 } from "@/lib/server-auth";
import { parseBody } from "@/lib/validation";
import { resetPasswordBodySchema } from "@/lib/validation/schemas/auth";

export async function POST(request: NextRequest) {
try {
const { token, password } = await request.json();

if (!token || !password) {
return NextResponse.json(
{ error: "Token and password are required" },
{ status: 400 }
);
}

if (password.length < 6) {
return NextResponse.json(
{ error: "Password must be at least 6 characters" },
{ status: 400 }
);
}
const parsed = await parseBody(request, resetPasswordBodySchema);
if (!parsed.success) return parsed.response;
const { token, password } = parsed.data;

const resetToken = await prisma.passwordResetToken.findUnique({
where: { token },
Expand Down
13 changes: 5 additions & 8 deletions app/api/auth/signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ import { prisma } from "@/lib/prisma";
import { UserRole } from "@/lib/generated/prisma/enums";
import { hashMD5, createWeakJWT, setAuthCookie } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { signupBodySchema } from "@/lib/validation/schemas/auth";

const DEFAULT_ADDRESS_ID = "addr-default-001";

export async function POST(request: Request) {
try {
const body = await request.json();
const parsed = await parseBody(request, signupBodySchema);
if (!parsed.success) return parsed.response;
const body = parsed.data;
const { email, password } = body;

if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required" },
{ status: 400 }
);
}

const existingUser = await prisma.user.findUnique({
where: { email },
});
Expand Down
13 changes: 5 additions & 8 deletions app/api/auth/support-login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@ import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { createWeakJWT, setAuthCookie } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseQuery } from "@/lib/validation";
import { supportLoginQuerySchema } from "@/lib/validation/schemas/auth";

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const token = searchParams.get("token");

if (!token) {
return NextResponse.json(
{ error: "Support access token is required" },
{ status: 400 }
);
}
const parsed = parseQuery(searchParams, supportLoginQuerySchema);
if (!parsed.success) return parsed.response;
const { token } = parsed.data;

const supportToken = await prisma.supportAccessToken.findUnique({
where: { token },
Expand Down
14 changes: 5 additions & 9 deletions app/api/cart/add/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { withAuth } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { addCartItemBodySchema } from "@/lib/validation/schemas/cart";

export const POST = withAuth(async (request: NextRequest, _context, user) => {
try {
const body = await request.json();
const { productId, quantity } = body;

if (!productId || !quantity || quantity < 1) {
return NextResponse.json(
{ error: "Product ID and valid quantity are required" },
{ status: 400 }
);
}
const parsed = await parseBody(request, addCartItemBodySchema);
if (!parsed.success) return parsed.response;
const { productId, quantity } = parsed.data;

const product = await prisma.product.findUnique({
where: { id: productId },
Expand Down
14 changes: 5 additions & 9 deletions app/api/cart/items/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { withAuth } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { updateCartItemBodySchema } from "@/lib/validation/schemas/cart";

export const DELETE = withAuth(async (_request, context, user) => {
try {
Expand Down Expand Up @@ -45,15 +47,9 @@ export const DELETE = withAuth(async (_request, context, user) => {
export const PATCH = withAuth(async (request: NextRequest, context, user) => {
try {
const { id } = await context.params;
const body = await request.json();
const { quantity } = body;

if (!quantity || quantity < 1) {
return NextResponse.json(
{ error: "Valid quantity is required" },
{ status: 400 }
);
}
const parsed = await parseBody(request, updateCartItemBodySchema);
if (!parsed.success) return parsed.response;
const { quantity } = parsed.data;

const cartItem = await prisma.cartItem.findUnique({
where: { id },
Expand Down
21 changes: 5 additions & 16 deletions app/api/coupon/apply/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,14 @@ import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { withAuth } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { applyCouponBodySchema } from "@/lib/validation/schemas/coupons";

export const POST = withAuth(async (request: NextRequest, _context, _user) => {
try {
const body = await request.json();
const { code, cartTotal } = body;

if (!code || typeof code !== "string") {
return NextResponse.json(
{ error: "Coupon code is required" },
{ status: 400 }
);
}

if (typeof cartTotal !== "number" || cartTotal <= 0) {
return NextResponse.json(
{ error: "Valid cart total is required" },
{ status: 400 }
);
}
const parsed = await parseBody(request, applyCouponBodySchema);
if (!parsed.success) return parsed.response;
const { code, cartTotal } = parsed.data;

const coupon = await prisma.coupon.findUnique({
where: { code: code.toUpperCase() },
Expand Down
10 changes: 5 additions & 5 deletions app/api/documents/share/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { decryptShareToken } from "@/lib/share-crypto";
import { parseQuery } from "@/lib/validation";
import { shareTokenQuerySchema } from "@/lib/validation/schemas/documents";

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const token = searchParams.get("token");

if (!token || !/^[0-9a-f]+$/i.test(token) || token.length < 64) {
return NextResponse.json({ error: "Missing share token" }, { status: 400 });
}
const parsed = parseQuery(searchParams, shareTokenQuerySchema);
if (!parsed.success) return parsed.response;
const { token } = parsed.data;

let resourcePath: string;
try {
Expand Down
10 changes: 7 additions & 3 deletions app/api/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { NextRequest, NextResponse } from "next/server";
import { readFile, readdir, stat } from "fs/promises";
import { join, extname } from "path";
import { logger } from "@/lib/logger";
import { parseQuery } from "@/lib/validation";
import { filesQuerySchema } from "@/lib/validation/schemas/files";

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const file = searchParams.get("file");
const listDir = searchParams.get("list");
const dirPath = searchParams.get("path") || "";
const parsed = parseQuery(searchParams, filesQuerySchema);
if (!parsed.success) return parsed.response;
const file = parsed.data.file ?? null;
const listDir = parsed.data.list ?? null;
const dirPath = parsed.data.path ?? "";

const baseDir = join(process.cwd(), "documents");

Expand Down
13 changes: 5 additions & 8 deletions app/api/flags/verify/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { verifyFlagBodySchema } from "@/lib/validation/schemas/flags";

export async function POST(request: Request) {
try {
const { flag } = await request.json();

if (!flag || typeof flag !== "string") {
return NextResponse.json(
{ error: "Flag is required", valid: false },
{ status: 400 }
);
}
const parsed = await parseBody(request, verifyFlagBodySchema);
if (!parsed.success) return parsed.response;
const { flag } = parsed.data;

const matchedFlag = await prisma.flag.findFirst({
where: {
Expand Down
14 changes: 5 additions & 9 deletions app/api/gift-cards/redeem/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { withAuth } from "@/lib/server-auth";
import { logger } from "@/lib/logger";
import { parseBody } from "@/lib/validation";
import { redeemGiftCardBodySchema } from "@/lib/validation/schemas/gift-cards";

const SEEDED_GIFT_CARD_ID = "gc-seeded-001";
const INSECURE_RANDOMNESS_FLAG = "OSS{1ns3cur3_r4nd0mn3ss_g1ft_c4rd}";
Expand All @@ -21,15 +23,9 @@ function timingSafeEqualStrings(a: string, b: string): boolean {

export const POST = withAuth(async (request: NextRequest, _context, user) => {
try {
const body = await request.json();
const rawCode = body?.code;

if (!rawCode || typeof rawCode !== "string") {
return NextResponse.json(
{ error: INVALID_CODE_MESSAGE },
{ status: 400 }
);
}
const parsed = await parseBody(request, redeemGiftCardBodySchema);
if (!parsed.success) return parsed.response;
const { code: rawCode } = parsed.data;

const normalized = rawCode.trim().toUpperCase();

Expand Down
Loading
Loading