diff --git a/api/v1/auth.go b/api/v1/auth.go index f8ede98..bbbb710 100644 --- a/api/v1/auth.go +++ b/api/v1/auth.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" - "github.com/boojack/shortify/api" "github.com/boojack/shortify/server/auth" "github.com/boojack/shortify/store" "github.com/labstack/echo/v4" @@ -38,7 +37,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err) } - user, err := s.Store.GetUserV1(ctx, &store.FindUser{ + user, err := s.Store.GetUser(ctx, &store.FindUser{ Username: &signin.Username, }) if err != nil { @@ -79,7 +78,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { } user.PasswordHash = string(passwordHash) - existingUsers, err := s.Store.FindUserList(ctx, &api.UserFind{}) + existingUsers, err := s.Store.ListUsers(ctx, &store.FindUser{}) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find existing users").SetInternal(err) } @@ -90,7 +89,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { user.Role = store.RoleUser } - user, err = s.Store.CreateUserV1(ctx, user) + user, err = s.Store.CreateUser(ctx, user) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err) } diff --git a/api/v1/user.go b/api/v1/user.go index 9a2fafb..bcc832f 100644 --- a/api/v1/user.go +++ b/api/v1/user.go @@ -1,12 +1,15 @@ package v1 import ( + "encoding/json" "fmt" "net/http" "net/mail" + "strconv" "github.com/boojack/shortify/store" "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" ) // Role is the type of a role. @@ -38,46 +41,42 @@ type User struct { RowStatus RowStatus `json:"rowStatus"` // Domain specific fields - Email string `json:"email"` - DisplayName string `json:"displayName"` - PasswordHash string `json:"-"` - Role Role `json:"role"` - UserSettingList []*UserSetting `json:"userSettingList"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Email string `json:"email"` + Role Role `json:"role"` } -type UserCreate struct { - Email string `json:"email"` - DisplayName string `json:"displayName"` - Password string `json:"password"` - PasswordHash string `json:"-"` - Role Role `json:"-"` +type CreateUserRequest struct { + Username string `json:"username"` + Nickname string `json:"nickname"` + Email string `json:"email"` + Password string `json:"password"` + Role Role `json:"-"` } -func (create UserCreate) Validate() error { - if len(create.Email) < 3 { - return fmt.Errorf("email is too short, minimum length is 6") +func (create CreateUserRequest) Validate() error { + if len(create.Username) < 3 { + return fmt.Errorf("username is too short, minimum length is 3") } - if !validateEmail(create.Email) { + if create.Nickname != "" && len(create.Nickname) < 3 { + return fmt.Errorf("username is too short, minimum length is 3") + } + if create.Email != "" && !validateEmail(create.Email) { return fmt.Errorf("invalid email format") } if len(create.Password) < 3 { - return fmt.Errorf("password is too short, minimum length is 6") + return fmt.Errorf("password is too short, minimum length is 3") } return nil } -type UserPatch struct { - ID int - - // Standard fields - RowStatus *RowStatus `json:"rowStatus"` - - // Domain specific fields - Email *string `json:"email"` - DisplayName *string `json:"displayName"` - Password *string `json:"password"` - PasswordHash *string `json:"-"` +type PatchUserRequest struct { + RowStatus *RowStatus `json:"rowStatus"` + Email *string `json:"email"` + DisplayName *string `json:"displayName"` + Password *string `json:"password"` } type UserDelete struct { @@ -86,7 +85,13 @@ type UserDelete struct { func (s *APIV1Service) registerUserRoutes(g *echo.Group) { g.GET("/user", func(c echo.Context) error { - return c.String(200, "GET /user") + ctx := c.Request().Context() + userList, err := s.Store.ListUsers(ctx, &store.FindUser{}) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user list").SetInternal(err) + } + + return c.JSON(http.StatusOK, userList) }) // GET /api/user/me is used to check if the user is logged in. @@ -97,7 +102,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") } - user, err := s.Store.GetUserV1(ctx, &store.FindUser{ + user, err := s.Store.GetUser(ctx, &store.FindUser{ ID: &userID, }) if err != nil { @@ -106,6 +111,101 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return c.JSON(http.StatusOK, user) }) + + g.GET("/user/:id", func(c echo.Context) error { + ctx := c.Request().Context() + userID, err := strconv.Atoi(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: &userID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) + } + + return c.JSON(http.StatusOK, user) + }) + + g.PATCH("/user/:id", func(c echo.Context) error { + ctx := c.Request().Context() + userID, err := strconv.Atoi(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(getUserIDContextKey()).(int) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") + } + if currentUserID != userID { + return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err) + } + + userPatch := &PatchUserRequest{} + if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err) + } + + updateUser := &store.UpdateUser{ + ID: currentUserID, + } + + if userPatch.Email != nil && !validateEmail(*userPatch.Email) { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format") + } + + if userPatch.Password != nil && *userPatch.Password != "" { + passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err) + } + + passwordHashStr := string(passwordHash) + updateUser.PasswordHash = &passwordHashStr + } + + user, err := s.Store.UpdateUser(ctx, updateUser) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to update user").SetInternal(err) + } + + return c.JSON(http.StatusOK, user) + }) + + g.DELETE("/user/:id", func(c echo.Context) error { + ctx := c.Request().Context() + currentUserID, ok := c.Get(getUserIDContextKey()).(int) + 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) + } + if currentUser.Role != store.RoleAdmin { + return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err) + } + + userID, err := strconv.Atoi(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err) + } + + if err := s.Store.DeleteUser(ctx, &store.DeleteUser{ + ID: userID, + }); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err) + } + + return c.JSON(http.StatusOK, true) + }) } // validateEmail validates the email. diff --git a/server/jwt.go b/server/jwt.go index ac816a3..8f33798 100644 --- a/server/jwt.go +++ b/server/jwt.go @@ -131,7 +131,7 @@ func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.Ha } // Even if there is no error, we still need to make sure the user still exists. - user, err := server.Store.GetUserV1(ctx, &store.FindUser{ + user, err := server.Store.GetUser(ctx, &store.FindUser{ ID: &userID, }) if err != nil { diff --git a/server/server.go b/server/server.go index 4acac71..bc777f2 100644 --- a/server/server.go +++ b/server/server.go @@ -65,7 +65,6 @@ func NewServer(profile *profile.Profile, store *store.Store) (*Server, error) { return JWTMiddleware(s, next, string(secret)) }) s.registerSystemRoutes(apiGroup) - s.registerUserRoutes(apiGroup) s.registerWorkspaceRoutes(apiGroup) s.registerWorkspaceUserRoutes(apiGroup) s.registerShortcutRoutes(apiGroup) diff --git a/server/user.go b/server/user.go deleted file mode 100644 index 3494c36..0000000 --- a/server/user.go +++ /dev/null @@ -1,162 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "net/mail" - "strconv" - - "github.com/boojack/shortify/api" - "github.com/google/uuid" - - "github.com/labstack/echo/v4" - "golang.org/x/crypto/bcrypt" -) - -func (s *Server) registerUserRoutes(g *echo.Group) { - g.GET("/user", func(c echo.Context) error { - ctx := c.Request().Context() - userList, err := s.Store.FindUserList(ctx, &api.UserFind{}) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(userList)) - }) - - // GET /api/user/me is used to check if the user is logged in. - g.GET("/user/me", func(c echo.Context) error { - ctx := c.Request().Context() - userID, ok := c.Get(getUserIDContextKey()).(int) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") - } - - userFind := &api.UserFind{ - ID: &userID, - } - user, err := s.Store.FindUser(ctx, userFind) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(user)) - }) - - g.GET("/user/:id", func(c echo.Context) error { - ctx := c.Request().Context() - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err) - } - - user, err := s.Store.FindUser(ctx, &api.UserFind{ - ID: &id, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(user)) - }) - - g.PATCH("/user/:id", func(c echo.Context) error { - ctx := c.Request().Context() - userID, err := strconv.Atoi(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(getUserIDContextKey()).(int) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") - } - currentUser, err := s.Store.FindUser(ctx, &api.UserFind{ - 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 currentUserID != userID { - return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err) - } - - userPatch := &api.UserPatch{ - ID: userID, - } - if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err) - } - - if userPatch.Email != nil && !validateEmail(*userPatch.Email) { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format") - } - - if userPatch.Password != nil && *userPatch.Password != "" { - passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err) - } - - passwordHashStr := string(passwordHash) - userPatch.PasswordHash = &passwordHashStr - } - - if userPatch.ResetOpenID != nil && *userPatch.ResetOpenID { - uuid := genUUID() - userPatch.OpenID = &uuid - } - - user, err := s.Store.PatchUser(ctx, userPatch) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(user)) - }) - - g.DELETE("/user/:id", func(c echo.Context) error { - ctx := c.Request().Context() - currentUserID, ok := c.Get(getUserIDContextKey()).(int) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") - } - currentUser, err := s.Store.FindUser(ctx, &api.UserFind{ - 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) - } - - userID, err := strconv.Atoi(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 := &api.UserDelete{ - 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) - }) -} - -// validateEmail validates the email. -func validateEmail(email string) bool { - if _, err := mail.ParseAddress(email); err != nil { - return false - } - return true -} - -func genUUID() string { - return uuid.New().String() -} diff --git a/store/shortcut.go b/store/shortcut.go index f8ceb5c..c1715e8 100644 --- a/store/shortcut.go +++ b/store/shortcut.go @@ -343,16 +343,8 @@ func (raw *shortcutRaw) toShortcut() *api.Shortcut { } } -func (s *Store) ComposeShortcut(ctx context.Context, shortcut *api.Shortcut) error { - user, err := s.FindUser(ctx, &api.UserFind{ - ID: &shortcut.CreatorID, - }) - if err != nil { - return err - } - user.OpenID = "" - shortcut.Creator = user - +func (*Store) ComposeShortcut(_ context.Context, _ *api.Shortcut) error { + // TODO: implement this. return nil } diff --git a/store/user.go b/store/user.go index bd7a4a0..8a16932 100644 --- a/store/user.go +++ b/store/user.go @@ -5,9 +5,6 @@ import ( "database/sql" "fmt" "strings" - - "github.com/boojack/shortify/api" - "github.com/boojack/shortify/internal/errorutil" ) type User struct { @@ -50,7 +47,7 @@ type DeleteUser struct { ID int } -func (s *Store) CreateUserV1(ctx context.Context, create *User) (*User, error) { +func (s *Store) CreateUser(ctx context.Context, create *User) (*User, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -88,10 +85,11 @@ func (s *Store) CreateUserV1(ctx context.Context, create *User) (*User, error) { } user := create + s.userCache.Store(user.ID, user) return user, nil } -func (s *Store) UpdateUserV1(ctx context.Context, update *UpdateUser) (*User, error) { +func (s *Store) UpdateUser(ctx context.Context, update *UpdateUser) (*User, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -148,6 +146,7 @@ func (s *Store) UpdateUserV1(ctx context.Context, update *UpdateUser) (*User, er return nil, err } + s.userCache.Store(user.ID, user) return user, nil } @@ -163,10 +162,20 @@ func (s *Store) ListUsers(ctx context.Context, find *FindUser) ([]*User, error) return nil, err } + for _, user := range list { + s.userCache.Store(user.ID, user) + } + return list, nil } -func (s *Store) GetUserV1(ctx context.Context, find *FindUser) (*User, error) { +func (s *Store) GetUser(ctx context.Context, find *FindUser) (*User, error) { + if find.ID != nil { + if cache, ok := s.workspaceCache.Load(*find.ID); ok { + return cache.(*User), nil + } + } + tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -185,7 +194,7 @@ func (s *Store) GetUserV1(ctx context.Context, find *FindUser) (*User, error) { return list[0], nil } -func (s *Store) DeleteUserV1(ctx context.Context, delete *DeleteUser) error { +func (s *Store) DeleteUser(ctx context.Context, delete *DeleteUser) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err @@ -203,6 +212,8 @@ func (s *Store) DeleteUserV1(ctx context.Context, delete *DeleteUser) error { return err } + s.userCache.Delete(delete.ID) + return nil } @@ -274,324 +285,3 @@ func listUsers(ctx context.Context, tx *sql.Tx, find *FindUser) ([]*User, error) return list, nil } - -// userRaw is the store model for an User. -// Fields have exactly the same meanings as User. -type userRaw struct { - ID int - - // Standard fields - CreatedTs int64 - UpdatedTs int64 - RowStatus api.RowStatus - - // Domain specific fields - Email string - DisplayName string - PasswordHash string - OpenID string - Role api.Role -} - -func (raw *userRaw) toUser() *api.User { - return &api.User{ - ID: raw.ID, - - CreatedTs: raw.CreatedTs, - UpdatedTs: raw.UpdatedTs, - RowStatus: raw.RowStatus, - - Email: raw.Email, - DisplayName: raw.DisplayName, - PasswordHash: raw.PasswordHash, - OpenID: raw.OpenID, - Role: raw.Role, - } -} - -func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.User, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - userRaw, err := createUser(ctx, tx, create) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - s.userCache.Store(userRaw.ID, userRaw) - user := userRaw.toUser() - return user, nil -} - -func (s *Store) PatchUser(ctx context.Context, patch *api.UserPatch) (*api.User, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - userRaw, err := patchUser(ctx, tx, patch) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - s.userCache.Store(userRaw.ID, userRaw) - user := userRaw.toUser() - return user, nil -} - -func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.User, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - userRawList, err := findUserList(ctx, tx, find) - if err != nil { - return nil, err - } - - list := []*api.User{} - for _, userRaw := range userRawList { - s.userCache.Store(userRaw.ID, userRaw) - list = append(list, userRaw.toUser()) - } - - return list, nil -} - -func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, error) { - if find.ID != nil { - if cache, ok := s.userCache.Load(*find.ID); ok { - return cache.(*userRaw).toUser(), nil - } - } - - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - list, err := findUserList(ctx, tx, find) - if err != nil { - return nil, err - } - - if len(list) == 0 { - return nil, &errorutil.Error{Code: errorutil.NotFound, Err: fmt.Errorf("not found user with filter %+v", find)} - } else if len(list) > 1 { - return nil, &errorutil.Error{Code: errorutil.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1", len(list), find)} - } - - userRaw := list[0] - s.userCache.Store(userRaw.ID, userRaw) - user := userRaw.toUser() - return user, nil -} - -func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer tx.Rollback() - - err = deleteUser(ctx, tx, delete) - if err != nil { - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - s.userCache.Delete(delete.ID) - return nil -} - -func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) { - query := ` - INSERT INTO user ( - email, - display_name, - password_hash, - open_id, - role - ) - VALUES (?, ?, ?, ?) - RETURNING id, created_ts, updated_ts, row_status, email, display_name, password_hash, open_id, role - ` - var userRaw userRaw - if err := tx.QueryRowContext(ctx, query, - create.Email, - create.DisplayName, - create.PasswordHash, - create.OpenID, - create.Role, - ).Scan( - &userRaw.ID, - &userRaw.CreatedTs, - &userRaw.UpdatedTs, - &userRaw.RowStatus, - &userRaw.Email, - &userRaw.DisplayName, - &userRaw.PasswordHash, - &userRaw.OpenID, - &userRaw.Role, - ); err != nil { - return nil, err - } - - return &userRaw, nil -} - -func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw, error) { - set, args := []string{}, []any{} - - if v := patch.RowStatus; v != nil { - set, args = append(set, "row_status = ?"), append(args, *v) - } - if v := patch.Email; v != nil { - set, args = append(set, "email = ?"), append(args, *v) - } - if v := patch.DisplayName; v != nil { - set, args = append(set, "display_name = ?"), append(args, *v) - } - if v := patch.PasswordHash; v != nil { - set, args = append(set, "password_hash = ?"), append(args, *v) - } - if v := patch.OpenID; v != nil { - set, args = append(set, "open_id = ?"), append(args, *v) - } - - args = append(args, patch.ID) - - query := ` - UPDATE user - SET ` + strings.Join(set, ", ") + ` - WHERE id = ? - RETURNING id, created_ts, updated_ts, row_status, email, display_name, password_hash, open_id, role - ` - row, err := tx.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer row.Close() - - if row.Next() { - var userRaw userRaw - if err := row.Scan( - &userRaw.ID, - &userRaw.CreatedTs, - &userRaw.UpdatedTs, - &userRaw.RowStatus, - &userRaw.Email, - &userRaw.DisplayName, - &userRaw.PasswordHash, - &userRaw.OpenID, - &userRaw.Role, - ); err != nil { - return nil, err - } - - if err := row.Err(); err != nil { - return nil, err - } - - return &userRaw, nil - } - - return nil, &errorutil.Error{Code: errorutil.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)} -} - -func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) { - where, args := []string{"1 = 1"}, []any{} - - if v := find.ID; v != nil { - where, args = append(where, "id = ?"), append(args, *v) - } - if v := find.Email; v != nil { - where, args = append(where, "email = ?"), append(args, *v) - } - if v := find.OpenID; v != nil { - where, args = append(where, "open_id = ?"), append(args, *v) - } - if v := find.Role; v != nil { - where, args = append(where, "role = ?"), append(args, *v) - } - - query := ` - SELECT - id, - created_ts, - updated_ts, - row_status, - email, - display_name, - password_hash, - open_id, - role - FROM user - WHERE ` + strings.Join(where, " AND ") + ` - ORDER BY updated_ts DESC, created_ts DESC - ` - rows, err := tx.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - userRawList := make([]*userRaw, 0) - for rows.Next() { - var userRaw userRaw - if err := rows.Scan( - &userRaw.ID, - &userRaw.CreatedTs, - &userRaw.UpdatedTs, - &userRaw.RowStatus, - &userRaw.Email, - &userRaw.DisplayName, - &userRaw.PasswordHash, - &userRaw.OpenID, - &userRaw.Role, - ); err != nil { - return nil, err - } - userRawList = append(userRawList, &userRaw) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return userRawList, nil -} - -func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error { - result, err := tx.ExecContext(ctx, ` - DELETE FROM user WHERE id = ? - `, delete.ID) - if err != nil { - return err - } - - rows, _ := result.RowsAffected() - if rows == 0 { - return &errorutil.Error{Code: errorutil.NotFound, Err: fmt.Errorf("user ID not found: %d", delete.ID)} - } - - return nil -} diff --git a/store/workspace_user.go b/store/workspace_user.go index b84435c..9b9e8a7 100644 --- a/store/workspace_user.go +++ b/store/workspace_user.go @@ -191,7 +191,7 @@ func (raw *workspaceUserRaw) toWorkspaceUser() *api.WorkspaceUser { } func (s *Store) ComposeWorkspaceUser(ctx context.Context, workspaceUser *api.WorkspaceUser) error { - user, err := s.FindUser(ctx, &api.UserFind{ + user, err := s.GetUser(ctx, &FindUser{ ID: &workspaceUser.UserID, }) if err != nil { @@ -199,7 +199,7 @@ func (s *Store) ComposeWorkspaceUser(ctx context.Context, workspaceUser *api.Wor } workspaceUser.Email = user.Email - workspaceUser.DisplayName = user.DisplayName + workspaceUser.DisplayName = user.Nickname return nil }