mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 23:06:36 +08:00
质量优化
This commit is contained in:
+15
-13
@@ -17,36 +17,38 @@ linters:
|
||||
# 代码坏味道
|
||||
- dupl # 重复代码
|
||||
- mnd # 魔法数字
|
||||
- gocyclo # 圈复杂度
|
||||
- cyclop # 包/函数复杂度
|
||||
- nestif # if 嵌套太深
|
||||
- maintidx # 维护性指数
|
||||
- revive # 风格/命名/坏味道
|
||||
- gocritic # 各类代码问题
|
||||
- funlen # 函数过长
|
||||
|
||||
- gosec # 安全问题检查
|
||||
- bodyclose # HTTP response body 没有正确关闭
|
||||
- noctx # 没有传递 context.Context
|
||||
- contextcheck # 其他检查
|
||||
- sqlclosecheck # SQL rows 没有正确关闭
|
||||
- unconvert # 不必要的类型转换
|
||||
- nilerr # 函数返回 nil 错误
|
||||
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 150
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
threshold: 120
|
||||
|
||||
cyclop:
|
||||
max-complexity: 15
|
||||
max-complexity: 20
|
||||
package-average: 10
|
||||
|
||||
funlen:
|
||||
lines: 150
|
||||
statements: 80
|
||||
|
||||
nestif:
|
||||
min-complexity: 5
|
||||
|
||||
funlen:
|
||||
lines: 200
|
||||
statements: 100
|
||||
|
||||
mnd:
|
||||
checks:
|
||||
- argument
|
||||
- case
|
||||
- condition
|
||||
- operation
|
||||
- return
|
||||
- assign
|
||||
|
||||
@@ -16,8 +16,8 @@ build-embedded:
|
||||
go build -tags embed_frontend -o bin/wavelet main.go
|
||||
|
||||
code-check:
|
||||
cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0
|
||||
golangci-lint run
|
||||
cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0
|
||||
|
||||
build-test:
|
||||
@echo "==> Running frontend and backend build tests in parallel..."
|
||||
|
||||
@@ -118,7 +118,11 @@ func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
|
||||
|
||||
var sizeStr string
|
||||
if fi, err := os.Stat(name); err == nil {
|
||||
sizeStr = formatBytes(uint64(fi.Size()))
|
||||
size := fi.Size()
|
||||
if size < 0 {
|
||||
size = 0
|
||||
}
|
||||
sizeStr = formatBytes(uint64(size))
|
||||
} else {
|
||||
sizeStr = "0 B"
|
||||
}
|
||||
@@ -160,7 +164,11 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
|
||||
var sizeStr string
|
||||
var sizeBytes sql.NullInt64
|
||||
if err := gormDB.Raw("SELECT pg_database_size(current_database())").Scan(&sizeBytes).Error; err == nil && sizeBytes.Valid {
|
||||
sizeStr = formatBytes(uint64(sizeBytes.Int64))
|
||||
size := sizeBytes.Int64
|
||||
if size < 0 {
|
||||
size = 0
|
||||
}
|
||||
sizeStr = formatBytes(uint64(size))
|
||||
} else {
|
||||
sizeStr = "0 B"
|
||||
}
|
||||
|
||||
@@ -147,9 +147,12 @@ func GetSystemStatus(c *gin.Context) {
|
||||
numGoroutine := runtime.NumGoroutine()
|
||||
|
||||
var lastGCTime string
|
||||
if m.LastGC > 0 {
|
||||
switch {
|
||||
case m.LastGC > 0 && m.LastGC <= math.MaxInt64:
|
||||
lastGCTime = formatDuration(time.Since(time.Unix(0, int64(m.LastGC))))
|
||||
} else {
|
||||
case m.LastGC > 0:
|
||||
lastGCTime = "未知"
|
||||
default:
|
||||
lastGCTime = "无"
|
||||
}
|
||||
|
||||
@@ -286,7 +289,7 @@ func exportSQLite(c *gin.Context) {
|
||||
path = "./data/wavelet.db"
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
f, err := os.Open(path) //nolint:gosec // path is loaded from server startup configuration, not user input
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("无法打开数据库文件: "+err.Error()))
|
||||
return
|
||||
@@ -329,7 +332,7 @@ func exportPostgres(c *gin.Context) {
|
||||
dbCfg.Database,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(c.Request.Context(), pgDumpPath, args...)
|
||||
cmd := exec.CommandContext(c.Request.Context(), pgDumpPath, args...) //nolint:gosec // pgDumpPath is a looked up command path, args are from database configuration
|
||||
if dbCfg.Password != "" {
|
||||
cmd.Env = append(os.Environ(), "PGPASSWORD="+dbCfg.Password)
|
||||
} else {
|
||||
|
||||
@@ -275,7 +275,7 @@ func TestSMTP(c *gin.Context) {
|
||||
<p>If you received this message, your SMTP configuration is correct and mail sending is working properly.</p>
|
||||
<p>Sent from Wavelet.</p>`
|
||||
|
||||
logs, err := mail.SendMailWithLog(cfg, req.To, subject, body)
|
||||
logs, err := mail.SendMailWithLog(c.Request.Context(), cfg, req.To, subject, body)
|
||||
resp := TestSMTPResponse{
|
||||
Success: err == nil,
|
||||
Log: logs,
|
||||
|
||||
@@ -14,6 +14,6 @@ const (
|
||||
deleteUserFailed = "删除用户失败"
|
||||
usernameExists = "用户名已存在"
|
||||
usernameRequired = "用户名不能为空"
|
||||
passwordTooShort = "密码长度不能少于 8 位"
|
||||
passwordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
createUserFailed = "创建用户失败"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
package cap
|
||||
|
||||
const (
|
||||
errCapTokenMissing = "验证码验证失败,缺少验证码凭证"
|
||||
errCapTokenInvalidOrExpired = "验证码校验失败或已过期,请重试"
|
||||
errCapTokenMissing = "验证码验证失败,缺少验证码凭证" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errCapTokenInvalidOrExpired = "验证码校验失败或已过期,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ package oauth
|
||||
// OAuth 认证相关错误消息
|
||||
const (
|
||||
InvalidState = "非法登录请求"
|
||||
IDTokenVerifyFailed = "ID Token 验证失败"
|
||||
IDTokenVerifyFailed = "ID Token 验证失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
IDTokenVerifyFailedFormat = "%s: %w"
|
||||
NonceMismatch = "nonce 不匹配,可能存在重放攻击"
|
||||
NoActiveAuthSource = "未配置可用认证源"
|
||||
|
||||
@@ -469,7 +469,7 @@ func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthS
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := model.BindExternalAccount(&model.ExternalAccount{
|
||||
if err := model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
AuthSourceID: source.ID,
|
||||
UserID: user.ID,
|
||||
ExternalID: userInfo.Sub,
|
||||
@@ -488,7 +488,7 @@ func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthS
|
||||
func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.AuthSource, userInfo *model.OAuthUserInfo) {
|
||||
var user model.User
|
||||
|
||||
account, err := model.FindExternalAccount(source.ID, userInfo.Sub)
|
||||
account, err := model.FindExternalAccount(ctx, source.ID, userInfo.Sub)
|
||||
switch {
|
||||
case err == nil:
|
||||
if err := db.DB(ctx).First(&user, "id = ?", account.UserID).Error; err != nil {
|
||||
@@ -549,7 +549,7 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
if err := model.BindExternalAccount(&model.ExternalAccount{
|
||||
if err := model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
AuthSourceID: source.ID,
|
||||
UserID: user.ID,
|
||||
ExternalID: userInfo.Sub,
|
||||
|
||||
@@ -60,6 +60,14 @@ func RiskControlMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
const maxHTTPStatus = 999
|
||||
status := c.Writer.Status()
|
||||
if status < 0 {
|
||||
status = 0
|
||||
} else if status > maxHTTPStatus {
|
||||
status = maxHTTPStatus
|
||||
}
|
||||
|
||||
logItem := &UserAccessLog{
|
||||
ID: idgen.NextUint64ID(),
|
||||
UserID: userObj.ID, // 直接从 Context 获取已登录用户ID,避免数据库查询
|
||||
@@ -68,7 +76,7 @@ func RiskControlMiddleware() gin.HandlerFunc {
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
Headers: headersStr,
|
||||
Status: int32(c.Writer.Status()),
|
||||
Status: int32(status),
|
||||
Latency: latency,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -499,7 +499,7 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User,
|
||||
|
||||
if err := db.DB(ctx).Create(&newUpload).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveUploadRecordFailed))
|
||||
return true, nil
|
||||
return true, err
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "文件触发秒传成功! ID: %d, Path: %s", id, existing.FilePath)
|
||||
|
||||
+12
-12
@@ -7,21 +7,21 @@ package user
|
||||
const (
|
||||
errBindParamsFailed = "参数绑定失败"
|
||||
errInvalidParams = "无效的参数"
|
||||
errPasswordLoginDisabled = "管理员关闭了密码登录"
|
||||
errUsernameOrPasswordWrong = "用户名或密码错误"
|
||||
errPasswordLoginDisabled = "管理员关闭了密码登录" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errUsernameOrPasswordWrong = "用户名或密码错误" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errLoginEmailMissing = "该账号未绑定邮箱,请联系管理员绑定邮箱后再登录"
|
||||
errNeedEmailCodePrefix = "need_email_code:"
|
||||
errEmailCodeInvalidOrExpired = "验证码错误或已过期"
|
||||
errPasswordUpgradeFailed = "升级密码安全算法失败,请重试"
|
||||
errPasswordUpgradeFailed = "升级密码安全算法失败,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errSaveSessionFailed = "无法保存会话信息,请重试"
|
||||
errRegistrationDisabled = "管理员关闭了注册"
|
||||
errPasswordTooShort = "密码长度不能少于 8 位"
|
||||
errPasswordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errEmailOrCodeRequired = "邮箱或验证码未填写"
|
||||
errNewPasswordTooShort = "新密码长度不能少于 8 位"
|
||||
errNewPasswordTooShort = "新密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errLoginRequired = "请先登录"
|
||||
errUserNotFound = "未找到该用户"
|
||||
errOldPasswordIncorrect = "原密码不正确"
|
||||
errPasswordEncryptFailed = "密码加密失败,请重试"
|
||||
errOldPasswordIncorrect = "原密码不正确" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errPasswordEncryptFailed = "密码加密失败,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errEmailRequired = "邮箱地址不能为空"
|
||||
errUnsupportedEmailScene = "不支持的验证场景"
|
||||
errEmailAlreadyRegistered = "该邮箱已被注册"
|
||||
@@ -31,11 +31,11 @@ const (
|
||||
errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w"
|
||||
errGenerateEmailCodeFailed = "生成验证码失败,请重试"
|
||||
errDispatchEmailTaskFailed = "投递验证邮件发送任务失败,请重试"
|
||||
errTokenNameRequired = "令牌名称不能为空"
|
||||
errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制"
|
||||
errGenerateTokenFailed = "生成令牌失败"
|
||||
errInvalidTokenID = "无效的令牌ID"
|
||||
errTokenNotFoundOrForbidden = "令牌不存在或无权操作"
|
||||
errTokenNameRequired = "令牌名称不能为空" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errGenerateTokenFailed = "生成令牌失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errInvalidTokenID = "无效的令牌ID" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errTokenNotFoundOrForbidden = "令牌不存在或无权操作" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errTaskPayloadRequired = "任务参数不能为空"
|
||||
errInvalidJSONFormat = "无效的 JSON 格式: %w"
|
||||
errEmailTaskFieldsRequired = "to、subject、body 不能为空"
|
||||
|
||||
@@ -28,16 +28,16 @@ type sendEmailCodeRequest struct {
|
||||
Scene string `json:"scene" binding:"required"`
|
||||
}
|
||||
|
||||
func isEmailLoginVerificationEnabled() bool {
|
||||
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyEmailLoginVerificationEnabled)
|
||||
func isEmailLoginVerificationEnabled(ctx context.Context) bool {
|
||||
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func isEmailRegisterVerificationEnabled() bool {
|
||||
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyEmailRegisterVerificationEnabled)
|
||||
func isEmailRegisterVerificationEnabled(ctx context.Context) bool {
|
||||
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func SendEmailCode(c *gin.Context) {
|
||||
}
|
||||
|
||||
func validateRegisterEmailVerification(ctx context.Context, req *registerRequest) error {
|
||||
if !isEmailRegisterVerificationEnabled() {
|
||||
if !isEmailRegisterVerificationEnabled(ctx) {
|
||||
return nil
|
||||
}
|
||||
if req.Email == "" || req.Code == "" {
|
||||
@@ -228,7 +228,7 @@ func validateRegisterEmailVerification(ctx context.Context, req *registerRequest
|
||||
}
|
||||
|
||||
// completePendingOAuthBinding 完成登录后的 OAuth 待绑定绑定流程
|
||||
func completePendingOAuthBinding(session sessions.Session, user *model.User) {
|
||||
func completePendingOAuthBinding(ctx context.Context, session sessions.Session, user *model.User) {
|
||||
pendingSourceID := session.Get(oauth.PendingOAuthSourceIDKey)
|
||||
pendingExternalID := session.Get(oauth.PendingOAuthExternalIDKey)
|
||||
pendingExternalUsername := session.Get(oauth.PendingOAuthExternalUsernameKey)
|
||||
@@ -243,16 +243,20 @@ func completePendingOAuthBinding(session sessions.Session, user *model.User) {
|
||||
case uint64:
|
||||
sourceID = v
|
||||
case int:
|
||||
sourceID = uint64(v)
|
||||
if v >= 0 {
|
||||
sourceID = uint64(v)
|
||||
}
|
||||
case float64:
|
||||
sourceID = uint64(v)
|
||||
if v >= 0 && v <= 18446744073709551615.0 {
|
||||
sourceID = uint64(v)
|
||||
}
|
||||
}
|
||||
externalID, _ := pendingExternalID.(string)
|
||||
externalUsername, _ := pendingExternalUsername.(string)
|
||||
email, _ := pendingEmail.(string)
|
||||
|
||||
if sourceID != 0 && externalID != "" {
|
||||
_ = model.BindExternalAccount(&model.ExternalAccount{
|
||||
_ = model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
AuthSourceID: sourceID,
|
||||
UserID: user.ID,
|
||||
ExternalID: externalID,
|
||||
|
||||
@@ -114,7 +114,7 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if isEmailLoginVerificationEnabled() {
|
||||
if isEmailLoginVerificationEnabled(ctx) {
|
||||
if emailErr := handleLoginEmailVerification(ctx, c, &req, &user); emailErr != nil {
|
||||
return
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 检查是否有未完成 of OAuth/OIDC 绑定
|
||||
completePendingOAuthBinding(session, &user)
|
||||
completePendingOAuthBinding(ctx, session, &user)
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T
|
||||
task.AppendLog(ctx, "连接 SMTP 服务器: %s:%d, 用户名: %s", smtpHost, smtpPort, smtpUsername)
|
||||
|
||||
// 调用 SendMailHTML 执行邮件发送,这里会有 5s 拨号超时和 10s 读写限制
|
||||
err = mail.SendMailHTML(cfg, req.To, req.Subject, req.Body)
|
||||
err = mail.SendMailHTML(ctx, cfg, req.To, req.Subject, req.Body)
|
||||
if err != nil {
|
||||
task.AppendLog(ctx, "邮件发送失败: %v", err)
|
||||
return nil, fmt.Errorf(errSendMailFailed, err)
|
||||
|
||||
@@ -70,7 +70,7 @@ func init() {
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
// 文件存在但读取/解析失败
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
if _, statErr := os.Stat(configPath); statErr == nil { //nolint:gosec // configPath is loaded from CONFIG_PATH environment variable
|
||||
log.Fatalf("[Config] read config failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,9 @@ func init() {
|
||||
|
||||
// NextUint64ID 生成下一个分布式唯一 ID
|
||||
func NextUint64ID() uint64 {
|
||||
return uint64(node.Generate().Int64())
|
||||
val := node.Generate().Int64()
|
||||
if val < 0 {
|
||||
return 0
|
||||
}
|
||||
return uint64(val)
|
||||
}
|
||||
|
||||
@@ -221,16 +221,16 @@ func DeleteAuthSource(id uint64) error {
|
||||
}
|
||||
|
||||
// FindExternalAccount 查找外部帐号绑定记录
|
||||
func FindExternalAccount(sourceID uint64, externalID string) (*ExternalAccount, error) {
|
||||
func FindExternalAccount(ctx context.Context, sourceID uint64, externalID string) (*ExternalAccount, error) {
|
||||
var account ExternalAccount
|
||||
if err := db.DB(context.Background()).Where("auth_source_id = ? AND external_id = ?", sourceID, externalID).First(&account).Error; err != nil {
|
||||
if err := db.DB(ctx).Where("auth_source_id = ? AND external_id = ?", sourceID, externalID).First(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// BindExternalAccount 绑定外部帐号(已存在时更新用户名和邮箱)
|
||||
func BindExternalAccount(account *ExternalAccount) error {
|
||||
func BindExternalAccount(ctx context.Context, account *ExternalAccount) error {
|
||||
if account.UserID == 0 || strings.TrimSpace(account.ExternalID) == "" {
|
||||
return errors.New(errExternalAccountBindingIncomplete)
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func BindExternalAccount(account *ExternalAccount) error {
|
||||
account.ExternalUsername = strings.TrimSpace(account.ExternalUsername)
|
||||
account.Email = strings.TrimSpace(account.Email)
|
||||
|
||||
return db.DB(context.Background()).Transaction(func(tx *gorm.DB) error {
|
||||
return db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var current ExternalAccount
|
||||
err := tx.Where("auth_source_id = ? AND external_id = ?", account.AuthSourceID, account.ExternalID).First(¤t).Error
|
||||
if err == nil {
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
errAuthSourceNameInvalid = "认证源名称只能包含字母、数字、短横线或下划线,且必须以字母或数字开头"
|
||||
errAuthSourceTypeUnsupported = "认证源类型仅支持 oidc"
|
||||
errAuthSourceDiscoveryURLRequired = "OIDC 认证源必须配置 Discovery URL"
|
||||
errAuthSourceClientCredentialsRequired = "启用认证源前必须配置 Client ID 和 Client Secret"
|
||||
errAuthSourceClientCredentialsRequired = "启用认证源前必须配置 Client ID 和 Client Secret" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errAuthSourceIDRequired = "认证源 ID 不能为空"
|
||||
errExternalAccountBindingIncomplete = "外部账号绑定信息不完整"
|
||||
errExternalAccountAlreadyBoundToAnother = "该外部账号已绑定到其他用户"
|
||||
|
||||
@@ -26,14 +26,14 @@ const (
|
||||
ConfigKeyRegistrationEnabled = "registration_enabled" // 是否允许注册
|
||||
ConfigKeyPasswordRegisterEnabled = "password_register_enabled" // 是否允许密码注册
|
||||
ConfigKeyOIDCLoginEnabled = "oidc_login_enabled" // 是否允许 OIDC 登录
|
||||
ConfigKeyMaxAPIKeysPerUser = "max_api_keys_per_user" // 每个用户最大 API Key 数量
|
||||
ConfigKeyMaxAPIKeysPerUser = "max_api_keys_per_user" //nolint:gosec // false positive: config key name. 每个用户最大 API Key 数量
|
||||
ConfigKeyCapLoginEnabled = "cap_login_enabled" // 是否启用登录人机验证
|
||||
ConfigKeyCapAutoSolve = "cap_auto_solve" // 打开页面后是否自动开始计算(false 则需用户手动点击)
|
||||
ConfigKeyCapChallengeCount = "cap_challenge_count" // 客户端需求解的 PoW 难题总数,默认 1,推荐 1~5
|
||||
ConfigKeyCapChallengeSize = "cap_challenge_size" // 人机验证盐值长度
|
||||
ConfigKeyCapChallengeDifficulty = "cap_challenge_difficulty" // 人机验证 PoW 难度(目标前缀长度)
|
||||
ConfigKeyCapChallengeTTL = "cap_challenge_ttl_seconds" // 人机验证难题有效时间(秒)
|
||||
ConfigKeyCapTokenTTL = "cap_token_ttl_seconds" // 人机验证兑换凭证有效时间(秒)
|
||||
ConfigKeyCapTokenTTL = "cap_token_ttl_seconds" //nolint:gosec // false positive: config key name. 人机验证兑换凭证有效时间(秒)
|
||||
ConfigKeyServerAddress = "server_address" // 服务器地址
|
||||
ConfigKeySMTPHost = "smtp_host" // SMTP 服务器地址
|
||||
ConfigKeySMTPPort = "smtp_port" // SMTP 端口
|
||||
|
||||
+34
-29
@@ -96,6 +96,40 @@ func Serve() {
|
||||
// 补充中间件
|
||||
r.Use(otelgin.Middleware(config.Config.App.AppName), loggerMiddleware(), risk_control.RiskControlMiddleware())
|
||||
|
||||
registerRoutes(r)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: config.Config.App.Addr,
|
||||
Handler: r,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[API] server starting on %s\n", config.Config.App.Addr)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("[API] server failed: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second)
|
||||
|
||||
otel_trace.Shutdown(shutdownCtx)
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("[API] server forced to shutdown: %v\n", err)
|
||||
cancel()
|
||||
os.Exit(1)
|
||||
}
|
||||
cancel()
|
||||
|
||||
log.Println("[API] server exited")
|
||||
}
|
||||
|
||||
func registerRoutes(r *gin.Engine) {
|
||||
// Serve files by ID
|
||||
r.GET("/f/:id", upload.ServeFileByID)
|
||||
|
||||
@@ -251,33 +285,4 @@ func Serve() {
|
||||
|
||||
// 注册前端静态路由(当启用 embed_frontend 编译标签时)
|
||||
registerFrontend(r)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: config.Config.App.Addr,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("[API] server starting on %s\n", config.Config.App.Addr)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("[API] server failed: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second)
|
||||
|
||||
otel_trace.Shutdown(shutdownCtx)
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("[API] server forced to shutdown: %v\n", err)
|
||||
cancel()
|
||||
os.Exit(1)
|
||||
}
|
||||
cancel()
|
||||
|
||||
log.Println("[API] server exited")
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func getLocalCacheFile(ctx context.Context, localPath, metaPath string) (*Object
|
||||
defer span.End()
|
||||
|
||||
// 尝试打开本地缓存文件
|
||||
file, err := os.Open(localPath)
|
||||
file, err := os.Open(localPath) //nolint:gosec // localPath is internally managed cache path
|
||||
if err == nil {
|
||||
defer func() { _ = file.Close() }()
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func getLocalCacheFile(ctx context.Context, localPath, metaPath string) (*Object
|
||||
}
|
||||
|
||||
// 读取元信息
|
||||
metaData, err := os.ReadFile(metaPath)
|
||||
metaData, err := os.ReadFile(metaPath) //nolint:gosec // metaPath is internally managed cache path
|
||||
|
||||
// 文件不存在
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
|
||||
@@ -94,7 +94,7 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco
|
||||
// TTL is set to the challenge's remaining lifetime so the slot auto-expires.
|
||||
payload, err := VerifyChallengeSolutions(token, solutions, m.conf.Secret, scope)
|
||||
if err != nil {
|
||||
return &RedeemResponse{Success: false, Error: err.Error()}, nil
|
||||
return &RedeemResponse{Success: false, Error: err.Error()}, nil //nolint:nilerr // expected behavior: validation error is returned as response, not system error
|
||||
}
|
||||
|
||||
// Calculate remaining lifetime of the challenge JWT for the nonce TTL.
|
||||
@@ -174,7 +174,7 @@ func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope s
|
||||
|
||||
expNano, err := strconv.ParseInt(valParts[0], 10, 64)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
return false, nil //nolint:nilerr // expected behavior: invalid format is treated as validation failure, not system error
|
||||
}
|
||||
tokenScope := valParts[1]
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func Request(ctx context.Context, method, url string, body io.Reader, headers, c
|
||||
}
|
||||
|
||||
for key, value := range cookies {
|
||||
req.AddCookie(&http.Cookie{Name: key, Value: value})
|
||||
req.AddCookie(&http.Cookie{Name: key, Value: value}) //nolint:gosec // client-side cookies do not require server attributes (Secure/HttpOnly)
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
|
||||
@@ -11,6 +11,6 @@ const (
|
||||
errSMTPMailCommandFailed = "smtp mail command failed: %w"
|
||||
errSMTPRcptCommandFailed = "smtp rcpt command failed: %w"
|
||||
errSMTPDataCommandFailed = "smtp data command failed: %w"
|
||||
errSMTPWritingBodyFailed = "smtp writing body failed: %w"
|
||||
errSMTPWritingBodyFailed = "smtp writing body failed: %w" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errSendMailFailed = "send mail failed: %w"
|
||||
)
|
||||
|
||||
+21
-12
@@ -6,6 +6,7 @@ package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -29,12 +30,12 @@ type Config struct {
|
||||
}
|
||||
|
||||
// SendMail sends an HTML email using the provided config and message details
|
||||
func SendMail(cfg Config, to string, subject, body string) error {
|
||||
return SendMailHTML(cfg, to, subject, body)
|
||||
func SendMail(ctx context.Context, cfg Config, to string, subject, body string) error {
|
||||
return SendMailHTML(ctx, cfg, to, subject, body)
|
||||
}
|
||||
|
||||
// SendMailHTML sends an HTML format email
|
||||
func SendMailHTML(cfg Config, to string, subject, body string) error {
|
||||
func SendMailHTML(ctx context.Context, cfg Config, to string, subject, body string) error {
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))
|
||||
|
||||
// Header & MIME settings for HTML email
|
||||
@@ -55,7 +56,7 @@ func SendMailHTML(cfg Config, to string, subject, body string) error {
|
||||
|
||||
// If using SSL port 465, we connection via TLS dial
|
||||
if cfg.Port == smtpSSLPort {
|
||||
return sendMailViaSSL(addr, auth, cfg, to, message)
|
||||
return sendMailViaSSL(ctx, addr, auth, cfg, to, message)
|
||||
}
|
||||
|
||||
// For standard port (587 / 25), use smtp.SendMail directly (handles STARTTLS automatically if server supports it)
|
||||
@@ -68,13 +69,17 @@ func SendMailHTML(cfg Config, to string, subject, body string) error {
|
||||
}
|
||||
|
||||
// sendMailViaSSL 通过 TLS 直接连接 SMTP SSL 端口发送邮件
|
||||
func sendMailViaSSL(addr string, auth smtp.Auth, cfg Config, to, message string) error {
|
||||
func sendMailViaSSL(ctx context.Context, addr string, auth smtp.Auth, cfg Config, to, message string) error {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: smtpDialTimeout}
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
conn, err := tlsDialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errDialTLSFailed, err)
|
||||
}
|
||||
@@ -111,7 +116,7 @@ func sendMailViaSSL(addr string, auth smtp.Auth, cfg Config, to, message string)
|
||||
}
|
||||
|
||||
// SendMailWithLog sends a test email and records a detailed SMTP connection log
|
||||
func SendMailWithLog(cfg Config, to string, subject, body string) (string, error) {
|
||||
func SendMailWithLog(ctx context.Context, cfg Config, to string, subject, body string) (string, error) {
|
||||
var logBuf bytes.Buffer
|
||||
logLine := func(dir string, format string, args ...interface{}) {
|
||||
fmt.Fprintf(&logBuf, "[%s] %s\n", dir, fmt.Sprintf(format, args...))
|
||||
@@ -125,12 +130,16 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error
|
||||
dialer := &net.Dialer{Timeout: smtpDialTimeout}
|
||||
if cfg.Port == smtpSSLPort {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
conn, err = tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
conn, err = tlsDialer.DialContext(ctx, "tcp", addr)
|
||||
} else {
|
||||
conn, err = dialer.Dial("tcp", addr)
|
||||
conn, err = dialer.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
if err != nil {
|
||||
logLine("Error", "Connection failed: %v", err)
|
||||
@@ -154,7 +163,7 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
logLine("C", "STARTTLS")
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
if err = client.StartTLS(tlsConfig); err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ package mail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"testing"
|
||||
@@ -84,7 +85,7 @@ func TestSendMailMock(t *testing.T) {
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
err = SendMail(cfg, "recipient@example.com", "Test Subject", "<h1>Test Body</h1>")
|
||||
err = SendMail(context.Background(), cfg, "recipient@example.com", "Test Subject", "<h1>Test Body</h1>")
|
||||
if err != nil {
|
||||
t.Errorf("failed to send mail: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user