取消游客模式,强制登录后使用

This commit is contained in:
2026-07-04 09:48:00 +08:00
parent 24630654a2
commit 323f50bfc0
10 changed files with 34 additions and 53 deletions
+1 -4
View File
@@ -1,6 +1,6 @@
# A股工具 · 用户体系 # A股工具 · 用户体系
基于 **Go + PostgreSQL + Docker Compose** 的用户权限管理模块,角色覆盖:系统管理员、管理员、用户、游客。前端界面复用 `refer/` 目录下的暗色主题设计语言。 基于 **Go + PostgreSQL + Docker Compose** 的用户权限管理模块,角色覆盖:系统管理员、管理员、用户。前端界面复用 `refer/` 目录下的暗色主题设计语言。
## 技术栈 ## 技术栈
@@ -50,13 +50,11 @@ WHERE username = '你的用户名';
| 系统管理员 | 管理管理员、用户、系统配置 | | 系统管理员 | 管理管理员、用户、系统配置 |
| 管理员 | 管理普通用户 | | 管理员 | 管理普通用户 |
| 用户 | 访问业务功能、修改个人资料 | | 用户 | 访问业务功能、修改个人资料 |
| 游客 | 仅查看公开内容,不可操作 |
## 主要 API ## 主要 API
| 方法 | 路径 | 说明 | 权限 | | 方法 | 路径 | 说明 | 权限 |
|---|---|---|---| |---|---|---|---|
| GET | /api/public | 公开信息 | 公开 |
| POST | /api/auth/register | 注册(默认 user | 公开 | | POST | /api/auth/register | 注册(默认 user | 公开 |
| POST | /api/auth/login | 登录 | 公开 | | POST | /api/auth/login | 登录 | 公开 |
| GET | /api/auth/me | 当前用户 | 需登录 | | GET | /api/auth/me | 当前用户 | 需登录 |
@@ -107,5 +105,4 @@ npm run dev
## 注意事项 ## 注意事项
- 生产环境请务必修改 `JWT_SECRET` - 生产环境请务必修改 `JWT_SECRET`
- 游客通过首页直接访问,无需登录。
- 首次启动时 GORM 会自动创建表并插入默认角色。 - 首次启动时 GORM 会自动创建表并插入默认角色。
+2 -2
View File
@@ -20,9 +20,9 @@ type AdminHandler struct {
func allowedRolesForCreation(actor models.RoleName) []models.RoleName { func allowedRolesForCreation(actor models.RoleName) []models.RoleName {
switch actor { switch actor {
case models.RoleSystemAdmin: case models.RoleSystemAdmin:
return []models.RoleName{models.RoleAdmin, models.RoleUser, models.RoleGuest} return []models.RoleName{models.RoleAdmin, models.RoleUser}
case models.RoleAdmin: case models.RoleAdmin:
return []models.RoleName{models.RoleUser, models.RoleGuest} return []models.RoleName{models.RoleUser}
default: default:
return nil return nil
} }
-7
View File
@@ -153,13 +153,6 @@ func (h *AuthHandler) Logout(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "登出成功"}) c.JSON(http.StatusOK, gin.H{"message": "登出成功"})
} }
func (h *AuthHandler) PublicInfo(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "欢迎访问 A股工具公开信息",
"guest_allowed": true,
})
}
func hashPassword(password string) (string, error) { func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err return string(bytes), err
+16
View File
@@ -19,6 +19,11 @@ type CurrentUser struct {
Role models.RoleName Role models.RoleName
} }
var publicPaths = map[string]struct{}{
"/api/auth/register": {},
"/api/auth/login": {},
}
func AuthMiddleware(cfg *config.Config, db *gorm.DB) gin.HandlerFunc { func AuthMiddleware(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
@@ -41,6 +46,17 @@ func AuthMiddleware(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
} }
} }
if _, exists := c.Get("currentUser"); !exists {
if c.Request.Method == "OPTIONS" {
c.Next()
return
}
if _, ok := publicPaths[c.Request.URL.Path]; !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"success": false, "error": "访问令牌无效或已过期"})
return
}
}
c.Next() c.Next()
} }
} }
+1 -5
View File
@@ -13,7 +13,6 @@ const (
RoleSystemAdmin RoleName = "system_admin" RoleSystemAdmin RoleName = "system_admin"
RoleAdmin RoleName = "admin" RoleAdmin RoleName = "admin"
RoleUser RoleName = "user" RoleUser RoleName = "user"
RoleGuest RoleName = "guest"
) )
func (r RoleName) String() string { return string(r) } func (r RoleName) String() string { return string(r) }
@@ -43,10 +42,8 @@ func ParseRoleName(s string) (RoleName, error) {
return RoleAdmin, nil return RoleAdmin, nil
case "user": case "user":
return RoleUser, nil return RoleUser, nil
case "guest":
return RoleGuest, nil
default: default:
return RoleGuest, fmt.Errorf("unknown role: %s", s) return "", fmt.Errorf("unknown role: %s", s)
} }
} }
@@ -105,7 +102,6 @@ func SeedRoles(db *gorm.DB) error {
{Name: string(RoleSystemAdmin), Description: strPtr("系统管理员,可管理管理员与系统配置"), Permissions: `["*"]`}, {Name: string(RoleSystemAdmin), Description: strPtr("系统管理员,可管理管理员与系统配置"), Permissions: `["*"]`},
{Name: string(RoleAdmin), Description: strPtr("管理员,可管理普通用户"), Permissions: `["users.read", "users.write", "users.create"]`}, {Name: string(RoleAdmin), Description: strPtr("管理员,可管理普通用户"), Permissions: `["users.read", "users.write", "users.create"]`},
{Name: string(RoleUser), Description: strPtr("普通用户,可访问业务功能"), Permissions: `["dashboard.read", "profile.write"]`}, {Name: string(RoleUser), Description: strPtr("普通用户,可访问业务功能"), Permissions: `["dashboard.read", "profile.write"]`},
{Name: string(RoleGuest), Description: strPtr("游客,仅可查看公开内容"), Permissions: `["public.read"]`},
} }
for _, role := range roles { for _, role := range roles {
var existing Role var existing Role
+1 -2
View File
@@ -30,8 +30,7 @@ func Setup(cfg *config.Config, db *gorm.DB) *gin.Engine {
r.Use(middleware.AuthMiddleware(cfg, db)) r.Use(middleware.AuthMiddleware(cfg, db))
// 公开接口 // 公开接口(仅登录/注册)
r.GET("/api/public", authHandler.PublicInfo)
r.POST("/api/auth/register", authHandler.Register) r.POST("/api/auth/register", authHandler.Register)
r.POST("/api/auth/login", authHandler.Login) r.POST("/api/auth/login", authHandler.Login)
+2 -5
View File
@@ -10,14 +10,14 @@ export interface UserInfo {
id: string id: string
username: string username: string
email?: string email?: string
role: 'system_admin' | 'admin' | 'user' | 'guest' role: 'system_admin' | 'admin' | 'user'
status: string status: string
created_at: string created_at: string
} }
export interface Role { export interface Role {
id: number id: number
name: 'system_admin' | 'admin' | 'user' | 'guest' name: 'system_admin' | 'admin' | 'user'
description?: string description?: string
permissions: string[] permissions: string[]
created_at: string created_at: string
@@ -79,9 +79,6 @@ async function request<T>(
} }
export const api = { export const api = {
publicInfo: () =>
request<{ message: string; guest_allowed: boolean }>('/api/public'),
register: (body: { username: string; email?: string; password: string }) => register: (body: { username: string; email?: string; password: string }) =>
request<AuthResponse>('/api/auth/register', { request<AuthResponse>('/api/auth/register', {
method: 'POST', method: 'POST',
+1 -4
View File
@@ -30,13 +30,12 @@ export function clearAuth() {
localStorage.removeItem('user') localStorage.removeItem('user')
} }
export type RoleName = 'system_admin' | 'admin' | 'user' | 'guest' export type RoleName = 'system_admin' | 'admin' | 'user'
const ROLE_RANK: Record<RoleName, number> = { const ROLE_RANK: Record<RoleName, number> = {
system_admin: 3, system_admin: 3,
admin: 2, admin: 2,
user: 1, user: 1,
guest: 0,
} }
export function roleRank(role: RoleName): number { export function roleRank(role: RoleName): number {
@@ -51,8 +50,6 @@ export function roleLabel(role: RoleName): string {
return '管理员' return '管理员'
case 'user': case 'user':
return '用户' return '用户'
case 'guest':
return '游客'
} }
} }
+4 -18
View File
@@ -1,16 +1,9 @@
import { useEffect, useState } from 'react' import { LayoutDashboard, ShieldCheck } from 'lucide-react'
import { LayoutDashboard, Globe, ShieldCheck } from 'lucide-react'
import { api } from '@/lib/api'
import { getStoredUser, roleLabel } from '@/lib/auth' import { getStoredUser, roleLabel } from '@/lib/auth'
export function Dashboard() { export function Dashboard() {
const [publicInfo, setPublicInfo] = useState<{ message: string } | null>(null)
const user = getStoredUser() const user = getStoredUser()
useEffect(() => {
api.publicInfo().then(setPublicInfo).catch(() => null)
}, [])
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -19,7 +12,7 @@ export function Dashboard() {
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-foreground"></h1> <h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-secondary">{user?.username || '游客'}</p> <p className="text-sm text-secondary">{user?.username || ''}</p>
</div> </div>
</div> </div>
@@ -27,14 +20,8 @@ export function Dashboard() {
<Card <Card
icon={<ShieldCheck className="h-5 w-5 text-accent" />} icon={<ShieldCheck className="h-5 w-5 text-accent" />}
title="当前身份" title="当前身份"
value={user ? roleLabel(user.role) : '游客'} value={user ? roleLabel(user.role) : ''}
desc={user ? `用户:${user.username}` : '未登录,仅可查看公开信息'} desc={user ? `用户:${user.username}` : '加载中…'}
/>
<Card
icon={<Globe className="h-5 w-5 text-bear" />}
title="公开信息"
value="已接入"
desc={publicInfo?.message || '加载中…'}
/> />
</div> </div>
@@ -44,7 +31,6 @@ export function Dashboard() {
<li><span className="text-accent font-medium"></span></li> <li><span className="text-accent font-medium"></span></li>
<li><span className="text-accent font-medium"></span></li> <li><span className="text-accent font-medium"></span></li>
<li><span className="text-accent font-medium"></span>访</li> <li><span className="text-accent font-medium"></span>访</li>
<li><span className="text-accent font-medium"></span></li>
</ul> </ul>
</div> </div>
</div> </div>
+6 -6
View File
@@ -10,7 +10,11 @@ export const router = createBrowserRouter([
{ path: '/login', element: <Login /> }, { path: '/login', element: <Login /> },
{ {
path: '/', path: '/',
element: <Layout />, element: (
<AuthGuard requireAuth>
<Layout />
</AuthGuard>
),
children: [ children: [
{ index: true, element: <Dashboard /> }, { index: true, element: <Dashboard /> },
{ {
@@ -23,11 +27,7 @@ export const router = createBrowserRouter([
}, },
{ {
path: 'profile', path: 'profile',
element: ( element: <Profile />,
<AuthGuard requireAuth>
<Profile />
</AuthGuard>
),
}, },
], ],
}, },