fix: normalize login and legacy menu routes

This commit is contained in:
chengkml
2026-05-16 22:33:53 +08:00
parent c96bb5fa2d
commit 5cf82f3468
9 changed files with 325 additions and 190 deletions
+18 -11
View File
@@ -119,7 +119,7 @@
- 登录页主视觉允许使用装饰性动效(如浮动背景与角色动画),但必须保持登录/注册接口调用链路与鉴权行为不变。
- 首页怪兽交互基线:眼睛跟随鼠标,密码输入框聚焦时主动挪开视线(避免“盯着密码输入”观感)。
- 当前实现位于 `web/src/app/page.tsx`;若后续继续扩展动效,优先抽离样式与展示组件,避免登录业务与视觉代码耦合过深。
- 当前实现位于 `web/src/app/login/page.tsx`;若后续继续扩展动效,优先抽离样式与展示组件,避免登录业务与视觉代码耦合过深。
## AI 聊天口径(2026-04-13
@@ -456,7 +456,7 @@
## 登录页双角色视觉口径(2026-04-23)
- 登录页主视觉已从单怪兽升级为“双角色怪兽”(毛怪 + 大眼仔)构图,实现在 `web/src/app/page.tsx`
- 登录页主视觉已从单怪兽升级为“双角色怪兽”(毛怪 + 大眼仔)构图,当前登录页实现位于 `web/src/app/login/page.tsx`
- 交互基线保持:眼睛跟随鼠标;密码输入时执行避视动作(毛怪转头,大眼仔轻微眯眼)。
- 视觉实现采用纯前端结构与 CSS 动效,不引入外部图片资源,不影响登录/注册链路。
@@ -557,14 +557,15 @@
- 安全边界:
- 前端仅负责显隐与交互;最终权限判定以后端依赖校验为准。
## 首页与登录口径(2026-04-23
## 首页与登录口径(2026-05-16
- `/` 默认作为登录入口页,不再承载“已登录后停留的首页面板”
- 登录态(含刷新会话恢复)进入 `/` 时,前端立即跳转 `/users`,实现“登录后直达后台”
- `/login` 作为规范登录入口;在当前部署 `NEXT_PUBLIC_APP_BASE_PATH=/fl` 下,对外地址为 `/fl/login`
- `/` 不再直接承载登录表单,仅用于重定向到 `/login`
- 登录态(含刷新会话恢复)进入 `/login``/` 时,前端立即跳转 `/users`,实现“登录后直达后台”。
- 后台壳层文案对齐:
- 未登录访问后台时提示“前往登录”(`/`);
- 未登录访问后台时提示“前往登录”(`/login`);
- 账号菜单提供“用户管理”(`/users`)作为默认后台入口。
- 退出登录口径:统一跳转到登录页 `/`(不保留在当前后台路由)。
- 退出登录口径:统一跳转到登录页 `/login`(不保留在当前后台路由)。
## 站点标题口径(2026-04-24
@@ -577,7 +578,7 @@
- 左侧为品牌与机器人主题视觉区;
- 右侧为白色登录卡片(品牌头、表单、主操作按钮、辅助链接)。
- 交互口径保持:
- 登录态进入 `/` 仍自动跳转 `/users`
- 登录态进入 `/login``/` 仍自动跳转 `/users`
- 登录/注册逻辑不变,视觉改造不改变鉴权接口契约。
## 后台账号入口口径(2026-04-23
@@ -615,9 +616,9 @@
- 兼容保留:`fquiz:theme:mode`legacy 四态映射)
- `AI 生成主题` 当前为交互与文案对齐态,未内置站内 AI 主题生成流程;“主题编辑器”默认跳转官方编辑器页。
## 登录页文案口径(2026-04-24
## 登录页文案口径(2026-05-16
- 登录页(`web/src/app/page.tsx`)默认展示文案统一为中文,不再保留英文提示文案。
- 登录页(`web/src/app/login/page.tsx`)默认展示文案统一为中文,不再保留英文提示文案。
## 前端编译口径(2026-04-24
@@ -660,10 +661,16 @@
- 后台“仪表盘”页面已下线,不再作为可访问菜单和默认首页。
- 前端路由口径:
- `/admin``/dashboard` 统一重定向到 `/users`
- 登录态进入 `/` 默认跳转到 `/users`
- 登录态进入 `/login``/` 默认跳转到 `/users`
- `web/src/app/admin/page.tsx` 改为重定向页,不再渲染卡片工作台。
- 后端菜单口径:
- `seed_service.DEFAULT_MENUS` 删除 `dashboard`
## 前端菜单路由兼容口径(2026-05-16)
- 前端公开菜单路由的规范地址统一使用真实页面路径,例如 `/users``/roles``/menus``/system-params``/power-lines`
- 历史别名路径(如 `/user``/role``/menu``/system-param``/power-line``/worker``/tower-model``/file`)由前端路由层自动规范到对应正式地址,避免旧菜单数据或手输地址直接落到 404。
- 后台菜单渲染与菜单管理页默认展示规范化后的 path,减少“菜单能点但高亮错位”或“列表里还是旧地址”的前后不一致。
- `ROLE_MENU_BINDINGS` 删除 admin/user 对 `dashboard` 的绑定;
- `legacy_authz_service``legacy_admin_rbac_service``admin_service``dashboard` 统一加入下线过滤集合,屏蔽历史库残留菜单记录。
+56
View File
@@ -72,3 +72,59 @@
- 风险与关注点:
- 当前在线 `REFRESH_COOKIE_SECURE=true`;若继续通过纯 HTTP IP 访问,浏览器可能仍不会持久化 refresh cookie。若部署后出现“登录后刷新即掉线”,需将当前环境的 `REFRESH_COOKIE_SECURE` 调整为 `false` 或切到 HTTPS 入口。
## Work Log - 登录入口迁移到 `/fl/login`2026-05-16
- 背景:
- 当前前端登录页挂在应用根路径;在生产 basePath=`/fl` 下,对外登录地址表现为 `/fl`
- 需求要求将登录路由显式调整为 `/fl/login`,同时保留旧入口兼容跳转。
- 本次改动:
- `web/src/app/login/page.tsx`
- 新增独立登录页,沿用现有登录表单、记住密码和登录态自动跳转逻辑。
- `web/src/app/page.tsx`
- 根页改为重定向到 `/login`,不再直接承载登录表单。
- `web/src/middleware.ts`
-`/login` 标记为公开直达路由,避免被后台 rewrite 到 `/admin/login`
- `web/src/components/auth-provider.tsx`
- 退出登录后统一跳转到 `/login`
- `web/src/app/admin/layout.tsx`
- 未登录态提示中的“前往登录”入口改为 `/login`
- `MEMORY.md`
- 同步更新长期路由口径:`/login` 为规范登录入口,`/` 仅做跳转。
- 验证:
- `npm --prefix web run lint -- src/app/page.tsx src/app/login/page.tsx src/middleware.ts src/components/auth-provider.tsx src/app/admin/layout.tsx`
通过;仅保留既有 `<img>` 使用警告,无新增 lint error。
- 已本地确认 Next.js 16 安装包的 app redirect 渲染链会按 basePath 处理重定向,当前 `redirect("/login")` 口径可用于 `/fl/login` 场景。
- `npm --prefix web run build` 已启动并进入 `next build --webpack`,但在本次会话等待窗口内未返回最终退出码;期间未出现与本次改动相关的编译报错输出。
- 风险与关注点:
- 仓库内仍有多处“返回首页”按钮保留指向 `/`;当前会经过根页跳转到 `/login`,功能不受影响,但若后续希望所有入口都直接落到规范地址,可再统一替换为 `/login`
## Work Log - 菜单旧路由兼容修复(2026-05-16)
- 背景:
- 用户反馈 `/fl/user` 按预期应进入用户页面,但实际返回 404。
- 当前前端真实页面路径为 `/users`,而部分历史菜单 path/手输地址仍可能使用旧别名(如 `/user``/role``/menu`)。
- 本次改动:
- `web/src/lib/app-route-path.ts`
- 新增统一路由规范化 helper,集中维护旧菜单别名到正式公开路由的映射。
- `web/src/middleware.ts`
-`/user``/role``/menu` 等旧地址,以及 `/admin/*` 旧入口,统一先重定向到规范公开路由,再走后台 rewrite。
- `web/src/app/admin/layout.tsx`
- 后台菜单树 path 统一先做规范化,避免菜单点击后高亮丢失或 path 仍指向旧地址。
- `web/src/app/admin/menus/page.tsx`
- 菜单管理页读取菜单列表时展示规范化 path;提交菜单编辑时也会把旧别名收敛到正式地址。
- `MEMORY.md`
- 补充“前端菜单路由兼容口径”,记录规范地址与旧别名自动收敛规则。
- 验证:
- `npm --prefix web run lint -- src/lib/app-route-path.ts src/middleware.ts src/app/admin/layout.tsx src/app/admin/menus/page.tsx`
通过;仅保留既有 `<img>` warning,无新增 lint error。
- `git diff --check -- web/src/lib/app-route-path.ts web/src/middleware.ts web/src/app/admin/layout.tsx web/src/app/admin/menus/page.tsx`
通过。
- 风险与关注点:
- 当前别名映射基于已知历史菜单路径补齐;若库里还存在其他非常规旧 path,仍需按实际反馈继续补别名表。
+5 -12
View File
@@ -37,6 +37,7 @@ import {
import { useAuth } from "@/components/auth-provider";
import { useTopicSubscription } from "@/hooks/use-topic-subscription";
import { readApiError } from "@/lib/api";
import { normalizeAppRoutePath } from "@/lib/app-route-path";
import type { MenuTreeItem } from "@/types/auth";
import { useThemeAppearance } from "@/components/ui-antd";
import { withBasePath } from "@/lib/base-path";
@@ -60,16 +61,7 @@ function ThemeIcon() {
}
function normalizeAdminPath(path: string | null): string | null {
if (!path) {
return path;
}
if (path === "/admin" || path === "/admin/") {
return "/users";
}
if (path.startsWith("/admin/")) {
return path.slice("/admin".length);
}
return path;
return normalizeAppRoutePath(path);
}
function normalizeMenuTreePaths(items: MenuTreeItem[]): MenuTreeItem[] {
@@ -152,7 +144,8 @@ function AdminCenteredState({ children }: { children: ReactNode }) {
}
export default function AdminLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
const rawPathname = usePathname();
const pathname = normalizeAppRoutePath(rawPathname) ?? rawPathname;
const screens = Grid.useBreakpoint();
const isDesktop = screens.md === true;
const { user, initializing, fetchWithAuth, logout } = useAuth();
@@ -340,7 +333,7 @@ export default function AdminLayout({ children }: { children: ReactNode }) {
subTitle="登录后才能访问后台工作台。"
extra={(
<Button type="primary">
<Link href="/"></Link>
<Link href="/login"></Link>
</Button>
)}
/>
+10 -2
View File
@@ -28,6 +28,7 @@ import type { CSSProperties, ComponentType } from "react";
import { useAuth } from "@/components/auth-provider";
import { useTopicSubscription } from "@/hooks/use-topic-subscription";
import { readApiError } from "@/lib/api";
import { normalizeAppRoutePath } from "@/lib/app-route-path";
import type { MenuItem, MenuListResponse } from "@/types/auth";
const AntCard = Card as unknown as ComponentType<CardProps>;
@@ -82,6 +83,13 @@ function compareMenuIds(a: string, b: string): number {
return a.localeCompare(b, "zh-CN");
}
function normalizeMenuItemPath(menu: MenuItem): MenuItem {
return {
...menu,
path: normalizeAppRoutePath(menu.path),
};
}
export default function AdminMenusPage() {
const { user, initializing, fetchWithAuth, hasPermission } = useAuth();
const { message: messageApi, modal } = App.useApp();
@@ -166,7 +174,7 @@ export default function AdminMenusPage() {
}
const payload = (await response.json()) as MenuListResponse;
setMenus(payload.items);
setMenus(payload.items.map(normalizeMenuItemPath));
setLoading(false);
}, [canRead, fetchWithAuth]);
@@ -227,7 +235,7 @@ export default function AdminMenusPage() {
const payload = {
code: values.code.trim(),
name: values.name.trim(),
path: values.path?.trim() ? values.path.trim() : null,
path: normalizeAppRoutePath(values.path?.trim() ? values.path.trim() : null),
icon: values.icon?.trim() ? values.icon.trim() : null,
parent_id: values.parent_id?.trim() ? values.parent_id.trim() : null,
type: values.type,
+153
View File
@@ -0,0 +1,153 @@
"use client";
import { IdcardOutlined, LockOutlined } from "@ant-design/icons";
import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Alert, Button, Checkbox, Input, Space, Typography } from "antd";
import { useAuth } from "@/components/auth-provider";
import { Card } from "@/components/ui-antd";
import { withBasePath } from "@/lib/base-path";
const LOGIN_REMEMBER_KEY = "login.remember";
const LOGIN_USER_ID_KEY = "login.user_id";
const LOGIN_PASSWORD_KEY = "login.password";
export default function LoginPage() {
const router = useRouter();
const { user, initializing, login } = useAuth();
const [userId, setUserId] = useState("");
const [password, setPassword] = useState("");
const [rememberPassword, setRememberPassword] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!initializing && user) {
router.replace("/users");
}
}, [initializing, router, user]);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const remembered = window.localStorage.getItem(LOGIN_REMEMBER_KEY) === "1";
if (!remembered) {
return;
}
setRememberPassword(true);
setUserId(window.localStorage.getItem(LOGIN_USER_ID_KEY) ?? "");
setPassword(window.localStorage.getItem(LOGIN_PASSWORD_KEY) ?? "");
}, []);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError("");
setBusy(true);
try {
const normalizedUserId = userId.trim();
await login(normalizedUserId, password);
if (typeof window !== "undefined") {
if (rememberPassword) {
window.localStorage.setItem(LOGIN_REMEMBER_KEY, "1");
window.localStorage.setItem(LOGIN_USER_ID_KEY, normalizedUserId);
window.localStorage.setItem(LOGIN_PASSWORD_KEY, password);
} else {
window.localStorage.removeItem(LOGIN_REMEMBER_KEY);
window.localStorage.removeItem(LOGIN_USER_ID_KEY);
window.localStorage.removeItem(LOGIN_PASSWORD_KEY);
}
}
setPassword("");
} catch (submitError) {
const message = submitError instanceof Error ? submitError.message : "未知错误";
setError(message);
} finally {
setBusy(false);
}
};
if (initializing) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-6 py-20">
<Typography.Text type="secondary">...</Typography.Text>
</main>
);
}
if (user) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-6 py-20">
<Typography.Text type="secondary">...</Typography.Text>
</main>
);
}
return (
<main className="flex min-h-screen items-center justify-center bg-[var(--fquiz-theme-bg-layout)] px-4 py-8">
<Card className="w-full max-w-[360px]">
<Space direction="vertical" size={20} className="w-full">
<div className="flex justify-center">
<img
src={withBasePath("/favicon.ico")}
alt="高压电塔图标"
width={72}
height={72}
className="h-[72px] w-[72px]"
/>
</div>
<Typography.Title level={3} className="!mb-0 !text-center">
</Typography.Title>
<form className="space-y-4" onSubmit={handleSubmit}>
<Input
size="large"
value={userId}
prefix={<IdcardOutlined />}
placeholder="用户 ID"
onChange={(event: ChangeEvent<HTMLInputElement>) => setUserId(event.currentTarget.value)}
autoComplete="username"
required
/>
<Input.Password
size="large"
value={password}
prefix={<LockOutlined />}
placeholder="密码"
onChange={(event: ChangeEvent<HTMLInputElement>) => setPassword(event.currentTarget.value)}
autoComplete="current-password"
minLength={1}
maxLength={128}
required
/>
<div className="flex items-center justify-between">
<Checkbox
checked={rememberPassword}
onChange={(event) => setRememberPassword(event.target.checked)}
>
</Checkbox>
</div>
<Button block size="large" type="primary" htmlType="submit" loading={busy}>
</Button>
</form>
{error && <Alert showIcon type="error" message={error} />}
</Space>
</Card>
</main>
);
}
+2 -150
View File
@@ -1,153 +1,5 @@
"use client";
import { IdcardOutlined, LockOutlined } from "@ant-design/icons";
import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Alert, Button, Checkbox, Input, Space, Typography } from "antd";
import { useAuth } from "@/components/auth-provider";
import { Card } from "@/components/ui-antd";
import { withBasePath } from "@/lib/base-path";
const LOGIN_REMEMBER_KEY = "login.remember";
const LOGIN_USER_ID_KEY = "login.user_id";
const LOGIN_PASSWORD_KEY = "login.password";
import { redirect } from "next/navigation";
export default function Home() {
const router = useRouter();
const { user, initializing, login } = useAuth();
const [userId, setUserId] = useState("");
const [password, setPassword] = useState("");
const [rememberPassword, setRememberPassword] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!initializing && user) {
router.replace("/users");
}
}, [initializing, router, user]);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const remembered = window.localStorage.getItem(LOGIN_REMEMBER_KEY) === "1";
if (!remembered) {
return;
}
setRememberPassword(true);
setUserId(window.localStorage.getItem(LOGIN_USER_ID_KEY) ?? "");
setPassword(window.localStorage.getItem(LOGIN_PASSWORD_KEY) ?? "");
}, []);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError("");
setBusy(true);
try {
const normalizedUserId = userId.trim();
await login(normalizedUserId, password);
if (typeof window !== "undefined") {
if (rememberPassword) {
window.localStorage.setItem(LOGIN_REMEMBER_KEY, "1");
window.localStorage.setItem(LOGIN_USER_ID_KEY, normalizedUserId);
window.localStorage.setItem(LOGIN_PASSWORD_KEY, password);
} else {
window.localStorage.removeItem(LOGIN_REMEMBER_KEY);
window.localStorage.removeItem(LOGIN_USER_ID_KEY);
window.localStorage.removeItem(LOGIN_PASSWORD_KEY);
}
}
setPassword("");
} catch (submitError) {
const message = submitError instanceof Error ? submitError.message : "未知错误";
setError(message);
} finally {
setBusy(false);
}
};
if (initializing) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-6 py-20">
<Typography.Text type="secondary">...</Typography.Text>
</main>
);
}
if (user) {
return (
<main className="mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-6 py-20">
<Typography.Text type="secondary">...</Typography.Text>
</main>
);
}
return (
<main className="flex min-h-screen items-center justify-center bg-[var(--fquiz-theme-bg-layout)] px-4 py-8">
<Card className="w-full max-w-[360px]">
<Space direction="vertical" size={20} className="w-full">
<div className="flex justify-center">
<img
src={withBasePath("/favicon.ico")}
alt="高压电塔图标"
width={72}
height={72}
className="h-[72px] w-[72px]"
/>
</div>
<Typography.Title level={3} className="!mb-0 !text-center">
</Typography.Title>
<form className="space-y-4" onSubmit={handleSubmit}>
<Input
size="large"
value={userId}
prefix={<IdcardOutlined />}
placeholder="用户 ID"
onChange={(event: ChangeEvent<HTMLInputElement>) => setUserId(event.currentTarget.value)}
autoComplete="username"
required
/>
<Input.Password
size="large"
value={password}
prefix={<LockOutlined />}
placeholder="密码"
onChange={(event: ChangeEvent<HTMLInputElement>) => setPassword(event.currentTarget.value)}
autoComplete="current-password"
minLength={1}
maxLength={128}
required
/>
<div className="flex items-center justify-between">
<Checkbox
checked={rememberPassword}
onChange={(event) => setRememberPassword(event.target.checked)}
>
</Checkbox>
</div>
<Button block size="large" type="primary" htmlType="submit" loading={busy}>
</Button>
</form>
{error && <Alert showIcon type="error" message={error} />}
</Space>
</Card>
</main>
);
redirect("/login");
}
+31 -1
View File
@@ -14,6 +14,8 @@ import { getApiBaseUrl, readApiError } from "@/lib/api";
import { withBasePath } from "@/lib/base-path";
import type { AuthTokenResponse, UserPublic } from "@/types/auth";
const AUTH_SESSION_HINT_KEY = "auth.session_hint";
type AuthContextValue = {
user: UserPublic | null;
accessToken: string | null;
@@ -41,6 +43,27 @@ function withApiPath(path: string): string {
return `${getApiBaseUrl()}${path}`;
}
function hasSessionHint(): boolean {
if (typeof window === "undefined") {
return false;
}
return window.localStorage.getItem(AUTH_SESSION_HINT_KEY) === "1";
}
function persistSessionHint(): void {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(AUTH_SESSION_HINT_KEY, "1");
}
function clearSessionHint(): void {
if (typeof window === "undefined") {
return;
}
window.localStorage.removeItem(AUTH_SESSION_HINT_KEY);
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<UserPublic | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
@@ -57,12 +80,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const clearAuth = useCallback(() => {
setUser(null);
applyToken(null);
clearSessionHint();
}, [applyToken]);
const applyAuthPayload = useCallback(
(payload: AuthTokenResponse) => {
setUser(payload.user);
applyToken(payload.access_token);
persistSessionHint();
},
[applyToken],
);
@@ -183,7 +208,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
});
} finally {
if (typeof window !== "undefined") {
window.location.replace(withBasePath("/"));
window.location.replace(withBasePath("/login"));
return;
}
clearAuth();
@@ -192,6 +217,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const bootstrap = async () => {
if (!hasSessionHint()) {
setInitializing(false);
return;
}
try {
await refreshAccessToken();
} finally {
+37
View File
@@ -0,0 +1,37 @@
const APP_ROUTE_ALIASES: Record<string, string> = {
"/admin": "/users",
"/dashboard": "/users",
"/user": "/users",
"/role": "/roles",
"/menu": "/menus",
"/system-param": "/system-params",
"/power-line": "/power-lines",
"/lightning-current": "/lightning-currents",
"/worker": "/workers",
"/tower-model": "/tower-models",
"/file": "/files",
};
function trimTrailingSlash(pathname: string): string {
if (pathname === "/") {
return pathname;
}
return pathname.replace(/\/+$/, "") || "/";
}
export function normalizeAppRoutePath(path: string | null): string | null {
if (!path) {
return path;
}
if (path.startsWith("http://") || path.startsWith("https://") || path.startsWith("//")) {
return path;
}
const normalizedPath = trimTrailingSlash(path.startsWith("/") ? path : `/${path}`);
const publicPath = normalizedPath.startsWith("/admin/")
? trimTrailingSlash(normalizedPath.slice("/admin".length) || "/")
: normalizedPath;
return APP_ROUTE_ALIASES[publicPath] ?? publicPath;
}
+13 -14
View File
@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { normalizeAppRoutePath } from "./lib/app-route-path";
function normalizeBasePath(value: string | undefined): string {
const trimmed = value?.trim() ?? "";
if (!trimmed || trimmed === "/") {
@@ -50,7 +52,7 @@ function withBasePath(pathname: string): string {
}
function isBypassedPath(pathname: string): boolean {
if (pathname === "/") {
if (pathname === "/" || pathname === "/login" || pathname === "/login/") {
return true;
}
if (PUBLIC_FILE.test(pathname)) {
@@ -68,26 +70,23 @@ export function middleware(request: NextRequest) {
return NextResponse.next();
}
// Keep backward compatibility for existing /admin links.
if (pathname === "/admin" || pathname === "/admin/") {
const url = request.nextUrl.clone();
url.pathname = withBasePath("/users");
return NextResponse.redirect(url);
}
if (pathname === "/dashboard" || pathname === "/dashboard/") {
const url = request.nextUrl.clone();
url.pathname = withBasePath("/users");
return NextResponse.redirect(url);
}
const canonicalPath = normalizeAppRoutePath(pathname) ?? pathname;
// Keep backward compatibility for existing /admin links and legacy menu aliases.
if (pathname.startsWith("/admin/")) {
const url = request.nextUrl.clone();
url.pathname = withBasePath(pathname.slice("/admin".length) || "/users");
url.pathname = withBasePath(canonicalPath);
return NextResponse.redirect(url);
}
if (canonicalPath !== pathname) {
const url = request.nextUrl.clone();
url.pathname = withBasePath(canonicalPath);
return NextResponse.redirect(url);
}
// New public URLs without /admin are internally rewritten to existing routes.
const url = request.nextUrl.clone();
url.pathname = withBasePath(`/admin${pathname}`);
url.pathname = withBasePath(`/admin${canonicalPath}`);
return NextResponse.rewrite(url);
}