fix(security): Multi-tenancy isolation and customer appointment filtering
- Add request tenant validation to all ViewSets (EventViewSet, ResourceViewSet, ParticipantViewSet, CustomerViewSet, StaffViewSet) to prevent cross-tenant data access via subdomain/header manipulation - Change permission_classes from AllowAny to IsAuthenticated for EventViewSet and ResourceViewSet - Filter events for customers to only show appointments where they are a participant - Add customer field to EventSerializer to create Customer participants when appointments are created - Update CustomerDashboard to fetch appointments from API instead of mock data - Fix TenantViewSet.destroy() to properly handle cross-schema cascade when deleting tenants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,60 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useOutletContext, Link } from 'react-router-dom';
|
||||
import { User, Business, Appointment } from '../../types';
|
||||
import { APPOINTMENTS, SERVICES } from '../../mockData';
|
||||
import { Calendar, Clock, MapPin, AlertTriangle } from 'lucide-react';
|
||||
import { useAppointments, useUpdateAppointment } from '../../hooks/useAppointments';
|
||||
import { useServices } from '../../hooks/useServices';
|
||||
import { Calendar, Clock, MapPin, AlertTriangle, Loader2 } from 'lucide-react';
|
||||
|
||||
const AppointmentList: React.FC<{ user: User, business: Business }> = ({ user, business }) => {
|
||||
const [appointments, setAppointments] = useState(APPOINTMENTS);
|
||||
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming');
|
||||
|
||||
const myAppointments = useMemo(() => appointments.filter(apt => apt.customerName.includes(user.name.split(' ')[0])).sort((a, b) => b.startTime.getTime() - a.startTime.getTime()), [user.name, appointments]);
|
||||
|
||||
const upcomingAppointments = myAppointments.filter(apt => new Date(apt.startTime) >= new Date() && apt.status !== 'CANCELLED');
|
||||
const pastAppointments = myAppointments.filter(apt => new Date(apt.startTime) < new Date() || apt.status === 'CANCELLED');
|
||||
// Fetch appointments from API - backend filters for current customer
|
||||
const { data: appointments = [], isLoading, error } = useAppointments();
|
||||
const { data: services = [] } = useServices();
|
||||
const updateAppointment = useUpdateAppointment();
|
||||
|
||||
const handleCancel = (appointment: Appointment) => {
|
||||
// Sort appointments by start time (newest first)
|
||||
const sortedAppointments = useMemo(() =>
|
||||
[...appointments].sort((a, b) => b.startTime.getTime() - a.startTime.getTime()),
|
||||
[appointments]
|
||||
);
|
||||
|
||||
const upcomingAppointments = sortedAppointments.filter(apt => new Date(apt.startTime) >= new Date() && apt.status !== 'CANCELLED');
|
||||
const pastAppointments = sortedAppointments.filter(apt => new Date(apt.startTime) < new Date() || apt.status === 'CANCELLED');
|
||||
|
||||
const handleCancel = async (appointment: Appointment) => {
|
||||
const hoursBefore = (new Date(appointment.startTime).getTime() - new Date().getTime()) / 3600000;
|
||||
if (hoursBefore < business.cancellationWindowHours) {
|
||||
const service = SERVICES.find(s => s.id === appointment.serviceId);
|
||||
const service = services.find(s => s.id === appointment.serviceId);
|
||||
const fee = service ? (service.price * (business.lateCancellationFeePercent / 100)).toFixed(2) : 'a fee';
|
||||
if (!window.confirm(`Cancelling within the ${business.cancellationWindowHours}-hour window may incur a fee of $${fee}. Are you sure?`)) return;
|
||||
} else {
|
||||
if (!window.confirm("Are you sure you want to cancel this appointment?")) return;
|
||||
}
|
||||
setAppointments(prev => prev.map(apt => apt.id === appointment.id ? {...apt, status: 'CANCELLED'} : apt));
|
||||
try {
|
||||
await updateAppointment.mutateAsync({ id: appointment.id, updates: { status: 'CANCELLED' } });
|
||||
} catch (err) {
|
||||
console.error('Failed to cancel appointment:', err);
|
||||
alert('Failed to cancel appointment. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-8 flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-brand-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="mt-8 text-center py-8 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<p className="text-red-600 dark:text-red-400">Failed to load appointments. Please try again later.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<h2 className="text-xl font-bold mb-4">Your Appointments</h2>
|
||||
@@ -34,14 +64,22 @@ const AppointmentList: React.FC<{ user: User, business: Business }> = ({ user, b
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{(activeTab === 'upcoming' ? upcomingAppointments : pastAppointments).map(apt => {
|
||||
const service = SERVICES.find(s => s.id === apt.serviceId);
|
||||
const service = services.find(s => s.id === apt.serviceId);
|
||||
return (
|
||||
<div key={apt.id} className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold">{service?.name}</h3>
|
||||
<h3 className="font-semibold">{service?.name || 'Appointment'}</h3>
|
||||
<p className="text-sm text-gray-500">{new Date(apt.startTime).toLocaleString()}</p>
|
||||
</div>
|
||||
{activeTab === 'upcoming' && <button onClick={() => handleCancel(apt)} className="text-sm font-medium text-red-600 hover:underline">Cancel</button>}
|
||||
{activeTab === 'upcoming' && (
|
||||
<button
|
||||
onClick={() => handleCancel(apt)}
|
||||
disabled={updateAppointment.isPending}
|
||||
className="text-sm font-medium text-red-600 hover:underline disabled:opacity-50"
|
||||
>
|
||||
{updateAppointment.isPending ? 'Cancelling...' : 'Cancel'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user