mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-10 04:36:38 +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
|
// 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) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 !==
|
|
||||||
initialSecurityValues.passwordExpiryDays
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function deleteOrg() {
|
<LogRetentionSectionForm org={org.org} />
|
||||||
setLoadingDelete(true);
|
|
||||||
try {
|
{build !== "oss" && (
|
||||||
const res = await api.delete<AxiosResponse<DeleteOrgResponse>>(
|
<SecuritySettingsSectionForm org={org.org} />
|
||||||
`/org/${org?.org.orgId}`
|
)}
|
||||||
|
{build !== "saas" && <DeleteForm org={org.org} />}
|
||||||
|
</div>
|
||||||
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
toast({
|
}
|
||||||
title: t("orgDeleted"),
|
|
||||||
description: t("orgDeletedMessage")
|
type SectionFormProps = {
|
||||||
});
|
org: OrgContextType["org"]["org"];
|
||||||
if (res.status === 200) {
|
};
|
||||||
pickNewOrgAndNavigate();
|
|
||||||
}
|
function DeleteForm({ org }: SectionFormProps) {
|
||||||
} catch (err) {
|
const t = useTranslations();
|
||||||
console.error(err);
|
const api = createApiClient(useEnvContext());
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
const router = useRouter();
|
||||||
title: t("orgErrorDelete"),
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
description: formatAxiosError(err, t("orgErrorDeleteMessage"))
|
const [loadingDelete, startTransition] = useTransition();
|
||||||
});
|
const { user } = useUserContext();
|
||||||
} 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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }>;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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 })
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user