580 lines
19 KiB
Go
580 lines
19 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"kra/internal/biz"
|
|
"kra/internal/conf"
|
|
"kra/internal/service"
|
|
"kra/pkg/adminauth"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/mojocn/base64Captcha"
|
|
)
|
|
|
|
// GinServer adapts Gin to the Kratos transport.Server lifecycle.
|
|
type GinServer struct {
|
|
network string
|
|
address string
|
|
server *http.Server
|
|
listener net.Listener
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.SystemService, access *service.AccessService, settings *service.SettingsService, versions *service.VersionService, exports *service.ExportService, audit *service.AuditService, tasks *service.TaskService, media *service.MediaService, announcements *service.AnnouncementService, emails *service.EmailService, scheduler *TaskScheduler, logger *slog.Logger) *GinServer {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
engine := gin.New()
|
|
engine.Use(ginRequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
|
|
if config != nil && config.Local != nil && config.Local.StorePath != "" {
|
|
pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
|
|
if pathPrefix != "/" {
|
|
engine.StaticFS(pathPrefix, filesOnly{FileSystem: http.Dir(config.Local.StorePath)})
|
|
}
|
|
}
|
|
|
|
prefix := ""
|
|
if config != nil {
|
|
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
|
|
}
|
|
public := engine.Group(prefix)
|
|
public.GET("/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, "ok")
|
|
})
|
|
registerPublicRoutes(public, engine, config, svc, settings, access, audit)
|
|
private := engine.Group(prefix)
|
|
private.Use(jwtAuth(config, settings), accessControl(access, audit))
|
|
registerPrivateRoutes(private, svc)
|
|
registerAccessRoutes(private, engine, access)
|
|
registerSettingsRoutes(private, public, settings)
|
|
registerSystemConfigRoutes(private, config, settings)
|
|
registerVersionRoutes(private, versions)
|
|
registerExportRoutes(private, public, svc, exports)
|
|
registerAuditRoutes(private, public, audit)
|
|
registerTaskRoutes(private, tasks, scheduler)
|
|
registerMediaRoutes(private, media)
|
|
registerAnnouncementRoutes(private, public, announcements)
|
|
registerEmailRoutes(private, emails)
|
|
|
|
engine.NoRoute(func(c *gin.Context) {
|
|
fail(c, "请求的接口不存在")
|
|
})
|
|
|
|
network := "tcp"
|
|
address := ":8000"
|
|
if c != nil && c.Http != nil {
|
|
if c.Http.Network != "" {
|
|
network = c.Http.Network
|
|
}
|
|
if c.Http.Addr != "" {
|
|
address = c.Http.Addr
|
|
}
|
|
}
|
|
|
|
return &GinServer{
|
|
network: network,
|
|
address: address,
|
|
server: &http.Server{Addr: address, Handler: engine},
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func jwtAuth(config *conf.AdminBackend, settings *service.SettingsService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
token := c.GetHeader("x-token")
|
|
if token == "" {
|
|
token, _ = c.Cookie("x-token")
|
|
}
|
|
secret := ""
|
|
if config != nil && config.Jwt != nil {
|
|
secret = config.Jwt.SigningKey
|
|
}
|
|
claims, err := adminauth.Parse(token, secret)
|
|
if err != nil {
|
|
noAuth(c, "未登录或非法访问")
|
|
return
|
|
}
|
|
if disabled, checkErr := settings.IsTokenDisabled(c.Request.Context(), token); checkErr != nil || disabled {
|
|
noAuth(c, "登录状态已失效")
|
|
return
|
|
}
|
|
c.Set("admin_claims", claims)
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func currentClaims(c *gin.Context) *adminauth.Claims {
|
|
claims, _ := c.Get("admin_claims")
|
|
result, _ := claims.(*adminauth.Claims)
|
|
return result
|
|
}
|
|
|
|
func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService) {
|
|
expiration := 3 * time.Minute
|
|
keyLong, width, height := 6, 240, 80
|
|
if config != nil && config.Captcha != nil {
|
|
if config.Captcha.StoreExpiration != nil {
|
|
expiration = config.Captcha.StoreExpiration.AsDuration()
|
|
}
|
|
if config.Captcha.KeyLong > 0 {
|
|
keyLong = int(config.Captcha.KeyLong)
|
|
}
|
|
if config.Captcha.ImgWidth > 0 {
|
|
width = int(config.Captcha.ImgWidth)
|
|
}
|
|
if config.Captcha.ImgHeight > 0 {
|
|
height = int(config.Captcha.ImgHeight)
|
|
}
|
|
}
|
|
store := &captchaStore{service: svc, expiration: expiration}
|
|
base := group.Group("/base")
|
|
base.POST("/captcha", func(c *gin.Context) {
|
|
security, _ := settings.CurrentSecurity(c.Request.Context())
|
|
openCaptcha := true
|
|
if security != nil {
|
|
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
|
|
if security.CaptchaOpen > 0 {
|
|
failures, _ := cachedInt(c.Request.Context(), svc, "login:fail:"+c.ClientIP())
|
|
openCaptcha = failures >= security.CaptchaOpen
|
|
}
|
|
}
|
|
if !openCaptcha {
|
|
writeResult(c, codeSuccess, gin.H{"captchaId": "", "picPath": "", "captchaLength": keyLong, "openCaptcha": false}, "验证码获取成功")
|
|
return
|
|
}
|
|
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
|
|
id, picture, _, err := base64Captcha.NewCaptcha(driver, store).Generate()
|
|
if err != nil {
|
|
fail(c, "验证码获取失败")
|
|
return
|
|
}
|
|
writeResult(c, codeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
|
|
})
|
|
base.POST("/login", func(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Captcha string `json:"captcha"`
|
|
CaptchaID string `json:"captchaId"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
|
|
fail(c, "请输入用户名和密码")
|
|
return
|
|
}
|
|
security, _ := settings.CurrentSecurity(c.Request.Context())
|
|
if security != nil && security.LockEnable {
|
|
if _, locked, _ := svc.CacheGet(c.Request.Context(), "login:lock:"+req.Username); locked {
|
|
fail(c, "账户已锁定,请稍后重试")
|
|
return
|
|
}
|
|
}
|
|
requireCaptcha := security == nil || security.CaptchaOpen == 0
|
|
if security != nil && security.CaptchaOpen > 0 {
|
|
failures, _ := cachedInt(c.Request.Context(), svc, "login:fail:"+c.ClientIP())
|
|
requireCaptcha = failures >= security.CaptchaOpen
|
|
}
|
|
if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !store.Verify(req.CaptchaID, req.Captcha, true)) {
|
|
fail(c, "验证码错误")
|
|
return
|
|
}
|
|
result, err := svc.Login(c.Request.Context(), req.Username, req.Password)
|
|
if err != nil {
|
|
_ = audit.RecordLogin(c.Request.Context(), &biz.LoginLog{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
|
|
ttl := time.Hour
|
|
if security != nil && security.CaptchaTimeout > 0 {
|
|
ttl = time.Duration(security.CaptchaTimeout) * time.Second
|
|
}
|
|
failures, _ := incrementCached(c.Request.Context(), svc, "login:fail:"+c.ClientIP(), ttl)
|
|
if security != nil && security.LockEnable && failures >= security.LockThreshold {
|
|
_ = svc.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", time.Duration(security.LockDuration)*time.Minute)
|
|
}
|
|
fail(c, "用户名不存在或者密码错误")
|
|
return
|
|
}
|
|
userID, _ := result.User["ID"].(uint)
|
|
_ = audit.RecordLogin(c.Request.Context(), &biz.LoginLog{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID})
|
|
_ = svc.CacheDelete(c.Request.Context(), "login:fail:"+c.ClientIP())
|
|
_ = svc.CacheDelete(c.Request.Context(), "login:lock:"+req.Username)
|
|
maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds())
|
|
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: result.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
|
|
writeResult(c, codeSuccess, result, "登录成功")
|
|
})
|
|
initGroup := group.Group("/init")
|
|
initGroup.POST("/checkdb", func(c *gin.Context) {
|
|
initialized, err := svc.IsInitialized(c.Request.Context())
|
|
if err != nil {
|
|
fail(c, "数据库状态检查失败")
|
|
return
|
|
}
|
|
message := "数据库无需初始化"
|
|
if !initialized {
|
|
message = "前往初始化数据库"
|
|
}
|
|
writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message)
|
|
})
|
|
initGroup.POST("/initdb", func(c *gin.Context) {
|
|
if err := svc.Initialize(c.Request.Context()); err != nil {
|
|
fail(c, "自动创建数据库失败")
|
|
return
|
|
}
|
|
routes := engine.Routes()
|
|
apis := make([]*biz.API, 0, len(routes))
|
|
for _, route := range routes {
|
|
apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)})
|
|
}
|
|
if err := access.ApplyAPISync(c.Request.Context(), apis, nil); err != nil {
|
|
fail(c, "数据库已初始化,但 API 权限数据写入失败")
|
|
return
|
|
}
|
|
writeResult(c, codeSuccess, gin.H{}, "自动创建数据库成功")
|
|
})
|
|
}
|
|
|
|
func routeGroup(path string) string {
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
return parts[0]
|
|
}
|
|
return "base"
|
|
}
|
|
|
|
func cachedInt(ctx context.Context, svc *service.SystemService, key string) (int, error) {
|
|
value, ok, err := svc.CacheGet(ctx, key)
|
|
if err != nil || !ok {
|
|
return 0, err
|
|
}
|
|
result, err := strconv.Atoi(value)
|
|
return result, err
|
|
}
|
|
func incrementCached(ctx context.Context, svc *service.SystemService, key string, ttl time.Duration) (int, error) {
|
|
value, _ := cachedInt(ctx, svc, key)
|
|
value++
|
|
return value, svc.CacheSet(ctx, key, strconv.Itoa(value), ttl)
|
|
}
|
|
func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
cfg, err := settings.CurrentSecurity(c.Request.Context())
|
|
if err != nil || cfg == nil || !cfg.LimitEnable {
|
|
c.Next()
|
|
return
|
|
}
|
|
window := cfg.LimitWindow
|
|
if window < 1 {
|
|
window = 60
|
|
}
|
|
bucket := time.Now().Unix() / int64(window)
|
|
key := "rate:" + c.ClientIP() + ":" + strconv.FormatInt(bucket, 10)
|
|
count, cacheErr := incrementCached(c.Request.Context(), svc, key, time.Duration(window+1)*time.Second)
|
|
if cacheErr == nil && count > cfg.LimitCount {
|
|
c.AbortWithStatusJSON(http.StatusTooManyRequests, response{Code: codeError, Data: nil, Msg: "请求过于频繁"})
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
type captchaStore struct {
|
|
service *service.SystemService
|
|
expiration time.Duration
|
|
}
|
|
|
|
func (s *captchaStore) key(id string) string { return "captcha:" + id }
|
|
func (s *captchaStore) Set(id, value string) error {
|
|
return s.service.CacheSet(context.Background(), s.key(id), value, s.expiration)
|
|
}
|
|
func (s *captchaStore) Get(id string, clear bool) string {
|
|
value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
|
|
if err != nil || !ok {
|
|
return ""
|
|
}
|
|
if clear {
|
|
_ = s.service.CacheDelete(context.Background(), s.key(id))
|
|
}
|
|
return value
|
|
}
|
|
func (s *captchaStore) Verify(id, answer string, clear bool) bool {
|
|
if id == "" || answer == "" {
|
|
return false
|
|
}
|
|
return strings.EqualFold(s.Get(id, clear), answer)
|
|
}
|
|
|
|
type filesOnly struct{ http.FileSystem }
|
|
|
|
func (f filesOnly) Open(name string) (http.File, error) {
|
|
file, err := f.FileSystem.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, statErr := file.Stat()
|
|
if statErr != nil {
|
|
_ = file.Close()
|
|
return nil, statErr
|
|
}
|
|
if info.IsDir() {
|
|
_ = file.Close()
|
|
return nil, os.ErrPermission
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func registerPrivateRoutes(group *gin.RouterGroup, svc *service.SystemService) {
|
|
user := group.Group("/user")
|
|
user.POST("/getUserList", func(c *gin.Context) {
|
|
var req struct {
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
Username string `json:"username"`
|
|
NickName string `json:"nickName"`
|
|
Phone string `json:"phone"`
|
|
Email string `json:"email"`
|
|
OrderKey string `json:"orderKey"`
|
|
Desc bool `json:"desc"`
|
|
}
|
|
if c.ShouldBindJSON(&req) != nil {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
list, total, err := svc.ListUsers(c.Request.Context(), req.Page, req.PageSize, &biz.UserListFilter{Username: req.Username, NickName: req.NickName, Phone: req.Phone, Email: req.Email, OrderKey: req.OrderKey, Desc: req.Desc})
|
|
if err != nil {
|
|
fail(c, "获取失败")
|
|
return
|
|
}
|
|
writeResult(c, codeSuccess, pageResult{List: list, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
|
|
})
|
|
user.POST("/admin_register", func(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"userName"`
|
|
Password string `json:"passWord"`
|
|
NickName string `json:"nickName"`
|
|
HeaderImg string `json:"headerImg"`
|
|
AuthorityID uint `json:"authorityId"`
|
|
AuthorityIDs []uint `json:"authorityIds"`
|
|
Enable int `json:"enable"`
|
|
Phone string `json:"phone"`
|
|
Email string `json:"email"`
|
|
}
|
|
if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
result, err := svc.CreateUser(c.Request.Context(), service.UserInput{Username: req.Username, Password: req.Password, NickName: req.NickName, HeaderImg: req.HeaderImg, AuthorityID: req.AuthorityID, AuthorityIDs: req.AuthorityIDs, Enable: req.Enable, Phone: req.Phone, Email: req.Email})
|
|
if err != nil {
|
|
fail(c, "注册失败")
|
|
return
|
|
}
|
|
writeResult(c, codeSuccess, gin.H{"user": result}, "注册成功")
|
|
})
|
|
user.PUT("/setUserInfo", func(c *gin.Context) {
|
|
var req struct {
|
|
ID uint `json:"ID"`
|
|
NickName string `json:"nickName"`
|
|
HeaderImg string `json:"headerImg"`
|
|
AuthorityID uint `json:"authorityId"`
|
|
AuthorityIDs []uint `json:"authorityIds"`
|
|
Enable int `json:"enable"`
|
|
Phone string `json:"phone"`
|
|
Email string `json:"email"`
|
|
}
|
|
if c.ShouldBindJSON(&req) != nil {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
if err := svc.UpdateUser(c.Request.Context(), service.UserInput{ID: req.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, AuthorityID: req.AuthorityID, AuthorityIDs: req.AuthorityIDs, Enable: req.Enable, Phone: req.Phone, Email: req.Email}); err != nil {
|
|
fail(c, "修改失败")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.PUT("/setSelfInfo", func(c *gin.Context) {
|
|
claims := currentClaims(c)
|
|
if claims == nil {
|
|
noAuth(c, "未登录或非法访问")
|
|
return
|
|
}
|
|
var req struct {
|
|
NickName string `json:"nickName"`
|
|
HeaderImg string `json:"headerImg"`
|
|
Phone string `json:"phone"`
|
|
Email string `json:"email"`
|
|
}
|
|
if c.ShouldBindJSON(&req) != nil {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
if err := svc.UpdateUser(c.Request.Context(), service.UserInput{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: 1}); err != nil {
|
|
fail(c, "修改失败")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.DELETE("/deleteUser", func(c *gin.Context) {
|
|
var req struct {
|
|
ID uint `form:"id" json:"id"`
|
|
}
|
|
_ = c.ShouldBind(&req)
|
|
if req.ID == 0 {
|
|
var body struct {
|
|
ID uint `json:"ID"`
|
|
}
|
|
_ = c.ShouldBindJSON(&body)
|
|
req.ID = body.ID
|
|
}
|
|
if err := svc.DeleteUser(c.Request.Context(), req.ID); err != nil {
|
|
fail(c, "删除失败")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.POST("/resetPassword", func(c *gin.Context) {
|
|
var req struct {
|
|
ID uint `json:"ID"`
|
|
Password string `json:"password"`
|
|
}
|
|
if c.ShouldBindJSON(&req) != nil {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
if err := svc.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
|
|
fail(c, "重置失败")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.POST("/changePassword", func(c *gin.Context) {
|
|
claims := currentClaims(c)
|
|
var req struct {
|
|
Password string `json:"password"`
|
|
NewPassword string `json:"newPassword"`
|
|
}
|
|
if claims == nil || c.ShouldBindJSON(&req) != nil {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
if err := svc.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
|
|
fail(c, "修改失败,原密码与当前账户不符")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.PUT("/setSelfSetting", func(c *gin.Context) {
|
|
claims := currentClaims(c)
|
|
var setting map[string]any
|
|
if claims == nil || c.ShouldBindJSON(&setting) != nil {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
if err := svc.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
|
|
fail(c, "设置失败")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.POST("/setUserAuthorities", func(c *gin.Context) {
|
|
var req struct {
|
|
ID uint `json:"ID"`
|
|
AuthorityIDs []uint `json:"authorityIds"`
|
|
}
|
|
if c.ShouldBindJSON(&req) != nil || req.ID == 0 || len(req.AuthorityIDs) == 0 {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
if err := svc.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
|
|
fail(c, "修改失败")
|
|
return
|
|
}
|
|
ok(c)
|
|
})
|
|
user.POST("/setUserAuthority", func(c *gin.Context) {
|
|
claims := currentClaims(c)
|
|
var req struct {
|
|
AuthorityID uint `json:"authorityId"`
|
|
}
|
|
if claims == nil || c.ShouldBindJSON(&req) != nil || req.AuthorityID == 0 {
|
|
fail(c, "参数错误")
|
|
return
|
|
}
|
|
login, err := svc.SwitchAuthority(c.Request.Context(), claims.ID, req.AuthorityID)
|
|
if err != nil {
|
|
fail(c, err.Error())
|
|
return
|
|
}
|
|
c.Header("new-token", login.Token)
|
|
c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
|
|
maxAge := int(time.Until(time.UnixMilli(login.ExpiresAt)).Seconds())
|
|
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: login.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
|
|
ok(c)
|
|
})
|
|
user.GET("/getUserInfo", func(c *gin.Context) {
|
|
claims := currentClaims(c)
|
|
if claims == nil {
|
|
noAuth(c, "未登录或非法访问")
|
|
return
|
|
}
|
|
result, err := svc.User(c.Request.Context(), claims.ID)
|
|
if err != nil {
|
|
fail(c, "获取失败")
|
|
return
|
|
}
|
|
writeResult(c, codeSuccess, gin.H{"userInfo": result}, "获取成功")
|
|
})
|
|
menu := group.Group("/menu")
|
|
menu.POST("/getMenu", func(c *gin.Context) {
|
|
claims := currentClaims(c)
|
|
if claims == nil {
|
|
noAuth(c, "未登录或非法访问")
|
|
return
|
|
}
|
|
menus, err := svc.Menus(c.Request.Context(), claims.AuthorityID)
|
|
if err != nil {
|
|
fail(c, "获取失败")
|
|
return
|
|
}
|
|
writeResult(c, codeSuccess, gin.H{"menus": menus}, "获取成功")
|
|
})
|
|
jwt := group.Group("/jwt")
|
|
jwt.POST("/jsonInBlacklist", func(c *gin.Context) {
|
|
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
|
|
ok(c)
|
|
})
|
|
}
|
|
|
|
func (s *GinServer) Start(ctx context.Context) error {
|
|
listener, err := net.Listen(s.network, s.address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.listener = listener
|
|
s.server.BaseContext = func(net.Listener) context.Context { return ctx }
|
|
s.logger.InfoContext(ctx, "Gin HTTP server listening", "addr", listener.Addr().String())
|
|
if err := s.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *GinServer) Stop(ctx context.Context) error {
|
|
s.logger.InfoContext(ctx, "Gin HTTP server stopping")
|
|
return s.server.Shutdown(ctx)
|
|
}
|
|
|
|
func ginRequestMeta() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
requestID := c.GetHeader("X-Request-Id")
|
|
if requestID == "" {
|
|
requestID = uuid.NewString()
|
|
}
|
|
c.Header("X-Request-Id", requestID)
|
|
c.Set("request_id", requestID)
|
|
c.Next()
|
|
}
|
|
}
|