mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-17 02:16:38 +00:00
I18n orgId/settings (#21)
* New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys if settings/resources/resourceId/authentication * New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys if settings/resources/resourceId/general * Small naming fix * New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys if settings/access/roles * New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys in orgId/settings * Fixes after merge * Fixes after merge * Fixes after merge * Small fix * Fix build
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface OrgStat {
|
||||
label: string;
|
||||
@@ -38,19 +39,21 @@ export default function OrganizationLandingCard(
|
||||
) {
|
||||
const [orgData] = useState(props);
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const orgStats: OrgStat[] = [
|
||||
{
|
||||
label: "Sites",
|
||||
label: t('sites'),
|
||||
value: orgData.overview.stats.sites,
|
||||
icon: <Combine className="h-6 w-6" />
|
||||
},
|
||||
{
|
||||
label: "Resources",
|
||||
label: t('resources'),
|
||||
value: orgData.overview.stats.resources,
|
||||
icon: <Waypoints className="h-6 w-6" />
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
label: t('users'),
|
||||
value: orgData.overview.stats.users,
|
||||
icon: <Users className="h-6 w-6" />
|
||||
}
|
||||
@@ -81,9 +84,9 @@ export default function OrganizationLandingCard(
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center text-lg">
|
||||
Your role:{" "}
|
||||
{t('accessRoleYour')}{" "}
|
||||
<span className="font-semibold">
|
||||
{orgData.overview.isOwner ? "Owner" : orgData.overview.userRole}
|
||||
{orgData.overview.isOwner ? t('accessRoleOwner') : orgData.overview.userRole}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -92,7 +95,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" />
|
||||
Organization Settings
|
||||
{t('orgGeneralSettings')}
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface AccessPageHeaderAndNavProps {
|
||||
children: React.ReactNode;
|
||||
@@ -12,20 +13,21 @@ export default function AccessPageHeaderAndNav({
|
||||
children,
|
||||
hasInvitations
|
||||
}: AccessPageHeaderAndNavProps) {
|
||||
const t = useTranslations();
|
||||
const navItems = [
|
||||
{
|
||||
title: "Users",
|
||||
title: t('users'),
|
||||
href: `/{orgId}/settings/access/users`
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
title: t('roles'),
|
||||
href: `/{orgId}/settings/access/roles`
|
||||
}
|
||||
];
|
||||
|
||||
if (hasInvitations) {
|
||||
navItems.push({
|
||||
title: "Invitations",
|
||||
title: t('invite'),
|
||||
href: `/{orgId}/settings/access/invitations`
|
||||
});
|
||||
}
|
||||
@@ -33,8 +35,8 @@ export default function AccessPageHeaderAndNav({
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title="Manage Users & Roles"
|
||||
description="Invite users and add them to roles to manage access to your organization"
|
||||
title={t('accessUsersRoles')}
|
||||
description={t('accessUsersRolesDescription')}
|
||||
/>
|
||||
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function InvitationsTable({
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t('inviteQuestionRemove', {email: selectedInvitation?.email})}
|
||||
{t('inviteQuestionRemove', {email: selectedInvitation?.email || ''})}
|
||||
</p>
|
||||
<p>
|
||||
{t('inviteMessageRemove')}
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function RegenerateInvitationForm({
|
||||
{!inviteLink ? (
|
||||
<div>
|
||||
<p>
|
||||
{t('inviteQuestionRegenerate', {email: invitation?.email})}
|
||||
{t('inviteQuestionRegenerate', {email: invitation?.email || ''})}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
<Checkbox
|
||||
|
||||
@@ -67,7 +67,7 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
||||
id: invite.inviteId,
|
||||
email: invite.email,
|
||||
expiresAt: new Date(Number(invite.expiresAt)).toISOString(),
|
||||
role: invite.roleName || "Unknown Role",
|
||||
role: invite.roleName || t('accessRoleUnknown'),
|
||||
roleId: invite.roleId
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
open: boolean;
|
||||
@@ -38,8 +39,10 @@ type CreateRoleFormProps = {
|
||||
afterCreate?: (res: CreateRoleResponse) => Promise<void>;
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string({ message: "Name is required" }).max(32),
|
||||
name: z.string({ message: t('accessRoleNameRequired') }).max(32),
|
||||
description: z.string().max(255).optional()
|
||||
});
|
||||
|
||||
@@ -76,10 +79,10 @@ export default function CreateRoleForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create role",
|
||||
title: t('accessRoleErrorCreate'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the role."
|
||||
t('accessRoleErrorCreateDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -87,8 +90,8 @@ export default function CreateRoleForm({
|
||||
if (res && res.status === 201) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Role created",
|
||||
description: "The role has been successfully created."
|
||||
title: t('accessRoleCreated'),
|
||||
description: t('accessRoleCreatedDescription')
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -115,10 +118,9 @@ export default function CreateRoleForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Create Role</CredenzaTitle>
|
||||
<CredenzaTitle>{t('accessRoleCreate')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Create a new role to group users and manage their
|
||||
permissions.
|
||||
{t('accessRoleCreateDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -133,7 +135,7 @@ export default function CreateRoleForm({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role Name</FormLabel>
|
||||
<FormLabel>{t('accessRoleName')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -146,7 +148,7 @@ export default function CreateRoleForm({
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t('description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -159,7 +161,7 @@ export default function CreateRoleForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -167,7 +169,7 @@ export default function CreateRoleForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Create Role
|
||||
{t('accessRoleCreateSubmit')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -38,6 +38,7 @@ import { RoleRow } from "./RolesTable";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
open: boolean;
|
||||
@@ -46,8 +47,10 @@ type CreateRoleFormProps = {
|
||||
afterDelete?: () => void;
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const formSchema = z.object({
|
||||
newRoleId: z.string({ message: "New role is required" })
|
||||
newRoleId: z.string({ message: t('accessRoleErrorNewRequired') })
|
||||
});
|
||||
|
||||
export default function DeleteRoleForm({
|
||||
@@ -73,10 +76,10 @@ export default function DeleteRoleForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
t('accessRoleErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -112,10 +115,10 @@ export default function DeleteRoleForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to remove role",
|
||||
title: t('accessRoleErrorRemove'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the role."
|
||||
t('accessRoleErrorRemoveDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -123,8 +126,8 @@ export default function DeleteRoleForm({
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Role removed",
|
||||
description: "The role has been successfully removed."
|
||||
title: t('accessRoleRemoved'),
|
||||
description: t('accessRoleRemovedDescription')
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -151,22 +154,19 @@ export default function DeleteRoleForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Remove Role</CredenzaTitle>
|
||||
<CredenzaTitle>{t('accessRoleRemove')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Remove a role from the organization
|
||||
{t('accessRoleRemoveDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
You're about to delete the{" "}
|
||||
<b>{roleToDelete.name}</b> role. You cannot
|
||||
undo this action.
|
||||
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
|
||||
</p>
|
||||
<p>
|
||||
Before deleting this role, please select a
|
||||
new role to transfer existing members to.
|
||||
{t('accessRoleRequiredRemove')}
|
||||
</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
@@ -180,7 +180,7 @@ export default function DeleteRoleForm({
|
||||
name="newRoleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormLabel>{t('role')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
@@ -189,7 +189,7 @@ export default function DeleteRoleForm({
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -215,7 +215,7 @@ export default function DeleteRoleForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -223,7 +223,7 @@ export default function DeleteRoleForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Remove Role
|
||||
{t('accessRoleRemoveSubmit')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function UsersDataTable<TData, TValue>({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Users"
|
||||
title={t('users')}
|
||||
searchPlaceholder={t('accessUsersSearch')}
|
||||
searchColumn="email"
|
||||
onAdd={inviteUser}
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
variant="ghost"
|
||||
className="opacity-0 cursor-default"
|
||||
>
|
||||
Placeholder
|
||||
{t('placeholder')}
|
||||
</Button>
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
@@ -192,7 +192,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
variant={"outlinePrimary"}
|
||||
className="ml-2"
|
||||
>
|
||||
Manage
|
||||
{t('manage')}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -210,10 +210,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to remove user",
|
||||
title: t('userErrorOrgRemove'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the user."
|
||||
t('userErrorOrgRemoveDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -221,8 +221,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User removed",
|
||||
description: `The user ${selectedUser.email} has been removed from the organization.`
|
||||
title: t('userOrgRemoved'),
|
||||
description: t('userOrgRemovedDescription', {email: selectedUser.email || ''})
|
||||
});
|
||||
|
||||
setUsers((prev) =>
|
||||
@@ -244,29 +244,19 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to remove{" "}
|
||||
<b>
|
||||
{selectedUser?.email ||
|
||||
selectedUser?.name ||
|
||||
selectedUser?.username}
|
||||
</b>{" "}
|
||||
from the organization?
|
||||
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ''})}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once removed, this user will no longer have access
|
||||
to the organization. You can always re-invite them
|
||||
later, but they will need to accept the invitation
|
||||
again.
|
||||
{t('userMessageOrgRemove')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To confirm, please type the name of the of the user
|
||||
below.
|
||||
{t('userMessageOrgConfirm')}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Remove User"
|
||||
buttonText={t('userRemoveOrgConfirm')}
|
||||
onConfirm={removeUser}
|
||||
string={
|
||||
selectedUser?.email ||
|
||||
@@ -274,7 +264,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
selectedUser?.username ||
|
||||
""
|
||||
}
|
||||
title="Remove User from Organization"
|
||||
title={t('userRemoveOrg')}
|
||||
/>
|
||||
|
||||
<UsersDataTable
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string(),
|
||||
@@ -64,6 +65,8 @@ export default function AccessControlsPage() {
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchRoles() {
|
||||
const res = await api
|
||||
@@ -72,10 +75,10 @@ export default function AccessControlsPage() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
t('accessRoleErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -100,10 +103,10 @@ export default function AccessControlsPage() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to add user to role",
|
||||
title: t('accessRoleErrorAdd'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while adding user to the role."
|
||||
t('accessRoleErrorAddDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -111,8 +114,8 @@ export default function AccessControlsPage() {
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User saved",
|
||||
description: "The user has been updated."
|
||||
title: t('userSaved'),
|
||||
description: t('userSavedDescription')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,10 +126,9 @@ export default function AccessControlsPage() {
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>Access Controls</SettingsSectionTitle>
|
||||
<SettingsSectionTitle>{t('accessControls')}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Manage what this user can access and do in the
|
||||
organization
|
||||
{t('accessControlsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
@@ -143,14 +145,14 @@ export default function AccessControlsPage() {
|
||||
name="roleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormLabel>{t('role')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -180,7 +182,7 @@ export default function AccessControlsPage() {
|
||||
disabled={loading}
|
||||
form="access-controls-form"
|
||||
>
|
||||
Save Access Controls
|
||||
{t('accessControlsSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { cache } from "react";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface UserLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -26,6 +27,8 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
let user = null;
|
||||
try {
|
||||
const getOrgUser = cache(async () =>
|
||||
@@ -42,7 +45,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "Access Controls",
|
||||
title: t('accessControls'),
|
||||
href: "/{orgId}/settings/access/users/{userId}/access-controls"
|
||||
}
|
||||
];
|
||||
@@ -51,7 +54,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={`${user?.email}`}
|
||||
description="Manage the settings on this user"
|
||||
description={t('userDescription2')}
|
||||
/>
|
||||
<OrgUserProvider orgUser={user}>
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -44,6 +44,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { ListIdpsResponse } from "@server/routers/idp";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type UserType = "internal" | "oidc";
|
||||
|
||||
@@ -59,28 +60,30 @@ interface IdpOption {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const internalFormSchema = z.object({
|
||||
email: z.string().email({ message: "Invalid email address" }),
|
||||
validForHours: z.string().min(1, { message: "Please select a duration" }),
|
||||
roleId: z.string().min(1, { message: "Please select a role" })
|
||||
email: z.string().email({ message: t('emailInvalid') }),
|
||||
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
|
||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
|
||||
});
|
||||
|
||||
const externalFormSchema = z.object({
|
||||
username: z.string().min(1, { message: "Username is required" }),
|
||||
username: z.string().min(1, { message: t('usernameRequired') }),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: "Invalid email address" })
|
||||
.email({ message: t('emailInvalid') })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
name: z.string().optional(),
|
||||
roleId: z.string().min(1, { message: "Please select a role" }),
|
||||
idpId: z.string().min(1, { message: "Please select an identity provider" })
|
||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
|
||||
idpId: z.string().min(1, { message: t('idpSelectPlease') })
|
||||
});
|
||||
|
||||
const formatIdpType = (type: string) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case "oidc":
|
||||
return "Generic OAuth2/OIDC provider.";
|
||||
return t('idpGenericOidc');
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
@@ -103,13 +106,13 @@ export default function Page() {
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
|
||||
const validFor = [
|
||||
{ hours: 24, name: "1 day" },
|
||||
{ hours: 48, name: "2 days" },
|
||||
{ hours: 72, name: "3 days" },
|
||||
{ hours: 96, name: "4 days" },
|
||||
{ hours: 120, name: "5 days" },
|
||||
{ hours: 144, name: "6 days" },
|
||||
{ hours: 168, name: "7 days" }
|
||||
{ hours: 24, name: t('day', {count: 1}) },
|
||||
{ hours: 48, name: t('day', {count: 2}) },
|
||||
{ hours: 72, name: t('day', {count: 3}) },
|
||||
{ hours: 96, name: t('day', {count: 4}) },
|
||||
{ hours: 120, name: t('day', {count: 5}) },
|
||||
{ hours: 144, name: t('day', {count: 6}) },
|
||||
{ hours: 168, name: t('day', {count: 7}) }
|
||||
];
|
||||
|
||||
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
|
||||
@@ -155,10 +158,10 @@ export default function Page() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
t('accessRoleErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -178,10 +181,10 @@ export default function Page() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch identity providers",
|
||||
title: t('idpErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching identity providers"
|
||||
t('idpErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -218,17 +221,16 @@ export default function Page() {
|
||||
if (e.response?.status === 409) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "User Already Exists",
|
||||
description:
|
||||
"This user is already a member of the organization."
|
||||
title: t('userErrorExists'),
|
||||
description: t('userErrorExistsDescription')
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to invite user",
|
||||
title: t('inviteError'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while inviting the user"
|
||||
t('inviteErrorDescription')
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -238,8 +240,8 @@ export default function Page() {
|
||||
setInviteLink(res.data.data.inviteLink);
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User invited",
|
||||
description: "The user has been successfully invited."
|
||||
title: t('userInvited'),
|
||||
description: t('userInvitedDescription')
|
||||
});
|
||||
|
||||
setExpiresInDays(parseInt(values.validForHours) / 24);
|
||||
@@ -265,10 +267,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create user",
|
||||
title: t('userErrorCreate'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the user"
|
||||
t('userErrorCreateDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -276,8 +278,8 @@ export default function Page() {
|
||||
if (res && res.status === 201) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User created",
|
||||
description: "The user has been successfully created."
|
||||
title: t('userCreated'),
|
||||
description: t('userCreatedDescription')
|
||||
});
|
||||
router.push(`/${orgId}/settings/access/users`);
|
||||
}
|
||||
@@ -288,13 +290,13 @@ export default function Page() {
|
||||
const userTypes: ReadonlyArray<UserTypeOption> = [
|
||||
{
|
||||
id: "internal",
|
||||
title: "Internal User",
|
||||
description: "Invite a user to join your organization directly."
|
||||
title: t('userTypeInternal'),
|
||||
description: t('userTypeInternalDescription')
|
||||
},
|
||||
{
|
||||
id: "oidc",
|
||||
title: "External User",
|
||||
description: "Create a user with an external identity provider."
|
||||
title: t('userTypeExternal'),
|
||||
description: t('userTypeExternalDescription')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -302,8 +304,8 @@ export default function Page() {
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title="Create User"
|
||||
description="Follow the steps below to create a new user"
|
||||
title={t('accessUserCreate')}
|
||||
description={t('accessUserCreateDescription')}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -311,7 +313,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/access/users`);
|
||||
}}
|
||||
>
|
||||
See All Users
|
||||
{t('userSeeAll')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -320,10 +322,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
User Type
|
||||
{t('userTypeTitle')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Determine how you want to create the user
|
||||
{t('userTypeDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -349,10 +351,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
User Information
|
||||
{t('userInfo')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Enter the details for the new user
|
||||
{t('userSettingsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -373,7 +375,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email
|
||||
{t('email')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -402,8 +404,7 @@ export default function Page() {
|
||||
htmlFor="send-email"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Send invite email to
|
||||
user
|
||||
{t('inviteEmailSent')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
@@ -416,7 +417,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Valid For
|
||||
{t('inviteValid')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -428,7 +429,7 @@ export default function Page() {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
<SelectValue placeholder={t('selectDuration')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -463,7 +464,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Role
|
||||
{t('role')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -472,7 +473,7 @@ export default function Page() {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -503,37 +504,16 @@ export default function Page() {
|
||||
<div className="max-w-md space-y-4">
|
||||
{sendEmail && (
|
||||
<p>
|
||||
An email has
|
||||
been sent to the
|
||||
user with the
|
||||
access link
|
||||
below. They must
|
||||
access the link
|
||||
to accept the
|
||||
invitation.
|
||||
{t('inviteEmailSentDescription')}
|
||||
</p>
|
||||
)}
|
||||
{!sendEmail && (
|
||||
<p>
|
||||
The user has
|
||||
been invited.
|
||||
They must access
|
||||
the link below
|
||||
to accept the
|
||||
invitation.
|
||||
{t('inviteSentDescription')}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
The invite will
|
||||
expire in{" "}
|
||||
<b>
|
||||
{expiresInDays}{" "}
|
||||
{expiresInDays ===
|
||||
1
|
||||
? "day"
|
||||
: "days"}
|
||||
</b>
|
||||
.
|
||||
{t('inviteExpiresIn', {days: expiresInDays})}
|
||||
</p>
|
||||
<CopyTextBox
|
||||
text={inviteLink}
|
||||
@@ -554,20 +534,16 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Identity Provider
|
||||
{t('idpTitle')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Select the identity provider for the
|
||||
external user
|
||||
{t('idpSelect')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
{idps.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
No identity providers are
|
||||
configured. Please configure an
|
||||
identity provider before creating
|
||||
external users.
|
||||
{t('idpNotConfigured')}
|
||||
</p>
|
||||
) : (
|
||||
<Form {...externalForm}>
|
||||
@@ -621,10 +597,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
User Information
|
||||
{t('userSettings')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Enter the details for the new user
|
||||
{t('userSettingsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -645,7 +621,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Username
|
||||
{t('username')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -653,15 +629,7 @@ export default function Page() {
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
This must
|
||||
match the
|
||||
unique
|
||||
username
|
||||
that exists
|
||||
in the
|
||||
selected
|
||||
identity
|
||||
provider.
|
||||
{t('usernameUniq')}
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -676,8 +644,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email
|
||||
(Optional)
|
||||
{t('emailOptional')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -697,8 +664,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
(Optional)
|
||||
{t('nameOptional')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -718,7 +684,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Role
|
||||
{t('role')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -727,7 +693,7 @@ export default function Page() {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -771,7 +737,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/access/users`);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
{userType && dataLoaded && (
|
||||
<Button
|
||||
@@ -783,7 +749,7 @@ export default function Page() {
|
||||
(userType === "internal" && inviteLink !== null)
|
||||
}
|
||||
>
|
||||
Create User
|
||||
{t('accessUserCreate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type UsersPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -24,6 +25,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
const getUser = cache(verifySession);
|
||||
const user = await getUser();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
let users: ListUsersResponse["users"] = [];
|
||||
let hasInvitations = false;
|
||||
|
||||
@@ -77,15 +80,13 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
email: user.email,
|
||||
type: user.type,
|
||||
idpId: user.idpId,
|
||||
idpName: user.idpName || "Internal",
|
||||
status: "Confirmed",
|
||||
role: user.isOwner ? "Owner" : user.roleName || "Member",
|
||||
idpName: user.idpName || t('idpNameInternal'),
|
||||
status: t('userConfirmed'),
|
||||
role: user.isOwner ? t('accessRoleOwner') : user.roleName || t('accessRoleMember'),
|
||||
isOwner: user.isOwner || false
|
||||
};
|
||||
});
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function OrgApiKeysTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Key",
|
||||
header: t('key'),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
@@ -129,7 +129,7 @@ export default function OrgApiKeysTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
header: t('createdAt'),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysPermissionsErrorLoadingActions'),
|
||||
title: "Error loading API key actions",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('apiKeysPermissionsErrorLoadingActions')
|
||||
"Error loading API key actions"
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -79,18 +79,18 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysPermissionsErrorUpdate'), e);
|
||||
console.error("Error setting permissions", e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysPermissionsErrorUpdate'),
|
||||
title: "Error setting permissions",
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (actionsRes && actionsRes.status === 200) {
|
||||
toast({
|
||||
title: t('apiKeysPermissionsUpdated'),
|
||||
description: t('apiKeysPermissionsUpdatedDescription')
|
||||
title: "Permissions updated",
|
||||
description: "The permissions have been updated."
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,16 +58,14 @@ import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const createFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: t('apiKeysNameMin')
|
||||
message: "Name must be at least 2 characters."
|
||||
})
|
||||
.max(255, {
|
||||
message: t('apiKeysNameMax')
|
||||
message: "Name must not be longer than 255 characters."
|
||||
})
|
||||
});
|
||||
|
||||
@@ -82,7 +80,7 @@ const copiedFormSchema = z
|
||||
return data.copied;
|
||||
},
|
||||
{
|
||||
message: t('apiKeysConfirmCopy2'),
|
||||
message: "You must confirm that you have copied the API key.",
|
||||
path: ["copied"]
|
||||
}
|
||||
);
|
||||
@@ -116,6 +114,8 @@ export default function Page() {
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
async function onSubmit(data: CreateFormValues) {
|
||||
setCreateLoading(true);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import UserProvider from "@app/providers/UserProvider";
|
||||
import { Layout } from "@app/components/Layout";
|
||||
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
|
||||
import { orgNavItems } from "@app/app/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -46,6 +47,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const cookie = await authCookieHeader();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
try {
|
||||
const getOrgUser = cache(() =>
|
||||
internal.get<AxiosResponse<GetOrgUserResponse>>(
|
||||
@@ -56,7 +59,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const orgUser = await getOrgUser();
|
||||
|
||||
if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) {
|
||||
throw new Error("User is not an admin or owner");
|
||||
throw new Error(t('userErrorNotAdminOrOwner'));
|
||||
}
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ResourcesDataTable<TData, TValue>({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Resources"
|
||||
title={t('resources')}
|
||||
searchPlaceholder={t('resourcesSearch')}
|
||||
searchColumn="name"
|
||||
onAdd={createResource}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Server, Lock, Key, Users, X, ArrowRight } from "lucide-react"; // Replace with actual imports
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const ResourcesSplashCard = () => {
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
@@ -22,6 +23,8 @@ export const ResourcesSplashCard = () => {
|
||||
localStorage.setItem(key, "true");
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
if (isDismissed) {
|
||||
return null;
|
||||
}
|
||||
@@ -31,7 +34,7 @@ export const ResourcesSplashCard = () => {
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="absolute top-2 right-2 p-2"
|
||||
aria-label="Dismiss"
|
||||
aria-label={t('dismiss')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -39,24 +42,23 @@ export const ResourcesSplashCard = () => {
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Server className="text-blue-500" />
|
||||
Resources
|
||||
{t('resources')}
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network.
|
||||
Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.
|
||||
{t('resourcesDescription')}
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Lock className="text-green-500 w-4 h-4" />
|
||||
Secure connectivity with WireGuard encryption
|
||||
{t('resourcesWireGuardConnect')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Key className="text-yellow-500 w-4 h-4" />
|
||||
Configure multiple authentication methods
|
||||
{t('resourcesMultipleAuthenticationMethods')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Users className="text-purple-500 w-4 h-4" />
|
||||
User and role-based access control
|
||||
{t('resourcesUsersRolesAccess')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -183,7 +183,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "protocol",
|
||||
header: "Protocol",
|
||||
header: t('protocol'),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
||||
@@ -191,7 +191,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "domain",
|
||||
header: "Access",
|
||||
header: t('access'),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
@@ -249,7 +249,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: "Enabled",
|
||||
header: t('enabled'),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DomainOption {
|
||||
baseDomain: string;
|
||||
@@ -23,10 +24,12 @@ interface CustomDomainInputProps {
|
||||
onChange?: (value: string, selectedDomainId: string) => void;
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
export default function CustomDomainInput({
|
||||
domainOptions,
|
||||
selectedDomainId,
|
||||
placeholder = "Subdomain",
|
||||
placeholder = t('subdomain'),
|
||||
value: defaultValue,
|
||||
onChange
|
||||
}: CustomDomainInputProps) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schemas";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const setPasswordFormSchema = z.object({
|
||||
password: z.string().min(4).max(100)
|
||||
@@ -64,6 +65,8 @@ export default function SetResourcePasswordForm({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
@@ -81,18 +84,17 @@ export default function SetResourcePasswordForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error setting resource password",
|
||||
title: t('resourceErrorPasswordSetup'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while setting the resource password"
|
||||
t('resourceErrorPasswordSetupDescription')
|
||||
)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Resource password set",
|
||||
description:
|
||||
"The resource password has been set successfully"
|
||||
title: t('resourcePasswordSetup'),
|
||||
description: t('resourcePasswordSetupDescription')
|
||||
});
|
||||
|
||||
if (onSetPassword) {
|
||||
@@ -114,9 +116,9 @@ export default function SetResourcePasswordForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Set Password</CredenzaTitle>
|
||||
<CredenzaTitle>{t('resourcePasswordSetupTitle')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Set a password to protect this resource
|
||||
{t('resourcePasswordSetupTitleDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -131,7 +133,7 @@ export default function SetResourcePasswordForm({
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
@@ -148,7 +150,7 @@ export default function SetResourcePasswordForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -156,7 +158,7 @@ export default function SetResourcePasswordForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Enable Password Protection
|
||||
{t('resourcePasswordSubmit')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@app/components/ui/input-otp";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const setPincodeFormSchema = z.object({
|
||||
pincode: z.string().length(6)
|
||||
@@ -69,6 +70,8 @@ export default function SetResourcePincodeForm({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
@@ -86,18 +89,17 @@ export default function SetResourcePincodeForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error setting resource PIN code",
|
||||
title: t('resourceErrorPincodeSetup'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while setting the resource PIN code"
|
||||
t('resourceErrorPincodeSetupDescription')
|
||||
)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Resource PIN code set",
|
||||
description:
|
||||
"The resource pincode has been set successfully"
|
||||
title: t('resourcePincodeSetup'),
|
||||
description: t('resourcePincodeSetupDescription')
|
||||
});
|
||||
|
||||
if (onSetPincode) {
|
||||
@@ -119,9 +121,9 @@ export default function SetResourcePincodeForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Set Pincode</CredenzaTitle>
|
||||
<CredenzaTitle>{t('resourcePincodeSetupTitle')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Set a pincode to protect this resource
|
||||
{t('resourcePincodeSetupTitleDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -136,7 +138,7 @@ export default function SetResourcePincodeForm({
|
||||
name="pincode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>PIN Code</FormLabel>
|
||||
<FormLabel>{t('resourcePincode')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
<InputOTP
|
||||
@@ -182,7 +184,7 @@ export default function SetResourcePincodeForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -190,7 +192,7 @@ export default function SetResourcePincodeForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Enable PIN Code Protection
|
||||
{t('resourcePincodeSubmit')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useRouter } from "next/navigation";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const UsersRolesFormSchema = z.object({
|
||||
roles: z.array(
|
||||
@@ -129,6 +130,8 @@ export default function ResourceAuthenticationPage() {
|
||||
defaultValues: { emails: [] }
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@@ -203,10 +206,10 @@ export default function ResourceAuthenticationPage() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch data",
|
||||
title: t('resourceErrorAuthFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the data"
|
||||
t('resourceErrorAuthFetchDescription')
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -233,18 +236,18 @@ export default function ResourceAuthenticationPage() {
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Saved successfully",
|
||||
description: "Whitelist settings have been saved"
|
||||
title: t('resourceWhitelistSave'),
|
||||
description: t('resourceWhitelistSaveDescription')
|
||||
});
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to save whitelist",
|
||||
title: t('resourceErrorWhitelistSave'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while saving the whitelist"
|
||||
t('resourceErrorWhitelistSaveDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -281,18 +284,18 @@ export default function ResourceAuthenticationPage() {
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Saved successfully",
|
||||
description: "Authentication settings have been saved"
|
||||
title: t('resourceAuthSettingsSave'),
|
||||
description: t('resourceAuthSettingsSaveDescription')
|
||||
});
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to set roles",
|
||||
title: t('resourceErrorUsersRolesSave'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while setting the roles"
|
||||
t('resourceErrorUsersRolesSaveDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -308,9 +311,8 @@ export default function ResourceAuthenticationPage() {
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Resource password removed",
|
||||
description:
|
||||
"The resource password has been removed successfully"
|
||||
title: t('resourcePasswordRemove'),
|
||||
description: t('resourcePasswordRemoveDescription')
|
||||
});
|
||||
|
||||
updateAuthInfo({
|
||||
@@ -321,10 +323,10 @@ export default function ResourceAuthenticationPage() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error removing resource password",
|
||||
title: t('resourceErrorPasswordRemove'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the resource password"
|
||||
t('resourceErrorPasswordRemoveDescription')
|
||||
)
|
||||
});
|
||||
})
|
||||
@@ -339,9 +341,8 @@ export default function ResourceAuthenticationPage() {
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Resource pincode removed",
|
||||
description:
|
||||
"The resource password has been removed successfully"
|
||||
title: t('resourcePincodeRemove'),
|
||||
description: t('resourcePincodeRemoveDescription')
|
||||
});
|
||||
|
||||
updateAuthInfo({
|
||||
@@ -352,10 +353,10 @@ export default function ResourceAuthenticationPage() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error removing resource pincode",
|
||||
title: t('resourceErrorPincodeRemove'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the resource pincode"
|
||||
t('resourceErrorPincodeRemoveDescription')
|
||||
)
|
||||
});
|
||||
})
|
||||
@@ -400,18 +401,17 @@ export default function ResourceAuthenticationPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Users & Roles
|
||||
{t('resourceUsersRoles')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure which users and roles can visit this
|
||||
resource
|
||||
{t('resourceUsersRolesDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SwitchInput
|
||||
id="sso-toggle"
|
||||
label="Use Platform SSO"
|
||||
description="Existing users will only have to log in once for all resources that have this enabled."
|
||||
label={t('ssoUse')}
|
||||
description={t('ssoUseDescription')}
|
||||
defaultChecked={resource.sso}
|
||||
onCheckedChange={(val) => setSsoEnabled(val)}
|
||||
/>
|
||||
@@ -431,7 +431,7 @@ export default function ResourceAuthenticationPage() {
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormLabel>{t('roles')}</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
@@ -441,7 +441,7 @@ export default function ResourceAuthenticationPage() {
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder="Select a role"
|
||||
placeholder={t('accessRoleSelect2')}
|
||||
size="sm"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
@@ -475,8 +475,7 @@ export default function ResourceAuthenticationPage() {
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
Admins can always access
|
||||
this resource.
|
||||
{t('resourceRoleDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -486,7 +485,7 @@ export default function ResourceAuthenticationPage() {
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Users</FormLabel>
|
||||
<FormLabel>{t('users')}</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
@@ -496,7 +495,7 @@ export default function ResourceAuthenticationPage() {
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder="Select a user"
|
||||
placeholder={t('accessUserSelect')}
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.users
|
||||
@@ -544,7 +543,7 @@ export default function ResourceAuthenticationPage() {
|
||||
disabled={loadingSaveUsersRoles}
|
||||
form="users-roles-form"
|
||||
>
|
||||
Save Users & Roles
|
||||
{t('resourceUsersRolesSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
@@ -552,11 +551,10 @@ export default function ResourceAuthenticationPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Authentication Methods
|
||||
{t('resourceAuthMethods')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Allow access to the resource via additional auth
|
||||
methods
|
||||
{t('resourceAuthMethodsDescriptions')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -568,7 +566,7 @@ export default function ResourceAuthenticationPage() {
|
||||
<Key />
|
||||
<span>
|
||||
Password Protection{" "}
|
||||
{authInfo.password ? "Enabled" : "Disabled"}
|
||||
{authInfo.password ? t('enabled') : t('disabled')}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -581,8 +579,8 @@ export default function ResourceAuthenticationPage() {
|
||||
loading={loadingRemoveResourcePassword}
|
||||
>
|
||||
{authInfo.password
|
||||
? "Remove Password"
|
||||
: "Add Password"}
|
||||
? t('passwordRemove')
|
||||
: t('passwordAdd')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -593,8 +591,7 @@ export default function ResourceAuthenticationPage() {
|
||||
>
|
||||
<Binary />
|
||||
<span>
|
||||
PIN Code Protection{" "}
|
||||
{authInfo.pincode ? "Enabled" : "Disabled"}
|
||||
{t('resourcePincodeProtection', {status: authInfo.pincode ? t('enabled') : t('disabled')})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -607,8 +604,8 @@ export default function ResourceAuthenticationPage() {
|
||||
loading={loadingRemoveResourcePincode}
|
||||
>
|
||||
{authInfo.pincode
|
||||
? "Remove PIN Code"
|
||||
: "Add PIN Code"}
|
||||
? t('pincodeRemove')
|
||||
: t('pincodeAdd')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
@@ -617,11 +614,10 @@ export default function ResourceAuthenticationPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
One-time Passwords
|
||||
{t('otpEmailTitle')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Require email-based authentication for resource
|
||||
access
|
||||
{t('otpEmailTitleDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -629,16 +625,16 @@ export default function ResourceAuthenticationPage() {
|
||||
<Alert variant="neutral" className="mb-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
SMTP Required
|
||||
{t('otpEmailSmtpRequired')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
SMTP must be enabled on the server to use one-time password authentication.
|
||||
{t('otpEmailSmtpRequiredDescription')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<SwitchInput
|
||||
id="whitelist-toggle"
|
||||
label="Email Whitelist"
|
||||
label={t('otpEmailWhitelist')}
|
||||
defaultChecked={resource.emailWhitelistEnabled}
|
||||
onCheckedChange={setWhitelistEnabled}
|
||||
disabled={!env.email.emailEnabled}
|
||||
@@ -654,8 +650,8 @@ export default function ResourceAuthenticationPage() {
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<InfoPopup
|
||||
text="Whitelisted Emails"
|
||||
info="Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain."
|
||||
text={t('otpEmailWhitelistList')}
|
||||
info={t('otpEmailWhitelistListDescription')}
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -678,8 +674,7 @@ export default function ResourceAuthenticationPage() {
|
||||
.regex(
|
||||
/^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
|
||||
{
|
||||
message:
|
||||
"Invalid email address. Wildcard (*) must be the entire local part."
|
||||
message: t('otpEmailErrorInvalid')
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -690,7 +685,7 @@ export default function ResourceAuthenticationPage() {
|
||||
setActiveTagIndex={
|
||||
setActiveEmailTagIndex
|
||||
}
|
||||
placeholder="Enter an email"
|
||||
placeholder={t('otpEmailEnter')}
|
||||
tags={
|
||||
whitelistForm.getValues()
|
||||
.emails
|
||||
@@ -713,9 +708,7 @@ export default function ResourceAuthenticationPage() {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Press enter to add an
|
||||
email after typing it in
|
||||
the input field.
|
||||
{t('otpEmailEnterDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -731,7 +724,7 @@ export default function ResourceAuthenticationPage() {
|
||||
loading={loadingSaveWhitelist}
|
||||
disabled={loadingSaveWhitelist}
|
||||
>
|
||||
Save Whitelist
|
||||
{t('otpEmailWhitelistSave')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -65,6 +65,9 @@ import {
|
||||
updateResourceRule
|
||||
} from "@server/routers/resource";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const GeneralFormSchema = z
|
||||
.object({
|
||||
@@ -88,7 +91,7 @@ const GeneralFormSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Invalid port number",
|
||||
message: t('proxyErrorInvalidPort'),
|
||||
path: ["proxyPort"]
|
||||
}
|
||||
)
|
||||
@@ -100,7 +103,7 @@ const GeneralFormSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Invalid subdomain",
|
||||
message: t('subdomainErrorInvalid'),
|
||||
path: ["subdomain"]
|
||||
}
|
||||
);
|
||||
@@ -174,10 +177,10 @@ export default function GeneralForm() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error fetching domains",
|
||||
title: t('domainErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred when fetching the domains"
|
||||
t('domainErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -216,18 +219,18 @@ export default function GeneralForm() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update resource",
|
||||
title: t('resourceErrorUpdate'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while updating the resource"
|
||||
t('resourceErrorUpdateDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
title: "Resource updated",
|
||||
description: "The resource has been updated successfully"
|
||||
title: t('resourceUpdated'),
|
||||
description: t('resourceUpdatedDescription')
|
||||
});
|
||||
|
||||
const resource = res.data.data;
|
||||
@@ -255,18 +258,18 @@ export default function GeneralForm() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to transfer resource",
|
||||
title: t('resourceErrorTransfer'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while transferring the resource"
|
||||
t('resourceErrorTransferDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
title: "Resource transferred",
|
||||
description: "The resource has been transferred successfully"
|
||||
title: t('resourceTransferred'),
|
||||
description: t('resourceTransferredDescription')
|
||||
});
|
||||
router.refresh();
|
||||
|
||||
@@ -290,10 +293,10 @@ export default function GeneralForm() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to toggle resource",
|
||||
title: t('resourceErrorToggle'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while updating the resource"
|
||||
t('resourceErrorToggleDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -308,15 +311,15 @@ export default function GeneralForm() {
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>Visibility</SettingsSectionTitle>
|
||||
<SettingsSectionTitle>{t('resourceVisibilityTitle')}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Completely enable or disable resource visibility
|
||||
{t('resourceVisibilityTitleDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SwitchInput
|
||||
id="enable-resource"
|
||||
label="Enable Resource"
|
||||
label={t('resourceEnable')}
|
||||
defaultChecked={resource.enabled}
|
||||
onCheckedChange={async (val) => {
|
||||
await toggleResourceEnabled(val);
|
||||
@@ -328,10 +331,10 @@ export default function GeneralForm() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
General Settings
|
||||
{t('resourceGeneral')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure the general settings for this resource
|
||||
{t('resourceGeneralDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
@@ -348,7 +351,7 @@ export default function GeneralForm() {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t('name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -367,7 +370,7 @@ export default function GeneralForm() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Domain Type
|
||||
{t('domainType')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={
|
||||
@@ -398,11 +401,10 @@ export default function GeneralForm() {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="subdomain">
|
||||
Subdomain
|
||||
{t('subdomain')}
|
||||
</SelectItem>
|
||||
<SelectItem value="basedomain">
|
||||
Base
|
||||
Domain
|
||||
{t('baseDomain')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -416,7 +418,7 @@ export default function GeneralForm() {
|
||||
{domainType === "subdomain" ? (
|
||||
<div className="w-fill space-y-2">
|
||||
<FormLabel>
|
||||
Subdomain
|
||||
{t('subdomain')}
|
||||
</FormLabel>
|
||||
<div className="flex">
|
||||
<div className="w-1/2">
|
||||
@@ -502,7 +504,7 @@ export default function GeneralForm() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Base Domain
|
||||
{t('baseDomain')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -556,7 +558,7 @@ export default function GeneralForm() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Port Number
|
||||
{t('resourcePortNumber')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -596,7 +598,7 @@ export default function GeneralForm() {
|
||||
disabled={saveLoading}
|
||||
form="general-settings-form"
|
||||
>
|
||||
Save General Settings
|
||||
{t('saveGeneralSettings')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
@@ -604,10 +606,10 @@ export default function GeneralForm() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Transfer Resource
|
||||
{t('resourceTransfer')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Transfer this resource to a different site
|
||||
{t('resourceTransferDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
@@ -627,7 +629,7 @@ export default function GeneralForm() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Destination Site
|
||||
{t('siteDestination')}
|
||||
</FormLabel>
|
||||
<Popover
|
||||
open={open}
|
||||
@@ -652,16 +654,16 @@ export default function GeneralForm() {
|
||||
site.siteId ===
|
||||
field.value
|
||||
)?.name
|
||||
: "Select site"}
|
||||
: t('siteSelect')}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search sites" />
|
||||
<CommandInput placeholder={t('searchSites')} />
|
||||
<CommandEmpty>
|
||||
No sites found.
|
||||
{t('sitesNotFound')}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sites.map(
|
||||
@@ -716,7 +718,7 @@ export default function GeneralForm() {
|
||||
disabled={transferLoading}
|
||||
form="transfer-form"
|
||||
>
|
||||
Transfer Resource
|
||||
{t('resourceTransferSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const addTargetSchema = z.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
@@ -92,6 +93,8 @@ type LocalTarget = Omit<
|
||||
"protocol"
|
||||
>;
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const proxySettingsSchema = z.object({
|
||||
setHostHeader: z
|
||||
.string()
|
||||
@@ -104,8 +107,7 @@ const proxySettingsSchema = z.object({
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header."
|
||||
message: t('proxyErrorInvalidHeader')
|
||||
}
|
||||
)
|
||||
});
|
||||
@@ -123,8 +125,7 @@ const tlsSettingsSchema = z.object({
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name."
|
||||
message: t('proxyErrorTls')
|
||||
}
|
||||
)
|
||||
});
|
||||
@@ -199,10 +200,10 @@ export default function ReverseProxyTargets(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch targets",
|
||||
title: t('targetErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while fetching targets"
|
||||
t('targetErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -224,10 +225,10 @@ export default function ReverseProxyTargets(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch resource",
|
||||
title: t('siteErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while fetching resource"
|
||||
t('siteErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -247,8 +248,8 @@ export default function ReverseProxyTargets(props: {
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Duplicate target",
|
||||
description: "A target with these settings already exists"
|
||||
title: t('targetErrorDuplicate'),
|
||||
description: t('targetErrorDuplicateDescription')
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -260,8 +261,8 @@ export default function ReverseProxyTargets(props: {
|
||||
if (!isIPInSubnet(targetIp, subnet)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid target IP",
|
||||
description: "Target IP must be within the site subnet"
|
||||
title: t('targetWireGuardErrorInvalidIp'),
|
||||
description: t('targetWireGuardErrorInvalidIpDescription')
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -339,8 +340,8 @@ export default function ReverseProxyTargets(props: {
|
||||
updateResource({ stickySession: stickySessionData.stickySession });
|
||||
|
||||
toast({
|
||||
title: "Targets updated",
|
||||
description: "Targets and settings updated successfully"
|
||||
title: t('targetsUpdated'),
|
||||
description: t('targetsUpdatedDescription')
|
||||
});
|
||||
|
||||
setTargetsToRemove([]);
|
||||
@@ -349,10 +350,10 @@ export default function ReverseProxyTargets(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update targets",
|
||||
title: t('targetsErrorUpdate'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while updating targets"
|
||||
t('targetsErrorUpdateDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -373,17 +374,17 @@ export default function ReverseProxyTargets(props: {
|
||||
tlsServerName: data.tlsServerName || null
|
||||
});
|
||||
toast({
|
||||
title: "TLS settings updated",
|
||||
description: "Your TLS settings have been updated successfully"
|
||||
title: t('targetTlsUpdate'),
|
||||
description: t('targetTlsUpdateDescription')
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update TLS settings",
|
||||
title: t('targetErrorTlsUpdate'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while updating TLS settings"
|
||||
t('targetErrorTlsUpdateDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -402,18 +403,17 @@ export default function ReverseProxyTargets(props: {
|
||||
setHostHeader: data.setHostHeader || null
|
||||
});
|
||||
toast({
|
||||
title: "Proxy settings updated",
|
||||
description:
|
||||
"Your proxy settings have been updated successfully"
|
||||
title: t('proxyUpdated'),
|
||||
description: t('proxyUpdatedDescription')
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update proxy settings",
|
||||
title: t('proxyErrorUpdate'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while updating proxy settings"
|
||||
t('proxyErrorUpdateDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -424,7 +424,7 @@ export default function ReverseProxyTargets(props: {
|
||||
const columns: ColumnDef<LocalTarget>[] = [
|
||||
{
|
||||
accessorKey: "ip",
|
||||
header: "IP / Hostname",
|
||||
header: t('targetAddr'),
|
||||
cell: ({ row }) => (
|
||||
<Input
|
||||
defaultValue={row.original.ip}
|
||||
@@ -439,7 +439,7 @@ export default function ReverseProxyTargets(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "port",
|
||||
header: "Port",
|
||||
header: t('targetPort'),
|
||||
cell: ({ row }) => (
|
||||
<Input
|
||||
type="number"
|
||||
@@ -455,7 +455,7 @@ export default function ReverseProxyTargets(props: {
|
||||
},
|
||||
// {
|
||||
// accessorKey: "protocol",
|
||||
// header: "Protocol",
|
||||
// header: t('targetProtocol'),
|
||||
// cell: ({ row }) => (
|
||||
// <Select
|
||||
// defaultValue={row.original.protocol!}
|
||||
@@ -473,7 +473,7 @@ export default function ReverseProxyTargets(props: {
|
||||
// },
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: "Enabled",
|
||||
header: t('enabled'),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
@@ -500,7 +500,7 @@ export default function ReverseProxyTargets(props: {
|
||||
variant="outline"
|
||||
onClick={() => removeTarget(row.original.targetId)}
|
||||
>
|
||||
Delete
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -511,7 +511,7 @@ export default function ReverseProxyTargets(props: {
|
||||
if (resource.http) {
|
||||
const methodCol: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "method",
|
||||
header: "Method",
|
||||
header: t('method'),
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.method ?? ""}
|
||||
@@ -560,10 +560,10 @@ export default function ReverseProxyTargets(props: {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
HTTPS & TLS Settings
|
||||
{t('targetTlsSettings')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure TLS settings for your resource
|
||||
{t('targetTlsSettingsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -611,12 +611,12 @@ export default function ReverseProxyTargets(props: {
|
||||
className="p-0 flex items-center justify-start gap-2 w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
Advanced TLS Settings
|
||||
{t('targetTlsSettingsAdvanced')}
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
Toggle
|
||||
{t('toggle')}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -631,17 +631,13 @@ export default function ReverseProxyTargets(props: {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
TLS Server Name
|
||||
(SNI)
|
||||
{t('targetTlsSni')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The TLS Server Name
|
||||
to use for SNI.
|
||||
Leave empty to use
|
||||
the default.
|
||||
{t('targetTlsSniDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -659,7 +655,7 @@ export default function ReverseProxyTargets(props: {
|
||||
loading={httpsTlsLoading}
|
||||
form="tls-settings-form"
|
||||
>
|
||||
Save Settings
|
||||
{t('targetTlsSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
@@ -668,10 +664,10 @@ export default function ReverseProxyTargets(props: {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Targets Configuration
|
||||
{t('targets')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Set up targets to route traffic to your services
|
||||
{t('targetsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -693,8 +689,8 @@ export default function ReverseProxyTargets(props: {
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="sticky-toggle"
|
||||
label="Enable Sticky Sessions"
|
||||
description="Keep connections on the same backend target for their entire session."
|
||||
label={t('targetStickySessions')}
|
||||
description={t('targetStickySessionsDescription')}
|
||||
defaultChecked={
|
||||
field.value
|
||||
}
|
||||
@@ -725,7 +721,7 @@ export default function ReverseProxyTargets(props: {
|
||||
name="method"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Method</FormLabel>
|
||||
<FormLabel>{t('method')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
@@ -742,7 +738,7 @@ export default function ReverseProxyTargets(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="method">
|
||||
<SelectValue placeholder="Select method" />
|
||||
<SelectValue placeholder={t('methodSelect')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">
|
||||
@@ -768,7 +764,7 @@ export default function ReverseProxyTargets(props: {
|
||||
name="ip"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IP / Hostname</FormLabel>
|
||||
<FormLabel>{t('targetAddr')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input id="ip" {...field} />
|
||||
</FormControl>
|
||||
@@ -781,7 +777,7 @@ export default function ReverseProxyTargets(props: {
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormLabel>{t('targetPort')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="port"
|
||||
@@ -805,7 +801,7 @@ export default function ReverseProxyTargets(props: {
|
||||
)
|
||||
}
|
||||
>
|
||||
Add Target
|
||||
{t('targetSubmit')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -849,14 +845,13 @@ export default function ReverseProxyTargets(props: {
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No targets. Add a target using the form.
|
||||
{t('targetNoOne')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<TableCaption>
|
||||
Adding more than one target above will enable load
|
||||
balancing.
|
||||
{t('targetNoOneDescription')}
|
||||
</TableCaption>
|
||||
</Table>
|
||||
</SettingsSectionBody>
|
||||
@@ -867,7 +862,7 @@ export default function ReverseProxyTargets(props: {
|
||||
disabled={targetsLoading}
|
||||
form="targets-settings-form"
|
||||
>
|
||||
Save Targets
|
||||
{t('targetsSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
@@ -876,10 +871,10 @@ export default function ReverseProxyTargets(props: {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Additional Proxy Settings
|
||||
{t('proxyAdditional')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure how your resource handles proxy settings
|
||||
{t('proxyAdditionalDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -898,15 +893,13 @@ export default function ReverseProxyTargets(props: {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Custom Host Header
|
||||
{t('proxyCustomHeader')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The host header to set when
|
||||
proxying requests. Leave
|
||||
empty to use the default.
|
||||
{t('proxyCustomHeaderDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -922,7 +915,7 @@ export default function ReverseProxyTargets(props: {
|
||||
loading={proxySettingsLoading}
|
||||
form="proxy-settings-form"
|
||||
>
|
||||
Save Proxy Settings
|
||||
{t('proxyAdditionalSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
@@ -937,7 +930,7 @@ function isIPInSubnet(subnet: string, ip: string): boolean {
|
||||
const mask = parseInt(maskBits);
|
||||
|
||||
if (mask < 0 || mask > 32) {
|
||||
throw new Error("Invalid subnet mask. Must be between 0 and 32.");
|
||||
throw new Error(t('subnetMaskErrorInvalid'));
|
||||
}
|
||||
|
||||
// Convert IP addresses to binary numbers
|
||||
@@ -955,14 +948,14 @@ function ipToNumber(ip: string): number {
|
||||
// Validate IP address format
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) {
|
||||
throw new Error("Invalid IP address format");
|
||||
throw new Error(t('ipAddressErrorInvalidFormat'));
|
||||
}
|
||||
|
||||
// Convert IP octets to 32-bit number
|
||||
return parts.reduce((num, octet) => {
|
||||
const oct = parseInt(octet);
|
||||
if (isNaN(oct) || oct < 0 || oct > 255) {
|
||||
throw new Error("Invalid IP address octet");
|
||||
throw new Error(t('ipAddressErrorInvalidOctet'));
|
||||
}
|
||||
return (num << 8) + oct;
|
||||
}, 0);
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
} from "@server/lib/validators";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// Schema for rule validation
|
||||
const addRuleSchema = z.object({
|
||||
@@ -87,16 +88,18 @@ type LocalRule = ArrayElement<ListResourceRulesResponse["rules"]> & {
|
||||
updated?: boolean;
|
||||
};
|
||||
|
||||
enum RuleAction {
|
||||
ACCEPT = "Always Allow",
|
||||
DROP = "Always Deny"
|
||||
}
|
||||
const t = useTranslations();
|
||||
|
||||
enum RuleMatch {
|
||||
PATH = "Path",
|
||||
IP = "IP",
|
||||
CIDR = "IP Range"
|
||||
}
|
||||
const RuleAction = {
|
||||
ACCEPT: t('alwaysAllow'),
|
||||
DROP: t('alwaysDeny')
|
||||
} as const;
|
||||
|
||||
const RuleMatch = {
|
||||
PATH: t('path'),
|
||||
IP: "IP",
|
||||
CIDR: t('ipAddressRange')
|
||||
} as const;
|
||||
|
||||
export default function ResourceRules(props: {
|
||||
params: Promise<{ resourceId: number }>;
|
||||
@@ -133,10 +136,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch rules",
|
||||
title: t('rulesErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while fetching rules"
|
||||
t('rulesErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -157,8 +160,8 @@ export default function ResourceRules(props: {
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Duplicate rule",
|
||||
description: "A rule with these settings already exists"
|
||||
title: t('rulesErrorDuplicate'),
|
||||
description: t('rulesErrorDuplicateDescription')
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -166,8 +169,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "CIDR" && !isValidCIDR(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid CIDR",
|
||||
description: "Please enter a valid CIDR value"
|
||||
title: t('rulesErrorInvalidIpAddressRange'),
|
||||
description: t('rulesErrorInvalidIpAddressRangeDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -175,8 +178,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid URL path",
|
||||
description: "Please enter a valid URL path value"
|
||||
title: t('rulesErrorInvalidUrl'),
|
||||
description: t('rulesErrorInvalidUrlDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -184,8 +187,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "IP" && !isValidIP(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid IP",
|
||||
description: "Please enter a valid IP address"
|
||||
title: t('rulesErrorInvalidIpAddress'),
|
||||
description: t('rulesErrorInvalidIpAddressDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -240,10 +243,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update rules",
|
||||
title: t('rulesErrorUpdate'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while updating rules"
|
||||
t('rulesErrorUpdateDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -253,8 +256,8 @@ export default function ResourceRules(props: {
|
||||
updateResource({ applyRules: val });
|
||||
|
||||
toast({
|
||||
title: "Enable Rules",
|
||||
description: "Rule evaluation has been updated"
|
||||
title: t('rulesUpdated'),
|
||||
description: t('rulesUpdatedDescription')
|
||||
});
|
||||
router.refresh();
|
||||
}
|
||||
@@ -263,11 +266,11 @@ export default function ResourceRules(props: {
|
||||
function getValueHelpText(type: string) {
|
||||
switch (type) {
|
||||
case "CIDR":
|
||||
return "Enter an address in CIDR format (e.g., 103.21.244.0/22)";
|
||||
return t('rulesMatchIpAddressRangeDescription');
|
||||
case "IP":
|
||||
return "Enter an IP address (e.g., 103.21.244.12)";
|
||||
return t('rulesMatchIpAddress');
|
||||
case "PATH":
|
||||
return "Enter a URL path or pattern (e.g., /api/v1/todos or /api/v1/*)";
|
||||
return t('rulesMatchUrl');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,8 +289,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid CIDR",
|
||||
description: "Please enter a valid CIDR value"
|
||||
title: t('rulesErrorInvalidIpAddressRange'),
|
||||
description: t('rulesErrorInvalidIpAddressRangeDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -298,8 +301,8 @@ export default function ResourceRules(props: {
|
||||
) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid URL path",
|
||||
description: "Please enter a valid URL path value"
|
||||
title: t('rulesErrorInvalidUrl'),
|
||||
description: t('rulesErrorInvalidUrlDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -307,8 +310,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid IP",
|
||||
description: "Please enter a valid IP address"
|
||||
title: t('rulesErrorInvalidIpAddress'),
|
||||
description: t('rulesErrorInvalidIpAddressDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -317,8 +320,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.priority === undefined) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid Priority",
|
||||
description: "Please enter a valid priority"
|
||||
title: t('rulesErrorInvalidPriority'),
|
||||
description: t('rulesErrorInvalidPriorityDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -329,8 +332,8 @@ export default function ResourceRules(props: {
|
||||
if (priorities.length !== new Set(priorities).size) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Duplicate Priorities",
|
||||
description: "Please enter unique priorities"
|
||||
title: t('rulesErrorDuplicatePriority'),
|
||||
description: t('rulesErrorDuplicatePriorityDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -369,8 +372,8 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Rules updated",
|
||||
description: "Rules updated successfully"
|
||||
title: t('ruleUpdated'),
|
||||
description: t('ruleUpdatedDescription')
|
||||
});
|
||||
|
||||
setRulesToRemove([]);
|
||||
@@ -379,10 +382,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Operation failed",
|
||||
title: t('ruleErrorUpdate'),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred during the save operation"
|
||||
t('ruleErrorUpdateDescription')
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -400,7 +403,7 @@ export default function ResourceRules(props: {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Priority
|
||||
{t('rulesPriority')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -420,8 +423,8 @@ export default function ResourceRules(props: {
|
||||
if (!parsed.data) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid IP",
|
||||
description: "Please enter a valid priority"
|
||||
title: t('rulesErrorInvalidIpAddress'), // correct priority or IP?
|
||||
description: t('rulesErrorInvalidPriorityDescription')
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -436,7 +439,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: "Action",
|
||||
header: t('rulesAction'),
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.action}
|
||||
@@ -458,7 +461,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "match",
|
||||
header: "Match Type",
|
||||
header: t('rulesMatchType'),
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.match}
|
||||
@@ -479,7 +482,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: "Value",
|
||||
header: t('value'),
|
||||
cell: ({ row }) => (
|
||||
<Input
|
||||
defaultValue={row.original.value}
|
||||
@@ -494,7 +497,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: "Enabled",
|
||||
header: t('enabled'),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
@@ -512,7 +515,7 @@ export default function ResourceRules(props: {
|
||||
variant="outline"
|
||||
onClick={() => removeRule(row.original.ruleId)}
|
||||
>
|
||||
Delete
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -542,46 +545,40 @@ export default function ResourceRules(props: {
|
||||
<SettingsContainer>
|
||||
<Alert className="hidden md:block">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">About Rules</AlertTitle>
|
||||
<AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle>
|
||||
<AlertDescription className="mt-4">
|
||||
<div className="space-y-1 mb-4">
|
||||
<p>
|
||||
Rules allow you to control access to your resource
|
||||
based on a set of criteria. You can create rules to
|
||||
allow or deny access based on IP address or URL
|
||||
path.
|
||||
{t('rulesAboutDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Actions</InfoSectionTitle>
|
||||
<InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="text-green-500 w-4 h-4" />
|
||||
Always Allow: Bypass all authentication
|
||||
methods
|
||||
{t('rulesActionAlwaysAllow')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<X className="text-red-500 w-4 h-4" />
|
||||
Always Deny: Block all requests; no
|
||||
authentication can be attempted
|
||||
{t('rulesActionAlwaysDeny')}
|
||||
</li>
|
||||
</ul>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Matching Criteria
|
||||
{t('rulesMatchCriteria')}
|
||||
</InfoSectionTitle>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li className="flex items-center gap-2">
|
||||
Match a specific IP address
|
||||
{t('rulesMatchCriteriaIpAddress')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
Match a range of IP addresses in CIDR
|
||||
notation
|
||||
{t('rulesMatchCriteriaIpAddressRange')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
Match a URL path or pattern
|
||||
{t('rulesMatchCriteriaUrl')}
|
||||
</li>
|
||||
</ul>
|
||||
</InfoSection>
|
||||
@@ -591,15 +588,15 @@ export default function ResourceRules(props: {
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>Enable Rules</SettingsSectionTitle>
|
||||
<SettingsSectionTitle>{t('rulesEnable')}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Enable or disable rule evaluation for this resource
|
||||
{t('rulesEnableDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SwitchInput
|
||||
id="rules-toggle"
|
||||
label="Enable Rules"
|
||||
label={t('rulesEnable')}
|
||||
defaultChecked={rulesEnabled}
|
||||
onCheckedChange={async (val) => {
|
||||
await saveApplyRules(val);
|
||||
@@ -611,10 +608,10 @@ export default function ResourceRules(props: {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Resource Rules Configuration
|
||||
{t('rulesResource')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure rules to control access to your resource
|
||||
{t('rulesResourceDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -629,7 +626,7 @@ export default function ResourceRules(props: {
|
||||
name="action"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Action</FormLabel>
|
||||
<FormLabel>{t('rulesAction')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
@@ -659,7 +656,7 @@ export default function ResourceRules(props: {
|
||||
name="match"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Match Type</FormLabel>
|
||||
<FormLabel>{t('rulesMatchType')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
@@ -695,7 +692,7 @@ export default function ResourceRules(props: {
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-0 mb-2">
|
||||
<InfoPopup
|
||||
text="Value"
|
||||
text={t('value')}
|
||||
info={
|
||||
getValueHelpText(
|
||||
addRuleForm.watch(
|
||||
@@ -717,7 +714,7 @@ export default function ResourceRules(props: {
|
||||
className="mb-2"
|
||||
disabled={!rulesEnabled}
|
||||
>
|
||||
Add Rule
|
||||
{t('ruleSubmit')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -760,13 +757,13 @@ export default function ResourceRules(props: {
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No rules. Add a rule using the form.
|
||||
{t('rulesNoOne')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<TableCaption>
|
||||
Rules are evaluated by priority in ascending order.
|
||||
{t('rulesOrder')}
|
||||
</TableCaption>
|
||||
</Table>
|
||||
</SettingsSectionBody>
|
||||
@@ -776,7 +773,7 @@ export default function ResourceRules(props: {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Save Rules
|
||||
{t('rulesSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -242,10 +242,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error fetching sites",
|
||||
title: t('sitesErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred when fetching the sites"
|
||||
t('sitesErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -270,10 +270,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error fetching domains",
|
||||
title: t('domainsErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred when fetching the domains"
|
||||
t('domainsErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { GetOrgResponse } from "@server/routers/org";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import ResourcesSplashCard from "./ResourcesSplashCard";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type ResourcesPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -46,14 +47,16 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||
redirect(`/${params.orgId}/settings/resources`);
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const resourceRows: ResourceRow[] = resources.map((resource) => {
|
||||
return {
|
||||
id: resource.resourceId,
|
||||
name: resource.name,
|
||||
orgId: params.orgId,
|
||||
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
||||
site: resource.siteName || "None",
|
||||
siteId: resource.siteId || "Unknown",
|
||||
site: resource.siteName || t('none'),
|
||||
siteId: resource.siteId || t('unknown'),
|
||||
protocol: resource.protocol,
|
||||
proxyPort: resource.proxyPort,
|
||||
http: resource.http,
|
||||
@@ -69,8 +72,6 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||
};
|
||||
});
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <ResourcesSplashCard /> */}
|
||||
|
||||
@@ -92,7 +92,6 @@ export default function CreateShareLinkForm({
|
||||
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const [accessTokenId, setAccessTokenId] = useState<string | null>(null);
|
||||
@@ -111,6 +110,8 @@ export default function CreateShareLinkForm({
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: "minutes", name: t('minutes') },
|
||||
{ unit: "hours", name: t('hours') },
|
||||
@@ -143,8 +144,11 @@ export default function CreateShareLinkForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('shareErrorFetchResource'),
|
||||
description: formatAxiosError(e, t('shareErrorFetchResourceDescription'))
|
||||
title: "Failed to fetch resources",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the resources"
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,8 +211,11 @@ export default function CreateShareLinkForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('shareErrorCreate'),
|
||||
description: formatAxiosError(e, t('shareErrorCreateDescription'))
|
||||
title: "Failed to create share link",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the share link"
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,7 +301,7 @@ export default function CreateShareLinkForm({
|
||||
? getSelectedResourceName(
|
||||
field.value
|
||||
)
|
||||
: "Select resource"}
|
||||
: t('resourceSelect')}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
@@ -387,7 +394,7 @@ export default function CreateShareLinkForm({
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
<SelectValue placeholder={t('selectDuration')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -497,7 +504,7 @@ export default function CreateShareLinkForm({
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
Toggle
|
||||
{t('toggle')}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -527,7 +534,7 @@ export default function CreateShareLinkForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -24,7 +24,7 @@ export function ShareLinksDataTable<TData, TValue>({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Share Links"
|
||||
title={t('shareLinks')}
|
||||
searchPlaceholder={t('shareSearch')}
|
||||
searchColumn="name"
|
||||
onAdd={createShareLink}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Link, X, Clock, Share, ArrowRight, Lock } from "lucide-react"; // Replace with actual imports
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export const ShareableLinksSplash = () => {
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
@@ -22,6 +23,8 @@ export const ShareableLinksSplash = () => {
|
||||
localStorage.setItem(key, "true");
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
if (isDismissed) {
|
||||
return null;
|
||||
}
|
||||
@@ -31,7 +34,7 @@ export const ShareableLinksSplash = () => {
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="absolute top-2 right-2 p-2"
|
||||
aria-label="Dismiss"
|
||||
aria-label={t('dismiss')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -39,26 +42,23 @@ export const ShareableLinksSplash = () => {
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Link className="text-blue-500" />
|
||||
Shareable Links
|
||||
{t('share')}
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
Create shareable links to your resources. Links provide
|
||||
temporary or unlimited access to your resource. You can
|
||||
configure the expiration duration of the link when you
|
||||
create one.
|
||||
{t('shareDescription2')}
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Share className="text-green-500 w-4 h-4" />
|
||||
Easy to create and share
|
||||
{t('shareEasyCreate')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Clock className="text-yellow-500 w-4 h-4" />
|
||||
Configurable expiration duration
|
||||
{t('shareConfigurableExpirationDuration')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Lock className="text-red-500 w-4 h-4" />
|
||||
Secure and revocable
|
||||
{t('shareSecureAndRevocable')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -69,8 +69,11 @@ export default function ShareLinksTable({
|
||||
async function deleteSharelink(id: string) {
|
||||
await api.delete(`/access-token/${id}`).catch((e) => {
|
||||
toast({
|
||||
title: t('shareErrorDelete'),
|
||||
description: formatAxiosError(e,t('shareErrorDeleteMessage'))
|
||||
title: "Failed to delete link",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred deleting link"
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,8 +81,8 @@ export default function ShareLinksTable({
|
||||
setRows(newRows);
|
||||
|
||||
toast({
|
||||
title: t('shareDeleted'),
|
||||
description: t('shareDeletedDesciption')
|
||||
title: "Link deleted",
|
||||
description: "The link has been deleted"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -52,16 +52,14 @@ import {
|
||||
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const createSiteFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: {t('siteNameMin')}
|
||||
message: "Name must be at least 2 characters."
|
||||
})
|
||||
.max(30, {
|
||||
message: {t('siteNameMax')}
|
||||
message: "Name must not be longer than 30 characters."
|
||||
}),
|
||||
method: z.enum(["wireguard", "newt", "local"])
|
||||
});
|
||||
@@ -117,6 +115,8 @@ export default function CreateSiteForm({
|
||||
const nameField = form.watch("name");
|
||||
const methodField = form.watch("method");
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const nameIsValid = nameField?.length >= 2 && nameField?.length <= 30;
|
||||
const isFormValid = methodField === "local" || isChecked;
|
||||
@@ -172,8 +172,8 @@ export default function CreateSiteForm({
|
||||
if (!keypair || !siteDefaults) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: {t('siteErrorCreate')},
|
||||
description: {t('siteErrorCreateKeyPair')}
|
||||
title: "Error creating site",
|
||||
description: "Key pair or site defaults not found"
|
||||
});
|
||||
setLoading?.(false);
|
||||
setIsLoading(false);
|
||||
@@ -191,8 +191,8 @@ export default function CreateSiteForm({
|
||||
if (!siteDefaults) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: {t('siteErrorCreate')},
|
||||
description: {t('siteErrorCreateDefaults')}
|
||||
title: "Error creating site",
|
||||
description: "Site defaults not found"
|
||||
});
|
||||
setLoading?.(false);
|
||||
setIsLoading(false);
|
||||
@@ -215,7 +215,7 @@ export default function CreateSiteForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: {t('siteErrorCreate')},
|
||||
title: "Error creating site",
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -315,7 +315,7 @@ PersistentKeepalive = 5`
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">
|
||||
Local
|
||||
{t('local')}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="newt"
|
||||
|
||||
@@ -22,8 +22,8 @@ export function SitesDataTable<TData, TValue>({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Sites"
|
||||
searchPlaceholder={t('searchSites')}
|
||||
title={t('sites')}
|
||||
searchPlaceholder={t('searchSitesProgress')}
|
||||
searchColumn="name"
|
||||
onAdd={createSite}
|
||||
addButtonText={t('siteAdd')}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const SitesSplashCard = () => {
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="absolute top-2 right-2 p-2"
|
||||
aria-label="Dismiss"
|
||||
aria-label={t('dismiss')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -70,7 +70,7 @@ export const SitesSplashCard = () => {
|
||||
className="w-full flex items-center"
|
||||
variant="secondary"
|
||||
>
|
||||
Install Newt{" "}
|
||||
{t('siteInstallNewt')}{" "}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -78,20 +78,19 @@ export const SitesSplashCard = () => {
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
Basic WireGuard
|
||||
{t('siteWg')}
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
Use any WireGuard client to connect. You will have to
|
||||
address your internal resources using the peer IP.
|
||||
{t('siteWgAnyClients')}
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Docker className="text-purple-500 w-4 h-4" />
|
||||
Compatible with all WireGuard clients
|
||||
{t('siteWgCompatibleAllClients')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Server className="text-purple-500 w-4 h-4" />
|
||||
Manual configuration required
|
||||
{t('siteWgManualConfigurationRequired')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
} else if (type === "local") {
|
||||
return t('local');
|
||||
} else {
|
||||
return "Unknown";
|
||||
return t('unknown');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ export default function GeneralPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
const form = useForm<GeneralFormValues>({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
@@ -56,6 +55,7 @@ export default function GeneralPage() {
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
const t = useTranslations();
|
||||
|
||||
async function onSubmit(data: GeneralFormValues) {
|
||||
setLoading(true);
|
||||
@@ -67,16 +67,19 @@ export default function GeneralPage() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('siteErrorUpdate'),
|
||||
description: formatAxiosError(e,t('siteErrorUpdateDescription'))
|
||||
title: "Failed to update site",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while updating the site."
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
updateSite({ name: data.name });
|
||||
|
||||
toast({
|
||||
title: t('siteUpdated'),
|
||||
description: t('siteUpdatedDescription')
|
||||
title: "Site updated",
|
||||
description: "The site has been updated."
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -70,7 +70,7 @@ const createSiteFormSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, { message: "Name must be at least 2 characters." })
|
||||
.min(2, "Name must be at least 2 characters.")
|
||||
.max(30, {
|
||||
message: "Name must not be longer than 30 characters."
|
||||
}),
|
||||
@@ -324,7 +324,7 @@ WantedBy=default.target`
|
||||
};
|
||||
|
||||
const getCommand = () => {
|
||||
const placeholder = ["Unknown command"];
|
||||
const placeholder = [t('unknownCommand')];
|
||||
if (!commands) {
|
||||
return placeholder;
|
||||
}
|
||||
@@ -381,8 +381,8 @@ WantedBy=default.target`
|
||||
if (!siteDefaults || !wgConfig) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('siteErrorCreate'),
|
||||
description: t('siteErrorCreateKeyPair')
|
||||
title: "Error creating site",
|
||||
description: "Key pair or site defaults not found"
|
||||
});
|
||||
setCreateLoading(false);
|
||||
return;
|
||||
@@ -399,8 +399,8 @@ WantedBy=default.target`
|
||||
if (!siteDefaults) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('siteErrorCreate'),
|
||||
description: t('siteErrorCreateDefaults')
|
||||
title: "Error creating site",
|
||||
description: "Site defaults not found"
|
||||
});
|
||||
setCreateLoading(false);
|
||||
return;
|
||||
@@ -422,7 +422,7 @@ WantedBy=default.target`
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('siteErrorCreate'),
|
||||
title: "Error creating site",
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -448,14 +448,14 @@ WantedBy=default.target`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch release info: ${response.statusText}`
|
||||
t('newtErrorFetchReleases', {err: response.statusText})
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
const latestVersion = data.tag_name;
|
||||
newtVersion = latestVersion;
|
||||
} catch (error) {
|
||||
console.error("Error fetching latest release:", error);
|
||||
console.error(t('newtErrorFetchLatest', {err: error instanceof Error ? error.message : String(error)}));
|
||||
}
|
||||
|
||||
const generatedKeypair = generateKeypair();
|
||||
@@ -612,7 +612,7 @@ WantedBy=default.target`
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Newt Endpoint
|
||||
{t('newtEndpoint')}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -624,7 +624,7 @@ WantedBy=default.target`
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Newt ID
|
||||
{t('newtId')}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -634,7 +634,7 @@ WantedBy=default.target`
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Newt Secret Key
|
||||
{t('newtSecretKey')}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -737,8 +737,8 @@ WantedBy=default.target`
|
||||
{["docker", "podman"].includes(
|
||||
platform
|
||||
)
|
||||
? "Method"
|
||||
: "Architecture"}
|
||||
? t('method')
|
||||
: t('architecture')}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures().map(
|
||||
|
||||
@@ -62,10 +62,10 @@ const createFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: t('apiKeysNameMin')
|
||||
message: t('nameMin', {len: 2})
|
||||
})
|
||||
.max(255, {
|
||||
message: t('apiKeysNameMax')
|
||||
message: t('nameMax', {len: 255})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -43,9 +43,12 @@ import {
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
|
||||
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
|
||||
clientId: z.string().min(1, { message: "Client ID is required." }),
|
||||
clientSecret: z.string().min(1, { message: "Client Secret is required." }),
|
||||
authUrl: z.string().url({ message: "Auth URL must be a valid URL." }),
|
||||
|
||||
@@ -37,9 +37,12 @@ import { StrategySelect } from "@app/components/StrategySelect";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const createIdpFormSchema = z.object({
|
||||
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
|
||||
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
|
||||
type: z.enum(["oidc"]),
|
||||
clientId: z.string().min(1, { message: "Client ID is required." }),
|
||||
clientSecret: z.string().min(1, { message: "Client Secret is required." }),
|
||||
|
||||
Reference in New Issue
Block a user