diff --git a/.prettierrc b/.prettierrc index e967149..6a830f2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "tabWidth": 4, - "printWidth": 80 + "printWidth": 80, + "trailingComma": "none" } diff --git a/config.example.yml b/config.example.yml index eb5ee1e..5f39206 100644 --- a/config.example.yml +++ b/config.example.yml @@ -37,4 +37,4 @@ email: no_reply: no-reply@example.io flags: - require_email_verification: true \ No newline at end of file + require_email_verification: true diff --git a/server/apiServer.ts b/server/apiServer.ts index 6a45a09..a72fa3e 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -3,54 +3,71 @@ import cors from "cors"; import cookieParser from "cookie-parser"; import config from "@server/config"; import logger from "@server/logger"; -import { errorHandlerMiddleware, notFoundMiddleware, rateLimitMiddleware } from "@server/middlewares"; +import { + errorHandlerMiddleware, + notFoundMiddleware, + rateLimitMiddleware, +} from "@server/middlewares"; import { authenticated, unauthenticated } from "@server/routers/external"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { logIncomingMiddleware } from "./middlewares/logIncoming"; +import helmet from "helmet"; const dev = process.env.ENVIRONMENT !== "prod"; const externalPort = config.server.external_port; export function createApiServer() { - const apiServer = express(); - - // Middleware setup - apiServer.set("trust proxy", 1); - apiServer.use(cors()); - apiServer.use(cookieParser()); - apiServer.use(express.json()); - - if (!dev) { - apiServer.use( - rateLimitMiddleware({ - windowMin: config.rate_limit.window_minutes, - max: config.rate_limit.max_requests, - type: "IP_ONLY", - }) - ); - } + const apiServer = express(); - // API routes - const prefix = `/api/v1`; - apiServer.use(logIncomingMiddleware); - apiServer.use(prefix, unauthenticated); - apiServer.use(prefix, authenticated); - - // WebSocket routes - apiServer.use(prefix, wsRouter); - - // Error handling - apiServer.use(notFoundMiddleware); - apiServer.use(errorHandlerMiddleware); + // Middleware setup + apiServer.set("trust proxy", 1); + if (dev) { + apiServer.use( + cors({ + origin: `http://localhost:${config.server.next_port}`, + credentials: true, + }), + ); + } else { + apiServer.use(cors()); + apiServer.use(helmet()); + } + apiServer.use(cookieParser()); + apiServer.use(express.json()); - // Create HTTP server - const httpServer = apiServer.listen(externalPort, (err?: any) => { - if (err) throw err; - logger.info(`API server is running on http://localhost:${externalPort}`); - }); + if (!dev) { + apiServer.use( + rateLimitMiddleware({ + windowMin: config.rate_limit.window_minutes, + max: config.rate_limit.max_requests, + type: "IP_ONLY", + }), + ); + } - // Handle WebSocket upgrades - handleWSUpgrade(httpServer); + // API routes + const prefix = `/api/v1`; + apiServer.use(logIncomingMiddleware); + apiServer.use(prefix, unauthenticated); + apiServer.use(prefix, authenticated); - return httpServer; + // WebSocket routes + apiServer.use(prefix, wsRouter); + + // Error handling + apiServer.use(notFoundMiddleware); + apiServer.use(errorHandlerMiddleware); + + // Create HTTP server + const httpServer = apiServer.listen(externalPort, (err?: any) => { + if (err) throw err; + logger.info( + `API server is running on http://localhost:${externalPort}`, + ); + }); + + // Handle WebSocket upgrades + handleWSUpgrade(httpServer); + + return httpServer; } diff --git a/server/config.ts b/server/config.ts index b64a402..eeef687 100644 --- a/server/config.ts +++ b/server/config.ts @@ -124,6 +124,7 @@ if (!parsedConfig.success) { throw new Error(`Invalid configuration file: ${errors}`); } +process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString(); process.env.SERVER_EXTERNAL_PORT = parsedConfig.data.server.external_port.toString(); process.env.SERVER_INTERNAL_PORT = diff --git a/server/index.ts b/server/index.ts index 56e985f..0e11a3a 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,31 +5,31 @@ import { createInternalServer } from "./internalServer"; import { User, UserOrg } from "./db/schema"; async function startServers() { - await ensureActions(); - - // Start all servers - const apiServer = createApiServer(); - const nextServer = await createNextServer(); - const internalServer = createInternalServer(); + await ensureActions(); - return { - apiServer, - nextServer, - internalServer - }; + // Start all servers + const apiServer = createApiServer(); + const nextServer = await createNextServer(); + const internalServer = createInternalServer(); + + return { + apiServer, + nextServer, + internalServer, + }; } // Types declare global { - namespace Express { - interface Request { - user?: User; - userOrg?: UserOrg; - userOrgRoleId?: number; - userOrgId?: string; - userOrgIds?: string[]; + namespace Express { + interface Request { + user?: User; + userOrg?: UserOrg; + userOrgRoleId?: number; + userOrgId?: string; + userOrgIds?: string[]; + } } - } } startServers().catch(console.error); diff --git a/server/nextServer.ts b/server/nextServer.ts index d858463..f32b262 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -26,4 +26,4 @@ export async function createNextServer() { }); return nextServer; -} \ No newline at end of file +} diff --git a/src/api/index.ts b/src/api/index.ts index d0c687f..306cc1b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,33 +1,53 @@ -import axios from "axios"; +import { env } from "@app/lib/types/env"; +import axios, { AxiosInstance } from "axios"; -let origin; -if (typeof window !== "undefined") { - origin = window.location.origin; +let apiInstance: AxiosInstance | null = null; + +export function createApiClient({ env }: { env: env }): AxiosInstance { + if (apiInstance) { + return apiInstance; + } + + let baseURL; + const suffix = "api/v1"; + + if (window.location.port === env.NEXT_PORT) { + // this means the user is addressing the server directly + baseURL = `${window.location.protocol}//${window.location.hostname}:${env.SERVER_EXTERNAL_PORT}/${suffix}`; + axios.defaults.withCredentials = true; + } else { + // user is accessing through a proxy + baseURL = window.location.origin + `/${suffix}`; + } + + if (!baseURL) { + throw new Error("Failed to create api client, invalid environment"); + } + + apiInstance = axios.create({ + baseURL, + timeout: 10000, + headers: { + "Content-Type": "application/json" + } + }); + + return apiInstance; } -export const api = axios.create({ - baseURL: `${origin}/api/v1`, - timeout: 10000, - headers: { - "Content-Type": "application/json", - }, -}); - // we can pull from env var here becuase it is only used in the server export const internal = axios.create({ baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`, timeout: 10000, headers: { - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } }); export const priv = axios.create({ baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`, timeout: 10000, headers: { - "Content-Type": "application/json", - }, + "Content-Type": "application/json" + } }); - -export default api; diff --git a/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx b/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx index b518872..dca58ae 100644 --- a/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button } from "@app/components/ui/button"; import { Form, @@ -30,6 +29,8 @@ import { import { useOrgContext } from "@app/hooks/useOrgContext"; import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type CreateRoleFormProps = { open: boolean; @@ -52,6 +53,8 @@ export default function CreateRoleForm({ const [loading, setLoading] = useState(false); + const api = createApiClient(useEnvContext()); + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { diff --git a/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx b/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx index a5c6f07..fb946c1 100644 --- a/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button } from "@app/components/ui/button"; import { Form, @@ -37,6 +36,8 @@ import { } from "@app/components/ui/select"; import { RoleRow } from "./RolesTable"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type CreateRoleFormProps = { open: boolean; @@ -61,6 +62,8 @@ export default function DeleteRoleForm({ const [loading, setLoading] = useState(false); const [roles, setRoles] = useState([]); + const api = createApiClient(useEnvContext()); + useEffect(() => { async function fetchRoles() { const res = await api diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx index f59e8cb..f8d3ed7 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx @@ -11,13 +11,14 @@ import { Button } from "@app/components/ui/button"; import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import api from "@app/api"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { useToast } from "@app/hooks/useToast"; import { RolesDataTable } from "./RolesDataTable"; import { Role } from "@server/db/schema"; import CreateRoleForm from "./CreateRoleForm"; import DeleteRoleForm from "./DeleteRoleForm"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; export type RoleRow = Role; @@ -33,6 +34,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) { const [roleToRemove, setUserToRemove] = useState(null); + const api = createApiClient(useEnvContext()); + const { org } = useOrgContext(); const { toast } = useToast(); diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index bcfc0b4..a89271f 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Form, FormControl, @@ -30,6 +29,8 @@ import { useParams } from "next/navigation"; import { Button } from "@app/components/ui/button"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const formSchema = z.object({ email: z.string().email({ message: "Please enter a valid email" }), @@ -40,6 +41,8 @@ export default function AccessControlsPage() { const { toast } = useToast(); const { orgUser: user } = userOrgUserContext(); + const api = createApiClient(useEnvContext()); + const { orgId } = useParams(); const [loading, setLoading] = useState(false); diff --git a/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx b/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx index 1496926..0a18c97 100644 --- a/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx +++ b/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button } from "@app/components/ui/button"; import { Form, @@ -39,6 +38,8 @@ import { import { useOrgContext } from "@app/hooks/useOrgContext"; import { ListRolesResponse } from "@server/routers/role"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type InviteUserFormProps = { open: boolean; @@ -55,6 +56,8 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { const { toast } = useToast(); const { org } = useOrgContext(); + const api = createApiClient(useEnvContext()); + const [inviteLink, setInviteLink] = useState(null); const [loading, setLoading] = useState(false); const [expiresInDays, setExpiresInDays] = useState(1); diff --git a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx index 0613a7d..23c3eda 100644 --- a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx @@ -14,12 +14,13 @@ import { useState } from "react"; import InviteUserForm from "./InviteUserForm"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useUserContext } from "@app/hooks/useUserContext"; -import api from "@app/api"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { useToast } from "@app/hooks/useToast"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; export type UserRow = { id: string; @@ -42,6 +43,8 @@ export default function UsersTable({ users: u }: UsersTableProps) { const router = useRouter(); + const api = createApiClient(useEnvContext()); + const user = useUserContext(); const { org } = useOrgContext(); const { toast } = useToast(); diff --git a/src/app/[orgId]/settings/components/Header.tsx b/src/app/[orgId]/settings/components/Header.tsx index 35a7f22..11fc68f 100644 --- a/src/app/[orgId]/settings/components/Header.tsx +++ b/src/app/[orgId]/settings/components/Header.tsx @@ -1,6 +1,6 @@ "use client"; -import api from "@app/api"; +import { createApiClient } from "@app/api"; import { Avatar, AvatarFallback } from "@app/components/ui/avatar"; import { Button } from "@app/components/ui/button"; import { @@ -33,6 +33,7 @@ import { SelectTrigger, SelectValue, } from "@app/components/ui/select"; +import { useEnvContext } from "@app/hooks/useEnvContext"; import { useToast } from "@app/hooks/useToast"; import { cn, formatAxiosError } from "@app/lib/utils"; import { ListOrgsResponse } from "@server/routers/org"; @@ -55,6 +56,8 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) { const router = useRouter(); + const api = createApiClient(useEnvContext()); + function getInitials() { if (name) { const [firstName, lastName] = name.split(" "); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx index 521e76c..7bc8f18 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button } from "@app/components/ui/button"; import { Form, @@ -30,6 +29,8 @@ import { import { formatAxiosError } from "@app/lib/utils"; import { AxiosResponse } from "axios"; import { Resource } from "@server/db/schema"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const setPasswordFormSchema = z.object({ password: z.string().min(4).max(100), @@ -56,6 +57,8 @@ export default function SetResourcePasswordForm({ }: SetPasswordFormProps) { const { toast } = useToast(); + const api = createApiClient(useEnvContext()); + const [loading, setLoading] = useState(false); const form = useForm({ diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx index f0d9a5f..3c4c36c 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button } from "@app/components/ui/button"; import { Form, @@ -35,6 +34,8 @@ import { InputOTPGroup, InputOTPSlot, } from "@app/components/ui/input-otp"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const setPincodeFormSchema = z.object({ pincode: z.string().length(6), @@ -63,6 +64,8 @@ export default function SetResourcePincodeForm({ const [loading, setLoading] = useState(false); + const api = createApiClient(useEnvContext()); + const form = useForm({ resolver: zodResolver(setPincodeFormSchema), defaultValues, diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index 6a35152..138deaf 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import api from "@app/api"; import { ListRolesResponse } from "@server/routers/role"; import { useToast } from "@app/hooks/useToast"; import { useOrgContext } from "@app/hooks/useOrgContext"; @@ -36,6 +35,8 @@ import { Binary, Key, ShieldCheck } from "lucide-react"; import SetResourcePasswordForm from "./components/SetResourcePasswordForm"; import { Separator } from "@app/components/ui/separator"; import SetResourcePincodeForm from "./components/SetResourcePincodeForm"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -58,6 +59,8 @@ export default function ResourceAuthenticationPage() { const { resource, updateResource, authInfo, updateAuthInfo } = useResourceContext(); + const api = createApiClient(useEnvContext()); + const [pageLoading, setPageLoading] = useState(true); const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index a9df5ae..9a47e03 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -12,7 +12,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import api from "@app/api"; import { AxiosResponse } from "axios"; import { ListTargetsResponse } from "@server/routers/target/listTargets"; import { useForm } from "react-hook-form"; @@ -49,9 +48,9 @@ import { useToast } from "@app/hooks/useToast"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { ArrayElement } from "@server/types/ArrayElement"; -import { Dot } from "lucide-react"; import { formatAxiosError } from "@app/lib/utils"; -import { Separator } from "@radix-ui/react-separator"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient } from "@app/api"; const addTargetSchema = z.object({ ip: z.string().ip(), @@ -83,6 +82,8 @@ export default function ReverseProxyTargets(props: { const { toast } = useToast(); const { resource, updateResource } = useResourceContext(); + const api = createApiClient(useEnvContext()); + const [targets, setTargets] = useState([]); const [targetsToRemove, setTargetsToRemove] = useState([]); const [sslEnabled, setSslEnabled] = useState(resource.ssl); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index bff69fd..f0301aa 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -33,7 +33,6 @@ import { useResourceContext } from "@app/hooks/useResourceContext"; import { ListSitesResponse } from "@server/routers/site"; import { useEffect, useState } from "react"; import { AxiosResponse } from "axios"; -import api from "@app/api"; import { useParams, useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { GetResourceAuthInfoResponse } from "@server/routers/resource"; @@ -43,6 +42,8 @@ import { useOrgContext } from "@app/hooks/useOrgContext"; import CustomDomainInput from "../components/CustomDomainInput"; import ResourceInfoBox from "../components/ResourceInfoBox"; import { subdomainSchema } from "@server/schemas/subdomainSchema"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const GeneralFormSchema = z.object({ name: z.string(), @@ -61,6 +62,8 @@ export default function GeneralForm() { const orgId = params.orgId; + const api = createApiClient(useEnvContext()); + const [sites, setSites] = useState([]); const [saveLoading, setSaveLoading] = useState(false); const [domainSuffix, setDomainSuffix] = useState(org.org.domain); diff --git a/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx b/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx index 18a5449..6e02aec 100644 --- a/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx +++ b/src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button, buttonVariants } from "@app/components/ui/button"; import { Form, @@ -9,7 +8,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { useToast } from "@app/hooks/useToast"; @@ -25,7 +24,7 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; import { useParams, useRouter } from "next/navigation"; import { ListSitesResponse } from "@server/routers/site"; @@ -34,7 +33,7 @@ import { CheckIcon } from "lucide-react"; import { Popover, PopoverContent, - PopoverTrigger, + PopoverTrigger } from "@app/components/ui/popover"; import { Command, @@ -42,7 +41,7 @@ import { CommandGroup, CommandInput, CommandItem, - CommandList, + CommandList } from "@app/components/ui/command"; import { CaretSortIcon } from "@radix-ui/react-icons"; import CustomDomainInput from "../[resourceId]/components/CustomDomainInput"; @@ -50,11 +49,13 @@ import { Axios, AxiosResponse } from "axios"; import { Resource } from "@server/db/schema"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { subdomainSchema } from "@server/schemas/subdomainSchema"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const accountFormSchema = z.object({ subdomain: subdomainSchema, name: z.string(), - siteId: z.number(), + siteId: z.number() }); type AccountFormValues = z.infer; @@ -66,10 +67,12 @@ type CreateResourceFormProps = { export default function CreateResourceForm({ open, - setOpen, + setOpen }: CreateResourceFormProps) { const { toast } = useToast(); + const api = createApiClient(useEnvContext()); + const [loading, setLoading] = useState(false); const params = useParams(); @@ -85,8 +88,8 @@ export default function CreateResourceForm({ resolver: zodResolver(accountFormSchema), defaultValues: { subdomain: "", - name: "My Resource", - }, + name: "My Resource" + } }); useEffect(() => { @@ -96,7 +99,7 @@ export default function CreateResourceForm({ const fetchSites = async () => { const res = await api.get>( - `/org/${orgId}/sites/`, + `/org/${orgId}/sites/` ); setSites(res.data.data.sites); @@ -116,9 +119,9 @@ export default function CreateResourceForm({ `/org/${orgId}/site/${data.siteId}/resource/`, { name: data.name, - subdomain: data.subdomain, + subdomain: data.subdomain // subdomain: data.subdomain, - }, + } ) .catch((e) => { toast({ @@ -126,8 +129,8 @@ export default function CreateResourceForm({ title: "Error creating resource", description: formatAxiosError( e, - "An error occurred when creating the resource", - ), + "An error occurred when creating the resource" + ) }); }); @@ -198,7 +201,7 @@ export default function CreateResourceForm({ onChange={(value) => form.setValue( "subdomain", - value, + value ) } /> @@ -227,14 +230,14 @@ export default function CreateResourceForm({ className={cn( "w-[350px] justify-between", !field.value && - "text-muted-foreground", + "text-muted-foreground" )} > {field.value ? sites.find( (site) => site.siteId === - field.value, + field.value )?.name : "Select site"} @@ -261,7 +264,7 @@ export default function CreateResourceForm({ onSelect={() => { form.setValue( "siteId", - site.siteId, + site.siteId ); }} > @@ -271,14 +274,14 @@ export default function CreateResourceForm({ site.siteId === field.value ? "opacity-100" - : "opacity-0", + : "opacity-0" )} /> { site.name } - ), + ) )} diff --git a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx index e8dfc64..a1c3003 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx @@ -21,13 +21,14 @@ import { } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import api from "@app/api"; import CreateResourceForm from "./CreateResourceForm"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { set } from "zod"; import { formatAxiosError } from "@app/lib/utils"; import { useToast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; export type ResourceRow = { id: number; @@ -49,6 +50,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { const { toast } = useToast(); + const api = createApiClient(useEnvContext()); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 7ab0ad9..d00bb6f 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -15,11 +15,12 @@ import { import { Input } from "@/components/ui/input"; import { useSiteContext } from "@app/hooks/useSiteContext"; import { useForm } from "react-hook-form"; -import api from "@app/api"; import { useToast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const GeneralFormSchema = z.object({ name: z.string(), @@ -31,6 +32,8 @@ export default function GeneralPage() { const { site, updateSite } = useSiteContext(); const { toast } = useToast(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); const form = useForm({ diff --git a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx index 2460583..eaf6462 100644 --- a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button, buttonVariants } from "@app/components/ui/button"; import { Form, @@ -41,6 +40,8 @@ import { SelectValue, } from "@app/components/ui/select"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const method = [ { label: "Newt", value: "newt" }, @@ -74,6 +75,8 @@ type CreateSiteFormProps = { export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { const { toast } = useToast(); + const api = createApiClient(useEnvContext()); + const [loading, setLoading] = useState(false); const params = useParams(); diff --git a/src/app/[orgId]/settings/sites/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/components/SitesTable.tsx index 1ab6927..fd9dc41 100644 --- a/src/app/[orgId]/settings/sites/components/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesTable.tsx @@ -12,13 +12,14 @@ import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import api from "@app/api"; import { AxiosResponse } from "axios"; import { useState } from "react"; import CreateSiteForm from "./CreateSiteForm"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useToast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; export type SiteRow = { id: number; @@ -44,6 +45,8 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedSite, setSelectedSite] = useState(null); + const api = createApiClient(useEnvContext()); + const callApi = async () => { const res = await api.put>(`/newt`); console.log(res); diff --git a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index 014c850..e2199db 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useSyncExternalStore } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -29,7 +29,6 @@ import { InputOTPGroup, InputOTPSlot, } from "@app/components/ui/input-otp"; -import api from "@app/api"; import { useRouter } from "next/navigation"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { formatAxiosError } from "@app/lib/utils"; @@ -38,6 +37,8 @@ import LoginForm from "@app/components/LoginForm"; import { AuthWithPasswordResponse } from "@server/routers/resource"; import { redirect } from "next/dist/server/api-utils"; import ResourceAccessDenied from "./ResourceAccessDenied"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const pinSchema = z.object({ pin: z @@ -83,6 +84,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const [accessDenied, setAccessDenied] = useState(false); const [loadingLogin, setLoadingLogin] = useState(false); + const api = createApiClient(useEnvContext()); + function getDefaultSelectedMethod() { if (props.methods.sso) { return "sso"; diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index cceddc6..fe603a8 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -27,7 +27,6 @@ import { InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; -import api from "@app/api"; import { AxiosResponse } from "axios"; import { VerifyEmailResponse } from "@server/routers/auth"; import { Loader2 } from "lucide-react"; @@ -35,6 +34,8 @@ import { Alert, AlertDescription } from "../../../components/ui/alert"; import { useToast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; const FormSchema = z.object({ email: z.string().email({ message: "Invalid email address" }), @@ -61,6 +62,8 @@ export default function VerifyEmailForm({ const { toast } = useToast(); + const api = createApiClient(useEnvContext()); + const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { diff --git a/src/app/globals.css b/src/app/globals.css index 9fe9acb..d51f40a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,7 +6,7 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 20 5.0% 10.0%; + --foreground: 0 0.0% 10.0%; --card: 0 0% 100%; --card-foreground: 20 5.0% 10.0%; --popover: 0 0% 100%; @@ -33,7 +33,7 @@ } .dark { - --background: 20 5.0% 10.0%; + --background: 0 0.0% 10.0%; --foreground: 60 9.1% 97.8%; --card: 20 5.0% 10.0%; --card-foreground: 60 9.1% 97.8%; diff --git a/src/app/invite/InviteStatusCard.tsx b/src/app/invite/InviteStatusCard.tsx index ac9e94a..c9417ef 100644 --- a/src/app/invite/InviteStatusCard.tsx +++ b/src/app/invite/InviteStatusCard.tsx @@ -1,14 +1,15 @@ "use client"; -import api from "@app/api"; +import { createApiClient } from "@app/api"; import { Button } from "@app/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, - CardTitle, + CardTitle } from "@app/components/ui/card"; +import { useEnvContext } from "@app/hooks/useEnvContext"; import { XCircle } from "lucide-react"; import { useRouter } from "next/navigation"; @@ -19,10 +20,12 @@ type InviteStatusCardProps = { export default function InviteStatusCard({ type, - token, + token }: InviteStatusCardProps) { const router = useRouter(); + const api = createApiClient(useEnvContext()); + async function goToLogin() { await api.post("/auth/logout", {}); router.push(`/auth/login?redirect=/invite?token=${token}`); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a5ff4f6..0802174 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,23 +1,19 @@ import type { Metadata } from "next"; import "./globals.css"; -import { Figtree, IBM_Plex_Sans, Inter, Work_Sans } from "next/font/google"; +import { Figtree } from "next/font/google"; import { Toaster } from "@/components/ui/toaster"; import { ThemeProvider } from "@app/providers/ThemeProvider"; +import EnvProvider from "@app/providers/EnvProvider"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, - description: "", + description: "" }; -// const font = Inter({ subsets: ["latin"] }); -// const font = Noto_Sans_Mono({ subsets: ["latin"] }); -// const font = Work_Sans({ subsets: ["latin"] }); -// const font = Space_Grotesk({subsets: ["latin"]}) -// const font = IBM_Plex_Sans({subsets: ["latin"], weight: "400"}) const font = Figtree({ subsets: ["latin"] }); export default async function RootLayout({ - children, + children }: Readonly<{ children: React.ReactNode; }>) { @@ -30,7 +26,18 @@ export default async function RootLayout({ enableSystem disableTransitionOnChange > - {children} + + {children} + diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 95b71e0..25c6aa5 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -4,7 +4,6 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import Link from "next/link"; -import api from "@app/api"; import { toast } from "@app/hooks/useToast"; import { useCallback, useEffect, useState } from "react"; import { @@ -16,6 +15,8 @@ import { } from "@app/components/ui/card"; import CopyTextBox from "@app/components/CopyTextBox"; import { formatAxiosError } from "@app/lib/utils"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type Step = "org" | "site" | "resources"; @@ -28,6 +29,8 @@ export default function StepperForm() { const [orgCreated, setOrgCreated] = useState(false); const [orgIdTaken, setOrgIdTaken] = useState(false); + const api = createApiClient(useEnvContext()); + const checkOrgIdAvailability = useCallback(async (value: string) => { try { const res = await api.get(`/org/checkId`, { diff --git a/src/components/ConfirmDeleteDialog.tsx b/src/components/ConfirmDeleteDialog.tsx index 508027d..8ce6b18 100644 --- a/src/components/ConfirmDeleteDialog.tsx +++ b/src/components/ConfirmDeleteDialog.tsx @@ -1,6 +1,5 @@ "use client"; -import api from "@app/api"; import { Button } from "@app/components/ui/button"; import { Form, @@ -42,6 +41,8 @@ import { } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { Description } from "@radix-ui/react-toast"; +import { createApiClient } from "@app/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; type InviteUserFormProps = { open: boolean; @@ -64,6 +65,8 @@ export default function InviteUserForm({ }: InviteUserFormProps) { const [loading, setLoading] = useState(false); + const api = createApiClient(useEnvContext()); + const formSchema = z.object({ string: z.string().refine((val) => val === string, { message: "Invalid confirmation", diff --git a/src/contexts/envContext.ts b/src/contexts/envContext.ts new file mode 100644 index 0000000..810cbd3 --- /dev/null +++ b/src/contexts/envContext.ts @@ -0,0 +1,10 @@ +import { env } from "@app/lib/types/env"; +import { createContext } from "react"; + +interface EnvContextType { + env: env; +} + +const EnvContext = createContext(undefined); + +export default EnvContext; diff --git a/src/hooks/useEnvContext.ts b/src/hooks/useEnvContext.ts new file mode 100644 index 0000000..5670e10 --- /dev/null +++ b/src/hooks/useEnvContext.ts @@ -0,0 +1,10 @@ +import EnvContext from "@app/contexts/envContext"; +import { useContext } from "react"; + +export function useEnvContext() { + const context = useContext(EnvContext); + if (context === undefined) { + throw new Error("useEnvContext must be used within an EnvProvider"); + } + return context; +} diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts new file mode 100644 index 0000000..89ad1b1 --- /dev/null +++ b/src/lib/types/env.ts @@ -0,0 +1,5 @@ +export type env = { + SERVER_EXTERNAL_PORT: string; + NEXT_PORT: string; + ENVIRONMENT: string; +}; diff --git a/src/providers/EnvProvider.tsx b/src/providers/EnvProvider.tsx new file mode 100644 index 0000000..a38ac78 --- /dev/null +++ b/src/providers/EnvProvider.tsx @@ -0,0 +1,17 @@ +"use client"; + +import EnvContext from "@app/contexts/envContext"; +import { env } from "@app/lib/types/env"; + +interface ApiProviderProps { + children: React.ReactNode; + env: env; +} + +export function EnvProvider({ children, env }: ApiProviderProps) { + return ( + {children} + ); +} + +export default EnvProvider;