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:
vlalx
2025-05-17 18:49:01 +03:00
committed by GitHub
parent 6f54e3da9e
commit 96bfc3cf36
49 changed files with 2749 additions and 590 deletions

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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')
)
});
});

View File

@@ -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 /> */}