Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,8 @@
"expiresAt": {
"label": "Expiration Date",
"placeholder": "Leave empty for never expires",
"description": "User will be automatically disabled after expiration"
"description": "User will be automatically disabled after expiration",
"pastWarning": "Selected date is in the past. The user will expire and be disabled immediately after saving."
},
"allowedClients": {
"label": "Allowed Clients",
Expand Down
8 changes: 7 additions & 1 deletion messages/en/settings/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@
"leasePercentMonthly": "Monthly Window Lease Percentage",
"leasePercentMonthlyDesc": "Percentage of monthly limit for each lease slice (0-1)",
"leaseCapUsd": "Lease Cap (USD)",
"leaseCapUsdDesc": "Maximum absolute cap per lease slice in USD, leave empty for no limit"
"leaseCapUsdDesc": "Maximum absolute cap per lease slice in USD, leave empty for no limit",
"warnings": {
"dbRefreshIntervalTooLow": "Refresh interval is {value}s. This may increase DB load.",
"dbRefreshIntervalTooHigh": "Refresh interval is {value}s. Quota/limit updates may be delayed.",
"leasePercentZero": "Percentage is 0. This may cause the lease budget to always be 0.",
"leaseCapZero": "Lease cap is 0. This may cause the per-lease budget to be 0."
}
}
},
"section": {
Expand Down
9 changes: 8 additions & 1 deletion messages/en/settings/providers/form/key.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@
"labelEdit": "API Key (Leave empty to keep unchanged)",
"leaveEmpty": "(Leave empty to keep unchanged)",
"leaveEmptyDesc": "Leave empty to keep existing key",
"placeholder": "Enter API Key"
"placeholder": "Enter API Key",
"warnings": {
"looks_like_auth_header": "Looks like you pasted a request header (e.g., Bearer/Authorization/x-api-key). Please enter the key value only.",
"wrapped_in_quotes": "Wrapped in quotes. Usually the quotes are not needed.",
"contains_non_ascii": "Contains non-ASCII characters. This is uncommon for API keys.",
"contains_whitespace": "Contains whitespace (spaces/newlines). This is uncommon for API keys.",
"contains_uncommon_ascii": "Contains uncommon symbols. This is uncommon for API keys."
}
}
3 changes: 2 additions & 1 deletion messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,8 @@
"expiresAt": {
"label": "有効期限",
"placeholder": "空白の場合は無期限",
"description": "有効期限切れ後、ユーザーは自動的に無効化されます"
"description": "有効期限切れ後、ユーザーは自動的に無効化されます",
"pastWarning": "選択した日付は過去です。保存するとユーザーは直ちに期限切れとなり無効化されます。"
},
"allowedClients": {
"label": "許可されたクライアント",
Expand Down
8 changes: 7 additions & 1 deletion messages/ja/settings/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@
"leasePercentMonthly": "月次ウィンドウリース比率",
"leasePercentMonthlyDesc": "各リーススライスの月次制限に対する比率(0-1)",
"leaseCapUsd": "リース上限(USD)",
"leaseCapUsdDesc": "リーススライスごとの絶対上限(米ドル)、空の場合は無制限"
"leaseCapUsdDesc": "リーススライスごとの絶対上限(米ドル)、空の場合は無制限",
"warnings": {
"dbRefreshIntervalTooLow": "更新間隔が {value}s です。DB 負荷が増える可能性があります。",
"dbRefreshIntervalTooHigh": "更新間隔が {value}s です。クォータ/制限の反映が遅れる可能性があります。",
"leasePercentZero": "比率が 0 です。リース予算が常に 0 になる可能性があります。",
"leaseCapZero": "上限が 0 です。リースごとの予算が 0 になる可能性があります。"
}
}
},
"section": {
Expand Down
9 changes: 8 additions & 1 deletion messages/ja/settings/providers/form/key.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@
"labelEdit": "API キー(空欄のままにすると変更しません)",
"leaveEmpty": "(空欄のままにすると変更しません)",
"leaveEmptyDesc": "空欄のままにすると既存のキーを保持します",
"placeholder": "API キーを入力"
"placeholder": "API キーを入力",
"warnings": {
"looks_like_auth_header": "リクエストヘッダー(例: Bearer/Authorization/x-api-key)を貼り付けたようです。キー本体のみを入力してください。",
"wrapped_in_quotes": "前後が引用符で囲まれています。通常、引用符は不要です。",
"contains_non_ascii": "非 ASCII 文字を含んでいます。API キーとしては一般的ではありません。",
"contains_whitespace": "空白文字(スペース/改行)を含んでいます。API キーとしては一般的ではありません。",
"contains_uncommon_ascii": "一般的でない記号を含んでいます。API キーとしては一般的ではありません。"
}
}
3 changes: 2 additions & 1 deletion messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,8 @@
"expiresAt": {
"label": "Срок действия",
"placeholder": "Оставьте пустым для бессрочного",
"description": "Пользователь будет автоматически отключен после истечения срока"
"description": "Пользователь будет автоматически отключен после истечения срока",
"pastWarning": "Выбранная дата в прошлом. После сохранения пользователь сразу станет просроченным и будет отключен."
},
"allowedClients": {
"label": "Разрешённые клиенты",
Expand Down
8 changes: 7 additions & 1 deletion messages/ru/settings/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@
"leasePercentMonthly": "Процент аренды месячного окна",
"leasePercentMonthlyDesc": "Процент месячного лимита для каждого среза аренды (0-1)",
"leaseCapUsd": "Предел аренды (USD)",
"leaseCapUsdDesc": "Максимальный абсолютный предел на срез аренды в долларах США, оставьте пустым для отсутствия ограничения"
"leaseCapUsdDesc": "Максимальный абсолютный предел на срез аренды в долларах США, оставьте пустым для отсутствия ограничения",
"warnings": {
"dbRefreshIntervalTooLow": "Интервал {value}s. Это может увеличить нагрузку на БД.",
"dbRefreshIntervalTooHigh": "Интервал {value}s. Обновление квот/лимитов может запаздывать.",
"leasePercentZero": "Процент равен 0. Бюджет аренды может всегда быть 0.",
"leaseCapZero": "Предел аренды равен 0. Бюджет на срез может быть 0."
}
}
},
"section": {
Expand Down
9 changes: 8 additions & 1 deletion messages/ru/settings/providers/form/key.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@
"labelEdit": "API ключ (Оставьте пустым, чтобы не менять)",
"leaveEmpty": "(Оставьте пустым, чтобы не менять)",
"leaveEmptyDesc": "Пустое значение — без изменений",
"placeholder": "Введите API ключ"
"placeholder": "Введите API ключ",
"warnings": {
"looks_like_auth_header": "Похоже, вы вставили заголовок запроса (например, Bearer/Authorization/x-api-key). Введите только значение ключа.",
"wrapped_in_quotes": "Обрамлено кавычками. Обычно кавычки не нужны.",
"contains_non_ascii": "Содержит не-ASCII символы. Для API ключей это обычно нетипично.",
"contains_whitespace": "Содержит пробелы/переносы строк. Для API ключей это обычно нетипично.",
"contains_uncommon_ascii": "Содержит нетипичные символы. Для API ключей это обычно нетипично."
}
}
3 changes: 2 additions & 1 deletion messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,8 @@
"expiresAt": {
"label": "过期时间",
"placeholder": "留空表示永不过期",
"description": "用户过期后将自动禁用"
"description": "用户过期后将自动禁用",
"pastWarning": "选择的日期已在过去,保存后用户将立即过期并被禁用。"
},
"allowedClients": {
"label": "允许的客户端",
Expand Down
8 changes: 7 additions & 1 deletion messages/zh-CN/settings/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@
"leasePercentMonthly": "每月窗口租约比例",
"leasePercentMonthlyDesc": "每次租约切片占每月限额的比例(0-1)",
"leaseCapUsd": "租约上限(USD)",
"leaseCapUsdDesc": "单次租约切片的绝对上限(美元),为空则不限制"
"leaseCapUsdDesc": "单次租约切片的绝对上限(美元),为空则不限制",
"warnings": {
"dbRefreshIntervalTooLow": "当前刷新间隔为 {value}s,可能增加 DB 压力。",
"dbRefreshIntervalTooHigh": "当前刷新间隔为 {value}s,配额/限额状态可能更新不及时。",
"leasePercentZero": "当前比例为 0,可能导致租约预算始终为 0。",
"leaseCapZero": "租约上限为 0 可能导致单次租约预算为 0。"
}
}
}
}
9 changes: 8 additions & 1 deletion messages/zh-CN/settings/providers/form/key.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@
"leaveEmpty": "(留空不更改)",
"placeholder": "输入 API 密钥",
"leaveEmptyDesc": "留空则不更改密钥",
"currentKey": "当前密钥: {key}"
"currentKey": "当前密钥: {key}",
"warnings": {
"looks_like_auth_header": "看起来像粘贴了请求头(如 Bearer/Authorization/x-api-key)。请仅填写 Key 本身。",
"wrapped_in_quotes": "检测到首尾引号,通常不需要引号。",
"contains_non_ascii": "包含非 ASCII 字符(如中文),通常不是常见 API Key。",
"contains_whitespace": "包含空白字符(空格/换行),通常不是常见 API Key。",
"contains_uncommon_ascii": "包含不常见符号,通常不是常见 API Key。"
}
}
3 changes: 2 additions & 1 deletion messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,8 @@
"expiresAt": {
"label": "到期時間",
"placeholder": "留空表示永不過期",
"description": "使用者過期後將自動停用"
"description": "使用者過期後將自動停用",
"pastWarning": "選擇的日期已在過去,儲存後使用者將立即到期並被停用。"
},
"allowedClients": {
"label": "允許的用戶端",
Expand Down
8 changes: 7 additions & 1 deletion messages/zh-TW/settings/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@
"leasePercentMonthly": "每月窗口租約比例",
"leasePercentMonthlyDesc": "每次租約切片佔每月限額的比例(0-1)",
"leaseCapUsd": "租約上限(USD)",
"leaseCapUsdDesc": "單次租約切片的絕對上限(美元),為空則不限制"
"leaseCapUsdDesc": "單次租約切片的絕對上限(美元),為空則不限制",
"warnings": {
"dbRefreshIntervalTooLow": "目前刷新間隔為 {value}s,可能增加 DB 壓力。",
"dbRefreshIntervalTooHigh": "目前刷新間隔為 {value}s,配額/限額狀態可能更新不及時。",
"leasePercentZero": "目前比例為 0,可能導致租約預算始終為 0。",
"leaseCapZero": "租約上限為 0 可能導致單次租約預算為 0。"
}
}
},
"section": {
Expand Down
9 changes: 8 additions & 1 deletion messages/zh-TW/settings/providers/form/key.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@
"labelEdit": "API 金鑰(留空不變更)",
"leaveEmpty": "(留空不變更)",
"leaveEmptyDesc": "留空則不變更金鑰",
"placeholder": "輸入 API 金鑰"
"placeholder": "輸入 API 金鑰",
"warnings": {
"looks_like_auth_header": "看起來像貼上了請求標頭(例如 Bearer/Authorization/x-api-key)。請只填入 Key 本身。",
"wrapped_in_quotes": "偵測到首尾引號,通常不需要引號。",
"contains_non_ascii": "包含非 ASCII 字元(例如中文),通常不是常見 API Key。",
"contains_whitespace": "包含空白字元(空格/換行),通常不是常見 API Key。",
"contains_uncommon_ascii": "包含不常見符號,通常不是常見 API Key。"
}
}
32 changes: 21 additions & 11 deletions src/app/[locale]/dashboard/_components/user/forms/user-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useState, useTransition } from "react";
import { useEffect, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { z } from "zod";
import { getAvailableProviderGroups } from "@/actions/providers";
Expand All @@ -10,11 +10,13 @@ import { DatePickerField } from "@/components/form/date-picker-field";
import { ArrayTagInputField, TagInputField, TextField } from "@/components/form/form-field";
import { DialogFormLayout, FormGrid } from "@/components/form/form-layout";
import { Checkbox } from "@/components/ui/checkbox";
import { InlineWarning } from "@/components/ui/inline-warning";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { PROVIDER_GROUP } from "@/lib/constants/provider.constants";
import { USER_LIMITS } from "@/lib/constants/user.constants";
import { useZodForm } from "@/lib/hooks/use-zod-form";
import { formatDateToLocalYmd, parseYmdToLocalEndOfDay } from "@/lib/utils/date-input";
import { getErrorMessage } from "@/lib/utils/error-messages";
import { setZodErrorMap } from "@/lib/utils/zod-i18n";
import { CreateUserSchema } from "@/lib/validation/schemas";
Expand Down Expand Up @@ -99,20 +101,19 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
limitTotalUsd: user?.limitTotalUsd ?? null,
limitConcurrentSessions: user?.limitConcurrentSessions ?? null,
isEnabled: user?.isEnabled ?? true,
expiresAt: user?.expiresAt ? user.expiresAt.toISOString().split("T")[0] : "",
expiresAt: user?.expiresAt ? formatDateToLocalYmd(user.expiresAt) : "",
allowedClients: user?.allowedClients || [],
allowedModels: user?.allowedModels || [],
},
onSubmit: async (data) => {
// 将纯日期转换为当天结束时间(本地时区 23:59:59.999),避免默认 UTC 零点导致提前过期
const toEndOfDay = (dateStr: string) => {
const d = new Date(dateStr);
d.setHours(23, 59, 59, 999);
return d;
};

startTransition(async () => {
try {
const expiresAt = data.expiresAt ? parseYmdToLocalEndOfDay(data.expiresAt) : null;
if (data.expiresAt && !expiresAt) {
toast.error(tErrors("INVALID_FORMAT", { field: tErrors("EXPIRES_AT_FIELD") }));
return;
}

let res;
if (isEdit && user?.id) {
res = await editUser(user.id, {
Expand All @@ -128,7 +129,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
limitTotalUsd: data.limitTotalUsd,
limitConcurrentSessions: data.limitConcurrentSessions,
isEnabled: data.isEnabled,
expiresAt: data.expiresAt ? toEndOfDay(data.expiresAt) : null,
expiresAt,
allowedClients: data.allowedClients,
allowedModels: data.allowedModels,
});
Expand All @@ -146,7 +147,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
limitTotalUsd: data.limitTotalUsd,
limitConcurrentSessions: data.limitConcurrentSessions,
isEnabled: data.isEnabled,
expiresAt: data.expiresAt ? toEndOfDay(data.expiresAt) : null,
expiresAt,
allowedClients: data.allowedClients,
allowedModels: data.allowedModels,
});
Expand Down Expand Up @@ -176,6 +177,14 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
// Use dashboard translations for form
const tForm = useTranslations("dashboard.userForm");

const expiresAtPastWarning = useMemo(() => {
const expiresAtYmd = form.values.expiresAt ?? "";
if (!expiresAtYmd) return null;
const date = parseYmdToLocalEndOfDay(expiresAtYmd);
if (!date) return null;
return date.getTime() <= Date.now() ? tForm("expiresAt.pastWarning") : null;
}, [form.values.expiresAt, tForm]);

return (
<DialogFormLayout
config={{
Expand Down Expand Up @@ -352,6 +361,7 @@ export function UserForm({ user, onSuccess, currentUser }: UserFormProps) {
error={form.getFieldProps("expiresAt").error}
touched={form.getFieldProps("expiresAt").touched}
/>
{expiresAtPastWarning && <InlineWarning>{expiresAtPastWarning}</InlineWarning>}

{/* Allowed Clients (CLI/IDE restrictions) */}
<div className="space-y-3">
Expand Down
Loading
Loading