Spaces:
Sleeping
Sleeping
feat: add user authentication checks and improve navigation with logout functionality
Browse files- web/app/api/analyze/route.ts +33 -1
- web/app/api/chat/route.ts +8 -0
- web/app/api/compare/route.ts +13 -1
- web/app/api/parse-upload/route.ts +20 -2
- web/app/auth/callback/route.ts +6 -1
- web/app/auth/login/page.tsx +3 -3
- web/app/auth/signup/page.tsx +2 -2
- web/components/nav.tsx +27 -4
web/app/api/analyze/route.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
| 2 |
|
| 3 |
const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
|
| 4 |
|
| 5 |
export async function POST(req: NextRequest) {
|
| 6 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
const body = await req.json();
|
| 8 |
-
|
| 9 |
|
| 10 |
if (!text || typeof text !== "string" || text.trim().length < 50) {
|
| 11 |
return NextResponse.json(
|
|
@@ -13,6 +21,30 @@ export async function POST(req: NextRequest) {
|
|
| 13 |
{ status: 400 }
|
| 14 |
);
|
| 15 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
// Step 1: Submit to Gradio Space
|
| 18 |
const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/_analysis_and_index`, {
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createClient } from "@/lib/supabase/server";
|
| 3 |
|
| 4 |
const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
|
| 5 |
|
| 6 |
export async function POST(req: NextRequest) {
|
| 7 |
try {
|
| 8 |
+
const supabase = await createClient();
|
| 9 |
+
const { data: { user } } = await supabase.auth.getUser();
|
| 10 |
+
|
| 11 |
+
if (!user) {
|
| 12 |
+
return NextResponse.json({ error: "Unauthorized. Please log in to analyze texts." }, { status: 401 });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
const body = await req.json();
|
| 16 |
+
let { text } = body;
|
| 17 |
|
| 18 |
if (!text || typeof text !== "string" || text.trim().length < 50) {
|
| 19 |
return NextResponse.json(
|
|
|
|
| 21 |
{ status: 400 }
|
| 22 |
);
|
| 23 |
}
|
| 24 |
+
|
| 25 |
+
// Check scan limits
|
| 26 |
+
const { data: profile } = await supabase
|
| 27 |
+
.from("profiles")
|
| 28 |
+
.select("plan, role")
|
| 29 |
+
.eq("id", user.id)
|
| 30 |
+
.single();
|
| 31 |
+
|
| 32 |
+
const isAdmin = profile?.role === "admin";
|
| 33 |
+
const plan = profile?.plan || "free";
|
| 34 |
+
|
| 35 |
+
const { count: scanCount } = await supabase
|
| 36 |
+
.from("analysis_history")
|
| 37 |
+
.select("*", { count: "exact", head: true })
|
| 38 |
+
.gte("created_at", new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString())
|
| 39 |
+
.eq("user_id", user.id);
|
| 40 |
+
|
| 41 |
+
const limit = isAdmin ? 999999 : plan === "free" ? 10 : 999999;
|
| 42 |
+
if ((scanCount ?? 0) >= limit) {
|
| 43 |
+
return NextResponse.json({ error: "Monthly scan limit reached. Please upgrade to premium." }, { status: 403 });
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Sanitize basic HTML tags if any to prevent XSS down the line
|
| 47 |
+
text = text.replace(/</g, "<").replace(/>/g, ">");
|
| 48 |
|
| 49 |
// Step 1: Submit to Gradio Space
|
| 50 |
const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/_analysis_and_index`, {
|
web/app/api/chat/route.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
| 2 |
|
| 3 |
const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
|
| 4 |
|
| 5 |
export async function POST(req: NextRequest) {
|
| 6 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
const body = await req.json();
|
| 8 |
const { message, history } = body;
|
| 9 |
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createClient } from "@/lib/supabase/server";
|
| 3 |
|
| 4 |
const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
|
| 5 |
|
| 6 |
export async function POST(req: NextRequest) {
|
| 7 |
try {
|
| 8 |
+
const supabase = await createClient();
|
| 9 |
+
const { data: { user } } = await supabase.auth.getUser();
|
| 10 |
+
|
| 11 |
+
if (!user) {
|
| 12 |
+
return NextResponse.json({ error: "Unauthorized. Please log in." }, { status: 401 });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
const body = await req.json();
|
| 16 |
const { message, history } = body;
|
| 17 |
|
web/app/api/compare/route.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
| 2 |
|
| 3 |
const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
|
| 4 |
|
| 5 |
export async function POST(req: NextRequest) {
|
| 6 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
const body = await req.json();
|
| 8 |
-
|
| 9 |
|
| 10 |
if (!text_a || !text_b || text_a.trim().length < 50 || text_b.trim().length < 50) {
|
| 11 |
return NextResponse.json(
|
|
@@ -13,6 +21,10 @@ export async function POST(req: NextRequest) {
|
|
| 13 |
{ status: 400 }
|
| 14 |
);
|
| 15 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
// Call Gradio Space API
|
| 18 |
const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/run_comparison`, {
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createClient } from "@/lib/supabase/server";
|
| 3 |
|
| 4 |
const GRADIO_URL = process.env.CLAUSEGUARD_GRADIO_URL || "https://gaurv007-clauseguard.hf.space";
|
| 5 |
|
| 6 |
export async function POST(req: NextRequest) {
|
| 7 |
try {
|
| 8 |
+
const supabase = await createClient();
|
| 9 |
+
const { data: { user } } = await supabase.auth.getUser();
|
| 10 |
+
|
| 11 |
+
if (!user) {
|
| 12 |
+
return NextResponse.json({ error: "Unauthorized. Please log in." }, { status: 401 });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
const body = await req.json();
|
| 16 |
+
let { text_a, text_b } = body;
|
| 17 |
|
| 18 |
if (!text_a || !text_b || text_a.trim().length < 50 || text_b.trim().length < 50) {
|
| 19 |
return NextResponse.json(
|
|
|
|
| 21 |
{ status: 400 }
|
| 22 |
);
|
| 23 |
}
|
| 24 |
+
|
| 25 |
+
// Sanitize basically
|
| 26 |
+
text_a = text_a.replace(/</g, "<").replace(/>/g, ">");
|
| 27 |
+
text_b = text_b.replace(/</g, "<").replace(/>/g, ">");
|
| 28 |
|
| 29 |
// Call Gradio Space API
|
| 30 |
const submitRes = await fetch(`${GRADIO_URL}/gradio_api/call/run_comparison`, {
|
web/app/api/parse-upload/route.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
| 2 |
|
| 3 |
export const runtime = "nodejs";
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
export async function POST(req: NextRequest) {
|
| 6 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
const formData = await req.formData();
|
| 8 |
const file = formData.get("file") as File | null;
|
| 9 |
|
|
@@ -11,13 +22,20 @@ export async function POST(req: NextRequest) {
|
|
| 11 |
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
| 12 |
}
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
const name = file.name.toLowerCase();
|
| 15 |
const buffer = Buffer.from(await file.arrayBuffer());
|
| 16 |
let text = "";
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
text = new TextDecoder().decode(buffer);
|
| 20 |
-
} else if (name.endsWith(".pdf")) {
|
| 21 |
// pdf-parse v2
|
| 22 |
await import("pdf-parse/worker");
|
| 23 |
const { PDFParse } = await import("pdf-parse");
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createClient } from "@/lib/supabase/server";
|
| 3 |
|
| 4 |
export const runtime = "nodejs";
|
| 5 |
|
| 6 |
+
// Add a 5MB size limit
|
| 7 |
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
| 8 |
+
|
| 9 |
export async function POST(req: NextRequest) {
|
| 10 |
try {
|
| 11 |
+
const supabase = await createClient();
|
| 12 |
+
const { data: { user } } = await supabase.auth.getUser();
|
| 13 |
+
|
| 14 |
+
if (!user) {
|
| 15 |
+
return NextResponse.json({ error: "Unauthorized. Please log in." }, { status: 401 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
const formData = await req.formData();
|
| 19 |
const file = formData.get("file") as File | null;
|
| 20 |
|
|
|
|
| 22 |
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
|
| 23 |
}
|
| 24 |
|
| 25 |
+
if (file.size > MAX_FILE_SIZE) {
|
| 26 |
+
return NextResponse.json({ error: "File exceeds 5MB size limit" }, { status: 400 });
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
const name = file.name.toLowerCase();
|
| 30 |
const buffer = Buffer.from(await file.arrayBuffer());
|
| 31 |
let text = "";
|
| 32 |
|
| 33 |
+
// Validate MIME types alongside extension
|
| 34 |
+
const mimeType = file.type;
|
| 35 |
+
|
| 36 |
+
if ((name.endsWith(".txt") || name.endsWith(".md")) && (mimeType.includes("text/plain") || mimeType.includes("text/markdown"))) {
|
| 37 |
text = new TextDecoder().decode(buffer);
|
| 38 |
+
} else if (name.endsWith(".pdf") && mimeType === "application/pdf") {
|
| 39 |
// pdf-parse v2
|
| 40 |
await import("pdf-parse/worker");
|
| 41 |
const { PDFParse } = await import("pdf-parse");
|
web/app/auth/callback/route.ts
CHANGED
|
@@ -4,9 +4,14 @@ import { NextResponse } from "next/server";
|
|
| 4 |
export async function GET(request: Request) {
|
| 5 |
const requestUrl = new URL(request.url);
|
| 6 |
const code = requestUrl.searchParams.get("code");
|
| 7 |
-
|
| 8 |
const origin = requestUrl.origin;
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
if (code) {
|
| 11 |
const supabase = await createClient();
|
| 12 |
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
|
|
| 4 |
export async function GET(request: Request) {
|
| 5 |
const requestUrl = new URL(request.url);
|
| 6 |
const code = requestUrl.searchParams.get("code");
|
| 7 |
+
let next = requestUrl.searchParams.get("next") || "/dashboard-pages/dashboard";
|
| 8 |
const origin = requestUrl.origin;
|
| 9 |
|
| 10 |
+
// Prevent open redirect
|
| 11 |
+
if (next && !next.startsWith("/")) {
|
| 12 |
+
next = "/dashboard-pages/dashboard";
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
if (code) {
|
| 16 |
const supabase = await createClient();
|
| 17 |
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
web/app/auth/login/page.tsx
CHANGED
|
@@ -21,16 +21,16 @@ function LoginForm() {
|
|
| 21 |
// Check if already logged in — redirect immediately
|
| 22 |
useEffect(() => {
|
| 23 |
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 24 |
-
if (user) {
|
| 25 |
else { setChecking(false); }
|
| 26 |
});
|
| 27 |
-
}, [next, supabase.auth]);
|
| 28 |
|
| 29 |
async function handleLogin(e: React.FormEvent) {
|
| 30 |
e.preventDefault(); setLoading(true); setError("");
|
| 31 |
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
| 32 |
if (error) { setError(error.message); setLoading(false); }
|
| 33 |
-
else {
|
| 34 |
}
|
| 35 |
|
| 36 |
async function handleMagicLink() {
|
|
|
|
| 21 |
// Check if already logged in — redirect immediately
|
| 22 |
useEffect(() => {
|
| 23 |
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 24 |
+
if (user) { router.push(next); }
|
| 25 |
else { setChecking(false); }
|
| 26 |
});
|
| 27 |
+
}, [next, supabase.auth, router]);
|
| 28 |
|
| 29 |
async function handleLogin(e: React.FormEvent) {
|
| 30 |
e.preventDefault(); setLoading(true); setError("");
|
| 31 |
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
| 32 |
if (error) { setError(error.message); setLoading(false); }
|
| 33 |
+
else { router.push(next); }
|
| 34 |
}
|
| 35 |
|
| 36 |
async function handleMagicLink() {
|
web/app/auth/signup/page.tsx
CHANGED
|
@@ -18,10 +18,10 @@ export default function SignupPage() {
|
|
| 18 |
// Redirect if already logged in
|
| 19 |
useEffect(() => {
|
| 20 |
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 21 |
-
if (user) {
|
| 22 |
else { setChecking(false); }
|
| 23 |
});
|
| 24 |
-
}, []);
|
| 25 |
|
| 26 |
async function handleSignup(e: React.FormEvent) {
|
| 27 |
e.preventDefault(); setLoading(true); setError("");
|
|
|
|
| 18 |
// Redirect if already logged in
|
| 19 |
useEffect(() => {
|
| 20 |
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 21 |
+
if (user) { router.push("/dashboard-pages/dashboard"); }
|
| 22 |
else { setChecking(false); }
|
| 23 |
});
|
| 24 |
+
}, [router, supabase.auth]);
|
| 25 |
|
| 26 |
async function handleSignup(e: React.FormEvent) {
|
| 27 |
e.preventDefault(); setLoading(true); setError("");
|
web/components/nav.tsx
CHANGED
|
@@ -119,6 +119,15 @@ export function Nav() {
|
|
| 119 |
}`}>
|
| 120 |
<Settings className="w-3.5 h-3.5" /> Settings
|
| 121 |
</Link>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
<Link href="/dashboard-pages/analyze"
|
| 123 |
className="ml-1 px-3 py-1.5 text-[13px] font-medium text-white bg-zinc-900 rounded-md hover:bg-zinc-800 transition-colors flex items-center gap-1.5">
|
| 124 |
<Zap className="w-3.5 h-3.5" /> New scan
|
|
@@ -188,10 +197,24 @@ export function Nav() {
|
|
| 188 |
|
| 189 |
{/* Auth actions */}
|
| 190 |
{isLoggedIn ? (
|
| 191 |
-
<
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
) : (
|
| 196 |
<>
|
| 197 |
<Link href="/auth/login" onClick={() => setOpen(false)}
|
|
|
|
| 119 |
}`}>
|
| 120 |
<Settings className="w-3.5 h-3.5" /> Settings
|
| 121 |
</Link>
|
| 122 |
+
<button onClick={async () => {
|
| 123 |
+
const supabase = createClient();
|
| 124 |
+
await supabase.auth.signOut();
|
| 125 |
+
setUserEmail(null);
|
| 126 |
+
setUserRole(null);
|
| 127 |
+
window.location.href = "/";
|
| 128 |
+
}} className="flex items-center gap-1.5 px-2.5 py-1.5 text-[13px] text-zinc-500 hover:text-zinc-900 rounded-md hover:bg-zinc-50 transition-colors">
|
| 129 |
+
Log out
|
| 130 |
+
</button>
|
| 131 |
<Link href="/dashboard-pages/analyze"
|
| 132 |
className="ml-1 px-3 py-1.5 text-[13px] font-medium text-white bg-zinc-900 rounded-md hover:bg-zinc-800 transition-colors flex items-center gap-1.5">
|
| 133 |
<Zap className="w-3.5 h-3.5" /> New scan
|
|
|
|
| 197 |
|
| 198 |
{/* Auth actions */}
|
| 199 |
{isLoggedIn ? (
|
| 200 |
+
<>
|
| 201 |
+
<Link href="/dashboard-pages/analyze" onClick={() => setOpen(false)}
|
| 202 |
+
className="block px-3 py-2.5 text-sm font-medium text-white bg-zinc-900 rounded-lg text-center hover:bg-zinc-800 mb-1">
|
| 203 |
+
New Scan
|
| 204 |
+
</Link>
|
| 205 |
+
<button
|
| 206 |
+
onClick={async () => {
|
| 207 |
+
setOpen(false);
|
| 208 |
+
const supabase = createClient();
|
| 209 |
+
await supabase.auth.signOut();
|
| 210 |
+
setUserEmail(null);
|
| 211 |
+
setUserRole(null);
|
| 212 |
+
window.location.href = "/";
|
| 213 |
+
}}
|
| 214 |
+
className="w-full text-left flex items-center gap-2.5 px-3 py-2.5 text-sm text-zinc-600 rounded-md hover:bg-zinc-50">
|
| 215 |
+
Log out
|
| 216 |
+
</button>
|
| 217 |
+
</>
|
| 218 |
) : (
|
| 219 |
<>
|
| 220 |
<Link href="/auth/login" onClick={() => setOpen(false)}
|