质量优化

This commit is contained in:
ryan
2026-06-10 13:42:15 +08:00
parent 3ed4dec4a3
commit 57944398e6
27 changed files with 155 additions and 111 deletions
+15 -13
View File
@@ -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
+1 -1
View File
@@ -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..."
+10 -2
View File
@@ -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"
}
+7 -4
View File
@@ -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 {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 = "创建用户失败"
)
+2 -2
View File
@@ -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
)
+1 -1
View File
@@ -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 = "未配置可用认证源"
+3 -3
View File
@@ -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,
+9 -1
View File
@@ -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(),
}
+1 -1
View File
@@ -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
View File
@@ -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 不能为空"
+13 -9
View File
@@ -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,
+2 -2
View File
@@ -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)))
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
}
}
+5 -1
View File
@@ -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)
}
+4 -4
View File
@@ -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(&current).Error
if err == nil {
+1 -1
View File
@@ -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 = "该外部账号已绑定到其他用户"
+2 -2
View File
@@ -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
View File
@@ -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")
}
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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]
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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 {
+2 -1
View File
@@ -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)
}