🎉初步完成用户

This commit is contained in:
coward
2024-10-18 17:19:19 +08:00
commit f375118c88
45 changed files with 3302 additions and 0 deletions

20
http/api/admin/base.go Normal file
View File

@@ -0,0 +1,20 @@
package admin
import (
"github.com/gin-gonic/gin"
"website-nav/http/response"
"website-nav/http/vo"
)
// GetCurrentLoginUser
// @description: 获取当前登陆用户
// @param c
// @return *vo.User
func GetCurrentLoginUser(c *gin.Context) *vo.User {
if user, ok := c.Get("user"); ok {
return user.(*vo.User)
}
response.R(c).AuthorizationFailed("暂未登陆")
c.Abort()
return nil
}

111
http/api/admin/login.go Normal file
View File

@@ -0,0 +1,111 @@
package admin
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/mojocn/base64Captcha"
"time"
"website-nav/component"
"website-nav/http/param"
"website-nav/http/response"
"website-nav/http/vo"
"website-nav/service"
"website-nav/utils"
)
type Login struct{}
func LoginAPI() Login {
return Login{}
}
// Captcha
// @description: 获取验证码
// @receiver login
// @param c
func (Login) Captcha(c *gin.Context) {
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
mathDriver := math.ConvertFonts()
capt := base64Captcha.NewCaptcha(mathDriver, component.Captcha{})
id, base64Str, _, err := capt.Generate()
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成验证码失败: %s", err.Error()))
return
}
response.R(c).OkWithData(map[string]any{
"id": id,
"captcha": base64Str,
})
}
// Login
// @description: 登陆
// @receiver login
// @param c
func (Login) Login(c *gin.Context) {
var p param.Login
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 验证验证码是否正确
ok := component.Captcha{}.Verify(p.CaptchaId, p.CaptchaCode, true)
if !ok {
response.R(c).FailedWithError("验证码错误")
return
}
// 验证码正确,查询用户信息
user, err := service.User().GetUserByAccount(p.Account)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// 对比密码
if !utils.Password().ComparePassword(user.Password, p.Password) {
response.R(c).FailedWithError("密码错误")
return
}
secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String())
// 生成token
token, expireAt, err := component.JWT().GenerateToken(user.Id, secret)
if err != nil {
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
response.R(c).FailedWithError("登陆失败!")
return
}
c.Writer.Header().Set("X-TOKEN", secret)
response.R(c).OkWithData(map[string]any{
"token": token,
"type": "Bearer",
"expireAt": expireAt,
})
}
// Logout
// @description: 退出登陆
// @receiver LoginApi
// @param c
func (Login) Logout(c *gin.Context) {
loginUser, ok := c.Get("user")
if !ok {
response.R(c).AuthorizationFailed("未登陆")
return
}
if err := component.JWT().Logout(loginUser.(*vo.User).Id); err != nil {
response.R(c).FailedWithError("退出登陆失败")
return
}
response.R(c).OK()
}

185
http/api/admin/user.go Normal file
View File

@@ -0,0 +1,185 @@
package admin
import (
"encoding/base64"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"website-nav/http/param"
"website-nav/http/response"
"website-nav/http/vo"
"website-nav/model"
"website-nav/service"
"website-nav/utils"
)
type User struct{}
func UserAPI() User {
return User{}
}
// GetLoginUser
// @description: 获取当前登陆用户信息
// @receiver u
// @param c
func (u User) GetLoginUser(c *gin.Context) {
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
return
}
response.R(c).OkWithData(loginUser)
}
// List
// @description: 用户列表
// @receiver u
// @param c
func (u User) List(c *gin.Context) {
var p param.UserList
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
data, total, err := service.User().List(p)
if err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).Paginate(data, total, p.Current, p.Size)
}
// Save
// @description: 新增/编辑用户
// @receiver u
// @param c
func (u User) Save(c *gin.Context) {
var p param.SaveUser
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
ent := &model.User{
Base: model.Base{
Id: p.Id,
},
Account: p.Account,
Password: p.Password,
Avatar: p.Avatar,
Nickname: p.Nickname,
Contact: p.Contact,
}
if err := service.User().CreateUser(ent); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// Delete
// @description: 删除用户
// @receiver u
// @param c
func (u User) Delete(c *gin.Context) {
var id = c.Param("id")
if id == "" || id == "undefined" {
response.R(c).Validator(errors.New("id不能为空"))
return
}
if err := service.User().Delete(id); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// ChangePassword
// @description: 修改密码
// @receiver u
// @param c
func (u User) ChangePassword(c *gin.Context) {
var p param.ChangePassword
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
user := GetCurrentLoginUser(c)
if user == nil {
response.R(c).FailedWithError("用户信息错误")
return
}
// 判断原密码是否对
if !utils.Password().ComparePassword(user.Password, p.OriginalPassword) {
response.R(c).FailedWithError("原密码错误")
return
}
// 修改密码
if err := service.User().ChangePassword(user.Id, p.NewPassword); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// ResetPassword
// @description: 重置密码
// @receiver UserApi
// @param c
func (u User) ResetPassword(c *gin.Context) {
var id = c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
loginUser := GetCurrentLoginUser(c)
if loginUser == nil {
response.R(c).FailedWithError("用户信息错误")
return
}
// 先查询一下
user, err := service.User().GetUserById(id)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
if user.Account == loginUser.Account {
response.R(c).FailedWithError("当前用户不可重置密码")
return
}
// 修改密码
if err := service.User().ChangePassword(user.Id, "admin123"); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// GenerateAvatar
// @description: 生成头像
// @receiver UserApi
// @param c
func (u User) GenerateAvatar(c *gin.Context) {
avatar, err := utils.Avatar().GenerateAvatar(false)
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成头像失败: %s", err.Error()))
return
}
response.R(c).OkWithData(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(avatar))))
}

View File

@@ -0,0 +1,77 @@
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"slices"
"strings"
"time"
"website-nav/component"
"website-nav/http/response"
"website-nav/service"
"website-nav/utils"
)
// Authorization
// @description: 授权中间件
// @return gin.HandlerFunc
func Authorization() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
hashPassword := c.Request.Header.Get("X-TOKEN")
if hashPassword == "" {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
userClaims, err := component.JWT().ParseToken(token, hashPassword)
if err != nil {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 如果token的颁发者与请求的站点不一致那么就给它抬出去
if !slices.Contains(strings.Split(userClaims.Issuer, ","), utils.WebSite().GetHost(c.Request.Header.Get("Referer"))) {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 查询用户
user, err := service.User().GetUserById(userClaims.ID)
if err != nil {
response.R(c).AuthorizationFailed("用户不存在")
c.Abort()
return
}
// 将用户信息放入上下文
c.Set("user", &user)
if c.Request.RequestURI == "/api/user/logout" {
c.Next()
}
// 生成一个新token
secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String())
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, userClaims.ExpiresAt.Time, time.Now().Local())
if err != nil {
response.R(c).AuthorizationFailed("校验失败")
c.Abort()
return
}
c.Writer.Header().Set("Authorization", fmt.Sprintf("Bearer %s", tokenStr))
c.Writer.Header().Set("X-TOKEN", secret)
c.Next()
}
}

10
http/param/login.go Normal file
View File

@@ -0,0 +1,10 @@
package param
// Login
// @description: 登陆
type Login struct {
Account string `json:"account" form:"account" label:"账号" binding:"required,min=2,max=20"`
Password string `json:"password" form:"password" label:"密码" binding:"required,min=8,max=32"`
CaptchaId string `json:"captchaId" form:"captchaId" label:"验证码ID" binding:"required"`
CaptchaCode string `json:"captchaCode" form:"captchaCode" label:"验证码" binding:"required,max=4" `
}

6
http/param/request.go Normal file
View File

@@ -0,0 +1,6 @@
package param
type Page struct {
Current int64 `json:"current" form:"current" label:"页码数" binding:"required"`
Size int64 `json:"size" form:"size" label:"每页数量" binging:"required"`
}

26
http/param/user.go Normal file
View File

@@ -0,0 +1,26 @@
package param
// UserList
// @description: 用户列表
type UserList struct {
Page
}
// SaveUser
// @description: 新增/编辑用户
type SaveUser struct {
Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
Account string `json:"account" form:"account" label:"账户号" binding:"required_without=Id"` // 账户号
Password string `json:"password" form:"password" label:"密码" binding:"omitempty"` // 密码
Nickname string `json:"nickname" form:"nickname" label:"昵称" binding:"required,min=2"` // 昵称
Avatar string `json:"avatar" form:"avatar" label:"头像" binding:"omitempty"` // 头像
Contact string `json:"contact" form:"contact" label:"联系方式" binding:"omitempty"` // 联系方式
}
// ChangePassword
// @description: 修改密码
type ChangePassword struct {
OriginalPassword string `json:"originalPassword" form:"originalPassword" label:"原密码" binding:"required,min=8,max=32"` // 原密码
NewPassword string `json:"newPassword" form:"newPassword" label:"新密码" binding:"required,min=8,max=32"` // 新密码
ConfirmPassword string `json:"confirmPassword" form:"confirmPassword" label:"确认密码" binding:"eqfield=NewPassword"` // 确认密码
}

131
http/response/response.go Normal file
View File

@@ -0,0 +1,131 @@
package response
import (
"github.com/gin-gonic/gin"
"net/http"
"website-nav/component"
"website-nav/utils"
)
type PageData[T any] struct {
Current int `json:"current"` // 当前页码
Size int `json:"size"` // 每页数量
Total int64 `json:"total"` // 总数
TotalPage int `json:"totalPage"` // 总页数
Records T `json:"records"` // 返回数据
}
type HttpResponse struct{ *gin.Context }
func R(c *gin.Context) HttpResponse {
return HttpResponse{c}
}
// OK
// @description: success
// @receiver h
func (h HttpResponse) OK() {
h.Context.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
})
}
// OkWithData
// @description: success with data
// @receiver h
// @param data
func (h HttpResponse) OkWithData(data any) {
h.Context.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
"data": data,
})
}
// Paginate
// @description: success page data
// @receiver h
// @param data
// @param total
// @param current
// @param size
func (h HttpResponse) Paginate(data any, total int64, current, size int64) {
// 处理一下页码、页数量
if current == -1 {
current = 1
size = total
}
// 计算总页码
totalPage := utils.Paginate().Generate(total, int(size))
// 返回结果
h.Context.JSON(http.StatusOK, map[string]any{
"code": http.StatusOK,
"data": &PageData[any]{Current: int(current), Size: int(size), Total: total, TotalPage: totalPage, Records: data},
"message": "success",
})
}
// AuthorizationFailed
// @description: authorization failed
// @receiver h
// @param msg
func (h HttpResponse) AuthorizationFailed(msg string) {
if msg == "" {
msg = "authorized failed"
}
h.Context.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"message": msg,
})
}
// Failed
// @description: request bad
// @receiver h
func (h HttpResponse) Failed() {
h.Context.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": "failed",
})
}
// FailedWithError
// @description: custom error
// @receiver h
// @param err
func (h HttpResponse) FailedWithError(err any) {
var errStr string
switch err.(type) {
case error:
errStr = err.(error).Error()
case string:
errStr = err.(string)
}
h.Context.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": errStr,
})
}
// Validator
// @description: request param error
// @receiver h
// @param err
func (h HttpResponse) Validator(err error) {
h.Context.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": component.Validate().Error(err),
})
}
// Internal
// @description: server error
// @receiver h
func (h HttpResponse) Internal() {
h.Context.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"message": "server error",
})
}

51
http/router/admin.go Normal file
View File

@@ -0,0 +1,51 @@
package router
import (
"github.com/gin-gonic/gin"
"website-nav/http/api/admin"
"website-nav/http/middleware"
)
func adminAPI(r *gin.RouterGroup) {
adminApi := r.Group("admin")
{
loginAPI(adminApi)
userAPI(adminApi)
navTypeAPI(adminApi)
}
}
// loginAPI
// @description: 登陆相关API
// @param r
func loginAPI(r *gin.RouterGroup) {
login := r.Group("/login")
{
login.GET("/captcha", admin.LoginAPI().Captcha) // 获取验证码
login.POST("/token", admin.LoginAPI().Login) // 登陆
}
}
// userAPI
// @description: 用户相关API
// @param r
func userAPI(r *gin.RouterGroup) {
user := r.Group("/user")
user.Use(middleware.Authorization())
{
user.GET("/info", admin.UserAPI().GetLoginUser) // 获取当前登陆用户信息
user.GET("/list", admin.UserAPI().List) // 用户列表
user.POST("/save", admin.UserAPI().Save) // 新增/编辑用户
user.DELETE("/:id", admin.UserAPI().Delete) // 删除用户
user.PUT("/change-password", admin.UserAPI().ChangePassword) // 修改密码【当前用户更改自己的】
user.PUT("/reset-password/:id", admin.UserAPI().ResetPassword) // 重置密码 【重置其他管理员的】
user.GET("/generate-avatar", admin.UserAPI().GenerateAvatar) // 生成头像
}
}
// navTypeAPI
// @description: 导航分类API
// @param r
func navTypeAPI(r *gin.RouterGroup) {
}

33
http/router/root.go Normal file
View File

@@ -0,0 +1,33 @@
package router
import (
"github.com/gin-gonic/gin"
)
type Option func(engine *gin.RouterGroup)
var options []Option
func includeRouters(opts ...Option) {
options = append(options, opts...)
}
func InitRouter() *gin.Engine {
r := gin.New()
// 开启IP 追踪
r.ForwardedByClientIP = true
// 将请求打印至控制台
r.Use(gin.Logger())
for _, opt := range options {
opt(r.Group("api"))
}
return r
}
func Rooters() {
includeRouters(
adminAPI,
)
}

18
http/vo/navigation.go Normal file
View File

@@ -0,0 +1,18 @@
package vo
// TypeItem
// @description: 导航分类
type TypeItem struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
}
type NavigationItem struct {
ID string `json:"id"`
Title string `json:"title"`
Url string `json:"url"`
Icon string `json:"icon"`
Desc string `json:"desc"`
Enabled bool `json:"enabled"`
}

25
http/vo/user.go Normal file
View File

@@ -0,0 +1,25 @@
package vo
import "website-nav/model"
// UserItem
// @description: 用户列表的数据
type UserItem struct {
Id string `json:"id"`
Account string `json:"account"`
Avatar string `json:"avatar"`
Nickname string `json:"nickname"`
Contact string `json:"contact"`
CreatedAt model.JsonTime `json:"createdAt"`
UpdatedAt model.JsonTime `json:"updatedAt"`
}
// User
// @description: 用户信息
type User struct {
Id string `json:"id"`
Account string `json:"account"`
Password string `json:"-"`
Nickname string `json:"nickname"`
Contact string `json:"contact"`
}