mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-15 10:34:43 -05:00
Improve input validation and data sanitization
The changes add consistent string trimming, better null handling, and improved validation logic across the business and client forms.
This commit is contained in:
@@ -43,17 +43,6 @@ interface BusinessesDataTableProps {
|
|||||||
businesses: Business[];
|
businesses: Business[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatAddress = (business: Business) => {
|
|
||||||
const parts = [
|
|
||||||
business.addressLine1,
|
|
||||||
business.addressLine2,
|
|
||||||
business.city,
|
|
||||||
business.state,
|
|
||||||
business.postalCode,
|
|
||||||
].filter(Boolean);
|
|
||||||
return parts.join(", ") || "—";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
|
export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [businessToDelete, setBusinessToDelete] = useState<Business | null>(
|
const [businessToDelete, setBusinessToDelete] = useState<Business | null>(
|
||||||
@@ -103,7 +92,7 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate font-medium">{business.name}</p>
|
<p className="truncate font-medium">{business.name}</p>
|
||||||
<p className="text-muted-foreground truncate text-sm">
|
<p className="text-muted-foreground truncate text-sm">
|
||||||
{business.email ?? "—"}
|
{business.nickname ?? "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,11 +100,11 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "nickname",
|
accessorKey: "email",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title="Nickname" />
|
<DataTableColumnHeader column={column} title="Email" />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => row.original.nickname ?? "—",
|
cell: ({ row }) => row.original.email ?? "—",
|
||||||
meta: {
|
meta: {
|
||||||
headerClassName: "hidden sm:table-cell",
|
headerClassName: "hidden sm:table-cell",
|
||||||
cellClassName: "hidden sm:table-cell",
|
cellClassName: "hidden sm:table-cell",
|
||||||
@@ -132,26 +121,6 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
|
|||||||
cellClassName: "hidden md:table-cell",
|
cellClassName: "hidden md:table-cell",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "address",
|
|
||||||
header: "Address",
|
|
||||||
cell: ({ row }) => formatAddress(row.original),
|
|
||||||
meta: {
|
|
||||||
headerClassName: "hidden lg:table-cell",
|
|
||||||
cellClassName: "hidden lg:table-cell",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "taxId",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader column={column} title="Tax ID" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => row.original.taxId ?? "—",
|
|
||||||
meta: {
|
|
||||||
headerClassName: "hidden xl:table-cell",
|
|
||||||
cellClassName: "hidden xl:table-cell",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: "website",
|
accessorKey: "website",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
|
|||||||
@@ -123,29 +123,18 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
|
|
||||||
// Update email configuration mutation
|
// Update email configuration mutation
|
||||||
const updateEmailConfig = api.businesses.updateEmailConfig.useMutation({
|
const updateEmailConfig = api.businesses.updateEmailConfig.useMutation({
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Email configuration updated successfully");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Failed to update email configuration: ${error.message}`);
|
toast.error(`Failed to update email configuration: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createBusiness = api.businesses.create.useMutation({
|
const createBusiness = api.businesses.create.useMutation({
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Business created successfully");
|
|
||||||
router.push("/dashboard/businesses");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message || "Failed to create business");
|
toast.error(error.message || "Failed to create business");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateBusiness = api.businesses.update.useMutation({
|
const updateBusiness = api.businesses.update.useMutation({
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Business updated successfully");
|
|
||||||
router.push("/dashboard/businesses");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message || "Failed to update business");
|
toast.error(error.message || "Failed to update business");
|
||||||
},
|
},
|
||||||
@@ -203,69 +192,87 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
newErrors.name = VALIDATION_MESSAGES.required;
|
newErrors.name = VALIDATION_MESSAGES.required;
|
||||||
}
|
}
|
||||||
// Nickname validation (optional, max 255 chars)
|
// Nickname validation (optional, max 255 chars)
|
||||||
if (formData.nickname && formData.nickname.length > 255) {
|
if (formData.nickname && formData.nickname.trim().length > 255) {
|
||||||
newErrors.nickname = "Nickname must be 255 characters or less";
|
newErrors.nickname = "Nickname must be 255 characters or less";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (formData.email && !isValidEmail(formData.email)) {
|
if (formData.email.trim() && !isValidEmail(formData.email.trim())) {
|
||||||
newErrors.email = VALIDATION_MESSAGES.email;
|
newErrors.email = VALIDATION_MESSAGES.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phone validation (basic check for US format)
|
// Phone validation (basic check for US format)
|
||||||
if (formData.phone) {
|
if (formData.phone.trim()) {
|
||||||
const phoneDigits = formData.phone.replace(/\D/g, "");
|
const phoneDigits = formData.phone.replace(/\D/g, "");
|
||||||
if (phoneDigits.length > 0 && phoneDigits.length < 10) {
|
if (phoneDigits.length > 0 && phoneDigits.length < 10) {
|
||||||
newErrors.phone = VALIDATION_MESSAGES.phone;
|
newErrors.phone = VALIDATION_MESSAGES.phone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address validation if any address field is filled
|
// Address validation if any address field is filled (excluding country as it has a default)
|
||||||
const hasAddressData =
|
const hasAddressData = !!(
|
||||||
formData.addressLine1 ||
|
formData.addressLine1.trim() ||
|
||||||
formData.city ||
|
formData.city.trim() ||
|
||||||
formData.state ||
|
formData.state.trim() ||
|
||||||
formData.postalCode;
|
formData.postalCode.trim()
|
||||||
|
);
|
||||||
|
|
||||||
if (hasAddressData) {
|
// Also check if country was explicitly changed from default
|
||||||
if (!formData.addressLine1)
|
const hasNonDefaultCountry =
|
||||||
|
formData.country.trim() && formData.country.trim() !== "United States";
|
||||||
|
const hasAnyAddressInput = hasAddressData || hasNonDefaultCountry;
|
||||||
|
|
||||||
|
// Only validate address if user has actually entered address data
|
||||||
|
if (hasAnyAddressInput) {
|
||||||
|
if (!formData.addressLine1.trim())
|
||||||
newErrors.addressLine1 = VALIDATION_MESSAGES.required;
|
newErrors.addressLine1 = VALIDATION_MESSAGES.required;
|
||||||
if (!formData.city) newErrors.city = VALIDATION_MESSAGES.required;
|
if (!formData.city.trim()) newErrors.city = VALIDATION_MESSAGES.required;
|
||||||
if (!formData.country) newErrors.country = VALIDATION_MESSAGES.required;
|
if (!formData.country.trim())
|
||||||
|
newErrors.country = VALIDATION_MESSAGES.required;
|
||||||
|
|
||||||
if (formData.country === "United States") {
|
// Only require US-specific fields if country is United States AND we have actual address data
|
||||||
if (!formData.state) newErrors.state = VALIDATION_MESSAGES.required;
|
if (formData.country.trim() === "United States" && hasAddressData) {
|
||||||
if (!formData.postalCode)
|
if (!formData.state.trim())
|
||||||
|
newErrors.state = VALIDATION_MESSAGES.required;
|
||||||
|
if (!formData.postalCode.trim())
|
||||||
newErrors.postalCode = VALIDATION_MESSAGES.required;
|
newErrors.postalCode = VALIDATION_MESSAGES.required;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email configuration validation
|
// Email configuration validation
|
||||||
// API Key validation
|
// API Key validation
|
||||||
if (formData.resendApiKey && !formData.resendApiKey.startsWith("re_")) {
|
if (
|
||||||
|
formData.resendApiKey.trim() &&
|
||||||
|
!formData.resendApiKey.trim().startsWith("re_")
|
||||||
|
) {
|
||||||
newErrors.resendApiKey = "Resend API key should start with 're_'";
|
newErrors.resendApiKey = "Resend API key should start with 're_'";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Domain validation
|
// Domain validation
|
||||||
if (formData.resendDomain) {
|
if (formData.resendDomain.trim()) {
|
||||||
const domainRegex =
|
const domainRegex =
|
||||||
/^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.([a-zA-Z]{2,})+$/;
|
/^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.([a-zA-Z]{2,})+$/;
|
||||||
if (!domainRegex.test(formData.resendDomain)) {
|
if (!domainRegex.test(formData.resendDomain.trim())) {
|
||||||
newErrors.resendDomain =
|
newErrors.resendDomain =
|
||||||
"Please enter a valid domain (e.g., yourdomain.com)";
|
"Please enter a valid domain (e.g., yourdomain.com)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If API key is provided, domain must also be provided
|
// If API key is provided, domain must also be provided
|
||||||
if (formData.resendApiKey && !formData.resendDomain) {
|
if (formData.resendApiKey.trim() && !formData.resendDomain.trim()) {
|
||||||
newErrors.resendDomain = "Domain is required when API key is provided";
|
newErrors.resendDomain = "Domain is required when API key is provided";
|
||||||
}
|
}
|
||||||
|
|
||||||
// If domain is provided, API key must also be provided
|
// If domain is provided, API key must also be provided (unless there's already one on the server)
|
||||||
|
// In edit mode, if domain comes from server and API key field is empty, don't require new API key
|
||||||
|
const userEnteredDomain =
|
||||||
|
formData.resendDomain.trim() !== (emailConfig?.resendDomain ?? "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
formData.resendDomain &&
|
formData.resendDomain.trim() &&
|
||||||
!formData.resendApiKey &&
|
!formData.resendApiKey.trim() &&
|
||||||
!emailConfig?.hasApiKey
|
!emailConfig?.hasApiKey &&
|
||||||
|
(mode === "create" || userEnteredDomain)
|
||||||
) {
|
) {
|
||||||
newErrors.resendApiKey = "API key is required when domain is provided";
|
newErrors.resendApiKey = "API key is required when domain is provided";
|
||||||
}
|
}
|
||||||
@@ -289,7 +296,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
const dataToSubmit = {
|
const dataToSubmit = {
|
||||||
...formData,
|
...formData,
|
||||||
name: formData.name.trim(),
|
name: formData.name.trim(),
|
||||||
nickname: formData.nickname.trim(),
|
nickname: formData.nickname.trim() || undefined,
|
||||||
website: formData.website ? formatWebsiteUrl(formData.website) : "",
|
website: formData.website ? formatWebsiteUrl(formData.website) : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -313,20 +320,24 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
|
|
||||||
const newBusiness = await createBusiness.mutateAsync(businessData);
|
const newBusiness = await createBusiness.mutateAsync(businessData);
|
||||||
|
|
||||||
// Update email configuration separately if any email fields are provided
|
// Update email configuration separately if any email fields have values
|
||||||
if (
|
const newApiKey = formData.resendApiKey.trim();
|
||||||
newBusiness &&
|
const newDomain = formData.resendDomain.trim();
|
||||||
(formData.resendApiKey ||
|
const newFromName = formData.emailFromName.trim();
|
||||||
formData.resendDomain ||
|
|
||||||
formData.emailFromName)
|
const hasEmailData = newApiKey || newDomain || newFromName;
|
||||||
) {
|
|
||||||
|
if (newBusiness && hasEmailData) {
|
||||||
await updateEmailConfig.mutateAsync({
|
await updateEmailConfig.mutateAsync({
|
||||||
id: newBusiness.id,
|
id: newBusiness.id,
|
||||||
resendApiKey: formData.resendApiKey || undefined,
|
resendApiKey: newApiKey || undefined,
|
||||||
resendDomain: formData.resendDomain || undefined,
|
resendDomain: newDomain || undefined,
|
||||||
emailFromName: formData.emailFromName || undefined,
|
emailFromName: newFromName || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.success("Business created successfully");
|
||||||
|
router.push("/dashboard/businesses");
|
||||||
} else {
|
} else {
|
||||||
// Update business data (excluding email config fields)
|
// Update business data (excluding email config fields)
|
||||||
const businessData = {
|
const businessData = {
|
||||||
@@ -350,19 +361,31 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
...businessData,
|
...businessData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update email configuration separately if any email fields are provided
|
// Only update email configuration if there are actual changes or new values
|
||||||
if (
|
const currentApiKey = emailConfig?.hasApiKey ? "EXISTING" : "";
|
||||||
formData.resendApiKey ||
|
const currentDomain = emailConfig?.resendDomain ?? "";
|
||||||
formData.resendDomain ||
|
const currentFromName = emailConfig?.emailFromName ?? "";
|
||||||
formData.emailFromName
|
|
||||||
) {
|
const newApiKey = formData.resendApiKey.trim();
|
||||||
|
const newDomain = formData.resendDomain.trim();
|
||||||
|
const newFromName = formData.emailFromName.trim();
|
||||||
|
|
||||||
|
const hasEmailChanges =
|
||||||
|
(newApiKey && newApiKey !== currentApiKey) ||
|
||||||
|
newDomain !== currentDomain ||
|
||||||
|
newFromName !== currentFromName;
|
||||||
|
|
||||||
|
if (hasEmailChanges) {
|
||||||
await updateEmailConfig.mutateAsync({
|
await updateEmailConfig.mutateAsync({
|
||||||
id: businessId!,
|
id: businessId!,
|
||||||
resendApiKey: formData.resendApiKey || undefined,
|
resendApiKey: newApiKey || undefined,
|
||||||
resendDomain: formData.resendDomain || undefined,
|
resendDomain: newDomain || undefined,
|
||||||
emailFromName: formData.emailFromName || undefined,
|
emailFromName: newFromName || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.success("Business updated successfully");
|
||||||
|
router.push("/dashboard/businesses");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@@ -667,7 +690,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Current Status */}
|
{/* Current Status */}
|
||||||
{mode === "edit" && (
|
{mode === "edit" && (
|
||||||
<div className="flex items-center justify-between bg-gray-50 p-4">
|
<div className="bg-muted/50 flex items-center justify-between p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Current Status:
|
Current Status:
|
||||||
@@ -688,7 +711,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{emailConfig?.resendDomain && (
|
{emailConfig?.resendDomain && (
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-muted-foreground text-sm">
|
||||||
Domain: {emailConfig.resendDomain}
|
Domain: {emailConfig.resendDomain}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -742,7 +765,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
mode === "edit" && emailConfig?.hasApiKey
|
mode === "edit" && emailConfig?.hasApiKey
|
||||||
? "Enter new API key to update"
|
? "••••••••••••••••••••••••••••••••"
|
||||||
: "re_..."
|
: "re_..."
|
||||||
}
|
}
|
||||||
className={
|
className={
|
||||||
|
|||||||
@@ -108,19 +108,41 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
.input(createClientSchema)
|
.input(createClientSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
// Clean up empty strings to null, but preserve required fields
|
|
||||||
const cleanInput = Object.fromEntries(
|
|
||||||
Object.entries(input).map(([key, value]) => [
|
|
||||||
key,
|
|
||||||
value === "" ? null : value,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [client] = await ctx.db
|
const [client] = await ctx.db
|
||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values({
|
||||||
name: input.name, // Ensure name is included
|
name: input.name.trim(),
|
||||||
...cleanInput,
|
email:
|
||||||
|
input.email && input.email.trim() !== ""
|
||||||
|
? input.email.trim()
|
||||||
|
: null,
|
||||||
|
phone:
|
||||||
|
input.phone && input.phone.trim() !== ""
|
||||||
|
? input.phone.trim()
|
||||||
|
: null,
|
||||||
|
addressLine1:
|
||||||
|
input.addressLine1 && input.addressLine1.trim() !== ""
|
||||||
|
? input.addressLine1.trim()
|
||||||
|
: null,
|
||||||
|
addressLine2:
|
||||||
|
input.addressLine2 && input.addressLine2.trim() !== ""
|
||||||
|
? input.addressLine2.trim()
|
||||||
|
: null,
|
||||||
|
city:
|
||||||
|
input.city && input.city.trim() !== "" ? input.city.trim() : null,
|
||||||
|
state:
|
||||||
|
input.state && input.state.trim() !== ""
|
||||||
|
? input.state.trim()
|
||||||
|
: null,
|
||||||
|
postalCode:
|
||||||
|
input.postalCode && input.postalCode.trim() !== ""
|
||||||
|
? input.postalCode.trim()
|
||||||
|
: null,
|
||||||
|
country:
|
||||||
|
input.country && input.country.trim() !== ""
|
||||||
|
? input.country.trim()
|
||||||
|
: null,
|
||||||
|
defaultHourlyRate: input.defaultHourlyRate ?? null,
|
||||||
createdById: ctx.session.user.id,
|
createdById: ctx.session.user.id,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@@ -168,18 +190,59 @@ export const clientsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up empty strings to null
|
|
||||||
const cleanData = Object.fromEntries(
|
|
||||||
Object.entries(data).map(([key, value]) => [
|
|
||||||
key,
|
|
||||||
value === "" ? null : value,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [updatedClient] = await ctx.db
|
const [updatedClient] = await ctx.db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
...cleanData,
|
name: data.name ? data.name.trim() : undefined,
|
||||||
|
email:
|
||||||
|
data.email !== undefined
|
||||||
|
? data.email && data.email.trim() !== ""
|
||||||
|
? data.email.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
phone:
|
||||||
|
data.phone !== undefined
|
||||||
|
? data.phone && data.phone.trim() !== ""
|
||||||
|
? data.phone.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
addressLine1:
|
||||||
|
data.addressLine1 !== undefined
|
||||||
|
? data.addressLine1 && data.addressLine1.trim() !== ""
|
||||||
|
? data.addressLine1.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
addressLine2:
|
||||||
|
data.addressLine2 !== undefined
|
||||||
|
? data.addressLine2 && data.addressLine2.trim() !== ""
|
||||||
|
? data.addressLine2.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
city:
|
||||||
|
data.city !== undefined
|
||||||
|
? data.city && data.city.trim() !== ""
|
||||||
|
? data.city.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
state:
|
||||||
|
data.state !== undefined
|
||||||
|
? data.state && data.state.trim() !== ""
|
||||||
|
? data.state.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
postalCode:
|
||||||
|
data.postalCode !== undefined
|
||||||
|
? data.postalCode && data.postalCode.trim() !== ""
|
||||||
|
? data.postalCode.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
country:
|
||||||
|
data.country !== undefined
|
||||||
|
? data.country && data.country.trim() !== ""
|
||||||
|
? data.country.trim()
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
|
defaultHourlyRate: data.defaultHourlyRate ?? undefined,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(clients.id, id))
|
.where(eq(clients.id, id))
|
||||||
|
|||||||
Reference in New Issue
Block a user