feat: 安全设置展示有效登录会话,支持剔除其它设备

按设备指纹去重并直出 IP;非当前会话可踢下线,对方需重新输入密码。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 01:48:52 +08:00
parent 2ccfb5f330
commit 3a7bfe59df
16 changed files with 808 additions and 28 deletions

View File

@@ -42,6 +42,11 @@ func InitDB(dsn string) error {
return fmt.Errorf("公告置顶列迁移失败: %w", err)
}
// refresh_tokens 会话时间列:存量行不能直接 ADD NOT NULL
if err := prepareRefreshTokenSessionColumns(db); err != nil {
return fmt.Errorf("refresh token 会话列迁移失败: %w", err)
}
if err := db.AutoMigrate(
&User{}, &Board{}, &Post{}, &Comment{}, &CommentEditHistory{}, &RefreshToken{}, &Like{}, &Notification{}, &Checkin{},
&Announcement{}, &SitePage{}, &SiteSetting{}, &SiteDailyStats{}, &SiteDailyVisitor{}, &Attachment{}, &UserBoard{}, &LoginLog{},
@@ -152,6 +157,37 @@ func prepareAnnouncementPinnedColumn(db *gorm.DB) error {
return db.Exec(`ALTER TABLE announcements ADD COLUMN pinned boolean NOT NULL DEFAULT false`).Error
}
// prepareRefreshTokenSessionColumns 存量 refresh_tokens 加 logged_in_at / last_used_at:
// 先可空加列,用 created_at 回填后再收紧 NOT NULL。
func prepareRefreshTokenSessionColumns(db *gorm.DB) error {
var tableCount int64
if err := db.Raw(`SELECT count(1) FROM information_schema.tables WHERE table_name = 'refresh_tokens'`).
Scan(&tableCount).Error; err != nil {
return err
}
if tableCount == 0 {
return nil
}
for _, col := range []string{"logged_in_at", "last_used_at"} {
if err := db.Exec(fmt.Sprintf(
`ALTER TABLE refresh_tokens ADD COLUMN IF NOT EXISTS %s timestamptz`, col,
)).Error; err != nil {
return err
}
if err := db.Exec(fmt.Sprintf(
`UPDATE refresh_tokens SET %s = created_at WHERE %s IS NULL`, col, col,
)).Error; err != nil {
return err
}
if err := db.Exec(fmt.Sprintf(
`ALTER TABLE refresh_tokens ALTER COLUMN %s SET NOT NULL`, col,
)).Error; err != nil {
return err
}
}
return nil
}
// prepareRefreshTokenMigration 旧版 refresh_tokens 表把明文存在 token 列,
// 新版改为 token_hash(SHA-256,NOT NULL+唯一索引)。在 AutoMigrate 之前:
// 1. 新增可带默认值的 token_hash 列(避免对存量行加 NOT NULL 列失败)