mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
[优化] 工具整理
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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("<p>您好,你正在进行%s邮箱验证。</p>"+
|
||||
"<p>您的验证码为: <strong>%s</strong></p>"+
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
|
||||
err := common.SendEmail(subject, email, content)
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", 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("<p>您好,你正在进行%s密码重置。</p>"+
|
||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
|
||||
err := common.SendEmail(subject, email, content)
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", 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": "",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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": "验证码错误或已过期",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "html/template"
|
||||
|
||||
func UnescapeHTML(x string) interface{} {
|
||||
return template.HTML(x)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package security
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package common
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type verificationValue struct {
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package validation
|
||||
|
||||
import "github.com/go-playground/validator/v10"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
declare module 'echarts-maps/world.js';
|
||||
@@ -71,13 +71,15 @@
|
||||
* `model/`:模型定义与持久化
|
||||
* `router/`:路由注册
|
||||
* `middleware/`:认证、鉴权、限流等横切逻辑
|
||||
* `common/`:配置与通用工具
|
||||
* `common/`:配置、全局运行时状态与初始化入口
|
||||
* `utils/`:纯工具函数与通用 helper;按功能聚合,多个同类 helper 应拆到对应子目录
|
||||
|
||||
禁止:
|
||||
|
||||
* 在 `controller/` 堆积业务逻辑
|
||||
* 在 `middleware/` 中实现业务流程
|
||||
* 为简单需求新增平台层抽象
|
||||
* 在 `common/` 混放不依赖全局状态的纯工具实现
|
||||
|
||||
### 3.2 Agent
|
||||
|
||||
|
||||
Reference in New Issue
Block a user