mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-01 15:49:08 +00:00
Merge dev into fix/log-analytics-adjustments
This commit is contained in:
@@ -10,7 +10,7 @@ import { GetOrgUserResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type BillingSettingsProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -19,7 +19,7 @@ type BillingSettingsProps = {
|
||||
|
||||
export default async function BillingSettingsPage({
|
||||
children,
|
||||
params,
|
||||
params
|
||||
}: BillingSettingsProps) {
|
||||
const { orgId } = await params;
|
||||
|
||||
@@ -35,8 +35,8 @@ export default async function BillingSettingsPage({
|
||||
const getOrgUser = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
`/org/${orgId}/user/${user.userId}`,
|
||||
await authCookieHeader(),
|
||||
),
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrgUser();
|
||||
orgUser = res.data.data;
|
||||
@@ -49,8 +49,8 @@ export default async function BillingSettingsPage({
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${orgId}`,
|
||||
await authCookieHeader(),
|
||||
),
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
org = res.data.data;
|
||||
@@ -65,11 +65,11 @@ export default async function BillingSettingsPage({
|
||||
<OrgProvider org={org}>
|
||||
<OrgUserProvider orgUser={orgUser}>
|
||||
<SettingsSectionTitle
|
||||
title={t('billing')}
|
||||
description={t('orgBillingDescription')}
|
||||
title={t("billing")}
|
||||
description={t("orgBillingDescription")}
|
||||
/>
|
||||
|
||||
{children}
|
||||
{children}
|
||||
</OrgUserProvider>
|
||||
</OrgProvider>
|
||||
</>
|
||||
|
||||
@@ -64,10 +64,8 @@ export default function Page() {
|
||||
clientSecret: z
|
||||
.string()
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") })
|
||||
.optional(),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") })
|
||||
.optional(),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }).optional(),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }).optional(),
|
||||
identifierPath: z
|
||||
.string()
|
||||
.min(1, { message: t("idpPathRequired") })
|
||||
@@ -379,9 +377,11 @@ export default function Page() {
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
control={form.control}
|
||||
autoProvision={form.watch(
|
||||
"autoProvision"
|
||||
) as boolean} // is this right?
|
||||
autoProvision={
|
||||
form.watch(
|
||||
"autoProvision"
|
||||
) as boolean
|
||||
} // is this right?
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
|
||||
@@ -19,18 +19,17 @@ export function ExitNodesDataTable<TData, TValue>({
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title={t('remoteExitNodes')}
|
||||
searchPlaceholder={t('searchRemoteExitNodes')}
|
||||
title={t("remoteExitNodes")}
|
||||
searchPlaceholder={t("searchRemoteExitNodes")}
|
||||
searchColumn="name"
|
||||
onAdd={createRemoteExitNode}
|
||||
addButtonText={t('remoteExitNodeAdd')}
|
||||
addButtonText={t("remoteExitNodeAdd")}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('credentials'),
|
||||
title: t("credentials"),
|
||||
href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/credentials"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function CreateRemoteExitNodePage() {
|
||||
useEffect(() => {
|
||||
const remoteExitNodeId = searchParams.get("remoteExitNodeId");
|
||||
const remoteExitNodeSecret = searchParams.get("remoteExitNodeSecret");
|
||||
|
||||
|
||||
if (remoteExitNodeId && remoteExitNodeSecret) {
|
||||
setStrategy("adopt");
|
||||
form.setValue("remoteExitNodeId", remoteExitNodeId);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable";
|
||||
import InvitationsTable, {
|
||||
InvitationRow
|
||||
} from "../../../../../components/InvitationsTable";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { cache } from "react";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
@@ -9,7 +11,7 @@ import UserProvider from "@app/providers/UserProvider";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type InvitationsPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -68,7 +70,7 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
||||
id: invite.inviteId,
|
||||
email: invite.email,
|
||||
expiresAt: new Date(Number(invite.expiresAt)).toISOString(),
|
||||
role: invite.roleName || t('accessRoleUnknown'),
|
||||
role: invite.roleName || t("accessRoleUnknown"),
|
||||
roleId: invite.roleId
|
||||
};
|
||||
});
|
||||
@@ -76,8 +78,8 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('inviteTitle')}
|
||||
description={t('inviteDescription')}
|
||||
title={t("inviteTitle")}
|
||||
description={t("inviteDescription")}
|
||||
/>
|
||||
<UserProvider user={user!}>
|
||||
<OrgProvider org={org}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
import RolesTable, { RoleRow } from "../../../../../components/RolesTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type RolesPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -66,8 +66,8 @@ export default async function RolesPage(props: RolesPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('accessRolesManage')}
|
||||
description={t('accessRolesDescription')}
|
||||
title={t("accessRolesManage")}
|
||||
description={t("accessRolesDescription")}
|
||||
/>
|
||||
<OrgProvider org={org}>
|
||||
<RolesTable roles={roleRows} />
|
||||
|
||||
@@ -106,7 +106,8 @@ export default function Page() {
|
||||
|
||||
const genericOidcFormSchema = z.object({
|
||||
username: z.string().min(1, { message: t("usernameRequired") }),
|
||||
email: z.email({ message: t("emailInvalid") })
|
||||
email: z
|
||||
.email({ message: t("emailInvalid") })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
name: z.string().optional(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import UserProvider from "@app/providers/UserProvider";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type UsersPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -79,9 +79,11 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
type: user.type,
|
||||
idpVariant: user.idpVariant,
|
||||
idpId: user.idpId,
|
||||
idpName: user.idpName || t('idpNameInternal'),
|
||||
status: t('userConfirmed'),
|
||||
role: user.isOwner ? t('accessRoleOwner') : user.roleName || t('accessRoleMember'),
|
||||
idpName: user.idpName || t("idpNameInternal"),
|
||||
status: t("userConfirmed"),
|
||||
role: user.isOwner
|
||||
? t("accessRoleOwner")
|
||||
: user.roleName || t("accessRoleMember"),
|
||||
isOwner: user.isOwner || false
|
||||
};
|
||||
});
|
||||
@@ -89,8 +91,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('accessUsersManage')}
|
||||
description={t('accessUsersDescription')}
|
||||
title={t("accessUsersManage")}
|
||||
description={t("accessUsersDescription")}
|
||||
/>
|
||||
<UserProvider user={user!}>
|
||||
<OrgProvider org={org}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -33,14 +33,16 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('apiKeysPermissionsTitle'),
|
||||
title: t("apiKeysPermissionsTitle"),
|
||||
href: "/{orgId}/settings/api-keys/{apiKeyId}/permissions"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle title={t('apiKeysSettings', {apiKeyName: apiKey?.name})} />
|
||||
<SettingsSectionTitle
|
||||
title={t("apiKeysSettings", { apiKeyName: apiKey?.name })}
|
||||
/>
|
||||
|
||||
<ApiKeyProvider apiKey={apiKey}>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
|
||||
@@ -4,5 +4,7 @@ export default async function ApiKeysPage(props: {
|
||||
params: Promise<{ orgId: string; apiKeyId: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`);
|
||||
redirect(
|
||||
`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysPermissionsErrorLoadingActions'),
|
||||
title: t("apiKeysPermissionsErrorLoadingActions"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('apiKeysPermissionsErrorLoadingActions')
|
||||
t("apiKeysPermissionsErrorLoadingActions")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -79,18 +79,18 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysErrorSetPermission'), e);
|
||||
console.error(t("apiKeysErrorSetPermission"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorSetPermission'),
|
||||
title: t("apiKeysErrorSetPermission"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (actionsRes && actionsRes.status === 200) {
|
||||
toast({
|
||||
title: t('apiKeysPermissionsUpdated'),
|
||||
description: t('apiKeysPermissionsUpdatedDescription')
|
||||
title: t("apiKeysPermissionsUpdated"),
|
||||
description: t("apiKeysPermissionsUpdatedDescription")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,10 +104,12 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysPermissionsGeneralSettings')}
|
||||
{t("apiKeysPermissionsGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('apiKeysPermissionsGeneralSettingsDescription')}
|
||||
{t(
|
||||
"apiKeysPermissionsGeneralSettingsDescription"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -124,7 +126,7 @@ export default function Page() {
|
||||
loading={loadingSavePermissions}
|
||||
disabled={loadingSavePermissions}
|
||||
>
|
||||
{t('apiKeysPermissionsSave')}
|
||||
{t("apiKeysPermissionsSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSectionBody>
|
||||
|
||||
@@ -66,10 +66,10 @@ export default function Page() {
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: t('nameMin', {len: 2})
|
||||
message: t("nameMin", { len: 2 })
|
||||
})
|
||||
.max(255, {
|
||||
message: t('nameMax', {len: 255})
|
||||
message: t("nameMax", { len: 255 })
|
||||
})
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function Page() {
|
||||
return data.copied;
|
||||
},
|
||||
{
|
||||
message: t('apiKeysConfirmCopy2'),
|
||||
message: t("apiKeysConfirmCopy2"),
|
||||
path: ["copied"]
|
||||
}
|
||||
);
|
||||
@@ -119,7 +119,7 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorCreate'),
|
||||
title: t("apiKeysErrorCreate"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -140,10 +140,10 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysErrorSetPermission'), e);
|
||||
console.error(t("apiKeysErrorSetPermission"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorSetPermission'),
|
||||
title: t("apiKeysErrorSetPermission"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -182,8 +182,8 @@ export default function Page() {
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title={t('apiKeysCreate')}
|
||||
description={t('apiKeysCreateDescription')}
|
||||
title={t("apiKeysCreate")}
|
||||
description={t("apiKeysCreateDescription")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -191,7 +191,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}}
|
||||
>
|
||||
{t('apiKeysSeeAll')}
|
||||
{t("apiKeysSeeAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysTitle')}
|
||||
{t("apiKeysTitle")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -224,7 +224,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -245,10 +245,12 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysGeneralSettings')}
|
||||
{t("apiKeysGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('apiKeysGeneralSettingsDescription')}
|
||||
{t(
|
||||
"apiKeysGeneralSettingsDescription"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -267,14 +269,14 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysList')}
|
||||
{t("apiKeysList")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -284,7 +286,7 @@ export default function Page() {
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('created')}
|
||||
{t("created")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{moment(
|
||||
@@ -297,10 +299,10 @@ export default function Page() {
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('apiKeysSave')}
|
||||
{t("apiKeysSave")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('apiKeysSaveDescription')}
|
||||
{t("apiKeysSaveDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -367,7 +369,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{!apiKey && (
|
||||
@@ -379,7 +381,7 @@ export default function Page() {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
{t('generate')}
|
||||
{t("generate")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -390,7 +392,7 @@ export default function Page() {
|
||||
copiedForm.handleSubmit(onCopiedSubmit)();
|
||||
}}
|
||||
>
|
||||
{t('done')}
|
||||
{t("done")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,11 @@ import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import OrgApiKeysTable, { OrgApiKeyRow } from "../../../../components/OrgApiKeysTable";
|
||||
import OrgApiKeysTable, {
|
||||
OrgApiKeyRow
|
||||
} from "../../../../components/OrgApiKeysTable";
|
||||
import { ListOrgApiKeysResponse } from "@server/routers/apiKeys";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type ApiKeyPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -37,8 +39,8 @@ export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('apiKeysManage')}
|
||||
description={t('apiKeysDescription')}
|
||||
title={t("apiKeysManage")}
|
||||
description={t("apiKeysDescription")}
|
||||
/>
|
||||
|
||||
<OrgApiKeysTable apiKeys={rows} orgId={params.orgId} />
|
||||
|
||||
@@ -21,7 +21,10 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
let client = null;
|
||||
try {
|
||||
console.log("making request to ", `/org/${params.orgId}/client/${params.niceId}`);
|
||||
console.log(
|
||||
"making request to ",
|
||||
`/org/${params.orgId}/client/${params.niceId}`
|
||||
);
|
||||
const res = await internal.get<AxiosResponse<GetClientResponse>>(
|
||||
`/org/${params.orgId}/client/${params.niceId}`,
|
||||
await authCookieHeader()
|
||||
|
||||
@@ -10,7 +10,7 @@ import { GetOrgUserResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { cache } from "react";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type GeneralSettingsProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -19,7 +19,7 @@ type GeneralSettingsProps = {
|
||||
|
||||
export default async function GeneralSettingsPage({
|
||||
children,
|
||||
params,
|
||||
params
|
||||
}: GeneralSettingsProps) {
|
||||
const { orgId } = await params;
|
||||
|
||||
@@ -35,8 +35,8 @@ export default async function GeneralSettingsPage({
|
||||
const getOrgUser = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
`/org/${orgId}/user/${user.userId}`,
|
||||
await authCookieHeader(),
|
||||
),
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrgUser();
|
||||
orgUser = res.data.data;
|
||||
@@ -49,8 +49,8 @@ export default async function GeneralSettingsPage({
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${orgId}`,
|
||||
await authCookieHeader(),
|
||||
),
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
org = res.data.data;
|
||||
@@ -62,9 +62,9 @@ export default async function GeneralSettingsPage({
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
href: `/{orgId}/settings/general`,
|
||||
},
|
||||
title: t("general"),
|
||||
href: `/{orgId}/settings/general`
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -72,13 +72,11 @@ export default async function GeneralSettingsPage({
|
||||
<OrgProvider org={org}>
|
||||
<OrgUserProvider orgUser={orgUser}>
|
||||
<SettingsSectionTitle
|
||||
title={t('orgGeneralSettings')}
|
||||
description={t('orgSettingsDescription')}
|
||||
title={t("orgGeneralSettings")}
|
||||
description={t("orgSettingsDescription")}
|
||||
/>
|
||||
|
||||
<HorizontalTabs items={navItems}>
|
||||
{children}
|
||||
</HorizontalTabs>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</OrgUserProvider>
|
||||
</OrgProvider>
|
||||
</>
|
||||
|
||||
@@ -11,12 +11,13 @@ import { Button } from "@app/components/ui/button";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function GeneralPage() {
|
||||
|
||||
@@ -837,7 +837,9 @@ export default function ResourceAuthenticationPage() {
|
||||
<Bot size="14" />
|
||||
<span>
|
||||
{authInfo.headerAuth
|
||||
? t("resourceHeaderAuthProtectionEnabled")
|
||||
? t(
|
||||
"resourceHeaderAuthProtectionEnabled"
|
||||
)
|
||||
: t(
|
||||
"resourceHeaderAuthProtectionDisabled"
|
||||
)}
|
||||
@@ -921,7 +923,8 @@ export default function ResourceAuthenticationPage() {
|
||||
validateTag={(
|
||||
tag
|
||||
) => {
|
||||
return z.email()
|
||||
return z
|
||||
.email()
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function GeneralForm() {
|
||||
name: z.string().min(1).max(255),
|
||||
niceId: z.string().min(1).max(255).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(
|
||||
@@ -134,7 +134,7 @@ export default function GeneralForm() {
|
||||
niceId: resource.niceId,
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
domainId: resource.domainId || undefined,
|
||||
proxyPort: resource.proxyPort || undefined,
|
||||
proxyPort: resource.proxyPort || undefined
|
||||
// enableProxy: resource.enableProxy || false
|
||||
},
|
||||
mode: "onChange"
|
||||
@@ -168,7 +168,7 @@ export default function GeneralForm() {
|
||||
const rawDomains = res.data.data.domains as DomainRow[];
|
||||
const domains = rawDomains.map((domain) => ({
|
||||
...domain,
|
||||
baseDomain: toUnicode(domain.baseDomain),
|
||||
baseDomain: toUnicode(domain.baseDomain)
|
||||
}));
|
||||
setBaseDomains(domains);
|
||||
setFormKey((key) => key + 1);
|
||||
@@ -195,9 +195,11 @@ export default function GeneralForm() {
|
||||
enabled: data.enabled,
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
|
||||
subdomain: data.subdomain
|
||||
? toASCII(data.subdomain)
|
||||
: undefined,
|
||||
domainId: data.domainId,
|
||||
proxyPort: data.proxyPort,
|
||||
proxyPort: data.proxyPort
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
// })
|
||||
@@ -223,7 +225,7 @@ export default function GeneralForm() {
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain,
|
||||
fullDomain: resource.fullDomain,
|
||||
proxyPort: data.proxyPort,
|
||||
proxyPort: data.proxyPort
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
// })
|
||||
@@ -235,7 +237,9 @@ export default function GeneralForm() {
|
||||
});
|
||||
|
||||
if (data.niceId && data.niceId !== resource?.niceId) {
|
||||
router.replace(`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`);
|
||||
router.replace(
|
||||
`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`
|
||||
);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
@@ -320,11 +324,15 @@ export default function GeneralForm() {
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("identifier")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("enterIdentifier")}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -360,10 +368,10 @@ export default function GeneralForm() {
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
@@ -498,17 +506,29 @@ export default function GeneralForm() {
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedDomain) {
|
||||
const sanitizedSubdomain = selectedDomain.subdomain
|
||||
? finalizeSubdomainSanitize(selectedDomain.subdomain)
|
||||
: "";
|
||||
const sanitizedSubdomain =
|
||||
selectedDomain.subdomain
|
||||
? finalizeSubdomainSanitize(
|
||||
selectedDomain.subdomain
|
||||
)
|
||||
: "";
|
||||
|
||||
const sanitizedFullDomain = sanitizedSubdomain
|
||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||
: selectedDomain.baseDomain;
|
||||
const sanitizedFullDomain =
|
||||
sanitizedSubdomain
|
||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||
: selectedDomain.baseDomain;
|
||||
|
||||
setResourceFullDomain(`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`);
|
||||
form.setValue("domainId", selectedDomain.domainId);
|
||||
form.setValue("subdomain", sanitizedSubdomain);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`
|
||||
);
|
||||
form.setValue(
|
||||
"domainId",
|
||||
selectedDomain.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
sanitizedSubdomain
|
||||
);
|
||||
|
||||
setEditDomainOpen(false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { cache } from "react";
|
||||
import ResourceInfoBox from "@app/components/ResourceInfoBox";
|
||||
import { GetSiteResponse } from "@server/routers/site";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface ResourceLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -76,22 +76,22 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
title: t("general"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/general`
|
||||
},
|
||||
{
|
||||
title: t('proxy'),
|
||||
title: t("proxy"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/proxy`
|
||||
}
|
||||
];
|
||||
|
||||
if (resource.http) {
|
||||
navItems.push({
|
||||
title: t('authentication'),
|
||||
title: t("authentication"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/authentication`
|
||||
});
|
||||
navItems.push({
|
||||
title: t('rules'),
|
||||
title: t("rules"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
|
||||
});
|
||||
}
|
||||
@@ -99,15 +99,12 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('resourceSetting', {resourceName: resource?.name})}
|
||||
description={t('resourceSettingDescription')}
|
||||
title={t("resourceSetting", { resourceName: resource?.name })}
|
||||
description={t("resourceSettingDescription")}
|
||||
/>
|
||||
|
||||
<OrgProvider org={org}>
|
||||
<ResourceProvider
|
||||
resource={resource}
|
||||
authInfo={authInfo}
|
||||
>
|
||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||
<div className="space-y-6">
|
||||
<ResourceInfoBox />
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -114,23 +114,25 @@ export default function ResourceRules(props: {
|
||||
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
|
||||
const [openCountrySelect, setOpenCountrySelect] = useState(false);
|
||||
const [countrySelectValue, setCountrySelectValue] = useState("");
|
||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
||||
useState(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||
const isMaxmindAvailable =
|
||||
env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||
|
||||
const RuleAction = {
|
||||
ACCEPT: t('alwaysAllow'),
|
||||
DROP: t('alwaysDeny'),
|
||||
PASS: t('passToAuth')
|
||||
ACCEPT: t("alwaysAllow"),
|
||||
DROP: t("alwaysDeny"),
|
||||
PASS: t("passToAuth")
|
||||
} as const;
|
||||
|
||||
const RuleMatch = {
|
||||
PATH: t('path'),
|
||||
PATH: t("path"),
|
||||
IP: "IP",
|
||||
CIDR: t('ipAddressRange'),
|
||||
COUNTRY: t('country')
|
||||
CIDR: t("ipAddressRange"),
|
||||
COUNTRY: t("country")
|
||||
} as const;
|
||||
|
||||
const addRuleForm = useForm({
|
||||
@@ -155,10 +157,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorFetch'),
|
||||
title: t("rulesErrorFetch"),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
t('rulesErrorFetchDescription')
|
||||
t("rulesErrorFetchDescription")
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -179,8 +181,8 @@ export default function ResourceRules(props: {
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorDuplicate'),
|
||||
description: t('rulesErrorDuplicateDescription')
|
||||
title: t("rulesErrorDuplicate"),
|
||||
description: t("rulesErrorDuplicateDescription")
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -188,8 +190,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "CIDR" && !isValidCIDR(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddressRange'),
|
||||
description: t('rulesErrorInvalidIpAddressRangeDescription')
|
||||
title: t("rulesErrorInvalidIpAddressRange"),
|
||||
description: t("rulesErrorInvalidIpAddressRangeDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -197,8 +199,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidUrl'),
|
||||
description: t('rulesErrorInvalidUrlDescription')
|
||||
title: t("rulesErrorInvalidUrl"),
|
||||
description: t("rulesErrorInvalidUrlDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -206,17 +208,22 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "IP" && !isValidIP(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddress'),
|
||||
description: t('rulesErrorInvalidIpAddressDescription')
|
||||
title: t("rulesErrorInvalidIpAddress"),
|
||||
description: t("rulesErrorInvalidIpAddressDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (data.match === "COUNTRY" && !COUNTRIES.some(c => c.code === data.value)) {
|
||||
if (
|
||||
data.match === "COUNTRY" &&
|
||||
!COUNTRIES.some((c) => c.code === data.value)
|
||||
) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidCountry'),
|
||||
description: t('rulesErrorInvalidCountryDescription') || "Invalid country code."
|
||||
title: t("rulesErrorInvalidCountry"),
|
||||
description:
|
||||
t("rulesErrorInvalidCountryDescription") ||
|
||||
"Invalid country code."
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -265,13 +272,13 @@ export default function ResourceRules(props: {
|
||||
function getValueHelpText(type: string) {
|
||||
switch (type) {
|
||||
case "CIDR":
|
||||
return t('rulesMatchIpAddressRangeDescription');
|
||||
return t("rulesMatchIpAddressRangeDescription");
|
||||
case "IP":
|
||||
return t('rulesMatchIpAddress');
|
||||
return t("rulesMatchIpAddress");
|
||||
case "PATH":
|
||||
return t('rulesMatchUrl');
|
||||
return t("rulesMatchUrl");
|
||||
case "COUNTRY":
|
||||
return t('rulesMatchCountry');
|
||||
return t("rulesMatchCountry");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,10 +295,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorUpdate'),
|
||||
title: t("rulesErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
t('rulesErrorUpdateDescription')
|
||||
t("rulesErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
throw err;
|
||||
@@ -314,8 +321,10 @@ export default function ResourceRules(props: {
|
||||
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddressRange'),
|
||||
description: t('rulesErrorInvalidIpAddressRangeDescription')
|
||||
title: t("rulesErrorInvalidIpAddressRange"),
|
||||
description: t(
|
||||
"rulesErrorInvalidIpAddressRangeDescription"
|
||||
)
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -326,8 +335,8 @@ export default function ResourceRules(props: {
|
||||
) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidUrl'),
|
||||
description: t('rulesErrorInvalidUrlDescription')
|
||||
title: t("rulesErrorInvalidUrl"),
|
||||
description: t("rulesErrorInvalidUrlDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -335,8 +344,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddress'),
|
||||
description: t('rulesErrorInvalidIpAddressDescription')
|
||||
title: t("rulesErrorInvalidIpAddress"),
|
||||
description: t("rulesErrorInvalidIpAddressDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -345,8 +354,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.priority === undefined) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidPriority'),
|
||||
description: t('rulesErrorInvalidPriorityDescription')
|
||||
title: t("rulesErrorInvalidPriority"),
|
||||
description: t("rulesErrorInvalidPriorityDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -357,8 +366,8 @@ export default function ResourceRules(props: {
|
||||
if (priorities.length !== new Set(priorities).size) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorDuplicatePriority'),
|
||||
description: t('rulesErrorDuplicatePriorityDescription')
|
||||
title: t("rulesErrorDuplicatePriority"),
|
||||
description: t("rulesErrorDuplicatePriorityDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -397,8 +406,8 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t('ruleUpdated'),
|
||||
description: t('ruleUpdatedDescription')
|
||||
title: t("ruleUpdated"),
|
||||
description: t("ruleUpdatedDescription")
|
||||
});
|
||||
|
||||
setRulesToRemove([]);
|
||||
@@ -407,10 +416,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('ruleErrorUpdate'),
|
||||
title: t("ruleErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
t('ruleErrorUpdateDescription')
|
||||
t("ruleErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -428,7 +437,7 @@ export default function ResourceRules(props: {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t('rulesPriority')}
|
||||
{t("rulesPriority")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -440,15 +449,18 @@ export default function ResourceRules(props: {
|
||||
type="number"
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onBlur={(e) => {
|
||||
const parsed = z.int()
|
||||
const parsed = z
|
||||
.int()
|
||||
.optional()
|
||||
.safeParse(e.target.value);
|
||||
|
||||
if (!parsed.data) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddress'), // correct priority or IP?
|
||||
description: t('rulesErrorInvalidPriorityDescription')
|
||||
title: t("rulesErrorInvalidIpAddress"), // correct priority or IP?
|
||||
description: t(
|
||||
"rulesErrorInvalidPriorityDescription"
|
||||
)
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -463,7 +475,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: () => (<span className="p-3">{t('rulesAction')}</span>),
|
||||
header: () => <span className="p-3">{t("rulesAction")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.action}
|
||||
@@ -486,12 +498,18 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "match",
|
||||
header: () => (<span className="p-3">{t('rulesMatchType')}</span>),
|
||||
header: () => <span className="p-3">{t("rulesMatchType")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.match}
|
||||
onValueChange={(value: "CIDR" | "IP" | "PATH" | "COUNTRY") =>
|
||||
updateRule(row.original.ruleId, { match: value, value: value === "COUNTRY" ? "US" : row.original.value })
|
||||
onValueChange={(
|
||||
value: "CIDR" | "IP" | "PATH" | "COUNTRY"
|
||||
) =>
|
||||
updateRule(row.original.ruleId, {
|
||||
match: value,
|
||||
value:
|
||||
value === "COUNTRY" ? "US" : row.original.value
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="min-w-[125px]">
|
||||
@@ -502,7 +520,9 @@ export default function ResourceRules(props: {
|
||||
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
|
||||
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
|
||||
{isMaxmindAvailable && (
|
||||
<SelectItem value="COUNTRY">{RuleMatch.COUNTRY}</SelectItem>
|
||||
<SelectItem value="COUNTRY">
|
||||
{RuleMatch.COUNTRY}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -510,8 +530,8 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: () => (<span className="p-3">{t('value')}</span>),
|
||||
cell: ({ row }) => (
|
||||
header: () => <span className="p-3">{t("value")}</span>,
|
||||
cell: ({ row }) =>
|
||||
row.original.match === "COUNTRY" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -521,29 +541,43 @@ export default function ResourceRules(props: {
|
||||
className="min-w-[200px] justify-between"
|
||||
>
|
||||
{row.original.value
|
||||
? COUNTRIES.find((country) => country.code === row.original.value)?.name +
|
||||
" (" + row.original.value + ")"
|
||||
: t('selectCountry')}
|
||||
? COUNTRIES.find(
|
||||
(country) =>
|
||||
country.code ===
|
||||
row.original.value
|
||||
)?.name +
|
||||
" (" +
|
||||
row.original.value +
|
||||
")"
|
||||
: t("selectCountry")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="min-w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('searchCountries')} />
|
||||
<CommandInput
|
||||
placeholder={t("searchCountries")}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
{t("noCountryFound")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{COUNTRIES.map((country) => (
|
||||
<CommandItem
|
||||
key={country.code}
|
||||
value={country.name}
|
||||
onSelect={() => {
|
||||
updateRule(row.original.ruleId, { value: country.code });
|
||||
updateRule(
|
||||
row.original.ruleId,
|
||||
{ value: country.code }
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
row.original.value === country.code
|
||||
row.original.value ===
|
||||
country.code
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
@@ -567,11 +601,10 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: () => (<span className="p-3">{t('enabled')}</span>),
|
||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
@@ -583,14 +616,14 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t('actions')}</span>),
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => removeRule(row.original.ruleId)}
|
||||
>
|
||||
{t('delete')}
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -664,10 +697,10 @@ export default function ResourceRules(props: {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('rulesResource')}
|
||||
{t("rulesResource")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('rulesResourceDescription')}
|
||||
{t("rulesResourceDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -675,7 +708,7 @@ export default function ResourceRules(props: {
|
||||
<div className="flex items-center space-x-2">
|
||||
<SwitchInput
|
||||
id="rules-toggle"
|
||||
label={t('rulesEnable')}
|
||||
label={t("rulesEnable")}
|
||||
defaultChecked={rulesEnabled}
|
||||
onCheckedChange={(val) => setRulesEnabled(val)}
|
||||
/>
|
||||
@@ -692,7 +725,9 @@ export default function ResourceRules(props: {
|
||||
name="action"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('rulesAction')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("rulesAction")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
@@ -705,13 +740,19 @@ export default function ResourceRules(props: {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ACCEPT">
|
||||
{RuleAction.ACCEPT}
|
||||
{
|
||||
RuleAction.ACCEPT
|
||||
}
|
||||
</SelectItem>
|
||||
<SelectItem value="DROP">
|
||||
{RuleAction.DROP}
|
||||
{
|
||||
RuleAction.DROP
|
||||
}
|
||||
</SelectItem>
|
||||
<SelectItem value="PASS">
|
||||
{RuleAction.PASS}
|
||||
{
|
||||
RuleAction.PASS
|
||||
}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -725,11 +766,15 @@ export default function ResourceRules(props: {
|
||||
name="match"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('rulesMatchType')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("rulesMatchType")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
@@ -737,7 +782,9 @@ export default function ResourceRules(props: {
|
||||
<SelectContent>
|
||||
{resource.http && (
|
||||
<SelectItem value="PATH">
|
||||
{RuleMatch.PATH}
|
||||
{
|
||||
RuleMatch.PATH
|
||||
}
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem value="IP">
|
||||
@@ -748,7 +795,9 @@ export default function ResourceRules(props: {
|
||||
</SelectItem>
|
||||
{isMaxmindAvailable && (
|
||||
<SelectItem value="COUNTRY">
|
||||
{RuleMatch.COUNTRY}
|
||||
{
|
||||
RuleMatch.COUNTRY
|
||||
}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
@@ -764,7 +813,7 @@ export default function ResourceRules(props: {
|
||||
render={({ field }) => (
|
||||
<FormItem className="gap-1">
|
||||
<InfoPopup
|
||||
text={t('value')}
|
||||
text={t("value")}
|
||||
info={
|
||||
getValueHelpText(
|
||||
addRuleForm.watch(
|
||||
@@ -774,47 +823,100 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
/>
|
||||
<FormControl>
|
||||
{addRuleForm.watch("match") === "COUNTRY" ? (
|
||||
<Popover open={openAddRuleCountrySelect} onOpenChange={setOpenAddRuleCountrySelect}>
|
||||
<PopoverTrigger asChild>
|
||||
{addRuleForm.watch(
|
||||
"match"
|
||||
) === "COUNTRY" ? (
|
||||
<Popover
|
||||
open={
|
||||
openAddRuleCountrySelect
|
||||
}
|
||||
onOpenChange={
|
||||
setOpenAddRuleCountrySelect
|
||||
}
|
||||
>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openAddRuleCountrySelect}
|
||||
aria-expanded={
|
||||
openAddRuleCountrySelect
|
||||
}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{field.value
|
||||
? COUNTRIES.find((country) => country.code === field.value)?.name +
|
||||
" (" + field.value + ")"
|
||||
: t('selectCountry')}
|
||||
? COUNTRIES.find(
|
||||
(
|
||||
country
|
||||
) =>
|
||||
country.code ===
|
||||
field.value
|
||||
)
|
||||
?.name +
|
||||
" (" +
|
||||
field.value +
|
||||
")"
|
||||
: t(
|
||||
"selectCountry"
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('searchCountries')} />
|
||||
<CommandInput
|
||||
placeholder={t(
|
||||
"searchCountries"
|
||||
)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
{t(
|
||||
"noCountryFound"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{COUNTRIES.map((country) => (
|
||||
<CommandItem
|
||||
key={country.code}
|
||||
value={country.name}
|
||||
onSelect={() => {
|
||||
field.onChange(country.code);
|
||||
setOpenAddRuleCountrySelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
field.value === country.code
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{country.name} ({country.code})
|
||||
</CommandItem>
|
||||
))}
|
||||
{COUNTRIES.map(
|
||||
(
|
||||
country
|
||||
) => (
|
||||
<CommandItem
|
||||
key={
|
||||
country.code
|
||||
}
|
||||
value={
|
||||
country.name
|
||||
}
|
||||
onSelect={() => {
|
||||
field.onChange(
|
||||
country.code
|
||||
);
|
||||
setOpenAddRuleCountrySelect(
|
||||
false
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
field.value ===
|
||||
country.code
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{
|
||||
country.name
|
||||
}{" "}
|
||||
(
|
||||
{
|
||||
country.code
|
||||
}
|
||||
|
||||
)
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
@@ -833,7 +935,7 @@ export default function ResourceRules(props: {
|
||||
variant="outline"
|
||||
disabled={!rulesEnabled}
|
||||
>
|
||||
{t('ruleSubmit')}
|
||||
{t("ruleSubmit")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -843,16 +945,22 @@ export default function ResourceRules(props: {
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const isActionsColumn = header.column.id === "actions";
|
||||
const isActionsColumn =
|
||||
header.column.id === "actions";
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef
|
||||
header.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
@@ -866,20 +974,30 @@ export default function ResourceRules(props: {
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const isActionsColumn = cell.column.id === "actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const isActionsColumn =
|
||||
cell.column.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
@@ -888,7 +1006,7 @@ export default function ResourceRules(props: {
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t('rulesNoOne')}
|
||||
{t("rulesNoOne")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -907,7 +1025,7 @@ export default function ResourceRules(props: {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('saveAllSettings')}
|
||||
{t("saveAllSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { cache } from "react";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListAccessTokensResponse } from "@server/routers/accessToken";
|
||||
import ShareLinksTable, { ShareLinkRow } from "../../../../components/ShareLinksTable";
|
||||
import ShareLinksTable, {
|
||||
ShareLinkRow
|
||||
} from "../../../../components/ShareLinksTable";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type ShareLinksPageProps = {
|
||||
@@ -58,8 +60,8 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) {
|
||||
{/* <ShareableLinksSplash /> */}
|
||||
|
||||
<SettingsSectionTitle
|
||||
title={t('shareTitle')}
|
||||
description={t('shareDescription')}
|
||||
title={t("shareTitle")}
|
||||
description={t("shareDescription")}
|
||||
/>
|
||||
|
||||
<OrgProvider org={org}>
|
||||
|
||||
@@ -37,7 +37,7 @@ import Link from "next/link";
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
dockerSocketEnabled: z.boolean().optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -52,14 +52,16 @@ export default function GeneralPage() {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(null);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: site?.name,
|
||||
niceId: site?.niceId || "",
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -71,17 +73,19 @@ export default function GeneralPage() {
|
||||
await api.post(`/site/${site?.siteId}`, {
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
});
|
||||
|
||||
updateSite({
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
});
|
||||
|
||||
if (data.niceId && data.niceId !== site?.niceId) {
|
||||
router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`);
|
||||
router.replace(
|
||||
`/${site?.orgId}/settings/sites/${data.niceId}/general`
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
@@ -92,7 +96,10 @@ export default function GeneralPage() {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorUpdate"),
|
||||
description: formatAxiosError(e, t("siteErrorUpdateDescription"))
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("siteErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,11 +147,15 @@ export default function GeneralPage() {
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("identifier")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("enterIdentifier")}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
function gf(init: number[] | undefined = undefined) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
for (var i = 0; i < init.length; ++i) r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function pack(o: Uint8Array, n: Float64Array) {
|
||||
var b, m = gf(), t = gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
var b,
|
||||
m = gf(),
|
||||
t = gf();
|
||||
for (var i = 0; i < 16; ++i) t[i] = n[i];
|
||||
carry(t);
|
||||
carry(t);
|
||||
carry(t);
|
||||
@@ -45,7 +45,8 @@ function carry(o: Float64Array) {
|
||||
}
|
||||
|
||||
function cswap(p: Float64Array, q: Float64Array, b: number) {
|
||||
var t, c = ~(b - 1);
|
||||
var t,
|
||||
c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
@@ -54,40 +55,32 @@ function cswap(p: Float64Array, q: Float64Array, b: number) {
|
||||
}
|
||||
|
||||
function add(o: Float64Array, a: Float64Array, b: Float64Array) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
for (var i = 0; i < 16; ++i) o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
function subtract(o: Float64Array, a: Float64Array, b: Float64Array) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
for (var i = 0; i < 16; ++i) o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
function multmod(o: Float64Array, a: Float64Array, b: Float64Array) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
for (var j = 0; j < 16; ++j) t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
for (var i = 0; i < 15; ++i) t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i) o[i] = t[i];
|
||||
carry(o);
|
||||
carry(o);
|
||||
}
|
||||
|
||||
function invert(o: Float64Array, i: Float64Array) {
|
||||
var c = gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 0; a < 16; ++a) c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
multmod(c, c, i);
|
||||
if (a !== 2 && a !== 4) multmod(c, c, i);
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
for (var a = 0; a < 16; ++a) o[a] = c[a];
|
||||
}
|
||||
|
||||
function clamp(z: Uint8Array) {
|
||||
@@ -96,7 +89,8 @@ function clamp(z: Uint8Array) {
|
||||
}
|
||||
|
||||
function generatePublicKey(privateKey: Uint8Array) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var r,
|
||||
z = new Uint8Array(32);
|
||||
var a = gf([1]),
|
||||
b = gf([9]),
|
||||
c = gf(),
|
||||
@@ -105,8 +99,7 @@ function generatePublicKey(privateKey: Uint8Array) {
|
||||
f = gf(),
|
||||
_121665 = gf([0xdb41, 1]),
|
||||
_9 = gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
for (var i = 0; i < 32; ++i) z[i] = privateKey[i];
|
||||
clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
@@ -152,9 +145,16 @@ function generatePrivateKey() {
|
||||
}
|
||||
|
||||
function encodeBase64(dest: Uint8Array, src: Uint8Array) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
var input = Uint8Array.from([
|
||||
(src[0] >> 2) & 63,
|
||||
((src[0] << 4) | (src[1] >> 4)) & 63,
|
||||
((src[1] << 2) | (src[2] >> 6)) & 63,
|
||||
src[2] & 63
|
||||
]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
dest[i] =
|
||||
input[i] +
|
||||
65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
@@ -162,10 +162,14 @@ function encodeBase64(dest: Uint8Array, src: Uint8Array) {
|
||||
}
|
||||
|
||||
function keyToBase64(key: Uint8Array) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
var i,
|
||||
base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
|
||||
encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
|
||||
encodeBase64(
|
||||
base64.subarray(i * 4),
|
||||
Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0])
|
||||
);
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64 as any);
|
||||
}
|
||||
@@ -177,4 +181,4 @@ export function generateKeypair() {
|
||||
publicKey: keyToBase64(publicKey),
|
||||
privateKey: keyToBase64(privateKey)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,6 @@ export default function Page() {
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
|
||||
const hydrateCommands = (
|
||||
id: string,
|
||||
secret: string,
|
||||
@@ -753,7 +752,9 @@ WantedBy=default.target`
|
||||
{tunnelTypes.length > 1 && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<span className="text-sm font-medium">{t("type")}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
<StrategySelect
|
||||
options={tunnelTypes}
|
||||
|
||||
@@ -31,11 +31,11 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
return "-"; // because we are not able to track the data use in a local site right now
|
||||
}
|
||||
if (mb >= 1024 * 1024) {
|
||||
return t('terabytes', {count: (mb / (1024 * 1024)).toFixed(2)});
|
||||
return t("terabytes", { count: (mb / (1024 * 1024)).toFixed(2) });
|
||||
} else if (mb >= 1024) {
|
||||
return t('gigabytes', {count: (mb / 1024).toFixed(2)});
|
||||
return t("gigabytes", { count: (mb / 1024).toFixed(2) });
|
||||
} else {
|
||||
return t('megabytes', {count: mb.toFixed(2)});
|
||||
return t("megabytes", { count: mb.toFixed(2) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
newtVersion: site.newtVersion || undefined,
|
||||
newtUpdateAvailable: site.newtUpdateAvailable || false,
|
||||
exitNodeName: site.exitNodeName || undefined,
|
||||
exitNodeEndpoint: site.exitNodeEndpoint || undefined,
|
||||
exitNodeEndpoint: site.exitNodeEndpoint || undefined
|
||||
};
|
||||
});
|
||||
|
||||
@@ -62,8 +62,8 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
{/* <SitesSplashCard /> */}
|
||||
|
||||
<SettingsSectionTitle
|
||||
title={t('siteManageSites')}
|
||||
description={t('siteDescription')}
|
||||
title={t("siteManageSites")}
|
||||
description={t("siteDescription")}
|
||||
/>
|
||||
|
||||
<SitesTable sites={siteRows} orgId={params.orgId} />
|
||||
|
||||
@@ -34,14 +34,16 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('apiKeysPermissionsTitle'),
|
||||
title: t("apiKeysPermissionsTitle"),
|
||||
href: "/admin/api-keys/{apiKeyId}/permissions"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle title={t('apiKeysSettings', {apiKeyName: apiKey?.name})} />
|
||||
<SettingsSectionTitle
|
||||
title={t("apiKeysSettings", { apiKeyName: apiKey?.name })}
|
||||
/>
|
||||
|
||||
<ApiKeyProvider apiKey={apiKey}>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysPermissionsErrorLoadingActions'),
|
||||
title: t("apiKeysPermissionsErrorLoadingActions"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('apiKeysPermissionsErrorLoadingActions')
|
||||
t("apiKeysPermissionsErrorLoadingActions")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -79,18 +79,18 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysErrorSetPermission'), e);
|
||||
console.error(t("apiKeysErrorSetPermission"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorSetPermission'),
|
||||
title: t("apiKeysErrorSetPermission"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (actionsRes && actionsRes.status === 200) {
|
||||
toast({
|
||||
title: t('apiKeysPermissionsUpdated'),
|
||||
description: t('apiKeysPermissionsUpdatedDescription')
|
||||
title: t("apiKeysPermissionsUpdated"),
|
||||
description: t("apiKeysPermissionsUpdatedDescription")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,10 +104,12 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysPermissionsTitle')}
|
||||
{t("apiKeysPermissionsTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('apiKeysPermissionsGeneralSettingsDescription')}
|
||||
{t(
|
||||
"apiKeysPermissionsGeneralSettingsDescription"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -125,7 +127,7 @@ export default function Page() {
|
||||
loading={loadingSavePermissions}
|
||||
disabled={loadingSavePermissions}
|
||||
>
|
||||
{t('apiKeysPermissionsSave')}
|
||||
{t("apiKeysPermissionsSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSectionBody>
|
||||
|
||||
@@ -30,7 +30,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
CreateOrgApiKeyBody,
|
||||
CreateOrgApiKeyResponse
|
||||
@@ -64,10 +64,10 @@ export default function Page() {
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: t('nameMin', {len: 2})
|
||||
message: t("nameMin", { len: 2 })
|
||||
})
|
||||
.max(255, {
|
||||
message: t('nameMax', {len: 255})
|
||||
message: t("nameMax", { len: 255 })
|
||||
})
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function Page() {
|
||||
return data.copied;
|
||||
},
|
||||
{
|
||||
message: t('apiKeysConfirmCopy2'),
|
||||
message: t("apiKeysConfirmCopy2"),
|
||||
path: ["copied"]
|
||||
}
|
||||
);
|
||||
@@ -115,7 +115,7 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorCreate'),
|
||||
title: t("apiKeysErrorCreate"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -136,10 +136,10 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysErrorSetPermission'), e);
|
||||
console.error(t("apiKeysErrorSetPermission"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorSetPermission'),
|
||||
title: t("apiKeysErrorSetPermission"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -172,8 +172,8 @@ export default function Page() {
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title={t('apiKeysCreate')}
|
||||
description={t('apiKeysCreateDescription')}
|
||||
title={t("apiKeysCreate")}
|
||||
description={t("apiKeysCreateDescription")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -181,7 +181,7 @@ export default function Page() {
|
||||
router.push(`/admin/api-keys`);
|
||||
}}
|
||||
>
|
||||
{t('apiKeysSeeAll')}
|
||||
{t("apiKeysSeeAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +193,7 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysTitle')}
|
||||
{t("apiKeysTitle")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -214,7 +214,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -235,10 +235,12 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysGeneralSettings')}
|
||||
{t("apiKeysGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('apiKeysGeneralSettingsDescription')}
|
||||
{t(
|
||||
"apiKeysGeneralSettingsDescription"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -258,14 +260,14 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysList')}
|
||||
{t("apiKeysList")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -275,7 +277,7 @@ export default function Page() {
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('created')}
|
||||
{t("created")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{moment(
|
||||
@@ -288,10 +290,10 @@ export default function Page() {
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('apiKeysSave')}
|
||||
{t("apiKeysSave")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('apiKeysSaveDescription')}
|
||||
{t("apiKeysSaveDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -358,7 +360,7 @@ export default function Page() {
|
||||
router.push(`/admin/api-keys`);
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{!apiKey && (
|
||||
@@ -370,7 +372,7 @@ export default function Page() {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
{t('generate')}
|
||||
{t("generate")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -381,7 +383,7 @@ export default function Page() {
|
||||
copiedForm.handleSubmit(onCopiedSubmit)();
|
||||
}}
|
||||
>
|
||||
{t('done')}
|
||||
{t("done")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -34,8 +34,8 @@ export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('apiKeysManage')}
|
||||
description={t('apiKeysDescription')}
|
||||
title={t("apiKeysManage")}
|
||||
description={t("apiKeysDescription")}
|
||||
/>
|
||||
|
||||
<ApiKeysTable apiKeys={rows} />
|
||||
|
||||
@@ -58,17 +58,17 @@ export default function GeneralPage() {
|
||||
const t = useTranslations();
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
|
||||
clientId: z.string().min(1, { message: t('idpClientIdRequired') }),
|
||||
clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }),
|
||||
authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
identifierPath: z
|
||||
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.min(1, { message: t('idpPathRequired') }),
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
|
||||
identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
|
||||
emailPath: z.string().optional(),
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().min(1, { message: t('idpScopeRequired') }),
|
||||
scopes: z.string().min(1, { message: t("idpScopeRequired") }),
|
||||
autoProvision: z.boolean().default(false)
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function GeneralPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -145,14 +145,14 @@ export default function GeneralPage() {
|
||||
|
||||
if (res.status === 200) {
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('idpUpdatedDescription')
|
||||
title: t("success"),
|
||||
description: t("idpUpdatedDescription")
|
||||
});
|
||||
router.refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -171,17 +171,17 @@ export default function GeneralPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpTitle')}
|
||||
{t("idpTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpSettingsDescription')}
|
||||
{t("idpSettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('redirectUrl')}
|
||||
{t("redirectUrl")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard text={redirectUrl} />
|
||||
@@ -192,10 +192,10 @@ export default function GeneralPage() {
|
||||
<Alert variant="neutral" className="">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('redirectUrlAbout')}
|
||||
{t("redirectUrlAbout")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('redirectUrlAboutDescription')}
|
||||
{t("redirectUrlAboutDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<SettingsSectionForm>
|
||||
@@ -210,12 +210,14 @@ export default function GeneralPage() {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpDisplayName')}
|
||||
{t("idpDisplayName")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -225,7 +227,7 @@ export default function GeneralPage() {
|
||||
<div className="flex items-start mb-0">
|
||||
<SwitchInput
|
||||
id="auto-provision-toggle"
|
||||
label={t('idpAutoProvisionUsers')}
|
||||
label={t("idpAutoProvisionUsers")}
|
||||
defaultChecked={form.getValues(
|
||||
"autoProvision"
|
||||
)}
|
||||
@@ -238,7 +240,7 @@ export default function GeneralPage() {
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('idpAutoProvisionUsersDescription')}
|
||||
{t("idpAutoProvisionUsersDescription")}
|
||||
</span>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -250,10 +252,10 @@ export default function GeneralPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpOidcConfigure')}
|
||||
{t("idpOidcConfigure")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpOidcConfigureDescription')}
|
||||
{t("idpOidcConfigureDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -270,13 +272,15 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpClientId')}
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpClientIdDescription')}
|
||||
{t(
|
||||
"idpClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -289,7 +293,7 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpClientSecret')}
|
||||
{t("idpClientSecret")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -298,7 +302,9 @@ export default function GeneralPage() {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpClientSecretDescription')}
|
||||
{t(
|
||||
"idpClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -311,13 +317,15 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpAuthUrl')}
|
||||
{t("idpAuthUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpAuthUrlDescription')}
|
||||
{t(
|
||||
"idpAuthUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -330,13 +338,15 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpTokenUrl')}
|
||||
{t("idpTokenUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpTokenUrlDescription')}
|
||||
{t(
|
||||
"idpTokenUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -351,10 +361,10 @@ export default function GeneralPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpToken')}
|
||||
{t("idpToken")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpTokenDescription')}
|
||||
{t("idpTokenDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -368,17 +378,21 @@ export default function GeneralPage() {
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('idpJmespathAbout')}
|
||||
{t("idpJmespathAbout")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('idpJmespathAboutDescription')}
|
||||
{t(
|
||||
"idpJmespathAboutDescription"
|
||||
)}
|
||||
<a
|
||||
href="https://jmespath.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center"
|
||||
>
|
||||
{t('idpJmespathAboutDescriptionLink')}{" "}
|
||||
{t(
|
||||
"idpJmespathAboutDescriptionLink"
|
||||
)}{" "}
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</a>
|
||||
</AlertDescription>
|
||||
@@ -390,13 +404,15 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathLabel')}
|
||||
{t("idpJmespathLabel")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathLabelDescription')}
|
||||
{t(
|
||||
"idpJmespathLabelDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -409,13 +425,17 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathEmailPathOptional')}
|
||||
{t(
|
||||
"idpJmespathEmailPathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathEmailPathOptionalDescription')}
|
||||
{t(
|
||||
"idpJmespathEmailPathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -428,13 +448,17 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathNamePathOptional')}
|
||||
{t(
|
||||
"idpJmespathNamePathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathNamePathOptionalDescription')}
|
||||
{t(
|
||||
"idpJmespathNamePathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -447,13 +471,17 @@ export default function GeneralPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpOidcConfigureScopes')}
|
||||
{t(
|
||||
"idpOidcConfigureScopes"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpOidcConfigureScopesDescription')}
|
||||
{t(
|
||||
"idpOidcConfigureScopesDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -474,7 +502,7 @@ export default function GeneralPage() {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('saveGeneralSettings')}
|
||||
{t("saveGeneralSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -30,11 +30,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems: HorizontalTabs = [
|
||||
{
|
||||
title: t('general'),
|
||||
title: t("general"),
|
||||
href: `/admin/idp/${params.idpId}/general`
|
||||
},
|
||||
{
|
||||
title: t('orgPolicies'),
|
||||
title: t("orgPolicies"),
|
||||
href: `/admin/idp/${params.idpId}/policies`
|
||||
}
|
||||
];
|
||||
@@ -42,8 +42,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('idpSettings', { idpName: idp.idp.name })}
|
||||
description={t('idpSettingsDescription')}
|
||||
title={t("idpSettings", { idpName: idp.idp.name })}
|
||||
description={t("idpSettingsDescription")}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function PoliciesPage() {
|
||||
const [editingPolicy, setEditingPolicy] = useState<PolicyRow | null>(null);
|
||||
|
||||
const policyFormSchema = z.object({
|
||||
orgId: z.string().min(1, { message: t('orgRequired') }),
|
||||
orgId: z.string().min(1, { message: t("orgRequired") }),
|
||||
roleMapping: z.string().optional(),
|
||||
orgMapping: z.string().optional()
|
||||
});
|
||||
@@ -133,7 +133,7 @@ export default function PoliciesPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -148,7 +148,7 @@ export default function PoliciesPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -167,7 +167,7 @@ export default function PoliciesPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -202,15 +202,15 @@ export default function PoliciesPage() {
|
||||
};
|
||||
setPolicies([...policies, newPolicy]);
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('orgPolicyAddedDescription')
|
||||
title: t("success"),
|
||||
description: t("orgPolicyAddedDescription")
|
||||
});
|
||||
setShowAddDialog(false);
|
||||
form.reset();
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -244,8 +244,8 @@ export default function PoliciesPage() {
|
||||
)
|
||||
);
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('orgPolicyUpdatedDescription')
|
||||
title: t("success"),
|
||||
description: t("orgPolicyUpdatedDescription")
|
||||
});
|
||||
setShowAddDialog(false);
|
||||
setEditingPolicy(null);
|
||||
@@ -253,7 +253,7 @@ export default function PoliciesPage() {
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -271,13 +271,13 @@ export default function PoliciesPage() {
|
||||
policies.filter((policy) => policy.orgId !== orgId)
|
||||
);
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('orgPolicyDeletedDescription')
|
||||
title: t("success"),
|
||||
description: t("orgPolicyDeletedDescription")
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -295,13 +295,13 @@ export default function PoliciesPage() {
|
||||
});
|
||||
if (res.status === 200) {
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('defaultMappingsUpdatedDescription')
|
||||
title: t("success"),
|
||||
description: t("defaultMappingsUpdatedDescription")
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -320,18 +320,18 @@ export default function PoliciesPage() {
|
||||
<Alert variant="neutral" className="mb-6">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('orgPoliciesAbout')}
|
||||
{t("orgPoliciesAbout")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{/*TODO(vlalx): Validate replacing */}
|
||||
{t('orgPoliciesAboutDescription')}{" "}
|
||||
{t("orgPoliciesAboutDescription")}{" "}
|
||||
<Link
|
||||
href="https://docs.pangolin.net/manage/identity-providers/auto-provisioning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t('orgPoliciesAboutDescriptionLink')}
|
||||
{t("orgPoliciesAboutDescriptionLink")}
|
||||
<ExternalLink className="ml-1 h-4 w-4 inline" />
|
||||
</Link>
|
||||
</AlertDescription>
|
||||
@@ -340,10 +340,10 @@ export default function PoliciesPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('defaultMappingsOptional')}
|
||||
{t("defaultMappingsOptional")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('defaultMappingsOptionalDescription')}
|
||||
{t("defaultMappingsOptionalDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -362,13 +362,15 @@ export default function PoliciesPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('defaultMappingsRole')}
|
||||
{t("defaultMappingsRole")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('defaultMappingsRoleDescription')}
|
||||
{t(
|
||||
"defaultMappingsRoleDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -381,13 +383,15 @@ export default function PoliciesPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('defaultMappingsOrg')}
|
||||
{t("defaultMappingsOrg")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('defaultMappingsOrgDescription')}
|
||||
{t(
|
||||
"defaultMappingsOrgDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -402,7 +406,7 @@ export default function PoliciesPage() {
|
||||
form="policy-default-mappings-form"
|
||||
loading={updateDefaultMappingsLoading}
|
||||
>
|
||||
{t('defaultMappingsSubmit')}
|
||||
{t("defaultMappingsSubmit")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSectionBody>
|
||||
@@ -445,11 +449,11 @@ export default function PoliciesPage() {
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{editingPolicy
|
||||
? t('orgPoliciesEdit')
|
||||
: t('orgPoliciesAdd')}
|
||||
? t("orgPoliciesEdit")
|
||||
: t("orgPoliciesAdd")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('orgPolicyConfig')}
|
||||
{t("orgPolicyConfig")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -466,7 +470,7 @@ export default function PoliciesPage() {
|
||||
name="orgId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{t('org')}</FormLabel>
|
||||
<FormLabel>{t("org")}</FormLabel>
|
||||
{editingPolicy ? (
|
||||
<Input {...field} disabled />
|
||||
) : (
|
||||
@@ -490,17 +494,25 @@ export default function PoliciesPage() {
|
||||
org.orgId ===
|
||||
field.value
|
||||
)?.name
|
||||
: t('orgSelect')}
|
||||
: t(
|
||||
"orgSelect"
|
||||
)}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('orgSearch')} />
|
||||
<CommandInput
|
||||
placeholder={t(
|
||||
"orgSearch"
|
||||
)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t('orgNotFound')}
|
||||
{t(
|
||||
"orgNotFound"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{organizations.map(
|
||||
@@ -551,13 +563,15 @@ export default function PoliciesPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('roleMappingPathOptional')}
|
||||
{t("roleMappingPathOptional")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('defaultMappingsRoleDescription')}
|
||||
{t(
|
||||
"defaultMappingsRoleDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -570,13 +584,15 @@ export default function PoliciesPage() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('orgMappingPathOptional')}
|
||||
{t("orgMappingPathOptional")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('defaultMappingsOrgDescription')}
|
||||
{t(
|
||||
"defaultMappingsOrgDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -603,7 +619,9 @@ export default function PoliciesPage() {
|
||||
: addPolicyLoading
|
||||
}
|
||||
>
|
||||
{editingPolicy ? t('orgPolicyUpdate') : t('orgPolicyAdd')}
|
||||
{editingPolicy
|
||||
? t("orgPolicyUpdate")
|
||||
: t("orgPolicyAdd")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -48,18 +48,18 @@ export default function Page() {
|
||||
const t = useTranslations();
|
||||
|
||||
const createIdpFormSchema = z.object({
|
||||
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
|
||||
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||
type: z.enum(["oidc"]),
|
||||
clientId: z.string().min(1, { message: t('idpClientIdRequired') }),
|
||||
clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }),
|
||||
authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
identifierPath: z
|
||||
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.min(1, { message: t('idpPathRequired') }),
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
|
||||
identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
|
||||
emailPath: z.string().optional(),
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().min(1, { message: t('idpScopeRequired') }),
|
||||
scopes: z.string().min(1, { message: t("idpScopeRequired") }),
|
||||
autoProvision: z.boolean().default(false)
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function Page() {
|
||||
{
|
||||
id: "oidc",
|
||||
title: "OAuth2/OIDC",
|
||||
description: t('idpOidcDescription')
|
||||
description: t("idpOidcDescription")
|
||||
}
|
||||
];
|
||||
|
||||
@@ -117,14 +117,14 @@ export default function Page() {
|
||||
|
||||
if (res.status === 201) {
|
||||
toast({
|
||||
title: t('success'),
|
||||
description: t('idpCreatedDescription')
|
||||
title: t("success"),
|
||||
description: t("idpCreatedDescription")
|
||||
});
|
||||
router.push(`/admin/idp/${res.data.data.idpId}`);
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t('error'),
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -137,8 +137,8 @@ export default function Page() {
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title={t('idpCreate')}
|
||||
description={t('idpCreateDescription')}
|
||||
title={t("idpCreate")}
|
||||
description={t("idpCreateDescription")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -146,7 +146,7 @@ export default function Page() {
|
||||
router.push("/admin/idp");
|
||||
}}
|
||||
>
|
||||
{t('idpSeeAll')}
|
||||
{t("idpSeeAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -154,10 +154,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpTitle')}
|
||||
{t("idpTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpCreateSettingsDescription')}
|
||||
{t("idpCreateSettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -173,12 +173,14 @@ export default function Page() {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpDisplayName')}
|
||||
{t("idpDisplayName")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -188,7 +190,7 @@ export default function Page() {
|
||||
<div className="flex items-start mb-0">
|
||||
<SwitchInput
|
||||
id="auto-provision-toggle"
|
||||
label={t('idpAutoProvisionUsers')}
|
||||
label={t("idpAutoProvisionUsers")}
|
||||
defaultChecked={form.getValues(
|
||||
"autoProvision"
|
||||
)}
|
||||
@@ -201,7 +203,7 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('idpAutoProvisionUsersDescription')}
|
||||
{t("idpAutoProvisionUsersDescription")}
|
||||
</span>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -212,10 +214,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpType')}
|
||||
{t("idpType")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpTypeDescription')}
|
||||
{t("idpTypeDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -235,10 +237,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpOidcConfigure')}
|
||||
{t("idpOidcConfigure")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpOidcConfigureDescription')}
|
||||
{t("idpOidcConfigureDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -254,13 +256,15 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpClientId')}
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpClientIdDescription')}
|
||||
{t(
|
||||
"idpClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -273,7 +277,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpClientSecret')}
|
||||
{t("idpClientSecret")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -282,7 +286,9 @@ export default function Page() {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpClientSecretDescription')}
|
||||
{t(
|
||||
"idpClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -295,7 +301,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpAuthUrl')}
|
||||
{t("idpAuthUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -304,7 +310,9 @@ export default function Page() {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpAuthUrlDescription')}
|
||||
{t(
|
||||
"idpAuthUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -317,7 +325,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpTokenUrl')}
|
||||
{t("idpTokenUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -326,7 +334,9 @@ export default function Page() {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpTokenUrlDescription')}
|
||||
{t(
|
||||
"idpTokenUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -338,10 +348,10 @@ export default function Page() {
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('idpOidcConfigureAlert')}
|
||||
{t("idpOidcConfigureAlert")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('idpOidcConfigureAlertDescription')}
|
||||
{t("idpOidcConfigureAlertDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</SettingsSectionBody>
|
||||
@@ -350,10 +360,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpToken')}
|
||||
{t("idpToken")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpTokenDescription')}
|
||||
{t("idpTokenDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -366,17 +376,21 @@ export default function Page() {
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('idpJmespathAbout')}
|
||||
{t("idpJmespathAbout")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('idpJmespathAboutDescription')}{" "}
|
||||
{t(
|
||||
"idpJmespathAboutDescription"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://jmespath.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center"
|
||||
>
|
||||
{t('idpJmespathAboutDescriptionLink')}{" "}
|
||||
{t(
|
||||
"idpJmespathAboutDescriptionLink"
|
||||
)}{" "}
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</a>
|
||||
</AlertDescription>
|
||||
@@ -388,13 +402,15 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathLabel')}
|
||||
{t("idpJmespathLabel")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathLabelDescription')}
|
||||
{t(
|
||||
"idpJmespathLabelDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -407,13 +423,17 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathEmailPathOptional')}
|
||||
{t(
|
||||
"idpJmespathEmailPathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathEmailPathOptionalDescription')}
|
||||
{t(
|
||||
"idpJmespathEmailPathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -426,13 +446,17 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathNamePathOptional')}
|
||||
{t(
|
||||
"idpJmespathNamePathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathNamePathOptionalDescription')}
|
||||
{t(
|
||||
"idpJmespathNamePathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -445,13 +469,17 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpOidcConfigureScopes')}
|
||||
{t(
|
||||
"idpOidcConfigureScopes"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpOidcConfigureScopesDescription')}
|
||||
{t(
|
||||
"idpOidcConfigureScopesDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -473,7 +501,7 @@ export default function Page() {
|
||||
router.push("/admin/idp");
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -481,7 +509,7 @@ export default function Page() {
|
||||
loading={createLoading}
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
{t('idpSubmit')}
|
||||
{t("idpSubmit")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -22,8 +22,8 @@ export default async function IdpPage() {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('idpManage')}
|
||||
description={t('idpManageDescription')}
|
||||
title={t("idpManage")}
|
||||
description={t("idpManageDescription")}
|
||||
/>
|
||||
<IdpTable idps={idps} />
|
||||
</>
|
||||
|
||||
@@ -14,4 +14,3 @@ export default async function AdminLicenseLayout(props: LayoutProps) {
|
||||
|
||||
return props.children;
|
||||
}
|
||||
|
||||
|
||||
@@ -316,9 +316,7 @@ export default function LicensePage() {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("licenseQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("licenseQuestionRemove")}</p>
|
||||
<p>
|
||||
<b>{t("licenseMessageRemove")}</b>
|
||||
</p>
|
||||
|
||||
@@ -188,7 +188,7 @@ export default function UsersTable({ users }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AdminGetUserResponse } from "@server/routers/user/adminGetUser";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { cache } from "react";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface UserLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -36,7 +36,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
title: t("general"),
|
||||
href: "/admin/users/{userId}/general"
|
||||
}
|
||||
];
|
||||
@@ -45,11 +45,9 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={`${user?.email || user?.name || user?.username}`}
|
||||
description={t('userDescription2')}
|
||||
description={t("userDescription2")}
|
||||
/>
|
||||
<HorizontalTabs items={navItems}>
|
||||
{children}
|
||||
</HorizontalTabs>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ export default async function UserPage(props: {
|
||||
}) {
|
||||
const { userId } = await props.params;
|
||||
redirect(`/admin/users/${userId}/general`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default async function UsersPage(props: PageProps) {
|
||||
username: row.username,
|
||||
type: row.type,
|
||||
idpId: row.idpId,
|
||||
idpName: row.idpName || t('idpNameInternal'),
|
||||
idpName: row.idpName || t("idpNameInternal"),
|
||||
dateCreated: row.dateCreated,
|
||||
serverAdmin: row.serverAdmin,
|
||||
twoFactorEnabled: row.twoFactorEnabled,
|
||||
@@ -47,14 +47,16 @@ export default async function UsersPage(props: PageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('userTitle')}
|
||||
description={t('userDescription')}
|
||||
title={t("userTitle")}
|
||||
description={t("userDescription")}
|
||||
/>
|
||||
<Alert variant="neutral" className="mb-6">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">{t('userAbount')}</AlertTitle>
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("userAbount")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('userAbountDescription')}
|
||||
{t("userAbountDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<UsersTable users={userRows} />
|
||||
|
||||
@@ -9,9 +9,7 @@ import { LoginFormIDP } from "@app/components/LoginForm";
|
||||
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
|
||||
import { build } from "@server/build";
|
||||
import { headers } from "next/headers";
|
||||
import {
|
||||
LoadLoginPageResponse
|
||||
} from "@server/routers/loginPage/types";
|
||||
import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
|
||||
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
|
||||
import {
|
||||
Card,
|
||||
|
||||
@@ -45,7 +45,9 @@ export default function Setup2FAPage() {
|
||||
<CardHeader>
|
||||
<CardTitle>{t("otpSetup")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("adminEnabled2FaOnYourAccount", { email: email || "your account" })}
|
||||
{t("adminEnabled2FaOnYourAccount", {
|
||||
email: email || "your account"
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -25,15 +25,17 @@ export default async function Page(props: {
|
||||
const allCookies = await cookies();
|
||||
const stateCookie = allCookies.get("p_oidc_state")?.value;
|
||||
|
||||
|
||||
const idpRes = await cache(
|
||||
async () => await priv.get<AxiosResponse<GetIdpResponse>>(`/idp/${params.idpId}`)
|
||||
async () =>
|
||||
await priv.get<AxiosResponse<GetIdpResponse>>(
|
||||
`/idp/${params.idpId}`
|
||||
)
|
||||
)();
|
||||
|
||||
const foundIdp = idpRes.data?.data?.idp;
|
||||
|
||||
if (!foundIdp) {
|
||||
return <div>{t('idpErrorNotFound')}</div>;
|
||||
return <div>{t("idpErrorNotFound")}</div>;
|
||||
}
|
||||
|
||||
const allHeaders = await headers();
|
||||
|
||||
@@ -19,7 +19,9 @@ export default async function DeviceLoginPage({ searchParams }: Props) {
|
||||
const redirectDestination = code
|
||||
? `/auth/login/device?code=${encodeURIComponent(code)}`
|
||||
: "/auth/login/device";
|
||||
redirect(`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}`);
|
||||
redirect(
|
||||
`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}`
|
||||
);
|
||||
}
|
||||
|
||||
const userName = user?.name || user?.username || "";
|
||||
|
||||
@@ -26,7 +26,9 @@ export default function DeviceAuthSuccessPage() {
|
||||
<BrandingLogo height={logoHeight} width={logoWidth} />
|
||||
</div>
|
||||
<div className="text-center space-y-1 pt-3">
|
||||
<p className="text-muted-foreground">{t("deviceActivation")}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{t("deviceActivation")}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
|
||||
@@ -98,7 +98,11 @@ export default async function Page(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DashboardLoginForm redirect={redirectUrl} idps={loginIdps} forceLogin={forceLogin} />
|
||||
<DashboardLoginForm
|
||||
redirect={redirectUrl}
|
||||
idps={loginIdps}
|
||||
forceLogin={forceLogin}
|
||||
/>
|
||||
|
||||
{(!signUpDisabled || isInvite) && (
|
||||
<p className="text-center text-muted-foreground mt-4">
|
||||
|
||||
@@ -88,18 +88,18 @@ export default function ResetPasswordForm({
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
email: z.email({ message: t('emailInvalid') }),
|
||||
token: z.string().min(8, { message: t('tokenInvalid') }),
|
||||
email: z.email({ message: t("emailInvalid") }),
|
||||
token: z.string().min(8, { message: t("tokenInvalid") }),
|
||||
password: passwordSchema,
|
||||
confirmPassword: passwordSchema
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
path: ["confirmPassword"],
|
||||
message: t('passwordNotMatch')
|
||||
message: t("passwordNotMatch")
|
||||
});
|
||||
|
||||
const mfaSchema = z.object({
|
||||
code: z.string().length(6, { message: t('pincodeInvalid') })
|
||||
code: z.string().length(6, { message: t("pincodeInvalid") })
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
@@ -139,8 +139,8 @@ export default function ResetPasswordForm({
|
||||
} as RequestPasswordResetBody
|
||||
)
|
||||
.catch((e) => {
|
||||
setError(formatAxiosError(e, t('errorOccurred')));
|
||||
console.error(t('passwordErrorRequestReset'), e);
|
||||
setError(formatAxiosError(e, t("errorOccurred")));
|
||||
console.error(t("passwordErrorRequestReset"), e);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
|
||||
@@ -169,8 +169,8 @@ export default function ResetPasswordForm({
|
||||
} as ResetPasswordBody
|
||||
)
|
||||
.catch((e) => {
|
||||
setError(formatAxiosError(e, t('errorOccurred')));
|
||||
console.error(t('passwordErrorReset'), e);
|
||||
setError(formatAxiosError(e, t("errorOccurred")));
|
||||
console.error(t("passwordErrorReset"), e);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
|
||||
@@ -186,7 +186,11 @@ export default function ResetPasswordForm({
|
||||
return;
|
||||
}
|
||||
|
||||
setSuccessMessage(quickstart ? t('accountSetupSuccess') : t('passwordResetSuccess'));
|
||||
setSuccessMessage(
|
||||
quickstart
|
||||
? t("accountSetupSuccess")
|
||||
: t("passwordResetSuccess")
|
||||
);
|
||||
|
||||
// Auto-login after successful password reset
|
||||
try {
|
||||
@@ -208,7 +212,10 @@ export default function ResetPasswordForm({
|
||||
try {
|
||||
await api.post("/auth/verify-email/request");
|
||||
} catch (verificationError) {
|
||||
console.error("Failed to send verification code:", verificationError);
|
||||
console.error(
|
||||
"Failed to send verification code:",
|
||||
verificationError
|
||||
);
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
@@ -229,7 +236,6 @@ export default function ResetPasswordForm({
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
}, 1500);
|
||||
|
||||
} catch (loginError) {
|
||||
// Auto-login failed, but password reset was successful
|
||||
console.error("Auto-login failed:", loginError);
|
||||
@@ -251,13 +257,14 @@ export default function ResetPasswordForm({
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{quickstart ? t('completeAccountSetup') : t('passwordReset')}
|
||||
{quickstart
|
||||
? t("completeAccountSetup")
|
||||
: t("passwordReset")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{quickstart
|
||||
? t('completeAccountSetupDescription')
|
||||
: t('passwordResetDescription')
|
||||
}
|
||||
? t("completeAccountSetupDescription")
|
||||
: t("passwordResetDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -276,16 +283,19 @@ export default function ResetPasswordForm({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('email')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("email")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{quickstart
|
||||
? t('accountSetupSent')
|
||||
: t('passwordResetSent')
|
||||
}
|
||||
? t("accountSetupSent")
|
||||
: t(
|
||||
"passwordResetSent"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -306,7 +316,9 @@ export default function ResetPasswordForm({
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('email')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("email")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@@ -326,9 +338,12 @@ export default function ResetPasswordForm({
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{quickstart
|
||||
? t('accountSetupCode')
|
||||
: t('passwordResetCode')
|
||||
}
|
||||
? t(
|
||||
"accountSetupCode"
|
||||
)
|
||||
: t(
|
||||
"passwordResetCode"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -339,9 +354,12 @@ export default function ResetPasswordForm({
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{quickstart
|
||||
? t('accountSetupCodeDescription')
|
||||
: t('passwordResetCodeDescription')
|
||||
}
|
||||
? t(
|
||||
"accountSetupCodeDescription"
|
||||
)
|
||||
: t(
|
||||
"passwordResetCodeDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -355,9 +373,8 @@ export default function ResetPasswordForm({
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{quickstart
|
||||
? t('passwordCreate')
|
||||
: t('passwordNew')
|
||||
}
|
||||
? t("passwordCreate")
|
||||
: t("passwordNew")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -376,9 +393,12 @@ export default function ResetPasswordForm({
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{quickstart
|
||||
? t('passwordCreateConfirm')
|
||||
: t('passwordNewConfirm')
|
||||
}
|
||||
? t(
|
||||
"passwordCreateConfirm"
|
||||
)
|
||||
: t(
|
||||
"passwordNewConfirm"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -407,7 +427,7 @@ export default function ResetPasswordForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('pincodeAuth')}
|
||||
{t("pincodeAuth")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
@@ -475,8 +495,10 @@ export default function ResetPasswordForm({
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{state === "reset"
|
||||
? (quickstart ? t('completeSetup') : t('passwordReset'))
|
||||
: t('pincodeSubmit2')}
|
||||
? quickstart
|
||||
? t("completeSetup")
|
||||
: t("passwordReset")
|
||||
: t("pincodeSubmit2")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -491,9 +513,8 @@ export default function ResetPasswordForm({
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{quickstart
|
||||
? t('accountSetupSubmit')
|
||||
: t('passwordResetSubmit')
|
||||
}
|
||||
? t("accountSetupSubmit")
|
||||
: t("passwordResetSubmit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -507,7 +528,7 @@ export default function ResetPasswordForm({
|
||||
mfaForm.reset();
|
||||
}}
|
||||
>
|
||||
{t('passwordBack')}
|
||||
{t("passwordBack")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -521,7 +542,7 @@ export default function ResetPasswordForm({
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
{t('backToEmail')}
|
||||
{t("backToEmail")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,10 +35,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerifyEmailForm
|
||||
email={user.email!}
|
||||
redirect={redirectUrl}
|
||||
/>
|
||||
<VerifyEmailForm email={user.email!} redirect={redirectUrl} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive: oklch(0.5382 0.1949 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.646 0.222 41.116);
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export default async function NotFound() {
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md mx-auto p-3 md:mt-32 text-center">
|
||||
<h1 className="text-6xl font-bold mb-4">404</h1>
|
||||
<h2 className="text-2xl font-semibold text-neutral-500 mb-4">
|
||||
{t('pageNotFound')}
|
||||
{t("pageNotFound")}
|
||||
</h2>
|
||||
<p className="text-neutral-500 dark:text-neutral-700 mb-8">
|
||||
{t('pageNotFoundDescription')}
|
||||
{t("pageNotFoundDescription")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -312,7 +312,9 @@ export default function StepperForm() {
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t("setupSubnetDescription")}
|
||||
{t(
|
||||
"setupSubnetDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -14,21 +14,21 @@ export default function AccessPageHeaderAndNav({
|
||||
hasInvitations
|
||||
}: AccessPageHeaderAndNavProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('users'),
|
||||
title: t("users"),
|
||||
href: `/{orgId}/settings/access/users`
|
||||
},
|
||||
{
|
||||
title: t('roles'),
|
||||
title: t("roles"),
|
||||
href: `/{orgId}/settings/access/roles`
|
||||
}
|
||||
];
|
||||
|
||||
if (hasInvitations) {
|
||||
navItems.push({
|
||||
title: t('invite'),
|
||||
title: t("invite"),
|
||||
href: `/{orgId}/settings/access/invitations`
|
||||
});
|
||||
}
|
||||
@@ -36,13 +36,11 @@ export default function AccessPageHeaderAndNav({
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('accessUsersRoles')}
|
||||
description={t('accessUsersRolesDescription')}
|
||||
title={t("accessUsersRoles")}
|
||||
description={t("accessUsersRolesDescription")}
|
||||
/>
|
||||
|
||||
<HorizontalTabs items={navItems}>
|
||||
{children}
|
||||
</HorizontalTabs>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,7 @@ type AccessTokenProps = {
|
||||
resourceId?: number;
|
||||
};
|
||||
|
||||
export default function AccessToken({
|
||||
token,
|
||||
resourceId
|
||||
}: AccessTokenProps) {
|
||||
export default function AccessToken({ token, resourceId }: AccessTokenProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
@@ -79,7 +76,7 @@ export default function AccessToken({
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(t('accessTokenError'), e);
|
||||
console.error(t("accessTokenError"), e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -102,7 +99,7 @@ export default function AccessToken({
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(t('accessTokenError'), e);
|
||||
console.error(t("accessTokenError"), e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -118,26 +115,22 @@ export default function AccessToken({
|
||||
|
||||
function renderTitle() {
|
||||
if (isValid) {
|
||||
return t('accessGranted');
|
||||
return t("accessGranted");
|
||||
} else {
|
||||
return t('accessUrlInvalid');
|
||||
return t("accessUrlInvalid");
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
if (isValid) {
|
||||
return (
|
||||
<div>
|
||||
{t('accessGrantedDescription')}
|
||||
</div>
|
||||
);
|
||||
return <div>{t("accessGrantedDescription")}</div>;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
{t('accessUrlInvalidDescription')}
|
||||
{t("accessUrlInvalidDescription")}
|
||||
<div className="text-center mt-4">
|
||||
<Button>
|
||||
<Link href="/">{t('goHome')}</Link>
|
||||
<Link href="/">{t("goHome")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,31 +44,35 @@ export default function AccessTokenSection({
|
||||
<>
|
||||
<div className="flex items-start space-x-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('shareTokenDescription')}
|
||||
{t("shareTokenDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="token" className="w-full mt-4">
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="token">{t('accessToken')}</TabsTrigger>
|
||||
<TabsTrigger value="usage">{t('usageExamples')}</TabsTrigger>
|
||||
<TabsTrigger value="token">{t("accessToken")}</TabsTrigger>
|
||||
<TabsTrigger value="usage">
|
||||
{t("usageExamples")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="token" className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold">{t('tokenId')}</div>
|
||||
<div className="font-bold">{t("tokenId")}</div>
|
||||
<CopyToClipboard text={tokenId} isLink={false} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold">{t('token')}</div>
|
||||
<div className="font-bold">{t("token")}</div>
|
||||
<CopyToClipboard text={token} isLink={false} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="usage" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">{t('requestHeades')}</h3>
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("requestHeades")}
|
||||
</h3>
|
||||
<CopyTextBox
|
||||
text={`${env.server.resourceAccessTokenHeadersId}: ${tokenId}
|
||||
${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
@@ -76,7 +80,9 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">{t('queryParameter')}</h3>
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("queryParameter")}
|
||||
</h3>
|
||||
<CopyTextBox
|
||||
text={`${resourceUrl}?${env.server.resourceAccessTokenParam}=${tokenId}.${token}`}
|
||||
/>
|
||||
@@ -85,17 +91,17 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('importantNote')}
|
||||
{t("importantNote")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('shareImportantDescription')}
|
||||
{t("shareImportantDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="text-sm text-muted-foreground mt-4">
|
||||
{t('shareTokenSecurety')}
|
||||
{t("shareTokenSecurety")}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -26,10 +26,10 @@ export function IdpDataTable<TData, TValue>({
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="idp-table"
|
||||
title={t('idp')}
|
||||
searchPlaceholder={t('idpSearch')}
|
||||
title={t("idp")}
|
||||
searchPlaceholder={t("idpSearch")}
|
||||
searchColumn="name"
|
||||
addButtonText={t('idpAdd')}
|
||||
addButtonText={t("idpAdd")}
|
||||
onAdd={() => {
|
||||
router.push("/admin/idp/create");
|
||||
}}
|
||||
|
||||
@@ -175,9 +175,7 @@ export default function IdpTable({ idps }: Props) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -19,7 +17,6 @@ export function UsersDataTable<TData, TValue>({
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -27,8 +24,8 @@ export function UsersDataTable<TData, TValue>({
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="userServer-table"
|
||||
title={t('userServer')}
|
||||
searchPlaceholder={t('userSearch')}
|
||||
title={t("userServer")}
|
||||
searchPlaceholder={t("userSearch")}
|
||||
searchColumn="email"
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
|
||||
@@ -44,7 +44,6 @@ export function ApiKeysDataTable<TData, TValue>({
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -52,11 +51,11 @@ export function ApiKeysDataTable<TData, TValue>({
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="apiKeys-table"
|
||||
title={t('apiKeys')}
|
||||
searchPlaceholder={t('searchApiKeys')}
|
||||
title={t("apiKeys")}
|
||||
searchPlaceholder={t("searchApiKeys")}
|
||||
searchColumn="name"
|
||||
onAdd={addApiKey}
|
||||
addButtonText={t('apiKeysAdd')}
|
||||
addButtonText={t("apiKeysAdd")}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
enableColumnVisibility={true}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
{
|
||||
accessorKey: "key",
|
||||
friendlyName: t("key"),
|
||||
header: () => (<span className="p-3">{t("key")}</span>),
|
||||
header: () => <span className="p-3">{t("key")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
@@ -117,7 +117,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
friendlyName: t("createdAt"),
|
||||
header: () => (<span className="p-3">{t("createdAt")}</span>),
|
||||
header: () => <span className="p-3">{t("createdAt")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
||||
@@ -161,9 +161,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link href={`/admin/api-keys/${r.id}`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -67,7 +67,8 @@ export default function AutoLoginHandler({
|
||||
console.error("Failed to generate OIDC URL:", e);
|
||||
setError(
|
||||
t("autoLoginErrorGeneratingUrl", {
|
||||
defaultValue: "An unexpected error occurred. Please try again."
|
||||
defaultValue:
|
||||
"An unexpected error occurred. Please try again."
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function BrandingLogo(props: BrandingLogoProps) {
|
||||
if (isUnlocked() && env.branding.logo?.darkPath) {
|
||||
return env.branding.logo.darkPath;
|
||||
}
|
||||
return "/logo/word_mark_white.png";
|
||||
return "/logo/word_mark_white.png";
|
||||
}
|
||||
|
||||
const path = getPath();
|
||||
|
||||
@@ -20,7 +20,10 @@ type ChangePasswordDialogProps = {
|
||||
setOpen: (val: boolean) => void;
|
||||
};
|
||||
|
||||
export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDialogProps) {
|
||||
export default function ChangePasswordDialog({
|
||||
open,
|
||||
setOpen
|
||||
}: ChangePasswordDialogProps) {
|
||||
const t = useTranslations();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -47,18 +50,16 @@ export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDi
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t('changePassword')}
|
||||
</CredenzaTitle>
|
||||
<CredenzaTitle>{t("changePassword")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('changePasswordDescription')}
|
||||
{t("changePasswordDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<ChangePasswordForm
|
||||
ref={formRef}
|
||||
isDialog={true}
|
||||
submitButtonText={t('submit')}
|
||||
submitButtonText={t("submit")}
|
||||
cancelButtonText="Close"
|
||||
showCancelButton={false}
|
||||
onComplete={() => setOpen(false)}
|
||||
@@ -77,7 +78,7 @@ export default function ChangePasswordDialog({ open, setOpen }: ChangePasswordDi
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t('submit')}
|
||||
{t("submit")}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
|
||||
@@ -22,11 +22,7 @@ import {
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot
|
||||
} from "./ui/input-otp";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
import { ChangePasswordResponse } from "@server/routers/auth";
|
||||
import { cn } from "@app/lib/cn";
|
||||
@@ -114,14 +110,22 @@ const ChangePasswordForm = forwardRef<
|
||||
onLoadingChange?.(loading);
|
||||
}, [loading, onLoadingChange]);
|
||||
|
||||
const passwordSchema = z.object({
|
||||
oldPassword: z.string().min(1, { message: t("passwordRequired") }),
|
||||
newPassword: z.string().min(8, { message: t("passwordRequirementsChars") }),
|
||||
confirmPassword: z.string().min(1, { message: t("passwordRequired") })
|
||||
}).refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: t("passwordsDoNotMatch"),
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
const passwordSchema = z
|
||||
.object({
|
||||
oldPassword: z
|
||||
.string()
|
||||
.min(1, { message: t("passwordRequired") }),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, { message: t("passwordRequirementsChars") }),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, { message: t("passwordRequired") })
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: t("passwordsDoNotMatch"),
|
||||
path: ["confirmPassword"]
|
||||
});
|
||||
|
||||
const mfaSchema = z.object({
|
||||
code: z.string().length(6, { message: t("pincodeInvalid") })
|
||||
@@ -143,11 +147,13 @@ const ChangePasswordForm = forwardRef<
|
||||
}
|
||||
});
|
||||
|
||||
const changePassword = async (values: z.infer<typeof passwordSchema>) => {
|
||||
const changePassword = async (
|
||||
values: z.infer<typeof passwordSchema>
|
||||
) => {
|
||||
setLoading(true);
|
||||
|
||||
const endpoint = `/auth/change-password`;
|
||||
const payload = {
|
||||
const payload = {
|
||||
oldPassword: values.oldPassword,
|
||||
newPassword: values.newPassword
|
||||
};
|
||||
@@ -181,7 +187,7 @@ const ChangePasswordForm = forwardRef<
|
||||
|
||||
const endpoint = `/auth/change-password`;
|
||||
const passwordValues = passwordForm.getValues();
|
||||
const payload = {
|
||||
const payload = {
|
||||
oldPassword: passwordValues.oldPassword,
|
||||
newPassword: passwordValues.newPassword,
|
||||
code: values.code
|
||||
@@ -303,7 +309,9 @@ const ChangePasswordForm = forwardRef<
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t("passwordStrength")}
|
||||
{t(
|
||||
"passwordStrength"
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -335,7 +343,9 @@ const ChangePasswordForm = forwardRef<
|
||||
{/* Requirements Checklist */}
|
||||
<div className="bg-muted rounded-lg p-3 space-y-2">
|
||||
<div className="text-sm font-medium text-foreground mb-2">
|
||||
{t("passwordRequirements")}
|
||||
{t(
|
||||
"passwordRequirements"
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -505,13 +515,14 @@ const ChangePasswordForm = forwardRef<
|
||||
{confirmPasswordValue.length > 0 &&
|
||||
!doPasswordsMatch && (
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
{t("passwordsDoNotMatch")}
|
||||
{t(
|
||||
"passwordsDoNotMatch"
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{/* Only show FormMessage when field is empty */}
|
||||
{confirmPasswordValue.length === 0 && (
|
||||
<FormMessage />
|
||||
)}
|
||||
{confirmPasswordValue.length ===
|
||||
0 && <FormMessage />}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -523,7 +534,9 @@ const ChangePasswordForm = forwardRef<
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium">{t("otpAuth")}</h3>
|
||||
<h3 className="text-lg font-medium">
|
||||
{t("otpAuth")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("otpAuthDescription")}
|
||||
</p>
|
||||
@@ -551,9 +564,12 @@ const ChangePasswordForm = forwardRef<
|
||||
onChange={(
|
||||
value: string
|
||||
) => {
|
||||
field.onChange(value);
|
||||
field.onChange(
|
||||
value
|
||||
);
|
||||
if (
|
||||
value.length === 6
|
||||
value.length ===
|
||||
6
|
||||
) {
|
||||
mfaForm.handleSubmit(
|
||||
confirmMfa
|
||||
@@ -630,10 +646,7 @@ const ChangePasswordForm = forwardRef<
|
||||
</Button>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
className="w-full"
|
||||
>
|
||||
<Button onClick={handleComplete} className="w-full">
|
||||
{t("continueToApplication")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -644,4 +657,4 @@ const ChangePasswordForm = forwardRef<
|
||||
}
|
||||
);
|
||||
|
||||
export default ChangePasswordForm;
|
||||
export default ChangePasswordForm;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
|
||||
type TabFilter = {
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from "@app/components/ui/command";
|
||||
import { CheckIcon, ChevronDownIcon, Filter } from "lucide-react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
@@ -31,7 +42,9 @@ export function ColumnFilter({
|
||||
}: ColumnFilterProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const selectedOption = options.find(option => option.value === selectedValue);
|
||||
const selectedOption = options.find(
|
||||
(option) => option.value === selectedValue
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -49,7 +62,9 @@ export function ColumnFilter({
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-4 w-4" />
|
||||
<span className="truncate">
|
||||
{selectedOption ? selectedOption.label : placeholder}
|
||||
{selectedOption
|
||||
? selectedOption.label
|
||||
: placeholder}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 opacity-50" />
|
||||
@@ -79,7 +94,9 @@ export function ColumnFilter({
|
||||
value={option.label}
|
||||
onSelect={() => {
|
||||
onValueChange(
|
||||
selectedValue === option.value ? undefined : option.value
|
||||
selectedValue === option.value
|
||||
? undefined
|
||||
: option.value
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
@@ -101,4 +118,4 @@ export function ColumnFilter({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ const DockerContainersTable: FC<{
|
||||
{
|
||||
accessorKey: "name",
|
||||
friendlyName: t("containerName"),
|
||||
header: () => (<span className="p-3">{t("containerName")}</span>),
|
||||
header: () => <span className="p-3">{t("containerName")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium">{row.original.name}</div>
|
||||
)
|
||||
@@ -194,7 +194,7 @@ const DockerContainersTable: FC<{
|
||||
{
|
||||
accessorKey: "image",
|
||||
friendlyName: t("containerImage"),
|
||||
header: () => (<span className="p-3">{t("containerImage")}</span>),
|
||||
header: () => <span className="p-3">{t("containerImage")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{row.original.image}
|
||||
@@ -204,7 +204,7 @@ const DockerContainersTable: FC<{
|
||||
{
|
||||
accessorKey: "state",
|
||||
friendlyName: t("containerState"),
|
||||
header: () => (<span className="p-3">{t("containerState")}</span>),
|
||||
header: () => <span className="p-3">{t("containerState")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant={
|
||||
@@ -220,7 +220,7 @@ const DockerContainersTable: FC<{
|
||||
{
|
||||
accessorKey: "networks",
|
||||
friendlyName: t("containerNetworks"),
|
||||
header: () => (<span className="p-3">{t("containerNetworks")}</span>),
|
||||
header: () => <span className="p-3">{t("containerNetworks")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const networks = Object.keys(row.original.networks);
|
||||
return (
|
||||
@@ -239,7 +239,9 @@ const DockerContainersTable: FC<{
|
||||
{
|
||||
accessorKey: "hostname",
|
||||
friendlyName: t("containerHostnameIp"),
|
||||
header: () => (<span className="p-3">{t("containerHostnameIp")}</span>),
|
||||
header: () => (
|
||||
<span className="p-3">{t("containerHostnameIp")}</span>
|
||||
),
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm font-mono">
|
||||
@@ -250,7 +252,7 @@ const DockerContainersTable: FC<{
|
||||
{
|
||||
accessorKey: "labels",
|
||||
friendlyName: t("containerLabels"),
|
||||
header: () => (<span className="p-3">{t("containerLabels")}</span>),
|
||||
header: () => <span className="p-3">{t("containerLabels")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const labels = row.original.labels || {};
|
||||
const labelEntries = Object.entries(labels);
|
||||
@@ -302,7 +304,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
accessorKey: "ports",
|
||||
header: () => (<span className="p-3">{t("containerPorts")}</span>),
|
||||
header: () => <span className="p-3">{t("containerPorts")}</span>,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const ports = getExposedPorts(row.original);
|
||||
@@ -360,7 +362,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("containerActions")}</span>),
|
||||
header: () => <span className="p-3">{t("containerActions")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const ports = getExposedPorts(row.original);
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function CopyTextBox({
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error(t('copyTextFailed'), err);
|
||||
console.error(t("copyTextFailed"), err);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export default function CopyTextBox({
|
||||
type="button"
|
||||
className="absolute top-0.5 right-0 z-10 bg-card"
|
||||
onClick={copyToClipboard}
|
||||
aria-label={t('copyTextClipboard')}
|
||||
aria-label={t("copyTextClipboard")}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
|
||||
@@ -9,7 +9,11 @@ type CopyToClipboardProps = {
|
||||
isLink?: boolean;
|
||||
};
|
||||
|
||||
const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) => {
|
||||
const CopyToClipboard = ({
|
||||
text,
|
||||
displayText,
|
||||
isLink
|
||||
}: CopyToClipboardProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
@@ -60,7 +64,7 @@ const CopyToClipboard = ({ text, displayText, isLink }: CopyToClipboardProps) =>
|
||||
) : (
|
||||
<Check className="text-green-500 h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">{t('copyText')}</span>
|
||||
<span className="sr-only">{t("copyText")}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,11 +45,16 @@ import {
|
||||
} from "@app/components/InfoSection";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { build } from "@server/build";
|
||||
import { toASCII, toUnicode } from 'punycode';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
import { toASCII, toUnicode } from "punycode";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "./ui/select";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
// Helper functions for Unicode domain handling
|
||||
function toPunycode(domain: string): string {
|
||||
try {
|
||||
@@ -76,9 +81,9 @@ function isValidDomainFormat(domain: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = domain.split('.');
|
||||
const parts = domain.split(".");
|
||||
for (const part of parts) {
|
||||
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
|
||||
if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) {
|
||||
return false;
|
||||
}
|
||||
if (part.length > 63) {
|
||||
@@ -137,7 +142,8 @@ export default function CreateDomainForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
baseDomain: "",
|
||||
type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
|
||||
type:
|
||||
build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
|
||||
certResolver: null,
|
||||
preferWildcardCert: false
|
||||
}
|
||||
@@ -172,7 +178,9 @@ export default function CreateDomainForm({
|
||||
description: t("domainCreatedDescription")
|
||||
});
|
||||
onCreated?.(domainData);
|
||||
router.push(`/${org.org.orgId}/settings/domains/${domainData.domainId}`);
|
||||
router.push(
|
||||
`/${org.org.orgId}/settings/domains/${domainData.domainId}`
|
||||
);
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
@@ -182,7 +190,7 @@ export default function CreateDomainForm({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Domain type options
|
||||
let domainOptions: any = [];
|
||||
@@ -225,145 +233,213 @@ export default function CreateDomainForm({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="create-domain-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<StrategySelect
|
||||
options={domainOptions}
|
||||
defaultValue={field.value}
|
||||
onChange={field.onChange}
|
||||
cols={1}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="baseDomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("domain")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{punycodePreview && (
|
||||
<FormDescription className="flex items-center gap-2 text-xs">
|
||||
<Alert>
|
||||
<Globe className="h-4 w-4" />
|
||||
<AlertTitle>{t("internationaldomaindetected")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="mt-2 space-y-1">
|
||||
<p>{t("willbestoredas")} <code className="font-mono px-1 py-0.5 rounded">{punycodePreview}</code></p>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{domainType === "wildcard" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certResolver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("certResolver")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value === null ? "default" :
|
||||
(field.value === "" || (field.value && field.value !== "default")) ? "custom" :
|
||||
"default"
|
||||
}
|
||||
onValueChange={(val) => {
|
||||
if (val === "default") {
|
||||
field.onChange(null);
|
||||
} else if (val === "custom") {
|
||||
field.onChange("");
|
||||
} else {
|
||||
field.onChange(val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("selectCertResolver")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{certResolverOptions.map((opt) => (
|
||||
<SelectItem key={opt.id} value={opt.id}>
|
||||
{opt.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="create-domain-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<StrategySelect
|
||||
options={domainOptions}
|
||||
defaultValue={field.value}
|
||||
onChange={field.onChange}
|
||||
cols={1}
|
||||
/>
|
||||
{form.watch("certResolver") !== null &&
|
||||
form.watch("certResolver") !== "default" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certResolver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("enterCustomResolver")}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="baseDomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("domain")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{punycodePreview && (
|
||||
<FormDescription className="flex items-center gap-2 text-xs">
|
||||
<Alert>
|
||||
<Globe className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{t(
|
||||
"internationaldomaindetected"
|
||||
)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="mt-2 space-y-1">
|
||||
<p>
|
||||
{t(
|
||||
"willbestoredas"
|
||||
)}{" "}
|
||||
<code className="font-mono px-1 py-0.5 rounded">
|
||||
{
|
||||
punycodePreview
|
||||
}
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{domainType === "wildcard" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certResolver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("certResolver")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value === null
|
||||
? "default"
|
||||
: field.value ===
|
||||
"" ||
|
||||
(field.value &&
|
||||
field.value !==
|
||||
"default")
|
||||
? "custom"
|
||||
: "default"
|
||||
}
|
||||
onValueChange={(
|
||||
val
|
||||
) => {
|
||||
if (
|
||||
val ===
|
||||
"default"
|
||||
) {
|
||||
field.onChange(
|
||||
null
|
||||
);
|
||||
} else if (
|
||||
val === "custom"
|
||||
) {
|
||||
field.onChange(
|
||||
""
|
||||
);
|
||||
} else {
|
||||
field.onChange(
|
||||
val
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectCertResolver"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{certResolverOptions.map(
|
||||
(opt) => (
|
||||
<SelectItem
|
||||
key={
|
||||
opt.id
|
||||
}
|
||||
value={
|
||||
opt.id
|
||||
}
|
||||
>
|
||||
{
|
||||
opt.title
|
||||
}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{form.watch("certResolver") !== null &&
|
||||
form.watch("certResolver") !==
|
||||
"default" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="certResolver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t(
|
||||
"enterCustomResolver"
|
||||
)}
|
||||
value={
|
||||
field.value ||
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{form.watch("certResolver") !== null &&
|
||||
form.watch("certResolver") !== "default" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="preferWildcardCert"
|
||||
render={({ field: checkboxField }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<CheckboxWithLabel
|
||||
label={t("preferWildcardCert")}
|
||||
checked={checkboxField.value}
|
||||
onCheckedChange={checkboxField.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
{/* <div className="space-y-1 leading-none">
|
||||
{form.watch("certResolver") !== null &&
|
||||
form.watch("certResolver") !==
|
||||
"default" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="preferWildcardCert"
|
||||
render={({
|
||||
field: checkboxField
|
||||
}) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<CheckboxWithLabel
|
||||
label={t(
|
||||
"preferWildcardCert"
|
||||
)}
|
||||
checked={
|
||||
checkboxField.value
|
||||
}
|
||||
onCheckedChange={
|
||||
checkboxField.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{/* <div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
{t("preferWildcardCert")}
|
||||
</FormLabel>
|
||||
</div> */}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
|
||||
@@ -272,7 +272,7 @@ export default function CreateInternalResourceDialog({
|
||||
// an alias is required
|
||||
if (data.mode === "host" && isHostname(data.destination)) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
|
||||
|
||||
if (!currentAlias) {
|
||||
// Prefill alias based on destination
|
||||
let aliasValue = data.destination;
|
||||
@@ -281,7 +281,7 @@ export default function CreateInternalResourceDialog({
|
||||
const cleanedName = cleanForFQDN(data.name);
|
||||
aliasValue = `${cleanedName}.internal`;
|
||||
}
|
||||
|
||||
|
||||
// Update the form with the prefilled alias
|
||||
form.setValue("alias", aliasValue);
|
||||
data.alias = aliasValue;
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function CreateRoleForm({
|
||||
const t = useTranslations();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string({ message: t('nameRequired') }).max(32),
|
||||
name: z.string({ message: t("nameRequired") }).max(32),
|
||||
description: z.string().max(255).optional()
|
||||
});
|
||||
|
||||
@@ -78,10 +78,10 @@ export default function CreateRoleForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('accessRoleErrorCreate'),
|
||||
title: t("accessRoleErrorCreate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('accessRoleErrorCreateDescription')
|
||||
t("accessRoleErrorCreateDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -89,8 +89,8 @@ export default function CreateRoleForm({
|
||||
if (res && res.status === 201) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t('accessRoleCreated'),
|
||||
description: t('accessRoleCreatedDescription')
|
||||
title: t("accessRoleCreated"),
|
||||
description: t("accessRoleCreatedDescription")
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -117,9 +117,9 @@ export default function CreateRoleForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t('accessRoleCreate')}</CredenzaTitle>
|
||||
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('accessRoleCreateDescription')}
|
||||
{t("accessRoleCreateDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -134,7 +134,9 @@ export default function CreateRoleForm({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accessRoleName')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("accessRoleName")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -147,7 +149,9 @@ export default function CreateRoleForm({
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('description')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("description")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -160,7 +164,7 @@ export default function CreateRoleForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -168,7 +172,7 @@ export default function CreateRoleForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('accessRoleCreateSubmit')}
|
||||
{t("accessRoleCreateSubmit")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
} from "@app/components/ui/collapsible";
|
||||
import AccessTokenSection from "@app/components/AccessTokenUsage";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toUnicode } from 'punycode';
|
||||
import { toUnicode } from "punycode";
|
||||
|
||||
type FormProps = {
|
||||
open: boolean;
|
||||
@@ -104,7 +104,7 @@ export default function CreateShareLinkForm({
|
||||
>([]);
|
||||
|
||||
const formSchema = z.object({
|
||||
resourceId: z.number({ message: t('shareErrorSelectResource') }),
|
||||
resourceId: z.number({ message: t("shareErrorSelectResource") }),
|
||||
resourceName: z.string(),
|
||||
resourceUrl: z.string(),
|
||||
timeUnit: z.string(),
|
||||
@@ -113,12 +113,12 @@ export default function CreateShareLinkForm({
|
||||
});
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: "minutes", name: t('minutes') },
|
||||
{ unit: "hours", name: t('hours') },
|
||||
{ unit: "days", name: t('days') },
|
||||
{ unit: "weeks", name: t('weeks') },
|
||||
{ unit: "months", name: t('months') },
|
||||
{ unit: "years", name: t('years') }
|
||||
{ unit: "minutes", name: t("minutes") },
|
||||
{ unit: "hours", name: t("hours") },
|
||||
{ unit: "days", name: t("days") },
|
||||
{ unit: "weeks", name: t("weeks") },
|
||||
{ unit: "months", name: t("months") },
|
||||
{ unit: "years", name: t("years") }
|
||||
];
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -144,10 +144,10 @@ export default function CreateShareLinkForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('shareErrorFetchResource'),
|
||||
title: t("shareErrorFetchResource"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('shareErrorFetchResourceDescription')
|
||||
t("shareErrorFetchResourceDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -204,17 +204,21 @@ export default function CreateShareLinkForm({
|
||||
validForSeconds: neverExpire ? undefined : timeInSeconds,
|
||||
title:
|
||||
values.title ||
|
||||
t('shareLink', {resource: (values.resourceName || "Resource" + values.resourceId)})
|
||||
t("shareLink", {
|
||||
resource:
|
||||
values.resourceName ||
|
||||
"Resource" + values.resourceId
|
||||
})
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('shareErrorCreate'),
|
||||
title: t("shareErrorCreate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('shareErrorCreateDescription')
|
||||
t("shareErrorCreateDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -263,9 +267,9 @@ export default function CreateShareLinkForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t('shareCreate')}</CredenzaTitle>
|
||||
<CredenzaTitle>{t("shareCreate")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('shareCreateDescription')}
|
||||
{t("shareCreateDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -283,7 +287,7 @@ export default function CreateShareLinkForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>
|
||||
{t('resource')}
|
||||
{t("resource")}
|
||||
</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -301,17 +305,25 @@ export default function CreateShareLinkForm({
|
||||
? getSelectedResourceName(
|
||||
field.value
|
||||
)
|
||||
: t('resourceSelect')}
|
||||
: t(
|
||||
"resourceSelect"
|
||||
)}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('resourceSearch')} />
|
||||
<CommandInput
|
||||
placeholder={t(
|
||||
"resourceSearch"
|
||||
)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{t('resourcesNotFound')}
|
||||
{t(
|
||||
"resourcesNotFound"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{resources.map(
|
||||
@@ -367,7 +379,9 @@ export default function CreateShareLinkForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('shareTitleOptional')}
|
||||
{t(
|
||||
"shareTitleOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -379,7 +393,9 @@ export default function CreateShareLinkForm({
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>{t('expireIn')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("expireIn")}
|
||||
</FormLabel>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -391,11 +407,17 @@ export default function CreateShareLinkForm({
|
||||
field.onChange
|
||||
}
|
||||
defaultValue={field.value.toString()}
|
||||
disabled={neverExpire}
|
||||
disabled={
|
||||
neverExpire
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('selectDuration')} />
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectDuration"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -458,12 +480,12 @@ export default function CreateShareLinkForm({
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{t('neverExpire')}
|
||||
{t("neverExpire")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('shareExpireDescription')}
|
||||
{t("shareExpireDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
@@ -471,16 +493,15 @@ export default function CreateShareLinkForm({
|
||||
)}
|
||||
{link && (
|
||||
<div className="max-w-md space-y-4">
|
||||
<p>
|
||||
{t('shareSeeOnce')}
|
||||
</p>
|
||||
<p>
|
||||
{t('shareAccessHint')}
|
||||
</p>
|
||||
<p>{t("shareSeeOnce")}</p>
|
||||
<p>{t("shareAccessHint")}</p>
|
||||
|
||||
<div className="h-[250px] w-full mx-auto flex items-center justify-center">
|
||||
<div className="bg-white p-6 border rounded-md">
|
||||
<QRCodeCanvas value={link} size={200} />
|
||||
<QRCodeCanvas
|
||||
value={link}
|
||||
size={200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -503,12 +524,12 @@ export default function CreateShareLinkForm({
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t('shareTokenUsage')}
|
||||
{t("shareTokenUsage")}
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{t('toggle')}
|
||||
{t("toggle")}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -538,7 +559,7 @@ export default function CreateShareLinkForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -546,7 +567,7 @@ export default function CreateShareLinkForm({
|
||||
loading={loading}
|
||||
disabled={link !== null || loading}
|
||||
>
|
||||
{t('createLink')}
|
||||
{t("createLink")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -78,7 +78,10 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||
|
||||
return (
|
||||
<CredenzaClose className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)} {...props}>
|
||||
<CredenzaClose
|
||||
className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</CredenzaClose>
|
||||
);
|
||||
@@ -128,7 +131,7 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader;
|
||||
|
||||
return (
|
||||
<CredenzaHeader className={className} {...props}>
|
||||
<CredenzaHeader className={cn("-mx-6 px-6 pb-6 border-b border-border", className)} {...props}>
|
||||
{children}
|
||||
</CredenzaHeader>
|
||||
);
|
||||
@@ -155,7 +158,13 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className={cn("px-0 mb-4 space-y-4 overflow-x-hidden min-w-0", className)} {...props}>
|
||||
<div
|
||||
className={cn(
|
||||
"px-0 mb-4 space-y-4 overflow-x-hidden min-w-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -168,7 +177,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
||||
|
||||
return (
|
||||
<CredenzaFooter className={cn("mt-8 md:mt-0", className)} {...props}>
|
||||
<CredenzaFooter className={cn("mt-8 md:mt-0 -mx-6 px-6 pt-6 border-t border-border", className)} {...props}>
|
||||
{children}
|
||||
</CredenzaFooter>
|
||||
);
|
||||
|
||||
@@ -21,10 +21,7 @@ type Props = {
|
||||
type: string | null;
|
||||
};
|
||||
|
||||
export default function DNSRecordsTable({
|
||||
records,
|
||||
type
|
||||
}: Props) {
|
||||
export default function DNSRecordsTable({ records, type }: Props) {
|
||||
const t = useTranslations();
|
||||
const env = useEnvContext();
|
||||
|
||||
@@ -114,11 +111,5 @@ export default function DNSRecordsTable({
|
||||
...(env.env.flags.usePangolinDns ? [statusColumn] : [])
|
||||
];
|
||||
|
||||
return (
|
||||
<DNSRecordsDataTable
|
||||
columns={columns}
|
||||
data={records}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
return <DNSRecordsDataTable columns={columns} data={records} type={type} />;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,11 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
<h1 className="font-bold">{t("dnsRecord")}</h1>
|
||||
<Badge variant="secondary">{t("required")}</Badge>
|
||||
</div>
|
||||
<Link href="https://docs.pangolin.net/manage/domains" target="_blank" rel="noopener noreferrer">
|
||||
<Link
|
||||
href="https://docs.pangolin.net/manage/domains"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline">
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
{t("howToAddRecords")}
|
||||
@@ -122,9 +126,7 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
>
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
|
||||
@@ -40,52 +40,54 @@ export function DataTablePagination<TData>({
|
||||
const handlePageSizeChange = (value: string) => {
|
||||
const newPageSize = Number(value);
|
||||
table.setPageSize(newPageSize);
|
||||
|
||||
|
||||
// Call the callback if provided (for persistence)
|
||||
if (onPageSizeChange) {
|
||||
onPageSizeChange(newPageSize);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageNavigation = (action: 'first' | 'previous' | 'next' | 'last') => {
|
||||
const handlePageNavigation = (
|
||||
action: "first" | "previous" | "next" | "last"
|
||||
) => {
|
||||
if (isServerPagination && onPageChange) {
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageCount = table.getPageCount();
|
||||
|
||||
|
||||
let newPage: number;
|
||||
switch (action) {
|
||||
case 'first':
|
||||
case "first":
|
||||
newPage = 0;
|
||||
break;
|
||||
case 'previous':
|
||||
case "previous":
|
||||
newPage = Math.max(0, currentPage - 1);
|
||||
break;
|
||||
case 'next':
|
||||
case "next":
|
||||
newPage = Math.min(pageCount - 1, currentPage + 1);
|
||||
break;
|
||||
case 'last':
|
||||
case "last":
|
||||
newPage = pageCount - 1;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (newPage !== currentPage) {
|
||||
onPageChange(newPage);
|
||||
}
|
||||
} else {
|
||||
// Use table's built-in navigation for client-side pagination
|
||||
switch (action) {
|
||||
case 'first':
|
||||
case "first":
|
||||
table.setPageIndex(0);
|
||||
break;
|
||||
case 'previous':
|
||||
case "previous":
|
||||
table.previousPage();
|
||||
break;
|
||||
case 'next':
|
||||
case "next":
|
||||
table.nextPage();
|
||||
break;
|
||||
case 'last':
|
||||
case "last":
|
||||
table.setPageIndex(table.getPageCount() - 1);
|
||||
break;
|
||||
}
|
||||
@@ -117,50 +119,66 @@ export function DataTablePagination<TData>({
|
||||
|
||||
<div className="flex items-center space-x-3 lg:space-x-8">
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
{isServerPagination && totalCount !== undefined ? (
|
||||
t('paginator', {
|
||||
current: table.getState().pagination.pageIndex + 1,
|
||||
last: Math.ceil(totalCount / table.getState().pagination.pageSize)
|
||||
})
|
||||
) : (
|
||||
t('paginator', {current: table.getState().pagination.pageIndex + 1, last: table.getPageCount()})
|
||||
)}
|
||||
{isServerPagination && totalCount !== undefined
|
||||
? t("paginator", {
|
||||
current:
|
||||
table.getState().pagination.pageIndex + 1,
|
||||
last: Math.ceil(
|
||||
totalCount /
|
||||
table.getState().pagination.pageSize
|
||||
)
|
||||
})
|
||||
: t("paginator", {
|
||||
current:
|
||||
table.getState().pagination.pageIndex + 1,
|
||||
last: table.getPageCount()
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => handlePageNavigation('first')}
|
||||
disabled={!table.getCanPreviousPage() || isLoading || disabled}
|
||||
onClick={() => handlePageNavigation("first")}
|
||||
disabled={
|
||||
!table.getCanPreviousPage() || isLoading || disabled
|
||||
}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToFirst')}</span>
|
||||
<span className="sr-only">{t("paginatorToFirst")}</span>
|
||||
<DoubleArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePageNavigation('previous')}
|
||||
disabled={!table.getCanPreviousPage() || isLoading || disabled}
|
||||
onClick={() => handlePageNavigation("previous")}
|
||||
disabled={
|
||||
!table.getCanPreviousPage() || isLoading || disabled
|
||||
}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToPrevious')}</span>
|
||||
<span className="sr-only">
|
||||
{t("paginatorToPrevious")}
|
||||
</span>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePageNavigation('next')}
|
||||
disabled={!table.getCanNextPage() || isLoading || disabled}
|
||||
onClick={() => handlePageNavigation("next")}
|
||||
disabled={
|
||||
!table.getCanNextPage() || isLoading || disabled
|
||||
}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToNext')}</span>
|
||||
<span className="sr-only">{t("paginatorToNext")}</span>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => handlePageNavigation('last')}
|
||||
disabled={!table.getCanNextPage() || isLoading || disabled}
|
||||
onClick={() => handlePageNavigation("last")}
|
||||
disabled={
|
||||
!table.getCanNextPage() || isLoading || disabled
|
||||
}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToLast')}</span>
|
||||
<span className="sr-only">{t("paginatorToLast")}</span>
|
||||
<DoubleArrowRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function DeleteRoleForm({
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const formSchema = z.object({
|
||||
newRoleId: z.string({ message: t('accessRoleErrorNewRequired') })
|
||||
newRoleId: z.string({ message: t("accessRoleErrorNewRequired") })
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -75,10 +75,10 @@ export default function DeleteRoleForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
title: t("accessRoleErrorFetch"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('accessRoleErrorFetchDescription')
|
||||
t("accessRoleErrorFetchDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -114,10 +114,10 @@ export default function DeleteRoleForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('accessRoleErrorRemove'),
|
||||
title: t("accessRoleErrorRemove"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('accessRoleErrorRemoveDescription')
|
||||
t("accessRoleErrorRemoveDescription")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -125,8 +125,8 @@ export default function DeleteRoleForm({
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t('accessRoleRemoved'),
|
||||
description: t('accessRoleRemovedDescription')
|
||||
title: t("accessRoleRemoved"),
|
||||
description: t("accessRoleRemovedDescription")
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -153,66 +153,66 @@ export default function DeleteRoleForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t('accessRoleRemove')}</CredenzaTitle>
|
||||
<CredenzaTitle>{t("accessRoleRemove")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('accessRoleRemoveDescription')}
|
||||
{t("accessRoleRemoveDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
|
||||
</p>
|
||||
<p>
|
||||
{t('accessRoleRequiredRemove')}
|
||||
</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="remove-role-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newRoleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('role')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem
|
||||
key={
|
||||
role.roleId
|
||||
}
|
||||
value={role.roleId.toString()}
|
||||
>
|
||||
{role.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t("accessRoleQuestionRemove", {
|
||||
name: roleToDelete.name
|
||||
})}
|
||||
</p>
|
||||
<p>{t("accessRoleRequiredRemove")}</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="remove-role-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newRoleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("role")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"accessRoleSelect"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem
|
||||
key={role.roleId}
|
||||
value={role.roleId.toString()}
|
||||
>
|
||||
{role.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@@ -221,7 +221,7 @@ export default function DeleteRoleForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('accessRoleRemoveSubmit')}
|
||||
{t("accessRoleRemoveSubmit")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -71,7 +71,9 @@ export function DeviceAuthConfirmation({
|
||||
<BrandingLogo height={logoHeight} width={logoWidth} />
|
||||
</div>
|
||||
<div className="text-center space-y-1 pt-3">
|
||||
<p className="text-muted-foreground">{t("deviceActivation")}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{t("deviceActivation")}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
@@ -93,7 +95,9 @@ export function DeviceAuthConfirmation({
|
||||
</p>
|
||||
{metadata.deviceName && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t("deviceLabel", { deviceName: metadata.deviceName })}
|
||||
{t("deviceLabel", {
|
||||
deviceName: metadata.deviceName
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
@@ -103,7 +107,9 @@ export function DeviceAuthConfirmation({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="text-sm font-medium">{t("deviceExistingAccess")}</p>
|
||||
<p className="text-sm font-medium">
|
||||
{t("deviceExistingAccess")}
|
||||
</p>
|
||||
<div className="space-y-1 pl-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
@@ -111,9 +117,7 @@ export function DeviceAuthConfirmation({
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>
|
||||
{t("deviceOrganizationsAccess")}
|
||||
</span>
|
||||
<span>{t("deviceOrganizationsAccess")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,7 +140,9 @@ export function DeviceAuthConfirmation({
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
{t("deviceAuthorize", { applicationName: metadata.applicationName })}
|
||||
{t("deviceAuthorize", {
|
||||
applicationName: metadata.applicationName
|
||||
})}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -51,8 +51,8 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const disableSchema = z.object({
|
||||
password: z.string().min(1, { message: t('passwordRequired') }),
|
||||
code: z.string().min(1, { message: t('verificationCodeRequired') })
|
||||
password: z.string().min(1, { message: t("passwordRequired") }),
|
||||
code: z.string().min(1, { message: t("verificationCodeRequired") })
|
||||
});
|
||||
|
||||
const disableForm = useForm<z.infer<typeof disableSchema>>({
|
||||
@@ -73,10 +73,10 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
} as Disable2faBody)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
title: t('otpErrorDisable'),
|
||||
title: t("otpErrorDisable"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('otpErrorDisableDescription')
|
||||
t("otpErrorDisableDescription")
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
@@ -111,11 +111,9 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t('otpRemove')}
|
||||
</CredenzaTitle>
|
||||
<CredenzaTitle>{t("otpRemove")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('otpRemoveDescription')}
|
||||
{t("otpRemoveDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -132,7 +130,9 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("password")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
@@ -150,7 +150,7 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('otpSetupSecretCode')}
|
||||
{t("otpSetupSecretCode")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -171,17 +171,15 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
size={48}
|
||||
/>
|
||||
<p className="font-semibold text-lg">
|
||||
{t('otpRemoveSuccess')}
|
||||
</p>
|
||||
<p>
|
||||
{t('otpRemoveSuccessMessage')}
|
||||
{t("otpRemoveSuccess")}
|
||||
</p>
|
||||
<p>{t("otpRemoveSuccessMessage")}</p>
|
||||
</div>
|
||||
)}
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
{step === "password" && (
|
||||
<Button
|
||||
@@ -190,7 +188,7 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('otpRemoveSubmit')}
|
||||
{t("otpRemoveSubmit")}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
|
||||
@@ -295,7 +295,7 @@ export default function EditInternalResourceDialog({
|
||||
// an alias is required
|
||||
if (data.mode === "host" && isHostname(data.destination)) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
|
||||
|
||||
if (!currentAlias) {
|
||||
// Prefill alias based on destination
|
||||
let aliasValue = data.destination;
|
||||
@@ -304,7 +304,7 @@ export default function EditInternalResourceDialog({
|
||||
const cleanedName = cleanForFQDN(data.name);
|
||||
aliasValue = `${cleanedName}.internal`;
|
||||
}
|
||||
|
||||
|
||||
// Update the form with the prefilled alias
|
||||
form.setValue("alias", aliasValue);
|
||||
data.alias = aliasValue;
|
||||
@@ -389,7 +389,7 @@ export default function EditInternalResourceDialog({
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const resourceChanged = previousResourceId.current !== resource.id;
|
||||
|
||||
|
||||
if (resourceChanged) {
|
||||
form.reset({
|
||||
name: resource.name,
|
||||
@@ -402,10 +402,18 @@ export default function EditInternalResourceDialog({
|
||||
});
|
||||
previousResourceId.current = resource.id;
|
||||
}
|
||||
|
||||
|
||||
hasInitialized.current = false;
|
||||
}
|
||||
}, [open, resource.id, resource.name, resource.mode, resource.destination, resource.alias, form]);
|
||||
}, [
|
||||
open,
|
||||
resource.id,
|
||||
resource.name,
|
||||
resource.mode,
|
||||
resource.destination,
|
||||
resource.alias,
|
||||
form
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && !loadingRolesUsers && !hasInitialized.current) {
|
||||
|
||||
@@ -21,7 +21,10 @@ type Enable2FaDialogProps = {
|
||||
setOpen: (val: boolean) => void;
|
||||
};
|
||||
|
||||
export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps) {
|
||||
export default function Enable2FaDialog({
|
||||
open,
|
||||
setOpen
|
||||
}: Enable2FaDialogProps) {
|
||||
const t = useTranslations();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -49,21 +52,22 @@ export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps)
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t('otpSetup')}
|
||||
</CredenzaTitle>
|
||||
<CredenzaTitle>{t("otpSetup")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t('otpSetupDescription')}
|
||||
{t("otpSetupDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<TwoFactorSetupForm
|
||||
ref={formRef}
|
||||
isDialog={true}
|
||||
submitButtonText={t('submit')}
|
||||
submitButtonText={t("submit")}
|
||||
cancelButtonText="Close"
|
||||
showCancelButton={false}
|
||||
onComplete={() => {setOpen(false); updateUser({ twoFactorEnabled: true });}}
|
||||
onComplete={() => {
|
||||
setOpen(false);
|
||||
updateUser({ twoFactorEnabled: true });
|
||||
}}
|
||||
onStepChange={setCurrentStep}
|
||||
onLoadingChange={setLoading}
|
||||
/>
|
||||
@@ -79,11 +83,11 @@ export default function Enable2FaDialog({ open, setOpen }: Enable2FaDialogProps)
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t('submit')}
|
||||
{t("submit")}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,24 +21,24 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
|
||||
<Alert>
|
||||
<AlertDescription className="mt-4">
|
||||
<InfoSections cols={2}>
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{remoteExitNode.online ? (
|
||||
<div className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>{t("online")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>{t("offline")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{remoteExitNode.online ? (
|
||||
<div className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>{t("online")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>{t("offline")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("address")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
|
||||
interface HeadersInputProps {
|
||||
value?: { name: string, value: string }[] | null;
|
||||
onChange: (value: { name: string, value: string }[] | null) => void;
|
||||
value?: { name: string; value: string }[] | null;
|
||||
onChange: (value: { name: string; value: string }[] | null) => void;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HeadersInput({
|
||||
value = [],
|
||||
onChange,
|
||||
export function HeadersInput({
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder = `X-Example-Header: example-value
|
||||
X-Another-Header: another-value`,
|
||||
rows = 4,
|
||||
@@ -25,34 +24,40 @@ X-Another-Header: another-value`,
|
||||
const isUserEditingRef = useRef(false);
|
||||
|
||||
// Convert header objects array to newline-separated string for display
|
||||
const convertToNewlineSeparated = (headers: { name: string, value: string }[] | null): string => {
|
||||
const convertToNewlineSeparated = (
|
||||
headers: { name: string; value: string }[] | null
|
||||
): string => {
|
||||
if (!headers || headers.length === 0) return "";
|
||||
|
||||
|
||||
return headers
|
||||
.map(header => `${header.name}: ${header.value}`)
|
||||
.join('\n');
|
||||
.map((header) => `${header.name}: ${header.value}`)
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
// Convert newline-separated string to header objects array
|
||||
const convertToHeadersArray = (newlineSeparated: string): { name: string, value: string }[] | null => {
|
||||
const convertToHeadersArray = (
|
||||
newlineSeparated: string
|
||||
): { name: string; value: string }[] | null => {
|
||||
if (!newlineSeparated || newlineSeparated.trim() === "") return [];
|
||||
|
||||
|
||||
return newlineSeparated
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0 && line.includes(':'))
|
||||
.map(line => {
|
||||
const colonIndex = line.indexOf(':');
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && line.includes(":"))
|
||||
.map((line) => {
|
||||
const colonIndex = line.indexOf(":");
|
||||
const name = line.substring(0, colonIndex).trim();
|
||||
const value = line.substring(colonIndex + 1).trim();
|
||||
|
||||
|
||||
// Ensure header name conforms to HTTP header requirements
|
||||
// Header names should be case-insensitive, contain only ASCII letters, digits, and hyphens
|
||||
const normalizedName = name.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase();
|
||||
|
||||
const normalizedName = name
|
||||
.replace(/[^a-zA-Z0-9\-]/g, "")
|
||||
.toLowerCase();
|
||||
|
||||
return { name: normalizedName, value };
|
||||
})
|
||||
.filter(header => header.name.length > 0); // Filter out headers with invalid names
|
||||
.filter((header) => header.name.length > 0); // Filter out headers with invalid names
|
||||
};
|
||||
|
||||
// Update internal value when external value changes
|
||||
@@ -66,26 +71,28 @@ X-Another-Header: another-value`,
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInternalValue(newValue);
|
||||
|
||||
|
||||
// Mark that user is actively editing
|
||||
isUserEditingRef.current = true;
|
||||
|
||||
|
||||
// Only update parent if the input is in a valid state
|
||||
// Valid states: empty/whitespace only, or contains properly formatted headers
|
||||
|
||||
|
||||
if (newValue.trim() === "") {
|
||||
// Empty input is valid - represents no headers
|
||||
onChange([]);
|
||||
} else {
|
||||
// Check if all non-empty lines are properly formatted (contain ':')
|
||||
const lines = newValue.split('\n');
|
||||
const lines = newValue.split("\n");
|
||||
const nonEmptyLines = lines
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
// If there are no non-empty lines, or all non-empty lines contain ':', it's valid
|
||||
const isValid = nonEmptyLines.length === 0 || nonEmptyLines.every(line => line.includes(':'));
|
||||
|
||||
const isValid =
|
||||
nonEmptyLines.length === 0 ||
|
||||
nonEmptyLines.every((line) => line.includes(":"));
|
||||
|
||||
if (isValid) {
|
||||
// Safe to convert and update parent
|
||||
const headersArray = convertToHeadersArray(newValue);
|
||||
|
||||
@@ -88,7 +88,7 @@ export function HorizontalTabs({
|
||||
variant="outlinePrimary"
|
||||
className="ml-2"
|
||||
>
|
||||
{t('licenseBadge')}
|
||||
{t("licenseBadge")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,22 +51,26 @@ type IdpCreateWizardProps = {
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: IdpCreateWizardProps) {
|
||||
export function IdpCreateWizard({
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
loading = false
|
||||
}: IdpCreateWizardProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const createIdpFormSchema = z.object({
|
||||
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
|
||||
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||
type: z.enum(["oidc"]),
|
||||
clientId: z.string().min(1, { message: t('idpClientIdRequired') }),
|
||||
clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }),
|
||||
authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
identifierPath: z
|
||||
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||
clientSecret: z
|
||||
.string()
|
||||
.min(1, { message: t('idpPathRequired') }),
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
|
||||
identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
|
||||
emailPath: z.string().optional(),
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().min(1, { message: t('idpScopeRequired') }),
|
||||
scopes: z.string().min(1, { message: t("idpScopeRequired") }),
|
||||
autoProvision: z.boolean().default(false)
|
||||
});
|
||||
|
||||
@@ -80,7 +84,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
{
|
||||
id: "oidc",
|
||||
title: "OAuth2/OIDC",
|
||||
description: t('idpOidcDescription')
|
||||
description: t("idpOidcDescription")
|
||||
}
|
||||
];
|
||||
|
||||
@@ -110,11 +114,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpTitle')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionTitle>{t("idpTitle")}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpCreateSettingsDescription')}
|
||||
{t("idpCreateSettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -130,12 +132,15 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormLabel>{t("name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={loading} />
|
||||
<Input
|
||||
{...field}
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpDisplayName')}
|
||||
{t("idpDisplayName")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -145,7 +150,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
<div className="flex items-start mb-0">
|
||||
<SwitchInput
|
||||
id="auto-provision-toggle"
|
||||
label={t('idpAutoProvisionUsers')}
|
||||
label={t("idpAutoProvisionUsers")}
|
||||
defaultChecked={form.getValues(
|
||||
"autoProvision"
|
||||
)}
|
||||
@@ -159,7 +164,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('idpAutoProvisionUsersDescription')}
|
||||
{t("idpAutoProvisionUsersDescription")}
|
||||
</span>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -169,11 +174,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpType')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionTitle>{t("idpType")}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpTypeDescription')}
|
||||
{t("idpTypeDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -193,10 +196,10 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpOidcConfigure')}
|
||||
{t("idpOidcConfigure")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpOidcConfigureDescription')}
|
||||
{t("idpOidcConfigureDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -212,13 +215,18 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpClientId')}
|
||||
{t("idpClientId")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={loading} />
|
||||
<Input
|
||||
{...field}
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpClientIdDescription')}
|
||||
{t(
|
||||
"idpClientIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -231,7 +239,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpClientSecret')}
|
||||
{t("idpClientSecret")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -241,7 +249,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpClientSecretDescription')}
|
||||
{t(
|
||||
"idpClientSecretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -254,7 +264,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpAuthUrl')}
|
||||
{t("idpAuthUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -264,7 +274,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpAuthUrlDescription')}
|
||||
{t("idpAuthUrlDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -277,7 +287,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpTokenUrl')}
|
||||
{t("idpTokenUrl")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -287,7 +297,9 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpTokenUrlDescription')}
|
||||
{t(
|
||||
"idpTokenUrlDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -299,10 +311,10 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('idpOidcConfigureAlert')}
|
||||
{t("idpOidcConfigureAlert")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('idpOidcConfigureAlertDescription')}
|
||||
{t("idpOidcConfigureAlertDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</SettingsSectionBody>
|
||||
@@ -311,10 +323,10 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('idpToken')}
|
||||
{t("idpToken")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('idpTokenDescription')}
|
||||
{t("idpTokenDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -327,17 +339,19 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('idpJmespathAbout')}
|
||||
{t("idpJmespathAbout")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('idpJmespathAboutDescription')}{" "}
|
||||
{t("idpJmespathAboutDescription")}{" "}
|
||||
<a
|
||||
href="https://jmespath.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center"
|
||||
>
|
||||
{t('idpJmespathAboutDescriptionLink')}{" "}
|
||||
{t(
|
||||
"idpJmespathAboutDescriptionLink"
|
||||
)}{" "}
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</a>
|
||||
</AlertDescription>
|
||||
@@ -349,13 +363,18 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathLabel')}
|
||||
{t("idpJmespathLabel")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={loading} />
|
||||
<Input
|
||||
{...field}
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathLabelDescription')}
|
||||
{t(
|
||||
"idpJmespathLabelDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -368,13 +387,20 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathEmailPathOptional')}
|
||||
{t(
|
||||
"idpJmespathEmailPathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={loading} />
|
||||
<Input
|
||||
{...field}
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathEmailPathOptionalDescription')}
|
||||
{t(
|
||||
"idpJmespathEmailPathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -387,13 +413,20 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpJmespathNamePathOptional')}
|
||||
{t(
|
||||
"idpJmespathNamePathOptional"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={loading} />
|
||||
<Input
|
||||
{...field}
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpJmespathNamePathOptionalDescription')}
|
||||
{t(
|
||||
"idpJmespathNamePathOptionalDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -406,13 +439,20 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('idpOidcConfigureScopes')}
|
||||
{t(
|
||||
"idpOidcConfigureScopes"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={loading} />
|
||||
<Input
|
||||
{...field}
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('idpOidcConfigureScopesDescription')}
|
||||
{t(
|
||||
"idpOidcConfigureScopesDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@@ -19,7 +17,6 @@ export function InvitationsDataTable<TData, TValue>({
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -27,8 +24,8 @@ export function InvitationsDataTable<TData, TValue>({
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="invitations-table"
|
||||
title={t('invite')}
|
||||
searchPlaceholder={t('inviteSearch')}
|
||||
title={t("invite")}
|
||||
searchPlaceholder={t("inviteSearch")}
|
||||
searchColumn="email"
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
|
||||
@@ -72,12 +72,12 @@ export default function InvitationsTable({
|
||||
accessorKey: "email",
|
||||
enableHiding: false,
|
||||
friendlyName: t("email"),
|
||||
header: () => (<span className="p-3">{t("email")}</span>)
|
||||
header: () => <span className="p-3">{t("email")}</span>
|
||||
},
|
||||
{
|
||||
accessorKey: "expiresAt",
|
||||
friendlyName: t("expiresAt"),
|
||||
header: () => (<span className="p-3">{t("expiresAt")}</span>),
|
||||
header: () => <span className="p-3">{t("expiresAt")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const expiresAt = new Date(row.original.expiresAt);
|
||||
const isExpired = expiresAt < new Date();
|
||||
@@ -92,7 +92,7 @@ export default function InvitationsTable({
|
||||
{
|
||||
accessorKey: "role",
|
||||
friendlyName: t("role"),
|
||||
header: () => (<span className="p-3">{t("role")}</span>)
|
||||
header: () => <span className="p-3">{t("role")}</span>
|
||||
},
|
||||
{
|
||||
id: "dots",
|
||||
@@ -183,9 +183,7 @@ export default function InvitationsTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("inviteQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("inviteQuestionRemove")}</p>
|
||||
<p>{t("inviteMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -72,10 +72,12 @@ export async function Layout({
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
|
||||
<div className={cn(
|
||||
"container mx-auto max-w-12xl mb-12",
|
||||
showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"container mx-auto max-w-12xl mb-12",
|
||||
showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -133,7 +133,8 @@ export function LicenseKeysDataTable({
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Button variant={"outline"}
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => onDelete(row.original)}
|
||||
>
|
||||
{t("delete")}
|
||||
|
||||
@@ -17,15 +17,13 @@ export default function LicenseViolation() {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 w-full bg-red-500 text-white p-4 text-center z-50">
|
||||
<div className="flex justify-between items-center">
|
||||
<p>
|
||||
{t('componentsInvalidKey')}
|
||||
</p>
|
||||
<p>{t("componentsInvalidKey")}</p>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className="hover:bg-yellow-500"
|
||||
onClick={() => setIsDismissed(true)}
|
||||
>
|
||||
{t('dismiss')}
|
||||
{t("dismiss")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,4 +64,4 @@ export default function LocaleSwitcher() {
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,10 @@ const STORAGE_KEYS = {
|
||||
tableId ? `${tableId}-size` : STORAGE_KEYS.PAGE_SIZE
|
||||
};
|
||||
|
||||
export const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
export const getStoredPageSize = (
|
||||
tableId?: string,
|
||||
defaultSize = 20
|
||||
): number => {
|
||||
if (typeof window === "undefined") return defaultSize;
|
||||
|
||||
try {
|
||||
@@ -145,7 +148,7 @@ export function LogDataTable<TData, TValue>({
|
||||
onPageSizeChange: onPageSizeChangeProp,
|
||||
isLoading = false,
|
||||
expandable = false,
|
||||
disabled=false,
|
||||
disabled = false,
|
||||
renderExpandedRow
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
@@ -313,7 +316,7 @@ export function LogDataTable<TData, TValue>({
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
if (disabled) return;
|
||||
|
||||
|
||||
setActiveTab(value);
|
||||
// Reset to first page when changing tabs
|
||||
table.setPageIndex(0);
|
||||
@@ -322,7 +325,7 @@ export function LogDataTable<TData, TValue>({
|
||||
// Enhanced pagination component that updates our local state
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
if (disabled) return;
|
||||
|
||||
|
||||
// setPageSize(newPageSize);
|
||||
table.setPageSize(newPageSize);
|
||||
|
||||
@@ -340,7 +343,7 @@ export function LogDataTable<TData, TValue>({
|
||||
// Handle page changes for server pagination
|
||||
const handlePageChange = (newPageIndex: number) => {
|
||||
if (disabled) return;
|
||||
|
||||
|
||||
if (isServerPagination && onPageChange) {
|
||||
onPageChange(newPageIndex);
|
||||
}
|
||||
@@ -351,7 +354,7 @@ export function LogDataTable<TData, TValue>({
|
||||
end: DateTimeValue
|
||||
) => {
|
||||
if (disabled) return;
|
||||
|
||||
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
onDateRangeChange?.(start, end);
|
||||
@@ -397,7 +400,10 @@ export function LogDataTable<TData, TValue>({
|
||||
</Button>
|
||||
)}
|
||||
{onExport && (
|
||||
<Button onClick={() => !disabled && onExport()} disabled={isExporting || disabled}>
|
||||
<Button
|
||||
onClick={() => !disabled && onExport()}
|
||||
disabled={isExporting || disabled}
|
||||
>
|
||||
<Download
|
||||
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
|
||||
/>
|
||||
@@ -427,61 +433,63 @@ export function LogDataTable<TData, TValue>({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => {
|
||||
const isExpanded =
|
||||
expandable && expandedRows.has(row.id);
|
||||
return [
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={
|
||||
row.getIsSelected() &&
|
||||
"selected"
|
||||
}
|
||||
onClick={() =>
|
||||
expandable && !disabled
|
||||
? toggleRowExpansion(
|
||||
row.id
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
className="text-xs" // made smaller
|
||||
>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const originalRow =
|
||||
row.original as any;
|
||||
const actionValue =
|
||||
originalRow?.action;
|
||||
let className = "";
|
||||
table
|
||||
.getRowModel()
|
||||
.rows.map((row) => {
|
||||
const isExpanded =
|
||||
expandable &&
|
||||
expandedRows.has(row.id);
|
||||
return [
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={
|
||||
row.getIsSelected() &&
|
||||
"selected"
|
||||
}
|
||||
onClick={() =>
|
||||
expandable && !disabled
|
||||
? toggleRowExpansion(
|
||||
row.id
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
className="text-xs" // made smaller
|
||||
>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const originalRow =
|
||||
row.original as any;
|
||||
const actionValue =
|
||||
originalRow?.action;
|
||||
let className = "";
|
||||
|
||||
if (
|
||||
typeof actionValue ===
|
||||
"boolean"
|
||||
) {
|
||||
className =
|
||||
actionValue
|
||||
? "bg-green-100 dark:bg-green-900/50"
|
||||
: "bg-red-100 dark:bg-red-900/50";
|
||||
}
|
||||
if (
|
||||
typeof actionValue ===
|
||||
"boolean"
|
||||
) {
|
||||
className =
|
||||
actionValue
|
||||
? "bg-green-100 dark:bg-green-900/50"
|
||||
: "bg-red-100 dark:bg-red-900/50";
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`${className} py-2`} // made smaller
|
||||
>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>,
|
||||
isExpanded &&
|
||||
renderExpandedRow && (
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`${className} py-2`} // made smaller
|
||||
>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>,
|
||||
isExpanded && renderExpandedRow && (
|
||||
<TableRow
|
||||
key={`${row.id}-expanded`}
|
||||
>
|
||||
@@ -497,8 +505,9 @@ export function LogDataTable<TData, TValue>({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
].filter(Boolean);
|
||||
}).flat()
|
||||
].filter(Boolean);
|
||||
})
|
||||
.flat()
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
|
||||
@@ -239,10 +239,10 @@ export default function LoginForm({
|
||||
try {
|
||||
const response = await loginProxy(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
code,
|
||||
resourceGuid: resourceGuid as string
|
||||
email,
|
||||
password,
|
||||
code,
|
||||
resourceGuid: resourceGuid as string
|
||||
},
|
||||
forceLogin
|
||||
);
|
||||
@@ -364,7 +364,7 @@ export default function LoginForm({
|
||||
{forceLogin && (
|
||||
<Alert variant="neutral">
|
||||
<AlertDescription className="flex items-center gap-2">
|
||||
<LockIcon className="w-4 h-4" />
|
||||
<LockIcon className="w-4 h-4" />
|
||||
{t("loginRequiredForDevice")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -19,7 +19,6 @@ export function OrgApiKeysDataTable<TData, TValue>({
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -27,13 +26,13 @@ export function OrgApiKeysDataTable<TData, TValue>({
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="Org-apikeys-table"
|
||||
title={t('apiKeys')}
|
||||
searchPlaceholder={t('searchApiKeys')}
|
||||
title={t("apiKeys")}
|
||||
searchPlaceholder={t("searchApiKeys")}
|
||||
searchColumn="name"
|
||||
onAdd={addApiKey}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
addButtonText={t('apiKeysAdd')}
|
||||
addButtonText={t("apiKeysAdd")}
|
||||
enableColumnVisibility={true}
|
||||
stickyLeftColumn="name"
|
||||
stickyRightColumn="actions"
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function OrgApiKeysTable({
|
||||
{
|
||||
accessorKey: "key",
|
||||
friendlyName: t("key"),
|
||||
header: () => (<span className="p-3">{t("key")}</span>),
|
||||
header: () => <span className="p-3">{t("key")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
@@ -120,7 +120,7 @@ export default function OrgApiKeysTable({
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
friendlyName: t("createdAt"),
|
||||
header: () => (<span className="p-3">{t("createdAt")}</span>),
|
||||
header: () => <span className="p-3">{t("createdAt")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")}</span>;
|
||||
@@ -149,7 +149,9 @@ export default function OrgApiKeysTable({
|
||||
setSelected(r);
|
||||
}}
|
||||
>
|
||||
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
||||
<Link
|
||||
href={`/${orgId}/settings/api-keys/${r.id}`}
|
||||
>
|
||||
{t("viewSettings")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
@@ -166,9 +168,7 @@ export default function OrgApiKeysTable({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
@@ -190,13 +190,9 @@ export default function OrgApiKeysTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("apiKeysQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("apiKeysQuestionRemove")}</p>
|
||||
|
||||
<p>
|
||||
{t("apiKeysMessageRemove")}
|
||||
</p>
|
||||
<p>{t("apiKeysMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("apiKeysDeleteConfirm")}
|
||||
|
||||
@@ -41,7 +41,8 @@ export default function OrgPolicyResult({
|
||||
accessRes
|
||||
}: OrgPolicyResultProps) {
|
||||
const [show2FaDialog, setShow2FaDialog] = useState(false);
|
||||
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false);
|
||||
const [showChangePasswordDialog, setShowChangePasswordDialog] =
|
||||
useState(false);
|
||||
const t = useTranslations();
|
||||
const { user } = useUserContext();
|
||||
const router = useRouter();
|
||||
@@ -77,15 +78,15 @@ export default function OrgPolicyResult({
|
||||
if (accessRes.policies?.maxSessionLength) {
|
||||
const maxSessionPolicy = accessRes.policies?.maxSessionLength;
|
||||
const maxHours = maxSessionPolicy.maxSessionLengthHours;
|
||||
|
||||
|
||||
// Use hours if less than 24, otherwise convert to days
|
||||
const useHours = maxHours < 24;
|
||||
const maxTime = useHours ? maxHours : Math.round(maxHours / 24);
|
||||
|
||||
const descriptionKey = useHours
|
||||
|
||||
const descriptionKey = useHours
|
||||
? "reauthenticationDescriptionHours"
|
||||
: "reauthenticationDescription";
|
||||
|
||||
|
||||
const description = useHours
|
||||
? t(descriptionKey, { maxHours })
|
||||
: t(descriptionKey, { maxDays: maxTime });
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipTrigger
|
||||
} from "@app/components/ui/tooltip";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { cn } from "@app/lib/cn";
|
||||
@@ -36,7 +36,11 @@ interface OrgSelectorProps {
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorProps) {
|
||||
export function OrgSelector({
|
||||
orgId,
|
||||
orgs,
|
||||
isCollapsed = false
|
||||
}: OrgSelectorProps) {
|
||||
const { user } = useUserContext();
|
||||
const [open, setOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
@@ -65,10 +69,10 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
|
||||
<Building2 className="h-4 w-4 mr-3 shrink-0" />
|
||||
<div className="flex flex-col items-start min-w-0 flex-1">
|
||||
<span className="font-bold text-sm">
|
||||
{t('org')}
|
||||
{t("org")}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground truncate w-full text-left">
|
||||
{selectedOrg?.name || t('noneSelected')}
|
||||
{selectedOrg?.name || t("noneSelected")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,17 +84,20 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
|
||||
<PopoverContent className="w-[320px] p-0" align="start">
|
||||
<Command className="rounded-lg">
|
||||
<CommandInput
|
||||
placeholder={t('searchProgress')}
|
||||
placeholder={t("searchProgress")}
|
||||
className="border-0 focus:ring-0"
|
||||
/>
|
||||
<CommandEmpty className="py-6 text-center">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t('orgNotFound2')}
|
||||
{t("orgNotFound2")}
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
|
||||
<>
|
||||
<CommandGroup heading={t('create')} className="py-2">
|
||||
<CommandGroup
|
||||
heading={t("create")}
|
||||
className="py-2"
|
||||
>
|
||||
<CommandList>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
@@ -103,8 +110,12 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
|
||||
<Plus className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{t('setupNewOrg')}</span>
|
||||
<span className="text-xs text-muted-foreground">{t('createNewOrgDescription')}</span>
|
||||
<span className="font-medium">
|
||||
{t("setupNewOrg")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("createNewOrgDescription")}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
@@ -112,7 +123,7 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
|
||||
<CommandSeparator className="my-2" />
|
||||
</>
|
||||
)}
|
||||
<CommandGroup heading={t('orgs')} className="py-2">
|
||||
<CommandGroup heading={t("orgs")} className="py-2">
|
||||
<CommandList>
|
||||
{orgs?.map((org) => (
|
||||
<CommandItem
|
||||
@@ -127,13 +138,19 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
<span className="font-medium">{org.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{t('organization')}</span>
|
||||
<span className="font-medium">
|
||||
{org.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("organization")}
|
||||
</span>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary",
|
||||
orgId === org.orgId ? "opacity-100" : "opacity-0"
|
||||
orgId === org.orgId
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -154,8 +171,12 @@ export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorPro
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<div className="text-center">
|
||||
<p className="font-medium">{selectedOrg?.name || t('noneSelected')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('org')}</p>
|
||||
<p className="font-medium">
|
||||
{selectedOrg?.name || t("noneSelected")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("org")}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
CardFooter
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react";
|
||||
import {
|
||||
Users,
|
||||
Globe,
|
||||
Database,
|
||||
Cog,
|
||||
Settings,
|
||||
Waypoints,
|
||||
Combine
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface OrgStat {
|
||||
@@ -43,17 +51,17 @@ export default function OrganizationLandingCard(
|
||||
|
||||
const orgStats: OrgStat[] = [
|
||||
{
|
||||
label: t('sites'),
|
||||
label: t("sites"),
|
||||
value: orgData.overview.stats.sites,
|
||||
icon: <Combine className="h-6 w-6" />
|
||||
},
|
||||
{
|
||||
label: t('resources'),
|
||||
label: t("resources"),
|
||||
value: orgData.overview.stats.resources,
|
||||
icon: <Waypoints className="h-6 w-6" />
|
||||
},
|
||||
{
|
||||
label: t('users'),
|
||||
label: t("users"),
|
||||
value: orgData.overview.stats.users,
|
||||
icon: <Users className="h-6 w-6" />
|
||||
}
|
||||
@@ -84,9 +92,11 @@ export default function OrganizationLandingCard(
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center text-lg">
|
||||
{t('accessRoleYour')}{" "}
|
||||
{t("accessRoleYour")}{" "}
|
||||
<span className="font-semibold">
|
||||
{orgData.overview.isOwner ? t('accessRoleOwner') : orgData.overview.userRole}
|
||||
{orgData.overview.isOwner
|
||||
? t("accessRoleOwner")
|
||||
: orgData.overview.userRole}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -95,7 +105,7 @@ export default function OrganizationLandingCard(
|
||||
<Link href={`/${orgData.overview.orgId}/settings`}>
|
||||
<Button size="lg" className="w-full md:w-auto">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t('orgGeneralSettings')}
|
||||
{t("orgGeneralSettings")}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
|
||||
@@ -60,7 +60,10 @@ export function PathMatchModal({
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const getPlaceholder = () => (matchType === "regex" ? t("pathMatchRegexPlaceholder") : t("pathMatchDefaultPlaceholder"));
|
||||
const getPlaceholder = () =>
|
||||
matchType === "regex"
|
||||
? t("pathMatchRegexPlaceholder")
|
||||
: t("pathMatchDefaultPlaceholder");
|
||||
|
||||
const getHelpText = () => {
|
||||
switch (matchType) {
|
||||
@@ -93,14 +96,22 @@ export function PathMatchModal({
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="prefix">{t("pathMatchPrefix")}</SelectItem>
|
||||
<SelectItem value="exact">{t("pathMatchExact")}</SelectItem>
|
||||
<SelectItem value="regex">{t("pathMatchRegex")}</SelectItem>
|
||||
<SelectItem value="prefix">
|
||||
{t("pathMatchPrefix")}
|
||||
</SelectItem>
|
||||
<SelectItem value="exact">
|
||||
{t("pathMatchExact")}
|
||||
</SelectItem>
|
||||
<SelectItem value="regex">
|
||||
{t("pathMatchRegex")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="path-value">{t("pathMatchValue")}</Label>
|
||||
<Label htmlFor="path-value">
|
||||
{t("pathMatchValue")}
|
||||
</Label>
|
||||
<Input
|
||||
id="path-value"
|
||||
placeholder={getPlaceholder()}
|
||||
@@ -115,9 +126,9 @@ export function PathMatchModal({
|
||||
<CredenzaFooter className="gap-2">
|
||||
{/* {value?.path && (
|
||||
)} */}
|
||||
<Button variant="outline" onClick={handleClear}>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClear}>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!path.trim()}>
|
||||
{t("saveChanges")}
|
||||
</Button>
|
||||
@@ -206,7 +217,9 @@ export function PathRewriteModal({
|
||||
</CredenzaHeader>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="rewrite-type">{t("pathRewriteType")}</Label>
|
||||
<Label htmlFor="rewrite-type">
|
||||
{t("pathRewriteType")}
|
||||
</Label>
|
||||
<Select
|
||||
value={rewriteType}
|
||||
onValueChange={setRewriteType}
|
||||
@@ -231,7 +244,9 @@ export function PathRewriteModal({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="rewrite-value">{t("pathRewriteValue")}</Label>
|
||||
<Label htmlFor="rewrite-value">
|
||||
{t("pathRewriteValue")}
|
||||
</Label>
|
||||
<Input
|
||||
id="rewrite-value"
|
||||
placeholder={getPlaceholder()}
|
||||
@@ -269,7 +284,7 @@ export function PathMatchDisplay({
|
||||
value: { path: string | null; pathMatchType: string | null };
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
|
||||
|
||||
if (!value?.path) return null;
|
||||
|
||||
const getTypeLabel = (type: string | null) => {
|
||||
@@ -300,7 +315,7 @@ export function PathRewriteDisplay({
|
||||
value: { rewritePath: string | null; rewritePathType: string | null };
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
|
||||
|
||||
if (!value?.rewritePath && value?.rewritePathType !== "stripPrefix")
|
||||
return null;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user