migrate from github
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package auth_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/auth"
|
||||
"github.com/XingfenD/yoresee_doc/internal/constant"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/media"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/config_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/service/invitation_service"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db *gorm.DB
|
||||
userRepo *user_repo.UserRepository
|
||||
}
|
||||
|
||||
func NewAuthService(repos *repository.Repositories) *AuthService {
|
||||
return &AuthService{
|
||||
db: repos.DB,
|
||||
userRepo: repos.User,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(ctx context.Context, userCreate *dto.UserCreate) error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
mode := config_service.ConfigSvc.GetSystemRegisterMode(ctx)
|
||||
if mode == constant.RegisterMode_Invite {
|
||||
if userCreate.InvitationCode == nil || *userCreate.InvitationCode == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "the system enable invitation mode, but the invitation code is empty")
|
||||
}
|
||||
err := invitation_service.InvitationSvc.ValidateAndUseWithTx(tx, *userCreate.InvitationCode)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] ValidateAndUseWithTx failed, invitation_code=%s, err=%+v", *userCreate.InvitationCode, err)
|
||||
_ = invitation_service.InvitationSvc.CreateInvitationRecord(&model.InvitationRecord{
|
||||
Code: *userCreate.InvitationCode,
|
||||
UsedBy: userCreate.Email,
|
||||
Status: "failed",
|
||||
UsedAt: time.Now(),
|
||||
})
|
||||
return status.GenErrWithCustomMsg(err, "invitation validation failed")
|
||||
}
|
||||
}
|
||||
|
||||
_, err := s.userRepo.GetByEmail(userCreate.Email).WithTx(tx).Exec()
|
||||
if err == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusUserAlreadyExists, "email already registered")
|
||||
}
|
||||
|
||||
hashedPwd, err := utils.HashPassword(userCreate.Password)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] HashPassword failed, email=%s, err=%+v", userCreate.Email, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "hash password failed")
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
ExternalID: utils.GenerateExternalID(utils.ExternalIDContextUser),
|
||||
Username: userCreate.Username,
|
||||
PasswordHash: hashedPwd,
|
||||
Email: userCreate.Email,
|
||||
Status: 1,
|
||||
InvitationCode: userCreate.InvitationCode,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] Create user failed, email=%s, err=%+v", userCreate.Email, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create user failed")
|
||||
}
|
||||
|
||||
if mode == constant.RegisterMode_Invite && userCreate.InvitationCode != nil && *userCreate.InvitationCode != "" {
|
||||
_ = invitation_service.InvitationSvc.CreateInvitationRecordWithTx(tx, &model.InvitationRecord{
|
||||
Code: *userCreate.InvitationCode,
|
||||
UsedByUserID: utils.Of(user.ID),
|
||||
UsedBy: user.Email,
|
||||
Status: "success",
|
||||
UsedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(email string, password string) (string, *dto.UserResponse, error) {
|
||||
user, err := s.userRepo.GetByEmail(email).Exec()
|
||||
if err != nil {
|
||||
return "", nil, status.StatusUserNotFound
|
||||
}
|
||||
if user.Status <= 0 {
|
||||
return "", nil, status.GenErrWithCustomMsg(status.StatusPermissionDenied, "user is banned")
|
||||
}
|
||||
|
||||
if !utils.CheckPassword(password, user.PasswordHash) {
|
||||
return "", nil, status.StatusInvalidPassword
|
||||
}
|
||||
|
||||
token, err := auth.GenerateToken(user.ExternalID, user.Username)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: AuthService] GenerateToken failed, user_external_id=%s, err=%+v", user.ExternalID, err)
|
||||
return "", nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "generate token failed")
|
||||
}
|
||||
|
||||
userResponse := dto.NewUserResponseFromModel(user)
|
||||
userResponse.Avatar = media.BuildAvatarURL(userResponse.ExternalID, userResponse.AvatarObjectKey, userResponse.AvatarVersion)
|
||||
|
||||
return token, userResponse, nil
|
||||
}
|
||||
|
||||
var AuthSvc *AuthService
|
||||
@@ -0,0 +1,71 @@
|
||||
package auth_service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
const (
|
||||
SideBarSceneHome = "home"
|
||||
SideBarSceneUserInfo = "user_info"
|
||||
SideBarSceneManage = "manage"
|
||||
)
|
||||
|
||||
var (
|
||||
homeTabs = []string{
|
||||
"home",
|
||||
"search",
|
||||
"documents",
|
||||
"knowledge-base",
|
||||
"templates",
|
||||
}
|
||||
userInfoTabs = []string{
|
||||
"home",
|
||||
"user-center",
|
||||
"user-notifications",
|
||||
"user-invite",
|
||||
"user-security",
|
||||
}
|
||||
manageTabsForAdmin = []string{
|
||||
"home",
|
||||
"manage-user",
|
||||
"manage-user-group",
|
||||
"manage-organization",
|
||||
"manage-invite",
|
||||
"manage-security",
|
||||
}
|
||||
manageTabsForNormalUser = []string{
|
||||
"home",
|
||||
}
|
||||
)
|
||||
|
||||
func (s *AuthService) QuerySideBarDisplay(scene string, isAdmin bool) ([]string, error) {
|
||||
if scene == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
switch scene {
|
||||
case SideBarSceneHome:
|
||||
return homeTabs, nil
|
||||
case SideBarSceneUserInfo:
|
||||
return userInfoTabs, nil
|
||||
case SideBarSceneManage:
|
||||
if isAdmin {
|
||||
return manageTabsForAdmin, nil
|
||||
}
|
||||
return manageTabsForNormalUser, nil
|
||||
default:
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid scene")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) IsAdmin(userExternalID string) (bool, error) {
|
||||
if userExternalID == "" {
|
||||
return false, status.StatusParamError
|
||||
}
|
||||
user, err := s.userRepo.GetByExternalID(userExternalID).Exec()
|
||||
if err != nil || user == nil {
|
||||
return false, status.StatusUserNotFound
|
||||
}
|
||||
return strings.EqualFold(user.Username, "admin"), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package auth_service
|
||||
|
||||
import "github.com/XingfenD/yoresee_doc/internal/status"
|
||||
|
||||
const (
|
||||
TopNavMenuUserCenter = "user-center"
|
||||
TopNavMenuSystemManage = "system-manage"
|
||||
)
|
||||
|
||||
var (
|
||||
topNavMenusForAdmin = []string{
|
||||
TopNavMenuUserCenter,
|
||||
TopNavMenuSystemManage,
|
||||
}
|
||||
topNavMenusForNormalUser = []string{
|
||||
TopNavMenuUserCenter,
|
||||
}
|
||||
)
|
||||
|
||||
func (s *AuthService) QueryTopNavDisplay(isAdmin bool) ([]string, error) {
|
||||
if isAdmin {
|
||||
return topNavMenusForAdmin, nil
|
||||
}
|
||||
if topNavMenusForNormalUser == nil {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
return topNavMenusForNormalUser, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package auth_service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/media"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
)
|
||||
|
||||
func (s *AuthService) UpdateProfile(userExternalID string, req *dto.UpdateProfileRequest) (*dto.UserResponse, error) {
|
||||
if strings.TrimSpace(userExternalID) == "" || req == nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "user_external_id and request are required")
|
||||
}
|
||||
if req.Username == nil && req.Email == nil && req.Nickname == nil && req.Password == nil && req.AvatarFile == nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "no profile fields to update")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByExternalID(userExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
|
||||
if req.Username != nil {
|
||||
username := strings.TrimSpace(*req.Username)
|
||||
if username == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "username cannot be empty")
|
||||
}
|
||||
user.Username = username
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
email := strings.TrimSpace(*req.Email)
|
||||
if email == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "email cannot be empty")
|
||||
}
|
||||
existingUser, getErr := s.userRepo.GetByEmail(email).Exec()
|
||||
if getErr == nil && existingUser.ID != user.ID {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserAlreadyExists, "email already registered")
|
||||
}
|
||||
user.Email = email
|
||||
}
|
||||
|
||||
if req.Nickname != nil {
|
||||
user.Nickname = strings.TrimSpace(*req.Nickname)
|
||||
}
|
||||
|
||||
if req.Password != nil {
|
||||
password := strings.TrimSpace(*req.Password)
|
||||
if password == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "password cannot be empty")
|
||||
}
|
||||
hashedPwd, hashErr := utils.HashPassword(password)
|
||||
if hashErr != nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "hash password failed")
|
||||
}
|
||||
user.PasswordHash = hashedPwd
|
||||
}
|
||||
|
||||
var oldAvatarObjectKey string
|
||||
if req.AvatarFile != nil {
|
||||
oldAvatarObjectKey = user.AvatarObjectKey
|
||||
avatarObjectKey, nextVersion, uploadErr := uploadAvatar(
|
||||
user.ExternalID,
|
||||
user.AvatarVersion+1,
|
||||
req.AvatarFile,
|
||||
req.AvatarFilename,
|
||||
req.AvatarContentType,
|
||||
)
|
||||
if uploadErr != nil {
|
||||
return nil, status.GenErrWithCustomMsg(uploadErr, "upload avatar failed")
|
||||
}
|
||||
now := time.Now()
|
||||
user.AvatarObjectKey = avatarObjectKey
|
||||
user.AvatarVersion = nextVersion
|
||||
user.AvatarUpdatedAt = &now
|
||||
user.Avatar = ""
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(user).Exec(); err != nil {
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
|
||||
if oldAvatarObjectKey != "" && oldAvatarObjectKey != user.AvatarObjectKey {
|
||||
_ = storage.DeleteFile(config.GlobalConfig.Minio.Bucket, oldAvatarObjectKey)
|
||||
}
|
||||
|
||||
resp := dto.NewUserResponseFromModel(user)
|
||||
resp.Avatar = media.BuildAvatarURL(resp.ExternalID, resp.AvatarObjectKey, resp.AvatarVersion)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func uploadAvatar(userExternalID string, avatarVersion int64, avatarFile []byte, filename, contentType *string) (string, int64, error) {
|
||||
if len(avatarFile) == 0 {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusParamError, "avatar file is empty")
|
||||
}
|
||||
if len(avatarFile) > media.MaxAvatarSize {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusParamError, "avatar file exceeds 5MB limit")
|
||||
}
|
||||
if storage.MinioClient == nil {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "minio client is not initialized")
|
||||
}
|
||||
|
||||
fileName := "avatar"
|
||||
if filename != nil {
|
||||
fileName = strings.TrimSpace(*filename)
|
||||
}
|
||||
|
||||
fileContentType := "application/octet-stream"
|
||||
if contentType != nil {
|
||||
fileContentType = strings.TrimSpace(*contentType)
|
||||
}
|
||||
fileContentType = media.NormalizeAvatarContentType(avatarFile, fileContentType)
|
||||
if !media.IsSupportedAvatarContentType(fileContentType) {
|
||||
return "", 0, status.GenErrWithCustomMsg(status.StatusParamError, "avatar content type is not supported")
|
||||
}
|
||||
|
||||
objectName := media.BuildAvatarObjectKey(userExternalID, avatarVersion, media.ResolveAvatarExt(fileName, fileContentType))
|
||||
|
||||
err := storage.PutFile(
|
||||
config.GlobalConfig.Minio.Bucket,
|
||||
objectName,
|
||||
bytes.NewReader(avatarFile),
|
||||
int64(len(avatarFile)),
|
||||
fileContentType,
|
||||
)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return objectName, avatarVersion, nil
|
||||
}
|
||||
Reference in New Issue
Block a user