mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
fix: improve the behavior for the web app
This commit is contained in:
@@ -9,6 +9,7 @@ import { cn } from "@/shared/utils/cn";
|
||||
import { APP_CONFIG } from "@/shared/constants/config";
|
||||
import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config";
|
||||
import { LOCALE_FLAGS } from "@/shared/constants/locales";
|
||||
import useUserStore from "@/store/userStore";
|
||||
|
||||
function getLocaleFromCookie() {
|
||||
if (typeof document === "undefined") return "en";
|
||||
@@ -21,6 +22,8 @@ function getLocaleFromCookie() {
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { theme, setTheme, isDark } = useTheme();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const fetchCurrentUser = useUserStore((state) => state.fetchCurrentUser);
|
||||
const [locale, setLocale] = useState("en");
|
||||
const [langOpen, setLangOpen] = useState(false);
|
||||
const [shutdownOpen, setShutdownOpen] = useState(false);
|
||||
@@ -58,6 +61,10 @@ export default function ProfilePage() {
|
||||
const [proxyLoading, setProxyLoading] = useState(false);
|
||||
const [proxyTestLoading, setProxyTestLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) fetchCurrentUser();
|
||||
}, [fetchCurrentUser, user]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocale(getLocaleFromCookie());
|
||||
}, [langOpen]);
|
||||
@@ -532,6 +539,13 @@ export default function ProfilePage() {
|
||||
throw new Error(data.error || "Failed to import database");
|
||||
}
|
||||
|
||||
// A backup may replace the current account and its permissions. The API
|
||||
// clears the session cookie; redirect immediately to prevent stale data.
|
||||
if (data.requiresLogin) {
|
||||
window.location.assign("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
await reloadSettings();
|
||||
setDbStatus({ type: "success", message: "Database imported successfully" });
|
||||
} catch (err) {
|
||||
@@ -610,46 +624,51 @@ export default function ProfilePage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-4 border-t border-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg bg-bg border border-border gap-2">
|
||||
<div>
|
||||
<p className="font-medium text-sm sm:text-base">Database Location</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted font-mono break-all">~/.9router/db/data.sqlite</p>
|
||||
{user?.role === "admin" ? (
|
||||
<div className="flex flex-col gap-3 pt-4 border-t border-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg bg-bg border border-border gap-2">
|
||||
<div>
|
||||
<p className="font-medium text-sm sm:text-base">Database Location</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted font-mono break-all">~/.9router/db/data.sqlite</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="download"
|
||||
onClick={() => setDbAuth({ open: true, mode: "export", password: "" })}
|
||||
loading={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Download Backup
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
icon="upload"
|
||||
onClick={() => importFileRef.current?.click()}
|
||||
disabled={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Import Backup
|
||||
</Button>
|
||||
<input
|
||||
ref={importFileRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
className="hidden"
|
||||
onChange={handleImportDatabase}
|
||||
/>
|
||||
</div>
|
||||
{dbStatus.message && (
|
||||
<p className={`text-sm ${dbStatus.type === "error" ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
|
||||
{dbStatus.message}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="download"
|
||||
onClick={() => setDbAuth({ open: true, mode: "export", password: "" })}
|
||||
loading={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Download Backup
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
icon="upload"
|
||||
onClick={() => importFileRef.current?.click()}
|
||||
disabled={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Import Backup
|
||||
</Button>
|
||||
<input
|
||||
ref={importFileRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
className="hidden"
|
||||
onChange={handleImportDatabase}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Backups include connected-model availability settings from the Models page.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{dbStatus.message && (
|
||||
<p className={`text-sm ${dbStatus.type === "error" ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
|
||||
{dbStatus.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
|
||||
{/* Language */}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { exportDb, getSettings, importDb } from "@/lib/localDb";
|
||||
import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy";
|
||||
import { verifyCurrentDashboardUserPassword } from "@/lib/auth/currentUser";
|
||||
import { requireAdminUser, verifyCurrentDashboardUserPassword } from "@/lib/auth/currentUser";
|
||||
import { clearDashboardAuthCookie } from "@/lib/auth/dashboardSession";
|
||||
|
||||
const CLI_TOKEN_HEADER = "x-9r-cli-token";
|
||||
const PASSWORD_HEADER = "x-9r-password";
|
||||
@@ -11,14 +12,29 @@ function isCliRequest(request) {
|
||||
return Boolean(request.headers.get(CLI_TOKEN_HEADER));
|
||||
}
|
||||
|
||||
function getAuthorizationErrorResponse(error) {
|
||||
if (error.message === "Unauthorized") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (error.message === "Forbidden") {
|
||||
return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function GET(request) {
|
||||
try {
|
||||
if (!isCliRequest(request) && !(await verifyCurrentDashboardUserPassword(request.headers.get(PASSWORD_HEADER)))) {
|
||||
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
|
||||
if (!isCliRequest(request)) {
|
||||
await requireAdminUser();
|
||||
if (!(await verifyCurrentDashboardUserPassword(request.headers.get(PASSWORD_HEADER)))) {
|
||||
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
const payload = await exportDb();
|
||||
return NextResponse.json(payload);
|
||||
} catch (error) {
|
||||
const authorizationError = getAuthorizationErrorResponse(error);
|
||||
if (authorizationError) return authorizationError;
|
||||
console.log("Error exporting database:", error);
|
||||
return NextResponse.json({ error: "Failed to export database" }, { status: 500 });
|
||||
}
|
||||
@@ -27,8 +43,11 @@ export async function GET(request) {
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { password, ...payload } = await request.json();
|
||||
if (!isCliRequest(request) && !(await verifyCurrentDashboardUserPassword(password))) {
|
||||
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
|
||||
if (!isCliRequest(request)) {
|
||||
await requireAdminUser();
|
||||
if (!(await verifyCurrentDashboardUserPassword(password))) {
|
||||
return NextResponse.json({ error: "Invalid password" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
await importDb(payload);
|
||||
|
||||
@@ -40,8 +59,21 @@ export async function POST(request) {
|
||||
console.warn("[Settings][DatabaseImport] Failed to re-apply outbound proxy env:", err);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
const response = NextResponse.json({ success: true, requiresLogin: !isCliRequest(request) });
|
||||
|
||||
// The imported database can replace the current account and permissions.
|
||||
// Remove the browser session so all dashboard data is loaded under a new login.
|
||||
if (!isCliRequest(request)) {
|
||||
clearDashboardAuthCookie(response.cookies);
|
||||
response.cookies.delete("oidc_state");
|
||||
response.cookies.delete("oidc_nonce");
|
||||
response.cookies.delete("oidc_code_verifier");
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
const authorizationError = getAuthorizationErrorResponse(error);
|
||||
if (authorizationError) return authorizationError;
|
||||
console.log("Error importing database:", error);
|
||||
return NextResponse.json(
|
||||
{ error: error?.message || "Failed to import database" },
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireUsageDashboardUser } from "@/lib/auth/currentUser";
|
||||
import { getUsageTopologyProviders } from "@/lib/providers/usageTopologyProviders";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/usage/topology-providers
|
||||
* Returns provider types that the request router can currently select.
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireUsageDashboardUser();
|
||||
const providers = await getUsageTopologyProviders();
|
||||
return NextResponse.json({ providers });
|
||||
} catch (error) {
|
||||
if (error?.message === "Unauthorized") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
console.error("[API] Failed to get usage topology providers:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch topology providers" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user