diff --git a/server/routers/external.ts b/server/routers/external.ts index 0910f07d..b63f982d 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -308,6 +308,13 @@ authenticated.get( resource.getResourceWhitelist ); +authenticated.post( + `/resource/:resourceId/transfer`, + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.updateResource), + resource.transferResource +); + authenticated.post( `/resource/:resourceId/access-token`, verifyResourceAccess, diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 7dbee1bf..6bde1a83 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -16,3 +16,4 @@ export * from "./setResourceWhitelist"; export * from "./getResourceWhitelist"; export * from "./authWithWhitelist"; export * from "./authWithAccessToken"; +export * from "./transferResource"; diff --git a/server/routers/resource/transferResource.ts b/server/routers/resource/transferResource.ts new file mode 100644 index 00000000..91cb9774 --- /dev/null +++ b/server/routers/resource/transferResource.ts @@ -0,0 +1,84 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resources } from "@server/db/schema"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const transferResourceParamsSchema = z + .object({ + resourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()) + }) + .strict(); + +const transferResourceBodySchema = z + .object({ + siteId: z.number().int().positive() + }) + .strict(); + +export async function transferResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = transferResourceParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = transferResourceBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + const { siteId } = parsedBody.data; + + const [updatedResource] = await db + .update(resources) + .set({ siteId }) + .where(eq(resources.resourceId, resourceId)) + .returning(); + + if (!updatedResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + return response(res, { + data: updatedResource, + success: true, + error: false, + message: "Resource transferred successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 6d3e5777..219854f3 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -23,6 +23,7 @@ import { CommandItem, CommandList } from "@/components/ui/command"; +import { cn } from "@app/lib/cn"; import { Popover, @@ -60,7 +61,12 @@ const GeneralFormSchema = z.object({ // siteId: z.number(), }); +const TransferFormSchema = z.object({ + siteId: z.number() +}); + type GeneralFormValues = z.infer; +type TransferFormValues = z.infer; export default function GeneralForm() { const params = useParams(); @@ -76,6 +82,8 @@ export default function GeneralForm() { const [sites, setSites] = useState([]); const [saveLoading, setSaveLoading] = useState(false); const [domainSuffix, setDomainSuffix] = useState(org.org.domain); + const [transferLoading, setTransferLoading] = useState(false); + const [open, setOpen] = useState(false); const form = useForm({ resolver: zodResolver(GeneralFormSchema), @@ -87,6 +95,13 @@ export default function GeneralForm() { mode: "onChange" }); + const transferForm = useForm({ + resolver: zodResolver(TransferFormSchema), + defaultValues: { + siteId: resource.siteId ? Number(resource.siteId) : undefined + } + }); + useEffect(() => { const fetchSites = async () => { const res = await api.get>( @@ -131,6 +146,33 @@ export default function GeneralForm() { .finally(() => setSaveLoading(false)); } + async function onTransfer(data: TransferFormValues) { + setTransferLoading(true); + + api.post(`resource/${resource?.resourceId}/transfer`, { + siteId: data.siteId + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to transfer resource", + description: formatAxiosError( + e, + "An error occurred while transferring the resource" + ) + }); + }) + .then(() => { + toast({ + title: "Resource transferred", + description: + "The resource has been transferred successfully" + }); + router.refresh(); + }) + .finally(() => setTransferLoading(false)); + } + return ( @@ -212,6 +254,132 @@ export default function GeneralForm() { + + + + + Transfer Resource + + + Transfer this resource to a different site + + + + + +
+ + ( + + + Destination Site + + + + + + + + + + + + No sites found. + + + {sites.map( + (site) => ( + { + transferForm.setValue( + "siteId", + site.siteId + ); + setOpen( + false + ); + }} + > + { + site.name + } + + + ) + )} + + + + + + Select the site you want to + transfer this resource to + + + + )} + /> + + +
+
+ + + + +
); }