浏览代码

env context and refactor api support different ports

Milo Schwartz 7 月之前
父节点
当前提交
d3d2fe398b
共有 35 个文件被更改,包括 291 次插入139 次删除
  1. 2 1
      .prettierrc
  2. 1 1
      config.example.yml
  3. 59 42
      server/apiServer.ts
  4. 1 0
      server/config.ts
  5. 19 19
      server/index.ts
  6. 1 1
      server/nextServer.ts
  7. 38 18
      src/api/index.ts
  8. 4 1
      src/app/[orgId]/settings/access/roles/components/CreateRoleForm.tsx
  9. 4 1
      src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx
  10. 4 1
      src/app/[orgId]/settings/access/roles/components/RolesTable.tsx
  11. 4 1
      src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx
  12. 4 1
      src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx
  13. 4 1
      src/app/[orgId]/settings/access/users/components/UsersTable.tsx
  14. 4 1
      src/app/[orgId]/settings/components/Header.tsx
  15. 4 1
      src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePasswordForm.tsx
  16. 4 1
      src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx
  17. 4 1
      src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx
  18. 4 3
      src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
  19. 4 1
      src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
  20. 23 20
      src/app/[orgId]/settings/resources/components/CreateResourceForm.tsx
  21. 4 1
      src/app/[orgId]/settings/resources/components/ResourcesTable.tsx
  22. 4 1
      src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
  23. 4 1
      src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx
  24. 4 1
      src/app/[orgId]/settings/sites/components/SitesTable.tsx
  25. 5 2
      src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx
  26. 4 1
      src/app/auth/verify-email/VerifyEmailForm.tsx
  27. 2 2
      src/app/globals.css
  28. 6 3
      src/app/invite/InviteStatusCard.tsx
  29. 16 9
      src/app/layout.tsx
  30. 4 1
      src/app/setup/page.tsx
  31. 4 1
      src/components/ConfirmDeleteDialog.tsx
  32. 10 0
      src/contexts/envContext.ts
  33. 10 0
      src/hooks/useEnvContext.ts
  34. 5 0
      src/lib/types/env.ts
  35. 17 0
      src/providers/EnvProvider.tsx

+ 2 - 1
.prettierrc

@@ -1,4 +1,5 @@
 {
     "tabWidth": 4,
-    "printWidth": 80
+    "printWidth": 80,
+    "trailingComma": "none"
 }

+ 1 - 1
config.example.yml

@@ -37,4 +37,4 @@ email:
     no_reply: no-reply@example.io
 
 flags:
-    require_email_verification: true
+    require_email_verification: true

+ 59 - 42
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",
-      })
-    );
-  }
-
-  // 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);
-
-  // 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;
+    const apiServer = express();
+
+    // 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());
+
+    if (!dev) {
+        apiServer.use(
+            rateLimitMiddleware({
+                windowMin: config.rate_limit.window_minutes,
+                max: config.rate_limit.max_requests,
+                type: "IP_ONLY",
+            }),
+        );
+    }
+
+    // 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);
+
+    // 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;
 }

+ 1 - 0
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 =

+ 19 - 19
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);

+ 1 - 1
server/nextServer.ts

@@ -26,4 +26,4 @@ export async function createNextServer() {
   });
 
   return nextServer;
-}
+}

+ 38 - 18
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 const api = axios.create({
-    baseURL: `${origin}/api/v1`,
-    timeout: 10000,
-    headers: {
-        "Content-Type": "application/json",
-    },
-});
+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;
+}
 
 // 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;

+ 4 - 1
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<z.infer<typeof formSchema>>({
         resolver: zodResolver(formSchema),
         defaultValues: {

+ 4 - 1
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<ListRolesResponse["roles"]>([]);
 
+    const api = createApiClient(useEnvContext());
+
     useEffect(() => {
         async function fetchRoles() {
             const res = await api

+ 4 - 1
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<RoleRow | null>(null);
 
+    const api = createApiClient(useEnvContext());
+
     const { org } = useOrgContext();
     const { toast } = useToast();
 

+ 4 - 1
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);

+ 4 - 1
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<string | null>(null);
     const [loading, setLoading] = useState(false);
     const [expiresInDays, setExpiresInDays] = useState(1);

+ 4 - 1
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();

+ 4 - 1
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(" ");

+ 4 - 1
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<SetPasswordFormValues>({

+ 4 - 1
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<SetPincodeFormValues>({
         resolver: zodResolver(setPincodeFormSchema),
         defaultValues,

+ 4 - 1
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 }[]>(

+ 4 - 3
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<LocalTarget[]>([]);
     const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
     const [sslEnabled, setSslEnabled] = useState(resource.ssl);

+ 4 - 1
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<ListSitesResponse["sites"]>([]);
     const [saveLoading, setSaveLoading] = useState(false);
     const [domainSuffix, setDomainSuffix] = useState(org.org.domain);

+ 23 - 20
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<typeof accountFormSchema>;
@@ -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<AxiosResponse<ListSitesResponse>>(
-                `/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"}
                                                             <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
@@ -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
                                                                             }
                                                                         </CommandItem>
-                                                                    ),
+                                                                    )
                                                                 )}
                                                             </CommandGroup>
                                                         </CommandList>

+ 4 - 1
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] =

+ 4 - 1
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<GeneralFormValues>({

+ 4 - 1
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();

+ 4 - 1
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<SiteRow | null>(null);
 
+    const api = createApiClient(useEnvContext());
+
     const callApi = async () => {
         const res = await api.put<AxiosResponse<any>>(`/newt`);
         console.log(res);

+ 5 - 2
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<boolean>(false);
     const [loadingLogin, setLoadingLogin] = useState(false);
 
+    const api = createApiClient(useEnvContext());
+
     function getDefaultSelectedMethod() {
         if (props.methods.sso) {
             return "sso";

+ 4 - 1
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<z.infer<typeof FormSchema>>({
         resolver: zodResolver(FormSchema),
         defaultValues: {

+ 2 - 2
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%;

+ 6 - 3
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}`);

+ 16 - 9
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}
+                    <EnvProvider
+                        // it's import not to pass all of process.env here in case of secrets
+                        // select only the necessary ones
+                        env={{
+                            NEXT_PORT: process.env.NEXT_PORT as string,
+                            SERVER_EXTERNAL_PORT: process.env
+                                .SERVER_EXTERNAL_PORT as string,
+                            ENVIRONMENT: process.env.ENVIRONMENT as string
+                        }}
+                    >
+                        {children}
+                    </EnvProvider>
                     <Toaster />
                 </ThemeProvider>
             </body>

+ 4 - 1
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`, {

+ 4 - 1
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",

+ 10 - 0
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<EnvContextType | undefined>(undefined);
+
+export default EnvContext;

+ 10 - 0
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;
+}

+ 5 - 0
src/lib/types/env.ts

@@ -0,0 +1,5 @@
+export type env = {
+    SERVER_EXTERNAL_PORT: string;
+    NEXT_PORT: string;
+    ENVIRONMENT: string;
+};

+ 17 - 0
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 (
+        <EnvContext.Provider value={{ env }}>{children}</EnvContext.Provider>
+    );
+}
+
+export default EnvProvider;