diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 6f0a7c9e..1b81e9bd 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import { db } from '@server/db'; -import { resources, roleResources, roles, userResources } from '@server/db/schema'; +import { orgs, resources, roleResources, roles, userResources } from '@server/db/schema'; import response from "@server/utils/response"; import HttpCode from '@server/types/HttpCode'; import createHttpError from 'http-errors'; @@ -10,7 +10,7 @@ import logger from '@server/logger'; import { eq, and } from 'drizzle-orm'; const createResourceParamsSchema = z.object({ - siteId: z.number().int().positive(), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), orgId: z.string() }); @@ -58,8 +58,23 @@ export async function createResource(req: Request, res: Response, next: NextFunc return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have a role')); } + // get the org + const org = await db.select() + .from(orgs) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + if (org.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Organization with ID ${orgId} not found` + ) + ); + } + // Generate a unique resourceId - const resourceId = "subdomain" // TODO: create the subdomain here + const resourceId = `${subdomain}.${org[0].domain}`; // Create new resource in the database const newResource = await db.insert(resources).values({ @@ -70,8 +85,6 @@ export async function createResource(req: Request, res: Response, next: NextFunc subdomain, }).returning(); - - // find the superuser roleId and also add the resource to the superuser role const superuserRole = await db.select() .from(roles) @@ -108,7 +121,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc status: HttpCode.CREATED, }); } catch (error) { - logger.error(error); + throw error; return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred...")); } } diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 7bba2d74..93dc6c93 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -14,6 +14,13 @@ const getResourceSchema = z.object({ resourceId: z.string().uuid() }); +export type GetResourceResponse = { + resourceId: string; + siteId: number; + orgId: string; + name: string; +} + export async function getResource(req: Request, res: Response, next: NextFunction): Promise { try { // Validate request parameters @@ -51,7 +58,12 @@ export async function getResource(req: Request, res: Response, next: NextFunctio } return response(res, { - data: resource[0], + data: { + resourceId: resource[0].resourceId, + siteId: resource[0].siteId, + orgId: resource[0].orgId, + name: resource[0].name + }, success: true, error: false, message: "Resource retrieved successfully", diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 33d96f97..76e7b04b 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -16,7 +16,7 @@ import logger from "@server/logger"; const listResourcesParamsSchema = z .object({ - siteId: z.number().int().positive().optional(), + siteId: z.string().optional().transform(Number).pipe(z.number().int().positive()), orgId: z.string().optional(), }) .refine((data) => !!data.siteId !== !!data.orgId, { diff --git a/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx b/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx new file mode 100644 index 00000000..7e928d95 --- /dev/null +++ b/src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { SidebarNav } from "@app/components/sidebar-nav"; +import { useResourceContext } from "@app/hooks/useResourceContext"; + +const sidebarNavItems = [ + { + title: "General", + href: "/{orgId}/resources/{resourceId}", + }, + // { + // title: "Appearance", + // href: "/{orgId}/resources/{resourceId}/appearance", + // }, + // { + // title: "Notifications", + // href: "/{orgId}/resources/{resourceId}/notifications", + // }, +] + + +export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) { + const { resource } = useResourceContext(); + return (
+
+

+ {isCreate + ? "New Resource" + : resource?.name + " Settings"} +

+

+ {isCreate + ? "Create a new resource" + : "Configure the settings on your resource: " + + resource?.name || ""} + . +

+
+
+ +
+ {children} +
+
+
); +} \ No newline at end of file diff --git a/src/app/[orgId]/resources/[resourceId]/components/CreateResource.tsx b/src/app/[orgId]/resources/[resourceId]/components/CreateResource.tsx new file mode 100644 index 00000000..5b3c4cfa --- /dev/null +++ b/src/app/[orgId]/resources/[resourceId]/components/CreateResource.tsx @@ -0,0 +1,241 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { CalendarIcon, CaretSortIcon, CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { cn } from "@/lib/utils" +import { toast } from "@/hooks/use-toast" +import { Button, buttonVariants } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import React, { useState, useEffect } from "react"; +import { api } from "@/api"; +import { useParams } from "next/navigation"; +import { useRouter } from "next/navigation"; +import { Checkbox } from "@app/components/ui/checkbox" + +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { ListSitesResponse } from "@server/routers/site" +import { AxiosResponse } from "axios" +import CustomDomainInput from "./CustomDomainInput" + +const method = [ + { label: "Wireguard", value: "wg" }, + { label: "Newt", value: "newt" }, +] as const; + +const accountFormSchema = z.object({ + subdomain: z + .string() + .min(2, { + message: "Name must be at least 2 characters.", + }) + .max(30, { + message: "Name must not be longer than 30 characters.", + }), + name: z.string(), + siteId: z.number() +}); + +type AccountFormValues = z.infer; + +const defaultValues: Partial = { + subdomain: "someanimalherefromapi", + name: "My Resource" +}; + +export function CreateResourceForm() { + const params = useParams(); + const orgId = params.orgId; + const router = useRouter(); + + const [sites, setSites] = useState([]); + const [domainSuffix, setDomainSuffix] = useState(".example.com"); + + const form = useForm({ + resolver: zodResolver(accountFormSchema), + defaultValues, + }); + + useEffect(() => { + if (typeof window !== "undefined") { + const fetchSites = async () => { + const res = await api.get>(`/org/${orgId}/sites/`); + setSites(res.data.data.sites); + }; + fetchSites(); + } + }, []); + + async function onSubmit(data: AccountFormValues) { + console.log(data); + + const res = await api + .put(`/org/${orgId}/site/${data.siteId}/resource/`, { + name: data.name, + subdomain: data.subdomain, + // subdomain: data.subdomain, + }) + .catch((e) => { + toast({ + title: "Error creating resource..." + }); + }); + + if (res && res.status === 201) { + const niceId = res.data.data.niceId; + // navigate to the resource page + router.push(`/${orgId}/resources/${niceId}`); + } + } + + return ( + <> +
+ + ( + + Name + + + + + This is the name that will be displayed for this resource. + + + + )} + /> + ( + + Subdomain + + {/* */} + + + + This is the fully qualified domain name that will be used to access the resource. + + + + )} + /> + {/* ( + + Subdomain + + + + + The subdomain of the resource. This will be used to access resources on the resource. + + + + )} + /> */} + ( + + Site + + + + + + + + + + + No site found. + + {sites.map((site) => ( + { + form.setValue("siteId", site.siteId) + }} + > + + {site.name} + + ))} + + + + + + + This is the site that will be used in the dashboard. + + + + )} + /> + + + + + + ); +} diff --git a/src/app/[orgId]/resources/[resourceId]/components/CustomDomainInput.tsx b/src/app/[orgId]/resources/[resourceId]/components/CustomDomainInput.tsx new file mode 100644 index 00000000..8f787db0 --- /dev/null +++ b/src/app/[orgId]/resources/[resourceId]/components/CustomDomainInput.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as React from "react" +import { Input } from "@/components/ui/input" + +interface CustomDomainInputProps { + domainSuffix: string + placeholder?: string + onChange?: (value: string) => void +} + +export default function CustomDomainInput({ + domainSuffix, + placeholder = "Enter subdomain", + onChange +}: CustomDomainInputProps = { + domainSuffix: ".example.com" +}) { + const [value, setValue] = React.useState("") + + const handleChange = (event: React.ChangeEvent) => { + const newValue = event.target.value + setValue(newValue) + if (onChange) { + onChange(newValue) + } + } + + return ( +
+
+ +
+ {domainSuffix} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/[orgId]/resources/[resourceId]/components/GeneralForm.tsx b/src/app/[orgId]/resources/[resourceId]/components/GeneralForm.tsx new file mode 100644 index 00000000..d2fd5f26 --- /dev/null +++ b/src/app/[orgId]/resources/[resourceId]/components/GeneralForm.tsx @@ -0,0 +1,174 @@ +"use client" + +import Link from "next/link" +import { zodResolver } from "@hookform/resolvers/zod" +import { useFieldArray, useForm } from "react-hook-form" +import { z } from "zod" + +import { cn } from "@/lib/utils" +import { toast } from "@/hooks/use-toast" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { useSiteContext } from "@app/hooks/useSiteContext" +import api from "@app/api" + +const GeneralFormSchema = z.object({ + name: z.string() + // email: z + // .string({ + // required_error: "Please select an email to display.", + // }) + // .email(), + // bio: z.string().max(160).min(4), + // urls: z + // .array( + // z.object({ + // value: z.string().url({ message: "Please enter a valid URL." }), + // }) + // ) + // .optional(), +}) + +type GeneralFormValues = z.infer + +export function GeneralForm() { + const { site, updateSite } = useSiteContext(); + + const form = useForm({ + resolver: zodResolver(GeneralFormSchema), + defaultValues: { + name: site?.name + }, + mode: "onChange", + }) + + // const { fields, append } = useFieldArray({ + // name: "urls", + // control: form.control, + // }) + + async function onSubmit(data: GeneralFormValues) { + await updateSite({ name: data.name }); + } + + return ( +
+ + ( + + Name + + + + + This is the display name of the site. + + + + )} + /> + {/* ( + + Email + + + You can manage verified email addresses in your{" "} + email settings. + + + + )} + /> + ( + + Bio + +