新增 OIDC/SSO、邮件验证码与 Gitea 项目同步,并强化 Feed 与管理后台。

作为 OIDC Provider 对接 Gitea;注册支持邮件验证码/验证码;侧栏同步公开仓库;Feed 分页、文章大纲、标签云与站点品牌设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-31 16:58:22 +08:00
parent 9487c8ab02
commit 822eef96be
83 changed files with 8578 additions and 1222 deletions

View File

@@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"gorm.io/driver/sqlite"
"github.com/glebarez/sqlite" // 纯 Go支持 CGO_ENABLED=0 交叉编译
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
@@ -36,6 +36,8 @@ func InitDB(dbPath string) error {
if err := db.AutoMigrate(
&User{}, &Board{}, &Post{}, &Comment{},
&PostLike{}, &PostFavorite{}, &PostRevision{}, &ForumSetting{},
&OAuthClient{}, &OAuthAuthCode{},
&GiteaRepo{},
); err != nil {
return fmt.Errorf("自动迁移失败: %w", err)
}

24
model/gitea.go Normal file
View File

@@ -0,0 +1,24 @@
package model
import "time"
// GiteaRepo 从 Gitea 同步的公开仓库缓存(侧栏 /projects 读取)
type GiteaRepo struct {
ID uint `gorm:"primaryKey" json:"id"`
GiteaID int64 `gorm:"uniqueIndex;not null" json:"gitea_id"`
OwnerLogin string `gorm:"size:128;not null;index" json:"owner_login"`
Name string `gorm:"size:255;not null" json:"name"`
FullName string `gorm:"size:512;not null" json:"full_name"`
Description string `gorm:"size:2048;default:''" json:"description"`
HTMLURL string `gorm:"size:1024;not null" json:"html_url"`
Private bool `gorm:"default:false" json:"private"`
UpdatedAtRemote *time.Time `json:"updated_at_remote"`
ForumUserID *uint `gorm:"index" json:"forum_user_id"`
SyncedAt time.Time `json:"synced_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (GiteaRepo) TableName() string {
return "gitea_repos"
}

View File

@@ -15,18 +15,23 @@ const (
)
// User 用户表
// Email / Password / LastLogin* 默认不随帖子等嵌套 User 序列化;
// 个人中心与后台列表请用 UserSelf / UserAdmin。
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:64;not null" json:"username"`
Password string `gorm:"size:128;not null" json:"-"`
Nickname string `gorm:"size:64" json:"nickname"`
Avatar string `gorm:"size:256" json:"avatar"`
Role Role `gorm:"size:16;default:user" json:"role"`
Banned bool `gorm:"default:false" json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:128;not null" json:"username"`
Email string `gorm:"index;size:128;default:''" json:"-"`
Password string `gorm:"size:128;not null" json:"-"`
Nickname string `gorm:"size:64" json:"nickname"`
Avatar string `gorm:"size:256" json:"avatar"`
Role Role `gorm:"size:16;default:user" json:"role"`
Banned bool `gorm:"default:false" json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
LastLoginAt *time.Time `json:"-"`
LastLoginIP string `gorm:"size:45;default:''" json:"-"` // 兼容 IPv6
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// Board 论坛板块
@@ -80,7 +85,7 @@ type PostRevision struct {
// ForumSetting 论坛全局设置(键值对)
type ForumSetting struct {
Key string `gorm:"primaryKey;size:64" json:"key"`
Value string `gorm:"size:256" json:"value"`
Value string `gorm:"size:2048" json:"value"`
}
// Comment 楼层评论

31
model/oauth.go Normal file
View File

@@ -0,0 +1,31 @@
package model
import "time"
// OAuthClient OIDC/OAuth2 客户端应用(密钥仅存 bcrypt 哈希)
type OAuthClient struct {
ID uint `gorm:"primaryKey" json:"id"`
ClientID string `gorm:"uniqueIndex;size:128;not null" json:"client_id"`
ClientSecretHash string `gorm:"size:128;not null" json:"-"`
Name string `gorm:"size:128;not null" json:"name"`
RedirectURIs string `gorm:"size:2048;not null" json:"redirect_uris"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuthAuthCode 一次性授权码OIDC Authorization Code Flow
type OAuthAuthCode struct {
ID uint `gorm:"primaryKey"`
Code string `gorm:"uniqueIndex;size:64;not null"`
ClientID string `gorm:"size:128;not null;index"`
UserID uint `gorm:"not null;index"`
RedirectURI string `gorm:"size:512;not null"`
Scope string `gorm:"size:256;default:''"`
Nonce string `gorm:"size:128;default:''"`
CodeChallenge string `gorm:"size:128;default:''"`
CodeChallengeMethod string `gorm:"size:16;default:''"`
ExpiresAt time.Time `gorm:"not null;index"`
Used bool `gorm:"default:false"`
CreatedAt time.Time
}

76
model/user_view.go Normal file
View File

@@ -0,0 +1,76 @@
package model
import "time"
// UserSelf 当前登录用户视图(含邮箱,不含登录 IP
type UserSelf struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Role Role `json:"role"`
Banned bool `json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserAdmin 后台用户管理视图(含邮箱与上次登录信息)
type UserAdmin struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Role Role `json:"role"`
Banned bool `json:"banned"`
BannedAt *time.Time `json:"banned_at,omitempty"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LastLoginIP string `json:"last_login_ip,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ToSelf 转为个人中心 /api/me 视图
func (u *User) ToSelf() UserSelf {
return UserSelf{
ID: u.ID,
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
Avatar: u.Avatar,
Role: u.Role,
Banned: u.Banned,
BannedAt: u.BannedAt,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// ToAdmin 转为后台用户列表视图
func (u *User) ToAdmin() UserAdmin {
return UserAdmin{
ID: u.ID,
Username: u.Username,
Email: u.Email,
Nickname: u.Nickname,
Avatar: u.Avatar,
Role: u.Role,
Banned: u.Banned,
BannedAt: u.BannedAt,
LastLoginAt: u.LastLoginAt,
LastLoginIP: u.LastLoginIP,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// UsersToAdmin 批量转换后台用户列表
func UsersToAdmin(users []User) []UserAdmin {
out := make([]UserAdmin, 0, len(users))
for i := range users {
out = append(out, users[i].ToAdmin())
}
return out
}