diff --git a/atsf_server/common/utils.go b/atsf_server/common/utils.go deleted file mode 100644 index 9f8a8478..00000000 --- a/atsf_server/common/utils.go +++ /dev/null @@ -1,141 +0,0 @@ -package common - -import ( - "fmt" - "github.com/google/uuid" - "html/template" - "log/slog" - "net" - "os/exec" - "runtime" - "strconv" - "strings" -) - -func OpenBrowser(url string) { - var err error - - switch runtime.GOOS { - case "linux": - err = exec.Command("xdg-open", url).Start() - case "windows": - err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - err = exec.Command("open", url).Start() - } - if err != nil { - slog.Error("open browser failed", "error", err) - } -} - -func GetIp() (ip string) { - ips, err := net.InterfaceAddrs() - if err != nil { - slog.Error("get interface addresses failed", "error", err) - return ip - } - - for _, a := range ips { - if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { - if ipNet.IP.To4() != nil { - ip = ipNet.IP.String() - if strings.HasPrefix(ip, "10") { - return - } - if strings.HasPrefix(ip, "172") { - return - } - if strings.HasPrefix(ip, "192.168") { - return - } - ip = "" - } - } - } - return -} - -var sizeKB = 1024 -var sizeMB = sizeKB * 1024 -var sizeGB = sizeMB * 1024 - -func Bytes2Size(num int64) string { - numStr := "" - unit := "B" - if num/int64(sizeGB) > 1 { - numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB)) - unit = "GB" - } else if num/int64(sizeMB) > 1 { - numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB))) - unit = "MB" - } else if num/int64(sizeKB) > 1 { - numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB))) - unit = "KB" - } else { - numStr = fmt.Sprintf("%d", num) - } - return numStr + " " + unit -} - -func Seconds2Time(num int) (time string) { - if num/31104000 > 0 { - time += strconv.Itoa(num/31104000) + " 年 " - num %= 31104000 - } - if num/2592000 > 0 { - time += strconv.Itoa(num/2592000) + " 个月 " - num %= 2592000 - } - if num/86400 > 0 { - time += strconv.Itoa(num/86400) + " 天 " - num %= 86400 - } - if num/3600 > 0 { - time += strconv.Itoa(num/3600) + " 小时 " - num %= 3600 - } - if num/60 > 0 { - time += strconv.Itoa(num/60) + " 分钟 " - num %= 60 - } - time += strconv.Itoa(num) + " 秒" - return -} - -func Interface2String(inter interface{}) string { - switch inter.(type) { - case string: - return inter.(string) - case int: - return fmt.Sprintf("%d", inter.(int)) - case float64: - return fmt.Sprintf("%f", inter.(float64)) - } - return "Not Implemented" -} - -func UnescapeHTML(x string) interface{} { - return template.HTML(x) -} - -func IntMax(a int, b int) int { - if a >= b { - return a - } else { - return b - } -} - -func GetUUID() string { - code := uuid.New().String() - code = strings.Replace(code, "-", "", -1) - return code -} - -func Max(a int, b int) int { - if a >= b { - return a - } else { - return b - } -} diff --git a/atsf_server/controller/file.go b/atsf_server/controller/file.go index 2514890e..8988989b 100644 --- a/atsf_server/controller/file.go +++ b/atsf_server/controller/file.go @@ -3,6 +3,7 @@ package controller import ( "atsflare/common" "atsflare/model" + "atsflare/utils" "fmt" "github.com/gin-gonic/gin" "net/http" @@ -75,7 +76,7 @@ func UploadFile(c *gin.Context) { for _, file := range files { filename := filepath.Base(file.Filename) ext := filepath.Ext(filename) - link := common.GetUUID() + ext + link := utils.GetUUID() + ext savePath := filepath.Join(uploadPath, link) // both parts are checked, so this path should be safe to use if err := c.SaveUploadedFile(file, savePath); err != nil { c.JSON(http.StatusOK, gin.H{ diff --git a/atsf_server/controller/misc.go b/atsf_server/controller/misc.go index 7c692ef8..698f2726 100644 --- a/atsf_server/controller/misc.go +++ b/atsf_server/controller/misc.go @@ -3,6 +3,9 @@ package controller import ( "atsflare/common" "atsflare/model" + "atsflare/utils/mail" + "atsflare/utils/security" + "atsflare/utils/validation" "encoding/json" "fmt" "github.com/gin-gonic/gin" @@ -62,7 +65,7 @@ func GetAbout(c *gin.Context) { func SendEmailVerification(c *gin.Context) { email := c.Query("email") - if err := common.Validate.Var(email, "required,email"); err != nil { + if err := validation.Validate.Var(email, "required,email"); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无效的参数", @@ -76,13 +79,13 @@ func SendEmailVerification(c *gin.Context) { }) return } - code := common.GenerateVerificationCode(6) - common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose) + code := security.GenerateVerificationCode(6) + security.RegisterVerificationCodeWithKey(email, code, security.EmailVerificationPurpose) subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName) content := fmt.Sprintf("

您好,你正在进行%s邮箱验证。

"+ "

您的验证码为: %s

"+ - "

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

", common.SystemName, code, common.VerificationValidMinutes) - err := common.SendEmail(subject, email, content) + "

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

", common.SystemName, code, security.VerificationValidMinutes) + err := mail.SendEmail(subject, email, content) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -99,7 +102,7 @@ func SendEmailVerification(c *gin.Context) { func SendPasswordResetEmail(c *gin.Context) { email := c.Query("email") - if err := common.Validate.Var(email, "required,email"); err != nil { + if err := validation.Validate.Var(email, "required,email"); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无效的参数", @@ -113,14 +116,14 @@ func SendPasswordResetEmail(c *gin.Context) { }) return } - code := common.GenerateVerificationCode(0) - common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose) + code := security.GenerateVerificationCode(0) + security.RegisterVerificationCodeWithKey(email, code, security.PasswordResetPurpose) link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code) subject := fmt.Sprintf("%s密码重置", common.SystemName) content := fmt.Sprintf("

您好,你正在进行%s密码重置。

"+ "

点击此处进行密码重置。

"+ - "

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

", common.SystemName, link, common.VerificationValidMinutes) - err := common.SendEmail(subject, email, content) + "

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

", common.SystemName, link, security.VerificationValidMinutes) + err := mail.SendEmail(subject, email, content) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -150,14 +153,14 @@ func ResetPassword(c *gin.Context) { }) return } - if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) { + if !security.VerifyCodeWithKey(req.Email, req.Token, security.PasswordResetPurpose) { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "重置链接非法或已过期", }) return } - password := common.GenerateVerificationCode(12) + password := security.GenerateVerificationCode(12) err = model.ResetUserPasswordByEmail(req.Email, password) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -166,7 +169,7 @@ func ResetPassword(c *gin.Context) { }) return } - common.DeleteKey(req.Email, common.PasswordResetPurpose) + security.DeleteKey(req.Email, security.PasswordResetPurpose) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", diff --git a/atsf_server/controller/option.go b/atsf_server/controller/option.go index 3f296ff7..d223f5b5 100644 --- a/atsf_server/controller/option.go +++ b/atsf_server/controller/option.go @@ -4,6 +4,7 @@ import ( "atsflare/common" "atsflare/model" "atsflare/service" + "atsflare/utils" "encoding/json" "fmt" "github.com/gin-gonic/gin" @@ -178,7 +179,7 @@ func GetOptions(c *gin.Context) { } options = append(options, &model.Option{ Key: k, - Value: common.Interface2String(v), + Value: utils.Interface2String(v), }) } common.OptionMapRWMutex.Unlock() diff --git a/atsf_server/controller/user.go b/atsf_server/controller/user.go index d22fb5bd..4adec4fe 100644 --- a/atsf_server/controller/user.go +++ b/atsf_server/controller/user.go @@ -3,6 +3,8 @@ package controller import ( "atsflare/common" "atsflare/model" + "atsflare/utils/security" + "atsflare/utils/validation" "encoding/json" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" @@ -128,7 +130,7 @@ func Register(c *gin.Context) { }) return } - if err := common.Validate.Struct(&user); err != nil { + if err := validation.Validate.Struct(&user); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "输入不合法 " + err.Error(), @@ -143,7 +145,7 @@ func Register(c *gin.Context) { }) return } - if !common.VerifyCodeWithKey(user.Email, user.VerificationCode, common.EmailVerificationPurpose) { + if !security.VerifyCodeWithKey(user.Email, user.VerificationCode, security.EmailVerificationPurpose) { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "验证码错误或已过期", @@ -313,7 +315,7 @@ func UpdateUser(c *gin.Context) { if updatedUser.Password == "" { updatedUser.Password = "$I_LOVE_U" // make Validator happy :) } - if err := common.Validate.Struct(&updatedUser); err != nil { + if err := validation.Validate.Struct(&updatedUser); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "输入不合法 " + err.Error(), @@ -374,7 +376,7 @@ func UpdateSelf(c *gin.Context) { if user.Password == "" { user.Password = "$I_LOVE_U" // make Validator happy :) } - if err := common.Validate.Struct(&user); err != nil { + if err := validation.Validate.Struct(&user); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "输入不合法 " + err.Error(), @@ -622,7 +624,7 @@ func ManageUser(c *gin.Context) { func EmailBind(c *gin.Context) { email := c.Query("email") code := c.Query("code") - if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) { + if !security.VerifyCodeWithKey(email, code, security.EmailVerificationPurpose) { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "验证码错误或已过期", diff --git a/atsf_server/middleware/rate-limit.go b/atsf_server/middleware/rate-limit.go index 538ee945..559bd35f 100644 --- a/atsf_server/middleware/rate-limit.go +++ b/atsf_server/middleware/rate-limit.go @@ -2,6 +2,7 @@ package middleware import ( "atsflare/common" + "atsflare/utils/ratelimit" "context" "github.com/gin-gonic/gin" "log/slog" @@ -11,7 +12,7 @@ import ( var timeFormat = "2006-01-02T15:04:05.000Z" -var inMemoryRateLimiter common.InMemoryRateLimiter +var inMemoryRateLimiter ratelimit.InMemoryRateLimiter func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) { ctx := context.Background() diff --git a/atsf_server/model/main.go b/atsf_server/model/main.go index 93eb300c..200732e0 100644 --- a/atsf_server/model/main.go +++ b/atsf_server/model/main.go @@ -2,6 +2,7 @@ package model import ( "atsflare/common" + "atsflare/utils/security" "github.com/glebarez/sqlite" "gorm.io/driver/mysql" "gorm.io/gorm" @@ -26,7 +27,7 @@ func createRootAccountIfNeed() error { //if user.Status != common.UserStatusEnabled { if err := DB.First(&user).Error; err != nil { slog.Info("no user exists, create a root user", "username", "root") - hashedPassword, err := common.Password2Hash("123456") + hashedPassword, err := security.Password2Hash("123456") if err != nil { return err } diff --git a/atsf_server/model/user.go b/atsf_server/model/user.go index 7ce73904..c99ce9b3 100644 --- a/atsf_server/model/user.go +++ b/atsf_server/model/user.go @@ -2,6 +2,7 @@ package model import ( "atsflare/common" + "atsflare/utils/security" "errors" "strings" ) @@ -63,7 +64,7 @@ func DeleteUserById(id int) (err error) { func (user *User) Insert() error { var err error if user.Password != "" { - user.Password, err = common.Password2Hash(user.Password) + user.Password, err = security.Password2Hash(user.Password) if err != nil { return err } @@ -75,7 +76,7 @@ func (user *User) Insert() error { func (user *User) Update(updatePassword bool) error { var err error if updatePassword { - user.Password, err = common.Password2Hash(user.Password) + user.Password, err = security.Password2Hash(user.Password) if err != nil { return err } @@ -102,7 +103,7 @@ func (user *User) ValidateAndFill() (err error) { return errors.New("用户名或密码为空") } DB.Where(User{Username: user.Username}).First(user) - okay := common.ValidatePasswordAndHash(password, user.Password) + okay := security.ValidatePasswordAndHash(password, user.Password) if !okay || user.Status != common.UserStatusEnabled { return errors.New("用户名或密码错误,或用户已被封禁") } @@ -181,7 +182,7 @@ func ResetUserPasswordByEmail(email string, password string) error { if email == "" || password == "" { return errors.New("邮箱地址或密码为空!") } - hashedPassword, err := common.Password2Hash(password) + hashedPassword, err := security.Password2Hash(password) if err != nil { return err } diff --git a/atsf_server/router/web-router.go b/atsf_server/router/web-router.go index cff055cf..d5c7f038 100644 --- a/atsf_server/router/web-router.go +++ b/atsf_server/router/web-router.go @@ -1,9 +1,9 @@ package router import ( - "atsflare/common" "atsflare/controller" "atsflare/middleware" + "atsflare/utils/embedfs" "embed" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" @@ -24,7 +24,7 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { fileDownloadRoute.GET("/upload/:file", middleware.DownloadRateLimit(), controller.DownloadFile) router.Use(normalizeStaticExportDataNavigation()) router.Use(middleware.Cache()) - router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) + router.Use(static.Serve("/", embedfs.EmbedFolder(buildFS, "web/build"))) router.NoRoute(func(c *gin.Context) { if serveExportedPage(c, exportedBuildFS) { return diff --git a/atsf_server/utils/browser.go b/atsf_server/utils/browser.go new file mode 100644 index 00000000..48c00ea3 --- /dev/null +++ b/atsf_server/utils/browser.go @@ -0,0 +1,23 @@ +package utils + +import ( + "log/slog" + "os/exec" + "runtime" +) + +func OpenBrowser(url string) { + var err error + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + } + if err != nil { + slog.Error("open browser failed", "error", err) + } +} diff --git a/atsf_server/common/embed-file-system.go b/atsf_server/utils/embedfs/static.go similarity index 76% rename from atsf_server/common/embed-file-system.go rename to atsf_server/utils/embedfs/static.go index 0c45367d..2ac93481 100644 --- a/atsf_server/common/embed-file-system.go +++ b/atsf_server/utils/embedfs/static.go @@ -1,39 +1,40 @@ -package common - -import ( - "embed" - "github.com/gin-contrib/static" - "io/fs" - "net/http" - "strings" -) - -// Credit: https://github.com/gin-contrib/static/issues/19 - -type embedFileSystem struct { - http.FileSystem -} - -func (e embedFileSystem) Exists(prefix string, path string) bool { - cleanPath := strings.TrimPrefix(path, prefix) - cleanPath = strings.TrimPrefix(cleanPath, "/") - if cleanPath == "" { - return false - } - - _, err := e.Open(cleanPath) - if err != nil { - return false - } - return true -} - -func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem { - efs, err := fs.Sub(fsEmbed, targetPath) - if err != nil { - panic(err) - } - return embedFileSystem{ - FileSystem: http.FS(efs), - } -} +package embedfs + +import ( + "embed" + "io/fs" + "net/http" + "strings" + + "github.com/gin-contrib/static" +) + +// Credit: https://github.com/gin-contrib/static/issues/19 + +type fileSystem struct { + http.FileSystem +} + +func (e fileSystem) Exists(prefix string, path string) bool { + cleanPath := strings.TrimPrefix(path, prefix) + cleanPath = strings.TrimPrefix(cleanPath, "/") + if cleanPath == "" { + return false + } + + _, err := e.Open(cleanPath) + if err != nil { + return false + } + return true +} + +func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem { + efs, err := fs.Sub(fsEmbed, targetPath) + if err != nil { + panic(err) + } + return fileSystem{ + FileSystem: http.FS(efs), + } +} diff --git a/atsf_server/utils/format.go b/atsf_server/utils/format.go new file mode 100644 index 00000000..ee6b0f96 --- /dev/null +++ b/atsf_server/utils/format.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "strconv" +) + +var sizeKB = 1024 +var sizeMB = sizeKB * 1024 +var sizeGB = sizeMB * 1024 + +func Bytes2Size(num int64) string { + numStr := "" + unit := "B" + if num/int64(sizeGB) > 1 { + numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB)) + unit = "GB" + } else if num/int64(sizeMB) > 1 { + numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB))) + unit = "MB" + } else if num/int64(sizeKB) > 1 { + numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB))) + unit = "KB" + } else { + numStr = fmt.Sprintf("%d", num) + } + return numStr + " " + unit +} + +func Seconds2Time(num int) (time string) { + if num/31104000 > 0 { + time += strconv.Itoa(num/31104000) + " 年 " + num %= 31104000 + } + if num/2592000 > 0 { + time += strconv.Itoa(num/2592000) + " 个月 " + num %= 2592000 + } + if num/86400 > 0 { + time += strconv.Itoa(num/86400) + " 天 " + num %= 86400 + } + if num/3600 > 0 { + time += strconv.Itoa(num/3600) + " 小时 " + num %= 3600 + } + if num/60 > 0 { + time += strconv.Itoa(num/60) + " 分钟 " + num %= 60 + } + time += strconv.Itoa(num) + " 秒" + return +} diff --git a/atsf_server/utils/html.go b/atsf_server/utils/html.go new file mode 100644 index 00000000..7dc2861f --- /dev/null +++ b/atsf_server/utils/html.go @@ -0,0 +1,7 @@ +package utils + +import "html/template" + +func UnescapeHTML(x string) interface{} { + return template.HTML(x) +} diff --git a/atsf_server/common/email.go b/atsf_server/utils/mail/email.go similarity index 63% rename from atsf_server/common/email.go rename to atsf_server/utils/mail/email.go index 3f2a47e4..611fd651 100644 --- a/atsf_server/common/email.go +++ b/atsf_server/utils/mail/email.go @@ -1,6 +1,7 @@ -package common +package mail import ( + "atsflare/common" "crypto/tls" "encoding/base64" "fmt" @@ -14,21 +15,21 @@ func SendEmail(subject string, receiver string, content string) error { "From: %s<%s>\r\n"+ "Subject: %s\r\n"+ "Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", - receiver, SystemName, SMTPAccount, encodedSubject, content)) - auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer) - addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort) + receiver, common.SystemName, common.SMTPAccount, encodedSubject, content)) + auth := smtp.PlainAuth("", common.SMTPAccount, common.SMTPToken, common.SMTPServer) + addr := fmt.Sprintf("%s:%d", common.SMTPServer, common.SMTPPort) to := strings.Split(receiver, ";") var err error - if SMTPPort == 465 { + if common.SMTPPort == 465 { tlsConfig := &tls.Config{ InsecureSkipVerify: true, - ServerName: SMTPServer, + ServerName: common.SMTPServer, } - conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig) + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", common.SMTPServer, common.SMTPPort), tlsConfig) if err != nil { return err } - client, err := smtp.NewClient(conn, SMTPServer) + client, err := smtp.NewClient(conn, common.SMTPServer) if err != nil { return err } @@ -36,7 +37,7 @@ func SendEmail(subject string, receiver string, content string) error { if err = client.Auth(auth); err != nil { return err } - if err = client.Mail(SMTPAccount); err != nil { + if err = client.Mail(common.SMTPAccount); err != nil { return err } receiverEmails := strings.Split(receiver, ";") @@ -58,7 +59,7 @@ func SendEmail(subject string, receiver string, content string) error { return err } } else { - err = smtp.SendMail(addr, auth, SMTPAccount, to, mail) + err = smtp.SendMail(addr, auth, common.SMTPAccount, to, mail) } return err } diff --git a/atsf_server/utils/math.go b/atsf_server/utils/math.go new file mode 100644 index 00000000..c2e31406 --- /dev/null +++ b/atsf_server/utils/math.go @@ -0,0 +1,15 @@ +package utils + +func IntMax(a int, b int) int { + if a >= b { + return a + } + return b +} + +func Max(a int, b int) int { + if a >= b { + return a + } + return b +} diff --git a/atsf_server/utils/network.go b/atsf_server/utils/network.go new file mode 100644 index 00000000..9d2060e7 --- /dev/null +++ b/atsf_server/utils/network.go @@ -0,0 +1,34 @@ +package utils + +import ( + "log/slog" + "net" + "strings" +) + +func GetIp() (ip string) { + ips, err := net.InterfaceAddrs() + if err != nil { + slog.Error("get interface addresses failed", "error", err) + return ip + } + + for _, a := range ips { + if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { + if ipNet.IP.To4() != nil { + ip = ipNet.IP.String() + if strings.HasPrefix(ip, "10") { + return + } + if strings.HasPrefix(ip, "172") { + return + } + if strings.HasPrefix(ip, "192.168") { + return + } + ip = "" + } + } + } + return +} diff --git a/atsf_server/common/rate-limit.go b/atsf_server/utils/ratelimit/in_memory.go similarity index 80% rename from atsf_server/common/rate-limit.go rename to atsf_server/utils/ratelimit/in_memory.go index 301c101c..a6334757 100644 --- a/atsf_server/common/rate-limit.go +++ b/atsf_server/utils/ratelimit/in_memory.go @@ -1,4 +1,4 @@ -package common +package ratelimit import ( "sync" @@ -52,19 +52,16 @@ func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration in if len(*queue) < maxRequestNum { *queue = append(*queue, now) return true - } else { - if now-(*queue)[0] >= duration { - *queue = (*queue)[1:] - *queue = append(*queue, now) - return true - } else { - return false - } } - } else { - s := make([]int64, 0, maxRequestNum) - l.store[key] = &s - *(l.store[key]) = append(*(l.store[key]), now) + if now-(*queue)[0] >= duration { + *queue = (*queue)[1:] + *queue = append(*queue, now) + return true + } + return false } + s := make([]int64, 0, maxRequestNum) + l.store[key] = &s + *(l.store[key]) = append(*(l.store[key]), now) return true } diff --git a/atsf_server/common/crypto.go b/atsf_server/utils/security/password.go similarity index 96% rename from atsf_server/common/crypto.go rename to atsf_server/utils/security/password.go index 45228416..85cff019 100644 --- a/atsf_server/common/crypto.go +++ b/atsf_server/utils/security/password.go @@ -1,4 +1,4 @@ -package common +package security import "golang.org/x/crypto/bcrypt" diff --git a/atsf_server/utils/security/random.go b/atsf_server/utils/security/random.go new file mode 100644 index 00000000..ade3d58b --- /dev/null +++ b/atsf_server/utils/security/random.go @@ -0,0 +1,37 @@ +package security + +import "crypto/rand" + +func GenerateRandomString(length int) string { + if length <= 0 { + return "" + } + const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + const n = byte(len(charset)) + const threshold = byte(256 - (256 % len(charset))) + + out := make([]byte, 0, length) + buf := make([]byte, length) + for len(out) < length { + if _, err := rand.Read(buf); err != nil { + return "" + } + for _, b := range buf { + if b < threshold { + out = append(out, charset[int(b%n)]) + if len(out) == length { + break + } + } + } + } + return string(out) +} + +func GeneratePassword() string { + return GenerateRandomString(12) +} + +func GenerateToken() string { + return GenerateRandomString(22) +} diff --git a/atsf_server/common/verification.go b/atsf_server/utils/security/verification.go similarity index 98% rename from atsf_server/common/verification.go rename to atsf_server/utils/security/verification.go index d8ccd6ea..e5eed22e 100644 --- a/atsf_server/common/verification.go +++ b/atsf_server/utils/security/verification.go @@ -1,10 +1,11 @@ -package common +package security import ( - "github.com/google/uuid" "strings" "sync" "time" + + "github.com/google/uuid" ) type verificationValue struct { diff --git a/atsf_server/utils/uuid.go b/atsf_server/utils/uuid.go new file mode 100644 index 00000000..0ada3793 --- /dev/null +++ b/atsf_server/utils/uuid.go @@ -0,0 +1,13 @@ +package utils + +import ( + "strings" + + "github.com/google/uuid" +) + +func GetUUID() string { + code := uuid.New().String() + code = strings.Replace(code, "-", "", -1) + return code +} diff --git a/atsf_server/common/validate.go b/atsf_server/utils/validation/validator.go similarity index 87% rename from atsf_server/common/validate.go rename to atsf_server/utils/validation/validator.go index b3c78591..3d08ff1c 100644 --- a/atsf_server/common/validate.go +++ b/atsf_server/utils/validation/validator.go @@ -1,4 +1,4 @@ -package common +package validation import "github.com/go-playground/validator/v10" diff --git a/atsf_server/utils/value.go b/atsf_server/utils/value.go new file mode 100644 index 00000000..e4c09cf6 --- /dev/null +++ b/atsf_server/utils/value.go @@ -0,0 +1,15 @@ +package utils + +import "fmt" + +func Interface2String(inter interface{}) string { + switch inter.(type) { + case string: + return inter.(string) + case int: + return fmt.Sprintf("%d", inter.(int)) + case float64: + return fmt.Sprintf("%f", inter.(float64)) + } + return "Not Implemented" +} diff --git a/atsf_server/web/features/dashboard/components/world-stage.tsx b/atsf_server/web/features/dashboard/components/world-stage.tsx index 25e10f68..3b465e4f 100644 --- a/atsf_server/web/features/dashboard/components/world-stage.tsx +++ b/atsf_server/web/features/dashboard/components/world-stage.tsx @@ -1,5 +1,6 @@ 'use client'; +import * as echarts from 'echarts'; import type { EChartsOption } from 'echarts'; import ReactECharts from 'echarts-for-react'; import Link from 'next/link'; @@ -272,14 +273,21 @@ export function WorldStage({ useEffect(() => { let disposed = false; - import('echarts-maps/world.js') - .then(() => { + import('@/features/dashboard/data/world-geo.json') + .then(({ default: worldGeoJson }) => { + if (!echarts.getMap('world')) { + echarts.registerMap('world', worldGeoJson); + } + if (!echarts.getMap('world')) { + throw new Error('world map registration failed'); + } if (!disposed) { setMapReady(true); setMapFailed(false); } }) - .catch(() => { + .catch((error) => { + console.error('Failed to register ECharts world map', error); if (!disposed) { setMapReady(false); setMapFailed(true); @@ -435,6 +443,7 @@ export function WorldStage({ map: 'world', roam: false, silent: true, + regions: [], top: 18, bottom: 18, left: 10, diff --git a/atsf_server/web/types/echarts-maps.d.ts b/atsf_server/web/types/echarts-maps.d.ts deleted file mode 100644 index ec27fa83..00000000 --- a/atsf_server/web/types/echarts-maps.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'echarts-maps/world.js'; diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md index 8f9b78a7..bdc95b37 100644 --- a/docs/development-guidelines.md +++ b/docs/development-guidelines.md @@ -71,13 +71,15 @@ * `model/`:模型定义与持久化 * `router/`:路由注册 * `middleware/`:认证、鉴权、限流等横切逻辑 -* `common/`:配置与通用工具 +* `common/`:配置、全局运行时状态与初始化入口 +* `utils/`:纯工具函数与通用 helper;按功能聚合,多个同类 helper 应拆到对应子目录 禁止: * 在 `controller/` 堆积业务逻辑 * 在 `middleware/` 中实现业务流程 * 为简单需求新增平台层抽象 +* 在 `common/` 混放不依赖全局状态的纯工具实现 ### 3.2 Agent