Spaces:
Sleeping
Sleeping
Merge branch 'main' of https://huggingface.co/spaces/gaurv007/ClauseGuard
Browse files- web/app/api/admin/route.ts +12 -4
- web/app/api/me/route.ts +59 -0
- web/app/dashboard-pages/analyze/page.tsx +26 -9
- web/components/nav.tsx +14 -5
- web/lib/admin-guard.ts +1 -8
- web/lib/supabase/schema.sql +8 -4
web/app/api/admin/route.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
import { createClient } from "@/lib/supabase/server";
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
async function checkAdmin() {
|
| 7 |
const supabase = await createClient();
|
| 8 |
const { data: { user } } = await supabase.auth.getUser();
|
| 9 |
-
if (!user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
return { supabase: null, user: null, error: true };
|
| 11 |
}
|
|
|
|
| 12 |
return { supabase, user, error: false };
|
| 13 |
}
|
| 14 |
|
|
@@ -30,7 +39,6 @@ export async function GET(req: NextRequest) {
|
|
| 30 |
const { count: totalApiKeys } = await supabase.from("api_keys").select("id", { count: "exact", head: true }).eq("is_active", true);
|
| 31 |
const { count: bannedUsers } = await supabase.from("profiles").select("id", { count: "exact", head: true }).eq("is_banned", true);
|
| 32 |
|
| 33 |
-
// Scans today
|
| 34 |
const today = new Date();
|
| 35 |
today.setHours(0, 0, 0, 0);
|
| 36 |
const { count: scansToday } = await supabase.from("analyses").select("id", { count: "exact", head: true }).gte("created_at", today.toISOString());
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
import { createClient } from "@/lib/supabase/server";
|
| 3 |
|
| 4 |
+
// No hardcoded emails β admin access is determined by profiles.role in the database
|
|
|
|
| 5 |
async function checkAdmin() {
|
| 6 |
const supabase = await createClient();
|
| 7 |
const { data: { user } } = await supabase.auth.getUser();
|
| 8 |
+
if (!user) return { supabase: null, user: null, error: true };
|
| 9 |
+
|
| 10 |
+
// Check role from database
|
| 11 |
+
const { data: profile } = await supabase
|
| 12 |
+
.from("profiles")
|
| 13 |
+
.select("role")
|
| 14 |
+
.eq("id", user.id)
|
| 15 |
+
.single();
|
| 16 |
+
|
| 17 |
+
if (profile?.role !== "admin") {
|
| 18 |
return { supabase: null, user: null, error: true };
|
| 19 |
}
|
| 20 |
+
|
| 21 |
return { supabase, user, error: false };
|
| 22 |
}
|
| 23 |
|
|
|
|
| 39 |
const { count: totalApiKeys } = await supabase.from("api_keys").select("id", { count: "exact", head: true }).eq("is_active", true);
|
| 40 |
const { count: bannedUsers } = await supabase.from("profiles").select("id", { count: "exact", head: true }).eq("is_banned", true);
|
| 41 |
|
|
|
|
| 42 |
const today = new Date();
|
| 43 |
today.setHours(0, 0, 0, 0);
|
| 44 |
const { count: scansToday } = await supabase.from("analyses").select("id", { count: "exact", head: true }).gte("created_at", today.toISOString());
|
web/app/api/me/route.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createClient } from "@/lib/supabase/server";
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* GET /api/me
|
| 6 |
+
* Returns the current user's profile from DB.
|
| 7 |
+
* Used by client components (analyze page, etc.) to determine plan, role, usage.
|
| 8 |
+
* No hardcoded emails β everything comes from the database.
|
| 9 |
+
*/
|
| 10 |
+
export async function GET(req: NextRequest) {
|
| 11 |
+
try {
|
| 12 |
+
const supabase = await createClient();
|
| 13 |
+
const { data: { user } } = await supabase.auth.getUser();
|
| 14 |
+
|
| 15 |
+
if (!user) {
|
| 16 |
+
return NextResponse.json({
|
| 17 |
+
authenticated: false,
|
| 18 |
+
plan: "free",
|
| 19 |
+
role: "user",
|
| 20 |
+
isAdmin: false,
|
| 21 |
+
analyses_this_month: 0,
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const { data: profile } = await supabase
|
| 26 |
+
.from("profiles")
|
| 27 |
+
.select("plan, role, is_banned, analyses_this_month, full_name, email")
|
| 28 |
+
.eq("id", user.id)
|
| 29 |
+
.single();
|
| 30 |
+
|
| 31 |
+
const plan = profile?.plan || "free";
|
| 32 |
+
const role = profile?.role || "user";
|
| 33 |
+
|
| 34 |
+
return NextResponse.json({
|
| 35 |
+
authenticated: true,
|
| 36 |
+
id: user.id,
|
| 37 |
+
email: profile?.email || user.email,
|
| 38 |
+
full_name: profile?.full_name || "",
|
| 39 |
+
plan,
|
| 40 |
+
role,
|
| 41 |
+
isAdmin: role === "admin",
|
| 42 |
+
is_banned: profile?.is_banned || false,
|
| 43 |
+
analyses_this_month: profile?.analyses_this_month || 0,
|
| 44 |
+
// Admins get unlimited everything
|
| 45 |
+
scan_limit: role === "admin" ? Infinity : plan === "free" ? 10 : Infinity,
|
| 46 |
+
can_upload: role === "admin" || plan !== "free",
|
| 47 |
+
can_compare: role === "admin" || plan !== "free",
|
| 48 |
+
can_export_pdf: role === "admin" || plan !== "free",
|
| 49 |
+
});
|
| 50 |
+
} catch (error) {
|
| 51 |
+
return NextResponse.json({
|
| 52 |
+
authenticated: false,
|
| 53 |
+
plan: "free",
|
| 54 |
+
role: "user",
|
| 55 |
+
isAdmin: false,
|
| 56 |
+
analyses_this_month: 0,
|
| 57 |
+
});
|
| 58 |
+
}
|
| 59 |
+
}
|
web/app/dashboard-pages/analyze/page.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import { useState, useRef } from "react";
|
| 4 |
import {
|
| 5 |
ScanText, ScanLine, TriangleAlert, CircleAlert, CircleCheck, Info,
|
| 6 |
FileDown, ChevronDown, ChevronUp, Copy, Check, Upload, FileText,
|
|
@@ -165,11 +165,28 @@ export default function AnalyzePage() {
|
|
| 165 |
const [copied, setCopied] = useState(false);
|
| 166 |
const [scanCount, setScanCount] = useState(0);
|
| 167 |
const [userPlan, setUserPlan] = useState("free");
|
|
|
|
|
|
|
|
|
|
| 168 |
const [showUpgrade, setShowUpgrade] = useState(false);
|
| 169 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
async function handleAnalyze() {
|
| 175 |
if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
|
|
@@ -187,7 +204,7 @@ export default function AnalyzePage() {
|
|
| 187 |
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
| 188 |
const file = e.target.files?.[0];
|
| 189 |
if (!file) return;
|
| 190 |
-
if (
|
| 191 |
setLoading(true); setError("");
|
| 192 |
try {
|
| 193 |
const formData = new FormData(); formData.append("file", file);
|
|
@@ -255,10 +272,10 @@ export default function AnalyzePage() {
|
|
| 255 |
<div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center"><Lock className="w-5 h-5 text-amber-600" /></div>
|
| 256 |
<button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
|
| 257 |
</div>
|
| 258 |
-
<h3 className="mt-4 text-lg font-semibold">{
|
| 259 |
<p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
|
| 260 |
-
{
|
| 261 |
-
?
|
| 262 |
: "File upload is available on the Pro plan."}
|
| 263 |
</p>
|
| 264 |
<div className="mt-5 flex gap-2">
|
|
@@ -279,8 +296,8 @@ export default function AnalyzePage() {
|
|
| 279 |
</h1>
|
| 280 |
<p className="mt-1 text-xs sm:text-sm text-zinc-500 max-w-xl">Paste text or upload a file. Get 41-category clause detection, risk scoring, ML NER, NLI contradictions, compliance checks, and obligation tracking.</p>
|
| 281 |
</div>
|
| 282 |
-
{userPlan === "free" && (
|
| 283 |
-
<span className="self-start text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md whitespace-nowrap">{scanCount}/{
|
| 284 |
)}
|
| 285 |
</div>
|
| 286 |
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { useState, useRef, useEffect } from "react";
|
| 4 |
import {
|
| 5 |
ScanText, ScanLine, TriangleAlert, CircleAlert, CircleCheck, Info,
|
| 6 |
FileDown, ChevronDown, ChevronUp, Copy, Check, Upload, FileText,
|
|
|
|
| 165 |
const [copied, setCopied] = useState(false);
|
| 166 |
const [scanCount, setScanCount] = useState(0);
|
| 167 |
const [userPlan, setUserPlan] = useState("free");
|
| 168 |
+
const [userRole, setUserRole] = useState("user");
|
| 169 |
+
const [scanLimit, setScanLimit] = useState(10);
|
| 170 |
+
const [canUpload, setCanUpload] = useState(false);
|
| 171 |
const [showUpgrade, setShowUpgrade] = useState(false);
|
| 172 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 173 |
|
| 174 |
+
// Fetch user profile from DB on mount β no hardcoded emails or plans
|
| 175 |
+
useEffect(() => {
|
| 176 |
+
fetch("/api/me")
|
| 177 |
+
.then(res => res.json())
|
| 178 |
+
.then(data => {
|
| 179 |
+
setUserPlan(data.plan || "free");
|
| 180 |
+
setUserRole(data.role || "user");
|
| 181 |
+
setScanCount(data.analyses_this_month || 0);
|
| 182 |
+
setScanLimit(data.scan_limit === Infinity || data.scan_limit > 9999 ? Infinity : (data.scan_limit || 10));
|
| 183 |
+
setCanUpload(data.can_upload || false);
|
| 184 |
+
})
|
| 185 |
+
.catch(() => {});
|
| 186 |
+
}, []);
|
| 187 |
+
|
| 188 |
+
const isAdmin = userRole === "admin";
|
| 189 |
+
const canScan = isAdmin || userPlan !== "free" || scanCount < scanLimit;
|
| 190 |
|
| 191 |
async function handleAnalyze() {
|
| 192 |
if (!text || text.trim().length < 50) { setError("Enter at least 50 characters."); return; }
|
|
|
|
| 204 |
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
| 205 |
const file = e.target.files?.[0];
|
| 206 |
if (!file) return;
|
| 207 |
+
if (!canUpload) { setShowUpgrade(true); return; }
|
| 208 |
setLoading(true); setError("");
|
| 209 |
try {
|
| 210 |
const formData = new FormData(); formData.append("file", file);
|
|
|
|
| 272 |
<div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center"><Lock className="w-5 h-5 text-amber-600" /></div>
|
| 273 |
<button onClick={() => setShowUpgrade(false)} className="p-1 hover:bg-zinc-100 rounded-md"><X className="w-4 h-4 text-zinc-400" /></button>
|
| 274 |
</div>
|
| 275 |
+
<h3 className="mt-4 text-lg font-semibold">{scanCount >= scanLimit ? "Scan limit reached" : "Pro feature"}</h3>
|
| 276 |
<p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
|
| 277 |
+
{scanCount >= scanLimit
|
| 278 |
+
? "You have used all your free scans. Upgrade to Pro for unlimited scans and full analysis."
|
| 279 |
: "File upload is available on the Pro plan."}
|
| 280 |
</p>
|
| 281 |
<div className="mt-5 flex gap-2">
|
|
|
|
| 296 |
</h1>
|
| 297 |
<p className="mt-1 text-xs sm:text-sm text-zinc-500 max-w-xl">Paste text or upload a file. Get 41-category clause detection, risk scoring, ML NER, NLI contradictions, compliance checks, and obligation tracking.</p>
|
| 298 |
</div>
|
| 299 |
+
{userPlan === "free" && !isAdmin && (
|
| 300 |
+
<span className="self-start text-xs text-zinc-400 border border-zinc-200 px-2.5 py-1 rounded-md whitespace-nowrap">{scanCount}/{scanLimit === Infinity ? "\u221E" : scanLimit} scans</span>
|
| 301 |
)}
|
| 302 |
</div>
|
| 303 |
|
web/components/nav.tsx
CHANGED
|
@@ -13,19 +13,28 @@ const links = [
|
|
| 13 |
{ href: "/dashboard-pages/compare", label: "Compare", icon: GitCompare },
|
| 14 |
];
|
| 15 |
|
| 16 |
-
const ADMIN_EMAILS = ["ankygaur9972@gmail.com"];
|
| 17 |
-
|
| 18 |
export function Nav() {
|
| 19 |
const [open, setOpen] = useState(false);
|
| 20 |
const [userEmail, setUserEmail] = useState<string | null>(null);
|
|
|
|
| 21 |
const pathname = usePathname();
|
| 22 |
const isDashboard = pathname?.startsWith("/dashboard");
|
| 23 |
-
const isAdmin =
|
| 24 |
|
| 25 |
useEffect(() => {
|
| 26 |
const supabase = createClient();
|
| 27 |
-
supabase.auth.getUser().then(({ data }) => {
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
});
|
| 30 |
}, []);
|
| 31 |
|
|
|
|
| 13 |
{ href: "/dashboard-pages/compare", label: "Compare", icon: GitCompare },
|
| 14 |
];
|
| 15 |
|
|
|
|
|
|
|
| 16 |
export function Nav() {
|
| 17 |
const [open, setOpen] = useState(false);
|
| 18 |
const [userEmail, setUserEmail] = useState<string | null>(null);
|
| 19 |
+
const [userRole, setUserRole] = useState<string | null>(null);
|
| 20 |
const pathname = usePathname();
|
| 21 |
const isDashboard = pathname?.startsWith("/dashboard");
|
| 22 |
+
const isAdmin = userRole === "admin";
|
| 23 |
|
| 24 |
useEffect(() => {
|
| 25 |
const supabase = createClient();
|
| 26 |
+
supabase.auth.getUser().then(async ({ data }) => {
|
| 27 |
+
const user = data.user;
|
| 28 |
+
setUserEmail(user?.email || null);
|
| 29 |
+
if (user) {
|
| 30 |
+
// Fetch role from database β no hardcoded emails
|
| 31 |
+
const { data: profile } = await supabase
|
| 32 |
+
.from("profiles")
|
| 33 |
+
.select("role")
|
| 34 |
+
.eq("id", user.id)
|
| 35 |
+
.single();
|
| 36 |
+
setUserRole(profile?.role || "user");
|
| 37 |
+
}
|
| 38 |
});
|
| 39 |
}, []);
|
| 40 |
|
web/lib/admin-guard.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
| 1 |
import { createClient } from "@/lib/supabase/server";
|
| 2 |
import { redirect } from "next/navigation";
|
| 3 |
|
| 4 |
-
const ADMIN_EMAILS = ["ankygaur9972@gmail.com"];
|
| 5 |
-
|
| 6 |
export async function requireAdmin() {
|
| 7 |
const supabase = await createClient();
|
| 8 |
const { data: { user } } = await supabase.auth.getUser();
|
| 9 |
|
| 10 |
if (!user) redirect("/auth/login");
|
| 11 |
|
| 12 |
-
// Check
|
| 13 |
-
if (!ADMIN_EMAILS.includes(user.email || "")) {
|
| 14 |
-
redirect("/dashboard-pages/dashboard");
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
// Double check role in DB
|
| 18 |
const { data: profile } = await supabase
|
| 19 |
.from("profiles")
|
| 20 |
.select("role")
|
|
|
|
| 1 |
import { createClient } from "@/lib/supabase/server";
|
| 2 |
import { redirect } from "next/navigation";
|
| 3 |
|
|
|
|
|
|
|
| 4 |
export async function requireAdmin() {
|
| 5 |
const supabase = await createClient();
|
| 6 |
const { data: { user } } = await supabase.auth.getUser();
|
| 7 |
|
| 8 |
if (!user) redirect("/auth/login");
|
| 9 |
|
| 10 |
+
// Check role from database β no hardcoded emails
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
const { data: profile } = await supabase
|
| 12 |
.from("profiles")
|
| 13 |
.select("role")
|
web/lib/supabase/schema.sql
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
-- ClauseGuard β Full Database Schema
|
| 2 |
-- Tables ordered by dependency (no forward references)
|
| 3 |
|
| 4 |
-- βββ 1. Teams (no dependencies) βββ
|
|
@@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS public.team_invites (
|
|
| 42 |
UNIQUE(team_id, email)
|
| 43 |
);
|
| 44 |
|
| 45 |
-
-- βββ 4. Analyses (depends on profiles, teams)
|
| 46 |
CREATE TABLE IF NOT EXISTS public.analyses (
|
| 47 |
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 48 |
user_id UUID REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
|
|
@@ -115,6 +115,8 @@ CREATE INDEX IF NOT EXISTS idx_team_invites_email ON public.team_invites(email);
|
|
| 115 |
CREATE INDEX IF NOT EXISTS idx_custom_rules_user_id ON public.custom_rules(user_id);
|
| 116 |
CREATE INDEX IF NOT EXISTS idx_custom_rules_team_id ON public.custom_rules(team_id);
|
| 117 |
CREATE INDEX IF NOT EXISTS idx_admin_logs_created ON public.admin_logs(created_at DESC);
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-- βββ Row Level Security βββ
|
| 120 |
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
|
@@ -184,9 +186,11 @@ CREATE TRIGGER on_auth_user_created
|
|
| 184 |
AFTER INSERT ON auth.users
|
| 185 |
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
| 186 |
|
| 187 |
-
-- βββ Set admin βββ
|
| 188 |
-- Run this AFTER your first signup with your email:
|
| 189 |
-
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-- βββ Monthly reset function βββ
|
| 192 |
CREATE OR REPLACE FUNCTION public.reset_monthly_usage()
|
|
|
|
| 1 |
+
-- ClauseGuard β Full Database Schema v3.0
|
| 2 |
-- Tables ordered by dependency (no forward references)
|
| 3 |
|
| 4 |
-- βββ 1. Teams (no dependencies) βββ
|
|
|
|
| 42 |
UNIQUE(team_id, email)
|
| 43 |
);
|
| 44 |
|
| 45 |
+
-- βββ 4. Analyses (depends on profiles, teams) βββ
|
| 46 |
CREATE TABLE IF NOT EXISTS public.analyses (
|
| 47 |
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 48 |
user_id UUID REFERENCES public.profiles ON DELETE CASCADE NOT NULL,
|
|
|
|
| 115 |
CREATE INDEX IF NOT EXISTS idx_custom_rules_user_id ON public.custom_rules(user_id);
|
| 116 |
CREATE INDEX IF NOT EXISTS idx_custom_rules_team_id ON public.custom_rules(team_id);
|
| 117 |
CREATE INDEX IF NOT EXISTS idx_admin_logs_created ON public.admin_logs(created_at DESC);
|
| 118 |
+
CREATE INDEX IF NOT EXISTS idx_profiles_role ON public.profiles(role);
|
| 119 |
+
CREATE INDEX IF NOT EXISTS idx_profiles_email ON public.profiles(email);
|
| 120 |
|
| 121 |
-- βββ Row Level Security βββ
|
| 122 |
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
|
|
|
| 186 |
AFTER INSERT ON auth.users
|
| 187 |
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
| 188 |
|
| 189 |
+
-- βββ Set owner as admin with full access βββ
|
| 190 |
-- Run this AFTER your first signup with your email:
|
| 191 |
+
UPDATE public.profiles
|
| 192 |
+
SET role = 'admin', plan = 'pro'
|
| 193 |
+
WHERE email = 'ankygaur9972@gmail.com';
|
| 194 |
|
| 195 |
-- βββ Monthly reset function βββ
|
| 196 |
CREATE OR REPLACE FUNCTION public.reset_monthly_usage()
|