mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Merge pull request #1989 from Fredkiss3/refactor/save-button-positions
refactor: save button positionning
This commit is contained in:
@@ -23,9 +23,9 @@ export const clearExitNodes: CommandModule<
|
||||
// Delete all exit nodes
|
||||
const deletedCount = await db
|
||||
.delete(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)); // delete all
|
||||
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
|
||||
|
||||
console.log(`Deleted ${deletedCount.changes} exit node(s) from the database`);
|
||||
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1310,8 +1310,11 @@
|
||||
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
||||
"documentation": "Documentation",
|
||||
"saveAllSettings": "Save All Settings",
|
||||
"saveResourceTargets": "Save Targets",
|
||||
"saveResourceHttp": "Save Additional fields",
|
||||
"saveProxyProtocol": "Save Proxy protocol settings",
|
||||
"settingsUpdated": "Settings updated",
|
||||
"settingsUpdatedDescription": "All settings have been updated successfully",
|
||||
"settingsUpdatedDescription": "Settings updated successfully",
|
||||
"settingsErrorUpdate": "Failed to update settings",
|
||||
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
||||
"sidebarCollapse": "Collapse",
|
||||
@@ -1874,6 +1877,8 @@
|
||||
"enableTwoFactorAuthentication": "Enable two-factor authentication",
|
||||
"completeSecuritySteps": "Complete Security Steps",
|
||||
"securitySettings": "Security Settings",
|
||||
"dangerSection": "Danger section",
|
||||
"dangerSectionDescription": "Delete organization alongside all its sites, clients, resources, etc...",
|
||||
"securitySettingsDescription": "Configure security policies for the organization",
|
||||
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
|
||||
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
||||
@@ -2084,7 +2089,7 @@
|
||||
"request": "Request",
|
||||
"requests": "Requests",
|
||||
"logs": "Logs",
|
||||
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
||||
"logsSettingsDescription": "Monitor logs collected from this organization",
|
||||
"searchLogs": "Search logs...",
|
||||
"action": "Action",
|
||||
"actor": "Actor",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
||||
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
||||
import {
|
||||
GetResourceWhitelistResponse,
|
||||
ListResourceRolesResponse,
|
||||
ListResourceUsersResponse
|
||||
} from "@server/routers/resource";
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -25,32 +26,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { ListUsersResponse } from "@server/routers/user";
|
||||
import { Binary, Key, Bot } from "lucide-react";
|
||||
import SetResourcePasswordForm from "components/SetResourcePasswordForm";
|
||||
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
||||
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionTitle,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm
|
||||
} from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -58,10 +34,32 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { Separator } from "@app/components/ui/separator";
|
||||
import { build } from "@server/build";
|
||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import SetResourcePasswordForm from "components/SetResourcePasswordForm";
|
||||
import { Binary, Bot, InfoIcon, Key } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
useActionState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition
|
||||
} from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
const UsersRolesFormSchema = z.object({
|
||||
roles: z.array(
|
||||
@@ -100,14 +98,83 @@ export default function ResourceAuthenticationPage() {
|
||||
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const [pageLoading, setPageLoading] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } =
|
||||
useQuery(
|
||||
resourceQueries.resourceRoles({
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
);
|
||||
const { data: resourceUsers = [], isLoading: isLoadingResourceUsers } =
|
||||
useQuery(
|
||||
resourceQueries.resourceUsers({
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
);
|
||||
|
||||
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(
|
||||
[]
|
||||
const { data: whitelist = [], isLoading: isLoadingWhiteList } = useQuery(
|
||||
resourceQueries.resourceWhitelist({
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
);
|
||||
const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>(
|
||||
[]
|
||||
|
||||
const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery(
|
||||
orgQueries.roles({
|
||||
orgId: org.org.orgId
|
||||
})
|
||||
);
|
||||
const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery(
|
||||
orgQueries.users({
|
||||
orgId: org.org.orgId
|
||||
})
|
||||
);
|
||||
const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery(
|
||||
orgQueries.identityProviders({
|
||||
orgId: org.org.orgId
|
||||
})
|
||||
);
|
||||
|
||||
const pageLoading =
|
||||
isLoadingOrgRoles ||
|
||||
isLoadingOrgUsers ||
|
||||
isLoadingResourceRoles ||
|
||||
isLoadingResourceUsers ||
|
||||
isLoadingWhiteList ||
|
||||
isLoadingOrgIdps;
|
||||
|
||||
const allRoles = useMemo(() => {
|
||||
return orgRoles
|
||||
.map((role) => ({
|
||||
id: role.roleId.toString(),
|
||||
text: role.name
|
||||
}))
|
||||
.filter((role) => role.text !== "Admin");
|
||||
}, [orgRoles]);
|
||||
|
||||
const allUsers = useMemo(() => {
|
||||
return orgUsers.map((user) => ({
|
||||
id: user.id.toString(),
|
||||
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
|
||||
}));
|
||||
}, [orgUsers]);
|
||||
|
||||
const allIdps = useMemo(() => {
|
||||
if (build === "saas") {
|
||||
if (subscription?.subscribed) {
|
||||
return orgIdps.map((idp) => ({
|
||||
id: idp.idpId,
|
||||
text: idp.name
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
return orgIdps.map((idp) => ({
|
||||
id: idp.idpId,
|
||||
text: idp.name
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}, [orgIdps]);
|
||||
|
||||
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
@@ -115,15 +182,7 @@ export default function ResourceAuthenticationPage() {
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [activeEmailTagIndex, setActiveEmailTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
|
||||
// const [blockAccess, setBlockAccess] = useState(resource.blockAccess);
|
||||
const [whitelistEnabled, setWhitelistEnabled] = useState(
|
||||
resource.emailWhitelistEnabled
|
||||
);
|
||||
|
||||
const [autoLoginEnabled, setAutoLoginEnabled] = useState(
|
||||
resource.skipToIdpId !== null && resource.skipToIdpId !== undefined
|
||||
@@ -131,10 +190,6 @@ export default function ResourceAuthenticationPage() {
|
||||
const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
|
||||
resource.skipToIdpId || null
|
||||
);
|
||||
const [allIdps, setAllIdps] = useState<{ id: number; text: string }[]>([]);
|
||||
|
||||
const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false);
|
||||
const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false);
|
||||
|
||||
const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
|
||||
useState(false);
|
||||
@@ -159,167 +214,61 @@ export default function ResourceAuthenticationPage() {
|
||||
defaultValues: { emails: [] }
|
||||
});
|
||||
|
||||
const hasInitializedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [
|
||||
rolesResponse,
|
||||
resourceRolesResponse,
|
||||
usersResponse,
|
||||
resourceUsersResponse,
|
||||
whitelist,
|
||||
idpsResponse
|
||||
] = await Promise.all([
|
||||
api.get<AxiosResponse<ListRolesResponse>>(
|
||||
`/org/${org?.org.orgId}/roles`
|
||||
),
|
||||
api.get<AxiosResponse<ListResourceRolesResponse>>(
|
||||
`/resource/${resource.resourceId}/roles`
|
||||
),
|
||||
api.get<AxiosResponse<ListUsersResponse>>(
|
||||
`/org/${org?.org.orgId}/users`
|
||||
),
|
||||
api.get<AxiosResponse<ListResourceUsersResponse>>(
|
||||
`/resource/${resource.resourceId}/users`
|
||||
),
|
||||
api.get<AxiosResponse<GetResourceWhitelistResponse>>(
|
||||
`/resource/${resource.resourceId}/whitelist`
|
||||
),
|
||||
api.get<
|
||||
AxiosResponse<{
|
||||
idps: { idpId: number; name: string }[];
|
||||
}>
|
||||
>(build === "saas" ? `/org/${org?.org.orgId}/idp` : "/idp")
|
||||
]);
|
||||
if (pageLoading || hasInitializedRef.current) return;
|
||||
|
||||
setAllRoles(
|
||||
rolesResponse.data.data.roles
|
||||
.map((role) => ({
|
||||
id: role.roleId.toString(),
|
||||
text: role.name
|
||||
}))
|
||||
.filter((role) => role.text !== "Admin")
|
||||
);
|
||||
usersRolesForm.setValue(
|
||||
"roles",
|
||||
resourceRoles
|
||||
.map((i) => ({
|
||||
id: i.roleId.toString(),
|
||||
text: i.name
|
||||
}))
|
||||
.filter((role) => role.text !== "Admin")
|
||||
);
|
||||
usersRolesForm.setValue(
|
||||
"users",
|
||||
resourceUsers.map((i) => ({
|
||||
id: i.userId.toString(),
|
||||
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
|
||||
}))
|
||||
);
|
||||
|
||||
usersRolesForm.setValue(
|
||||
"roles",
|
||||
resourceRolesResponse.data.data.roles
|
||||
.map((i) => ({
|
||||
id: i.roleId.toString(),
|
||||
text: i.name
|
||||
}))
|
||||
.filter((role) => role.text !== "Admin")
|
||||
);
|
||||
|
||||
setAllUsers(
|
||||
usersResponse.data.data.users.map((user) => ({
|
||||
id: user.id.toString(),
|
||||
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
|
||||
}))
|
||||
);
|
||||
|
||||
usersRolesForm.setValue(
|
||||
"users",
|
||||
resourceUsersResponse.data.data.users.map((i) => ({
|
||||
id: i.userId.toString(),
|
||||
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
|
||||
}))
|
||||
);
|
||||
|
||||
whitelistForm.setValue(
|
||||
"emails",
|
||||
whitelist.data.data.whitelist.map((w) => ({
|
||||
id: w.email,
|
||||
text: w.email
|
||||
}))
|
||||
);
|
||||
|
||||
if (build === "saas") {
|
||||
if (subscription?.subscribed) {
|
||||
setAllIdps(
|
||||
idpsResponse.data.data.idps.map((idp) => ({
|
||||
id: idp.idpId,
|
||||
text: idp.name
|
||||
}))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setAllIdps(
|
||||
idpsResponse.data.data.idps.map((idp) => ({
|
||||
id: idp.idpId,
|
||||
text: idp.name
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
autoLoginEnabled &&
|
||||
!selectedIdpId &&
|
||||
idpsResponse.data.data.idps.length > 0
|
||||
) {
|
||||
setSelectedIdpId(idpsResponse.data.data.idps[0].idpId);
|
||||
}
|
||||
|
||||
setPageLoading(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorAuthFetch"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("resourceErrorAuthFetchDescription")
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
async function saveWhitelist() {
|
||||
setLoadingSaveWhitelist(true);
|
||||
try {
|
||||
await api.post(`/resource/${resource.resourceId}`, {
|
||||
emailWhitelistEnabled: whitelistEnabled
|
||||
});
|
||||
|
||||
if (whitelistEnabled) {
|
||||
await api.post(`/resource/${resource.resourceId}/whitelist`, {
|
||||
emails: whitelistForm.getValues().emails.map((i) => i.text)
|
||||
});
|
||||
}
|
||||
|
||||
updateResource({
|
||||
emailWhitelistEnabled: whitelistEnabled
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t("resourceWhitelistSave"),
|
||||
description: t("resourceWhitelistSaveDescription")
|
||||
});
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorWhitelistSave"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("resourceErrorWhitelistSaveDescription")
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setLoadingSaveWhitelist(false);
|
||||
whitelistForm.setValue(
|
||||
"emails",
|
||||
whitelist.map((w) => ({
|
||||
id: w.email,
|
||||
text: w.email
|
||||
}))
|
||||
);
|
||||
if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) {
|
||||
setSelectedIdpId(orgIdps[0].idpId);
|
||||
}
|
||||
}
|
||||
hasInitializedRef.current = true;
|
||||
}, [
|
||||
pageLoading,
|
||||
resourceRoles,
|
||||
resourceUsers,
|
||||
whitelist,
|
||||
autoLoginEnabled,
|
||||
selectedIdpId,
|
||||
orgIdps
|
||||
]);
|
||||
|
||||
const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState(
|
||||
onSubmitUsersRoles,
|
||||
null
|
||||
);
|
||||
|
||||
async function onSubmitUsersRoles() {
|
||||
const isValid = usersRolesForm.trigger();
|
||||
if (!isValid) return;
|
||||
|
||||
const data = usersRolesForm.getValues();
|
||||
|
||||
async function onSubmitUsersRoles(
|
||||
data: z.infer<typeof UsersRolesFormSchema>
|
||||
) {
|
||||
try {
|
||||
setLoadingSaveUsersRoles(true);
|
||||
|
||||
// Validate that an IDP is selected if auto login is enabled
|
||||
if (autoLoginEnabled && !selectedIdpId) {
|
||||
toast({
|
||||
@@ -358,6 +307,17 @@ export default function ResourceAuthenticationPage() {
|
||||
title: t("resourceAuthSettingsSave"),
|
||||
description: t("resourceAuthSettingsSaveDescription")
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
predicate(query) {
|
||||
const resourceKey = resourceQueries.resourceClients({
|
||||
resourceId: resource.resourceId
|
||||
}).queryKey;
|
||||
return (
|
||||
query.queryKey[0] === resourceKey[0] &&
|
||||
query.queryKey[1] === resourceKey[1]
|
||||
);
|
||||
}
|
||||
});
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -369,8 +329,6 @@ export default function ResourceAuthenticationPage() {
|
||||
t("resourceErrorUsersRolesSaveDescription")
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setLoadingSaveUsersRoles(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,9 +492,7 @@ export default function ResourceAuthenticationPage() {
|
||||
|
||||
<Form {...usersRolesForm}>
|
||||
<form
|
||||
onSubmit={usersRolesForm.handleSubmit(
|
||||
onSubmitUsersRoles
|
||||
)}
|
||||
action={submitUserRolesForm}
|
||||
id="users-roles-form"
|
||||
className="space-y-4"
|
||||
>
|
||||
@@ -864,136 +820,202 @@ export default function ResourceAuthenticationPage() {
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("otpEmailTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("otpEmailTitleDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
{!env.email.emailEnabled && (
|
||||
<Alert variant="neutral" className="mb-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("otpEmailSmtpRequired")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("otpEmailSmtpRequiredDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<SwitchInput
|
||||
id="whitelist-toggle"
|
||||
label={t("otpEmailWhitelist")}
|
||||
defaultChecked={resource.emailWhitelistEnabled}
|
||||
onCheckedChange={setWhitelistEnabled}
|
||||
disabled={!env.email.emailEnabled}
|
||||
/>
|
||||
|
||||
{whitelistEnabled && env.email.emailEnabled && (
|
||||
<Form {...whitelistForm}>
|
||||
<form id="whitelist-form">
|
||||
<FormField
|
||||
control={whitelistForm.control}
|
||||
name="emails"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<InfoPopup
|
||||
text={t(
|
||||
"otpEmailWhitelistList"
|
||||
)}
|
||||
info={t(
|
||||
"otpEmailWhitelistListDescription"
|
||||
)}
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeEmailTagIndex
|
||||
}
|
||||
size={"sm"}
|
||||
validateTag={(
|
||||
tag
|
||||
) => {
|
||||
return z
|
||||
.email()
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
|
||||
{
|
||||
message:
|
||||
t(
|
||||
"otpEmailErrorInvalid"
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
.safeParse(
|
||||
tag
|
||||
).success;
|
||||
}}
|
||||
setActiveTagIndex={
|
||||
setActiveEmailTagIndex
|
||||
}
|
||||
placeholder={t(
|
||||
"otpEmailEnter"
|
||||
)}
|
||||
tags={
|
||||
whitelistForm.getValues()
|
||||
.emails
|
||||
}
|
||||
setTags={(
|
||||
newRoles
|
||||
) => {
|
||||
whitelistForm.setValue(
|
||||
"emails",
|
||||
newRoles as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
allowDuplicates={
|
||||
false
|
||||
}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"otpEmailEnterDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={saveWhitelist}
|
||||
form="whitelist-form"
|
||||
loading={loadingSaveWhitelist}
|
||||
disabled={loadingSaveWhitelist}
|
||||
>
|
||||
{t("otpEmailWhitelistSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
<OneTimePasswordFormSection
|
||||
resource={resource}
|
||||
updateResource={updateResource}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type OneTimePasswordFormSectionProps = Pick<
|
||||
ResourceContextType,
|
||||
"resource" | "updateResource"
|
||||
>;
|
||||
|
||||
function OneTimePasswordFormSection({
|
||||
resource,
|
||||
updateResource
|
||||
}: OneTimePasswordFormSectionProps) {
|
||||
const { env } = useEnvContext();
|
||||
const [whitelistEnabled, setWhitelistEnabled] = useState(
|
||||
resource.emailWhitelistEnabled
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loadingSaveWhitelist, startTransition] = useTransition();
|
||||
const whitelistForm = useForm({
|
||||
resolver: zodResolver(whitelistSchema),
|
||||
defaultValues: { emails: [] }
|
||||
});
|
||||
const api = createApiClient({ env });
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
const [activeEmailTagIndex, setActiveEmailTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
async function saveWhitelist() {
|
||||
try {
|
||||
await api.post(`/resource/${resource.resourceId}`, {
|
||||
emailWhitelistEnabled: whitelistEnabled
|
||||
});
|
||||
|
||||
if (whitelistEnabled) {
|
||||
await api.post(`/resource/${resource.resourceId}/whitelist`, {
|
||||
emails: whitelistForm.getValues().emails.map((i) => i.text)
|
||||
});
|
||||
}
|
||||
|
||||
updateResource({
|
||||
emailWhitelistEnabled: whitelistEnabled
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t("resourceWhitelistSave"),
|
||||
description: t("resourceWhitelistSaveDescription")
|
||||
});
|
||||
router.refresh();
|
||||
await queryClient.invalidateQueries(
|
||||
resourceQueries.resourceWhitelist({
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("resourceErrorWhitelistSave"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("resourceErrorWhitelistSaveDescription")
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("otpEmailTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("otpEmailTitleDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
{!env.email.emailEnabled && (
|
||||
<Alert variant="neutral" className="mb-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("otpEmailSmtpRequired")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("otpEmailSmtpRequiredDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<SwitchInput
|
||||
id="whitelist-toggle"
|
||||
label={t("otpEmailWhitelist")}
|
||||
defaultChecked={resource.emailWhitelistEnabled}
|
||||
onCheckedChange={setWhitelistEnabled}
|
||||
disabled={!env.email.emailEnabled}
|
||||
/>
|
||||
|
||||
{whitelistEnabled && env.email.emailEnabled && (
|
||||
<Form {...whitelistForm}>
|
||||
<form id="whitelist-form">
|
||||
<FormField
|
||||
control={whitelistForm.control}
|
||||
name="emails"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<InfoPopup
|
||||
text={t(
|
||||
"otpEmailWhitelistList"
|
||||
)}
|
||||
info={t(
|
||||
"otpEmailWhitelistListDescription"
|
||||
)}
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeEmailTagIndex
|
||||
}
|
||||
size={"sm"}
|
||||
validateTag={(tag) => {
|
||||
return z
|
||||
.email()
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
|
||||
{
|
||||
message:
|
||||
t(
|
||||
"otpEmailErrorInvalid"
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
.safeParse(tag)
|
||||
.success;
|
||||
}}
|
||||
setActiveTagIndex={
|
||||
setActiveEmailTagIndex
|
||||
}
|
||||
placeholder={t(
|
||||
"otpEmailEnter"
|
||||
)}
|
||||
tags={
|
||||
whitelistForm.getValues()
|
||||
.emails
|
||||
}
|
||||
setTags={(newRoles) => {
|
||||
whitelistForm.setValue(
|
||||
"emails",
|
||||
newRoles as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
allowDuplicates={false}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("otpEmailEnterDescription")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={() => startTransition(saveWhitelist)}
|
||||
form="whitelist-form"
|
||||
loading={loadingSaveWhitelist}
|
||||
disabled={loadingSaveWhitelist}
|
||||
>
|
||||
{t("otpEmailWhitelistSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
@@ -15,31 +12,6 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionFooter
|
||||
} from "@app/components/Settings";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { ListDomainsResponse } from "@server/routers/domain";
|
||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
@@ -51,26 +23,39 @@ import {
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import DomainPicker from "@app/components/DomainPicker";
|
||||
import { Globe } from "lucide-react";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||
import { DomainRow } from "@app/components/DomainsTable";
|
||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Globe } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { toASCII, toUnicode } from "punycode";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useActionState, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
|
||||
export default function GeneralForm() {
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
const params = useParams();
|
||||
const { resource, updateResource } = useResourceContext();
|
||||
const { org } = useOrgContext();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const [editDomainOpen, setEditDomainOpen] = useState(false);
|
||||
const { licenseStatus } = useLicenseStatusContext();
|
||||
const subscriptionStatus = useSubscriptionStatusContext();
|
||||
const { user } = useUserContext();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
|
||||
@@ -78,15 +63,6 @@ export default function GeneralForm() {
|
||||
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [transferLoading, setTransferLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [baseDomains, setBaseDomains] = useState<
|
||||
ListDomainsResponse["domains"]
|
||||
>([]);
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
const [resourceFullDomain, setResourceFullDomain] = useState(
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
|
||||
);
|
||||
@@ -112,7 +88,6 @@ export default function GeneralForm() {
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
domainId: z.string().optional(),
|
||||
proxyPort: z.int().min(1).max(65535).optional()
|
||||
// enableProxy: z.boolean().optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -131,8 +106,6 @@ export default function GeneralForm() {
|
||||
}
|
||||
);
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
@@ -142,58 +115,17 @@ export default function GeneralForm() {
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
domainId: resource.domainId || undefined,
|
||||
proxyPort: resource.proxyPort || undefined
|
||||
// enableProxy: resource.enableProxy || false
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSites = async () => {
|
||||
const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
||||
`/org/${orgId}/sites/`
|
||||
);
|
||||
setSites(res.data.data.sites);
|
||||
};
|
||||
const [, formAction, saveLoading] = useActionState(onSubmit, null);
|
||||
|
||||
const fetchDomains = async () => {
|
||||
const res = await api
|
||||
.get<
|
||||
AxiosResponse<ListDomainsResponse>
|
||||
>(`/org/${orgId}/domains/`)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("domainErrorFetch"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("domainErrorFetchDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
async function onSubmit() {
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) return;
|
||||
|
||||
if (res?.status === 200) {
|
||||
const rawDomains = res.data.data.domains as DomainRow[];
|
||||
const domains = rawDomains.map((domain) => ({
|
||||
...domain,
|
||||
baseDomain: toUnicode(domain.baseDomain)
|
||||
}));
|
||||
setBaseDomains(domains);
|
||||
setFormKey((key) => key + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
await fetchDomains();
|
||||
await fetchSites();
|
||||
|
||||
setLoadingPage(false);
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function onSubmit(data: GeneralFormValues) {
|
||||
setSaveLoading(true);
|
||||
const data = form.getValues();
|
||||
|
||||
const res = await api
|
||||
.post<AxiosResponse<UpdateResourceResponse>>(
|
||||
@@ -207,9 +139,6 @@ export default function GeneralForm() {
|
||||
: undefined,
|
||||
domainId: data.domainId,
|
||||
proxyPort: data.proxyPort
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
// })
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
@@ -248,320 +177,265 @@ export default function GeneralForm() {
|
||||
router.replace(
|
||||
`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`
|
||||
);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
setSaveLoading(false);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
setSaveLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
!loadingPage && (
|
||||
<>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceGeneral")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourceGeneralDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceGeneral")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourceGeneralDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form} key={formKey}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="enable-resource"
|
||||
defaultChecked={
|
||||
resource.enabled
|
||||
}
|
||||
label={t(
|
||||
"resourceEnable"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
className="space-y-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={() => (
|
||||
<FormItem className="col-span-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="enable-resource"
|
||||
defaultChecked={
|
||||
resource.enabled
|
||||
}
|
||||
label={t(
|
||||
"resourceEnable"
|
||||
)}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) =>
|
||||
form.setValue(
|
||||
"enabled",
|
||||
val
|
||||
) =>
|
||||
form.setValue(
|
||||
"enabled",
|
||||
val
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!resource.http && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortNumberDescription"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!resource.http && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortNumberDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* {build == "oss" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableProxy"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
variant={
|
||||
"outlinePrimarySquare"
|
||||
}
|
||||
checked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourceEnableProxy"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceEnableProxyDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
|
||||
{resource.http && (
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
{t("resourceDomain")}
|
||||
</Label>
|
||||
<div className="border p-2 rounded-md flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<Globe size="14" />
|
||||
{resourceFullDomain}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setEditDomainOpen(
|
||||
true
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"resourceEditDomain"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{resource.http && (
|
||||
<div className="space-y-2">
|
||||
<Label>{t("resourceDomain")}</Label>
|
||||
<div className="border p-2 rounded-md flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<Globe size="14" />
|
||||
{resourceFullDomain}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setEditDomainOpen(true)
|
||||
}
|
||||
>
|
||||
{t("resourceEditDomain")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
console.log(form.getValues());
|
||||
}}
|
||||
loading={saveLoading}
|
||||
disabled={saveLoading}
|
||||
form="general-settings-form"
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={saveLoading}
|
||||
disabled={saveLoading}
|
||||
form="general-settings-form"
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
|
||||
<Credenza
|
||||
open={editDomainOpen}
|
||||
onOpenChange={(setOpen) => setEditDomainOpen(setOpen)}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Edit Domain</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Select a domain for your resource
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<DomainPicker
|
||||
orgId={orgId as string}
|
||||
cols={1}
|
||||
defaultSubdomain={
|
||||
form.getValues("subdomain") ??
|
||||
resource.subdomain
|
||||
<Credenza
|
||||
open={editDomainOpen}
|
||||
onOpenChange={(setOpen) => setEditDomainOpen(setOpen)}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Edit Domain</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Select a domain for your resource
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<DomainPicker
|
||||
orgId={orgId as string}
|
||||
cols={1}
|
||||
defaultSubdomain={
|
||||
form.watch("subdomain") ?? resource.subdomain
|
||||
}
|
||||
defaultDomainId={
|
||||
form.watch("domainId") ?? resource.domainId
|
||||
}
|
||||
defaultFullDomain={resourceFullDomainName}
|
||||
onDomainChange={(res) => {
|
||||
const selected =
|
||||
res === null
|
||||
? null
|
||||
: {
|
||||
domainId: res.domainId,
|
||||
subdomain: res.subdomain,
|
||||
fullDomain: res.fullDomain,
|
||||
baseDomain: res.baseDomain,
|
||||
domainNamespaceId:
|
||||
res.domainNamespaceId
|
||||
};
|
||||
|
||||
setSelectedDomain(selected);
|
||||
}}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("cancel")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedDomain) {
|
||||
const sanitizedSubdomain =
|
||||
selectedDomain.subdomain
|
||||
? finalizeSubdomainSanitize(
|
||||
selectedDomain.subdomain
|
||||
)
|
||||
: "";
|
||||
|
||||
const sanitizedFullDomain =
|
||||
sanitizedSubdomain
|
||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||
: selectedDomain.baseDomain;
|
||||
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`
|
||||
);
|
||||
form.setValue(
|
||||
"domainId",
|
||||
selectedDomain.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
sanitizedSubdomain
|
||||
);
|
||||
|
||||
setEditDomainOpen(false);
|
||||
}
|
||||
defaultDomainId={
|
||||
form.getValues("domainId") ??
|
||||
resource.domainId
|
||||
}
|
||||
defaultFullDomain={resourceFullDomainName}
|
||||
onDomainChange={(res) => {
|
||||
const selected =
|
||||
res === null
|
||||
? null
|
||||
: {
|
||||
domainId: res.domainId,
|
||||
subdomain: res.subdomain,
|
||||
fullDomain: res.fullDomain,
|
||||
baseDomain: res.baseDomain,
|
||||
domainNamespaceId:
|
||||
res.domainNamespaceId
|
||||
};
|
||||
setSelectedDomain(selected);
|
||||
}}
|
||||
/>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("cancel")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedDomain) {
|
||||
const sanitizedSubdomain =
|
||||
selectedDomain.subdomain
|
||||
? finalizeSubdomainSanitize(
|
||||
selectedDomain.subdomain
|
||||
)
|
||||
: "";
|
||||
|
||||
const sanitizedFullDomain =
|
||||
sanitizedSubdomain
|
||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||
: selectedDomain.baseDomain;
|
||||
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`
|
||||
);
|
||||
form.setValue(
|
||||
"domainId",
|
||||
selectedDomain.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
sanitizedSubdomain
|
||||
);
|
||||
|
||||
setEditDomainOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Select Domain
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
>
|
||||
Select Domain
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import { GetOrgResponse } from "@server/routers/org";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { cache } from "react";
|
||||
import ResourceInfoBox from "@app/components/ResourceInfoBox";
|
||||
import { GetSiteResponse } from "@server/routers/site";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface ResourceLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ niceId: string; orgId: string }>;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -94,6 +94,12 @@ export default function DomainPicker({
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
|
||||
console.log({
|
||||
defaultFullDomain,
|
||||
defaultSubdomain,
|
||||
defaultDomainId
|
||||
});
|
||||
|
||||
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||
orgQueries.domains({ orgId })
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { createContext } from "react";
|
||||
|
||||
interface OrgContextType {
|
||||
export interface OrgContextType {
|
||||
org: GetOrgResponse;
|
||||
updateOrg: (updateOrg: Partial<GetOrgResponse>) => void;
|
||||
}
|
||||
|
||||
const OrgContext = createContext<OrgContextType | undefined>(undefined);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GetResourceAuthInfoResponse } from "@server/routers/resource";
|
||||
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||
import { createContext } from "react";
|
||||
|
||||
interface ResourceContextType {
|
||||
export interface ResourceContextType {
|
||||
resource: GetResourceResponse;
|
||||
authInfo: GetResourceAuthInfoResponse;
|
||||
updateResource: (updatedResource: Partial<GetResourceResponse>) => void;
|
||||
|
||||
@@ -15,7 +15,11 @@ import z from "zod";
|
||||
import { remote } from "./api";
|
||||
import { durationToMs } from "./durationToMs";
|
||||
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
||||
import type { ListResourceNamesResponse } from "@server/routers/resource";
|
||||
import type {
|
||||
GetResourceWhitelistResponse,
|
||||
ListResourceNamesResponse
|
||||
} from "@server/routers/resource";
|
||||
import type { ListTargetsResponse } from "@server/routers/target";
|
||||
import type { ListDomainsResponse } from "@server/routers/domain";
|
||||
|
||||
export type ProductUpdate = {
|
||||
@@ -151,6 +155,18 @@ export const orgQueries = {
|
||||
>(`/org/${orgId}/domains`, { signal });
|
||||
return res.data.data.domains;
|
||||
}
|
||||
}),
|
||||
identityProviders: ({ orgId }: { orgId: string }) =>
|
||||
queryOptions({
|
||||
queryKey: ["ORG", orgId, "IDPS"] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<{
|
||||
idps: { idpId: number; name: string }[];
|
||||
}>
|
||||
>(build === "saas" ? `/org/${orgId}/idp` : "/idp", { signal });
|
||||
return res.data.data.idps;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -212,7 +228,7 @@ export const resourceQueries = {
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListSiteResourceUsersResponse>
|
||||
>(`/site-resource/${resourceId}/users`, { signal });
|
||||
>(`/resource/${resourceId}/users`, { signal });
|
||||
return res.data.data.users;
|
||||
}
|
||||
}),
|
||||
@@ -222,7 +238,7 @@ export const resourceQueries = {
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListSiteResourceRolesResponse>
|
||||
>(`/site-resource/${resourceId}/roles`, { signal });
|
||||
>(`/resource/${resourceId}/roles`, { signal });
|
||||
|
||||
return res.data.data.roles;
|
||||
}
|
||||
@@ -233,11 +249,33 @@ export const resourceQueries = {
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListSiteResourceClientsResponse>
|
||||
>(`/site-resource/${resourceId}/clients`, { signal });
|
||||
>(`/resource/${resourceId}/clients`, { signal });
|
||||
|
||||
return res.data.data.clients;
|
||||
}
|
||||
}),
|
||||
resourceTargets: ({ resourceId }: { resourceId: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES", resourceId, "TARGETS"] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListTargetsResponse>
|
||||
>(`/resource/${resourceId}/targets`, { signal });
|
||||
|
||||
return res.data.data.targets;
|
||||
}
|
||||
}),
|
||||
resourceWhitelist: ({ resourceId }: { resourceId: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES", resourceId, "WHITELISTS"] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<GetResourceWhitelistResponse>
|
||||
>(`/resource/${resourceId}/whitelist`, { signal });
|
||||
|
||||
return res.data.data.whitelist;
|
||||
}
|
||||
}),
|
||||
listNamesPerOrg: (orgId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES_NAMES", orgId] as const,
|
||||
|
||||
@@ -10,36 +10,15 @@ interface OrgProviderProps {
|
||||
org: GetOrgResponse | null;
|
||||
}
|
||||
|
||||
export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) {
|
||||
const [org, setOrg] = useState<GetOrgResponse | null>(serverOrg);
|
||||
|
||||
export function OrgProvider({ children, org }: OrgProviderProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
if (!org) {
|
||||
throw new Error(t("orgErrorNoProvided"));
|
||||
}
|
||||
|
||||
const updateOrg = (updatedOrg: Partial<GetOrgResponse>) => {
|
||||
if (!org) {
|
||||
throw new Error(t("orgErrorNoUpdate"));
|
||||
}
|
||||
|
||||
setOrg((prev) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...updatedOrg
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<OrgContext.Provider value={{ org, updateOrg }}>
|
||||
{children}
|
||||
</OrgContext.Provider>
|
||||
<OrgContext.Provider value={{ org }}>{children}</OrgContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user