feat: integrate real backend API with centralized API client

- Add centralized apiPost() client (src/services/api.ts) that handles
  auth injection, error handling, and token expiry uniformly
- Implement real authentication against /accounts/api/login/ and /logout/
- Rewrite partnerApi and userAppApi to use centralized client
- Connect Partners page to /partner/list/ and /partner/create/ APIs
- Add Vite proxy rules for /accounts/api and /partner endpoints
- Update AuthUser type to match full backend user response
- Move NuqsAdapter inside BrowserRouter for correct routing context

Made-with: Cursor
This commit is contained in:
Vivek P Prakash
2026-03-15 00:25:55 +05:30
parent 457004a0ef
commit 9cff4344d0
12 changed files with 470 additions and 637 deletions

3
.gitignore vendored
View File

@@ -11,6 +11,9 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
.env
.env.*
.npm-cache/
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@@ -8,7 +8,7 @@ import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
import { PageLoader } from "@/components/ui/PageLoader"; // Added import for PageLoader import { PageLoader } from "@/components/ui/PageLoader"; // Added import for PageLoader
import Login from "./pages/Login"; import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import PartnerDirectory from "./features/partners/PartnerDirectory"; import Partners from "./pages/Partners";
import PartnerProfile from "./features/partners/PartnerProfile"; import PartnerProfile from "./features/partners/PartnerProfile";
import Events from "./pages/Events"; import Events from "./pages/Events";
import Users from "./pages/Users"; import Users from "./pages/Users";
@@ -45,7 +45,7 @@ const App = () => (
path="/partners" path="/partners"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<PartnerDirectory /> <Partners />
</ProtectedRoute> </ProtectedRoute>
} }
/> />

View File

@@ -72,7 +72,7 @@ export function TopBar({ title, description }: TopBarProps) {
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="text-sm font-medium text-foreground">{getDisplayName()}</p> <p className="text-sm font-medium text-foreground">{getDisplayName()}</p>
<p className="text-xs text-muted-foreground">Admin</p> <p className="text-xs text-muted-foreground capitalize">{user?.role || 'Admin'}</p>
</div> </div>
<ChevronDown className="h-4 w-4 text-muted-foreground" /> <ChevronDown className="h-4 w-4 text-muted-foreground" />
</button> </button>

View File

@@ -59,24 +59,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setIsLoading(true); setIsLoading(true);
const response = await authLogin(username, password); const response = await authLogin(username, password);
// Store auth data
storeAuth(response); storeAuth(response);
// Set user state
const authUser: AuthUser = { const authUser: AuthUser = {
username: response.username, ...response.user,
token: response.token, token: response.token,
first_name: response.user?.first_name,
last_name: response.user?.last_name,
email: response.user?.email,
profile_photo: response.user?.profile_photo,
}; };
setUser(authUser); setUser(authUser);
toast({ toast({
title: 'Login Successful', title: 'Login Successful',
description: `Welcome back, ${username}!`, description: `Welcome back, ${response.user.first_name || username}!`,
}); });
} catch (error) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);
@@ -97,7 +90,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const logout = async () => { const logout = async () => {
try { try {
if (user) { if (user) {
await authLogout(user.username, user.token); await authLogout();
} }
} catch (error) { } catch (error) {
console.error('Logout error:', error); console.error('Logout error:', error);

View File

@@ -40,6 +40,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { subDays } from 'date-fns'; import { subDays } from 'date-fns';
import { AddPartnerSheet } from './components/AddPartnerSheet';
function RiskGauge({ score }: { score: number }) { function RiskGauge({ score }: { score: number }) {
const level = getRiskLevel(score); const level = getRiskLevel(score);
@@ -173,9 +174,11 @@ export default function PartnerDirectory() {
className="pl-9" className="pl-9"
/> />
</div> </div>
<AddPartnerSheet>
<Button className="gap-2 shrink-0"> <Button className="gap-2 shrink-0">
<Plus className="h-4 w-4" /> Add Partner <Plus className="h-4 w-4" /> Add Partner
</Button> </Button>
</AddPartnerSheet>
</div> </div>
{/* Tabs + Table */} {/* Tabs + Table */}

View File

@@ -3,7 +3,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import * as z from "zod";
import { Loader2, Upload } from "lucide-react"; import { Loader2 } from "lucide-react";
import { import {
Sheet, Sheet,
@@ -17,7 +17,6 @@ import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@@ -32,16 +31,15 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { toast } from "sonner"; import { toast } from "sonner";
import { createPartner } from "@/services/partnerApi";
const partnerFormSchema = z.object({ const partnerFormSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"), name: z.string().min(2, "Name must be at least 2 characters"),
type: z.enum(['Venue', 'Promoter', 'Sponsor', 'Vendor', 'Affiliate', 'Other']), partner_type: z.enum(['Venue', 'Promoter', 'Sponsor', 'Vendor', 'Affiliate', 'Other']),
email: z.string().email("Invalid email address"), primary_contact_person_email: z.string().email("Invalid email address"),
phone: z.string().optional(), primary_contact_person_phone: z.string().optional(),
website: z.string().url().optional().or(z.literal("")), website_url: z.string().url().optional().or(z.literal("")),
address: z.string().optional(), primary_contact_person_name: z.string().min(2, "Contact name required"),
contactName: z.string().min(2, "Contact name required"),
contactRole: z.string().optional(),
}); });
type FormValues = z.infer<typeof partnerFormSchema>; type FormValues = z.infer<typeof partnerFormSchema>;
@@ -58,27 +56,27 @@ export function AddPartnerSheet({ children }: AddPartnerSheetProps) {
resolver: zodResolver(partnerFormSchema), resolver: zodResolver(partnerFormSchema),
defaultValues: { defaultValues: {
name: "", name: "",
type: "Venue", partner_type: "Venue",
email: "", primary_contact_person_email: "",
phone: "", primary_contact_person_phone: "",
website: "", website_url: "",
address: "", primary_contact_person_name: "",
contactName: "",
contactRole: "",
}, },
}); });
async function onSubmit(data: FormValues) { async function onSubmit(data: FormValues) {
setIsSubmitting(true); setIsSubmitting(true);
// Simulate API call try {
await new Promise((resolve) => setTimeout(resolve, 1500)); await createPartner(data);
console.log("Partner created:", data);
toast.success("Partner added successfully"); toast.success("Partner added successfully");
setIsSubmitting(false);
setOpen(false); setOpen(false);
form.reset(); form.reset();
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to create partner';
toast.error(message);
} finally {
setIsSubmitting(false);
}
} }
return ( return (
@@ -113,7 +111,7 @@ export function AddPartnerSheet({ children }: AddPartnerSheetProps) {
<FormField <FormField
control={form.control} control={form.control}
name="type" name="partner_type"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Partner Type</FormLabel> <FormLabel>Partner Type</FormLabel>
@@ -142,7 +140,7 @@ export function AddPartnerSheet({ children }: AddPartnerSheetProps) {
<FormField <FormField
control={form.control} control={form.control}
name="contactName" name="primary_contact_person_name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Contact Name</FormLabel> <FormLabel>Contact Name</FormLabel>
@@ -157,7 +155,7 @@ export function AddPartnerSheet({ children }: AddPartnerSheetProps) {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
name="email" name="primary_contact_person_email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
@@ -170,7 +168,7 @@ export function AddPartnerSheet({ children }: AddPartnerSheetProps) {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="phone" name="primary_contact_person_phone"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Phone</FormLabel> <FormLabel>Phone</FormLabel>
@@ -186,7 +184,7 @@ export function AddPartnerSheet({ children }: AddPartnerSheetProps) {
<FormField <FormField
control={form.control} control={form.control}
name="website" name="website_url"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Website</FormLabel> <FormLabel>Website</FormLabel>

View File

@@ -1,129 +1,264 @@
import { Users, UserCheck, AlertTriangle, Search, Filter } from 'lucide-react'; import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Users, UserCheck, AlertTriangle, Search, Plus, Clock, MoreHorizontal, Eye, Ban, ShieldCheck, Loader2 } from 'lucide-react';
import { AppLayout } from '@/components/layout/AppLayout'; import { AppLayout } from '@/components/layout/AppLayout';
import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button';
import { mockPartners, formatCurrency } from '@/data/mockData'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { AddPartnerSheet } from '@/features/partners/components/AddPartnerSheet';
import { fetchPartners as fetchPartnersApi, Partner } from '@/services/partnerApi';
function KycBadge({ status }: { status: string }) {
const normalized = status?.toLowerCase();
if (normalized === 'approved') return (
<Badge variant="outline" className="bg-success/10 text-success border-success/20 gap-1 text-xs">
<ShieldCheck className="h-3 w-3" /> Approved
</Badge>
);
if (normalized === 'rejected') return (
<Badge variant="outline" className="bg-destructive/10 text-destructive border-destructive/20 gap-1 text-xs">
<Ban className="h-3 w-3" /> Rejected
</Badge>
);
return (
<Badge variant="outline" className="bg-warning/10 text-warning border-warning/20 gap-1 text-xs">
<Clock className="h-3 w-3" /> Pending
</Badge>
);
}
function StatusBadge({ status }: { status: string }) {
const normalized = status?.toLowerCase();
if (normalized === 'active') return (
<Badge variant="outline" className="bg-success/10 text-success border-success/20 text-xs">Active</Badge>
);
if (normalized === 'suspended') return (
<Badge variant="outline" className="bg-destructive/10 text-destructive border-destructive/20 text-xs">Suspended</Badge>
);
return (
<Badge variant="outline" className="bg-warning/10 text-warning border-warning/20 text-xs capitalize">{status}</Badge>
);
}
export default function Partners() { export default function Partners() {
const navigate = useNavigate();
const [partners, setPartners] = useState<Partner[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState('all');
const loadPartners = async () => {
setIsLoading(true);
setError(null);
try {
const data = await fetchPartnersApi();
setPartners(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch partners');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadPartners();
}, []);
const filteredPartners = useMemo(() => {
let list = [...partners];
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
list = list.filter(p =>
p.name.toLowerCase().includes(q) ||
p.primary_contact_person_name.toLowerCase().includes(q) ||
p.primary_contact_person_email.toLowerCase().includes(q)
);
}
switch (activeTab) {
case 'pending_kyc':
list = list.filter(p => p.kyc_compliance_status?.toLowerCase() === 'pending');
break;
case 'active':
list = list.filter(p => p.status?.toLowerCase() === 'active');
break;
}
return list;
}, [partners, searchQuery, activeTab]);
const stats = useMemo(() => ({
total: partners.length,
active: partners.filter(p => p.status?.toLowerCase() === 'active').length,
pendingKyc: partners.filter(p => p.kyc_compliance_status?.toLowerCase() === 'pending').length,
kycApproved: partners.filter(p => p.is_kyc_compliant).length,
}), [partners]);
return ( return (
<AppLayout <AppLayout
title="Partner Management" title="Partner Management"
description="Manage partner accounts, KYC approvals, and Stripe connections." description="Manage partner accounts, KYC approvals, and onboarding."
> >
{/* Quick Stats */} {/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="neu-card p-6"> <div className="neu-card p-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-2 text-muted-foreground text-xs mb-1">
<div className="h-12 w-12 rounded-xl bg-accent/10 flex items-center justify-center"> <Users className="h-3.5 w-3.5" /> Total Partners
<Users className="h-6 w-6 text-accent" />
</div> </div>
<div> <p className="text-2xl font-bold">{stats.total}</p>
<p className="text-2xl font-bold text-foreground">156</p>
<p className="text-sm text-muted-foreground">Total Partners</p>
</div> </div>
<div className="neu-card p-4">
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-1">
<UserCheck className="h-3.5 w-3.5" /> Active
</div> </div>
<p className="text-2xl font-bold text-success">{stats.active}</p>
</div> </div>
<div className="neu-card p-6"> <div className="neu-card p-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-2 text-muted-foreground text-xs mb-1">
<div className="h-12 w-12 rounded-xl bg-warning/10 flex items-center justify-center"> <Clock className="h-3.5 w-3.5" /> Pending KYC
<UserCheck className="h-6 w-6 text-warning" />
</div> </div>
<div> <p className="text-2xl font-bold text-warning">{stats.pendingKyc}</p>
<p className="text-2xl font-bold text-foreground">12</p>
<p className="text-sm text-muted-foreground">Pending KYC</p>
</div>
</div>
</div>
<div className="neu-card p-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-xl bg-error/10 flex items-center justify-center">
<AlertTriangle className="h-6 w-6 text-error" />
</div>
<div>
<p className="text-2xl font-bold text-foreground">2</p>
<p className="text-sm text-muted-foreground">Stripe Issues</p>
</div> </div>
<div className="neu-card p-4">
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-1">
<ShieldCheck className="h-3.5 w-3.5" /> KYC Approved
</div> </div>
<p className="text-2xl font-bold text-success">{stats.kycApproved}</p>
</div> </div>
</div> </div>
{/* Partners Table */} {/* Toolbar */}
<div className="neu-card p-6"> <div className="flex flex-col sm:flex-row gap-3 mb-6">
<div className="flex items-center justify-between mb-6"> <div className="relative flex-1">
<h2 className="text-lg font-bold text-foreground">All Partners</h2>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input <Input
type="text" placeholder="Search by name, email, or contact..."
placeholder="Search partners..." value={searchQuery}
className="h-10 w-64 pl-10 pr-4 rounded-xl text-sm bg-secondary shadow-neu-inset focus:outline-none focus:ring-2 focus:ring-accent/50" onChange={e => setSearchQuery(e.target.value)}
className="pl-9"
/> />
</div> </div>
<button className="h-10 px-4 rounded-xl neu-button flex items-center gap-2"> <AddPartnerSheet>
<Filter className="h-4 w-4" /> <Button className="gap-2 shrink-0">
<span className="text-sm font-medium">Filter</span> <Plus className="h-4 w-4" /> Add Partner
</button> </Button>
</div> </AddPartnerSheet>
</div> </div>
<div className="overflow-x-auto"> {/* Tabs + Table */}
<table className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<thead> <TabsList className="mb-4">
<tr className="border-b border-border/50"> <TabsTrigger value="all" className="gap-1.5">
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Partner</th> All <Badge variant="secondary" className="text-[10px] h-5 px-1.5 ml-1">{stats.total}</Badge>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">KYC Status</th> </TabsTrigger>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Stripe</th> <TabsTrigger value="active" className="gap-1.5">
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Revenue</th> Active <Badge variant="secondary" className="text-[10px] h-5 px-1.5 ml-1 bg-success/10 text-success">{stats.active}</Badge>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Events</th> </TabsTrigger>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Actions</th> <TabsTrigger value="pending_kyc" className="gap-1.5">
</tr> Pending KYC <Badge variant="secondary" className="text-[10px] h-5 px-1.5 ml-1 bg-warning/10 text-warning">{stats.pendingKyc}</Badge>
</thead> </TabsTrigger>
<tbody> </TabsList>
{mockPartners.map((partner) => (
<tr key={partner.id} className="border-b border-border/30 hover:bg-secondary/30 transition-colors"> <div className="neu-card overflow-hidden">
<td className="py-4 px-4"> {isLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : error ? (
<div className="text-center py-16">
<AlertTriangle className="h-10 w-10 mx-auto mb-3 text-destructive/50" />
<p className="text-destructive text-sm font-medium">{error}</p>
<Button variant="outline" size="sm" className="mt-4" onClick={loadPartners}>
Retry
</Button>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[250px]">Partner</TableHead>
<TableHead>Type</TableHead>
<TableHead>KYC Status</TableHead>
<TableHead>Location</TableHead>
<TableHead>Status</TableHead>
<TableHead className="w-[50px]" />
</TableRow>
</TableHeader>
<TableBody>
{filteredPartners.length > 0 ? (
filteredPartners.map(partner => (
<TableRow
key={partner.id}
className="cursor-pointer hover:bg-secondary/30"
onClick={() => navigate(`/partners/${partner.id}`)}
>
<TableCell>
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-lg bg-secondary flex items-center justify-center border border-border/50 shrink-0">
<span className="text-xs font-bold text-muted-foreground">
{partner.name.substring(0, 2).toUpperCase()}
</span>
</div>
<div> <div>
<p className="font-medium text-foreground">{partner.name}</p> <p className="font-medium text-sm">{partner.name}</p>
<p className="text-sm text-muted-foreground">{partner.email}</p> <p className="text-xs text-muted-foreground">{partner.primary_contact_person_email}</p>
</div>
</td>
<td className="py-4 px-4">
<span className={cn(
"inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium",
partner.kycStatus === 'approved' && "bg-success/10 text-success",
partner.kycStatus === 'pending' && "bg-warning/10 text-warning",
partner.kycStatus === 'rejected' && "bg-error/10 text-error"
)}>
{partner.kycStatus.charAt(0).toUpperCase() + partner.kycStatus.slice(1)}
</span>
</td>
<td className="py-4 px-4">
<span className={cn(
"inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium",
partner.stripeStatus === 'connected' && "bg-success/10 text-success",
partner.stripeStatus === 'pending' && "bg-warning/10 text-warning",
partner.stripeStatus === 'failed' && "bg-error/10 text-error"
)}>
{partner.stripeStatus.charAt(0).toUpperCase() + partner.stripeStatus.slice(1)}
</span>
</td>
<td className="py-4 px-4 text-right font-medium text-foreground">
{formatCurrency(partner.totalRevenue)}
</td>
<td className="py-4 px-4 text-right font-medium text-foreground">
{partner.eventsCount}
</td>
<td className="py-4 px-4 text-right">
<button className="text-sm font-medium text-accent hover:underline">
View
</button>
</td>
</tr>
))}
</tbody>
</table>
</div> </div>
</div> </div>
</TableCell>
<TableCell>
<Badge variant="secondary" className="text-xs capitalize">
{partner.partner_type}
</Badge>
</TableCell>
<TableCell>
<KycBadge status={partner.kyc_compliance_status} />
</TableCell>
<TableCell>
<p className="text-sm">{partner.city}</p>
<p className="text-xs text-muted-foreground">{partner.state}</p>
</TableCell>
<TableCell>
<StatusBadge status={partner.status} />
</TableCell>
<TableCell onClick={e => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/partners/${partner.id}`)}>
<Eye className="h-4 w-4 mr-2" /> View Details
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="text-center py-12">
<Users className="h-10 w-10 mx-auto mb-3 text-muted-foreground/30" />
<p className="text-muted-foreground text-sm">No partners found</p>
<p className="text-muted-foreground text-xs mt-1">Try adjusting your search or filters</p>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
</Tabs>
</AppLayout> </AppLayout>
); );
} }

45
src/services/api.ts Normal file
View File

@@ -0,0 +1,45 @@
import { AuthError, getStoredAuth } from './auth';
/**
* Centralized API client. Every backend call in the project goes through here.
*
* - Automatically injects `username` and `token` from localStorage
* - All requests are POST with JSON body (matches backend convention)
* - Handles error responses and token expiry uniformly
*/
export async function apiPost<T = any>(
url: string,
body: Record<string, any> = {},
{ skipAuth = false }: { skipAuth?: boolean } = {},
): Promise<T> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
let authPayload: Record<string, string> = {};
if (!skipAuth) {
const stored = getStoredAuth();
if (!stored) {
throw new AuthError('Not authenticated', true);
}
authPayload = { username: stored.username, token: stored.token };
}
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...authPayload, ...body }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const message = data.message || data.error || data.errors || 'Request failed';
const isTokenError =
res.status === 401 ||
res.status === 403 ||
(typeof message === 'string' && message.toLowerCase().includes('invalid token'));
throw new AuthError(message, isTokenError);
}
return data as T;
}

View File

@@ -1,26 +1,52 @@
// Authentication service based on UAT admin panel pattern import { apiPost } from './api';
const AUTH_API_URL = import.meta.env.VITE_AUTH_API_URL || 'https://uat.eventifyplus.com/api/'; const AUTH_BASE_URL = '/accounts/api/';
export interface AuthUser { export interface AuthUser {
id: number;
username: string; username: string;
token: string; token: string;
first_name?: string; first_name: string;
last_name?: string; last_name: string;
email?: string; email: string;
profile_photo?: string; phone_number: string;
role: string;
is_staff: boolean;
is_customer: boolean;
is_user: boolean;
pincode: string | null;
district: string | null;
state: string | null;
country: string | null;
place: string | null;
latitude: number | null;
longitude: number | null;
profile_picture: string;
} }
export interface LoginResponse { export interface LoginResponse {
username: string; status: string;
message: string;
token: string; token: string;
message?: string; user: {
user?: { id: number;
first_name?: string; username: string;
last_name?: string; first_name: string;
email?: string; last_name: string;
phone_number?: string; email: string;
profile_photo?: string; phone_number: string;
role: string;
is_staff: boolean;
is_customer: boolean;
is_user: boolean;
pincode: string | null;
district: string | null;
state: string | null;
country: string | null;
place: string | null;
latitude: number | null;
longitude: number | null;
profile_picture: string;
}; };
} }
@@ -34,132 +60,51 @@ export class AuthError extends Error {
} }
} }
/**
* Login with username and password
*/
export const login = async (username: string, password: string): Promise<LoginResponse> => { export const login = async (username: string, password: string): Promise<LoginResponse> => {
console.log('Bypassing auth for dev as requested'); const data = await apiPost<LoginResponse>(
`${AUTH_BASE_URL}login/`,
{ username, password },
{ skipAuth: true },
);
// Return mock successful response immediately if (data.status !== 'success') {
return { throw new AuthError(data.message || 'Login failed', false);
username: username,
token: 'dev-bypass-token-' + Date.now(),
message: 'Login successful (Bypass)',
user: {
first_name: 'Admin',
last_name: 'User (Bypass)',
email: username,
profile_photo: '', // Placeholder or empty
},
};
};
/**
* Check user status with token
*/
export const checkUserStatus = async (username: string, token: string): Promise<any> => {
// Support bypass token
if (token && token.startsWith('dev-bypass-token')) {
return {
status: 'active',
user: {
first_name: 'Admin',
last_name: 'User (Bypass)',
email: username || 'admin@example.com',
}
};
}
const statusUrl = `${AUTH_API_URL}user/status`;
const res = await fetch(statusUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Status check failed';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') ||
errorMessage.toLowerCase().includes('token') && (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
} }
return data; return data;
}; };
/** export const logout = async (): Promise<{ message: string }> => {
* Logout user return apiPost(`${AUTH_BASE_URL}logout/`);
*/ };
export const logout = async (username: string, token: string): Promise<any> => {
// Handle bypass token logout locally export const checkUserStatus = async (_username: string, token: string): Promise<{ valid: true }> => {
if (token && token.startsWith('dev-bypass-token')) { const stored = getStoredAuth();
return { message: 'Logged out successfully' }; if (stored && stored.token === token) {
} return { valid: true };
}
const logoutUrl = `${AUTH_API_URL}user/logout`; throw new AuthError('No valid session found', true);
const res = await fetch(logoutUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new AuthError(data.message || data.errors || data.error || 'Logout failed');
}
return data;
}; };
/**
* Get stored authentication data from localStorage
*/
export const getStoredAuth = (): AuthUser | null => { export const getStoredAuth = (): AuthUser | null => {
try { try {
const username = localStorage.getItem('username'); const raw = localStorage.getItem('authUser');
const token = localStorage.getItem('token'); if (!raw) return null;
const userData = localStorage.getItem('userData'); return JSON.parse(raw) as AuthUser;
} catch {
if (!username || !token) { console.error('Error reading stored auth');
return null; return null;
} }
};
const parsedUserData = userData ? JSON.parse(userData) : {}; export const storeAuth = (response: LoginResponse): void => {
const authUser: AuthUser = {
return { ...response.user,
username, token: response.token,
token,
...parsedUserData,
}; };
} catch (error) { localStorage.setItem('authUser', JSON.stringify(authUser));
console.error('Error reading stored auth:', error);
return null;
}
}; };
/**
* Store authentication data in localStorage
*/
export const storeAuth = (loginResponse: LoginResponse): void => {
localStorage.setItem('username', loginResponse.username);
localStorage.setItem('token', loginResponse.token);
if (loginResponse.user) {
localStorage.setItem('userData', JSON.stringify(loginResponse.user));
}
};
/**
* Clear authentication data from localStorage
*/
export const clearAuth = (): void => { export const clearAuth = (): void => {
localStorage.removeItem('username'); localStorage.removeItem('authUser');
localStorage.removeItem('token');
localStorage.removeItem('userData');
}; };

View File

@@ -1,207 +1,44 @@
// API service for Partner Dashboard (partner.prototype.eventifyplus.com) import { apiPost } from './api';
import { AuthError } from './auth';
const API_URL = import.meta.env.VITE_PARTNER_APP_API_URL || 'https://partner.prototype.eventifyplus.com/api/';
export interface Partner { export interface Partner {
id: number; id: number;
name: string; name: string;
email: string; partner_type: string;
phone?: string; primary_contact_person_name: string;
company_name?: string; primary_contact_person_email: string;
kyc_status: 'pending' | 'approved' | 'rejected'; primary_contact_person_phone: string;
stripe_status: 'pending' | 'connected' | 'failed'; status: string;
stripe_account_id?: string; address: string;
total_revenue?: number; city: string;
events_count?: number; state: string;
created_at?: string; country: string;
kyc_documents?: { website_url: string | null;
id_proof?: string; pincode: string;
address_proof?: string; latitude: string;
business_registration?: string; longitude: string;
}; is_kyc_compliant: boolean;
kyc_compliance_status: string;
kyc_compliance_reason: string | null;
kyc_compliance_document_type: string | null;
kyc_compliance_document_other_type: string | null;
kyc_compliance_document_number: string | null;
kyc_compliance_document_file: string | null;
} }
export interface PartnerEvent { export interface CreatePartnerPayload {
id: number;
partner_id: number;
title: string;
description?: string;
date: string;
time?: string;
venue?: string;
ticket_price?: number;
total_tickets?: number;
tickets_sold?: number;
status: 'draft' | 'pending_approval' | 'approved' | 'live' | 'completed' | 'cancelled';
revenue?: number;
}
export interface Staff {
id: number;
partner_id: number;
name: string; name: string;
email: string; partner_type: string;
role: string; primary_contact_person_name: string;
permissions?: string[]; primary_contact_person_email: string;
status: 'active' | 'inactive'; primary_contact_person_phone?: string;
website_url?: string;
} }
/** export const fetchPartners = async (): Promise<Partner[]> => {
* Fetch all partners (admin only) const data = await apiPost<{ status: string; partners: Partner[] }>('/partner/list/');
*/
export const fetchPartners = async (username: string, token: string): Promise<Partner[]> => {
const res = await fetch(`${API_URL}partners/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch partners';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.partners || []; return data.partners || [];
}; };
/** export const createPartner = async (payload: CreatePartnerPayload): Promise<any> => {
* Update partner KYC status (admin only) return apiPost('/partner/create/', payload);
*/
export const updatePartnerKYC = async (
username: string,
token: string,
partnerId: number,
status: 'approved' | 'rejected',
notes?: string
): Promise<any> => {
const res = await fetch(`${API_URL}partners/kyc/update/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, partner_id: partnerId, status, notes }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to update KYC status';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};
/**
* Fetch partner events (admin can view all)
*/
export const fetchPartnerEvents = async (
username: string,
token: string,
partnerId?: number
): Promise<PartnerEvent[]> => {
const res = await fetch(`${API_URL}events/partner/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, partner_id: partnerId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch partner events';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || [];
};
/**
* Approve or reject partner event (admin only)
*/
export const moderatePartnerEvent = async (
username: string,
token: string,
eventId: number,
action: 'approve' | 'reject',
reason?: string
): Promise<any> => {
const res = await fetch(`${API_URL}events/moderate/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, event_id: eventId, action, reason }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to moderate event';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
};
/**
* Fetch partner staff members
*/
export const fetchPartnerStaff = async (
username: string,
token: string,
partnerId?: number
): Promise<Staff[]> => {
const res = await fetch(`${API_URL}staff/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, partner_id: partnerId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch staff';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.staff || [];
};
/**
* Update partner Stripe connection status
*/
export const updatePartnerStripe = async (
username: string,
token: string,
partnerId: number,
stripeAccountId: string,
status: 'connected' | 'failed'
): Promise<any> => {
const res = await fetch(`${API_URL}partners/stripe/update/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
token,
partner_id: partnerId,
stripe_account_id: stripeAccountId,
status
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to update Stripe status';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
}; };

View File

@@ -1,7 +1,4 @@
// API service for User App (mvnew.eventifyplus.com) import { apiPost } from './api';
// Based on UAT admin panel API patterns
import { AuthError } from './auth';
const API_URL = import.meta.env.VITE_USER_APP_API_URL || 'https://uat.eventifyplus.com/api/'; const API_URL = import.meta.env.VITE_USER_APP_API_URL || 'https://uat.eventifyplus.com/api/';
@@ -54,167 +51,36 @@ export interface Booking {
status?: string; status?: string;
} }
/** export const fetchEvents = async (): Promise<Event[]> => {
* Fetch all events const data = await apiPost<{ events: Event[] }>(`${API_URL}events/all/`);
*/
export const fetchEvents = async (username: string, token: string): Promise<Event[]> => {
const res = await fetch(`${API_URL}events/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch events';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || []; return data.events || [];
}; };
/** export const fetchCalendarEvents = async (month: number, year: number): Promise<Event[]> => {
* Fetch calendar events for a specific month const data = await apiPost<{ events: Event[] }>(`${API_URL}calendar/`, { month, year });
*/
export const fetchCalendarEvents = async (
username: string,
token: string,
month: number,
year: number
): Promise<Event[]> => {
const res = await fetch(`${API_URL}calendar/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, month, year }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch calendar events';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || []; return data.events || [];
}; };
/** export const fetchEventsByDate = async (date_of_event: string): Promise<Event[]> => {
* Fetch events by date const data = await apiPost<{ events: Event[] }>(`${API_URL}events/by-date/`, { date_of_event });
*/
export const fetchEventsByDate = async (
username: string,
token: string,
date_of_event: string
): Promise<Event[]> => {
const res = await fetch(`${API_URL}events/by-date/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, date_of_event }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch events by date';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.events || []; return data.events || [];
}; };
/** export const fetchUsers = async (): Promise<User[]> => {
* Fetch all users (admin only) const data = await apiPost<{ users: User[] }>(`${API_URL}users/all/`);
*/
export const fetchUsers = async (username: string, token: string): Promise<User[]> => {
const res = await fetch(`${API_URL}users/all/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch users';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.users || []; return data.users || [];
}; };
/** export const fetchCategories = async (): Promise<Category[]> => {
* Fetch event categories const data = await apiPost<{ categories: Category[] }>(`${API_URL}categories/`);
*/
export const fetchCategories = async (username: string, token: string): Promise<Category[]> => {
const res = await fetch(`${API_URL}categories/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch categories';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.categories || []; return data.categories || [];
}; };
/** export const fetchUserBookings = async (userId: number): Promise<Booking[]> => {
* Fetch user bookings const data = await apiPost<{ bookings: Booking[] }>(`${API_URL}bookings/user/`, { user_id: userId });
*/
export const fetchUserBookings = async (
username: string,
token: string,
userId: number
): Promise<Booking[]> => {
const res = await fetch(`${API_URL}bookings/user/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, user_id: userId }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to fetch bookings';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data.bookings || []; return data.bookings || [];
}; };
/** export const updateEventStatus = async (eventId: number, status: string): Promise<any> => {
* Update event status (admin only) return apiPost(`${API_URL}events/update-status/`, { event_id: eventId, status });
*/
export const updateEventStatus = async (
username: string,
token: string,
eventId: number,
status: string
): Promise<any> => {
const res = await fetch(`${API_URL}events/update-status/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, token, event_id: eventId, status }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorMessage = data.message || data.errors || data.error || 'Failed to update event';
const isTokenError = errorMessage.toLowerCase().includes('invalid token') || (res.status === 401 || res.status === 403);
throw new AuthError(errorMessage, isTokenError);
}
return data;
}; };

View File

@@ -12,12 +12,20 @@ export default defineConfig(({ mode }) => ({
overlay: false, overlay: false,
}, },
proxy: { proxy: {
// Proxy API requests to bypass CORS during development '/accounts/api': {
target: 'http://0.0.0.0:8000',
changeOrigin: true,
secure: false,
},
'/partner': {
target: 'http://0.0.0.0:8000',
changeOrigin: true,
secure: false,
},
'/api': { '/api': {
target: 'https://uat.eventifyplus.com', target: 'https://uat.eventifyplus.com',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
rewrite: (path) => path,
}, },
}, },
}, },