From 44ef82fb4acd3eda8bd4da2af8e12dd82979ffda Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 20 Jun 2023 17:37:50 +0800 Subject: [PATCH] feat: migrate workspace user to api v1 --- api/user.go | 75 +++------ api/v1/v1.go | 1 + {server => api/v1}/workspace_user.go | 124 +++++++++++---- api/workspace.go | 56 ------- api/workspace_user.go | 48 ------ server/server.go | 1 - server/shortcut.go | 5 +- store/workspace_user.go | 221 --------------------------- 8 files changed, 113 insertions(+), 418 deletions(-) rename {server => api/v1}/workspace_user.go (56%) delete mode 100644 api/workspace.go delete mode 100644 api/workspace_user.go diff --git a/api/user.go b/api/user.go index 5437fed..a7aea12 100644 --- a/api/user.go +++ b/api/user.go @@ -1,10 +1,25 @@ package api -import ( - "fmt" - "net/mail" +// Role is the type of a role. +type Role string + +const ( + // RoleAdmin is the ADMIN role. + RoleAdmin Role = "ADMIN" + // RoleUser is the USER role. + RoleUser Role = "USER" ) +func (e Role) String() string { + switch e { + case RoleAdmin: + return "ADMIN" + case RoleUser: + return "USER" + } + return "USER" +} + type User struct { ID int `json:"id"` @@ -29,57 +44,3 @@ type UserCreate struct { OpenID string `json:"-"` Role Role `json:"-"` } - -func (create UserCreate) Validate() error { - if len(create.Email) < 3 { - return fmt.Errorf("email is too short, minimum length is 6") - } - if !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 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"` - ResetOpenID *bool `json:"resetOpenId"` - PasswordHash *string `json:"-"` - OpenID *string `json:"-"` -} - -type UserFind struct { - ID *int `json:"id"` - - // Standard fields - RowStatus *RowStatus `json:"rowStatus"` - - // Domain specific fields - Email *string `json:"email"` - DisplayName *string `json:"displayName"` - OpenID *string `json:"openId"` - Role *Role `json:"-"` -} - -type UserDelete struct { - ID int -} - -// validateEmail validates the email. -func validateEmail(email string) bool { - if _, err := mail.ParseAddress(email); err != nil { - return false - } - return true -} diff --git a/api/v1/v1.go b/api/v1/v1.go index 57684f4..3b48b65 100644 --- a/api/v1/v1.go +++ b/api/v1/v1.go @@ -24,4 +24,5 @@ func (s *APIV1Service) Start(apiV1Group *echo.Group, secret string) { s.registerAuthRoutes(apiV1Group, secret) s.registerUserRoutes(apiV1Group) s.registerWorkspaceRoutes(apiV1Group) + s.registerWorkspaceUserRoutes(apiV1Group) } diff --git a/server/workspace_user.go b/api/v1/workspace_user.go similarity index 56% rename from server/workspace_user.go rename to api/v1/workspace_user.go index 72dd274..2569046 100644 --- a/server/workspace_user.go +++ b/api/v1/workspace_user.go @@ -1,18 +1,34 @@ -package server +package v1 import ( + "context" "encoding/json" "fmt" "net/http" "strconv" - "github.com/boojack/shortify/api" - "github.com/boojack/shortify/internal/errorutil" + "github.com/boojack/shortify/store" "github.com/labstack/echo/v4" ) -func (s *Server) registerWorkspaceUserRoutes(g *echo.Group) { +type WorkspaceUser struct { + WorkspaceID int `json:"workspaceId"` + UserID int `json:"userId"` + Role Role `json:"role"` + + // Related fields + Username string `json:"username"` + Nickname string `json:"nickname"` +} + +type UpsertWorkspaceUserRequest struct { + WorkspaceID int `json:"workspaceId"` + UserID int `json:"userId"` + Role Role `json:"role"` +} + +func (s *APIV1Service) registerWorkspaceUserRoutes(g *echo.Group) { g.POST("/workspace/:id/user", func(c echo.Context) error { ctx := c.Request().Context() userID, ok := c.Get(getUserIDContextKey()).(int) @@ -25,33 +41,39 @@ func (s *Server) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted workspace id").SetInternal(err) } - currentWorkspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{ + currentWorkspaceUser, err := s.Store.GetWorkspaceUser(ctx, &store.FindWorkspaceUser{ WorkspaceID: &workspaceID, UserID: &userID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err) } - if currentWorkspaceUser.Role != api.RoleAdmin { + if currentWorkspaceUser.Role != store.RoleAdmin { return echo.NewHTTPError(http.StatusForbidden, "Access forbidden to add workspace user").SetInternal(err) } - workspaceUserUpsert := &api.WorkspaceUserUpsert{ + upsert := &UpsertWorkspaceUserRequest{ WorkspaceID: workspaceID, } - if err := json.NewDecoder(c.Request().Body).Decode(workspaceUserUpsert); err != nil { + if err := json.NewDecoder(c.Request().Body).Decode(upsert); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post workspace user request").SetInternal(err) } - workspaceUser, err := s.Store.UpsertWorkspaceUser(ctx, workspaceUserUpsert) + workspaceUser, err := s.Store.UpsertWorkspaceUserV1(ctx, &store.WorkspaceUser{ + WorkspaceID: upsert.WorkspaceID, + UserID: upsert.UserID, + Role: convertRoleToStore(upsert.Role), + }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert workspace user").SetInternal(err) } - if err := s.Store.ComposeWorkspaceUser(ctx, workspaceUser); err != nil { + + composedWorkspaceUser, err := s.composeWorkspaceUser(ctx, workspaceUser) + if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose workspace user").SetInternal(err) } - return c.JSON(http.StatusOK, composeResponse(workspaceUser)) + return c.JSON(http.StatusOK, composedWorkspaceUser) }) g.GET("/workspace/:id/user", func(c echo.Context) error { @@ -61,20 +83,23 @@ func (s *Server) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted workspace id").SetInternal(err) } - workspaceUserList, err := s.Store.FindWordspaceUserList(ctx, &api.WorkspaceUserFind{ + workspaceUserList, err := s.Store.ListWorkspaceUsers(ctx, &store.FindWorkspaceUser{ WorkspaceID: &workspaceID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user list").SetInternal(err) } + composedList := make([]*WorkspaceUser, 0, len(workspaceUserList)) for _, workspaceUser := range workspaceUserList { - if err := s.Store.ComposeWorkspaceUser(ctx, workspaceUser); err != nil { + composedWorkspaceUser, err := s.composeWorkspaceUser(ctx, workspaceUser) + if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose workspace user").SetInternal(err) } + composedList = append(composedList, composedWorkspaceUser) } - return c.JSON(http.StatusOK, composeResponse(workspaceUserList)) + return c.JSON(http.StatusOK, composedList) }) g.GET("/workspace/:workspaceId/user/:userId", func(c echo.Context) error { @@ -88,7 +113,7 @@ func (s *Server) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err) } - workspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{ + workspaceUser, err := s.Store.GetWorkspaceUser(ctx, &store.FindWorkspaceUser{ WorkspaceID: &workspaceID, UserID: &userID, }) @@ -96,11 +121,12 @@ func (s *Server) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err) } - if err := s.Store.ComposeWorkspaceUser(ctx, workspaceUser); err != nil { + composedWorkspaceUser, err := s.composeWorkspaceUser(ctx, workspaceUser) + if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose workspace user").SetInternal(err) } - return c.JSON(http.StatusOK, composeResponse(workspaceUser)) + return c.JSON(http.StatusOK, composedWorkspaceUser) }) g.DELETE("/workspace/:workspaceId/user/:userId", func(c echo.Context) error { @@ -120,36 +146,68 @@ func (s *Server) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err) } - currentWorkspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{ + currentWorkspaceUser, err := s.Store.GetWorkspaceUser(ctx, &store.FindWorkspaceUser{ WorkspaceID: &workspaceID, UserID: ¤tUserID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err) } - if currentUserID != userID && currentWorkspaceUser.Role != api.RoleAdmin { + if currentUserID != userID && currentWorkspaceUser.Role != store.RoleAdmin { return echo.NewHTTPError(http.StatusForbidden, "Access forbidden to delete workspace user").SetInternal(err) } - workspaceUserDelete := &api.WorkspaceUserDelete{ + if err := s.Store.DeleteWorkspaceUserV1(ctx, &store.DeleteWorkspaceUser{ WorkspaceID: workspaceID, UserID: userID, - } - if err := s.Store.DeleteWorkspaceUser(ctx, workspaceUserDelete); err != nil { - if errorutil.ErrorCode(err) == errorutil.NotFound { - return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Workspace user not found with workspace id %d and user id %d", workspaceID, userID)) - } + }); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete workspace user").SetInternal(err) } - shortcutDelete := &api.ShortcutDelete{ - CreatorID: &userID, - WorkspaceID: &workspaceID, - } - if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err) - } - return c.JSON(http.StatusOK, true) }) } + +func convertRoleToStore(role Role) store.Role { + switch role { + case RoleAdmin: + return store.RoleAdmin + case RoleUser: + return store.RoleUser + default: + return store.RoleUser + } +} + +func convertRoleFromStore(role store.Role) Role { + switch role { + case store.RoleAdmin: + return RoleAdmin + case store.RoleUser: + return RoleUser + default: + return RoleUser + } +} + +func (s *APIV1Service) composeWorkspaceUser(ctx context.Context, workspaceUser *store.WorkspaceUser) (*WorkspaceUser, error) { + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &workspaceUser.UserID, + }) + if err != nil { + return nil, err + } + if user == nil { + return nil, fmt.Errorf("Failed to find user %d", workspaceUser.UserID) + } + + composedWorkspaceUser := &WorkspaceUser{ + WorkspaceID: workspaceUser.WorkspaceID, + UserID: workspaceUser.UserID, + Role: convertRoleFromStore(workspaceUser.Role), + Username: user.Username, + Nickname: user.Nickname, + } + + return composedWorkspaceUser, nil +} diff --git a/api/workspace.go b/api/workspace.go deleted file mode 100644 index a1f6602..0000000 --- a/api/workspace.go +++ /dev/null @@ -1,56 +0,0 @@ -package api - -type Workspace struct { - ID int `json:"id"` - - // Standard fields - CreatorID int `json:"creatorId"` - CreatedTs int64 `json:"createdTs"` - UpdatedTs int64 `json:"updatedTs"` - RowStatus RowStatus `json:"rowStatus"` - - // Domain specific fields - Name string `json:"name"` - Title string `json:"title"` - Description string `json:"description"` - - // Related fields - WorkspaceUserList []*WorkspaceUser `json:"workspaceUserList"` -} - -type WorkspaceCreate struct { - CreatorID int - - Name string `json:"name"` - Title string `json:"title"` - Description string `json:"description"` -} - -type WorkspacePatch struct { - ID int - - // Standard fields - RowStatus *RowStatus `json:"rowStatus"` - - // Domain specific fields - Name *string `json:"name"` - Title *string `json:"title"` - Description *string `json:"description"` -} - -type WorkspaceFind struct { - ID *int `json:"id"` - - // Standard fields - RowStatus *RowStatus `json:"rowStatus"` - - // Domain specific fields - Name *string `json:"name"` - - // Related fields - MemberID *int -} - -type WorkspaceDelete struct { - ID int -} diff --git a/api/workspace_user.go b/api/workspace_user.go deleted file mode 100644 index 33391c1..0000000 --- a/api/workspace_user.go +++ /dev/null @@ -1,48 +0,0 @@ -package api - -// Role is the type of a role. -type Role string - -const ( - // RoleAdmin is the ADMIN role. - RoleAdmin Role = "ADMIN" - // RoleUser is the USER role. - RoleUser Role = "USER" -) - -func (e Role) String() string { - switch e { - case RoleAdmin: - return "ADMIN" - case RoleUser: - return "USER" - } - return "USER" -} - -type WorkspaceUser struct { - WorkspaceID int `json:"workspaceId"` - UserID int `json:"userId"` - Role Role `json:"role"` - - // Related fields - Email string `json:"email"` - DisplayName string `json:"displayName"` -} - -type WorkspaceUserUpsert struct { - WorkspaceID int `json:"workspaceId"` - UserID int `json:"userId"` - Role Role `json:"role"` - UpdatedTs *int64 `json:"updatedTs"` -} - -type WorkspaceUserFind struct { - WorkspaceID *int - UserID *int -} - -type WorkspaceUserDelete struct { - WorkspaceID int - UserID int -} diff --git a/server/server.go b/server/server.go index 306fb4c..ce15301 100644 --- a/server/server.go +++ b/server/server.go @@ -64,7 +64,6 @@ func NewServer(profile *profile.Profile, store *store.Store) (*Server, error) { apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return JWTMiddleware(s, next, string(secret)) }) - s.registerWorkspaceUserRoutes(apiGroup) s.registerShortcutRoutes(apiGroup) // Register API v1 routes. diff --git a/server/shortcut.go b/server/shortcut.go index 9401f23..eec36cb 100644 --- a/server/shortcut.go +++ b/server/shortcut.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/boojack/shortify/api" + "github.com/boojack/shortify/store" "github.com/labstack/echo/v4" ) @@ -66,7 +67,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err) } - workspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{ + workspaceUser, err := s.Store.GetWorkspaceUser(ctx, &store.FindWorkspaceUser{ UserID: &userID, WorkspaceID: &shortcut.WorkspaceID, }) @@ -74,7 +75,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err) } - if shortcut.CreatorID != userID && workspaceUser.Role != api.RoleAdmin { + if shortcut.CreatorID != userID && workspaceUser.Role != store.RoleAdmin { return echo.NewHTTPError(http.StatusForbidden, "Forbidden to patch shortcut") } diff --git a/store/workspace_user.go b/store/workspace_user.go index 9b9e8a7..d9daddc 100644 --- a/store/workspace_user.go +++ b/store/workspace_user.go @@ -3,11 +3,7 @@ package store import ( "context" "database/sql" - "fmt" "strings" - - "github.com/boojack/shortify/api" - "github.com/boojack/shortify/internal/errorutil" ) // Role is the type of a role. @@ -174,220 +170,3 @@ func listWorkspaceUsers(ctx context.Context, tx *sql.Tx, find *FindWorkspaceUser return list, nil } - -// workspaceUserRaw is the store model for WorkspaceUser. -type workspaceUserRaw struct { - WorkspaceID int - UserID int - Role api.Role -} - -func (raw *workspaceUserRaw) toWorkspaceUser() *api.WorkspaceUser { - return &api.WorkspaceUser{ - WorkspaceID: raw.WorkspaceID, - UserID: raw.UserID, - Role: raw.Role, - } -} - -func (s *Store) ComposeWorkspaceUser(ctx context.Context, workspaceUser *api.WorkspaceUser) error { - user, err := s.GetUser(ctx, &FindUser{ - ID: &workspaceUser.UserID, - }) - if err != nil { - return err - } - - workspaceUser.Email = user.Email - workspaceUser.DisplayName = user.Nickname - - return nil -} - -func (s *Store) ComposeWorkspaceUserListForWorkspace(ctx context.Context, workspace *api.Workspace) error { - workspaceUserList, err := s.FindWordspaceUserList(ctx, &api.WorkspaceUserFind{ - WorkspaceID: &workspace.ID, - }) - if err != nil { - return err - } - - for _, workspaceUser := range workspaceUserList { - if err := s.ComposeWorkspaceUser(ctx, workspaceUser); err != nil { - return err - } - } - workspace.WorkspaceUserList = workspaceUserList - - return nil -} - -func (s *Store) UpsertWorkspaceUser(ctx context.Context, upsert *api.WorkspaceUserUpsert) (*api.WorkspaceUser, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - workspaceUserRaw, err := upsertWorkspaceUser(ctx, tx, upsert) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - workspaceUser := workspaceUserRaw.toWorkspaceUser() - - return workspaceUser, nil -} - -func (s *Store) FindWordspaceUserList(ctx context.Context, find *api.WorkspaceUserFind) ([]*api.WorkspaceUser, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - workspaceUserRawList, err := findWorkspaceUserList(ctx, tx, find) - if err != nil { - return nil, err - } - - list := []*api.WorkspaceUser{} - for _, raw := range workspaceUserRawList { - list = append(list, raw.toWorkspaceUser()) - } - - return list, nil -} - -func (s *Store) FindWordspaceUser(ctx context.Context, find *api.WorkspaceUserFind) (*api.WorkspaceUser, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - list, err := findWorkspaceUserList(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 workspace user with filter %+v", find)} - } else if len(list) > 1 { - return nil, &errorutil.Error{Code: errorutil.Conflict, Err: fmt.Errorf("found %d workspaces user with filter %+v, expect 1", len(list), find)} - } - - workspaceUser := list[0].toWorkspaceUser() - return workspaceUser, nil -} - -func (s *Store) DeleteWorkspaceUser(ctx context.Context, delete *api.WorkspaceUserDelete) error { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer tx.Rollback() - - err = deleteWorkspaceUser(ctx, tx, delete) - if err != nil { - return err - } - - err = tx.Commit() - return err -} - -func upsertWorkspaceUser(ctx context.Context, tx *sql.Tx, upsert *api.WorkspaceUserUpsert) (*workspaceUserRaw, error) { - set := []string{"workspace_id", "user_id", "role"} - args := []any{upsert.WorkspaceID, upsert.UserID, upsert.Role} - placeholder := []string{"?", "?", "?"} - - if v := upsert.UpdatedTs; v != nil { - set, args, placeholder = append(set, "updated_ts"), append(args, *v), append(placeholder, "?") - } - - query := ` - INSERT INTO workspace_user ( - ` + strings.Join(set, ", ") + ` - ) - VALUES (` + strings.Join(placeholder, ",") + `) - ON CONFLICT(workspace_id, user_id) DO UPDATE - SET - role = EXCLUDED.role - RETURNING workspace_id, user_id, role - ` - var workspaceUserRaw workspaceUserRaw - if err := tx.QueryRowContext(ctx, query, args...).Scan( - &workspaceUserRaw.WorkspaceID, - &workspaceUserRaw.UserID, - &workspaceUserRaw.Role, - ); err != nil { - return nil, err - } - - return &workspaceUserRaw, nil -} - -func findWorkspaceUserList(ctx context.Context, tx *sql.Tx, find *api.WorkspaceUserFind) ([]*workspaceUserRaw, error) { - where, args := []string{"1 = 1"}, []any{} - - if v := find.WorkspaceID; v != nil { - where, args = append(where, "workspace_id = ?"), append(args, *v) - } - if v := find.UserID; v != nil { - where, args = append(where, "user_id = ?"), append(args, *v) - } - - query := ` - SELECT - workspace_id, - user_id, - role - FROM workspace_user - WHERE ` + strings.Join(where, " AND ") - rows, err := tx.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - workspaceUserRawList := make([]*workspaceUserRaw, 0) - for rows.Next() { - var workspaceUserRaw workspaceUserRaw - if err := rows.Scan( - &workspaceUserRaw.WorkspaceID, - &workspaceUserRaw.UserID, - &workspaceUserRaw.Role, - ); err != nil { - return nil, err - } - - workspaceUserRawList = append(workspaceUserRawList, &workspaceUserRaw) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return workspaceUserRawList, nil -} - -func deleteWorkspaceUser(ctx context.Context, tx *sql.Tx, delete *api.WorkspaceUserDelete) error { - result, err := tx.ExecContext(ctx, ` - DELETE FROM workspace_user WHERE workspace_id = ? AND user_id = ? - `, delete.WorkspaceID, delete.UserID) - if err != nil { - return err - } - - rows, _ := result.RowsAffected() - if rows == 0 { - return &errorutil.Error{Code: errorutil.NotFound, Err: fmt.Errorf("workspace user not found")} - } - - return nil -}