mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(i18n): migrate Settings batch 2 — SecurityTab + RoutingTab (25+ strings)
- SecurityTab: password form, blocked providers, endpoint protection - RoutingTab: strategy labels, model aliases, sticky limit - Expanded settings namespace by 40 security/routing keys
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Input, Toggle, Button } from "@/shared/components";
|
||||
import FallbackChainsEditor from "./FallbackChainsEditor";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const STRATEGIES = [
|
||||
{
|
||||
@@ -34,6 +35,7 @@ export default function RoutingTab() {
|
||||
const [aliases, setAliases] = useState([]);
|
||||
const [newPattern, setNewPattern] = useState("");
|
||||
const [newTarget, setNewTarget] = useState("");
|
||||
const t = useTranslations("settings");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings")
|
||||
@@ -86,7 +88,7 @@ export default function RoutingTab() {
|
||||
route
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Routing Strategy</h3>
|
||||
<h3 className="text-lg font-semibold">{t("routingStrategy")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 mb-4" style={{ gridAutoRows: "1fr" }}>
|
||||
@@ -123,8 +125,8 @@ export default function RoutingTab() {
|
||||
{settings.fallbackStrategy === "round-robin" && (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-border/30">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Sticky Limit</p>
|
||||
<p className="text-xs text-text-muted">Calls per account before switching</p>
|
||||
<p className="text-sm font-medium">{t("stickyLimit")}</p>
|
||||
<p className="text-xs text-text-muted">{t("stickyLimitDesc")}</p>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -163,10 +165,8 @@ export default function RoutingTab() {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Model Aliases</h3>
|
||||
<p className="text-sm text-text-muted">
|
||||
Wildcard patterns to remap model names • Use * and ?
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold">{t("modelAliases")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("modelAliasesDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function RoutingTab() {
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Pattern"
|
||||
label={t("pattern")}
|
||||
placeholder="claude-sonnet-*"
|
||||
value={newPattern}
|
||||
onChange={(e) => setNewPattern(e.target.value)}
|
||||
@@ -206,14 +206,14 @@ export default function RoutingTab() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label="Target Model"
|
||||
label={t("targetModel")}
|
||||
placeholder="claude-sonnet-4-20250514"
|
||||
value={newTarget}
|
||||
onChange={(e) => setNewTarget(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" variant="primary" onClick={addAlias} className="mb-[2px]">
|
||||
+ Add
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, Button, Input, Toggle } from "@/shared/components";
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import IPFilterSection from "./IPFilterSection";
|
||||
import SessionInfoCard from "./SessionInfoCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function SecurityTab() {
|
||||
const [settings, setSettings] = useState<any>({ requireLogin: false, hasPassword: false });
|
||||
@@ -12,6 +13,7 @@ export default function SecurityTab() {
|
||||
const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" });
|
||||
const [passStatus, setPassStatus] = useState({ type: "", message: "" });
|
||||
const [passLoading, setPassLoading] = useState(false);
|
||||
const t = useTranslations("settings");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings")
|
||||
@@ -64,7 +66,7 @@ export default function SecurityTab() {
|
||||
const handlePasswordChange = async (e) => {
|
||||
e.preventDefault();
|
||||
if (passwords.new !== passwords.confirm) {
|
||||
setPassStatus({ type: "error", message: "Passwords do not match" });
|
||||
setPassStatus({ type: "error", message: t("passwordsNoMatch") });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,13 +84,13 @@ export default function SecurityTab() {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setPassStatus({ type: "success", message: "Password updated successfully" });
|
||||
setPassStatus({ type: "success", message: t("passwordUpdated") });
|
||||
setPasswords({ current: "", new: "", confirm: "" });
|
||||
} else {
|
||||
setPassStatus({ type: "error", message: data.error || "Failed to update password" });
|
||||
setPassStatus({ type: "error", message: data.error || t("failedUpdatePassword") });
|
||||
}
|
||||
} catch {
|
||||
setPassStatus({ type: "error", message: "An error occurred" });
|
||||
setPassStatus({ type: "error", message: t("errorOccurred") });
|
||||
} finally {
|
||||
setPassLoading(false);
|
||||
}
|
||||
@@ -105,15 +107,13 @@ export default function SecurityTab() {
|
||||
shield
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Security</h3>
|
||||
<h3 className="text-lg font-semibold">{t("security")}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Require login</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
When ON, dashboard requires password. When OFF, access without login.
|
||||
</p>
|
||||
<p className="font-medium">{t("requireLogin")}</p>
|
||||
<p className="text-sm text-text-muted">{t("requireLoginDesc")}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={settings.requireLogin === true}
|
||||
@@ -128,9 +128,9 @@ export default function SecurityTab() {
|
||||
>
|
||||
{settings.hasPassword && (
|
||||
<Input
|
||||
label="Current Password"
|
||||
label={t("currentPassword")}
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
placeholder={t("enterCurrentPassword")}
|
||||
value={passwords.current}
|
||||
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
|
||||
required
|
||||
@@ -138,17 +138,17 @@ export default function SecurityTab() {
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="New Password"
|
||||
label={t("newPassword")}
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
placeholder={t("enterNewPassword")}
|
||||
value={passwords.new}
|
||||
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Confirm New Password"
|
||||
label={t("confirmPassword")}
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
placeholder={t("confirmPasswordPlaceholder")}
|
||||
value={passwords.confirm}
|
||||
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
|
||||
required
|
||||
@@ -165,7 +165,7 @@ export default function SecurityTab() {
|
||||
|
||||
<div className="pt-2">
|
||||
<Button type="submit" variant="primary" loading={passLoading}>
|
||||
{settings.hasPassword ? "Update Password" : "Set Password"}
|
||||
{settings.hasPassword ? t("updatePassword") : t("setPassword")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -181,13 +181,13 @@ export default function SecurityTab() {
|
||||
api
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">API Endpoint Protection</h3>
|
||||
<h3 className="text-lg font-semibold">{t("apiEndpointProtection")}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Require auth for /models */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Require API key for /models</p>
|
||||
<p className="font-medium">{t("requireAuthModels")}</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
When ON, the{" "}
|
||||
<code className="text-xs bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded">
|
||||
@@ -207,7 +207,7 @@ export default function SecurityTab() {
|
||||
{/* Blocked Providers */}
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<div className="mb-3">
|
||||
<p className="font-medium">Blocked Providers</p>
|
||||
<p className="font-medium">{t("blockedProviders")}</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
Hide specific providers from the{" "}
|
||||
<code className="text-xs bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded">
|
||||
|
||||
@@ -666,7 +666,47 @@
|
||||
"tokenBudget": "Token Budget",
|
||||
"tokens": "tokens",
|
||||
"baseEffortLevel": "Base Effort Level",
|
||||
"adaptiveHint": "Adaptive mode scales from this base level based on message count, tool usage, and prompt length."
|
||||
"adaptiveHint": "Adaptive mode scales from this base level based on message count, tool usage, and prompt length.",
|
||||
"requireLogin": "Require login",
|
||||
"requireLoginDesc": "When ON, dashboard requires password. When OFF, access without login.",
|
||||
"currentPassword": "Current Password",
|
||||
"enterCurrentPassword": "Enter current password",
|
||||
"newPassword": "New Password",
|
||||
"enterNewPassword": "Enter new password",
|
||||
"confirmPassword": "Confirm New Password",
|
||||
"confirmPasswordPlaceholder": "Confirm new password",
|
||||
"passwordsNoMatch": "Passwords do not match",
|
||||
"passwordUpdated": "Password updated successfully",
|
||||
"failedUpdatePassword": "Failed to update password",
|
||||
"errorOccurred": "An error occurred",
|
||||
"updatePassword": "Update Password",
|
||||
"setPassword": "Set Password",
|
||||
"apiEndpointProtection": "API Endpoint Protection",
|
||||
"requireAuthModels": "Require API key for /models",
|
||||
"requireAuthModelsDesc": "When ON, the /v1/models endpoint returns 404 for unauthenticated requests. Prevents model discovery by unauthorized users.",
|
||||
"blockedProviders": "Blocked Providers",
|
||||
"blockedProvidersDesc": "Hide specific providers from the /v1/models response. Blocked providers will not appear in model listings.",
|
||||
"providersBlocked": "{count} provider(s) blocked from /models",
|
||||
"routingStrategy": "Routing Strategy",
|
||||
"fillFirst": "Fill First",
|
||||
"fillFirstDesc": "Use accounts in priority order",
|
||||
"roundRobin": "Round Robin",
|
||||
"roundRobinDesc": "Cycle through all accounts",
|
||||
"p2c": "P2C",
|
||||
"p2cDesc": "Pick 2 random, use the healthier one",
|
||||
"random": "Random",
|
||||
"randomDesc": "Random account each request",
|
||||
"leastUsed": "Least Used",
|
||||
"leastUsedDesc": "Pick least recently used account",
|
||||
"costOpt": "Cost Opt",
|
||||
"costOptDesc": "Prefer cheapest available account",
|
||||
"stickyLimit": "Sticky Limit",
|
||||
"stickyLimitDesc": "Calls per account before switching",
|
||||
"modelAliases": "Model Aliases",
|
||||
"modelAliasesDesc": "Wildcard patterns to remap model names • Use * and ?",
|
||||
"pattern": "Pattern",
|
||||
"targetModel": "Target Model",
|
||||
"add": "+ Add"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Translator",
|
||||
|
||||
@@ -666,7 +666,47 @@
|
||||
"tokenBudget": "Orçamento de Tokens",
|
||||
"tokens": "tokens",
|
||||
"baseEffortLevel": "Nível de Esforço Base",
|
||||
"adaptiveHint": "Modo adaptativo escala a partir deste nível base baseado na quantidade de mensagens, uso de ferramentas e tamanho do prompt."
|
||||
"adaptiveHint": "Modo adaptativo escala a partir deste nível base baseado na quantidade de mensagens, uso de ferramentas e tamanho do prompt.",
|
||||
"requireLogin": "Exigir login",
|
||||
"requireLoginDesc": "Quando ATIVADO, dashboard exige senha. Quando DESATIVADO, acesso sem login.",
|
||||
"currentPassword": "Senha Atual",
|
||||
"enterCurrentPassword": "Digite a senha atual",
|
||||
"newPassword": "Nova Senha",
|
||||
"enterNewPassword": "Digite a nova senha",
|
||||
"confirmPassword": "Confirmar Nova Senha",
|
||||
"confirmPasswordPlaceholder": "Confirme a nova senha",
|
||||
"passwordsNoMatch": "As senhas não coincidem",
|
||||
"passwordUpdated": "Senha atualizada com sucesso",
|
||||
"failedUpdatePassword": "Falha ao atualizar senha",
|
||||
"errorOccurred": "Ocorreu um erro",
|
||||
"updatePassword": "Atualizar Senha",
|
||||
"setPassword": "Definir Senha",
|
||||
"apiEndpointProtection": "Proteção de Endpoint da API",
|
||||
"requireAuthModels": "Exigir chave de API para /models",
|
||||
"requireAuthModelsDesc": "Quando ATIVADO, o endpoint /v1/models retorna 404 para requisições não autenticadas. Impede descoberta de modelos por usuários não autorizados.",
|
||||
"blockedProviders": "Provedores Bloqueados",
|
||||
"blockedProvidersDesc": "Ocultar provedores específicos da resposta /v1/models. Provedores bloqueados não aparecerão nas listagens de modelos.",
|
||||
"providersBlocked": "{count} provedor(es) bloqueado(s) do /models",
|
||||
"routingStrategy": "Estratégia de Roteamento",
|
||||
"fillFirst": "Preencher Primeiro",
|
||||
"fillFirstDesc": "Usar contas em ordem de prioridade",
|
||||
"roundRobin": "Round Robin",
|
||||
"roundRobinDesc": "Ciclar entre todas as contas",
|
||||
"p2c": "P2C",
|
||||
"p2cDesc": "Escolher 2 aleatórias, usar a mais saudável",
|
||||
"random": "Aleatório",
|
||||
"randomDesc": "Conta aleatória em cada requisição",
|
||||
"leastUsed": "Menos Usado",
|
||||
"leastUsedDesc": "Escolher a conta usada menos recentemente",
|
||||
"costOpt": "Custo Otimizado",
|
||||
"costOptDesc": "Preferir conta mais barata disponível",
|
||||
"stickyLimit": "Limite Fixo",
|
||||
"stickyLimitDesc": "Chamadas por conta antes de trocar",
|
||||
"modelAliases": "Aliases de Modelo",
|
||||
"modelAliasesDesc": "Padrões coringa para remapear nomes de modelos • Use * e ?",
|
||||
"pattern": "Padrão",
|
||||
"targetModel": "Modelo Alvo",
|
||||
"add": "+ Adicionar"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tradutor",
|
||||
|
||||
Reference in New Issue
Block a user