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
23 changes: 8 additions & 15 deletions src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { validateAuthCredentials } from "@/lib/auth/validation";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";

Expand All @@ -10,28 +11,20 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}

const { email, password } = body as {
email?: unknown;
password?: unknown;
};
if (
typeof email !== "string" ||
typeof password !== "string" ||
!email ||
!password
) {
const validation = validateAuthCredentials(body);

if (!validation.success || !validation.credentials) {
return NextResponse.json(
{ error: "Email and password are required" },
{ error: validation.error ?? "Invalid credentials" },
{ status: 400 }
);
}

const supabase = await createServerSupabaseClient();

const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
const { data, error } = await supabase.auth.signInWithPassword(
validation.credentials
);
Comment on lines 23 to +27

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure
fd -t f -e ts -e tsx "route.ts" src/app/api/ | head -20

Repository: orimcoding/swale

Length of output: 158


🏁 Script executed:

# Also check the lib structure for wrappers
fd -t f -e ts -e tsx "lib/" | head -20

Repository: orimcoding/swale

Length of output: 42


🏁 Script executed:

# Read the login route file to see what's currently there
cat -n src/app/api/auth/login/route.ts

Repository: orimcoding/swale

Length of output: 1424


🏁 Script executed:

# Search for rate limiting utilities/middleware
fd -t f -e ts -e tsx "lib/" | xargs grep -l "rate" 2>/dev/null

Repository: orimcoding/swale

Length of output: 44


🏁 Script executed:

# Search for timeout handling utilities
fd -t f -e ts -e tsx "lib/" | xargs grep -l "timeout" 2>/dev/null

Repository: orimcoding/swale

Length of output: 44


🏁 Script executed:

# Check if there's middleware in the api/auth directory
ls -la src/app/api/auth/

Repository: orimcoding/swale

Length of output: 333


🏁 Script executed:

# Search for rate limiting in the entire codebase
rg "rateLimit|rate-limit" --type ts --type tsx

Repository: orimcoding/swale

Length of output: 88


🏁 Script executed:

# Check for any middleware.ts or middleware file
fd -t f "middleware" src/

Repository: orimcoding/swale

Length of output: 78


🏁 Script executed:

# Read the middleware file to check for rate limiting
cat -n src/middleware.ts

Repository: orimcoding/swale

Length of output: 2320


🏁 Script executed:

# Search for rate limiting with correct rg syntax (no type suffix)
rg "rateLimit|Ratelimit|rate-limit" --max-count 50

Repository: orimcoding/swale

Length of output: 42


🏁 Script executed:

# Check package.json for rate limiting dependencies
cat package.json | grep -i "rate\|limit"

Repository: orimcoding/swale

Length of output: 42


🏁 Script executed:

# Also search for timeout patterns
rg "timeout|TimeoutError|AbortSignal" --max-count 50

Repository: orimcoding/swale

Length of output: 430


🏁 Script executed:

# Read the timeout utility to understand how it's implemented
cat -n src/lib/utils.ts

Repository: orimcoding/swale

Length of output: 12936


🏁 Script executed:

# Search for how the timeout utility is used in the codebase
rg "withTimeout" --max-count 50

Repository: orimcoding/swale

Length of output: 42


🏁 Script executed:

# Check package.json to see what dependencies are available
cat package.json

Repository: orimcoding/swale

Length of output: 1140


🏁 Script executed:

# Search for withTimeout usage in the codebase
rg "withTimeout" --max-count 20

Repository: orimcoding/swale

Length of output: 42


🏁 Script executed:

# Check if there are any other auth route files to see if they have rate limiting
cat -n src/app/api/auth/signup/route.ts

Repository: orimcoding/swale

Length of output: 1434


🏁 Script executed:

# Search for any rate limiting or throttle patterns in the api directory
rg "Ratelimit|RateLimit|throttle|brute" src/app/api/

Repository: orimcoding/swale

Length of output: 42


Add rate limiting and timeout guards to the login endpoint.

This credential route currently has no rate limiter and no bounded-time guard around the external auth call, which leaves brute-force and request-exhaustion paths open.

As per coding guidelines, "Check for rate limiting and timeout handling." While fetchWithTimeout utility exists in the codebase, it is not applied to the supabase.auth.signInWithPassword() call. Additionally, no rate limiting mechanism (e.g., using Upstash Redis) is implemented on this endpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/auth/login/route.ts` around lines 23 - 27, The login endpoint
lacks both rate limiting and timeout protection on the external auth call. Apply
a timeout guard around the supabase.auth.signInWithPassword() call using the
existing fetchWithTimeout utility from the codebase to prevent request
exhaustion. Additionally, implement rate limiting on this route using Upstash
Redis (or similar mechanism) to prevent brute-force attacks on the credentials
endpoint by tracking and limiting login attempts per IP address or user.

Source: Coding guidelines


if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
Expand Down
20 changes: 20 additions & 0 deletions src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function POST() {
try {
const supabase = await createServerSupabaseClient();
const { error } = await supabase.auth.signOut();

if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}

return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
53 changes: 5 additions & 48 deletions src/app/api/auth/signup/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { validateAuthCredentials } from "@/lib/auth/validation";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
try {
// Parse request body
let body: unknown;
try {
body = await request.json();
Expand All @@ -14,61 +14,18 @@ export async function POST(request: NextRequest) {
);
}

// Validate body is an object
if (!body || typeof body !== "object") {
return NextResponse.json(
{ error: "Invalid request body" },
{ status: 400 }
);
}

const { email, password } = body as Record<string, unknown>;

// Validate email and password exist and are strings
if (typeof email !== "string" || email.trim().length === 0) {
return NextResponse.json(
{ error: "Email is required and must be a string" },
{ status: 400 }
);
}

if (typeof password !== "string" || password.length === 0) {
return NextResponse.json(
{ error: "Password is required and must be a string" },
{ status: 400 }
);
}

// Validate email format - simple check, Supabase will do deeper validation
const trimmedEmail = email.trim();
if (!trimmedEmail.includes("@") || !trimmedEmail.includes(".")) {
return NextResponse.json(
{ error: "Invalid email format" },
{ status: 400 }
);
}
const [localPart, domain] = trimmedEmail.split("@");
if (!localPart || !domain || localPart.length === 0 || domain.length < 3) {
return NextResponse.json(
{ error: "Invalid email format" },
{ status: 400 }
);
}
const validation = validateAuthCredentials(body);

// Validate password length
if (password.length < 8) {
if (!validation.success || !validation.credentials) {
return NextResponse.json(
{ error: "Password must be at least 8 characters" },
{ error: validation.error ?? "Invalid credentials" },
{ status: 400 }
);
}

const supabase = await createServerSupabaseClient();

const { data, error } = await supabase.auth.signUp({
email: email.trim(),
password,
});
const { data, error } = await supabase.auth.signUp(validation.credentials);

if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
Expand Down
71 changes: 71 additions & 0 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { AuthForm } from "@/components/auth/auth-form";
import { AuthShell } from "@/components/auth/auth-shell";

export default function LoginPage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
setIsSubmitting(true);

const formData = new FormData(event.currentTarget);
const email = formData.get("email");
const password = formData.get("password");

try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});

const result = (await response.json()) as { error?: string };

if (!response.ok) {
setError(result.error ?? "Unable to sign in");
return;
}

router.push("/dashboard");
router.refresh();
} catch {
setError("Something went wrong. Please try again.");
} finally {
setIsSubmitting(false);
}
}

return (
<AuthShell
title="Welcome back."
subtitle="Sign in to open your football workspace, follow live matchdays, and pick up where you left off."
footer={
<>
New to Swale?{" "}
<Link href="/signup" className="font-medium text-blue-700 hover:text-blue-800">
Create an account
</Link>
</>
}
>
<AuthForm
mode="login"
submitLabel={isSubmitting ? "Signing in..." : "Sign in"}
helperText="Use your email and password to access your live dashboard, saved context, and player tracking workspace."
isSubmitting={isSubmitting}
error={error}
onSubmit={handleSubmit}
/>
</AuthShell>
);
}
Loading
Loading