feat: Closes #14 更新用户模型,确保用户名字段为非空;优化OIDC请求和用户信息处理逻辑;添加新的重写规则以支持HTTPS
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 19s

This commit is contained in:
2025-09-14 13:54:23 +08:00
parent 78cc596544
commit a0b2c75839
6 changed files with 405 additions and 399 deletions

View File

@ -2,7 +2,6 @@ package v1
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
@ -92,7 +91,6 @@ func (u *UserController) OidcLogin(ctx context.Context, c *app.RequestContext) {
if redirectUri == "" { if redirectUri == "" {
redirectUri = "/" redirectUri = "/"
} }
fmt.Println("redirectBack:", redirectUri)
oidcLoginReq := &dto.OidcLoginReq{ oidcLoginReq := &dto.OidcLoginReq{
Name: name, Name: name,
Code: code, Code: code,

View File

@ -7,7 +7,7 @@ import (
type User struct { type User struct {
gorm.Model gorm.Model
Username string `gorm:"uniqueIndex"` // 用户名,唯一 Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一
Nickname string Nickname string
AvatarUrl string AvatarUrl string
Email string `gorm:"uniqueIndex"` Email string `gorm:"uniqueIndex"`

View File

@ -1,400 +1,401 @@
package service package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/internal/dto"
"github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/internal/model"
"github.com/snowykami/neo-blog/internal/repo" "github.com/snowykami/neo-blog/internal/repo"
"github.com/snowykami/neo-blog/internal/static" "github.com/snowykami/neo-blog/internal/static"
"github.com/snowykami/neo-blog/pkg/constant" "github.com/snowykami/neo-blog/pkg/constant"
"github.com/snowykami/neo-blog/pkg/errs" "github.com/snowykami/neo-blog/pkg/errs"
"github.com/snowykami/neo-blog/pkg/utils" "github.com/snowykami/neo-blog/pkg/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
type UserService struct{} type UserService struct{}
func NewUserService() *UserService { func NewUserService() *UserService {
return &UserService{} return &UserService{}
} }
func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, error) { func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, error) {
user, err := repo.User.GetUserByUsernameOrEmail(req.Username) user, err := repo.User.GetUserByUsernameOrEmail(req.Username)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
logrus.Warnf("User not found: %s", req.Username) logrus.Warnf("User not found: %s", req.Username)
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if user == nil { if user == nil {
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) { if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) {
token, refreshToken, err := s.generate2Token(user.ID) token, refreshToken, err := s.generate2Token(user.ID)
if err != nil { if err != nil {
logrus.Errorln("Failed to generate tokens:", err) logrus.Errorln("Failed to generate tokens:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
resp := &dto.UserLoginResp{ resp := &dto.UserLoginResp{
Token: token, Token: token,
RefreshToken: refreshToken, RefreshToken: refreshToken,
User: user.ToDto(), User: user.ToDto(),
} }
return resp, nil return resp, nil
} else { } else {
return nil, errs.New(http.StatusUnauthorized, "Invalid username or password", nil) return nil, errs.New(http.StatusUnauthorized, "Invalid username or password", nil)
} }
} }
func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) { func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) {
// 验证邮箱验证码 // 验证邮箱验证码
if !utils.Env.GetAsBool("ENABLE_REGISTER", true) { if !utils.Env.GetAsBool("ENABLE_REGISTER", true) {
return nil, errs.ErrForbidden return nil, errs.ErrForbidden
} }
if utils.Env.GetAsBool("ENABLE_EMAIL_VERIFICATION", true) { if utils.Env.GetAsBool("ENABLE_EMAIL_VERIFICATION", true) {
ok, err := s.verifyEmail(req.Email, req.VerificationCode) ok, err := s.verifyEmail(req.Email, req.VerificationCode)
if err != nil { if err != nil {
logrus.Errorln("Failed to verify email:", err) logrus.Errorln("Failed to verify email:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if !ok { if !ok {
return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil) return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil)
} }
} }
// 检查用户名或邮箱是否已存在 // 检查用户名或邮箱是否已存在
usernameExist, err := repo.User.CheckUsernameExists(req.Username) usernameExist, err := repo.User.CheckUsernameExists(req.Username)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
emailExist, err := repo.User.CheckEmailExists(req.Email) emailExist, err := repo.User.CheckEmailExists(req.Email)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if usernameExist || emailExist { if usernameExist || emailExist {
return nil, errs.New(http.StatusConflict, "Username or email already exists", nil) return nil, errs.New(http.StatusConflict, "Username or email already exists", nil)
} }
// 创建新用户 // 创建新用户
hashedPassword, err := utils.Password.HashPassword(req.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) hashedPassword, err := utils.Password.HashPassword(req.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt"))
if err != nil { if err != nil {
logrus.Errorln("Failed to hash password:", err) logrus.Errorln("Failed to hash password:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
newUser := &model.User{ newUser := &model.User{
Username: req.Username, Username: req.Username,
Nickname: req.Nickname, Nickname: req.Nickname,
Email: req.Email, Email: req.Email,
Gender: "", Gender: "",
Role: "user", Role: "user",
Password: hashedPassword, Password: hashedPassword,
} }
err = repo.User.CreateUser(newUser) err = repo.User.CreateUser(newUser)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
// 创建默认管理员账户 // 创建默认管理员账户
if newUser.ID == 1 { if newUser.ID == 1 {
newUser.Role = constant.RoleAdmin newUser.Role = constant.RoleAdmin
err = repo.User.UpdateUser(newUser) err = repo.User.UpdateUser(newUser)
if err != nil { if err != nil {
logrus.Errorln("Failed to update user role to admin:", err) logrus.Errorln("Failed to update user role to admin:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
} }
// 生成访问令牌和刷新令牌 // 生成访问令牌和刷新令牌
token, refreshToken, err := s.generate2Token(newUser.ID) token, refreshToken, err := s.generate2Token(newUser.ID)
if err != nil { if err != nil {
logrus.Errorln("Failed to generate tokens:", err) logrus.Errorln("Failed to generate tokens:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
resp := &dto.UserRegisterResp{ resp := &dto.UserRegisterResp{
Token: token, Token: token,
RefreshToken: refreshToken, RefreshToken: refreshToken,
User: newUser.ToDto(), User: newUser.ToDto(),
} }
return resp, nil return resp, nil
} }
func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) { func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) {
generatedVerificationCode := utils.Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef") generatedVerificationCode := utils.Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef")
kv := utils.KV.GetInstance() kv := utils.KV.GetInstance()
kv.Set(constant.KVKeyEmailVerificationCode+req.Email, generatedVerificationCode, time.Minute*10) kv.Set(constant.KVKeyEmailVerificationCode+req.Email, generatedVerificationCode, time.Minute*10)
template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{}) template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{})
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if utils.IsDevMode { if utils.IsDevMode {
logrus.Infof("%s's verification code is %s", req.Email, generatedVerificationCode) logrus.Infof("%s's verification code is %s", req.Email, generatedVerificationCode)
} }
err = utils.Email.SendEmail(utils.Email.GetEmailConfigFromEnv(), req.Email, "验证你的电子邮件 / Verify your email", template, true) err = utils.Email.SendEmail(utils.Email.GetEmailConfigFromEnv(), req.Email, "验证你的电子邮件 / Verify your email", template, true)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
return &dto.VerifyEmailResp{Success: true}, nil return &dto.VerifyEmailResp{Success: true}, nil
} }
func (s *UserService) ListOidcConfigs() ([]dto.UserOidcConfigDto, error) { func (s *UserService) ListOidcConfigs() ([]dto.UserOidcConfigDto, error) {
enabledOidcConfigs, err := repo.Oidc.ListOidcConfigs(true) enabledOidcConfigs, err := repo.Oidc.ListOidcConfigs(true)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
var oidcConfigsDtos []dto.UserOidcConfigDto var oidcConfigsDtos []dto.UserOidcConfigDto
for _, oidcConfig := range enabledOidcConfigs { for _, oidcConfig := range enabledOidcConfigs {
state := utils.Strings.GenerateRandomString(32) state := utils.Strings.GenerateRandomString(32)
kvStore := utils.KV.GetInstance() kvStore := utils.KV.GetInstance()
kvStore.Set(constant.KVKeyOidcState+state, oidcConfig.Name, 5*time.Minute) kvStore.Set(constant.KVKeyOidcState+state, oidcConfig.Name, 5*time.Minute)
loginUrl := utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{ loginUrl := utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{
"client_id": oidcConfig.ClientID, "client_id": oidcConfig.ClientID,
"redirect_uri": fmt.Sprintf("%s%s%s/%sREDIRECT_BACK", // 这个大占位符给前端替换用的替换时也要uri编码因为是层层包的 "redirect_uri": fmt.Sprintf("%s%s%s/%sREDIRECT_BACK", // 这个大占位符给前端替换用的替换时也要uri编码因为是层层包的
strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"), strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"),
constant.ApiSuffix, constant.ApiSuffix,
constant.OidcUri, constant.OidcUri,
oidcConfig.Name, oidcConfig.Name,
), ),
"response_type": "code", "response_type": "code",
"scope": "openid email profile", "scope": "openid email profile",
"state": state, "state": state,
}) })
if oidcConfig.Type == constant.OidcProviderTypeMisskey { if oidcConfig.Type == constant.OidcProviderTypeMisskey {
// Misskey OIDC 特殊处理 // Misskey OIDC 特殊处理
loginUrl = utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{ loginUrl = utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{
"client_id": oidcConfig.ClientID, "client_id": oidcConfig.ClientID,
"redirect_uri": fmt.Sprintf("%s%s%s/%s", // 这个大占位符给前端替换用的替换时也要uri编码因为是层层包的 "redirect_uri": fmt.Sprintf("%s%s%s/%s", // 这个大占位符给前端替换用的替换时也要uri编码因为是层层包的
strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"), strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"),
constant.ApiSuffix, constant.ApiSuffix,
constant.OidcUri, constant.OidcUri,
oidcConfig.Name, oidcConfig.Name,
), ),
"response_type": "code", "response_type": "code",
"scope": "read:account", "scope": "read:account",
"state": state, "state": state,
}) })
} }
oidcConfigsDtos = append(oidcConfigsDtos, dto.UserOidcConfigDto{ oidcConfigsDtos = append(oidcConfigsDtos, dto.UserOidcConfigDto{
Name: oidcConfig.Name, Name: oidcConfig.Name,
DisplayName: oidcConfig.DisplayName, DisplayName: oidcConfig.DisplayName,
Icon: oidcConfig.Icon, Icon: oidcConfig.Icon,
LoginUrl: loginUrl, LoginUrl: loginUrl,
}) })
} }
return oidcConfigsDtos, nil return oidcConfigsDtos, nil
} }
func (s *UserService) OidcLogin(req *dto.OidcLoginReq) (*dto.OidcLoginResp, error) { func (s *UserService) OidcLogin(req *dto.OidcLoginReq) (*dto.OidcLoginResp, error) {
// 验证state // 验证state
kvStore := utils.KV.GetInstance() kvStore := utils.KV.GetInstance()
storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State) storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State)
if !ok || storedName != req.Name { if !ok || storedName != req.Name {
return nil, errs.New(http.StatusForbidden, "invalid oidc state", nil) return nil, errs.New(http.StatusForbidden, "invalid oidc state", nil)
} }
// 获取OIDC配置 // 获取OIDC配置
oidcConfig, err := repo.Oidc.GetOidcConfigByName(req.Name) oidcConfig, err := repo.Oidc.GetOidcConfigByName(req.Name)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if oidcConfig == nil { if oidcConfig == nil {
return nil, errs.New(http.StatusNotFound, "OIDC configuration not found", nil) return nil, errs.New(http.StatusNotFound, "OIDC configuration not found", nil)
} }
// 请求访问令牌 // 请求访问令牌
tokenResp, err := utils.Oidc.RequestToken( tokenResp, err := utils.Oidc.RequestToken(
oidcConfig.TokenEndpoint, oidcConfig.TokenEndpoint,
oidcConfig.ClientID, oidcConfig.ClientID,
oidcConfig.ClientSecret, oidcConfig.ClientSecret,
req.Code, req.Code,
strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/")+constant.OidcUri+oidcConfig.Name, strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/")+constant.OidcUri+oidcConfig.Name,
) )
if err != nil { if err != nil {
logrus.Errorln("Failed to request OIDC token:", err) logrus.Errorln("Failed to request OIDC token:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
userInfo, err := utils.Oidc.RequestUserInfo(oidcConfig.UserInfoEndpoint, tokenResp.AccessToken) userInfo, err := utils.Oidc.RequestUserInfo(oidcConfig.UserInfoEndpoint, tokenResp.AccessToken)
if err != nil { if err != nil {
logrus.Errorln("Failed to request OIDC user info:", err) logrus.Errorln("Failed to request OIDC user info:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
// 绑定过登录 // 绑定过登录
userOpenID, err := repo.User.GetUserOpenIDByIssuerAndSub(oidcConfig.Issuer, userInfo.Sub) userOpenID, err := repo.User.GetUserOpenIDByIssuerAndSub(oidcConfig.Issuer, userInfo.Sub)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if userOpenID != nil { if userOpenID != nil {
user, err := repo.User.GetUserByID(userOpenID.UserID) user, err := repo.User.GetUserByID(userOpenID.UserID)
if err != nil { if err != nil {
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
token, refreshToken, err := s.generate2Token(user.ID) token, refreshToken, err := s.generate2Token(user.ID)
if err != nil { if err != nil {
logrus.Errorln("Failed to generate tokens:", err) logrus.Errorln("Failed to generate tokens:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
resp := &dto.OidcLoginResp{ resp := &dto.OidcLoginResp{
Token: token, Token: token,
RefreshToken: refreshToken, RefreshToken: refreshToken,
User: user.ToDto(), User: user.ToDto(),
} }
return resp, nil return resp, nil
} else { } else {
// 若没有绑定过登录,则先通过邮箱查找用户,若没有再创建新用户 // 若没有绑定过登录,则先通过邮箱查找用户,若没有再创建新用户
user, err := repo.User.GetUserByEmail(userInfo.Email) user, err := repo.User.GetUserByEmail(userInfo.Email)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logrus.Errorln("Failed to get user by email:", err) logrus.Errorln("Failed to get user by email:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if user != nil { if user != nil {
userOpenID = &model.UserOpenID{ userOpenID = &model.UserOpenID{
UserID: user.ID, UserID: user.ID,
Issuer: oidcConfig.Issuer, Issuer: oidcConfig.Issuer,
Sub: userInfo.Sub, Sub: userInfo.Sub,
} }
err = repo.User.CreateOrUpdateUserOpenID(userOpenID) err = repo.User.CreateOrUpdateUserOpenID(userOpenID)
if err != nil { if err != nil {
logrus.Errorln("Failed to create or update user OpenID:", err) logrus.Errorln("Failed to create or update user OpenID:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
token, refreshToken, err := s.generate2Token(user.ID) token, refreshToken, err := s.generate2Token(user.ID)
if err != nil { if err != nil {
logrus.Errorln("Failed to generate tokens:", err) logrus.Errorln("Failed to generate tokens:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
resp := &dto.OidcLoginResp{ resp := &dto.OidcLoginResp{
Token: token, Token: token,
RefreshToken: refreshToken, RefreshToken: refreshToken,
User: user.ToDto(), User: user.ToDto(),
} }
return resp, nil return resp, nil
} else { } else {
user = &model.User{ // 第一次登录,创建新用户时才获取头像
Username: userInfo.Name, user = &model.User{
Nickname: userInfo.Name, Username: userInfo.PreferredUsername,
AvatarUrl: userInfo.Picture, Nickname: userInfo.Name,
Email: userInfo.Email, AvatarUrl: userInfo.Picture,
} Email: userInfo.Email,
err = repo.User.CreateUser(user) }
if err != nil { err = repo.User.CreateUser(user)
logrus.Errorln("Failed to create user:", err) if err != nil {
return nil, errs.ErrInternalServer logrus.Errorln("Failed to create user:", err)
} return nil, errs.ErrInternalServer
userOpenID = &model.UserOpenID{ }
UserID: user.ID, userOpenID = &model.UserOpenID{
Issuer: oidcConfig.Issuer, UserID: user.ID,
Sub: userInfo.Sub, Issuer: oidcConfig.Issuer,
} Sub: userInfo.Sub,
err = repo.User.CreateOrUpdateUserOpenID(userOpenID) }
if err != nil { err = repo.User.CreateOrUpdateUserOpenID(userOpenID)
logrus.Errorln("Failed to create or update user OpenID:", err) if err != nil {
return nil, errs.ErrInternalServer logrus.Errorln("Failed to create or update user OpenID:", err)
} return nil, errs.ErrInternalServer
token, refreshToken, err := s.generate2Token(user.ID) }
if err != nil { token, refreshToken, err := s.generate2Token(user.ID)
logrus.Errorln("Failed to generate tokens:", err) if err != nil {
return nil, errs.ErrInternalServer logrus.Errorln("Failed to generate tokens:", err)
} return nil, errs.ErrInternalServer
resp := &dto.OidcLoginResp{ }
Token: token, resp := &dto.OidcLoginResp{
RefreshToken: refreshToken, Token: token,
User: user.ToDto(), RefreshToken: refreshToken,
} User: user.ToDto(),
return resp, nil }
} return resp, nil
} }
}
} }
func (s *UserService) GetUser(req *dto.GetUserReq) (*dto.GetUserResp, error) { func (s *UserService) GetUser(req *dto.GetUserReq) (*dto.GetUserResp, error) {
if req.UserID == 0 { if req.UserID == 0 {
return nil, errs.New(http.StatusBadRequest, "user_id is required", nil) return nil, errs.New(http.StatusBadRequest, "user_id is required", nil)
} }
user, err := repo.User.GetUserByID(req.UserID) user, err := repo.User.GetUserByID(req.UserID)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
logrus.Errorln("Failed to get user by ID:", err) logrus.Errorln("Failed to get user by ID:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if user == nil { if user == nil {
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
return &dto.GetUserResp{ return &dto.GetUserResp{
User: user.ToDto(), User: user.ToDto(),
}, nil }, nil
} }
func (s *UserService) GetUserByUsername(req *dto.GetUserByUsernameReq) (*dto.GetUserResp, error) { func (s *UserService) GetUserByUsername(req *dto.GetUserByUsernameReq) (*dto.GetUserResp, error) {
if req.Username == "" { if req.Username == "" {
return nil, errs.New(http.StatusBadRequest, "username is required", nil) return nil, errs.New(http.StatusBadRequest, "username is required", nil)
} }
user, err := repo.User.GetUserByUsername(req.Username) user, err := repo.User.GetUserByUsername(req.Username)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
logrus.Errorln("Failed to get user by username:", err) logrus.Errorln("Failed to get user by username:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
if user == nil { if user == nil {
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
return &dto.GetUserResp{ return &dto.GetUserResp{
User: user.ToDto(), User: user.ToDto(),
}, nil }, nil
} }
func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, error) { func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, error) {
user := &model.User{ user := &model.User{
Model: gorm.Model{ Model: gorm.Model{
ID: req.ID, ID: req.ID,
}, },
Username: req.Username, Username: req.Username,
Nickname: req.Nickname, Nickname: req.Nickname,
Gender: req.Gender, Gender: req.Gender,
AvatarUrl: req.AvatarUrl, AvatarUrl: req.AvatarUrl,
} }
err := repo.User.UpdateUser(user) err := repo.User.UpdateUser(user)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errs.ErrNotFound return nil, errs.ErrNotFound
} }
logrus.Errorln("Failed to update user:", err) logrus.Errorln("Failed to update user:", err)
return nil, errs.ErrInternalServer return nil, errs.ErrInternalServer
} }
return &dto.UpdateUserResp{}, nil return &dto.UpdateUserResp{}, nil
} }
func (s *UserService) generate2Token(userID uint) (string, string, error) { func (s *UserService) generate2Token(userID uint) (string, string, error) {
token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault))*time.Second) token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault))*time.Second)
tokenString, err := token.ToString() tokenString, err := token.ToString()
if err != nil { if err != nil {
return "", "", errs.ErrInternalServer return "", "", errs.ErrInternalServer
} }
refreshToken := utils.Jwt.NewClaims(userID, utils.Strings.GenerateRandomString(64), true, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, constant.EnvKeyRefreshTokenDurationDefault))*time.Second) refreshToken := utils.Jwt.NewClaims(userID, utils.Strings.GenerateRandomString(64), true, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, constant.EnvKeyRefreshTokenDurationDefault))*time.Second)
refreshTokenString, err := refreshToken.ToString() refreshTokenString, err := refreshToken.ToString()
if err != nil { if err != nil {
return "", "", errs.ErrInternalServer return "", "", errs.ErrInternalServer
} }
err = repo.Session.SaveSession(refreshToken.SessionKey) err = repo.Session.SaveSession(refreshToken.SessionKey)
if err != nil { if err != nil {
return "", "", errs.ErrInternalServer return "", "", errs.ErrInternalServer
} }
return tokenString, refreshTokenString, nil return tokenString, refreshTokenString, nil
} }
func (s *UserService) verifyEmail(email, code string) (bool, error) { func (s *UserService) verifyEmail(email, code string) (bool, error) {
kv := utils.KV.GetInstance() kv := utils.KV.GetInstance()
verificationCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email) verificationCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email)
if !ok || verificationCode != code { if !ok || verificationCode != code {
return false, errs.New(http.StatusForbidden, "Invalid email verification code", nil) return false, errs.New(http.StatusForbidden, "Invalid email verification code", nil)
} }
return true, nil return true, nil
} }

View File

@ -1,7 +1,7 @@
package utils package utils
import ( import (
"fmt" "fmt"
) )
type oidcUtils struct{} type oidcUtils struct{}
@ -10,59 +10,60 @@ var Oidc = oidcUtils{}
// RequestToken 请求访问令牌 // RequestToken 请求访问令牌
func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) { func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) {
tokenResp, err := client.R(). tokenResp, err := client.R().
SetFormData(map[string]string{ SetFormData(map[string]string{
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": clientID, "client_id": clientID,
"client_secret": clientSecret, "client_secret": clientSecret,
"code": code, "code": code,
"redirect_uri": redirectURI, "redirect_uri": redirectURI,
}). }).
SetHeader("Accept", "application/json"). SetHeader("Accept", "application/json").
SetResult(&TokenResponse{}). SetResult(&TokenResponse{}).
Post(tokenEndpoint) Post(tokenEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if tokenResp.StatusCode() != 200 { if tokenResp.StatusCode() != 200 {
return nil, fmt.Errorf("状态码: %d响应: %s", tokenResp.StatusCode(), tokenResp.String()) return nil, fmt.Errorf("状态码: %d响应: %s", tokenResp.StatusCode(), tokenResp.String())
} }
return tokenResp.Result().(*TokenResponse), nil return tokenResp.Result().(*TokenResponse), nil
} }
// RequestUserInfo 请求用户信息 // RequestUserInfo 请求用户信息
func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) { func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) {
userInfoResp, err := client.R(). userInfoResp, err := client.R().
SetHeader("Authorization", "Bearer "+accessToken). SetHeader("Authorization", "Bearer "+accessToken).
SetHeader("Accept", "application/json"). SetHeader("Accept", "application/json").
SetResult(&UserInfo{}). SetResult(&UserInfo{}).
Get(userInfoEndpoint) Get(userInfoEndpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if userInfoResp.StatusCode() != 200 { if userInfoResp.StatusCode() != 200 {
return nil, fmt.Errorf("状态码: %d响应: %s", userInfoResp.StatusCode(), userInfoResp.String()) return nil, fmt.Errorf("状态码: %d响应: %s", userInfoResp.StatusCode(), userInfoResp.String())
} }
return userInfoResp.Result().(*UserInfo), nil return userInfoResp.Result().(*UserInfo), nil
} }
type TokenResponse struct { type TokenResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
TokenType string `json:"token_type"` TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
IDToken string `json:"id_token,omitempty"` IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"`
} }
// UserInfo 定义用户信息结构 // UserInfo 定义用户信息结构
type UserInfo struct { type UserInfo struct {
Sub string `json:"sub"` Sub string `json:"sub"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Picture string `json:"picture,omitempty"` Picture string `json:"picture,omitempty"`
Groups []string `json:"groups,omitempty"` // 可选字段OIDC提供的用户组信息 PreferredUsername string `json:"preferred_username"`
Groups []string `json:"groups,omitempty"` // 可选字段OIDC提供的用户组信息
} }

View File

@ -18,6 +18,12 @@ const nextConfig: NextConfig = {
port: '', port: '',
pathname: '/**', pathname: '/**',
}, },
{
protocol: 'https',
hostname: 'pass.liteyuki.org',
port: '',
pathname: '/**',
},
], ],
}, },
async rewrites() { async rewrites() {

View File

@ -166,7 +166,7 @@ export function CommentItem(
<div className="flex-1 pl-2 fade-in-up"> <div className="flex-1 pl-2 fade-in-up">
<div className="flex gap-2 md:gap-4 items-center"> <div className="flex gap-2 md:gap-4 items-center">
<div onClick={() => clickToUserProfile(commentState.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up"> <div onClick={() => clickToUserProfile(commentState.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up">
{commentState.user.nickname} {commentState.user.nickname || commentState.user.username}
</div> </div>
<span className="text-xs">{formatDateTime({ <span className="text-xs">{formatDateTime({
dateTimeString: commentState.createdAt, dateTimeString: commentState.createdAt,