feat: add user management system with roles and permissions
- Introduce SQLite persistence via GORM (stored at <data>/.ushare.db) - Add Role model with two built-in roles: admin (all perms) and user (upload only) - Add three permissions: user_manage, upload, token_manage (reserved) - Rewrite UserManager: DB-backed login with in-memory session tokens - Auto-seed default roles and admin user on first startup - Add AuthPermission middleware for fine-grained permission checks - Add /api/uauth/me endpoint for current session info - Add /api/admin/* CRUD routes for user and role management - Add admin console page (/admin) with user table and role permissions view - Show admin console link in share page for users with user_manage permission 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
211
internal/handler/admin.go
Normal file
211
internal/handler/admin.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/log"
|
||||
"github.com/loveuer/ushare/internal/model"
|
||||
"github.com/loveuer/ushare/internal/pkg/db"
|
||||
"github.com/loveuer/ushare/internal/pkg/tool"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func AdminListUsers() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
var users []model.User
|
||||
if err := db.Default.Session().Preload("Role").Find(&users).Error; err != nil {
|
||||
log.Error("handler.AdminListUsers: %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询失败"})
|
||||
}
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": users})
|
||||
}
|
||||
}
|
||||
|
||||
func AdminCreateUser() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
RoleID uint `json:"role_id"`
|
||||
}
|
||||
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "参数错误"})
|
||||
}
|
||||
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
if req.Username == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "用户名不能为空"})
|
||||
}
|
||||
if req.Password == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "密码不能为空"})
|
||||
}
|
||||
if req.RoleID == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "角色不能为空"})
|
||||
}
|
||||
|
||||
if err := tool.CheckPassword(req.Password); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()})
|
||||
}
|
||||
|
||||
var count int64
|
||||
db.Default.Session().Model(&model.User{}).Where("username = ?", req.Username).Count(&count)
|
||||
if count > 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "用户名已存在"})
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
Password: tool.NewPassword(req.Password),
|
||||
RoleID: req.RoleID,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
if err := db.Default.Session().Create(user).Error; err != nil {
|
||||
log.Error("handler.AdminCreateUser: %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "创建用户失败"})
|
||||
}
|
||||
|
||||
if err := db.Default.Session().Preload("Role").First(user, user.ID).Error; err != nil {
|
||||
log.Error("handler.AdminCreateUser: preload role: %s", err.Error())
|
||||
}
|
||||
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": user})
|
||||
}
|
||||
}
|
||||
|
||||
func AdminUpdateUser() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
RoleID *uint `json:"role_id"`
|
||||
Active *bool `json:"active"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
id, err := cast.ToUintE(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无效的用户ID"})
|
||||
}
|
||||
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "参数错误"})
|
||||
}
|
||||
|
||||
session := c.Locals("user").(*model.Session)
|
||||
|
||||
user := new(model.User)
|
||||
if err := db.Default.Session().Preload("Role").First(user, id).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"})
|
||||
}
|
||||
|
||||
updates := map[string]any{}
|
||||
|
||||
if req.RoleID != nil && *req.RoleID != user.RoleID {
|
||||
var newRole model.Role
|
||||
if err := db.Default.Session().First(&newRole, *req.RoleID).Error; err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无效的角色"})
|
||||
}
|
||||
// If demoting from admin, ensure at least one other active admin remains
|
||||
if user.Role.Name == model.RoleAdmin && newRole.Name != model.RoleAdmin {
|
||||
var adminCount int64
|
||||
db.Default.Session().Model(&model.User{}).
|
||||
Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id).
|
||||
Count(&adminCount)
|
||||
if adminCount == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无法更改角色: 系统中至少需要一个管理员"})
|
||||
}
|
||||
}
|
||||
updates["role_id"] = *req.RoleID
|
||||
}
|
||||
|
||||
if req.Active != nil && *req.Active != user.Active {
|
||||
if user.ID == session.UserID && !*req.Active {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能禁用自己的账号"})
|
||||
}
|
||||
if user.Role.Name == model.RoleAdmin && !*req.Active {
|
||||
var adminCount int64
|
||||
db.Default.Session().Model(&model.User{}).
|
||||
Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id).
|
||||
Count(&adminCount)
|
||||
if adminCount == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无法禁用: 系统中至少需要一个启用的管理员"})
|
||||
}
|
||||
}
|
||||
updates["active"] = *req.Active
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
if err := tool.CheckPassword(req.Password); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()})
|
||||
}
|
||||
updates["password"] = tool.NewPassword(req.Password)
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "没有需要更新的字段"})
|
||||
}
|
||||
|
||||
if err := db.Default.Session().Model(user).Updates(updates).Error; err != nil {
|
||||
log.Error("handler.AdminUpdateUser: %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "更新失败"})
|
||||
}
|
||||
|
||||
if err := db.Default.Session().Preload("Role").First(user, user.ID).Error; err != nil {
|
||||
log.Error("handler.AdminUpdateUser: preload: %s", err.Error())
|
||||
}
|
||||
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": user})
|
||||
}
|
||||
}
|
||||
|
||||
func AdminDeleteUser() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
id, err := cast.ToUintE(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无效的用户ID"})
|
||||
}
|
||||
|
||||
session := c.Locals("user").(*model.Session)
|
||||
if session.UserID == id {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能删除自己的账号"})
|
||||
}
|
||||
|
||||
user := new(model.User)
|
||||
if err := db.Default.Session().Preload("Role").First(user, id).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"})
|
||||
}
|
||||
|
||||
// Prevent deleting the last admin
|
||||
if user.Role.Name == model.RoleAdmin {
|
||||
var adminCount int64
|
||||
db.Default.Session().Model(&model.User{}).
|
||||
Where("role_id = ? AND id != ?", user.RoleID, id).
|
||||
Count(&adminCount)
|
||||
if adminCount == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无法删除最后一个管理员"})
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Default.Session().Delete(user).Error; err != nil {
|
||||
log.Error("handler.AdminDeleteUser: %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "删除失败"})
|
||||
}
|
||||
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
func AdminListRoles() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
var roles []model.Role
|
||||
if err := db.Default.Session().Find(&roles).Error; err != nil {
|
||||
log.Error("handler.AdminListRoles: %s", err.Error())
|
||||
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询失败"})
|
||||
}
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": roles})
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/ushare/internal/controller"
|
||||
"github.com/loveuer/ushare/internal/model"
|
||||
"github.com/loveuer/ushare/internal/opt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func AuthVerify() nf.HandlerFunc {
|
||||
@@ -14,33 +14,54 @@ func AuthVerify() nf.HandlerFunc {
|
||||
if token = c.Get("Authorization"); token != "" {
|
||||
return
|
||||
}
|
||||
|
||||
token = c.Cookies("ushare")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return func(c *nf.Ctx) error {
|
||||
if opt.Cfg.Username == "" || opt.Cfg.Password == "" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
token := tokenFn(c)
|
||||
if token == "" {
|
||||
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
op, err := controller.UserManager.Verify(token)
|
||||
session, err := controller.UserManager.Verify(token)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized", "msg": err.Error()})
|
||||
}
|
||||
|
||||
c.Locals("user", op)
|
||||
c.Locals("user", session)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AuthPermission(perm string) nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
session, ok := c.Locals("user").(*model.Session)
|
||||
if !ok || session == nil {
|
||||
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
for _, p := range session.Permissions {
|
||||
if p == perm {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
return c.Status(http.StatusForbidden).JSON(map[string]string{"error": "forbidden", "msg": "权限不足"})
|
||||
}
|
||||
}
|
||||
|
||||
func AuthMe() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
session, ok := c.Locals("user").(*model.Session)
|
||||
if !ok || session == nil {
|
||||
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
|
||||
}
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": session})
|
||||
}
|
||||
}
|
||||
|
||||
func AuthLogin() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
@@ -49,22 +70,22 @@ func AuthLogin() nf.HandlerFunc {
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req Req
|
||||
op *model.User
|
||||
err error
|
||||
req Req
|
||||
session *model.Session
|
||||
)
|
||||
|
||||
if err = c.BodyParser(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "错误的用户名或密码<1>"})
|
||||
}
|
||||
|
||||
if op, err = controller.UserManager.Login(req.Username, req.Password); err != nil {
|
||||
if session, err = controller.UserManager.Login(req.Username, req.Password); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()})
|
||||
}
|
||||
|
||||
header := fmt.Sprintf("ushare=%s; Path=/; Max-Age=%d", op.Token, 8*3600)
|
||||
header := fmt.Sprintf("ushare=%s; Path=/; Max-Age=%d", session.Token, 8*3600)
|
||||
c.SetHeader("Set-Cookie", header)
|
||||
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": op})
|
||||
return c.Status(http.StatusOK).JSON(map[string]any{"data": session})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user