I18n admin (#22)

* 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 app/admin

* Fix build

---------

Co-authored-by: Lokowitz <marvinlokowitz@gmail.com>
This commit is contained in:
vlalx
2025-05-17 19:04:19 +03:00
committed by GitHub
parent 96bfc3cf36
commit d2d84be99a
27 changed files with 1028 additions and 306 deletions

View File

@@ -123,7 +123,7 @@ export default function RegenerateInvitationForm({
onRegenerate({
id: invitation.id,
email: invitation.email,
expiresAt: res.data.data.expiresAt,
expiresAt: res.data.data.expiresAt ?? "",
role: invitation.role,
roleId: invitation.roleId
});

View File

@@ -172,8 +172,8 @@ export default function CreateSiteForm({
if (!keypair || !siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Key pair or site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateKeyPair')
});
setLoading?.(false);
setIsLoading(false);
@@ -191,8 +191,8 @@ export default function CreateSiteForm({
if (!siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateDefaults')
});
setLoading?.(false);
setIsLoading(false);
@@ -215,7 +215,7 @@ export default function CreateSiteForm({
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating site",
title: t('siteErrorCreate'),
description: formatAxiosError(e)
});
});

View File

@@ -74,7 +74,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -117,7 +117,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
},
{
accessorKey: "key",
header: "Key",
header: t('key'),
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@@ -125,7 +125,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
},
{
accessorKey: "createdAt",
header: "Created At",
header: t('createdAt'),
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")} </span>;

View File

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

View File

@@ -56,16 +56,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('nameMin', {len: 2})
message: "Name must be at least 2 characters."
})
.max(255, {
message: t('nameMax', {len: 255})
message: "Name must not be longer than 255 characters."
})
});
@@ -80,7 +78,7 @@ const copiedFormSchema = z
return data.copied;
},
{
message: t('apiKeysConfirmCopy2'),
message: "You must confirm that you have copied the API key.",
path: ["copied"]
}
);
@@ -113,6 +111,8 @@ export default function Page() {
}
});
const t = useTranslations();
async function onSubmit(data: CreateFormValues) {
setCreateLoading(true);
@@ -125,7 +125,7 @@ export default function Page() {
.catch((e) => {
toast({
variant: "destructive",
title: t('apiKeysErrorCreate'),
title: "Error creating API key",
description: formatAxiosError(e)
});
});
@@ -146,10 +146,10 @@ export default function Page() {
)
})
.catch((e) => {
console.error(t('apiKeysErrorSetPermission'), e);
console.error("Error setting permissions", e);
toast({
variant: "destructive",
title: t('apiKeysErrorSetPermission'),
title: "Error setting permissions",
description: formatAxiosError(e)
});
});
@@ -191,7 +191,7 @@ export default function Page() {
router.push(`/admin/api-keys`);
}}
>
See All API Keys
{t('apiKeysSeeAll')}
</Button>
</div>

View File

@@ -3,6 +3,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -14,15 +15,16 @@ export function IdpDataTable<TData, TValue>({
data
}: DataTableProps<TData, TValue>) {
const router = useRouter();
const t = useTranslations();
return (
<DataTable
columns={columns}
data={data}
title="Identity Providers"
searchPlaceholder="Search identity providers..."
title={t('idp')}
searchPlaceholder={t('idpSearch')}
searchColumn="name"
addButtonText="Add Identity Provider"
addButtonText={t('idpAdd')}
onAdd={() => {
router.push("/admin/idp/create");
}}

View File

@@ -19,6 +19,7 @@ import {
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import Link from "next/link";
import { useTranslations } from "next-intl";
export type IdpRow = {
idpId: number;
@@ -36,19 +37,20 @@ export default function IdpTable({ idps }: Props) {
const [selectedIdp, setSelectedIdp] = useState<IdpRow | null>(null);
const api = createApiClient(useEnvContext());
const router = useRouter();
const t = useTranslations();
const deleteIdp = async (idpId: number) => {
try {
await api.delete(`/idp/${idpId}`);
toast({
title: "Success",
description: "Identity provider deleted successfully"
title: t('success'),
description: t('idpDeletedDescription')
});
setIsDeleteModalOpen(false);
router.refresh();
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -58,7 +60,7 @@ export default function IdpTable({ idps }: Props) {
const getTypeDisplay = (type: string) => {
switch (type) {
case "oidc":
return "OAuth2/OIDC";
return t('idpOidc');
default:
return type;
}
@@ -74,7 +76,7 @@ export default function IdpTable({ idps }: Props) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -84,7 +86,7 @@ export default function IdpTable({ idps }: Props) {
href={`/admin/idp/${r.idpId}/general`}
>
<DropdownMenuItem>
View settings
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
@@ -93,7 +95,7 @@ export default function IdpTable({ idps }: Props) {
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">Delete</span>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -126,7 +128,7 @@ export default function IdpTable({ idps }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Name
{t('name')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -142,7 +144,7 @@ export default function IdpTable({ idps }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Type
{t('type')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -162,7 +164,7 @@ export default function IdpTable({ idps }: Props) {
<div className="flex items-center justify-end">
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
<Button variant={"outlinePrimary"} className="ml-2">
Edit
{t('edit')}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -184,27 +186,22 @@ export default function IdpTable({ idps }: Props) {
dialog={
<div className="space-y-4">
<p>
Are you sure you want to permanently delete the
identity provider <b>{selectedIdp.name}</b>?
{t('idpQuestionRemove', {name: selectedIdp.name})}
</p>
<p>
<b>
This will remove the identity provider and
all associated configurations. Users who
authenticate through this provider will no
longer be able to log in.
{t('idpMessageRemove')}
</b>
</p>
<p>
To confirm, please type the name of the identity
provider below.
{t('idpMessageConfirm')}
</p>
</div>
}
buttonText="Confirm Delete Identity Provider"
buttonText={t('idpConfirmDelete')}
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
string={selectedIdp.name}
title="Delete Identity Provider"
title={t('idpDelete')}
/>
)}

View File

@@ -45,10 +45,8 @@ 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: t('nameMin', {len: 2}) }),
name: z.string().min(2, "Name must be at least 2 characters."),
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." }),
@@ -91,6 +89,8 @@ export default function GeneralPage() {
}
});
const t = useTranslations();
useEffect(() => {
const loadIdp = async () => {
try {
@@ -112,7 +112,7 @@ export default function GeneralPage() {
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -172,18 +172,17 @@ export default function GeneralPage() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
General Information
{t('idpTitle')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the basic information for your identity
provider
{t('idpSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
Redirect URL
{t('redirectUrl')}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard text={redirectUrl} />
@@ -194,13 +193,10 @@ export default function GeneralPage() {
<Alert variant="neutral" className="">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About Redirect URL
{t('redirectUrlAbout')}
</AlertTitle>
<AlertDescription>
This is the URL to which users will be
redirected after authentication. You need to
configure this URL in your identity provider
settings.
{t('redirectUrlAboutDescription')}
</AlertDescription>
</Alert>
<SettingsSectionForm>
@@ -215,13 +211,12 @@ export default function GeneralPage() {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
A display name for this
identity provider
{t('idpDisplayName')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -231,7 +226,7 @@ export default function GeneralPage() {
<div className="flex items-start mb-0">
<SwitchInput
id="auto-provision-toggle"
label="Auto Provision Users"
label={t('idpAutoProvisionUsers')}
defaultChecked={form.getValues(
"autoProvision"
)}
@@ -244,10 +239,7 @@ export default function GeneralPage() {
/>
</div>
<span className="text-sm text-muted-foreground">
When enabled, users will be
automatically created in the system upon
first login with the ability to map
users to roles and organizations.
{t('idpAutoProvisionUsersDescription')}
</span>
</form>
</Form>
@@ -259,11 +251,10 @@ export default function GeneralPage() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
OAuth2/OIDC Configuration
{t('idpOidcConfigure')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the OAuth2/OIDC provider endpoints and
credentials
{t('idpOidcConfigureDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -280,15 +271,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Client ID
{t('idpClientId')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 client ID
from your identity
provider
{t('idpClientIdDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -301,7 +290,7 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Client Secret
{t('idpClientSecret')}
</FormLabel>
<FormControl>
<Input
@@ -310,9 +299,7 @@ export default function GeneralPage() {
/>
</FormControl>
<FormDescription>
The OAuth2 client secret
from your identity
provider
{t('idpClientSecretDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -325,14 +312,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Authorization URL
{t('idpAuthUrl')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 authorization
endpoint URL
{t('idpAuthUrlDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -345,14 +331,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Token URL
{t('idpTokenUrl')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 token
endpoint URL
{t('idpTokenUrlDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -367,11 +352,10 @@ export default function GeneralPage() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Token Configuration
{t('idpToken')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how to extract user information from
the ID token
{t('idpTokenDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -385,19 +369,18 @@ export default function GeneralPage() {
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About JMESPath
{t('idpJmespathAbout')}
</AlertTitle>
<AlertDescription>
The paths below use JMESPath
syntax to extract values from
the ID token.
{/*TODO(vlalx): Validate replacing */}
{t('idpJmespathAboutDescription')}
<a
href="https://jmespath.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more about JMESPath{" "}
{t('idpJmespathAboutDescriptionLink')}{" "}
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</AlertDescription>
@@ -409,15 +392,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Identifier Path
{t('idpJmespathLabel')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the user
identifier in the ID
token
{t('idpJmespathLabelDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -430,15 +411,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Email Path (Optional)
{t('idpJmespathEmailPathOptional')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's email in the ID
token
{t('idpJmespathEmailPathOptionalDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -451,15 +430,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Name Path (Optional)
{t('idpJmespathNamePathOptional')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
user's name in the ID
token
{t('idpJmespathNamePathOptionalDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -472,14 +449,13 @@ export default function GeneralPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Scopes
{t('idpOidcConfigureScopes')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Space-separated list of
OAuth2 scopes to request
{t('idpOidcConfigureScopesDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -500,7 +476,7 @@ export default function GeneralPage() {
loading={loading}
disabled={loading}
>
Save General Settings
{t('saveGeneralSettings')}
</Button>
</div>
</>

View File

@@ -15,6 +15,7 @@ import {
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { useTranslations } from "next-intl";
interface SettingsLayoutProps {
children: React.ReactNode;
@@ -36,13 +37,15 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
redirect("/admin/idp");
}
const t = useTranslations();
const navItems: HorizontalTabs = [
{
title: "General",
title: t('general'),
href: `/admin/idp/${params.idpId}/general`
},
{
title: "Organization Policies",
title: t('orgPolicies'),
href: `/admin/idp/${params.idpId}/policies`
}
];
@@ -50,8 +53,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return (
<>
<SettingsSectionTitle
title={`${idp.idp.name} Settings`}
description="Configure the settings for your identity provider"
title={t('idpSettings', { idpName: idp?.idp.name })}
description={t('idpSettingsDescription')}
/>
<div className="space-y-6">

View File

@@ -2,6 +2,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from "next-intl";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -14,14 +15,15 @@ export function PolicyDataTable<TData, TValue>({
data,
onAdd
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
<DataTable
columns={columns}
data={data}
title="Organization Policies"
searchPlaceholder="Search organization policies..."
title={t('orgPolicies')}
searchPlaceholder={t('orgPoliciesSearch')}
searchColumn="orgId"
addButtonText="Add Organization Policy"
addButtonText={t('orgPoliciesAdd')}
onAdd={onAdd}
/>
);

View File

@@ -19,6 +19,7 @@ import {
} from "@app/components/ui/dropdown-menu";
import Link from "next/link";
import { InfoPopup } from "@app/components/ui/info-popup";
import { useTranslations } from "next-intl";
export interface PolicyRow {
orgId: string;
@@ -34,6 +35,7 @@ interface Props {
}
export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props) {
const t = useTranslations();
const columns: ColumnDef<PolicyRow>[] = [
{
id: "dots",
@@ -44,7 +46,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -54,7 +56,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
onDelete(r.orgId);
}}
>
<span className="text-red-500">Delete</span>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -71,7 +73,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Organization ID
{t('orgId')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -87,7 +89,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Role Mapping
{t('roleMapping')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -114,7 +116,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Organization Mapping
{t('orgMapping')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -142,7 +144,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
className="ml-2"
onClick={() => onEdit(policy)}
>
Edit
{t('edit')}
</Button>
</div>
);

View File

@@ -63,6 +63,7 @@ import {
SettingsSectionFooter,
SettingsSectionForm
} from "@app/components/Settings";
import { useTranslations } from "next-intl";
type Organization = {
orgId: string;
@@ -117,6 +118,8 @@ export default function PoliciesPage() {
}
});
const t = useTranslations();
const loadIdp = async () => {
try {
const res = await api.get<AxiosResponse<GetIdpResponse>>(
@@ -131,7 +134,7 @@ export default function PoliciesPage() {
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -146,7 +149,7 @@ export default function PoliciesPage() {
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -165,7 +168,7 @@ export default function PoliciesPage() {
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -200,15 +203,15 @@ export default function PoliciesPage() {
};
setPolicies([...policies, newPolicy]);
toast({
title: "Success",
description: "Policy added successfully"
title: t('success'),
description: t('orgPolicyAddedDescription')
});
setShowAddDialog(false);
form.reset();
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -242,8 +245,8 @@ export default function PoliciesPage() {
)
);
toast({
title: "Success",
description: "Policy updated successfully"
title: t('success'),
description: t('orgPolicyUpdatedDescription')
});
setShowAddDialog(false);
setEditingPolicy(null);
@@ -251,7 +254,7 @@ export default function PoliciesPage() {
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -269,13 +272,13 @@ export default function PoliciesPage() {
policies.filter((policy) => policy.orgId !== orgId)
);
toast({
title: "Success",
description: "Policy deleted successfully"
title: t('success'),
description: t('orgPolicyDeletedDescription')
});
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -293,13 +296,13 @@ export default function PoliciesPage() {
});
if (res.status === 200) {
toast({
title: "Success",
description: "Default mappings updated successfully"
title: t('success'),
description: t('defaultMappingsUpdatedDescription')
});
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -318,21 +321,18 @@ export default function PoliciesPage() {
<Alert variant="neutral" className="mb-6">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About Organization Policies
{t('orgPoliciesAbout')}
</AlertTitle>
<AlertDescription>
Organization policies are used to control access to
organizations based on the user's ID token. You can
specify JMESPath expressions to extract role and
organization information from the ID token. For more
information, see{" "}
{/*TODO(vlalx): Validate replacing */}
{t('orgPoliciesAboutDescription')}{" "}
<Link
href="https://docs.fossorial.io/Pangolin/Identity%20Providers/auto-provision"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
the documentation
{t('orgPoliciesAboutDescriptionLink')}
<ExternalLink className="ml-1 h-4 w-4 inline" />
</Link>
</AlertDescription>
@@ -341,13 +341,10 @@ export default function PoliciesPage() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Default Mappings (Optional)
{t('defaultMappingsOptional')}
</SettingsSectionTitle>
<SettingsSectionDescription>
The default mappings are used when when there is not
an organization policy defined for an organization.
You can specify the default role and organization
mappings to fall back to here.
{t('defaultMappingsOptionalDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -366,16 +363,13 @@ export default function PoliciesPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Default Role Mapping
{t('defaultMappingsRole')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The result of this
expression must return the
role name as defined in the
organization as a string.
{t('defaultMappingsRoleDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -388,16 +382,13 @@ export default function PoliciesPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Default Organization Mapping
{t('defaultMappingsOrg')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This expression must return
thr org ID or true for the
user to be allowed to access
the organization.
{t('defaultMappingsOrgDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -412,7 +403,7 @@ export default function PoliciesPage() {
form="policy-default-mappings-form"
loading={updateDefaultMappingsLoading}
>
Save Default Mappings
{t('defaultMappingsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSectionBody>
@@ -455,8 +446,8 @@ export default function PoliciesPage() {
<CredenzaHeader>
<CredenzaTitle>
{editingPolicy
? "Edit Organization Policy"
: "Add Organization Policy"}
? t('orgPoliciesEdit')
: t('orgPoliciesAdd')}
</CredenzaTitle>
<CredenzaDescription>
Configure access for an organization
@@ -476,7 +467,7 @@ export default function PoliciesPage() {
name="orgId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Organization</FormLabel>
<FormLabel>{t('org')}</FormLabel>
{editingPolicy ? (
<Input {...field} disabled />
) : (
@@ -500,18 +491,17 @@ export default function PoliciesPage() {
org.orgId ===
field.value
)?.name
: "Select organization"}
: t('orgSelect')}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput placeholder="Search org" />
<CommandInput placeholder={t('orgSearch')} />
<CommandList>
<CommandEmpty>
No org
found.
{t('orgNotFound')}
</CommandEmpty>
<CommandGroup>
{organizations.map(
@@ -562,16 +552,13 @@ export default function PoliciesPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Role Mapping Path (Optional)
{t('roleMappingPathOptional')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The result of this expression
must return the role name as
defined in the organization as a
string.
{t('defaultMappingsRoleDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -584,17 +571,13 @@ export default function PoliciesPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Organization Mapping Path
(Optional)
{t('orgMappingPathOptional')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This expression must return the
org ID or true for the user to
be allowed to access the
organization.
{t('defaultMappingsOrgDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -605,7 +588,7 @@ export default function PoliciesPage() {
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Cancel</Button>
<Button variant="outline">{t("cancel")}</Button>
</CredenzaClose>
<Button
type="submit"
@@ -621,7 +604,7 @@ export default function PoliciesPage() {
: addPolicyLoading
}
>
{editingPolicy ? "Update Policy" : "Add Policy"}
{editingPolicy ? t('orgPolicyUpdate') : t('orgPolicyAdd')}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -39,10 +39,8 @@ 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: t('nameMin', {len: 2}) }),
name: z.string().min(2, "Name must be at least 2 characters."),
type: z.enum(["oidc"]),
clientId: z.string().min(1, { message: "Client ID is required." }),
clientSecret: z.string().min(1, { message: "Client Secret is required." }),
@@ -97,6 +95,8 @@ export default function Page() {
}
});
const t = useTranslations();
async function onSubmit(data: CreateIdpFormValues) {
setCreateLoading(true);
@@ -118,14 +118,14 @@ export default function Page() {
if (res.status === 201) {
toast({
title: "Success",
description: "Identity provider created successfully"
title: t('success'),
description: t('idpCreatedDescription')
});
router.push(`/admin/idp/${res.data.data.idpId}`);
}
} catch (e) {
toast({
title: "Error",
title: t('error'),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -138,8 +138,8 @@ export default function Page() {
<>
<div className="flex justify-between">
<HeaderTitle
title="Create Identity Provider"
description="Configure a new identity provider for user authentication"
title={t('idpCreate')}
description={t('idpCreateDescription')}
/>
<Button
variant="outline"
@@ -147,7 +147,7 @@ export default function Page() {
router.push("/admin/idp");
}}
>
See All Identity Providers
{t('idpSeeAll')}
</Button>
</div>
@@ -155,11 +155,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
General Information
{t('idpTitle')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the basic information for your identity
provider
{t('idpCreateSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -175,13 +174,12 @@ export default function Page() {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
A display name for this
identity provider
{t('idpDisplayName')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -191,7 +189,7 @@ export default function Page() {
<div className="flex items-start mb-0">
<SwitchInput
id="auto-provision-toggle"
label="Auto Provision Users"
label={t('idpAutoProvisionUsers')}
defaultChecked={form.getValues(
"autoProvision"
)}
@@ -204,10 +202,7 @@ export default function Page() {
/>
</div>
<span className="text-sm text-muted-foreground">
When enabled, users will be
automatically created in the system upon
first login with the ability to map
users to roles and organizations.
{t('idpAutoProvisionUsersDescription')}
</span>
</form>
</Form>
@@ -218,11 +213,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Provider Type
{t('idpType')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Select the type of identity provider you want to
configure
{t('idpTypeDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -242,11 +236,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
OAuth2/OIDC Configuration
{t('idpOidcConfigure')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the OAuth2/OIDC provider endpoints
and credentials
{t('idpOidcConfigureDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -262,15 +255,13 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Client ID
{t('idpClientId')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The OAuth2 client ID
from your identity
provider
{t('idpClientIdDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -283,7 +274,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Client Secret
{t('idpClientSecret')}
</FormLabel>
<FormControl>
<Input
@@ -292,9 +283,7 @@ export default function Page() {
/>
</FormControl>
<FormDescription>
The OAuth2 client secret
from your identity
provider
{t('idpClientSecretDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -307,7 +296,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Authorization URL
{t('idpAuthUrl')}
</FormLabel>
<FormControl>
<Input
@@ -316,8 +305,7 @@ export default function Page() {
/>
</FormControl>
<FormDescription>
The OAuth2 authorization
endpoint URL
{t('idpAuthUrlDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -330,7 +318,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Token URL
{t('idpTokenUrl')}
</FormLabel>
<FormControl>
<Input
@@ -339,8 +327,7 @@ export default function Page() {
/>
</FormControl>
<FormDescription>
The OAuth2 token
endpoint URL
{t('idpTokenUrlDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -352,14 +339,10 @@ export default function Page() {
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Important Information
{t('idpOidcConfigureAlert')}
</AlertTitle>
<AlertDescription>
After creating the identity provider,
you will need to configure the callback
URL in your identity provider's
settings. The callback URL will be
provided after successful creation.
{t('idpOidcConfigureAlertDescription')}
</AlertDescription>
</Alert>
</SettingsSectionBody>
@@ -368,11 +351,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Token Configuration
{t('idpToken')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how to extract user information
from the ID token
{t('idpTokenDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -385,19 +367,18 @@ export default function Page() {
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About JMESPath
{t('idpJmespathAbout')}
</AlertTitle>
<AlertDescription>
The paths below use JMESPath
syntax to extract values from
the ID token.
{/*TODO(vlalx): Validate replacing */}
{t('idpJmespathAboutDescription')}
<a
href="https://jmespath.org"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more about JMESPath{" "}
{t('idpJmespathAboutDescriptionLink')}{" "}
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</AlertDescription>
@@ -409,15 +390,13 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Identifier Path
{t('idpJmespathLabel')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The path to the user
identifier in the ID
token
{t('idpJmespathLabelDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -430,15 +409,13 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Email Path (Optional)
{t('idpJmespathEmailPathOptional')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The path to the
user's email in the ID
token
{t('idpJmespathEmailPathOptionalDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -451,15 +428,13 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Name Path (Optional)
{t('idpJmespathNamePathOptional')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The path to the
user's name in the ID
token
{t('idpJmespathNamePathOptionalDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -472,14 +447,14 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Scopes
"idpOidcConfigureScopes": "Scopes"
{t('')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Space-separated list of
OAuth2 scopes to request
{t('idpOidcConfigureScopesDescription')}
</FormDescription>
<FormMessage />
</FormItem>
@@ -501,7 +476,7 @@ export default function Page() {
router.push("/admin/idp");
}}
>
Cancel
{t('cancel')}
</Button>
<Button
type="submit"
@@ -509,7 +484,7 @@ export default function Page() {
loading={createLoading}
onClick={form.handleSubmit(onSubmit)}
>
Create Identity Provider
{t('idpSubmit')}
</Button>
</div>
</>

View File

@@ -3,6 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import IdpTable, { IdpRow } from "./AdminIdpTable";
import { useTranslations } from "next-intl";
export default async function IdpPage() {
let idps: IdpRow[] = [];
@@ -15,12 +16,13 @@ export default async function IdpPage() {
} catch (e) {
console.error(e);
}
const t = useTranslations();
return (
<>
<SettingsSectionTitle
title="Manage Identity Providers"
description="View and manage identity providers in the system"
title={t('idpManage')}
description={t('idpManageDescription')}
/>
<IdpTable idps={idps} />
</>

View File

@@ -10,6 +10,7 @@ import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout";
import { adminNavItems } from "../navigation";
import { useTranslations } from "next-intl";
export const dynamic = "force-dynamic";

View File

@@ -73,7 +73,7 @@ export function LicenseKeysDataTable({
);
},
cell: ({ row }) => {
return row.original.valid ? "Yes" : "No";
return row.original.valid ? t('yes') : t('no');
}
},
{
@@ -94,7 +94,7 @@ export function LicenseKeysDataTable({
cell: ({ row }) => {
const type = row.original.type;
const label =
type === "SITES" ? "Additional Sites" : "Host License";
type === "SITES" ? t('sitesAdditional') : t('licenseHost');
const variant = type === "SITES" ? "secondary" : "default";
return row.original.valid ? (
<Badge variant={variant}>{label}</Badge>
@@ -136,7 +136,7 @@ export function LicenseKeysDataTable({
<DataTable
columns={columns}
data={licenseKeys}
title="License Keys"
title={t('licenseKeys')}
searchPlaceholder={t('licenseKeySearch')}
searchColumn="licenseKey"
onAdd={onCreate}

View File

@@ -83,7 +83,7 @@ export function SitePriceCalculator({
size="icon"
onClick={decrementSites}
disabled={siteCount <= 1}
aria-label="Decrease site count"
aria-label={t('sitestCountDecrease')}
>
<MinusCircle className="h-5 w-5" />
</Button>
@@ -94,7 +94,7 @@ export function SitePriceCalculator({
variant="ghost"
size="icon"
onClick={incrementSites}
aria-label="Increase site count"
aria-label={t('sitestCountIncrease')}
>
<PlusCircle className="h-5 w-5" />
</Button>

View File

@@ -75,7 +75,6 @@ function obfuscateLicenseKey(key: string): string {
export default function LicensePage() {
const api = createApiClient(useEnvContext());
const t = useTranslations();
const [rows, setRows] = useState<LicenseKeyCache[]>([]);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -104,6 +103,8 @@ export default function LicensePage() {
}
});
const t = useTranslations();
useEffect(() => {
async function load() {
setIsInitialLoading(true);
@@ -129,8 +130,11 @@ export default function LicensePage() {
}
} catch (e) {
toast({
title: t('licenseErrorKeyLoad'),
description: formatAxiosError(e, t('licenseErrorKeyLoadDescription'))
title: "Failed to load license keys",
description: formatAxiosError(
e,
"An error occurred loading license keys."
)
});
}
}
@@ -145,14 +149,17 @@ export default function LicensePage() {
}
await loadLicenseKeys();
toast({
title: t('licenseKeyDeleted'),
description: t('licenseKeyDeletedDescription')
title: "License key deleted",
description: "The license key has been deleted."
});
setIsDeleteModalOpen(false);
} catch (e) {
toast({
title: t('licenseErrorKeyDelete'),
description: formatAxiosError(e, t('licenseErrorKeyDeleteDescription'))
title: "Failed to delete license key",
description: formatAxiosError(
e,
"An error occurred deleting license key."
)
});
} finally {
setIsDeletingLicense(false);
@@ -168,13 +175,16 @@ export default function LicensePage() {
}
await loadLicenseKeys();
toast({
title: t('licenseErrorKeyRechecked'),
description: t('licenseErrorKeyRecheckedDescription')
title: "License keys rechecked",
description: "All license keys have been rechecked"
});
} catch (e) {
toast({
title: t('licenseErrorKeyRecheck'),
description: formatAxiosError(e, t('licenseErrorKeyRecheckDescription'))
title: "Failed to recheck license keys",
description: formatAxiosError(
e,
"An error occurred rechecking license keys."
)
});
} finally {
setIsRecheckingLicense(false);
@@ -192,8 +202,8 @@ export default function LicensePage() {
}
toast({
title: t('licenseKeyActivated'),
description: t('licenseKeyActivatedDescription')
title: "License key activated",
description: "The license key has been successfully activated."
});
setIsCreateModalOpen(false);
@@ -202,8 +212,11 @@ export default function LicensePage() {
} catch (e) {
toast({
variant: "destructive",
title: t('licenseErrorKeyActivate'),
description: formatAxiosError(e, t('licenseErrorKeyActivateDescription'))
title: "Failed to activate license key",
description: formatAxiosError(
e,
"An error occurred while activating the license key."
)
});
} finally {
setIsActivatingLicense(false);
@@ -317,7 +330,7 @@ export default function LicensePage() {
}}
dialog={
<div className="space-y-4">
<p>
<p>
{t('licenseQuestionRemove', {selectedKey: obfuscateLicenseKey(selectedLicenseKey.licenseKey)})}
</p>
<p>
@@ -373,11 +386,11 @@ export default function LicensePage() {
<Check />
{licenseStatus?.tier ===
"PROFESSIONAL"
? "Commercial License"
? t('licenseTierProfessional')
: licenseStatus?.tier ===
"ENTERPRISE"
? "Commercial License"
: "Licensed"}
? t('licenseTierEnterprise')
: t('licensed')}
</div>
</div>
) : (

View File

@@ -7,6 +7,7 @@ import UsersTable, { GlobalUserRow } from "./AdminUsersTable";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
import { getTranslations } from 'next-intl/server';
import { useTranslations } from "next-intl";
type PageProps = {
params: Promise<{ orgId: string }>;
@@ -25,6 +26,7 @@ export default async function UsersPage(props: PageProps) {
} catch (e) {
console.error(e);
}
const t = useTranslations();
const userRows: GlobalUserRow[] = rows.map((row) => {
return {
@@ -34,14 +36,12 @@ export default async function UsersPage(props: PageProps) {
username: row.username,
type: row.type,
idpId: row.idpId,
idpName: row.idpName || "Internal",
idpName: row.idpName || t('idpNameInternal'),
dateCreated: row.dateCreated,
serverAdmin: row.serverAdmin
};
});
const t = await getTranslations();
return (
<>
<SettingsSectionTitle

View File

@@ -121,7 +121,7 @@ export default function StepperForm() {
<CardHeader>
<CardTitle>{t('setupNewOrg')}</CardTitle>
<CardDescription>
{t('setupCreate')}
{t('setupCreate')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -231,7 +231,7 @@ export default function StepperForm() {
</FormControl>
<FormMessage />
<FormDescription>
{t('orgDisplayName')}
{t('orgDisplayName')}
</FormDescription>
</FormItem>
)}
@@ -242,7 +242,7 @@ export default function StepperForm() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('setupOrgId')}
{t('orgId')}
</FormLabel>
<FormControl>
<Input