env context and refactor api support different ports

This commit is contained in:
Milo Schwartz 2024-12-12 22:46:58 -05:00
parent d79760dad9
commit d3d2fe398b
No known key found for this signature in database
35 changed files with 287 additions and 135 deletions

View file

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

View file

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

View file

@ -3,54 +3,71 @@ import cors from "cors";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import config from "@server/config"; import config from "@server/config";
import logger from "@server/logger"; 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 { authenticated, unauthenticated } from "@server/routers/external";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod"; const dev = process.env.ENVIRONMENT !== "prod";
const externalPort = config.server.external_port; const externalPort = config.server.external_port;
export function createApiServer() { export function createApiServer() {
const apiServer = express(); 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 // Middleware setup
const prefix = `/api/v1`; apiServer.set("trust proxy", 1);
apiServer.use(logIncomingMiddleware); if (dev) {
apiServer.use(prefix, unauthenticated); apiServer.use(
apiServer.use(prefix, authenticated); cors({
origin: `http://localhost:${config.server.next_port}`,
// WebSocket routes credentials: true,
apiServer.use(prefix, wsRouter); }),
);
// Error handling } else {
apiServer.use(notFoundMiddleware); apiServer.use(cors());
apiServer.use(errorHandlerMiddleware); apiServer.use(helmet());
}
apiServer.use(cookieParser());
apiServer.use(express.json());
// Create HTTP server if (!dev) {
const httpServer = apiServer.listen(externalPort, (err?: any) => { apiServer.use(
if (err) throw err; rateLimitMiddleware({
logger.info(`API server is running on http://localhost:${externalPort}`); windowMin: config.rate_limit.window_minutes,
}); max: config.rate_limit.max_requests,
type: "IP_ONLY",
}),
);
}
// Handle WebSocket upgrades // API routes
handleWSUpgrade(httpServer); 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;
} }

View file

@ -124,6 +124,7 @@ if (!parsedConfig.success) {
throw new Error(`Invalid configuration file: ${errors}`); throw new Error(`Invalid configuration file: ${errors}`);
} }
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
process.env.SERVER_EXTERNAL_PORT = process.env.SERVER_EXTERNAL_PORT =
parsedConfig.data.server.external_port.toString(); parsedConfig.data.server.external_port.toString();
process.env.SERVER_INTERNAL_PORT = process.env.SERVER_INTERNAL_PORT =

View file

@ -5,31 +5,31 @@ import { createInternalServer } from "./internalServer";
import { User, UserOrg } from "./db/schema"; import { User, UserOrg } from "./db/schema";
async function startServers() { async function startServers() {
await ensureActions(); await ensureActions();
// Start all servers
const apiServer = createApiServer();
const nextServer = await createNextServer();
const internalServer = createInternalServer();
return { // Start all servers
apiServer, const apiServer = createApiServer();
nextServer, const nextServer = await createNextServer();
internalServer const internalServer = createInternalServer();
};
return {
apiServer,
nextServer,
internalServer,
};
} }
// Types // Types
declare global { declare global {
namespace Express { namespace Express {
interface Request { interface Request {
user?: User; user?: User;
userOrg?: UserOrg; userOrg?: UserOrg;
userOrgRoleId?: number; userOrgRoleId?: number;
userOrgId?: string; userOrgId?: string;
userOrgIds?: string[]; userOrgIds?: string[];
}
} }
}
} }
startServers().catch(console.error); startServers().catch(console.error);

View file

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

View file

@ -1,33 +1,53 @@
import axios from "axios"; import { env } from "@app/lib/types/env";
import axios, { AxiosInstance } from "axios";
let origin; let apiInstance: AxiosInstance | null = null;
if (typeof window !== "undefined") {
origin = window.location.origin; 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 // we can pull from env var here becuase it is only used in the server
export const internal = axios.create({ export const internal = axios.create({
baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`, baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`,
timeout: 10000, timeout: 10000,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, }
}); });
export const priv = axios.create({ export const priv = axios.create({
baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`, baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`,
timeout: 10000, timeout: 10000,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
}, }
}); });
export default api;

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@ -30,6 +29,8 @@ import {
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role"; import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
type CreateRoleFormProps = { type CreateRoleFormProps = {
open: boolean; open: boolean;
@ -52,6 +53,8 @@ export default function CreateRoleForm({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const api = createApiClient(useEnvContext());
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@ -37,6 +36,8 @@ import {
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { RoleRow } from "./RolesTable"; import { RoleRow } from "./RolesTable";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
type CreateRoleFormProps = { type CreateRoleFormProps = {
open: boolean; open: boolean;
@ -61,6 +62,8 @@ export default function DeleteRoleForm({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [roles, setRoles] = useState<ListRolesResponse["roles"]>([]); const [roles, setRoles] = useState<ListRolesResponse["roles"]>([]);
const api = createApiClient(useEnvContext());
useEffect(() => { useEffect(() => {
async function fetchRoles() { async function fetchRoles() {
const res = await api const res = await api

View file

@ -11,13 +11,14 @@ import { Button } from "@app/components/ui/button";
import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import api from "@app/api";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { RolesDataTable } from "./RolesDataTable"; import { RolesDataTable } from "./RolesDataTable";
import { Role } from "@server/db/schema"; import { Role } from "@server/db/schema";
import CreateRoleForm from "./CreateRoleForm"; import CreateRoleForm from "./CreateRoleForm";
import DeleteRoleForm from "./DeleteRoleForm"; import DeleteRoleForm from "./DeleteRoleForm";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
export type RoleRow = Role; export type RoleRow = Role;
@ -33,6 +34,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null); const [roleToRemove, setUserToRemove] = useState<RoleRow | null>(null);
const api = createApiClient(useEnvContext());
const { org } = useOrgContext(); const { org } = useOrgContext();
const { toast } = useToast(); const { toast } = useToast();

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { import {
Form, Form,
FormControl, FormControl,
@ -30,6 +29,8 @@ import { useParams } from "next/navigation";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email({ message: "Please enter a valid email" }), email: z.string().email({ message: "Please enter a valid email" }),
@ -40,6 +41,8 @@ export default function AccessControlsPage() {
const { toast } = useToast(); const { toast } = useToast();
const { orgUser: user } = userOrgUserContext(); const { orgUser: user } = userOrgUserContext();
const api = createApiClient(useEnvContext());
const { orgId } = useParams(); const { orgId } = useParams();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@ -39,6 +38,8 @@ import {
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
type InviteUserFormProps = { type InviteUserFormProps = {
open: boolean; open: boolean;
@ -55,6 +56,8 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const { org } = useOrgContext(); const { org } = useOrgContext();
const api = createApiClient(useEnvContext());
const [inviteLink, setInviteLink] = useState<string | null>(null); const [inviteLink, setInviteLink] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [expiresInDays, setExpiresInDays] = useState(1); const [expiresInDays, setExpiresInDays] = useState(1);

View file

@ -14,12 +14,13 @@ import { useState } from "react";
import InviteUserForm from "./InviteUserForm"; import InviteUserForm from "./InviteUserForm";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
import api from "@app/api";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
export type UserRow = { export type UserRow = {
id: string; id: string;
@ -42,6 +43,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext());
const user = useUserContext(); const user = useUserContext();
const { org } = useOrgContext(); const { org } = useOrgContext();
const { toast } = useToast(); const { toast } = useToast();

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import api from "@app/api"; import { createApiClient } from "@app/api";
import { Avatar, AvatarFallback } from "@app/components/ui/avatar"; import { Avatar, AvatarFallback } from "@app/components/ui/avatar";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
@ -33,6 +33,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { cn, formatAxiosError } from "@app/lib/utils"; import { cn, formatAxiosError } from "@app/lib/utils";
import { ListOrgsResponse } from "@server/routers/org"; import { ListOrgsResponse } from "@server/routers/org";
@ -55,6 +56,8 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext());
function getInitials() { function getInitials() {
if (name) { if (name) {
const [firstName, lastName] = name.split(" "); const [firstName, lastName] = name.split(" ");

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@ -30,6 +29,8 @@ import {
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { Resource } from "@server/db/schema"; import { Resource } from "@server/db/schema";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const setPasswordFormSchema = z.object({ const setPasswordFormSchema = z.object({
password: z.string().min(4).max(100), password: z.string().min(4).max(100),
@ -56,6 +57,8 @@ export default function SetResourcePasswordForm({
}: SetPasswordFormProps) { }: SetPasswordFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const form = useForm<SetPasswordFormValues>({ const form = useForm<SetPasswordFormValues>({

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@ -35,6 +34,8 @@ import {
InputOTPGroup, InputOTPGroup,
InputOTPSlot, InputOTPSlot,
} from "@app/components/ui/input-otp"; } from "@app/components/ui/input-otp";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const setPincodeFormSchema = z.object({ const setPincodeFormSchema = z.object({
pincode: z.string().length(6), pincode: z.string().length(6),
@ -63,6 +64,8 @@ export default function SetResourcePincodeForm({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const api = createApiClient(useEnvContext());
const form = useForm<SetPincodeFormValues>({ const form = useForm<SetPincodeFormValues>({
resolver: zodResolver(setPincodeFormSchema), resolver: zodResolver(setPincodeFormSchema),
defaultValues, defaultValues,

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import api from "@app/api";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
@ -36,6 +35,8 @@ import { Binary, Key, ShieldCheck } from "lucide-react";
import SetResourcePasswordForm from "./components/SetResourcePasswordForm"; import SetResourcePasswordForm from "./components/SetResourcePasswordForm";
import { Separator } from "@app/components/ui/separator"; import { Separator } from "@app/components/ui/separator";
import SetResourcePincodeForm from "./components/SetResourcePincodeForm"; import SetResourcePincodeForm from "./components/SetResourcePincodeForm";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const UsersRolesFormSchema = z.object({ const UsersRolesFormSchema = z.object({
roles: z.array( roles: z.array(
@ -58,6 +59,8 @@ export default function ResourceAuthenticationPage() {
const { resource, updateResource, authInfo, updateAuthInfo } = const { resource, updateResource, authInfo, updateAuthInfo } =
useResourceContext(); useResourceContext();
const api = createApiClient(useEnvContext());
const [pageLoading, setPageLoading] = useState(true); const [pageLoading, setPageLoading] = useState(true);
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(

View file

@ -12,7 +12,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import api from "@app/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ListTargetsResponse } from "@server/routers/target/listTargets"; import { ListTargetsResponse } from "@server/routers/target/listTargets";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -49,9 +48,9 @@ import { useToast } from "@app/hooks/useToast";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import { ArrayElement } from "@server/types/ArrayElement"; import { ArrayElement } from "@server/types/ArrayElement";
import { Dot } from "lucide-react";
import { formatAxiosError } from "@app/lib/utils"; 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({ const addTargetSchema = z.object({
ip: z.string().ip(), ip: z.string().ip(),
@ -83,6 +82,8 @@ export default function ReverseProxyTargets(props: {
const { toast } = useToast(); const { toast } = useToast();
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const api = createApiClient(useEnvContext());
const [targets, setTargets] = useState<LocalTarget[]>([]); const [targets, setTargets] = useState<LocalTarget[]>([]);
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]); const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [sslEnabled, setSslEnabled] = useState(resource.ssl); const [sslEnabled, setSslEnabled] = useState(resource.ssl);

View file

@ -33,7 +33,6 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
import { ListSitesResponse } from "@server/routers/site"; import { ListSitesResponse } from "@server/routers/site";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import api from "@app/api";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { GetResourceAuthInfoResponse } from "@server/routers/resource"; import { GetResourceAuthInfoResponse } from "@server/routers/resource";
@ -43,6 +42,8 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
import CustomDomainInput from "../components/CustomDomainInput"; import CustomDomainInput from "../components/CustomDomainInput";
import ResourceInfoBox from "../components/ResourceInfoBox"; import ResourceInfoBox from "../components/ResourceInfoBox";
import { subdomainSchema } from "@server/schemas/subdomainSchema"; import { subdomainSchema } from "@server/schemas/subdomainSchema";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string(), name: z.string(),
@ -61,6 +62,8 @@ export default function GeneralForm() {
const orgId = params.orgId; const orgId = params.orgId;
const api = createApiClient(useEnvContext());
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]); const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
const [domainSuffix, setDomainSuffix] = useState(org.org.domain); const [domainSuffix, setDomainSuffix] = useState(org.org.domain);

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button, buttonVariants } from "@app/components/ui/button"; import { Button, buttonVariants } from "@app/components/ui/button";
import { import {
Form, Form,
@ -9,7 +8,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
@ -25,7 +24,7 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { ListSitesResponse } from "@server/routers/site"; import { ListSitesResponse } from "@server/routers/site";
@ -34,7 +33,7 @@ import { CheckIcon } from "lucide-react";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger
} from "@app/components/ui/popover"; } from "@app/components/ui/popover";
import { import {
Command, Command,
@ -42,7 +41,7 @@ import {
CommandGroup, CommandGroup,
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList
} from "@app/components/ui/command"; } from "@app/components/ui/command";
import { CaretSortIcon } from "@radix-ui/react-icons"; import { CaretSortIcon } from "@radix-ui/react-icons";
import CustomDomainInput from "../[resourceId]/components/CustomDomainInput"; import CustomDomainInput from "../[resourceId]/components/CustomDomainInput";
@ -50,11 +49,13 @@ import { Axios, AxiosResponse } from "axios";
import { Resource } from "@server/db/schema"; import { Resource } from "@server/db/schema";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { subdomainSchema } from "@server/schemas/subdomainSchema"; import { subdomainSchema } from "@server/schemas/subdomainSchema";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const accountFormSchema = z.object({ const accountFormSchema = z.object({
subdomain: subdomainSchema, subdomain: subdomainSchema,
name: z.string(), name: z.string(),
siteId: z.number(), siteId: z.number()
}); });
type AccountFormValues = z.infer<typeof accountFormSchema>; type AccountFormValues = z.infer<typeof accountFormSchema>;
@ -66,10 +67,12 @@ type CreateResourceFormProps = {
export default function CreateResourceForm({ export default function CreateResourceForm({
open, open,
setOpen, setOpen
}: CreateResourceFormProps) { }: CreateResourceFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const params = useParams(); const params = useParams();
@ -85,8 +88,8 @@ export default function CreateResourceForm({
resolver: zodResolver(accountFormSchema), resolver: zodResolver(accountFormSchema),
defaultValues: { defaultValues: {
subdomain: "", subdomain: "",
name: "My Resource", name: "My Resource"
}, }
}); });
useEffect(() => { useEffect(() => {
@ -96,7 +99,7 @@ export default function CreateResourceForm({
const fetchSites = async () => { const fetchSites = async () => {
const res = await api.get<AxiosResponse<ListSitesResponse>>( const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${orgId}/sites/`, `/org/${orgId}/sites/`
); );
setSites(res.data.data.sites); setSites(res.data.data.sites);
@ -116,9 +119,9 @@ export default function CreateResourceForm({
`/org/${orgId}/site/${data.siteId}/resource/`, `/org/${orgId}/site/${data.siteId}/resource/`,
{ {
name: data.name, name: data.name,
subdomain: data.subdomain, subdomain: data.subdomain
// subdomain: data.subdomain, // subdomain: data.subdomain,
}, }
) )
.catch((e) => { .catch((e) => {
toast({ toast({
@ -126,8 +129,8 @@ export default function CreateResourceForm({
title: "Error creating resource", title: "Error creating resource",
description: formatAxiosError( description: formatAxiosError(
e, 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) => onChange={(value) =>
form.setValue( form.setValue(
"subdomain", "subdomain",
value, value
) )
} }
/> />
@ -227,14 +230,14 @@ export default function CreateResourceForm({
className={cn( className={cn(
"w-[350px] justify-between", "w-[350px] justify-between",
!field.value && !field.value &&
"text-muted-foreground", "text-muted-foreground"
)} )}
> >
{field.value {field.value
? sites.find( ? sites.find(
(site) => (site) =>
site.siteId === site.siteId ===
field.value, field.value
)?.name )?.name
: "Select site"} : "Select site"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
@ -261,7 +264,7 @@ export default function CreateResourceForm({
onSelect={() => { onSelect={() => {
form.setValue( form.setValue(
"siteId", "siteId",
site.siteId, site.siteId
); );
}} }}
> >
@ -271,14 +274,14 @@ export default function CreateResourceForm({
site.siteId === site.siteId ===
field.value field.value
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0"
)} )}
/> />
{ {
site.name site.name
} }
</CommandItem> </CommandItem>
), )
)} )}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>

View file

@ -21,13 +21,14 @@ import {
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import api from "@app/api";
import CreateResourceForm from "./CreateResourceForm"; import CreateResourceForm from "./CreateResourceForm";
import { useState } from "react"; import { useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { set } from "zod"; import { set } from "zod";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
export type ResourceRow = { export type ResourceRow = {
id: number; id: number;
@ -49,6 +50,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext());
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedResource, setSelectedResource] = const [selectedResource, setSelectedResource] =

View file

@ -15,11 +15,12 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useSiteContext } from "@app/hooks/useSiteContext"; import { useSiteContext } from "@app/hooks/useSiteContext";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import api from "@app/api";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string(), name: z.string(),
@ -31,6 +32,8 @@ export default function GeneralPage() {
const { site, updateSite } = useSiteContext(); const { site, updateSite } = useSiteContext();
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext());
const router = useRouter(); const router = useRouter();
const form = useForm<GeneralFormValues>({ const form = useForm<GeneralFormValues>({

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button, buttonVariants } from "@app/components/ui/button"; import { Button, buttonVariants } from "@app/components/ui/button";
import { import {
Form, Form,
@ -41,6 +40,8 @@ import {
SelectValue, SelectValue,
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const method = [ const method = [
{ label: "Newt", value: "newt" }, { label: "Newt", value: "newt" },
@ -74,6 +75,8 @@ type CreateSiteFormProps = {
export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const params = useParams(); const params = useParams();

View file

@ -12,13 +12,14 @@ import { Button } from "@app/components/ui/button";
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import api from "@app/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useState } from "react"; import { useState } from "react";
import CreateSiteForm from "./CreateSiteForm"; import CreateSiteForm from "./CreateSiteForm";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
export type SiteRow = { export type SiteRow = {
id: number; id: number;
@ -44,6 +45,8 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null); const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
const api = createApiClient(useEnvContext());
const callApi = async () => { const callApi = async () => {
const res = await api.put<AxiosResponse<any>>(`/newt`); const res = await api.put<AxiosResponse<any>>(`/newt`);
console.log(res); console.log(res);

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, useSyncExternalStore } from "react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import * as z from "zod"; import * as z from "zod";
@ -29,7 +29,6 @@ import {
InputOTPGroup, InputOTPGroup,
InputOTPSlot, InputOTPSlot,
} from "@app/components/ui/input-otp"; } from "@app/components/ui/input-otp";
import api from "@app/api";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
@ -38,6 +37,8 @@ import LoginForm from "@app/components/LoginForm";
import { AuthWithPasswordResponse } from "@server/routers/resource"; import { AuthWithPasswordResponse } from "@server/routers/resource";
import { redirect } from "next/dist/server/api-utils"; import { redirect } from "next/dist/server/api-utils";
import ResourceAccessDenied from "./ResourceAccessDenied"; import ResourceAccessDenied from "./ResourceAccessDenied";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const pinSchema = z.object({ const pinSchema = z.object({
pin: z pin: z
@ -83,6 +84,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
const [accessDenied, setAccessDenied] = useState<boolean>(false); const [accessDenied, setAccessDenied] = useState<boolean>(false);
const [loadingLogin, setLoadingLogin] = useState(false); const [loadingLogin, setLoadingLogin] = useState(false);
const api = createApiClient(useEnvContext());
function getDefaultSelectedMethod() { function getDefaultSelectedMethod() {
if (props.methods.sso) { if (props.methods.sso) {
return "sso"; return "sso";

View file

@ -27,7 +27,6 @@ import {
InputOTPGroup, InputOTPGroup,
InputOTPSlot, InputOTPSlot,
} from "@/components/ui/input-otp"; } from "@/components/ui/input-otp";
import api from "@app/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { VerifyEmailResponse } from "@server/routers/auth"; import { VerifyEmailResponse } from "@server/routers/auth";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
@ -35,6 +34,8 @@ import { Alert, AlertDescription } from "../../../components/ui/alert";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const FormSchema = z.object({ const FormSchema = z.object({
email: z.string().email({ message: "Invalid email address" }), email: z.string().email({ message: "Invalid email address" }),
@ -61,6 +62,8 @@ export default function VerifyEmailForm({
const { toast } = useToast(); const { toast } = useToast();
const api = createApiClient(useEnvContext());
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { defaultValues: {

View file

@ -6,7 +6,7 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 20 5.0% 10.0%; --foreground: 0 0.0% 10.0%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 20 5.0% 10.0%; --card-foreground: 20 5.0% 10.0%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
@ -33,7 +33,7 @@
} }
.dark { .dark {
--background: 20 5.0% 10.0%; --background: 0 0.0% 10.0%;
--foreground: 60 9.1% 97.8%; --foreground: 60 9.1% 97.8%;
--card: 20 5.0% 10.0%; --card: 20 5.0% 10.0%;
--card-foreground: 60 9.1% 97.8%; --card-foreground: 60 9.1% 97.8%;

View file

@ -1,14 +1,15 @@
"use client"; "use client";
import api from "@app/api"; import { createApiClient } from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle
} from "@app/components/ui/card"; } from "@app/components/ui/card";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { XCircle } from "lucide-react"; import { XCircle } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -19,10 +20,12 @@ type InviteStatusCardProps = {
export default function InviteStatusCard({ export default function InviteStatusCard({
type, type,
token, token
}: InviteStatusCardProps) { }: InviteStatusCardProps) {
const router = useRouter(); const router = useRouter();
const api = createApiClient(useEnvContext());
async function goToLogin() { async function goToLogin() {
await api.post("/auth/logout", {}); await api.post("/auth/logout", {});
router.push(`/auth/login?redirect=/invite?token=${token}`); router.push(`/auth/login?redirect=/invite?token=${token}`);

View file

@ -1,23 +1,19 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; 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 { Toaster } from "@/components/ui/toaster";
import { ThemeProvider } from "@app/providers/ThemeProvider"; import { ThemeProvider } from "@app/providers/ThemeProvider";
import EnvProvider from "@app/providers/EnvProvider";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - Pangolin`, 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"] }); const font = Figtree({ subsets: ["latin"] });
export default async function RootLayout({ export default async function RootLayout({
children, children
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
@ -30,7 +26,18 @@ export default async function RootLayout({
enableSystem enableSystem
disableTransitionOnChange 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 /> <Toaster />
</ThemeProvider> </ThemeProvider>
</body> </body>

View file

@ -4,7 +4,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import Link from "next/link"; import Link from "next/link";
import api from "@app/api";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { import {
@ -16,6 +15,8 @@ import {
} from "@app/components/ui/card"; } from "@app/components/ui/card";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import { formatAxiosError } from "@app/lib/utils"; import { formatAxiosError } from "@app/lib/utils";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
type Step = "org" | "site" | "resources"; type Step = "org" | "site" | "resources";
@ -28,6 +29,8 @@ export default function StepperForm() {
const [orgCreated, setOrgCreated] = useState(false); const [orgCreated, setOrgCreated] = useState(false);
const [orgIdTaken, setOrgIdTaken] = useState(false); const [orgIdTaken, setOrgIdTaken] = useState(false);
const api = createApiClient(useEnvContext());
const checkOrgIdAvailability = useCallback(async (value: string) => { const checkOrgIdAvailability = useCallback(async (value: string) => {
try { try {
const res = await api.get(`/org/checkId`, { const res = await api.get(`/org/checkId`, {

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import api from "@app/api";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
@ -42,6 +41,8 @@ import {
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { Description } from "@radix-ui/react-toast"; import { Description } from "@radix-ui/react-toast";
import { createApiClient } from "@app/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
type InviteUserFormProps = { type InviteUserFormProps = {
open: boolean; open: boolean;
@ -64,6 +65,8 @@ export default function InviteUserForm({
}: InviteUserFormProps) { }: InviteUserFormProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const api = createApiClient(useEnvContext());
const formSchema = z.object({ const formSchema = z.object({
string: z.string().refine((val) => val === string, { string: z.string().refine((val) => val === string, {
message: "Invalid confirmation", message: "Invalid confirmation",

View file

@ -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;

View file

@ -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
src/lib/types/env.ts Normal file
View file

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

View file

@ -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;