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 ClientErrorMessage from "@/components/client-error-message";
import Search from "@/components/search";
import { Badge } from "@/components/ui/badge";
import { getDevice } from "@/queries/devices";
import { tryCatch } from "@/utils/tryCatch";
import { redirect } from "next/navigation";
import React from "react";
export default async function DeviceDetails({
@ -12,7 +14,15 @@ export default async function DeviceDetails({
}) {
const deviceId = (await params)?.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;
return (
@ -38,11 +48,6 @@ export default async function DeviceDetails({
ACTIVE
</p>
)}
<BlockDeviceDialog
device={device}
type={device.blocked ? "unblock" : "block"}
/>
<pre>{JSON.stringify(device.blocked, null, 2)}</pre>
</div>
</div>

View File

@ -15,7 +15,7 @@ export default function LoginForm() {
return (
<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}
>
<div className="py-4 px-4">
@ -23,7 +23,6 @@ export default function LoginForm() {
<PhoneInput
id="phone-number"
name="phoneNumber"
className="b0rder"
maxLength={8}
disabled={isPending}
placeholder="Enter phone number"

View File

@ -57,11 +57,11 @@ export default function VerifyOTPForm({
return (
<form
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="">
<Label htmlFor="otp-number" className="text-gray-500">
<div className="flex flex-col gap-4">
<Label htmlFor="otp-number" className="sr-only text-gray-500">
Enter the OTP
</Label>
<Input
@ -69,6 +69,8 @@ export default function VerifyOTPForm({
id="otp-number"
{...register("pin")}
type="text"
placeholder="Enter OTP"
className="bg-white dark:bg-sarLinkOrange/10"
/>
{errors.pin && (
<p className="text-red-500 text-sm">{errors.pin.message}</p>

View File

@ -1,6 +1,6 @@
"use client";
import { blockDevice } from "@/actions/omada-actions";
import { blockDevice as BlockDeviceFromOmada } from "@/actions/omada-actions";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -13,6 +13,8 @@ import {
import { Label } from "@/components/ui/label";
import type { Device } from "@/lib/backend-types";
import { cn } from "@/lib/utils";
import { blockDevice } from "@/queries/devices";
import { zodResolver } from "@hookform/resolvers/zod";
import { OctagonX } from "lucide-react";
import { useState } from "react";
@ -46,11 +48,9 @@ export default function BlockDeviceDialog({
console.log(data);
toast.promise(
blockDevice({
macAddress: device.mac,
type: type,
reason: data.reasonForBlocking,
blockedBy: "ADMIN",
// reason: data.reasonForBlocking,
deviceId: String(device.id),
reason_for_blocking: data.reasonForBlocking,
blocked_by: "ADMIN",
}),
{
loading: "Blocking...",
@ -75,7 +75,7 @@ export default function BlockDeviceDialog({
onClick={() => {
setDisabled(true);
toast.promise(
blockDevice({
BlockDeviceFromOmada({
macAddress: device.mac,
type: "unblock",
reason: "",
@ -104,7 +104,7 @@ export default function BlockDeviceDialog({
onClick={() => {
setDisabled(true);
toast.promise(
blockDevice({
BlockDeviceFromOmada({
macAddress: device.mac,
type: "block",
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">
<TriangleAlert color="red" />
<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">
Please contact the administrator to give you permissions.
</span>
@ -16,6 +18,8 @@ export default function ClientErrorMessage({ message }: { message: string }) {
<Phone /> 919-8026
</Button>
</Link>
</>
)}
</div>
</div>
);

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@ const InputComponent = React.forwardRef<
HTMLInputElement,
React.ComponentProps<"input">
>(({ 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";

View File

@ -3,6 +3,7 @@
import { authOptions } from "@/app/auth";
import type { ApiError, ApiResponse, Device } from "@/lib/backend-types";
import { checkSession } from "@/utils/session";
import { handleApiResponse } from "@/utils/tryCatch";
import { getServerSession } from "next-auth";
import { revalidatePath } from "next/cache";
@ -27,17 +28,7 @@ export async function getDevices({ query, offset, limit }: GetDevicesProps) {
},
);
if (!response.ok) {
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;
return handleApiResponse<ApiResponse<Device>>(response, "getDevices");
}
export async function getDevice({ deviceId }: { deviceId: string }) {
@ -52,16 +43,7 @@ export async function getDevice({ deviceId }: { deviceId: string }) {
},
},
);
if (!response.ok) {
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;
return handleApiResponse<Device>(response, "getDevice");
}
export async function addDevice({
@ -88,15 +70,35 @@ export async function addDevice({
}),
},
);
if (!response.ok) {
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;
return handleApiResponse<SingleDevice>(response, "addDevice");
}
const data = (await response.json()) as SingleDevice;
revalidatePath("/devices");
return data;
export async function blockDevice({
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;
}
}
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;
}