139 lines
4.5 KiB
Go
139 lines
4.5 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"github.com/freefire/jiang13-bbs/model"
|
||
"github.com/freefire/jiang13-bbs/version"
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
func (o *Operations) ResetPassword(email, code, password string) error {
|
||
cfg, e := o.Security()
|
||
if e != nil || !cfg.PasswordReset {
|
||
return errors.New("找回密码未开启")
|
||
}
|
||
if len(password) < 6 || len(password) > 64 {
|
||
return errors.New("密码长度须为 6–64 字节")
|
||
}
|
||
hash, e := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
if e != nil {
|
||
return e
|
||
}
|
||
return o.db.Transaction(func(tx *gorm.DB) error {
|
||
scoped := *o
|
||
scoped.db = tx
|
||
if e := scoped.ConsumeCode(email, "reset", code); e != nil {
|
||
return e
|
||
}
|
||
var users []model.User
|
||
if e := tx.Where("LOWER(email) = ?", strings.ToLower(strings.TrimSpace(email))).Limit(2).Find(&users).Error; e != nil {
|
||
return e
|
||
}
|
||
if len(users) != 1 {
|
||
return errors.New("账号不可用")
|
||
}
|
||
if e := tx.Model(&model.User{}).Where("id = ?", users[0].ID).Updates(map[string]any{
|
||
"password": string(hash),
|
||
"token_version": gorm.Expr("token_version + 1"),
|
||
}).Error; e != nil {
|
||
return e
|
||
}
|
||
return tx.Model(&model.RefreshToken{}).Where("user_id = ? AND revoked = ?", users[0].ID, false).
|
||
Updates(map[string]any{"revoked": true, "token_cipher": ""}).Error
|
||
})
|
||
}
|
||
func (o *Operations) Diagnostics(ctx context.Context) map[string]any {
|
||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||
defer cancel()
|
||
dbState := "正常"
|
||
sql, e := o.db.DB()
|
||
if e != nil {
|
||
dbState = "异常"
|
||
} else if sql.PingContext(ctx) != nil {
|
||
dbState = "异常"
|
||
}
|
||
local := "正常"
|
||
if o.LocalWritable() != nil {
|
||
local = "异常"
|
||
}
|
||
v, _, e := o.read(o.db, "storage")
|
||
storage := "未知"
|
||
if e == nil {
|
||
storage = "本地:" + local
|
||
if v.(*StorageConfig).Backend == "s3" {
|
||
storage = "S3:未知(使用存储页测试)"
|
||
}
|
||
}
|
||
var last model.SettingsAudit
|
||
result := map[string]any{"state": "未知"}
|
||
if o.db.Where("module = ? AND action = ?", "mail", "test").Order("id desc").First(&last).Error == nil {
|
||
result = map[string]any{"result": last.Result, "tested_at": last.CreatedAt, "note": "历史测试,不代表持续健康"}
|
||
}
|
||
var queued int64
|
||
queueErr := o.db.WithContext(ctx).Model(&model.MailTask{}).Where("status IN ?", []string{"queued", "retry", "sending"}).Count(&queued).Error
|
||
queueState := "正常 · PostgreSQL 持久邮件队列"
|
||
if queueErr != nil {
|
||
queueState = "异常 · 暂时无法读取邮件队列"
|
||
}
|
||
return map[string]any{"version": version.Version, "uptime_seconds": int(time.Since(o.Started).Seconds()), "database": dbState, "cache": "未配置独立应用缓存", "tasks": queueState, "queued_mail": queued, "storage": storage, "mail_test": result}
|
||
}
|
||
|
||
// Draft references live in browsers, so unattached database rows are NEVER deleted.
|
||
func (o *Operations) ScanTemporary() (map[string]any, error) {
|
||
m, e := o.Maintenance()
|
||
if e != nil {
|
||
return nil, e
|
||
}
|
||
count, size := 0, int64(0)
|
||
cutoff := time.Now().Add(-time.Duration(m.TempDays) * 24 * time.Hour)
|
||
for _, dir := range []string{filepath.Join(o.cfg.DataDir, "uploads"), filepath.Join(o.cfg.DataDir, "private")} {
|
||
e = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if d.Type()&os.ModeSymlink != 0 {
|
||
return nil
|
||
}
|
||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".partial") {
|
||
return nil
|
||
}
|
||
info, e := d.Info()
|
||
if e != nil {
|
||
return e
|
||
}
|
||
if info.ModTime().Before(cutoff) {
|
||
count++
|
||
size += info.Size()
|
||
}
|
||
return nil
|
||
})
|
||
if e != nil {
|
||
return nil, e
|
||
}
|
||
}
|
||
candidates, e := o.temporaryCandidates()
|
||
if e != nil {
|
||
return nil, e
|
||
}
|
||
var drafts int64
|
||
if e = o.db.Model(&model.PostAttachment{}).Where("post_id = 0").Count(&drafts).Error; e != nil {
|
||
return nil, e
|
||
}
|
||
return map[string]any{"candidates": candidates, "expired_partial_count": count, "expired_partial_bytes": size, "protected_draft_attachments": drafts, "retention_days": m.TempDays, "message": "保留所有已入库图片和草稿附件。只清理受本版本上传锁保护且已过期的候选文件;未知来源的旧 partial 不在线删除。"}, nil
|
||
}
|
||
func (o *Operations) ClearMailLogs() (int64, error) {
|
||
v, _, e := o.read(o.db, "mail")
|
||
if e != nil {
|
||
return 0, e
|
||
}
|
||
c := v.(*MailConfig)
|
||
r := o.db.Where("status IN ? AND updated_at < ?", []string{"accepted", "failed"}, time.Now().Add(-time.Duration(c.Retention)*24*time.Hour)).Delete(&model.MailTask{})
|
||
return r.RowsAffected, r.Error
|
||
}
|