anky2002 commited on
Commit
970b3d5
Β·
2 Parent(s): 597ddc6adad3b7

Merge branch 'main' of https://huggingface.co/spaces/gaurv007/ClauseGuard

Browse files
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
- const ADMIN_EMAILS = ["ankygaur9972@gmail.com"];
5
-
6
  async function checkAdmin() {
7
  const supabase = await createClient();
8
  const { data: { user } } = await supabase.auth.getUser();
9
- if (!user || !ADMIN_EMAILS.includes(user.email || "")) {
 
 
 
 
 
 
 
 
 
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
- const FREE_LIMIT = 10;
172
- const canScan = userPlan !== "free" || scanCount < FREE_LIMIT;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (userPlan === "free") { setShowUpgrade(true); return; }
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">{userPlan === "free" && scanCount >= FREE_LIMIT ? "Free limit reached" : "Pro feature"}</h3>
259
  <p className="mt-1.5 text-sm text-zinc-500 leading-relaxed">
260
- {userPlan === "free" && scanCount >= FREE_LIMIT
261
- ? `You have used all ${FREE_LIMIT} free scans. Upgrade to Pro for unlimited scans and full analysis.`
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}/{FREE_LIMIT} free scans</span>
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 = userEmail && ADMIN_EMAILS.includes(userEmail);
24
 
25
  useEffect(() => {
26
  const supabase = createClient();
27
- supabase.auth.getUser().then(({ data }) => {
28
- setUserEmail(data.user?.email || null);
 
 
 
 
 
 
 
 
 
 
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 email first (fast)
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 v2.0
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) β€” v2.0 with full analysis data ───
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
- -- UPDATE public.profiles SET role = 'admin' WHERE email = 'your-email@example.com';
 
 
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()