diff --git a/internal/controller/v1/user.go b/internal/controller/v1/user.go index ef20db6..835ee4b 100644 --- a/internal/controller/v1/user.go +++ b/internal/controller/v1/user.go @@ -2,7 +2,6 @@ package v1 import ( "context" - "fmt" "strconv" "github.com/cloudwego/hertz/pkg/app" @@ -92,7 +91,6 @@ func (u *UserController) OidcLogin(ctx context.Context, c *app.RequestContext) { if redirectUri == "" { redirectUri = "/" } - fmt.Println("redirectBack:", redirectUri) oidcLoginReq := &dto.OidcLoginReq{ Name: name, Code: code, diff --git a/internal/model/user.go b/internal/model/user.go index ea808fc..3b7d896 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -7,7 +7,7 @@ import ( type User struct { gorm.Model - Username string `gorm:"uniqueIndex"` // 用户名,唯一 + Username string `gorm:"uniqueIndex;not null"` // 用户名,唯一 Nickname string AvatarUrl string Email string `gorm:"uniqueIndex"` diff --git a/internal/service/user.go b/internal/service/user.go index 6cbb6af..7ef6137 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -1,400 +1,401 @@ package service import ( - "errors" - "fmt" - "net/http" - "strings" - "time" + "errors" + "fmt" + "net/http" + "strings" + "time" - "github.com/sirupsen/logrus" - "github.com/snowykami/neo-blog/internal/dto" - "github.com/snowykami/neo-blog/internal/model" - "github.com/snowykami/neo-blog/internal/repo" - "github.com/snowykami/neo-blog/internal/static" - "github.com/snowykami/neo-blog/pkg/constant" - "github.com/snowykami/neo-blog/pkg/errs" - "github.com/snowykami/neo-blog/pkg/utils" - "gorm.io/gorm" + "github.com/sirupsen/logrus" + "github.com/snowykami/neo-blog/internal/dto" + "github.com/snowykami/neo-blog/internal/model" + "github.com/snowykami/neo-blog/internal/repo" + "github.com/snowykami/neo-blog/internal/static" + "github.com/snowykami/neo-blog/pkg/constant" + "github.com/snowykami/neo-blog/pkg/errs" + "github.com/snowykami/neo-blog/pkg/utils" + "gorm.io/gorm" ) type UserService struct{} func NewUserService() *UserService { - return &UserService{} + return &UserService{} } func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, error) { - user, err := repo.User.GetUserByUsernameOrEmail(req.Username) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - logrus.Warnf("User not found: %s", req.Username) - return nil, errs.ErrNotFound - } - return nil, errs.ErrInternalServer - } - if user == nil { - return nil, errs.ErrNotFound - } - if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) { - token, refreshToken, err := s.generate2Token(user.ID) - if err != nil { - logrus.Errorln("Failed to generate tokens:", err) - return nil, errs.ErrInternalServer - } - resp := &dto.UserLoginResp{ - Token: token, - RefreshToken: refreshToken, - User: user.ToDto(), - } - return resp, nil - } else { - return nil, errs.New(http.StatusUnauthorized, "Invalid username or password", nil) - } + user, err := repo.User.GetUserByUsernameOrEmail(req.Username) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + logrus.Warnf("User not found: %s", req.Username) + return nil, errs.ErrNotFound + } + return nil, errs.ErrInternalServer + } + if user == nil { + return nil, errs.ErrNotFound + } + if utils.Password.VerifyPassword(req.Password, user.Password, utils.Env.Get(constant.EnvKeyPasswordSalt, "default_salt")) { + token, refreshToken, err := s.generate2Token(user.ID) + if err != nil { + logrus.Errorln("Failed to generate tokens:", err) + return nil, errs.ErrInternalServer + } + resp := &dto.UserLoginResp{ + Token: token, + RefreshToken: refreshToken, + User: user.ToDto(), + } + return resp, nil + } else { + return nil, errs.New(http.StatusUnauthorized, "Invalid username or password", nil) + } } func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) { - // 验证邮箱验证码 - if !utils.Env.GetAsBool("ENABLE_REGISTER", true) { - return nil, errs.ErrForbidden - } - if utils.Env.GetAsBool("ENABLE_EMAIL_VERIFICATION", true) { - ok, err := s.verifyEmail(req.Email, req.VerificationCode) - if err != nil { - logrus.Errorln("Failed to verify email:", err) - return nil, errs.ErrInternalServer - } - if !ok { - return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil) - } - } - // 检查用户名或邮箱是否已存在 - usernameExist, err := repo.User.CheckUsernameExists(req.Username) - if err != nil { - return nil, errs.ErrInternalServer - } - emailExist, err := repo.User.CheckEmailExists(req.Email) - if err != nil { - return nil, errs.ErrInternalServer - } - if usernameExist || emailExist { - 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")) - if err != nil { - logrus.Errorln("Failed to hash password:", err) - return nil, errs.ErrInternalServer - } - newUser := &model.User{ - Username: req.Username, - Nickname: req.Nickname, - Email: req.Email, - Gender: "", - Role: "user", - Password: hashedPassword, - } - err = repo.User.CreateUser(newUser) - if err != nil { - return nil, errs.ErrInternalServer - } - // 创建默认管理员账户 - if newUser.ID == 1 { - newUser.Role = constant.RoleAdmin - err = repo.User.UpdateUser(newUser) - if err != nil { - logrus.Errorln("Failed to update user role to admin:", err) - return nil, errs.ErrInternalServer - } - } - // 生成访问令牌和刷新令牌 - token, refreshToken, err := s.generate2Token(newUser.ID) - if err != nil { - logrus.Errorln("Failed to generate tokens:", err) - return nil, errs.ErrInternalServer - } - resp := &dto.UserRegisterResp{ - Token: token, - RefreshToken: refreshToken, - User: newUser.ToDto(), - } - return resp, nil + // 验证邮箱验证码 + if !utils.Env.GetAsBool("ENABLE_REGISTER", true) { + return nil, errs.ErrForbidden + } + if utils.Env.GetAsBool("ENABLE_EMAIL_VERIFICATION", true) { + ok, err := s.verifyEmail(req.Email, req.VerificationCode) + if err != nil { + logrus.Errorln("Failed to verify email:", err) + return nil, errs.ErrInternalServer + } + if !ok { + return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil) + } + } + // 检查用户名或邮箱是否已存在 + usernameExist, err := repo.User.CheckUsernameExists(req.Username) + if err != nil { + return nil, errs.ErrInternalServer + } + emailExist, err := repo.User.CheckEmailExists(req.Email) + if err != nil { + return nil, errs.ErrInternalServer + } + if usernameExist || emailExist { + 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")) + if err != nil { + logrus.Errorln("Failed to hash password:", err) + return nil, errs.ErrInternalServer + } + newUser := &model.User{ + Username: req.Username, + Nickname: req.Nickname, + Email: req.Email, + Gender: "", + Role: "user", + Password: hashedPassword, + } + err = repo.User.CreateUser(newUser) + if err != nil { + return nil, errs.ErrInternalServer + } + // 创建默认管理员账户 + if newUser.ID == 1 { + newUser.Role = constant.RoleAdmin + err = repo.User.UpdateUser(newUser) + if err != nil { + logrus.Errorln("Failed to update user role to admin:", err) + return nil, errs.ErrInternalServer + } + } + // 生成访问令牌和刷新令牌 + token, refreshToken, err := s.generate2Token(newUser.ID) + if err != nil { + logrus.Errorln("Failed to generate tokens:", err) + return nil, errs.ErrInternalServer + } + resp := &dto.UserRegisterResp{ + Token: token, + RefreshToken: refreshToken, + User: newUser.ToDto(), + } + return resp, nil } func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) { - generatedVerificationCode := utils.Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef") - kv := utils.KV.GetInstance() - kv.Set(constant.KVKeyEmailVerificationCode+req.Email, generatedVerificationCode, time.Minute*10) + generatedVerificationCode := utils.Strings.GenerateRandomStringWithCharset(6, "0123456789abcdef") + kv := utils.KV.GetInstance() + kv.Set(constant.KVKeyEmailVerificationCode+req.Email, generatedVerificationCode, time.Minute*10) - template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{}) - if err != nil { - return nil, errs.ErrInternalServer - } - if utils.IsDevMode { - 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) + template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{}) + if err != nil { + return nil, errs.ErrInternalServer + } + if utils.IsDevMode { + 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) - if err != nil { - return nil, errs.ErrInternalServer - } - return &dto.VerifyEmailResp{Success: true}, nil + if err != nil { + return nil, errs.ErrInternalServer + } + return &dto.VerifyEmailResp{Success: true}, nil } func (s *UserService) ListOidcConfigs() ([]dto.UserOidcConfigDto, error) { - enabledOidcConfigs, err := repo.Oidc.ListOidcConfigs(true) - if err != nil { - return nil, errs.ErrInternalServer - } - var oidcConfigsDtos []dto.UserOidcConfigDto + enabledOidcConfigs, err := repo.Oidc.ListOidcConfigs(true) + if err != nil { + return nil, errs.ErrInternalServer + } + var oidcConfigsDtos []dto.UserOidcConfigDto - for _, oidcConfig := range enabledOidcConfigs { - state := utils.Strings.GenerateRandomString(32) - kvStore := utils.KV.GetInstance() - kvStore.Set(constant.KVKeyOidcState+state, oidcConfig.Name, 5*time.Minute) - loginUrl := utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{ - "client_id": oidcConfig.ClientID, - "redirect_uri": fmt.Sprintf("%s%s%s/%sREDIRECT_BACK", // 这个大占位符给前端替换用的,替换时也要uri编码因为是层层包的 - strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"), - constant.ApiSuffix, - constant.OidcUri, - oidcConfig.Name, - ), - "response_type": "code", - "scope": "openid email profile", - "state": state, - }) + for _, oidcConfig := range enabledOidcConfigs { + state := utils.Strings.GenerateRandomString(32) + kvStore := utils.KV.GetInstance() + kvStore.Set(constant.KVKeyOidcState+state, oidcConfig.Name, 5*time.Minute) + loginUrl := utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{ + "client_id": oidcConfig.ClientID, + "redirect_uri": fmt.Sprintf("%s%s%s/%sREDIRECT_BACK", // 这个大占位符给前端替换用的,替换时也要uri编码因为是层层包的 + strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"), + constant.ApiSuffix, + constant.OidcUri, + oidcConfig.Name, + ), + "response_type": "code", + "scope": "openid email profile", + "state": state, + }) - if oidcConfig.Type == constant.OidcProviderTypeMisskey { - // Misskey OIDC 特殊处理 - loginUrl = utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{ - "client_id": oidcConfig.ClientID, - "redirect_uri": fmt.Sprintf("%s%s%s/%s", // 这个大占位符给前端替换用的,替换时也要uri编码因为是层层包的 - strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"), - constant.ApiSuffix, - constant.OidcUri, - oidcConfig.Name, - ), - "response_type": "code", - "scope": "read:account", - "state": state, - }) - } + if oidcConfig.Type == constant.OidcProviderTypeMisskey { + // Misskey OIDC 特殊处理 + loginUrl = utils.Url.BuildUrl(oidcConfig.AuthorizationEndpoint, map[string]string{ + "client_id": oidcConfig.ClientID, + "redirect_uri": fmt.Sprintf("%s%s%s/%s", // 这个大占位符给前端替换用的,替换时也要uri编码因为是层层包的 + strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/"), + constant.ApiSuffix, + constant.OidcUri, + oidcConfig.Name, + ), + "response_type": "code", + "scope": "read:account", + "state": state, + }) + } - oidcConfigsDtos = append(oidcConfigsDtos, dto.UserOidcConfigDto{ - Name: oidcConfig.Name, - DisplayName: oidcConfig.DisplayName, - Icon: oidcConfig.Icon, - LoginUrl: loginUrl, - }) - } - return oidcConfigsDtos, nil + oidcConfigsDtos = append(oidcConfigsDtos, dto.UserOidcConfigDto{ + Name: oidcConfig.Name, + DisplayName: oidcConfig.DisplayName, + Icon: oidcConfig.Icon, + LoginUrl: loginUrl, + }) + } + return oidcConfigsDtos, nil } func (s *UserService) OidcLogin(req *dto.OidcLoginReq) (*dto.OidcLoginResp, error) { - // 验证state - kvStore := utils.KV.GetInstance() - storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State) - if !ok || storedName != req.Name { - return nil, errs.New(http.StatusForbidden, "invalid oidc state", nil) - } - // 获取OIDC配置 - oidcConfig, err := repo.Oidc.GetOidcConfigByName(req.Name) - if err != nil { - return nil, errs.ErrInternalServer - } - if oidcConfig == nil { - return nil, errs.New(http.StatusNotFound, "OIDC configuration not found", nil) - } - // 请求访问令牌 - tokenResp, err := utils.Oidc.RequestToken( - oidcConfig.TokenEndpoint, - oidcConfig.ClientID, - oidcConfig.ClientSecret, - req.Code, - strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/")+constant.OidcUri+oidcConfig.Name, - ) - if err != nil { - logrus.Errorln("Failed to request OIDC token:", err) - return nil, errs.ErrInternalServer - } - userInfo, err := utils.Oidc.RequestUserInfo(oidcConfig.UserInfoEndpoint, tokenResp.AccessToken) - if err != nil { - logrus.Errorln("Failed to request OIDC user info:", err) - return nil, errs.ErrInternalServer - } + // 验证state + kvStore := utils.KV.GetInstance() + storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State) + if !ok || storedName != req.Name { + return nil, errs.New(http.StatusForbidden, "invalid oidc state", nil) + } + // 获取OIDC配置 + oidcConfig, err := repo.Oidc.GetOidcConfigByName(req.Name) + if err != nil { + return nil, errs.ErrInternalServer + } + if oidcConfig == nil { + return nil, errs.New(http.StatusNotFound, "OIDC configuration not found", nil) + } + // 请求访问令牌 + tokenResp, err := utils.Oidc.RequestToken( + oidcConfig.TokenEndpoint, + oidcConfig.ClientID, + oidcConfig.ClientSecret, + req.Code, + strings.TrimSuffix(utils.Env.Get(constant.EnvKeyBaseUrl, constant.DefaultBaseUrl), "/")+constant.OidcUri+oidcConfig.Name, + ) + if err != nil { + logrus.Errorln("Failed to request OIDC token:", err) + return nil, errs.ErrInternalServer + } + userInfo, err := utils.Oidc.RequestUserInfo(oidcConfig.UserInfoEndpoint, tokenResp.AccessToken) + if err != nil { + logrus.Errorln("Failed to request OIDC user info:", err) + return nil, errs.ErrInternalServer + } - // 绑定过登录 - userOpenID, err := repo.User.GetUserOpenIDByIssuerAndSub(oidcConfig.Issuer, userInfo.Sub) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errs.ErrInternalServer - } - if userOpenID != nil { - user, err := repo.User.GetUserByID(userOpenID.UserID) - if err != nil { - return nil, errs.ErrInternalServer - } - token, refreshToken, err := s.generate2Token(user.ID) - if err != nil { - logrus.Errorln("Failed to generate tokens:", err) - return nil, errs.ErrInternalServer - } - resp := &dto.OidcLoginResp{ - Token: token, - RefreshToken: refreshToken, - User: user.ToDto(), - } - return resp, nil - } else { - // 若没有绑定过登录,则先通过邮箱查找用户,若没有再创建新用户 - user, err := repo.User.GetUserByEmail(userInfo.Email) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - logrus.Errorln("Failed to get user by email:", err) - return nil, errs.ErrInternalServer - } - if user != nil { - userOpenID = &model.UserOpenID{ - UserID: user.ID, - Issuer: oidcConfig.Issuer, - Sub: userInfo.Sub, - } - err = repo.User.CreateOrUpdateUserOpenID(userOpenID) - if err != nil { - logrus.Errorln("Failed to create or update user OpenID:", err) - return nil, errs.ErrInternalServer - } - token, refreshToken, err := s.generate2Token(user.ID) - if err != nil { - logrus.Errorln("Failed to generate tokens:", err) - return nil, errs.ErrInternalServer - } - resp := &dto.OidcLoginResp{ - Token: token, - RefreshToken: refreshToken, - User: user.ToDto(), - } - return resp, nil - } else { - user = &model.User{ - Username: userInfo.Name, - Nickname: userInfo.Name, - AvatarUrl: userInfo.Picture, - Email: userInfo.Email, - } - err = repo.User.CreateUser(user) - if err != nil { - logrus.Errorln("Failed to create user:", err) - return nil, errs.ErrInternalServer - } - userOpenID = &model.UserOpenID{ - UserID: user.ID, - Issuer: oidcConfig.Issuer, - Sub: userInfo.Sub, - } - err = repo.User.CreateOrUpdateUserOpenID(userOpenID) - if err != nil { - logrus.Errorln("Failed to create or update user OpenID:", err) - return nil, errs.ErrInternalServer - } - token, refreshToken, err := s.generate2Token(user.ID) - if err != nil { - logrus.Errorln("Failed to generate tokens:", err) - return nil, errs.ErrInternalServer - } - resp := &dto.OidcLoginResp{ - Token: token, - RefreshToken: refreshToken, - User: user.ToDto(), - } - return resp, nil - } - } + // 绑定过登录 + userOpenID, err := repo.User.GetUserOpenIDByIssuerAndSub(oidcConfig.Issuer, userInfo.Sub) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errs.ErrInternalServer + } + if userOpenID != nil { + user, err := repo.User.GetUserByID(userOpenID.UserID) + if err != nil { + return nil, errs.ErrInternalServer + } + token, refreshToken, err := s.generate2Token(user.ID) + if err != nil { + logrus.Errorln("Failed to generate tokens:", err) + return nil, errs.ErrInternalServer + } + resp := &dto.OidcLoginResp{ + Token: token, + RefreshToken: refreshToken, + User: user.ToDto(), + } + return resp, nil + } else { + // 若没有绑定过登录,则先通过邮箱查找用户,若没有再创建新用户 + user, err := repo.User.GetUserByEmail(userInfo.Email) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logrus.Errorln("Failed to get user by email:", err) + return nil, errs.ErrInternalServer + } + if user != nil { + userOpenID = &model.UserOpenID{ + UserID: user.ID, + Issuer: oidcConfig.Issuer, + Sub: userInfo.Sub, + } + err = repo.User.CreateOrUpdateUserOpenID(userOpenID) + if err != nil { + logrus.Errorln("Failed to create or update user OpenID:", err) + return nil, errs.ErrInternalServer + } + token, refreshToken, err := s.generate2Token(user.ID) + if err != nil { + logrus.Errorln("Failed to generate tokens:", err) + return nil, errs.ErrInternalServer + } + resp := &dto.OidcLoginResp{ + Token: token, + RefreshToken: refreshToken, + User: user.ToDto(), + } + return resp, nil + } else { + // 第一次登录,创建新用户时才获取头像 + user = &model.User{ + Username: userInfo.PreferredUsername, + Nickname: userInfo.Name, + AvatarUrl: userInfo.Picture, + Email: userInfo.Email, + } + err = repo.User.CreateUser(user) + if err != nil { + logrus.Errorln("Failed to create user:", err) + return nil, errs.ErrInternalServer + } + userOpenID = &model.UserOpenID{ + UserID: user.ID, + Issuer: oidcConfig.Issuer, + Sub: userInfo.Sub, + } + err = repo.User.CreateOrUpdateUserOpenID(userOpenID) + if err != nil { + logrus.Errorln("Failed to create or update user OpenID:", err) + return nil, errs.ErrInternalServer + } + token, refreshToken, err := s.generate2Token(user.ID) + if err != nil { + logrus.Errorln("Failed to generate tokens:", err) + return nil, errs.ErrInternalServer + } + resp := &dto.OidcLoginResp{ + Token: token, + RefreshToken: refreshToken, + User: user.ToDto(), + } + return resp, nil + } + } } func (s *UserService) GetUser(req *dto.GetUserReq) (*dto.GetUserResp, error) { - if req.UserID == 0 { - return nil, errs.New(http.StatusBadRequest, "user_id is required", nil) - } - user, err := repo.User.GetUserByID(req.UserID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errs.ErrNotFound - } - logrus.Errorln("Failed to get user by ID:", err) - return nil, errs.ErrInternalServer - } - if user == nil { - return nil, errs.ErrNotFound - } - return &dto.GetUserResp{ - User: user.ToDto(), - }, nil + if req.UserID == 0 { + return nil, errs.New(http.StatusBadRequest, "user_id is required", nil) + } + user, err := repo.User.GetUserByID(req.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errs.ErrNotFound + } + logrus.Errorln("Failed to get user by ID:", err) + return nil, errs.ErrInternalServer + } + if user == nil { + return nil, errs.ErrNotFound + } + return &dto.GetUserResp{ + User: user.ToDto(), + }, nil } func (s *UserService) GetUserByUsername(req *dto.GetUserByUsernameReq) (*dto.GetUserResp, error) { - if req.Username == "" { - return nil, errs.New(http.StatusBadRequest, "username is required", nil) - } - user, err := repo.User.GetUserByUsername(req.Username) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errs.ErrNotFound - } - logrus.Errorln("Failed to get user by username:", err) - return nil, errs.ErrInternalServer - } - if user == nil { - return nil, errs.ErrNotFound - } - return &dto.GetUserResp{ - User: user.ToDto(), - }, nil + if req.Username == "" { + return nil, errs.New(http.StatusBadRequest, "username is required", nil) + } + user, err := repo.User.GetUserByUsername(req.Username) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errs.ErrNotFound + } + logrus.Errorln("Failed to get user by username:", err) + return nil, errs.ErrInternalServer + } + if user == nil { + return nil, errs.ErrNotFound + } + return &dto.GetUserResp{ + User: user.ToDto(), + }, nil } func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, error) { - user := &model.User{ - Model: gorm.Model{ - ID: req.ID, - }, - Username: req.Username, - Nickname: req.Nickname, - Gender: req.Gender, - AvatarUrl: req.AvatarUrl, - } - err := repo.User.UpdateUser(user) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errs.ErrNotFound - } - logrus.Errorln("Failed to update user:", err) - return nil, errs.ErrInternalServer - } - return &dto.UpdateUserResp{}, nil + user := &model.User{ + Model: gorm.Model{ + ID: req.ID, + }, + Username: req.Username, + Nickname: req.Nickname, + Gender: req.Gender, + AvatarUrl: req.AvatarUrl, + } + err := repo.User.UpdateUser(user) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errs.ErrNotFound + } + logrus.Errorln("Failed to update user:", err) + return nil, errs.ErrInternalServer + } + return &dto.UpdateUserResp{}, nil } 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) - tokenString, err := token.ToString() - if err != nil { - return "", "", errs.ErrInternalServer - } - 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() - if err != nil { - return "", "", errs.ErrInternalServer - } - err = repo.Session.SaveSession(refreshToken.SessionKey) - if err != nil { - return "", "", errs.ErrInternalServer - } - return tokenString, refreshTokenString, nil + token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault))*time.Second) + tokenString, err := token.ToString() + if err != nil { + return "", "", errs.ErrInternalServer + } + 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() + if err != nil { + return "", "", errs.ErrInternalServer + } + err = repo.Session.SaveSession(refreshToken.SessionKey) + if err != nil { + return "", "", errs.ErrInternalServer + } + return tokenString, refreshTokenString, nil } func (s *UserService) verifyEmail(email, code string) (bool, error) { - kv := utils.KV.GetInstance() - verificationCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email) - if !ok || verificationCode != code { - return false, errs.New(http.StatusForbidden, "Invalid email verification code", nil) - } - return true, nil + kv := utils.KV.GetInstance() + verificationCode, ok := kv.Get(constant.KVKeyEmailVerificationCode + email) + if !ok || verificationCode != code { + return false, errs.New(http.StatusForbidden, "Invalid email verification code", nil) + } + return true, nil } diff --git a/pkg/utils/oidc.go b/pkg/utils/oidc.go index c571764..63af7ad 100644 --- a/pkg/utils/oidc.go +++ b/pkg/utils/oidc.go @@ -1,7 +1,7 @@ package utils import ( - "fmt" + "fmt" ) type oidcUtils struct{} @@ -10,59 +10,60 @@ var Oidc = oidcUtils{} // RequestToken 请求访问令牌 func (u *oidcUtils) RequestToken(tokenEndpoint, clientID, clientSecret, code, redirectURI string) (*TokenResponse, error) { - tokenResp, err := client.R(). - SetFormData(map[string]string{ - "grant_type": "authorization_code", - "client_id": clientID, - "client_secret": clientSecret, - "code": code, - "redirect_uri": redirectURI, - }). - SetHeader("Accept", "application/json"). - SetResult(&TokenResponse{}). - Post(tokenEndpoint) + tokenResp, err := client.R(). + SetFormData(map[string]string{ + "grant_type": "authorization_code", + "client_id": clientID, + "client_secret": clientSecret, + "code": code, + "redirect_uri": redirectURI, + }). + SetHeader("Accept", "application/json"). + SetResult(&TokenResponse{}). + Post(tokenEndpoint) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - if tokenResp.StatusCode() != 200 { - return nil, fmt.Errorf("状态码: %d,响应: %s", tokenResp.StatusCode(), tokenResp.String()) - } - return tokenResp.Result().(*TokenResponse), nil + if tokenResp.StatusCode() != 200 { + return nil, fmt.Errorf("状态码: %d,响应: %s", tokenResp.StatusCode(), tokenResp.String()) + } + return tokenResp.Result().(*TokenResponse), nil } // RequestUserInfo 请求用户信息 func (u *oidcUtils) RequestUserInfo(userInfoEndpoint, accessToken string) (*UserInfo, error) { - userInfoResp, err := client.R(). - SetHeader("Authorization", "Bearer "+accessToken). - SetHeader("Accept", "application/json"). - SetResult(&UserInfo{}). - Get(userInfoEndpoint) - if err != nil { - return nil, err - } + userInfoResp, err := client.R(). + SetHeader("Authorization", "Bearer "+accessToken). + SetHeader("Accept", "application/json"). + SetResult(&UserInfo{}). + Get(userInfoEndpoint) + if err != nil { + return nil, err + } - if userInfoResp.StatusCode() != 200 { - return nil, fmt.Errorf("状态码: %d,响应: %s", userInfoResp.StatusCode(), userInfoResp.String()) - } + if userInfoResp.StatusCode() != 200 { + return nil, fmt.Errorf("状态码: %d,响应: %s", userInfoResp.StatusCode(), userInfoResp.String()) + } - return userInfoResp.Result().(*UserInfo), nil + return userInfoResp.Result().(*UserInfo), nil } type TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - IDToken string `json:"id_token,omitempty"` - RefreshToken string `json:"refresh_token,omitempty"` + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + IDToken string `json:"id_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` } // UserInfo 定义用户信息结构 type UserInfo struct { - Sub string `json:"sub"` - Name string `json:"name"` - Email string `json:"email"` - Picture string `json:"picture,omitempty"` - Groups []string `json:"groups,omitempty"` // 可选字段,OIDC提供的用户组信息 + Sub string `json:"sub"` + Name string `json:"name"` + Email string `json:"email"` + Picture string `json:"picture,omitempty"` + PreferredUsername string `json:"preferred_username"` + Groups []string `json:"groups,omitempty"` // 可选字段,OIDC提供的用户组信息 } diff --git a/web/next.config.ts b/web/next.config.ts index 3a0d4e6..6b429b2 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -18,6 +18,12 @@ const nextConfig: NextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'pass.liteyuki.org', + port: '', + pathname: '/**', + }, ], }, async rewrites() { diff --git a/web/src/components/comment/comment-item.tsx b/web/src/components/comment/comment-item.tsx index 30b6e0f..85cb2a4 100644 --- a/web/src/components/comment/comment-item.tsx +++ b/web/src/components/comment/comment-item.tsx @@ -166,7 +166,7 @@ export function CommentItem(
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}
{formatDateTime({ dateTimeString: commentState.createdAt,