新增 OIDC/SSO、邮件验证码与 Gitea 项目同步,并强化 Feed 与管理后台。
作为 OIDC Provider 对接 Gitea;注册支持邮件验证码/验证码;侧栏同步公开仓库;Feed 分页、文章大纲、标签云与站点品牌设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
24
model/gitea.go
Normal 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"
|
||||
}
|
||||
@@ -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
31
model/oauth.go
Normal 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
76
model/user_view.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user