Skip to content
Open
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
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"dependencies": {
"@google/generative-ai": "^0.21.0",
"@hookform/resolvers": "^3.9.1",
"@hookform/resolvers": "^3.10.0",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^6.1.0",
"@radix-ui/react-accordion": "^1.2.3",
Expand Down Expand Up @@ -43,7 +43,7 @@
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.2",
"zod": "^3.24.1"
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
169 changes: 143 additions & 26 deletions src/app/admin/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { loginSchema } from "@/schemas/loginSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState } from "react";
import React, { useState, useEffect, useRef } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
Expand All @@ -17,11 +17,32 @@ import { signIn } from "next-auth/react";
import { useToast } from "@/hooks/use-toast";
import { useRouter } from "next/navigation";
import { LoaderCircle } from "lucide-react";
import { TurnstileRenderOptions } from "@/types/turnstile";

// Declare Turnstile types
declare global {
interface Window {
turnstile: {
render: (element: string | HTMLElement, options: TurnstileRenderOptions) => string;
reset: (widgetId: string) => void;
remove: (widgetId: string) => void;
getResponse: (widgetId: string) => string;
isExpired: (widgetId: string) => boolean;
execute: (element: string | HTMLElement) => void;
};
onTurnstileLoad?: () => void;
}
}

export default function Login() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [turnstileToken, setTurnstileToken] = useState<string>("");
const [turnstileLoaded, setTurnstileLoaded] = useState(false);
const turnstileRef = useRef<HTMLDivElement>(null);
const widgetId = useRef<string>("");
const router = useRouter();
const {toast} = useToast();
const { toast } = useToast();

const form = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
defaultValues: {
Expand All @@ -30,34 +51,123 @@ export default function Login() {
},
});

// Load Turnstile script
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
script.async = true;
script.defer = true;

window.onTurnstileLoad = () => {
setTurnstileLoaded(true);
};

script.onload = () => {
if (window.turnstile) {
setTurnstileLoaded(true);
}
};

document.head.appendChild(script);

return () => {
document.head.removeChild(script);
if (widgetId.current && window.turnstile) {
window.turnstile.remove(widgetId.current);
}
};
}, []);

// Render Turnstile widget when loaded
useEffect(() => {
if (turnstileLoaded && turnstileRef.current && window.turnstile) {
widgetId.current = window.turnstile.render(turnstileRef.current, {
sitekey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "",
callback: (token: string) => {
setTurnstileToken(token);
},
'error-callback': () => {
setTurnstileToken("");
toast({
title: "Captcha Error",
description: "Please try again",
variant: "destructive",
});
},
'expired-callback': () => {
setTurnstileToken("");
},
theme: 'dark', // matches your dark theme
size: 'normal',
});
}
}, [turnstileLoaded, toast]);

const onSubmit = async (data: z.infer<typeof loginSchema>) => {
if (!turnstileToken) {
toast({
title: "Captcha Required",
description: "Please complete the captcha verification",
variant: "destructive",
});
return;
}

setIsSubmitting(true);
const result = await signIn("credentials", {
redirect: false,
email: data.email,
password: data.password,
});
console.log(result);
if (result?.error) {
if (result.error === "CredentialsSignin") {
toast({
title: "Login Failed",
description: "Incorrect username or password",
variant: "destructive",
});
} else {
toast({
title: "Error",
description: result.error,
variant: "destructive",
});

try {
const result = await signIn("credentials", {
redirect: false,
email: data.email,
password: data.password,
turnstileToken, // Pass the turnstile token
});

console.log(result);

if (result?.error) {
// Reset captcha on error
if (widgetId.current && window.turnstile) {
window.turnstile.reset(widgetId.current);
setTurnstileToken("");
}

if (result.error === "CredentialsSignin") {
toast({
title: "Login Failed",
description: "Incorrect username or password",
variant: "destructive",
});
} else if (result.error === "TurnstileError") {
toast({
title: "Captcha Verification Failed",
description: "Please complete the captcha again",
variant: "destructive",
});
} else {
toast({
title: "Error",
description: result.error,
variant: "destructive",
});
}
}
}
setIsSubmitting(false);
if (result?.url) {
router.replace('/admin/dashboard');

if (result?.url) {
router.replace('/admin/dashboard');
}
} catch (error) {
console.error("Login error:", error);
toast({
title: "Error",
description: "An unexpected error occurred",
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
};

return (
<div className="flex justify-center items-center w-full min-h-screen max-md:px-5">
<div className="flex justify-center items-center flex-col w-96 bg-white/5 p-8 rounded-xl border border-white/10">
Expand Down Expand Up @@ -93,9 +203,16 @@ export default function Login() {
</FormItem>
)}
/>

{/* Turnstile Widget */}
<div className="flex justify-center">
<div ref={turnstileRef} />
</div>

<Button
type="submit"
className="w-full h-10 bg-blue-500 hover:bg-blue-600"
disabled={!turnstileToken || isSubmitting}
>
{isSubmitting ? (
<>
Expand All @@ -111,4 +228,4 @@ export default function Login() {
</div>
</div>
);
}
}
25 changes: 24 additions & 1 deletion src/app/api/analyze-image/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import { GoogleGenerativeAI } from "@google/generative-ai";
import { NextResponse } from "next/server";
import { imageAnalysisLimiter } from "@/lib/rate-limit";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

export async function POST(req:Request){
import { NextRequest } from "next/server";

export async function POST(req: Request) {
const nextReq = new NextRequest(req);

try {
const rateLimitResult = await imageAnalysisLimiter.isRateLimited(nextReq);
const rateLimitHeaders = imageAnalysisLimiter.createHeaders(rateLimitResult);

if (rateLimitResult.limited) {
console.log("Rate limit exceeded for image analysis");
return NextResponse.json(
{
success: false,
error: "Rate limit exceeded. Too many image analysis requests. Please try again later.",
retryAfter: rateLimitResult.retryAfter,
},
{
status: 429,
headers: rateLimitHeaders,
}
);
}


const {image} = await req.json() // look like this -> data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...

const base64Data = image.split(',')[1]
Expand Down
1 change: 1 addition & 0 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import { authOptions } from "@/lib/options";

Expand Down
25 changes: 24 additions & 1 deletion src/app/api/get-current-location/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import axios from "axios";
import { locationLimiter } from "@/lib/rate-limit";

export async function POST(req: Request) {
const nextReq = new NextRequest(req);

// Apply rate limiting
const rateLimitResult = await locationLimiter.isRateLimited(nextReq);
const rateLimitHeaders = locationLimiter.createHeaders(rateLimitResult);

if (rateLimitResult.limited) {
console.log("Rate limit exceeded for location requests");
return NextResponse.json(
{
success: false,
error: "Rate limit exceeded. Too many location requests. Please try again later.",
retryAfter: rateLimitResult.retryAfter,
},
{
status: 429,
headers: rateLimitHeaders,
}
);
}


const { latitude, longitude } = await req.json();

if (!latitude || !longitude) {
Expand Down
Loading