mirror of
				https://github.com/i701/sarlink-portal.git
				synced 2025-10-30 21:37:00 +00:00 
			
		
		
		
	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
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build and Push Docker Images / Build and Push Docker Images (push) Failing after 1m39s
				
			This commit is contained in:
		| @@ -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> | ||||
|  | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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: "", | ||||
|   | ||||
| @@ -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> | ||||
| 	); | ||||
|   | ||||
| @@ -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 "{query} | ||||
| 												Showing {meta?.total} devices for "{query} | ||||
| 												" | ||||
| 											</p> | ||||
| 										)} | ||||
|   | ||||
| @@ -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 " | ||||
| 												Showing {payments?.data?.length} payments for " | ||||
| 												{query} | ||||
| 												" | ||||
| 											</p> | ||||
|   | ||||
| @@ -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", | ||||
| 				}, | ||||
|   | ||||
| @@ -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"; | ||||
|  | ||||
|   | ||||
| @@ -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"); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user