mirror of
				https://github.com/usememos/memos.git
				synced 2025-10-25 13:56:27 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			512 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			512 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package v1
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/labstack/echo/v4"
 | |
| 	"github.com/pkg/errors"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| 
 | |
| 	"github.com/usememos/memos/common/util"
 | |
| 	"github.com/usememos/memos/server/service/metric"
 | |
| 	"github.com/usememos/memos/store"
 | |
| )
 | |
| 
 | |
| // Role is the type of a role.
 | |
| type Role string
 | |
| 
 | |
| const (
 | |
| 	// RoleHost is the HOST role.
 | |
| 	RoleHost Role = "HOST"
 | |
| 	// RoleAdmin is the ADMIN role.
 | |
| 	RoleAdmin Role = "ADMIN"
 | |
| 	// RoleUser is the USER role.
 | |
| 	RoleUser Role = "USER"
 | |
| )
 | |
| 
 | |
| func (role Role) String() string {
 | |
| 	return string(role)
 | |
| }
 | |
| 
 | |
| type User struct {
 | |
| 	ID int32 `json:"id"`
 | |
| 
 | |
| 	// Standard fields
 | |
| 	RowStatus RowStatus `json:"rowStatus"`
 | |
| 	CreatedTs int64     `json:"createdTs"`
 | |
| 	UpdatedTs int64     `json:"updatedTs"`
 | |
| 
 | |
| 	// Domain specific fields
 | |
| 	Username        string         `json:"username"`
 | |
| 	Role            Role           `json:"role"`
 | |
| 	Email           string         `json:"email"`
 | |
| 	Nickname        string         `json:"nickname"`
 | |
| 	PasswordHash    string         `json:"-"`
 | |
| 	AvatarURL       string         `json:"avatarUrl"`
 | |
| 	UserSettingList []*UserSetting `json:"userSettingList"`
 | |
| }
 | |
| 
 | |
| type CreateUserRequest struct {
 | |
| 	Username string `json:"username"`
 | |
| 	Role     Role   `json:"role"`
 | |
| 	Email    string `json:"email"`
 | |
| 	Nickname string `json:"nickname"`
 | |
| 	Password string `json:"password"`
 | |
| }
 | |
| 
 | |
| type UpdateUserRequest struct {
 | |
| 	RowStatus *RowStatus `json:"rowStatus"`
 | |
| 	Username  *string    `json:"username"`
 | |
| 	Email     *string    `json:"email"`
 | |
| 	Nickname  *string    `json:"nickname"`
 | |
| 	Password  *string    `json:"password"`
 | |
| 	AvatarURL *string    `json:"avatarUrl"`
 | |
| }
 | |
| 
 | |
| func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
 | |
| 	g.GET("/user", s.GetUserList)
 | |
| 	g.POST("/user", s.CreateUser)
 | |
| 	g.GET("/user/me", s.GetCurrentUser)
 | |
| 	// NOTE: This should be moved to /api/v2/user/:username
 | |
| 	g.GET("/user/name/:username", s.GetUserByUsername)
 | |
| 	g.GET("/user/:id", s.GetUserByID)
 | |
| 	g.PATCH("/user/:id", s.UpdateUser)
 | |
| 	g.DELETE("/user/:id", s.DeleteUser)
 | |
| }
 | |
| 
 | |
| // GetUserList godoc
 | |
| //
 | |
| //	@Summary	Get a list of users
 | |
| //	@Tags		user
 | |
| //	@Produce	json
 | |
| //	@Success	200	{object}	[]store.User	"User list"
 | |
| //	@Failure	500	{object}	nil				"Failed to fetch user list"
 | |
| //	@Router		/api/v1/user [GET]
 | |
| func (s *APIV1Service) GetUserList(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	list, err := s.Store.ListUsers(ctx, &store.FindUser{})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	userMessageList := make([]*User, 0, len(list))
 | |
| 	for _, user := range list {
 | |
| 		userMessage := convertUserFromStore(user)
 | |
| 		// data desensitize
 | |
| 		userMessage.Email = ""
 | |
| 		userMessageList = append(userMessageList, userMessage)
 | |
| 	}
 | |
| 	return c.JSON(http.StatusOK, userMessageList)
 | |
| }
 | |
| 
 | |
| // CreateUser godoc
 | |
| //
 | |
| //	@Summary	Create a user
 | |
| //	@Tags		user
 | |
| //	@Accept		json
 | |
| //	@Produce	json
 | |
| //	@Param		body	body		CreateUserRequest	true	"Request object"
 | |
| //	@Success	200		{object}	store.User			"Created user"
 | |
| //	@Failure	400		{object}	nil					"Malformatted post user request | Invalid user create format"
 | |
| //	@Failure	401		{object}	nil					"Missing auth session | Unauthorized to create user"
 | |
| //	@Failure	403		{object}	nil					"Could not create host user"
 | |
| //	@Failure	500		{object}	nil					"Failed to find user by id | Failed to generate password hash | Failed to create user | Failed to create activity"
 | |
| //	@Router		/api/v1/user [POST]
 | |
| func (s *APIV1Service) CreateUser(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	userID, ok := c.Get(userIDContextKey).(int32)
 | |
| 	if !ok {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
 | |
| 	}
 | |
| 	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
 | |
| 		ID: &userID,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
 | |
| 	}
 | |
| 	if currentUser == nil {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
 | |
| 	}
 | |
| 	if currentUser.Role != store.RoleHost {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to create user")
 | |
| 	}
 | |
| 
 | |
| 	userCreate := &CreateUserRequest{}
 | |
| 	if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
 | |
| 	}
 | |
| 	if err := userCreate.Validate(); err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
 | |
| 	}
 | |
| 	if !usernameMatcher.MatchString(strings.ToLower(userCreate.Username)) {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err)
 | |
| 	}
 | |
| 	// Disallow host user to be created.
 | |
| 	if userCreate.Role == RoleHost {
 | |
| 		return echo.NewHTTPError(http.StatusForbidden, "Could not create host user")
 | |
| 	}
 | |
| 
 | |
| 	passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	user, err := s.Store.CreateUser(ctx, &store.User{
 | |
| 		Username:     userCreate.Username,
 | |
| 		Role:         store.Role(userCreate.Role),
 | |
| 		Email:        userCreate.Email,
 | |
| 		Nickname:     userCreate.Nickname,
 | |
| 		PasswordHash: string(passwordHash),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	userMessage := convertUserFromStore(user)
 | |
| 	if err := s.createUserCreateActivity(c, userMessage); err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
 | |
| 	}
 | |
| 	metric.Enqueue("user create")
 | |
| 	return c.JSON(http.StatusOK, userMessage)
 | |
| }
 | |
| 
 | |
| // GetCurrentUser godoc
 | |
| //
 | |
| //	@Summary	Get current user
 | |
| //	@Tags		user
 | |
| //	@Produce	json
 | |
| //	@Success	200	{object}	store.User	"Current user"
 | |
| //	@Failure	401	{object}	nil			"Missing auth session"
 | |
| //	@Failure	500	{object}	nil			"Failed to find user | Failed to find userSettingList"
 | |
| //	@Router		/api/v1/user/me [GET]
 | |
| func (s *APIV1Service) GetCurrentUser(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	userID, ok := c.Get(userIDContextKey).(int32)
 | |
| 	if !ok {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
 | |
| 	}
 | |
| 
 | |
| 	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
 | |
| 	}
 | |
| 	if user == nil {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
 | |
| 	}
 | |
| 
 | |
| 	list, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{
 | |
| 		UserID: &userID,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
 | |
| 	}
 | |
| 	userSettingList := []*UserSetting{}
 | |
| 	for _, userSetting := range list {
 | |
| 		userSettingList = append(userSettingList, convertUserSettingFromStore(userSetting))
 | |
| 	}
 | |
| 	userMessage := convertUserFromStore(user)
 | |
| 	userMessage.UserSettingList = userSettingList
 | |
| 	return c.JSON(http.StatusOK, userMessage)
 | |
| }
 | |
| 
 | |
| // GetUserByUsername godoc
 | |
| //
 | |
| //	@Summary	Get user by username
 | |
| //	@Tags		user
 | |
| //	@Produce	json
 | |
| //	@Param		username	path		string		true	"Username"
 | |
| //	@Success	200			{object}	store.User	"Requested user"
 | |
| //	@Failure	404			{object}	nil			"User not found"
 | |
| //	@Failure	500			{object}	nil			"Failed to find user"
 | |
| //	@Router		/api/v1/user/name/{username} [GET]
 | |
| func (s *APIV1Service) GetUserByUsername(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	username := c.Param("username")
 | |
| 	user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
 | |
| 	}
 | |
| 	if user == nil {
 | |
| 		return echo.NewHTTPError(http.StatusNotFound, "User not found")
 | |
| 	}
 | |
| 
 | |
| 	userMessage := convertUserFromStore(user)
 | |
| 	// data desensitize
 | |
| 	userMessage.Email = ""
 | |
| 	return c.JSON(http.StatusOK, userMessage)
 | |
| }
 | |
| 
 | |
| // GetUserByID godoc
 | |
| //
 | |
| //	@Summary	Get user by id
 | |
| //	@Tags		user
 | |
| //	@Produce	json
 | |
| //	@Param		id	path		int			true	"User ID"
 | |
| //	@Success	200	{object}	store.User	"Requested user"
 | |
| //	@Failure	400	{object}	nil			"Malformatted user id"
 | |
| //	@Failure	404	{object}	nil			"User not found"
 | |
| //	@Failure	500	{object}	nil			"Failed to find user"
 | |
| //	@Router		/api/v1/user/{id} [GET]
 | |
| func (s *APIV1Service) GetUserByID(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	id, err := util.ConvertStringToInt32(c.Param("id"))
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &id})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
 | |
| 	}
 | |
| 	if user == nil {
 | |
| 		return echo.NewHTTPError(http.StatusNotFound, "User not found")
 | |
| 	}
 | |
| 
 | |
| 	userMessage := convertUserFromStore(user)
 | |
| 	// data desensitize
 | |
| 	userMessage.Email = ""
 | |
| 	return c.JSON(http.StatusOK, userMessage)
 | |
| }
 | |
| 
 | |
| // DeleteUser godoc
 | |
| //
 | |
| //	@Summary	Delete a user
 | |
| //	@Tags		user
 | |
| //	@Produce	json
 | |
| //	@Param		id	path		string	true	"User ID"
 | |
| //	@Success	200	{boolean}	true	"User deleted"
 | |
| //	@Failure	400	{object}	nil		"ID is not a number: %s | Current session user not found with ID: %d"
 | |
| //	@Failure	401	{object}	nil		"Missing user in session"
 | |
| //	@Failure	403	{object}	nil		"Unauthorized to delete user"
 | |
| //	@Failure	500	{object}	nil		"Failed to find user | Failed to delete user"
 | |
| //	@Router		/api/v1/user/{id} [DELETE]
 | |
| func (s *APIV1Service) DeleteUser(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	currentUserID, ok := c.Get(userIDContextKey).(int32)
 | |
| 	if !ok {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
 | |
| 	}
 | |
| 	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
 | |
| 		ID: ¤tUserID,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
 | |
| 	}
 | |
| 	if currentUser == nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
 | |
| 	} else if currentUser.Role != store.RoleHost {
 | |
| 		return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete user").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	userID, err := util.ConvertStringToInt32(c.Param("id"))
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	userDelete := &store.DeleteUser{
 | |
| 		ID: userID,
 | |
| 	}
 | |
| 	if err := s.Store.DeleteUser(ctx, userDelete); err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
 | |
| 	}
 | |
| 	return c.JSON(http.StatusOK, true)
 | |
| }
 | |
| 
 | |
| // UpdateUser godoc
 | |
| //
 | |
| //	@Summary	Update a user
 | |
| //	@Tags		user
 | |
| //	@Produce	json
 | |
| //	@Param		id		path		string				true	"User ID"
 | |
| //	@Param		patch	body		UpdateUserRequest	true	"Patch request"
 | |
| //	@Success	200		{object}	store.User			"Updated user"
 | |
| //	@Failure	400		{object}	nil					"ID is not a number: %s | Current session user not found with ID: %d | Malformatted patch user request | Invalid update user request"
 | |
| //	@Failure	401		{object}	nil					"Missing user in session"
 | |
| //	@Failure	403		{object}	nil					"Unauthorized to update user"
 | |
| //	@Failure	500		{object}	nil					"Failed to find user | Failed to generate password hash | Failed to patch user | Failed to find userSettingList"
 | |
| //	@Router		/api/v1/user/{id} [PATCH]
 | |
| func (s *APIV1Service) UpdateUser(c echo.Context) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	userID, err := util.ConvertStringToInt32(c.Param("id"))
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	currentUserID, ok := c.Get(userIDContextKey).(int32)
 | |
| 	if !ok {
 | |
| 		return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
 | |
| 	}
 | |
| 	currentUser, err := s.Store.GetUser(ctx, &store.FindUser{ID: ¤tUserID})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
 | |
| 	}
 | |
| 	if currentUser == nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
 | |
| 	} else if currentUser.Role != store.RoleHost && currentUserID != userID {
 | |
| 		return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to update user").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	request := &UpdateUserRequest{}
 | |
| 	if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
 | |
| 	}
 | |
| 	if err := request.Validate(); err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid update user request").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	currentTs := time.Now().Unix()
 | |
| 	userUpdate := &store.UpdateUser{
 | |
| 		ID:        userID,
 | |
| 		UpdatedTs: ¤tTs,
 | |
| 	}
 | |
| 	if request.RowStatus != nil {
 | |
| 		rowStatus := store.RowStatus(request.RowStatus.String())
 | |
| 		userUpdate.RowStatus = &rowStatus
 | |
| 	}
 | |
| 	if request.Username != nil {
 | |
| 		if !usernameMatcher.MatchString(strings.ToLower(*request.Username)) {
 | |
| 			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err)
 | |
| 		}
 | |
| 		userUpdate.Username = request.Username
 | |
| 	}
 | |
| 	if request.Email != nil {
 | |
| 		userUpdate.Email = request.Email
 | |
| 	}
 | |
| 	if request.Nickname != nil {
 | |
| 		userUpdate.Nickname = request.Nickname
 | |
| 	}
 | |
| 	if request.Password != nil {
 | |
| 		passwordHash, err := bcrypt.GenerateFromPassword([]byte(*request.Password), bcrypt.DefaultCost)
 | |
| 		if err != nil {
 | |
| 			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
 | |
| 		}
 | |
| 
 | |
| 		passwordHashStr := string(passwordHash)
 | |
| 		userUpdate.PasswordHash = &passwordHashStr
 | |
| 	}
 | |
| 	if request.AvatarURL != nil {
 | |
| 		userUpdate.AvatarURL = request.AvatarURL
 | |
| 	}
 | |
| 
 | |
| 	user, err := s.Store.UpdateUser(ctx, userUpdate)
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
 | |
| 	}
 | |
| 
 | |
| 	list, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{
 | |
| 		UserID: &userID,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
 | |
| 	}
 | |
| 	userSettingList := []*UserSetting{}
 | |
| 	for _, userSetting := range list {
 | |
| 		userSettingList = append(userSettingList, convertUserSettingFromStore(userSetting))
 | |
| 	}
 | |
| 	userMessage := convertUserFromStore(user)
 | |
| 	userMessage.UserSettingList = userSettingList
 | |
| 	return c.JSON(http.StatusOK, userMessage)
 | |
| }
 | |
| 
 | |
| func (create CreateUserRequest) Validate() error {
 | |
| 	if len(create.Username) < 3 {
 | |
| 		return errors.New("username is too short, minimum length is 3")
 | |
| 	}
 | |
| 	if len(create.Username) > 32 {
 | |
| 		return errors.New("username is too long, maximum length is 32")
 | |
| 	}
 | |
| 	if len(create.Password) < 3 {
 | |
| 		return errors.New("password is too short, minimum length is 3")
 | |
| 	}
 | |
| 	if len(create.Password) > 512 {
 | |
| 		return errors.New("password is too long, maximum length is 512")
 | |
| 	}
 | |
| 	if len(create.Nickname) > 64 {
 | |
| 		return errors.New("nickname is too long, maximum length is 64")
 | |
| 	}
 | |
| 	if create.Email != "" {
 | |
| 		if len(create.Email) > 256 {
 | |
| 			return errors.New("email is too long, maximum length is 256")
 | |
| 		}
 | |
| 		if !util.ValidateEmail(create.Email) {
 | |
| 			return errors.New("invalid email format")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (update UpdateUserRequest) Validate() error {
 | |
| 	if update.Username != nil && len(*update.Username) < 3 {
 | |
| 		return errors.New("username is too short, minimum length is 3")
 | |
| 	}
 | |
| 	if update.Username != nil && len(*update.Username) > 32 {
 | |
| 		return errors.New("username is too long, maximum length is 32")
 | |
| 	}
 | |
| 	if update.Password != nil && len(*update.Password) < 3 {
 | |
| 		return errors.New("password is too short, minimum length is 3")
 | |
| 	}
 | |
| 	if update.Password != nil && len(*update.Password) > 512 {
 | |
| 		return errors.New("password is too long, maximum length is 512")
 | |
| 	}
 | |
| 	if update.Nickname != nil && len(*update.Nickname) > 64 {
 | |
| 		return errors.New("nickname is too long, maximum length is 64")
 | |
| 	}
 | |
| 	if update.AvatarURL != nil {
 | |
| 		if len(*update.AvatarURL) > 2<<20 {
 | |
| 			return errors.New("avatar is too large, maximum is 2MB")
 | |
| 		}
 | |
| 	}
 | |
| 	if update.Email != nil && *update.Email != "" {
 | |
| 		if len(*update.Email) > 256 {
 | |
| 			return errors.New("email is too long, maximum length is 256")
 | |
| 		}
 | |
| 		if !util.ValidateEmail(*update.Email) {
 | |
| 			return errors.New("invalid email format")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *APIV1Service) createUserCreateActivity(c echo.Context, user *User) error {
 | |
| 	ctx := c.Request().Context()
 | |
| 	payload := ActivityUserCreatePayload{
 | |
| 		UserID:   user.ID,
 | |
| 		Username: user.Username,
 | |
| 		Role:     user.Role,
 | |
| 	}
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return errors.Wrap(err, "failed to marshal activity payload")
 | |
| 	}
 | |
| 	activity, err := s.Store.CreateActivity(ctx, &store.Activity{
 | |
| 		CreatorID: user.ID,
 | |
| 		Type:      ActivityUserCreate.String(),
 | |
| 		Level:     ActivityInfo.String(),
 | |
| 		Payload:   string(payloadBytes),
 | |
| 	})
 | |
| 	if err != nil || activity == nil {
 | |
| 		return errors.Wrap(err, "failed to create activity")
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func convertUserFromStore(user *store.User) *User {
 | |
| 	return &User{
 | |
| 		ID:           user.ID,
 | |
| 		RowStatus:    RowStatus(user.RowStatus),
 | |
| 		CreatedTs:    user.CreatedTs,
 | |
| 		UpdatedTs:    user.UpdatedTs,
 | |
| 		Username:     user.Username,
 | |
| 		Role:         Role(user.Role),
 | |
| 		Email:        user.Email,
 | |
| 		Nickname:     user.Nickname,
 | |
| 		PasswordHash: user.PasswordHash,
 | |
| 		AvatarURL:    user.AvatarURL,
 | |
| 	}
 | |
| }
 |