feat: enhance error handling and improve API response management across components
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 1m39s

This commit is contained in:
i701 2025-04-14 01:05:07 +05:00
parent 0d578c9add
commit 6365a701ba
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
11 changed files with 111 additions and 66 deletions

View File

@ -1,8 +1,10 @@
import BlockDeviceDialog from "@/components/block-device-dialog"; import BlockDeviceDialog from "@/components/block-device-dialog";
import ClientErrorMessage from "@/components/client-error-message";
import Search from "@/components/search"; import Search from "@/components/search";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { getDevice } from "@/queries/devices"; import { getDevice } from "@/queries/devices";
import { tryCatch } from "@/utils/tryCatch"; import { tryCatch } from "@/utils/tryCatch";
import { redirect } from "next/navigation";
import React from "react"; import React from "react";
export default async function DeviceDetails({ export default async function DeviceDetails({
@ -12,7 +14,15 @@ export default async function DeviceDetails({
}) { }) {
const deviceId = (await params)?.deviceId; const deviceId = (await params)?.deviceId;
const [error, device] = await tryCatch(getDevice({ deviceId: deviceId })); const [error, device] = await tryCatch(getDevice({ deviceId: deviceId }));
if (error) return <div>{error.message}</div>; if (error) {
// Handle specific actions for certain errors, but reuse the error message
if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
// For all other errors, display the error message directly
return <ClientErrorMessage message={error.message} />;
}
}
if (!device) return null; if (!device) return null;
return ( return (
@ -38,11 +48,6 @@ export default async function DeviceDetails({
ACTIVE ACTIVE
</p> </p>
)} )}
<BlockDeviceDialog
device={device}
type={device.blocked ? "unblock" : "block"}
/>
<pre>{JSON.stringify(device.blocked, null, 2)}</pre>
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@ export default function LoginForm() {
return ( return (
<form <form
className="bg-white overflow-clip dark:bg-transparent dark:border-2 w-full max-w-xs mx-auto rounded-lg shadow mt-4" className="overflow-clip title-bg dark:border-2 w-full max-w-xs mx-auto rounded-lg shadow border mt-4"
action={formAction} action={formAction}
> >
<div className="py-4 px-4"> <div className="py-4 px-4">
@ -23,7 +23,6 @@ export default function LoginForm() {
<PhoneInput <PhoneInput
id="phone-number" id="phone-number"
name="phoneNumber" name="phoneNumber"
className="b0rder"
maxLength={8} maxLength={8}
disabled={isPending} disabled={isPending}
placeholder="Enter phone number" placeholder="Enter phone number"

View File

@ -57,11 +57,11 @@ export default function VerifyOTPForm({
return ( return (
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full max-w-xs rounded-lg shadow my-4" className="w-full max-w-xs title-bg border rounded-lg shadow my-4"
> >
<div className="grid pb-4 pt-4 gap-4 px-4"> <div className="grid pb-4 pt-4 gap-4 px-4">
<div className=""> <div className="flex flex-col gap-4">
<Label htmlFor="otp-number" className="text-gray-500"> <Label htmlFor="otp-number" className="sr-only text-gray-500">
Enter the OTP Enter the OTP
</Label> </Label>
<Input <Input
@ -69,6 +69,8 @@ export default function VerifyOTPForm({
id="otp-number" id="otp-number"
{...register("pin")} {...register("pin")}
type="text" type="text"
placeholder="Enter OTP"
className="bg-white dark:bg-sarLinkOrange/10"
/> />
{errors.pin && ( {errors.pin && (
<p className="text-red-500 text-sm">{errors.pin.message}</p> <p className="text-red-500 text-sm">{errors.pin.message}</p>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { blockDevice } from "@/actions/omada-actions"; import { blockDevice as BlockDeviceFromOmada } from "@/actions/omada-actions";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -13,6 +13,8 @@ import {
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import type { Device } from "@/lib/backend-types"; import type { Device } from "@/lib/backend-types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { blockDevice } from "@/queries/devices";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { OctagonX } from "lucide-react"; import { OctagonX } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@ -46,11 +48,9 @@ export default function BlockDeviceDialog({
console.log(data); console.log(data);
toast.promise( toast.promise(
blockDevice({ blockDevice({
macAddress: device.mac, deviceId: String(device.id),
type: type, reason_for_blocking: data.reasonForBlocking,
reason: data.reasonForBlocking, blocked_by: "ADMIN",
blockedBy: "ADMIN",
// reason: data.reasonForBlocking,
}), }),
{ {
loading: "Blocking...", loading: "Blocking...",
@ -75,7 +75,7 @@ export default function BlockDeviceDialog({
onClick={() => { onClick={() => {
setDisabled(true); setDisabled(true);
toast.promise( toast.promise(
blockDevice({ BlockDeviceFromOmada({
macAddress: device.mac, macAddress: device.mac,
type: "unblock", type: "unblock",
reason: "", reason: "",
@ -104,7 +104,7 @@ export default function BlockDeviceDialog({
onClick={() => { onClick={() => {
setDisabled(true); setDisabled(true);
toast.promise( toast.promise(
blockDevice({ BlockDeviceFromOmada({
macAddress: device.mac, macAddress: device.mac,
type: "block", type: "block",
reason: "", reason: "",

View File

@ -8,6 +8,8 @@ export default function ClientErrorMessage({ message }: { message: string }) {
<div className="bg-white dark:bg-transparent p-6 rounded flex flex-col items-center justify-center gap-4"> <div className="bg-white dark:bg-transparent p-6 rounded flex flex-col items-center justify-center gap-4">
<TriangleAlert color="red" /> <TriangleAlert color="red" />
<h6 className="text-red-500 text-sm font-semibold">{message}</h6> <h6 className="text-red-500 text-sm font-semibold">{message}</h6>
{message === "You do not have permission to perform this action." && (
<>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Please contact the administrator to give you permissions. Please contact the administrator to give you permissions.
</span> </span>
@ -16,6 +18,8 @@ export default function ClientErrorMessage({ message }: { message: string }) {
<Phone /> 919-8026 <Phone /> 919-8026
</Button> </Button>
</Link> </Link>
</>
)}
</div> </div>
</div> </div>
); );

View File

@ -40,9 +40,12 @@ export async function DevicesTable({
getDevices({ query: query, limit: limit, offset: offset }), getDevices({ query: query, limit: limit, offset: offset }),
); );
if (error) { if (error) {
if (error.message === "Invalid token.") redirect("/auth/signin"); if (error.message === "UNAUTHORIZED") {
redirect("/auth/signin");
} else {
return <ClientErrorMessage message={error.message} />; return <ClientErrorMessage message={error.message} />;
} }
}
const { meta, data } = devices; const { meta, data } = devices;
return ( return (
<div> <div>
@ -77,7 +80,7 @@ export async function DevicesTable({
<TableCell colSpan={2}> <TableCell colSpan={2}>
{query?.length > 0 && ( {query?.length > 0 && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Showing {meta?.total} locations for &quot;{query} Showing {meta?.total} devices for &quot;{query}
&quot; &quot;
</p> </p>
)} )}

View File

@ -191,7 +191,7 @@ export async function PaymentsTable({
<TableCell colSpan={2}> <TableCell colSpan={2}>
{query.length > 0 && ( {query.length > 0 && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Showing {payments?.data?.length} locations for &quot; Showing {payments?.data?.length} payments for &quot;
{query} {query}
&quot; &quot;
</p> </p>

View File

@ -64,13 +64,13 @@ export async function AppSidebar({
children: [ children: [
{ {
title: "Devices", title: "Devices",
link: "/devices", link: "/devices?page=1",
perm_identifier: "device", perm_identifier: "device",
icon: <Smartphone size={16} />, icon: <Smartphone size={16} />,
}, },
{ {
title: "Payments", title: "Payments",
link: "/payments", link: "/payments?page=1",
icon: <CreditCard size={16} />, icon: <CreditCard size={16} />,
perm_identifier: "payment", perm_identifier: "payment",
}, },

View File

@ -61,7 +61,7 @@ const InputComponent = React.forwardRef<
HTMLInputElement, HTMLInputElement,
React.ComponentProps<"input"> React.ComponentProps<"input">
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<Input className={cn("mx-2", className)} {...props} ref={ref} /> <Input className={cn("mx-2 bg-white/10", className)} {...props} ref={ref} />
)); ));
InputComponent.displayName = "InputComponent"; InputComponent.displayName = "InputComponent";

View File

@ -3,6 +3,7 @@
import { authOptions } from "@/app/auth"; import { authOptions } from "@/app/auth";
import type { ApiError, ApiResponse, Device } from "@/lib/backend-types"; import type { ApiError, ApiResponse, Device } from "@/lib/backend-types";
import { checkSession } from "@/utils/session"; import { checkSession } from "@/utils/session";
import { handleApiResponse } from "@/utils/tryCatch";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
@ -27,17 +28,7 @@ export async function getDevices({ query, offset, limit }: GetDevicesProps) {
}, },
); );
if (!response.ok) { return handleApiResponse<ApiResponse<Device>>(response, "getDevices");
const errorData = (await response.json()) as ApiError;
const errorMessage =
errorData.message || errorData.detail || "An error occurred.";
const error = new Error(errorMessage);
(error as ApiError & { details?: ApiError }).details = errorData; // Attach the errorData to the error object
throw error;
}
const data = (await response.json()) as ApiResponse<Device>;
return data;
} }
export async function getDevice({ deviceId }: { deviceId: string }) { export async function getDevice({ deviceId }: { deviceId: string }) {
@ -52,16 +43,7 @@ export async function getDevice({ deviceId }: { deviceId: string }) {
}, },
}, },
); );
if (!response.ok) { return handleApiResponse<Device>(response, "getDevice");
const errorData = (await response.json()) as ApiError;
const errorMessage =
errorData.message || errorData.detail || "An error occurred.";
const error = new Error(errorMessage);
(error as ApiError & { details?: ApiError }).details = errorData; // Attach the errorData to the error object
throw error;
}
const device = (await response.json()) as Device;
return device;
} }
export async function addDevice({ export async function addDevice({
@ -88,15 +70,35 @@ export async function addDevice({
}), }),
}, },
); );
if (!response.ok) { return handleApiResponse<SingleDevice>(response, "addDevice");
const errorData = (await response.json()) as ApiError;
const errorMessage =
errorData.message || errorData.detail || "An error occurred.";
const error = new Error(errorMessage);
(error as ApiError & { details?: ApiError }).details = errorData; // Attach the errorData to the error object
throw error;
} }
const data = (await response.json()) as SingleDevice;
revalidatePath("/devices"); export async function blockDevice({
return data; deviceId,
reason_for_blocking,
blocked_by,
}: {
deviceId: string;
reason_for_blocking: string;
blocked_by: "ADMIN" | "PARENT";
}) {
const session = await getServerSession(authOptions);
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/devices/${deviceId}/block/`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${session?.apiToken}`,
},
body: JSON.stringify({
blocked: true,
reason_for_blocking: session?.user?.is_superuser
? reason_for_blocking
: "Blocked by parent",
blocked_by: session?.user?.is_superuser ? "ADMIN" : "PARENT",
}),
},
);
return handleApiResponse<Device>(response, "blockDevice");
} }

View File

@ -6,3 +6,33 @@ export async function tryCatch<T, E = Error>(promise: T | Promise<T>) {
return [error as E, null] as const; return [error as E, null] as const;
} }
} }
export async function handleApiResponse<T>(
response: Response,
fnName?: string,
) {
const responseData = await response.json();
if (response.status === 401) {
throw new Error("UNAUTHORIZED");
}
if (response.status === 403) {
throw new Error(
responseData.message ||
"Forbidden; you do not have permission to access this resource.",
);
}
if (response.status === 429) {
throw new Error(
responseData.message || "Too many requests; please try again later.",
);
}
if (!response.ok) {
console.log(`API Error Response from ${fnName}:`, responseData);
throw new Error(responseData.message || "Something went wrong.");
}
return responseData as T;
}