diff --git a/messages/en-US.json b/messages/en-US.json index c3e93ba8..a60e432c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1896,5 +1896,8 @@ "certResolverDescription": "Select the certificate resolver to use for this resource.", "selectCertResolver": "Select Certificate Resolver", "enterCustomResolver": "Enter Custom Resolver", - "preferWildcardCert": "Prefer Wildcard Certificate" + "preferWildcardCert": "Prefer Wildcard Certificate", + "unverified": "Unverified", + "domainSetting": "DomainSetting", + "domainSettingDescription": "Configure settings for your domain" } diff --git a/server/auth/actions.ts b/server/auth/actions.ts index e48bc502..4e2738e1 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -81,6 +81,7 @@ export enum ActionsEnum { listClients = "listClients", getClient = "getClient", listOrgDomains = "listOrgDomains", + getDomain = "getDomain", createNewt = "createNewt", createIdp = "createIdp", updateIdp = "updateIdp", diff --git a/server/routers/domain/getDomain.ts b/server/routers/domain/getDomain.ts new file mode 100644 index 00000000..77bd18ae --- /dev/null +++ b/server/routers/domain/getDomain.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, domains } from "@server/db"; +import { eq, and } 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"; +import { OpenAPITags, registry } from "@server/openApi"; +import { domain } from "zod/v4/core/regexes"; + +const getDomainSchema = z + .object({ + domainId: z + .string() + .optional(), + orgId: z.string().optional() + }) + .strict(); + +async function query(domainId?: string, orgId?: string) { + if (domainId) { + const [res] = await db + .select() + .from(domains) + .where(eq(domains.domainId, domainId)) + .limit(1); + return res; + } +} + +export type GetDomainResponse = NonNullable>>; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/domain/{domainId}", + description: "Get a domain by domainId.", + tags: [OpenAPITags.Domain], + request: { + params: z.object({ + domainId: z.string(), + orgId: z.string() + }) + }, + responses: {} +}); + +export async function getDomain( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getDomainSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, domainId } = parsedParams.data; + + const domain = await query(domainId, orgId); + + if (!domain) { + return next(createHttpError(HttpCode.NOT_FOUND, "Domain not found")); + } + + return response(res, { + data: domain, + success: true, + error: false, + message: "Domain retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index c0cafafe..e131e6a4 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -1,4 +1,5 @@ export * from "./listDomains"; export * from "./createOrgDomain"; export * from "./deleteOrgDomain"; -export * from "./restartOrgDomain"; \ No newline at end of file +export * from "./restartOrgDomain"; +export * from "./getDomain"; \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 8bd72f62..cadbbad7 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -302,6 +302,13 @@ authenticated.get( domain.listDomains ); +authenticated.get( + "/org/:orgId/domain/:domainId", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getDomain), + domain.getDomain +); + authenticated.get( "/org/:orgId/invitations", verifyOrgAccess, diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx new file mode 100644 index 00000000..1e1fec7b --- /dev/null +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -0,0 +1,50 @@ +import { internal } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { getTranslations } from "next-intl/server"; +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import DomainProvider from "@app/providers/DomainProvider"; +import DomainInfoCard from "@app/components/DomainInfoCard"; + +interface SettingsLayoutProps { + children: React.ReactNode; + params: Promise<{ domainId: string; orgId: string }>; +} + +export default async function SettingsLayout(props: SettingsLayoutProps) { + const params = await props.params; + + const { children } = props; + + let domain = null; + try { + const res = await internal.get>( + `/org/${params.orgId}/domain/${params.domainId}`, + await authCookieHeader() + ); + domain = res.data.data; + console.log(JSON.stringify(domain)); + } catch { + redirect(`/${params.orgId}/settings/domains`); + } + + const t = await getTranslations(); + + + return ( + <> + + + +
+ +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx new file mode 100644 index 00000000..e8187b95 --- /dev/null +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -0,0 +1,8 @@ +import { redirect } from "next/navigation"; + +export default async function DomainPage(props: { + params: Promise<{ orgId: string; domainId: string }>; +}) { + const params = await props.params; + redirect(`/${params.orgId}/settings/domains/${params.domainId}`); +} \ No newline at end of file diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx new file mode 100644 index 00000000..951633f2 --- /dev/null +++ b/src/components/DomainInfoCard.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { InfoIcon } from "lucide-react"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { useTranslations } from "next-intl"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useDomainContext } from "@app/hooks/useDomainContext"; + +type DomainInfoCardProps = {}; + +export default function DomainInfoCard({ }: DomainInfoCardProps) { + const { domain, updateDomain } = useDomainContext(); + const t = useTranslations(); + const { env } = useEnvContext(); + + + return ( + + + + + + {t("type")} + + + + {domain.type} + + + + + + {t("status")} + + + {domain.verified ? ( +
+
+ {t("verified")} +
+ ) : ( +
+
+ {t("unverified")} +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/contexts/domainContext.ts b/src/contexts/domainContext.ts new file mode 100644 index 00000000..d60c4ca4 --- /dev/null +++ b/src/contexts/domainContext.ts @@ -0,0 +1,11 @@ +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import { createContext } from "react"; + +interface DomainContextType { + domain: GetDomainResponse; + updateDomain: (updatedDomain: Partial) => void; +} + +const DomainContext = createContext(undefined); + +export default DomainContext; \ No newline at end of file diff --git a/src/hooks/useDomainContext.ts b/src/hooks/useDomainContext.ts new file mode 100644 index 00000000..36d3840f --- /dev/null +++ b/src/hooks/useDomainContext.ts @@ -0,0 +1,10 @@ +import DomainContext from "@app/contexts/domainContext"; +import { useContext } from "react"; + +export function useDomainContext() { + const context = useContext(DomainContext); + if (context === undefined) { + throw new Error('useDomainContext must be used within a DomainProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/providers/DomainProvider.tsx b/src/providers/DomainProvider.tsx new file mode 100644 index 00000000..9b014449 --- /dev/null +++ b/src/providers/DomainProvider.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import DomainContext from "@app/contexts/domainContext"; + +interface DomainProviderProps { + children: React.ReactNode; + domain: GetDomainResponse; +} + +export function DomainProvider({ + children, + domain: serverDomain +}: DomainProviderProps) { + const [domain, setDomain] = useState(serverDomain); + + const t = useTranslations(); + + const updateDomain = (updatedDomain: Partial) => { + if (!domain) { + throw new Error(t('domainErrorNoUpdate')); + } + setDomain((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + ...updatedDomain + }; + }); + }; + + return ( + + {children} + + ); +} + +export default DomainProvider; \ No newline at end of file