完善权限获取机制
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use server"
|
"use server"
|
||||||
import { cookies } from "next/headers"
|
import { cookies } from "next/headers"
|
||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import type { User } from "@/models/user"
|
import type { Admin } from "@/models/admin"
|
||||||
import { callByDevice, callByUser } from "./base"
|
import { callByDevice, callByUser } from "./base"
|
||||||
|
|
||||||
export type TokenResp = {
|
export type TokenResp = {
|
||||||
@@ -79,7 +79,7 @@ export async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getProfile() {
|
export async function getProfile() {
|
||||||
return await callByUser<User>("/api/auth/introspect")
|
return await callByUser<Admin & { scopes: string[] }>("/api/auth/introspect")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAuth() {
|
export async function refreshAuth() {
|
||||||
@@ -128,5 +128,6 @@ export async function refreshAuth() {
|
|||||||
return {
|
return {
|
||||||
access_token: nextAccessToken,
|
access_token: nextAccessToken,
|
||||||
refresh_token: nextRefreshToken,
|
refresh_token: nextRefreshToken,
|
||||||
|
scopes: data.scope?.split(" ") || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ import Image from "next/image"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { usePathname, useRouter } from "next/navigation"
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { getProfile, logout } from "@/actions/auth"
|
import { logout } from "@/actions/auth"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import type { User } from "@/models/user"
|
import type { Admin } from "@/models/admin"
|
||||||
|
|
||||||
export default function Appbar() {
|
export default function Appbar(props: { admin: Admin }) {
|
||||||
const [currentUser, setCurrentUser] = useState<User>()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [showDropdown, setShowDropdown] = useState(false)
|
const [showDropdown, setShowDropdown] = useState(false)
|
||||||
const [showNotifications, setShowNotifications] = useState(false)
|
const [showNotifications, setShowNotifications] = useState(false)
|
||||||
@@ -122,28 +121,6 @@ export default function Appbar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchUserProfile() {
|
|
||||||
try {
|
|
||||||
const resp = await getProfile()
|
|
||||||
console.log(resp, "resp")
|
|
||||||
|
|
||||||
if (resp.success) {
|
|
||||||
setCurrentUser(resp.data)
|
|
||||||
} else {
|
|
||||||
console.error("获取用户信息失败:", resp.message)
|
|
||||||
if (resp.status === 401) {
|
|
||||||
router.replace("/login")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取用户信息时出错:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchUserProfile()
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white h-16 border-b border-gray-200 flex items-center justify-between px-6">
|
<header className="bg-white h-16 border-b border-gray-200 flex items-center justify-between px-6">
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
@@ -259,10 +236,10 @@ export default function Appbar() {
|
|||||||
aria-label="用户菜单"
|
aria-label="用户菜单"
|
||||||
>
|
>
|
||||||
<div className="h-8 w-8 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center overflow-hidden border-2 border-white shadow-sm">
|
<div className="h-8 w-8 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center overflow-hidden border-2 border-white shadow-sm">
|
||||||
{currentUser ? (
|
{props.admin ? (
|
||||||
currentUser.avatar ? (
|
props.admin.avatar ? (
|
||||||
<Image
|
<Image
|
||||||
src={currentUser.avatar}
|
src={props.admin.avatar}
|
||||||
alt="用户头像"
|
alt="用户头像"
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
@@ -271,8 +248,8 @@ export default function Appbar() {
|
|||||||
const target = e.target as HTMLImageElement
|
const target = e.target as HTMLImageElement
|
||||||
target.style.display = "none"
|
target.style.display = "none"
|
||||||
const parent = target.parentElement
|
const parent = target.parentElement
|
||||||
if (parent && currentUser?.name) {
|
if (parent && props.admin?.name) {
|
||||||
parent.textContent = currentUser.name
|
parent.textContent = props.admin.name
|
||||||
.charAt(0)
|
.charAt(0)
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
}
|
}
|
||||||
@@ -281,7 +258,7 @@ export default function Appbar() {
|
|||||||
) : (
|
) : (
|
||||||
// 如果没有头像,直接显示用户名首字母
|
// 如果没有头像,直接显示用户名首字母
|
||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
{currentUser.name.charAt(0).toUpperCase()}
|
{props.admin.name?.charAt(0).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -290,12 +267,14 @@ export default function Appbar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block text-left">
|
<div className="hidden md:block text-left">
|
||||||
{currentUser && (
|
{props.admin && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-800">
|
<p className="text-sm font-medium text-gray-800">
|
||||||
{currentUser.name}
|
{props.admin.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{props.admin.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">{currentUser.username}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -305,13 +284,13 @@ export default function Appbar() {
|
|||||||
{/* 用户下拉内容 */}
|
{/* 用户下拉内容 */}
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 z-20 border border-gray-200">
|
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 z-20 border border-gray-200">
|
||||||
{currentUser && (
|
{props.admin && (
|
||||||
<div className="px-4 py-2 border-b border-gray-100 md:hidden">
|
<div className="px-4 py-2 border-b border-gray-100 md:hidden">
|
||||||
<p className="font-medium text-gray-800">
|
<p className="font-medium text-gray-800">
|
||||||
{currentUser.name}
|
{props.admin.name}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500">{currentUser.name}</p>
|
<p className="text-xs text-gray-500">{props.admin.name}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
import type { ReactNode } from "react"
|
import { type ReactNode, Suspense } from "react"
|
||||||
|
import { getProfile } from "@/actions/auth"
|
||||||
import Appbar from "@/app/(root)/appbar"
|
import Appbar from "@/app/(root)/appbar"
|
||||||
import Navigation from "@/app/(root)/navigation"
|
import Navigation from "@/app/(root)/navigation"
|
||||||
|
import SetScopes from "./scopes"
|
||||||
|
|
||||||
export type RootLayoutProps = {
|
export type RootLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({ children }: RootLayoutProps) {
|
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<Layout>{children}</Layout>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function Layout(props: { children: ReactNode }) {
|
||||||
|
const profile = await getProfile()
|
||||||
|
if (!profile.success) throw new Error("页面渲染失败:无法获取账号信息")
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-100">
|
<div className="flex h-screen bg-gray-100">
|
||||||
|
<SetScopes admin={profile.data} />
|
||||||
|
|
||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<Appbar />
|
<Appbar admin={profile.data} />
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<main className="flex-1 overflow-auto p-6">{children}</main>
|
<main className="flex-1 overflow-auto p-6">{props.children}</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
14
src/app/(root)/scopes.tsx
Normal file
14
src/app/(root)/scopes.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use client"
|
||||||
|
import { useSetAtom } from "jotai"
|
||||||
|
import { scopesAtom } from "@/lib/stores/scopes"
|
||||||
|
import type { Admin } from "@/models/admin"
|
||||||
|
|
||||||
|
export default function SetScopes(props: {
|
||||||
|
admin: Admin & { scopes: string[] }
|
||||||
|
}) {
|
||||||
|
const setScopes = useSetAtom(scopesAtom)
|
||||||
|
|
||||||
|
console.log("用户权限", props.admin.scopes)
|
||||||
|
setScopes(props.admin.scopes)
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useSetAtom } from "jotai"
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Controller, useForm } from "react-hook-form"
|
import { Controller, useForm } from "react-hook-form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
@@ -17,7 +16,6 @@ import {
|
|||||||
FieldLegend,
|
FieldLegend,
|
||||||
} from "@/components/ui/field"
|
} from "@/components/ui/field"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { scopesAtom } from "@/lib/stores/scopes"
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().min(4).max(50),
|
username: z.string().min(4).max(50),
|
||||||
@@ -38,7 +36,6 @@ export default function LoginPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const setScopes = useSetAtom(scopesAtom)
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
try {
|
try {
|
||||||
const resp = await login(data)
|
const resp = await login(data)
|
||||||
@@ -47,9 +44,8 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功后跳转到首页
|
// 登录成功后跳转到首页
|
||||||
console.log("用户权限列表", resp.data)
|
|
||||||
setScopes(resp.data)
|
|
||||||
router.push("/")
|
router.push("/")
|
||||||
|
router.refresh()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error("登录失败", {
|
toast.error("登录失败", {
|
||||||
description: e instanceof Error ? e.message : "未知错误",
|
description: e instanceof Error ? e.message : "未知错误",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Admin } from "./admin"
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: number
|
||||||
admin_id?: number
|
admin_id?: number
|
||||||
@@ -21,7 +23,3 @@ export type User = {
|
|||||||
created_at: Date
|
created_at: Date
|
||||||
updated_at: Date
|
updated_at: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Admin = {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useSetAtom } from "jotai"
|
||||||
import { type NextRequest, NextResponse, type ProxyConfig } from "next/server"
|
import { type NextRequest, NextResponse, type ProxyConfig } from "next/server"
|
||||||
import { refreshAuth } from "@/actions/auth"
|
import { refreshAuth } from "@/actions/auth"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user