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