mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-04-20 03:50:20 +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:
parent
0d578c9add
commit
6365a701ba
@ -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>
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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: "",
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 "{query}
|
Showing {meta?.total} devices for "{query}
|
||||||
"
|
"
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -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 "
|
Showing {payments?.data?.length} payments for "
|
||||||
{query}
|
{query}
|
||||||
"
|
"
|
||||||
</p>
|
</p>
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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.";
|
export async function blockDevice({
|
||||||
const error = new Error(errorMessage);
|
deviceId,
|
||||||
(error as ApiError & { details?: ApiError }).details = errorData; // Attach the errorData to the error object
|
reason_for_blocking,
|
||||||
throw error;
|
blocked_by,
|
||||||
}
|
}: {
|
||||||
const data = (await response.json()) as SingleDevice;
|
deviceId: string;
|
||||||
revalidatePath("/devices");
|
reason_for_blocking: string;
|
||||||
return data;
|
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;
|
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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user