Merge pull request #1989 from Fredkiss3/refactor/save-button-positions

refactor: save button positionning
This commit is contained in:
Milo Schwartz
2025-12-18 07:28:47 -08:00
committed by GitHub
12 changed files with 2349 additions and 2511 deletions

View File

@@ -23,9 +23,9 @@ export const clearExitNodes: CommandModule<
// Delete all exit nodes // Delete all exit nodes
const deletedCount = await db const deletedCount = await db
.delete(exitNodes) .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); process.exit(0);
} catch (error) { } catch (error) {

View File

@@ -1310,8 +1310,11 @@
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
"documentation": "Documentation", "documentation": "Documentation",
"saveAllSettings": "Save All Settings", "saveAllSettings": "Save All Settings",
"saveResourceTargets": "Save Targets",
"saveResourceHttp": "Save Additional fields",
"saveProxyProtocol": "Save Proxy protocol settings",
"settingsUpdated": "Settings updated", "settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "All settings have been updated successfully", "settingsUpdatedDescription": "Settings updated successfully",
"settingsErrorUpdate": "Failed to update settings", "settingsErrorUpdate": "Failed to update settings",
"settingsErrorUpdateDescription": "An error occurred while updating settings", "settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse", "sidebarCollapse": "Collapse",
@@ -1874,6 +1877,8 @@
"enableTwoFactorAuthentication": "Enable two-factor authentication", "enableTwoFactorAuthentication": "Enable two-factor authentication",
"completeSecuritySteps": "Complete Security Steps", "completeSecuritySteps": "Complete Security Steps",
"securitySettings": "Security Settings", "securitySettings": "Security Settings",
"dangerSection": "Danger section",
"dangerSectionDescription": "Delete organization alongside all its sites, clients, resources, etc...",
"securitySettingsDescription": "Configure security policies for the organization", "securitySettingsDescription": "Configure security policies for the organization",
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users", "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.", "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", "request": "Request",
"requests": "Requests", "requests": "Requests",
"logs": "Logs", "logs": "Logs",
"logsSettingsDescription": "Monitor logs collected from this orginization", "logsSettingsDescription": "Monitor logs collected from this organization",
"searchLogs": "Search logs...", "searchLogs": "Search logs...",
"action": "Action", "action": "Action",
"actor": "Actor", "actor": "Actor",

View File

@@ -8,7 +8,13 @@ import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useState, useRef } from "react"; import {
useState,
useRef,
useTransition,
useActionState,
type ComponentRef
} from "react";
import { import {
Form, Form,
FormControl, FormControl,
@@ -53,6 +59,8 @@ import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import type { t } from "@faker-js/faker/dist/airline-DF6RqYmq";
import type { OrgContextType } from "@app/contexts/orgContext";
// Session length options in hours // Session length options in hours
const SESSION_LENGTH_OPTIONS = [ const SESSION_LENGTH_OPTIONS = [
@@ -111,82 +119,35 @@ const LOG_RETENTION_OPTIONS = [
]; ];
export default function GeneralPage() { export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const router = useRouter();
const { org } = useOrgContext(); const { org } = useOrgContext();
const api = createApiClient(useEnvContext());
const { user } = useUserContext();
const t = useTranslations();
const { env } = useEnvContext();
const { isPaidUser, hasSaasSubscription } = usePaidStatus();
const [loadingDelete, setLoadingDelete] = useState(false);
const [loadingSave, setLoadingSave] = useState(false);
const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] =
useState(false);
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: org?.org.name,
subnet: org?.org.subnet || "", // Add default value for subnet
requireTwoFactor: org?.org.requireTwoFactor || false,
maxSessionLengthHours: org?.org.maxSessionLengthHours || null,
passwordExpiryDays: org?.org.passwordExpiryDays || null,
settingsLogRetentionDaysRequest:
org.org.settingsLogRetentionDaysRequest ?? 15,
settingsLogRetentionDaysAccess:
org.org.settingsLogRetentionDaysAccess ?? 15,
settingsLogRetentionDaysAction:
org.org.settingsLogRetentionDaysAction ?? 15
},
mode: "onChange"
});
// Track initial security policy values
const initialSecurityValues = {
requireTwoFactor: org?.org.requireTwoFactor || false,
maxSessionLengthHours: org?.org.maxSessionLengthHours || null,
passwordExpiryDays: org?.org.passwordExpiryDays || null
};
// Check if security policies have changed
const hasSecurityPolicyChanged = () => {
const currentValues = form.getValues();
return ( return (
currentValues.requireTwoFactor !== <SettingsContainer>
initialSecurityValues.requireTwoFactor || <div className="grid gap-y-8">
currentValues.maxSessionLengthHours !== <GeneralSectionForm org={org.org} />
initialSecurityValues.maxSessionLengthHours ||
currentValues.passwordExpiryDays !== <LogRetentionSectionForm org={org.org} />
initialSecurityValues.passwordExpiryDays
{build !== "oss" && (
<SecuritySettingsSectionForm org={org.org} />
)}
{build !== "saas" && <DeleteForm org={org.org} />}
</div>
</SettingsContainer>
); );
}
type SectionFormProps = {
org: OrgContextType["org"]["org"];
}; };
async function deleteOrg() { function DeleteForm({ org }: SectionFormProps) {
setLoadingDelete(true); const t = useTranslations();
try { const api = createApiClient(useEnvContext());
const res = await api.delete<AxiosResponse<DeleteOrgResponse>>(
`/org/${org?.org.orgId}` const router = useRouter();
); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
toast({ const [loadingDelete, startTransition] = useTransition();
title: t("orgDeleted"), const { user } = useUserContext();
description: t("orgDeletedMessage")
});
if (res.status === 200) {
pickNewOrgAndNavigate();
}
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t("orgErrorDelete"),
description: formatAxiosError(err, t("orgErrorDeleteMessage"))
});
} finally {
setLoadingDelete(false);
}
}
async function pickNewOrgAndNavigate() { async function pickNewOrgAndNavigate() {
try { try {
@@ -213,57 +174,29 @@ export default function GeneralPage() {
}); });
} }
} }
async function deleteOrg() {
async function onSubmit(data: GeneralFormValues) {
// Check if security policies have changed
if (hasSecurityPolicyChanged()) {
setIsSecurityPolicyConfirmOpen(true);
return;
}
await performSave(data);
}
async function performSave(data: GeneralFormValues) {
setLoadingSave(true);
try { try {
const reqData = { const res = await api.delete<AxiosResponse<DeleteOrgResponse>>(
name: data.name, `/org/${org.orgId}`
settingsLogRetentionDaysRequest: );
data.settingsLogRetentionDaysRequest,
settingsLogRetentionDaysAccess:
data.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysAction:
data.settingsLogRetentionDaysAction
} as any;
if (build !== "oss") {
reqData.requireTwoFactor = data.requireTwoFactor || false;
reqData.maxSessionLengthHours = data.maxSessionLengthHours;
reqData.passwordExpiryDays = data.passwordExpiryDays;
}
// Update organization
await api.post(`/org/${org?.org.orgId}`, reqData);
toast({ toast({
title: t("orgUpdated"), title: t("orgDeleted"),
description: t("orgUpdatedDescription") description: t("orgDeletedMessage")
}); });
router.refresh(); if (res.status === 200) {
} catch (e) { pickNewOrgAndNavigate();
}
} catch (err) {
console.error(err);
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("orgErrorUpdate"), title: t("orgErrorDelete"),
description: formatAxiosError(e, t("orgErrorUpdateMessage")) description: formatAxiosError(err, t("orgErrorDeleteMessage"))
}); });
} finally {
setLoadingSave(false);
} }
} }
return ( return (
<SettingsContainer> <>
<ConfirmDeleteDialog <ConfirmDeleteDialog
open={isDeleteModalOpen} open={isDeleteModalOpen}
setOpen={(val) => { setOpen={(val) => {
@@ -276,42 +209,99 @@ export default function GeneralPage() {
</div> </div>
} }
buttonText={t("orgDeleteConfirm")} buttonText={t("orgDeleteConfirm")}
onConfirm={deleteOrg} onConfirm={async () => startTransition(deleteOrg)}
string={org?.org.name || ""} string={org.name || ""}
title={t("orgDelete")} title={t("orgDelete")}
/> />
<ConfirmDeleteDialog
open={isSecurityPolicyConfirmOpen}
setOpen={setIsSecurityPolicyConfirmOpen}
dialog={
<div className="space-y-2">
<p>{t("securityPolicyChangeDescription")}</p>
</div>
}
buttonText={t("saveSettings")}
onConfirm={() => performSave(form.getValues())}
string={t("securityPolicyChangeConfirmMessage")}
title={t("securityPolicyChangeWarning")}
warningText={t("securityPolicyChangeWarningText")}
/>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="org-settings-form"
>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("general")} {t("dangerSection")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription>
{t("dangerSectionDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<div className="flex justify-start gap-2">
<Button
variant="destructive"
onClick={() => setIsDeleteModalOpen(true)}
className="flex items-center gap-2"
loading={loadingDelete}
disabled={loadingDelete}
>
{t("orgDelete")}
</Button>
</div>
</SettingsSection>
</>
);
}
function GeneralSectionForm({ org }: SectionFormProps) {
const form = useForm({
resolver: zodResolver(
GeneralFormSchema.pick({
name: true,
subnet: true
})
),
defaultValues: {
name: org.name,
subnet: org.subnet || "" // Add default value for subnet
},
mode: "onChange"
});
const t = useTranslations();
const router = useRouter();
const [, formAction, loadingSave] = useActionState(performSave, null);
const api = createApiClient(useEnvContext());
async function performSave() {
const isValid = await form.trigger();
if (!isValid) return;
const data = form.getValues();
try {
const reqData = {
name: data.name
} as any;
// Update organization
await api.post(`/org/${org.orgId}`, reqData);
toast({
title: t("orgUpdated"),
description: t("orgUpdatedDescription")
});
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("orgErrorUpdate"),
description: formatAxiosError(e, t("orgErrorUpdateMessage"))
});
}
}
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>{t("general")}</SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("orgGeneralSettingsDescription")} {t("orgGeneralSettingsDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}>
<form
action={formAction}
className="grid gap-4"
id="org-general-settings-form"
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -335,10 +325,7 @@ export default function GeneralPage() {
<FormItem> <FormItem>
<FormLabel>{t("subnet")}</FormLabel> <FormLabel>{t("subnet")}</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} disabled={true} />
{...field}
disabled={true}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
@@ -347,21 +334,101 @@ export default function GeneralPage() {
</FormItem> </FormItem>
)} )}
/> />
</form>
</Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection>
<div className="flex justify-end gap-2 mt-4">
<Button
type="submit"
form="org-general-settings-form"
loading={loadingSave}
disabled={loadingSave}
>
{t("saveSettings")}
</Button>
</div>
</SettingsSection>
);
}
function LogRetentionSectionForm({ org }: SectionFormProps) {
const form = useForm({
resolver: zodResolver(
GeneralFormSchema.pick({
settingsLogRetentionDaysRequest: true,
settingsLogRetentionDaysAccess: true,
settingsLogRetentionDaysAction: true
})
),
defaultValues: {
settingsLogRetentionDaysRequest:
org.settingsLogRetentionDaysRequest ?? 15,
settingsLogRetentionDaysAccess:
org.settingsLogRetentionDaysAccess ?? 15,
settingsLogRetentionDaysAction:
org.settingsLogRetentionDaysAction ?? 15
},
mode: "onChange"
});
const router = useRouter();
const t = useTranslations();
const { isPaidUser, hasSaasSubscription } = usePaidStatus();
const [, formAction, loadingSave] = useActionState(performSave, null);
const api = createApiClient(useEnvContext());
async function performSave() {
const isValid = await form.trigger();
if (!isValid) return;
const data = form.getValues();
try {
const reqData = {
settingsLogRetentionDaysRequest:
data.settingsLogRetentionDaysRequest,
settingsLogRetentionDaysAccess:
data.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysAction:
data.settingsLogRetentionDaysAction
} as any;
// Update organization
await api.post(`/org/${org.orgId}`, reqData);
toast({
title: t("orgUpdated"),
description: t("orgUpdatedDescription")
});
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("orgErrorUpdate"),
description: formatAxiosError(e, t("orgErrorUpdateMessage"))
});
}
}
return (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>{t("logRetention")}</SettingsSectionTitle>
{t("logRetention")}
</SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("logRetentionDescription")} {t("logRetentionDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}>
<form
action={formAction}
className="grid gap-4"
id="org-log-retention-settings-form"
>
<FormField <FormField
control={form.control} control={form.control}
name="settingsLogRetentionDaysRequest" name="settingsLogRetentionDaysRequest"
@@ -400,14 +467,10 @@ export default function GeneralPage() {
} }
).map((option) => ( ).map((option) => (
<SelectItem <SelectItem
key={ key={option.value}
option.value
}
value={option.value.toString()} value={option.value.toString()}
> >
{t( {t(option.label)}
option.label
)}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -556,11 +619,129 @@ export default function GeneralPage() {
/> />
</> </>
)} )}
</form>
</Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection>
{build !== "oss" && ( <div className="flex justify-end gap-2 mt-4">
<Button
type="submit"
form="org-log-retention-settings-form"
loading={loadingSave}
disabled={loadingSave}
>
{t("saveSettings")}
</Button>
</div>
</SettingsSection>
);
}
function SecuritySettingsSectionForm({ org }: SectionFormProps) {
const router = useRouter();
const form = useForm({
resolver: zodResolver(
GeneralFormSchema.pick({
requireTwoFactor: true,
maxSessionLengthHours: true,
passwordExpiryDays: true
})
),
defaultValues: {
requireTwoFactor: org.requireTwoFactor || false,
maxSessionLengthHours: org.maxSessionLengthHours || null,
passwordExpiryDays: org.passwordExpiryDays || null
},
mode: "onChange"
});
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
// Track initial security policy values
const initialSecurityValues = {
requireTwoFactor: org.requireTwoFactor || false,
maxSessionLengthHours: org.maxSessionLengthHours || null,
passwordExpiryDays: org.passwordExpiryDays || null
};
const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] =
useState(false);
// Check if security policies have changed
const hasSecurityPolicyChanged = () => {
const currentValues = form.getValues();
return (
currentValues.requireTwoFactor !==
initialSecurityValues.requireTwoFactor ||
currentValues.maxSessionLengthHours !==
initialSecurityValues.maxSessionLengthHours ||
currentValues.passwordExpiryDays !==
initialSecurityValues.passwordExpiryDays
);
};
const [, formAction, loadingSave] = useActionState(onSubmit, null);
const api = createApiClient(useEnvContext());
const formRef = useRef<ComponentRef<"form">>(null);
async function onSubmit() {
// Check if security policies have changed
if (hasSecurityPolicyChanged()) {
setIsSecurityPolicyConfirmOpen(true);
return;
}
await performSave();
}
async function performSave() {
const isValid = await form.trigger();
if (!isValid) return;
const data = form.getValues();
try {
const reqData = {
requireTwoFactor: data.requireTwoFactor || false,
maxSessionLengthHours: data.maxSessionLengthHours,
passwordExpiryDays: data.passwordExpiryDays
} as any;
// Update organization
await api.post(`/org/${org.orgId}`, reqData);
toast({
title: t("orgUpdated"),
description: t("orgUpdatedDescription")
});
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("orgErrorUpdate"),
description: formatAxiosError(e, t("orgErrorUpdateMessage"))
});
}
}
return (
<>
<ConfirmDeleteDialog
open={isSecurityPolicyConfirmOpen}
setOpen={setIsSecurityPolicyConfirmOpen}
dialog={
<div className="space-y-2">
<p>{t("securityPolicyChangeDescription")}</p>
</div>
}
buttonText={t("saveSettings")}
onConfirm={performSave}
string={t("securityPolicyChangeConfirmMessage")}
title={t("securityPolicyChangeWarning")}
warningText={t("securityPolicyChangeWarningText")}
/>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -572,6 +753,12 @@ export default function GeneralPage() {
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}>
<form
action={formAction}
ref={formRef}
id="security-settings-section-form"
>
<PaidFeaturesAlert /> <PaidFeaturesAlert />
<FormField <FormField
control={form.control} control={form.control}
@@ -640,9 +827,7 @@ export default function GeneralPage() {
onValueChange={( onValueChange={(
value value
) => { ) => {
if ( if (!isDisabled) {
!isDisabled
) {
const numValue = const numValue =
value === value ===
"null" "null"
@@ -657,9 +842,7 @@ export default function GeneralPage() {
); );
} }
}} }}
disabled={ disabled={isDisabled}
isDisabled
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue <SelectValue
@@ -670,9 +853,7 @@ export default function GeneralPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{SESSION_LENGTH_OPTIONS.map( {SESSION_LENGTH_OPTIONS.map(
( (option) => (
option
) => (
<SelectItem <SelectItem
key={ key={
option.value === option.value ===
@@ -715,9 +896,7 @@ export default function GeneralPage() {
return ( return (
<FormItem className="col-span-2"> <FormItem className="col-span-2">
<FormLabel> <FormLabel>
{t( {t("passwordExpiryDays")}
"passwordExpiryDays"
)}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Select <Select
@@ -728,9 +907,7 @@ export default function GeneralPage() {
onValueChange={( onValueChange={(
value value
) => { ) => {
if ( if (!isDisabled) {
!isDisabled
) {
const numValue = const numValue =
value === value ===
"null" "null"
@@ -745,9 +922,7 @@ export default function GeneralPage() {
); );
} }
}} }}
disabled={ disabled={isDisabled}
isDisabled
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue <SelectValue
@@ -758,9 +933,7 @@ export default function GeneralPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{PASSWORD_EXPIRY_OPTIONS.map( {PASSWORD_EXPIRY_OPTIONS.map(
( (option) => (
option
) => (
<SelectItem <SelectItem
key={ key={
option.value === option.value ===
@@ -794,34 +967,22 @@ export default function GeneralPage() {
); );
}} }}
/> />
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
</form> </form>
</Form> </Form>
</SettingsSectionForm>
</SettingsSectionBody>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2 mt-4">
{build !== "saas" && (
<Button
variant="destructive"
onClick={() => setIsDeleteModalOpen(true)}
className="flex items-center gap-2"
loading={loadingDelete}
disabled={loadingDelete}
>
{t("orgDelete")}
</Button>
)}
<Button <Button
type="submit" type="submit"
form="org-settings-form" form="security-settings-section-form"
loading={loadingSave} loading={loadingSave}
disabled={loadingSave} disabled={loadingSave}
> >
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>
</div> </div>
</SettingsContainer> </SettingsSection>
</>
); );
} }

View File

@@ -1,21 +1,22 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
import { ListRolesResponse } from "@server/routers/role"; import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
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 { import {
GetResourceWhitelistResponse, SettingsContainer,
ListResourceRolesResponse, SettingsSection,
ListResourceUsersResponse SettingsSectionBody,
} from "@server/routers/resource"; 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 { Button } from "@app/components/ui/button";
import { z } from "zod"; import { CheckboxWithLabel } from "@app/components/ui/checkbox";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { import {
Form, Form,
FormControl, FormControl,
@@ -25,32 +26,7 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } 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 { 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 { import {
Select, Select,
SelectContent, SelectContent,
@@ -58,10 +34,32 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { Separator } from "@app/components/ui/separator"; import type { ResourceContextType } from "@app/contexts/resourceContext";
import { build } from "@server/build"; 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 { 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({ const UsersRolesFormSchema = z.object({
roles: z.array( roles: z.array(
@@ -100,14 +98,83 @@ export default function ResourceAuthenticationPage() {
const subscription = useSubscriptionStatusContext(); 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< const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
number | null number | null
>(null); >(null);
@@ -115,15 +182,7 @@ export default function ResourceAuthenticationPage() {
number | null number | null
>(null); >(null);
const [activeEmailTagIndex, setActiveEmailTagIndex] = useState<
number | null
>(null);
const [ssoEnabled, setSsoEnabled] = useState(resource.sso); const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
// const [blockAccess, setBlockAccess] = useState(resource.blockAccess);
const [whitelistEnabled, setWhitelistEnabled] = useState(
resource.emailWhitelistEnabled
);
const [autoLoginEnabled, setAutoLoginEnabled] = useState( const [autoLoginEnabled, setAutoLoginEnabled] = useState(
resource.skipToIdpId !== null && resource.skipToIdpId !== undefined resource.skipToIdpId !== null && resource.skipToIdpId !== undefined
@@ -131,10 +190,6 @@ export default function ResourceAuthenticationPage() {
const [selectedIdpId, setSelectedIdpId] = useState<number | null>( const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
resource.skipToIdpId || 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] = const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
useState(false); useState(false);
@@ -159,68 +214,23 @@ export default function ResourceAuthenticationPage() {
defaultValues: { emails: [] } defaultValues: { emails: [] }
}); });
useEffect(() => { const hasInitializedRef = useRef(false);
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")
]);
setAllRoles( useEffect(() => {
rolesResponse.data.data.roles if (pageLoading || hasInitializedRef.current) return;
.map((role) => ({
id: role.roleId.toString(),
text: role.name
}))
.filter((role) => role.text !== "Admin")
);
usersRolesForm.setValue( usersRolesForm.setValue(
"roles", "roles",
resourceRolesResponse.data.data.roles resourceRoles
.map((i) => ({ .map((i) => ({
id: i.roleId.toString(), id: i.roleId.toString(),
text: i.name text: i.name
})) }))
.filter((role) => role.text !== "Admin") .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( usersRolesForm.setValue(
"users", "users",
resourceUsersResponse.data.data.users.map((i) => ({ resourceUsers.map((i) => ({
id: i.userId.toString(), id: i.userId.toString(),
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
})) }))
@@ -228,98 +238,37 @@ export default function ResourceAuthenticationPage() {
whitelistForm.setValue( whitelistForm.setValue(
"emails", "emails",
whitelist.data.data.whitelist.map((w) => ({ whitelist.map((w) => ({
id: w.email, id: w.email,
text: 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
]);
if (build === "saas") { const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState(
if (subscription?.subscribed) { onSubmitUsersRoles,
setAllIdps( null
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 ( async function onSubmitUsersRoles() {
autoLoginEnabled && const isValid = usersRolesForm.trigger();
!selectedIdpId && if (!isValid) return;
idpsResponse.data.data.idps.length > 0
) {
setSelectedIdpId(idpsResponse.data.data.idps[0].idpId);
}
setPageLoading(false); const data = usersRolesForm.getValues();
} catch (e) {
console.error(e);
toast({
variant: "destructive",
title: t("resourceErrorAuthFetch"),
description: formatAxiosError(
e,
t("resourceErrorAuthFetchDescription")
)
});
}
};
fetchData();
}, []);
async function saveWhitelist() {
setLoadingSaveWhitelist(true);
try { 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);
}
}
async function onSubmitUsersRoles(
data: z.infer<typeof UsersRolesFormSchema>
) {
try {
setLoadingSaveUsersRoles(true);
// Validate that an IDP is selected if auto login is enabled // Validate that an IDP is selected if auto login is enabled
if (autoLoginEnabled && !selectedIdpId) { if (autoLoginEnabled && !selectedIdpId) {
toast({ toast({
@@ -358,6 +307,17 @@ export default function ResourceAuthenticationPage() {
title: t("resourceAuthSettingsSave"), title: t("resourceAuthSettingsSave"),
description: t("resourceAuthSettingsSaveDescription") 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(); router.refresh();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -369,8 +329,6 @@ export default function ResourceAuthenticationPage() {
t("resourceErrorUsersRolesSaveDescription") t("resourceErrorUsersRolesSaveDescription")
) )
}); });
} finally {
setLoadingSaveUsersRoles(false);
} }
} }
@@ -534,9 +492,7 @@ export default function ResourceAuthenticationPage() {
<Form {...usersRolesForm}> <Form {...usersRolesForm}>
<form <form
onSubmit={usersRolesForm.handleSubmit( action={submitUserRolesForm}
onSubmitUsersRoles
)}
id="users-roles-form" id="users-roles-form"
className="space-y-4" className="space-y-4"
> >
@@ -864,6 +820,83 @@ export default function ResourceAuthenticationPage() {
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </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> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -920,9 +953,7 @@ export default function ResourceAuthenticationPage() {
activeEmailTagIndex activeEmailTagIndex
} }
size={"sm"} size={"sm"}
validateTag={( validateTag={(tag) => {
tag
) => {
return z return z
.email() .email()
.or( .or(
@@ -938,9 +969,8 @@ export default function ResourceAuthenticationPage() {
} }
) )
) )
.safeParse( .safeParse(tag)
tag .success;
).success;
}} }}
setActiveTagIndex={ setActiveTagIndex={
setActiveEmailTagIndex setActiveEmailTagIndex
@@ -952,9 +982,7 @@ export default function ResourceAuthenticationPage() {
whitelistForm.getValues() whitelistForm.getValues()
.emails .emails
} }
setTags={( setTags={(newRoles) => {
newRoles
) => {
whitelistForm.setValue( whitelistForm.setValue(
"emails", "emails",
newRoles as [ newRoles as [
@@ -963,16 +991,12 @@ export default function ResourceAuthenticationPage() {
] ]
); );
}} }}
allowDuplicates={ allowDuplicates={false}
false
}
sortTags={true} sortTags={true}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
{t( {t("otpEmailEnterDescription")}
"otpEmailEnterDescription"
)}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
@@ -984,7 +1008,7 @@ export default function ResourceAuthenticationPage() {
</SettingsSectionBody> </SettingsSectionBody>
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button
onClick={saveWhitelist} onClick={() => startTransition(saveWhitelist)}
form="whitelist-form" form="whitelist-form"
loading={loadingSaveWhitelist} loading={loadingSaveWhitelist}
disabled={loadingSaveWhitelist} disabled={loadingSaveWhitelist}
@@ -993,7 +1017,5 @@ export default function ResourceAuthenticationPage() {
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
</SettingsContainer>
</>
); );
} }

View File

@@ -1,8 +1,5 @@
"use client"; "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 { Button } from "@/components/ui/button";
import { import {
Form, Form,
@@ -15,31 +12,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useResourceContext } from "@app/hooks/useResourceContext"; 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 { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@@ -51,26 +23,39 @@ import {
CredenzaTitle CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import DomainPicker from "@app/components/DomainPicker"; import DomainPicker from "@app/components/DomainPicker";
import { Globe } from "lucide-react"; import {
import { build } from "@server/build"; 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 { 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 { toASCII, toUnicode } from "punycode";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useActionState, useMemo, useState } from "react";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { useForm } from "react-hook-form";
import { useUserContext } from "@app/hooks/useUserContext"; import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
export default function GeneralForm() { export default function GeneralForm() {
const [formKey, setFormKey] = useState(0);
const params = useParams(); const params = useParams();
const { resource, updateResource } = useResourceContext(); const { resource, updateResource } = useResourceContext();
const { org } = useOrgContext();
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const [editDomainOpen, setEditDomainOpen] = useState(false); const [editDomainOpen, setEditDomainOpen] = useState(false);
const { licenseStatus } = useLicenseStatusContext();
const subscriptionStatus = useSubscriptionStatusContext();
const { user } = useUserContext();
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -78,15 +63,6 @@ export default function GeneralForm() {
const api = createApiClient({ env }); 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( const [resourceFullDomain, setResourceFullDomain] = useState(
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
); );
@@ -112,7 +88,6 @@ export default function GeneralForm() {
niceId: z.string().min(1).max(255).optional(), niceId: z.string().min(1).max(255).optional(),
domainId: z.string().optional(), domainId: z.string().optional(),
proxyPort: z.int().min(1).max(65535).optional() proxyPort: z.int().min(1).max(65535).optional()
// enableProxy: z.boolean().optional()
}) })
.refine( .refine(
(data) => { (data) => {
@@ -131,8 +106,6 @@ export default function GeneralForm() {
} }
); );
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm({ const form = useForm({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
defaultValues: { defaultValues: {
@@ -142,58 +115,17 @@ export default function GeneralForm() {
subdomain: resource.subdomain ? resource.subdomain : undefined, subdomain: resource.subdomain ? resource.subdomain : undefined,
domainId: resource.domainId || undefined, domainId: resource.domainId || undefined,
proxyPort: resource.proxyPort || undefined proxyPort: resource.proxyPort || undefined
// enableProxy: resource.enableProxy || false
}, },
mode: "onChange" mode: "onChange"
}); });
useEffect(() => { const [, formAction, saveLoading] = useActionState(onSubmit, null);
const fetchSites = async () => {
const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${orgId}/sites/`
);
setSites(res.data.data.sites);
};
const fetchDomains = async () => { async function onSubmit() {
const res = await api const isValid = await form.trigger();
.get< if (!isValid) return;
AxiosResponse<ListDomainsResponse>
>(`/org/${orgId}/domains/`)
.catch((e) => {
toast({
variant: "destructive",
title: t("domainErrorFetch"),
description: formatAxiosError(
e,
t("domainErrorFetchDescription")
)
});
});
if (res?.status === 200) { const data = form.getValues();
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 res = await api const res = await api
.post<AxiosResponse<UpdateResourceResponse>>( .post<AxiosResponse<UpdateResourceResponse>>(
@@ -207,9 +139,6 @@ export default function GeneralForm() {
: undefined, : undefined,
domainId: data.domainId, domainId: data.domainId,
proxyPort: data.proxyPort proxyPort: data.proxyPort
// ...(!resource.http && {
// enableProxy: data.enableProxy
// })
} }
) )
.catch((e) => { .catch((e) => {
@@ -248,18 +177,13 @@ export default function GeneralForm() {
router.replace( router.replace(
`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general` `/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`
); );
} else { }
router.refresh(); router.refresh();
} }
setSaveLoading(false);
}
setSaveLoading(false);
} }
return ( return (
!loadingPage && (
<> <>
<SettingsContainer> <SettingsContainer>
<SettingsSection> <SettingsSection>
@@ -274,16 +198,16 @@ export default function GeneralForm() {
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form} key={formKey}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} action={formAction}
className="space-y-4" className="space-y-4"
id="general-settings-form" id="general-settings-form"
> >
<FormField <FormField
control={form.control} control={form.control}
name="enabled" name="enabled"
render={({ field }) => ( render={() => (
<FormItem className="col-span-2"> <FormItem className="col-span-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<FormControl> <FormControl>
@@ -368,12 +292,9 @@ export default function GeneralForm() {
field.value ?? field.value ??
"" ""
} }
onChange={( onChange={(e) =>
e
) =>
field.onChange( field.onChange(
e e.target
.target
.value .value
? parseInt( ? parseInt(
e e
@@ -394,50 +315,12 @@ export default function GeneralForm() {
</FormItem> </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 && ( {resource.http && (
<div className="space-y-2"> <div className="space-y-2">
<Label> <Label>{t("resourceDomain")}</Label>
{t("resourceDomain")}
</Label>
<div className="border p-2 rounded-md flex items-center justify-between"> <div className="border p-2 rounded-md flex items-center justify-between">
<span className="text-sm text-muted-foreground flex items-center gap-2"> <span className="text-sm text-muted-foreground flex items-center gap-2">
<Globe size="14" /> <Globe size="14" />
@@ -448,14 +331,10 @@ export default function GeneralForm() {
type="button" type="button"
size="sm" size="sm"
onClick={() => onClick={() =>
setEditDomainOpen( setEditDomainOpen(true)
true
)
} }
> >
{t( {t("resourceEditDomain")}
"resourceEditDomain"
)}
</Button> </Button>
</div> </div>
</div> </div>
@@ -468,9 +347,6 @@ export default function GeneralForm() {
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button
type="submit" type="submit"
onClick={() => {
console.log(form.getValues());
}}
loading={saveLoading} loading={saveLoading}
disabled={saveLoading} disabled={saveLoading}
form="general-settings-form" form="general-settings-form"
@@ -497,12 +373,10 @@ export default function GeneralForm() {
orgId={orgId as string} orgId={orgId as string}
cols={1} cols={1}
defaultSubdomain={ defaultSubdomain={
form.getValues("subdomain") ?? form.watch("subdomain") ?? resource.subdomain
resource.subdomain
} }
defaultDomainId={ defaultDomainId={
form.getValues("domainId") ?? form.watch("domainId") ?? resource.domainId
resource.domainId
} }
defaultFullDomain={resourceFullDomainName} defaultFullDomain={resourceFullDomainName}
onDomainChange={(res) => { onDomainChange={(res) => {
@@ -517,6 +391,7 @@ export default function GeneralForm() {
domainNamespaceId: domainNamespaceId:
res.domainNamespaceId res.domainNamespaceId
}; };
setSelectedDomain(selected); setSelectedDomain(selected);
}} }}
/> />
@@ -562,6 +437,5 @@ export default function GeneralForm() {
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>
</> </>
)
); );
} }

View File

@@ -13,9 +13,10 @@ import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider"; import OrgProvider from "@app/providers/OrgProvider";
import { cache } from "react"; import { cache } from "react";
import ResourceInfoBox from "@app/components/ResourceInfoBox"; import ResourceInfoBox from "@app/components/ResourceInfoBox";
import { GetSiteResponse } from "@server/routers/site";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
export const dynamic = "force-dynamic";
interface ResourceLayoutProps { interface ResourceLayoutProps {
children: React.ReactNode; children: React.ReactNode;
params: Promise<{ niceId: string; orgId: string }>; params: Promise<{ niceId: string; orgId: string }>;

View File

@@ -94,6 +94,12 @@ export default function DomainPicker({
const api = createApiClient({ env }); const api = createApiClient({ env });
const t = useTranslations(); const t = useTranslations();
console.log({
defaultFullDomain,
defaultSubdomain,
defaultDomainId
});
const { data = [], isLoading: loadingDomains } = useQuery( const { data = [], isLoading: loadingDomains } = useQuery(
orgQueries.domains({ orgId }) orgQueries.domains({ orgId })
); );

View File

@@ -1,9 +1,8 @@
import { GetOrgResponse } from "@server/routers/org"; import { GetOrgResponse } from "@server/routers/org";
import { createContext } from "react"; import { createContext } from "react";
interface OrgContextType { export interface OrgContextType {
org: GetOrgResponse; org: GetOrgResponse;
updateOrg: (updateOrg: Partial<GetOrgResponse>) => void;
} }
const OrgContext = createContext<OrgContextType | undefined>(undefined); const OrgContext = createContext<OrgContextType | undefined>(undefined);

View File

@@ -2,7 +2,7 @@ import { GetResourceAuthInfoResponse } from "@server/routers/resource";
import { GetResourceResponse } from "@server/routers/resource/getResource"; import { GetResourceResponse } from "@server/routers/resource/getResource";
import { createContext } from "react"; import { createContext } from "react";
interface ResourceContextType { export interface ResourceContextType {
resource: GetResourceResponse; resource: GetResourceResponse;
authInfo: GetResourceAuthInfoResponse; authInfo: GetResourceAuthInfoResponse;
updateResource: (updatedResource: Partial<GetResourceResponse>) => void; updateResource: (updatedResource: Partial<GetResourceResponse>) => void;

View File

@@ -15,7 +15,11 @@ import z from "zod";
import { remote } from "./api"; import { remote } from "./api";
import { durationToMs } from "./durationToMs"; import { durationToMs } from "./durationToMs";
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; 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"; import type { ListDomainsResponse } from "@server/routers/domain";
export type ProductUpdate = { export type ProductUpdate = {
@@ -151,6 +155,18 @@ export const orgQueries = {
>(`/org/${orgId}/domains`, { signal }); >(`/org/${orgId}/domains`, { signal });
return res.data.data.domains; 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 }) => { queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<ListSiteResourceUsersResponse> AxiosResponse<ListSiteResourceUsersResponse>
>(`/site-resource/${resourceId}/users`, { signal }); >(`/resource/${resourceId}/users`, { signal });
return res.data.data.users; return res.data.data.users;
} }
}), }),
@@ -222,7 +238,7 @@ export const resourceQueries = {
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<ListSiteResourceRolesResponse> AxiosResponse<ListSiteResourceRolesResponse>
>(`/site-resource/${resourceId}/roles`, { signal }); >(`/resource/${resourceId}/roles`, { signal });
return res.data.data.roles; return res.data.data.roles;
} }
@@ -233,11 +249,33 @@ export const resourceQueries = {
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<ListSiteResourceClientsResponse> AxiosResponse<ListSiteResourceClientsResponse>
>(`/site-resource/${resourceId}/clients`, { signal }); >(`/resource/${resourceId}/clients`, { signal });
return res.data.data.clients; 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) => listNamesPerOrg: (orgId: string) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES_NAMES", orgId] as const, queryKey: ["RESOURCES_NAMES", orgId] as const,

View File

@@ -10,36 +10,15 @@ interface OrgProviderProps {
org: GetOrgResponse | null; org: GetOrgResponse | null;
} }
export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) { export function OrgProvider({ children, org }: OrgProviderProps) {
const [org, setOrg] = useState<GetOrgResponse | null>(serverOrg);
const t = useTranslations(); const t = useTranslations();
if (!org) { if (!org) {
throw new Error(t("orgErrorNoProvided")); 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 ( return (
<OrgContext.Provider value={{ org, updateOrg }}> <OrgContext.Provider value={{ org }}>{children}</OrgContext.Provider>
{children}
</OrgContext.Provider>
); );
} }