mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
⚡ implement user authentication and database initialization, add models for user, comment, label, and OIDC configuration
This commit is contained in:
@ -3,6 +3,8 @@ package v1
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
)
|
||||
|
||||
type userType struct{}
|
||||
@ -10,11 +12,13 @@ type userType struct{}
|
||||
var User = new(userType)
|
||||
|
||||
func (u *userType) Login(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: Impl
|
||||
var userLoginReq dto.UserLoginReq
|
||||
if err := c.BindAndValidate(&userLoginReq); err != nil {
|
||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *userType) Register(ctx context.Context, c *app.RequestContext) {
|
||||
// TODO: Impl
|
||||
}
|
||||
|
||||
func (u *userType) Logout(ctx context.Context, c *app.RequestContext) {
|
||||
|
7
internal/dto/dto.go
Normal file
7
internal/dto/dto.go
Normal file
@ -0,0 +1,7 @@
|
||||
package dto
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
33
internal/dto/user.go
Normal file
33
internal/dto/user.go
Normal file
@ -0,0 +1,33 @@
|
||||
package dto
|
||||
|
||||
type UserDto struct {
|
||||
Username string `json:"username"` // 用户名
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"` // 头像URL
|
||||
Email string `json:"email"` // 邮箱
|
||||
Gender string `json:"gender"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
type UserLoginReq struct {
|
||||
Username string `json:"username"` // username or email
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type UserLoginResp struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
User UserDto `json:"user"`
|
||||
}
|
||||
|
||||
type UserRegisterReq struct {
|
||||
Username string `json:"username"` // 用户名
|
||||
Nickname string `json:"nickname"` // 昵称
|
||||
Password string `json:"password"` // 密码
|
||||
Email string `json:"email"` // 邮箱
|
||||
}
|
||||
|
||||
type UserRegisterResp struct {
|
||||
Token string `json:"token"` // 访问令牌
|
||||
RefreshToken string `json:"refresh_token"` // 刷新令牌
|
||||
User UserDto `json:"user"` // 用户信息
|
||||
}
|
14
internal/model/comment.go
Normal file
14
internal/model/comment.go
Normal file
@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Comment struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"index"` // 评论的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
TargetID uint `gorm:"index"` // 目标ID
|
||||
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
|
||||
ReplyID uint `gorm:"index"` // 回复的评论ID
|
||||
Content string `gorm:"type:text"` // 评论内容
|
||||
Depth int `gorm:"default:0"` // 评论的层级深度
|
||||
}
|
1
internal/model/file.go
Normal file
1
internal/model/file.go
Normal file
@ -0,0 +1 @@
|
||||
package model
|
10
internal/model/label.go
Normal file
10
internal/model/label.go
Normal file
@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Label struct {
|
||||
gorm.Model
|
||||
Key string `gorm:"uniqueIndex"` // 标签键,唯一标识
|
||||
Value string `gorm:"type:text"` // 标签值,描述标签的内容
|
||||
Color string `gorm:"type:text"` // 前端可用颜色代码
|
||||
}
|
89
internal/model/oidc_config.go
Normal file
89
internal/model/oidc_config.go
Normal file
@ -0,0 +1,89 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"resty.dev/v3"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OidcConfig struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
ClientID string `gorm:"column:client_id"` // 客户端ID
|
||||
ClientSecret string `gorm:"column:client_secret"` // 客户端密钥
|
||||
DisplayName string `gorm:"column:display_name"` // 显示名称,例如:轻雪通行证
|
||||
GroupsClaim *string `gorm:"default:groups"` // 组声明,默认为:"groups"
|
||||
Icon *string `gorm:"column:icon"` // 图标url,为空则使用内置默认图标
|
||||
OidcDiscoveryUrl string `gorm:"column:oidc_discovery_url"` // OpenID自动发现URL,例如 :https://pass.liteyuki.icu/.well-known/openid-configuration
|
||||
Enabled bool `gorm:"column:enabled;default:true"` // 是否启用
|
||||
// 以下字段为自动获取字段,每次更新配置时自动填充
|
||||
Issuer string
|
||||
AuthorizationEndpoint string
|
||||
TokenEndpoint string
|
||||
UserInfoEndpoint string
|
||||
JwksUri string
|
||||
}
|
||||
|
||||
type oidcDiscoveryResp struct {
|
||||
Issuer string `json:"issuer" validate:"required"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint" validate:"required"`
|
||||
TokenEndpoint string `json:"token_endpoint" validate:"required"`
|
||||
UserInfoEndpoint string `json:"userinfo_endpoint" validate:"required"`
|
||||
JwksUri string `json:"jwks_uri" validate:"required"`
|
||||
// 可选字段
|
||||
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||
IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
|
||||
ClaimsSupported []string `json:"claims_supported,omitempty"`
|
||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||
}
|
||||
|
||||
func updateOidcConfigFromUrl(url string) (*oidcDiscoveryResp, error) {
|
||||
client := resty.New()
|
||||
client.SetTimeout(10 * time.Second) // 设置超时时间
|
||||
var discovery oidcDiscoveryResp
|
||||
resp, err := client.R().
|
||||
SetHeader("Accept", "application/json").
|
||||
SetResult(&discovery).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求OIDC发现端点失败: %w", err)
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return nil, fmt.Errorf("请求OIDC发现端点失败,状态码: %d", resp.StatusCode())
|
||||
}
|
||||
// 验证必要字段
|
||||
if discovery.Issuer == "" ||
|
||||
discovery.AuthorizationEndpoint == "" ||
|
||||
discovery.TokenEndpoint == "" ||
|
||||
discovery.UserInfoEndpoint == "" ||
|
||||
discovery.JwksUri == "" {
|
||||
return nil, fmt.Errorf("OIDC发现端点响应缺少必要字段")
|
||||
}
|
||||
return &discovery, nil
|
||||
}
|
||||
|
||||
func (o *OidcConfig) BeforeSave(tx *gorm.DB) (err error) {
|
||||
// 设置默认值
|
||||
if o.GroupsClaim == nil {
|
||||
defaultGroupsClaim := "groups"
|
||||
o.GroupsClaim = &defaultGroupsClaim
|
||||
}
|
||||
// 只有在创建新记录或更新 OidcDiscoveryUrl 字段时才更新端点信息
|
||||
if tx.Statement.Changed("OidcDiscoveryUrl") {
|
||||
discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新OIDC配置失败: %w", err)
|
||||
}
|
||||
o.Issuer = discoveryResp.Issuer
|
||||
o.AuthorizationEndpoint = discoveryResp.AuthorizationEndpoint
|
||||
o.TokenEndpoint = discoveryResp.TokenEndpoint
|
||||
o.UserInfoEndpoint = discoveryResp.UserInfoEndpoint
|
||||
o.JwksUri = discoveryResp.JwksUri
|
||||
}
|
||||
return nil
|
||||
}
|
1
internal/model/page.go
Normal file
1
internal/model/page.go
Normal file
@ -0,0 +1 @@
|
||||
package model
|
12
internal/model/post.go
Normal file
12
internal/model/post.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Post struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"index"` // 发布者的用户ID
|
||||
User User `gorm:"foreignKey:UserID;references:ID"` // 关联的用户
|
||||
Title string `gorm:"type:text;not null"` // 帖子标题
|
||||
Content string `gorm:"type:text;not null"` // 帖子内容
|
||||
Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签
|
||||
}
|
15
internal/model/user.go
Normal file
15
internal/model/user.go
Normal file
@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `gorm:"unique;index"` // 用户名,唯一
|
||||
Nickname string
|
||||
AvatarUrl string
|
||||
Email string `gorm:"unique;index"`
|
||||
Gender string
|
||||
Role string `gorm:"default:'user'"`
|
||||
|
||||
Password string // 密码,存储加密后的值
|
||||
}
|
133
internal/repo/init.go
Normal file
133
internal/repo/init.go
Normal file
@ -0,0 +1,133 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/model"
|
||||
"github.com/snowykami/neo-blog/pkg/utils"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
// DBConfig 数据库配置结构体
|
||||
type DBConfig struct {
|
||||
Driver string // 数据库驱动类型,例如 "sqlite" 或 "postgres" Database driver type, e.g., "sqlite" or "postgres"
|
||||
Path string // SQLite 路径 SQLite path
|
||||
Host string // PostgreSQL 主机名 PostgreSQL hostname
|
||||
Port int // PostgreSQL 端口 PostgreSQL port
|
||||
User string // PostgreSQL 用户名 PostgreSQL username
|
||||
Password string // PostgreSQL 密码 PostgreSQL password
|
||||
DBName string // PostgreSQL 数据库名 PostgreSQL database name
|
||||
SSLMode string // PostgreSQL SSL 模式 PostgreSQL SSL mode
|
||||
}
|
||||
|
||||
// loadDBConfig 从配置文件加载数据库配置
|
||||
func loadDBConfig() DBConfig {
|
||||
return DBConfig{
|
||||
Driver: utils.Env.Get("database.driver", "sqlite"),
|
||||
Path: utils.Env.Get("database.path", "./data/data.db"),
|
||||
Host: utils.Env.Get("database.host", "postgres"),
|
||||
Port: utils.Env.GetenvAsInt("database.port", 5432),
|
||||
User: utils.Env.Get("database.user", "spage"),
|
||||
Password: utils.Env.Get("database.password", "spage"),
|
||||
DBName: utils.Env.Get("database.dbname", "spage"),
|
||||
SSLMode: utils.Env.Get("database.sslmode", "disable"),
|
||||
}
|
||||
}
|
||||
|
||||
// InitDatabase 手动初始化数据库连接
|
||||
func InitDatabase() error {
|
||||
dbConfig := loadDBConfig()
|
||||
// 创建通用的 GORM 配置
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch dbConfig.Driver {
|
||||
case "postgres":
|
||||
if db, err = initPostgres(dbConfig, gormConfig); err != nil {
|
||||
return fmt.Errorf("postgres initialization failed: %w", err)
|
||||
}
|
||||
logrus.Infoln("postgres initialization succeeded", dbConfig)
|
||||
case "sqlite":
|
||||
if db, err = initSQLite(dbConfig.Path, gormConfig); err != nil {
|
||||
return fmt.Errorf("sqlite initialization failed: %w", err)
|
||||
}
|
||||
logrus.Infoln("sqlite initialization succeeded", dbConfig)
|
||||
default:
|
||||
return errors.New("unsupported database driver, only sqlite and postgres are supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
// TODO: impl
|
||||
|
||||
//// 迁移模型
|
||||
//if err = models.Migrate(db); err != nil {
|
||||
// logrus.Error("Failed to migrate models:", err)
|
||||
// return err
|
||||
//}
|
||||
//// 执行初始化数据
|
||||
//// 创建管理员账户
|
||||
//hashedPassword, err := utils.Password.HashPassword(config.AdminPassword, config.JwtSecret)
|
||||
//if err != nil {
|
||||
// logrus.Error("Failed to hash password:", err)
|
||||
// return err
|
||||
//}
|
||||
//user := &models.User{
|
||||
// Name: config.AdminUsername,
|
||||
// Password: &hashedPassword,
|
||||
// Role: constants.GlobalRoleAdmin,
|
||||
//}
|
||||
//if err = User.UpdateSystemAdmin(user); err != nil {
|
||||
// logrus.Error("Failed to update admin user:", err)
|
||||
// return err
|
||||
//}
|
||||
//return nil
|
||||
}
|
||||
|
||||
// initPostgres 初始化PostgreSQL连接
|
||||
func initPostgres(config DBConfig, gormConfig *gorm.Config) (db *gorm.DB, err error) {
|
||||
if config.Host == "" || config.User == "" || config.Password == "" || config.DBName == "" {
|
||||
err = errors.New("PostgreSQL configuration is incomplete: host, user, password, and dbname are required")
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
||||
|
||||
db, err = gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
return
|
||||
}
|
||||
|
||||
// initSQLite 初始化 SQLite 连接
|
||||
func initSQLite(path string, gormConfig *gorm.Config) (*gorm.DB, error) {
|
||||
if path == "" {
|
||||
path = "./data/data.db"
|
||||
}
|
||||
// 创建 SQLite 数据库文件的目录
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory for SQLite database: %w", err)
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open(path), gormConfig)
|
||||
return db, err
|
||||
}
|
||||
|
||||
func migrate() error {
|
||||
return GetDB().AutoMigrate(
|
||||
&model.Comment{},
|
||||
&model.Label{},
|
||||
&model.Post{},
|
||||
&model.User{})
|
||||
}
|
45
internal/repo/user.go
Normal file
45
internal/repo/user.go
Normal file
@ -0,0 +1,45 @@
|
||||
package repo
|
||||
|
||||
import "github.com/snowykami/neo-blog/internal/model"
|
||||
|
||||
type userRepo struct{}
|
||||
|
||||
var User = &userRepo{}
|
||||
|
||||
func (user *userRepo) GetByUsername(username string) (*model.User, error) {
|
||||
var userModel model.User
|
||||
if err := GetDB().Where("username = ?", username).First(&userModel).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userModel, nil
|
||||
}
|
||||
|
||||
func (user *userRepo) GetByEmail(email string) (*model.User, error) {
|
||||
var userModel model.User
|
||||
if err := GetDB().Where("email = ?", email).First(&userModel).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userModel, nil
|
||||
}
|
||||
|
||||
func (user *userRepo) GetByUsernameOrEmail(usernameOrEmail string) (*model.User, error) {
|
||||
var userModel model.User
|
||||
if err := GetDB().Where("username = ? OR email = ?", usernameOrEmail, usernameOrEmail).First(&userModel).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userModel, nil
|
||||
}
|
||||
|
||||
func (user *userRepo) Create(userModel *model.User) error {
|
||||
if err := GetDB().Create(userModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *userRepo) Update(userModel *model.User) error {
|
||||
if err := GetDB().Updates(userModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -11,7 +11,7 @@ import (
|
||||
var h *server.Hertz
|
||||
|
||||
func Run() error {
|
||||
mode := utils.Getenv("MODE", constant.ModeProd) // dev | prod
|
||||
mode := utils.Env.Get("MODE", constant.ModeProd) // dev | prod
|
||||
switch mode {
|
||||
case constant.ModeProd:
|
||||
h.Spin()
|
||||
@ -25,8 +25,8 @@ func Run() error {
|
||||
|
||||
func init() {
|
||||
h = server.New(
|
||||
server.WithHostPorts(":"+utils.Getenv("PORT", "8888")),
|
||||
server.WithMaxRequestBodySize(utils.GetenvAsInt("MAX_REQUEST_BODY_SIZE", 1048576000)), // 1000MiB
|
||||
server.WithHostPorts(":"+utils.Env.Get("PORT", "8888")),
|
||||
server.WithMaxRequestBodySize(utils.Env.GetenvAsInt("MAX_REQUEST_BODY_SIZE", 1048576000)), // 1000MiB
|
||||
)
|
||||
apiv1.RegisterRoutes(h)
|
||||
}
|
||||
|
32
internal/service/user.go
Normal file
32
internal/service/user.go
Normal file
@ -0,0 +1,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/snowykami/neo-blog/internal/dto"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"github.com/snowykami/neo-blog/pkg/resps"
|
||||
"github.com/snowykami/neo-blog/pkg/utils"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
UserLogin(dto *dto.UserLoginReq) (*dto.UserLoginResp, error)
|
||||
UserRegister(dto *dto.UserRegisterReq) (*dto.UserRegisterResp, error)
|
||||
}
|
||||
|
||||
type userService struct{}
|
||||
|
||||
func NewUserService() UserService {
|
||||
return &userService{}
|
||||
}
|
||||
|
||||
func (s *userService) UserLogin(dto *dto.UserLoginReq) (*dto.UserLoginResp, error) {
|
||||
user, err := repo.User.GetByUsernameOrEmail(dto.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New(resps.ErrNotFound)
|
||||
}
|
||||
utils.Password.VerifyPassword(dto.Password, user.Password, utils.Env.Get(constant.EnvVarPasswordSalt, "default_salt"))
|
||||
}
|
Reference in New Issue
Block a user