mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +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 (
|
import (
|
||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
"atsflare/model"
|
"atsflare/model"
|
||||||
|
"atsflare/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -75,7 +76,7 @@ func UploadFile(c *gin.Context) {
|
|||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
filename := filepath.Base(file.Filename)
|
filename := filepath.Base(file.Filename)
|
||||||
ext := filepath.Ext(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
|
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 {
|
if err := c.SaveUploadedFile(file, savePath); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
"atsflare/model"
|
"atsflare/model"
|
||||||
|
"atsflare/utils/mail"
|
||||||
|
"atsflare/utils/security"
|
||||||
|
"atsflare/utils/validation"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -62,7 +65,7 @@ func GetAbout(c *gin.Context) {
|
|||||||
|
|
||||||
func SendEmailVerification(c *gin.Context) {
|
func SendEmailVerification(c *gin.Context) {
|
||||||
email := c.Query("email")
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "无效的参数",
|
"message": "无效的参数",
|
||||||
@@ -76,13 +79,13 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
code := common.GenerateVerificationCode(6)
|
code := security.GenerateVerificationCode(6)
|
||||||
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
|
security.RegisterVerificationCodeWithKey(email, code, security.EmailVerificationPurpose)
|
||||||
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
|
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
|
||||||
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
||||||
"<p>您的验证码为: <strong>%s</strong></p>"+
|
"<p>您的验证码为: <strong>%s</strong></p>"+
|
||||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
|
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, security.VerificationValidMinutes)
|
||||||
err := common.SendEmail(subject, email, content)
|
err := mail.SendEmail(subject, email, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -99,7 +102,7 @@ func SendEmailVerification(c *gin.Context) {
|
|||||||
|
|
||||||
func SendPasswordResetEmail(c *gin.Context) {
|
func SendPasswordResetEmail(c *gin.Context) {
|
||||||
email := c.Query("email")
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "无效的参数",
|
"message": "无效的参数",
|
||||||
@@ -113,14 +116,14 @@ func SendPasswordResetEmail(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
code := common.GenerateVerificationCode(0)
|
code := security.GenerateVerificationCode(0)
|
||||||
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
security.RegisterVerificationCodeWithKey(email, code, security.PasswordResetPurpose)
|
||||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
||||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
||||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes)
|
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, security.VerificationValidMinutes)
|
||||||
err := common.SendEmail(subject, email, content)
|
err := mail.SendEmail(subject, email, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -150,14 +153,14 @@ func ResetPassword(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "重置链接非法或已过期",
|
"message": "重置链接非法或已过期",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
password := common.GenerateVerificationCode(12)
|
password := security.GenerateVerificationCode(12)
|
||||||
err = model.ResetUserPasswordByEmail(req.Email, password)
|
err = model.ResetUserPasswordByEmail(req.Email, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -166,7 +169,7 @@ func ResetPassword(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
common.DeleteKey(req.Email, common.PasswordResetPurpose)
|
security.DeleteKey(req.Email, security.PasswordResetPurpose)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
"atsflare/model"
|
"atsflare/model"
|
||||||
"atsflare/service"
|
"atsflare/service"
|
||||||
|
"atsflare/utils"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -178,7 +179,7 @@ func GetOptions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
options = append(options, &model.Option{
|
options = append(options, &model.Option{
|
||||||
Key: k,
|
Key: k,
|
||||||
Value: common.Interface2String(v),
|
Value: utils.Interface2String(v),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
common.OptionMapRWMutex.Unlock()
|
common.OptionMapRWMutex.Unlock()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
"atsflare/model"
|
"atsflare/model"
|
||||||
|
"atsflare/utils/security"
|
||||||
|
"atsflare/utils/validation"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -128,7 +130,7 @@ func Register(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := common.Validate.Struct(&user); err != nil {
|
if err := validation.Validate.Struct(&user); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "输入不合法 " + err.Error(),
|
"message": "输入不合法 " + err.Error(),
|
||||||
@@ -143,7 +145,7 @@ func Register(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "验证码错误或已过期",
|
"message": "验证码错误或已过期",
|
||||||
@@ -313,7 +315,7 @@ func UpdateUser(c *gin.Context) {
|
|||||||
if updatedUser.Password == "" {
|
if updatedUser.Password == "" {
|
||||||
updatedUser.Password = "$I_LOVE_U" // make Validator happy :)
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "输入不合法 " + err.Error(),
|
"message": "输入不合法 " + err.Error(),
|
||||||
@@ -374,7 +376,7 @@ func UpdateSelf(c *gin.Context) {
|
|||||||
if user.Password == "" {
|
if user.Password == "" {
|
||||||
user.Password = "$I_LOVE_U" // make Validator happy :)
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "输入不合法 " + err.Error(),
|
"message": "输入不合法 " + err.Error(),
|
||||||
@@ -622,7 +624,7 @@ func ManageUser(c *gin.Context) {
|
|||||||
func EmailBind(c *gin.Context) {
|
func EmailBind(c *gin.Context) {
|
||||||
email := c.Query("email")
|
email := c.Query("email")
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
|
if !security.VerifyCodeWithKey(email, code, security.EmailVerificationPurpose) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "验证码错误或已过期",
|
"message": "验证码错误或已过期",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
|
"atsflare/utils/ratelimit"
|
||||||
"context"
|
"context"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
|
|
||||||
var timeFormat = "2006-01-02T15:04:05.000Z"
|
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) {
|
func redisRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
|
"atsflare/utils/security"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -26,7 +27,7 @@ func createRootAccountIfNeed() error {
|
|||||||
//if user.Status != common.UserStatusEnabled {
|
//if user.Status != common.UserStatusEnabled {
|
||||||
if err := DB.First(&user).Error; err != nil {
|
if err := DB.First(&user).Error; err != nil {
|
||||||
slog.Info("no user exists, create a root user", "username", "root")
|
slog.Info("no user exists, create a root user", "username", "root")
|
||||||
hashedPassword, err := common.Password2Hash("123456")
|
hashedPassword, err := security.Password2Hash("123456")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"atsflare/common"
|
"atsflare/common"
|
||||||
|
"atsflare/utils/security"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -63,7 +64,7 @@ func DeleteUserById(id int) (err error) {
|
|||||||
func (user *User) Insert() error {
|
func (user *User) Insert() error {
|
||||||
var err error
|
var err error
|
||||||
if user.Password != "" {
|
if user.Password != "" {
|
||||||
user.Password, err = common.Password2Hash(user.Password)
|
user.Password, err = security.Password2Hash(user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ func (user *User) Insert() error {
|
|||||||
func (user *User) Update(updatePassword bool) error {
|
func (user *User) Update(updatePassword bool) error {
|
||||||
var err error
|
var err error
|
||||||
if updatePassword {
|
if updatePassword {
|
||||||
user.Password, err = common.Password2Hash(user.Password)
|
user.Password, err = security.Password2Hash(user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -102,7 +103,7 @@ func (user *User) ValidateAndFill() (err error) {
|
|||||||
return errors.New("用户名或密码为空")
|
return errors.New("用户名或密码为空")
|
||||||
}
|
}
|
||||||
DB.Where(User{Username: user.Username}).First(user)
|
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 {
|
if !okay || user.Status != common.UserStatusEnabled {
|
||||||
return errors.New("用户名或密码错误,或用户已被封禁")
|
return errors.New("用户名或密码错误,或用户已被封禁")
|
||||||
}
|
}
|
||||||
@@ -181,7 +182,7 @@ func ResetUserPasswordByEmail(email string, password string) error {
|
|||||||
if email == "" || password == "" {
|
if email == "" || password == "" {
|
||||||
return errors.New("邮箱地址或密码为空!")
|
return errors.New("邮箱地址或密码为空!")
|
||||||
}
|
}
|
||||||
hashedPassword, err := common.Password2Hash(password)
|
hashedPassword, err := security.Password2Hash(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"atsflare/common"
|
|
||||||
"atsflare/controller"
|
"atsflare/controller"
|
||||||
"atsflare/middleware"
|
"atsflare/middleware"
|
||||||
|
"atsflare/utils/embedfs"
|
||||||
"embed"
|
"embed"
|
||||||
"github.com/gin-contrib/static"
|
"github.com/gin-contrib/static"
|
||||||
"github.com/gin-gonic/gin"
|
"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)
|
fileDownloadRoute.GET("/upload/:file", middleware.DownloadRateLimit(), controller.DownloadFile)
|
||||||
router.Use(normalizeStaticExportDataNavigation())
|
router.Use(normalizeStaticExportDataNavigation())
|
||||||
router.Use(middleware.Cache())
|
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) {
|
router.NoRoute(func(c *gin.Context) {
|
||||||
if serveExportedPage(c, exportedBuildFS) {
|
if serveExportedPage(c, exportedBuildFS) {
|
||||||
return
|
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
|
package embedfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"github.com/gin-contrib/static"
|
"io/fs"
|
||||||
"io/fs"
|
"net/http"
|
||||||
"net/http"
|
"strings"
|
||||||
"strings"
|
|
||||||
)
|
"github.com/gin-contrib/static"
|
||||||
|
)
|
||||||
// Credit: https://github.com/gin-contrib/static/issues/19
|
|
||||||
|
// Credit: https://github.com/gin-contrib/static/issues/19
|
||||||
type embedFileSystem struct {
|
|
||||||
http.FileSystem
|
type fileSystem struct {
|
||||||
}
|
http.FileSystem
|
||||||
|
}
|
||||||
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
|
||||||
cleanPath := strings.TrimPrefix(path, prefix)
|
func (e fileSystem) Exists(prefix string, path string) bool {
|
||||||
cleanPath = strings.TrimPrefix(cleanPath, "/")
|
cleanPath := strings.TrimPrefix(path, prefix)
|
||||||
if cleanPath == "" {
|
cleanPath = strings.TrimPrefix(cleanPath, "/")
|
||||||
return false
|
if cleanPath == "" {
|
||||||
}
|
return false
|
||||||
|
}
|
||||||
_, err := e.Open(cleanPath)
|
|
||||||
if err != nil {
|
_, err := e.Open(cleanPath)
|
||||||
return false
|
if err != nil {
|
||||||
}
|
return false
|
||||||
return true
|
}
|
||||||
}
|
return true
|
||||||
|
}
|
||||||
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
|
||||||
efs, err := fs.Sub(fsEmbed, targetPath)
|
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
||||||
if err != nil {
|
efs, err := fs.Sub(fsEmbed, targetPath)
|
||||||
panic(err)
|
if err != nil {
|
||||||
}
|
panic(err)
|
||||||
return embedFileSystem{
|
}
|
||||||
FileSystem: http.FS(efs),
|
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 (
|
import (
|
||||||
|
"atsflare/common"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,21 +15,21 @@ func SendEmail(subject string, receiver string, content string) error {
|
|||||||
"From: %s<%s>\r\n"+
|
"From: %s<%s>\r\n"+
|
||||||
"Subject: %s\r\n"+
|
"Subject: %s\r\n"+
|
||||||
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||||
receiver, SystemName, SMTPAccount, encodedSubject, content))
|
receiver, common.SystemName, common.SMTPAccount, encodedSubject, content))
|
||||||
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
auth := smtp.PlainAuth("", common.SMTPAccount, common.SMTPToken, common.SMTPServer)
|
||||||
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
addr := fmt.Sprintf("%s:%d", common.SMTPServer, common.SMTPPort)
|
||||||
to := strings.Split(receiver, ";")
|
to := strings.Split(receiver, ";")
|
||||||
var err error
|
var err error
|
||||||
if SMTPPort == 465 {
|
if common.SMTPPort == 465 {
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client, err := smtp.NewClient(conn, SMTPServer)
|
client, err := smtp.NewClient(conn, common.SMTPServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -36,7 +37,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
|||||||
if err = client.Auth(auth); err != nil {
|
if err = client.Auth(auth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = client.Mail(SMTPAccount); err != nil {
|
if err = client.Mail(common.SMTPAccount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
receiverEmails := strings.Split(receiver, ";")
|
receiverEmails := strings.Split(receiver, ";")
|
||||||
@@ -58,7 +59,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
|
err = smtp.SendMail(addr, auth, common.SMTPAccount, to, mail)
|
||||||
}
|
}
|
||||||
return err
|
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 (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
@@ -52,19 +52,16 @@ func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration in
|
|||||||
if len(*queue) < maxRequestNum {
|
if len(*queue) < maxRequestNum {
|
||||||
*queue = append(*queue, now)
|
*queue = append(*queue, now)
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
if now-(*queue)[0] >= duration {
|
|
||||||
*queue = (*queue)[1:]
|
|
||||||
*queue = append(*queue, now)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
if now-(*queue)[0] >= duration {
|
||||||
s := make([]int64, 0, maxRequestNum)
|
*queue = (*queue)[1:]
|
||||||
l.store[key] = &s
|
*queue = append(*queue, now)
|
||||||
*(l.store[key]) = append(*(l.store[key]), now)
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
s := make([]int64, 0, maxRequestNum)
|
||||||
|
l.store[key] = &s
|
||||||
|
*(l.store[key]) = append(*(l.store[key]), now)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package common
|
package security
|
||||||
|
|
||||||
import "golang.org/x/crypto/bcrypt"
|
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 (
|
import (
|
||||||
"github.com/google/uuid"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type verificationValue struct {
|
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"
|
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';
|
'use client';
|
||||||
|
|
||||||
|
import * as echarts from 'echarts';
|
||||||
import type { EChartsOption } from 'echarts';
|
import type { EChartsOption } from 'echarts';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -272,14 +273,21 @@ export function WorldStage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
|
||||||
import('echarts-maps/world.js')
|
import('@/features/dashboard/data/world-geo.json')
|
||||||
.then(() => {
|
.then(({ default: worldGeoJson }) => {
|
||||||
|
if (!echarts.getMap('world')) {
|
||||||
|
echarts.registerMap('world', worldGeoJson);
|
||||||
|
}
|
||||||
|
if (!echarts.getMap('world')) {
|
||||||
|
throw new Error('world map registration failed');
|
||||||
|
}
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setMapReady(true);
|
setMapReady(true);
|
||||||
setMapFailed(false);
|
setMapFailed(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
|
console.error('Failed to register ECharts world map', error);
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setMapReady(false);
|
setMapReady(false);
|
||||||
setMapFailed(true);
|
setMapFailed(true);
|
||||||
@@ -435,6 +443,7 @@ export function WorldStage({
|
|||||||
map: 'world',
|
map: 'world',
|
||||||
roam: false,
|
roam: false,
|
||||||
silent: true,
|
silent: true,
|
||||||
|
regions: [],
|
||||||
top: 18,
|
top: 18,
|
||||||
bottom: 18,
|
bottom: 18,
|
||||||
left: 10,
|
left: 10,
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
declare module 'echarts-maps/world.js';
|
|
||||||
@@ -71,13 +71,15 @@
|
|||||||
* `model/`:模型定义与持久化
|
* `model/`:模型定义与持久化
|
||||||
* `router/`:路由注册
|
* `router/`:路由注册
|
||||||
* `middleware/`:认证、鉴权、限流等横切逻辑
|
* `middleware/`:认证、鉴权、限流等横切逻辑
|
||||||
* `common/`:配置与通用工具
|
* `common/`:配置、全局运行时状态与初始化入口
|
||||||
|
* `utils/`:纯工具函数与通用 helper;按功能聚合,多个同类 helper 应拆到对应子目录
|
||||||
|
|
||||||
禁止:
|
禁止:
|
||||||
|
|
||||||
* 在 `controller/` 堆积业务逻辑
|
* 在 `controller/` 堆积业务逻辑
|
||||||
* 在 `middleware/` 中实现业务流程
|
* 在 `middleware/` 中实现业务流程
|
||||||
* 为简单需求新增平台层抽象
|
* 为简单需求新增平台层抽象
|
||||||
|
* 在 `common/` 混放不依赖全局状态的纯工具实现
|
||||||
|
|
||||||
### 3.2 Agent
|
### 3.2 Agent
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user