fix
This commit is contained in:
@@ -46,8 +46,9 @@ export const authApi = {
|
||||
const { data } = await api.post("/auth/register", { email, username, password });
|
||||
return data;
|
||||
},
|
||||
login: async (email: string, password: string): Promise<TokenOut> => {
|
||||
const { data } = await api.post("/auth/login", { email, password });
|
||||
login: async (login: string, password: string): Promise<TokenOut> => {
|
||||
// `login` accepts either email or username.
|
||||
const { data } = await api.post("/auth/login", { login, password });
|
||||
return data;
|
||||
},
|
||||
me: async (): Promise<User> => {
|
||||
@@ -93,6 +94,10 @@ export const adminApi = {
|
||||
const { data } = await api.get("/admin/users");
|
||||
return data;
|
||||
},
|
||||
setUserActive: async (userId: string, isActive: boolean): Promise<any> => {
|
||||
const { data } = await api.post(`/admin/users/${userId}/set-active`, { is_active: isActive });
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
export const presetsApi = {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const en = {
|
||||
register_title: "Register",
|
||||
email: "Email",
|
||||
username: "Username",
|
||||
login_or_email: "Email or username",
|
||||
password: "Password",
|
||||
login_btn: "Login",
|
||||
register_btn: "Register",
|
||||
@@ -102,8 +103,9 @@ export const en = {
|
||||
summary_messages: "Messages per summary",
|
||||
max_tokens_total: "Context token budget",
|
||||
trigger_settings: "Deferred triggers",
|
||||
trigger_settings_desc: "Fire when in-world time advances (no polling)",
|
||||
triggers_enabled: "Enabled",
|
||||
triggers_check_interval: "Check interval (sec)",
|
||||
triggers_enabled_desc: "Triggers fire automatically inside the orchestrator when world time advances past their scheduled fire_at.",
|
||||
embedding_settings: "Embeddings (RAG)",
|
||||
embedding_provider: "Provider",
|
||||
embedding_provider_hash: "Hash (offline fallback, no semantics)",
|
||||
@@ -121,6 +123,9 @@ export const en = {
|
||||
saved: "Saved!",
|
||||
llm_logs: "LLM logs",
|
||||
users: "Users",
|
||||
users_actions: "Actions",
|
||||
users_ban: "Ban",
|
||||
users_unban: "Unban",
|
||||
back: "Back",
|
||||
},
|
||||
glossary: {
|
||||
|
||||
@@ -12,8 +12,11 @@ i18n
|
||||
ru: { translation: ru },
|
||||
en: { translation: en },
|
||||
},
|
||||
fallbackLng: "ru",
|
||||
supportedLngs: ["ru", "en"],
|
||||
// English is the default. The user can switch via the language picker
|
||||
// in the navbar; the choice is cached in localStorage and overrides
|
||||
// browser settings on subsequent visits.
|
||||
fallbackLng: "en",
|
||||
supportedLngs: ["en", "ru"],
|
||||
interpolation: { escapeValue: false },
|
||||
detection: {
|
||||
order: ["localStorage", "navigator"],
|
||||
|
||||
@@ -17,6 +17,7 @@ export const ru = {
|
||||
register_title: "Регистрация",
|
||||
email: "Email",
|
||||
username: "Имя пользователя",
|
||||
login_or_email: "Email или имя пользователя",
|
||||
password: "Пароль",
|
||||
login_btn: "Войти",
|
||||
register_btn: "Зарегистрироваться",
|
||||
@@ -102,8 +103,9 @@ export const ru = {
|
||||
summary_messages: "Сообщений на сводку",
|
||||
max_tokens_total: "Бюджет токенов контекста",
|
||||
trigger_settings: "Отложенные триггеры",
|
||||
trigger_settings_desc: "Срабатывают при сдвиге внутриигрового времени (без поллинга)",
|
||||
triggers_enabled: "Включены",
|
||||
triggers_check_interval: "Интервал проверки (сек)",
|
||||
triggers_enabled_desc: "Триггеры срабатывают автоматически внутри оркестратора, когда время мира проходит запланированное fire_at.",
|
||||
embedding_settings: "Эмбеддинги (RAG)",
|
||||
embedding_provider: "Провайдер",
|
||||
embedding_provider_hash: "Hash (офлайн-фолбэк, без семантики)",
|
||||
@@ -121,6 +123,9 @@ export const ru = {
|
||||
saved: "Сохранено!",
|
||||
llm_logs: "Логи LLM",
|
||||
users: "Пользователи",
|
||||
users_actions: "Действия",
|
||||
users_ban: "Забанить",
|
||||
users_unban: "Разбанить",
|
||||
back: "Назад",
|
||||
},
|
||||
glossary: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { LlmLog, SettingsOut } from "@/types";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { Card, CardBody, CardHeader } from "@/components/ui/Card";
|
||||
import { Save, ArrowLeft, Activity, Users, Zap } from "lucide-react";
|
||||
import { Save, ArrowLeft, Activity, Users, Zap, Ban, CheckCircle2 } from "lucide-react";
|
||||
|
||||
export function AdminPanelPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,6 +22,7 @@ export function AdminPanelPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [embeddingTest, setEmbeddingTest] = useState<null | { ok: boolean; msg: string }>(null);
|
||||
const [testingEmbeddings, setTestingEmbeddings] = useState(false);
|
||||
const [userActionError, setUserActionError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -110,6 +111,16 @@ export function AdminPanelPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleUserActive = async (userId: string, currentActive: boolean) => {
|
||||
setUserActionError("");
|
||||
try {
|
||||
const updated = await adminApi.setUserActive(userId, !currentActive);
|
||||
setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, is_active: updated.is_active } : u)));
|
||||
} catch (err: any) {
|
||||
setUserActionError(err.response?.data?.detail || t("errors.unknown"));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-8 text-center text-ink-400">{t("common.loading")}</div>;
|
||||
}
|
||||
@@ -302,23 +313,17 @@ export function AdminPanelPage() {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader title={t("admin.trigger_settings")} />
|
||||
<CardHeader title={t("admin.trigger_settings")} subtitle={t("admin.trigger_settings_desc")} />
|
||||
<CardBody>
|
||||
<div className="grid grid-cols-2 gap-3 items-end">
|
||||
<label className="flex items-center gap-2 text-xs text-ink-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!values["triggers.enabled"]}
|
||||
onChange={(e) => setValues({ ...values, "triggers.enabled": e.target.checked })}
|
||||
/>
|
||||
{t("admin.triggers_enabled")}
|
||||
</label>
|
||||
<NumberInput
|
||||
label={t("admin.triggers_check_interval")}
|
||||
value={values["triggers.check_interval"]}
|
||||
onChange={(v) => setValues({ ...values, "triggers.check_interval": v })}
|
||||
<label className="flex items-center gap-2 text-xs text-ink-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!values["triggers.enabled"]}
|
||||
onChange={(e) => setValues({ ...values, "triggers.enabled": e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
{t("admin.triggers_enabled")}
|
||||
</label>
|
||||
<p className="text-xs text-ink-500 mt-2">{t("admin.triggers_enabled_desc")}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@@ -382,6 +387,9 @@ export function AdminPanelPage() {
|
||||
<Card>
|
||||
<CardHeader title={t("admin.users")} subtitle={`${users.length} users`} />
|
||||
<CardBody>
|
||||
{userActionError && (
|
||||
<p className="text-sm text-red-400 mb-3">{userActionError}</p>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
@@ -390,7 +398,8 @@ export function AdminPanelPage() {
|
||||
<th className="py-2 pr-3">Username</th>
|
||||
<th className="py-2 pr-3">Role</th>
|
||||
<th className="py-2 pr-3">Active</th>
|
||||
<th className="py-2">Created</th>
|
||||
<th className="py-2 pr-3">Created</th>
|
||||
<th className="py-2 text-right">{t("admin.users_actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -412,9 +421,32 @@ export function AdminPanelPage() {
|
||||
<span className="text-red-400">●</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 text-ink-400">
|
||||
<td className="py-2 pr-3 text-ink-400">
|
||||
{new Date(u.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="py-2 text-right">
|
||||
{u.is_admin ? (
|
||||
<span className="text-ink-500 text-xs">—</span>
|
||||
) : u.is_active ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toggleUserActive(u.id, u.is_active)}
|
||||
>
|
||||
<Ban size={12} className="mr-1" />
|
||||
{t("admin.users_ban")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toggleUserActive(u.id, u.is_active)}
|
||||
>
|
||||
<CheckCircle2 size={12} className="mr-1" />
|
||||
{t("admin.users_unban")}
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -55,12 +55,6 @@ export function HomePage() {
|
||||
desc="RU / EN"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center text-sm text-ink-500">
|
||||
<Link to="/admin/setup" className="hover:text-accent-400 underline">
|
||||
{t("auth.admin_setup_title")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function LoginPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { setAuth } = useAuthStore();
|
||||
const [email, setEmail] = useState("");
|
||||
const [login, setLogin] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -21,7 +21,8 @@ export function LoginPage() {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const { access_token, user } = await authApi.login(email, password);
|
||||
// `login` accepts either email or username.
|
||||
const { access_token, user } = await authApi.login(login, password);
|
||||
setAuth(access_token, user);
|
||||
navigate("/dashboard");
|
||||
} catch (err: any) {
|
||||
@@ -38,13 +39,14 @@ export function LoginPage() {
|
||||
<CardBody>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<Input
|
||||
label={t("auth.email")}
|
||||
type="email"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
label={t("auth.login_or_email")}
|
||||
type="text"
|
||||
name="login"
|
||||
value={login}
|
||||
onChange={(e) => setLogin(e.target.value)}
|
||||
required
|
||||
autoComplete="email"
|
||||
autoComplete="username"
|
||||
placeholder="alice / alice@example.com"
|
||||
/>
|
||||
<Input
|
||||
label={t("auth.password")}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function WorldBuilderPage() {
|
||||
const fromScratch = location.state?.fromScratch as boolean | undefined;
|
||||
|
||||
const [worldName, setWorldName] = useState("");
|
||||
const [language, setLanguage] = useState(i18n.language === "en" ? "en" : "ru");
|
||||
const [language, setLanguage] = useState(i18n.language === "ru" ? "ru" : "en");
|
||||
const [settingBrief, setSettingBrief] = useState("");
|
||||
const [characterBrief, setCharacterBrief] = useState("");
|
||||
const [rulesBrief, setRulesBrief] = useState("");
|
||||
|
||||
Reference in New Issue
Block a user