From 8a798080b5d90a2d2ccd41c8a3597248fcdec3bf Mon Sep 17 00:00:00 2001 From: "alessandro.pinna" Date: Mon, 4 Apr 2022 07:46:42 -0700 Subject: [PATCH] Gateway: added api to get user projects --- internal/services/gateway/action/user.go | 84 ++++++++++++++++++++++++ internal/services/gateway/api/user.go | 78 ++++++++++++++++++++++ internal/services/gateway/gateway.go | 2 + services/gateway/client/client.go | 15 +++++ tests/setup_test.go | 63 ++++++++++++++++++ 5 files changed, 242 insertions(+) diff --git a/internal/services/gateway/action/user.go b/internal/services/gateway/action/user.go index f07719956..7c6174ba3 100644 --- a/internal/services/gateway/action/user.go +++ b/internal/services/gateway/action/user.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "regexp" + "sort" "strings" "time" @@ -997,3 +998,86 @@ func (h *ActionHandler) UserCreateRun(ctx context.Context, req *UserCreateRunReq return h.CreateRuns(ctx, creq) } + +type GetUserProjectsRequest struct { + UserRef string + Limit int + Page int +} + +func ProjectsPaginate(page int, limit int, data []*csapitypes.Project) []*csapitypes.Project { + start := page * limit + + if start > len(data) { + return nil + } + + end := start + limit + if end > len(data) { + end = len(data) + } + + return data[start:end] +} + +func (h *ActionHandler) GetUserProjects(ctx context.Context, req *GetUserProjectsRequest) ([]*csapitypes.Project, error) { + if !common.IsUserLogged(ctx) { + return nil, errors.Errorf("user not logged in") + } + user, _, err := h.configstoreClient.GetUser(ctx, req.UserRef) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to get user %q", req.UserRef)) + } + + projects := make([]*csapitypes.Project, 0) + + prj, err := h.GetProjectgroupProjects(ctx, "user"+"/"+user.Name) + if err != nil { + return nil, errors.Wrapf(err, "failed to get projects for user %q", user.Name) + } + projects = append(projects, prj...) + + userOrgs, err := h.GetUserOrgs(ctx, req.UserRef) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to get organizations for user %q", req.UserRef)) + } + for _, org := range userOrgs { + prj, err := h.GetProjectgroupProjects(ctx, "org"+"/"+org.Organization.Name) + if err != nil { + return nil, errors.Wrapf(err, "failed to get projects for org %q", org.Organization.Name) + } + projects = append(projects, prj...) + } + + sort.SliceStable(projects, func(i, j int) bool { + return strings.Compare(strings.ToLower(projects[i].Path), strings.ToLower(projects[j].Path)) < 0 + }) + + projects = ProjectsPaginate(req.Page, req.Limit, projects) + + return projects, nil +} + +func (h *ActionHandler) GetProjectgroupProjects(ctx context.Context, projectgroup string) ([]*csapitypes.Project, error) { + projects := make([]*csapitypes.Project, 0) + + prj, err := h.GetProjectGroupProjects(ctx, projectgroup) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to get projects for group %q", projectgroup)) + } + projects = append(projects, prj...) + + subgroups, err := h.GetProjectGroupSubgroups(ctx, projectgroup) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to get subgroups for group %q", projectgroup)) + } + for _, s := range subgroups { + prj, err := h.GetProjectgroupProjects(ctx, s.ID) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to get projects for group %q", s.ID)) + } + projects = append(projects, prj...) + } + + return projects, nil +} diff --git a/internal/services/gateway/api/user.go b/internal/services/gateway/api/user.go index 6ec78f4db..8749069a1 100644 --- a/internal/services/gateway/api/user.go +++ b/internal/services/gateway/api/user.go @@ -33,6 +33,11 @@ import ( "github.com/rs/zerolog" ) +const ( + DefaultProjectsLimit = 25 + MaxProjectsLimit = 40 +) + type CreateUserHandler struct { log zerolog.Logger ah *action.ActionHandler @@ -643,3 +648,76 @@ func createUserOrgsResponse(o *csapitypes.UserOrgsResponse) *gwapitypes.UserOrgs return userOrgs } + +type UserProjectsHandler struct { + log zerolog.Logger + ah *action.ActionHandler +} + +func NewUserProjectsHandler(log zerolog.Logger, ah *action.ActionHandler) *UserProjectsHandler { + return &UserProjectsHandler{log: log, ah: ah} +} + +func (h *UserProjectsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userID := common.CurrentUserID(ctx) + if userID == "" { + util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("user not authenticated"))) + return + } + + q := r.URL.Query() + + limitS := q.Get("limit") + limit := DefaultProjectsLimit + if limitS != "" { + var err error + limit, err = strconv.Atoi(limitS) + if err != nil { + util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Wrapf(err, "cannot parse limit"))) + return + } + } + if limit < 0 { + util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("limit must be greater or equal than 0"))) + return + } + if limit > MaxProjectsLimit { + limit = MaxRunsLimit + } + + pageS := q.Get("page") + page := 0 + if pageS != "" { + var err error + page, err = strconv.Atoi(pageS) + if err != nil { + util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Wrapf(err, "cannot parse page"))) + return + } + } + if page < 0 { + util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("page must be greater or equal than 0"))) + return + } + + userProjects, err := h.ah.GetUserProjects(ctx, &action.GetUserProjectsRequest{ + UserRef: userID, + Limit: limit, + Page: page, + }) + if util.HTTPError(w, err) { + h.log.Err(err).Send() + return + } + + res := make([]*gwapitypes.ProjectResponse, len(userProjects)) + for i, userProject := range userProjects { + res[i] = createProjectResponse(userProject) + } + + if err := util.HTTPResponse(w, http.StatusOK, res); err != nil { + h.log.Err(err).Send() + } +} diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index 3fed3b271..9ab62d950 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -181,6 +181,7 @@ func (g *Gateway) Run(ctx context.Context) error { deleteUserHandler := api.NewDeleteUserHandler(g.log, g.ah) userCreateRunHandler := api.NewUserCreateRunHandler(g.log, g.ah) userOrgsHandler := api.NewUserOrgsHandler(g.log, g.ah) + userProjectsHandler := api.NewUserProjectsHandler(g.log, g.ah) createUserLAHandler := api.NewCreateUserLAHandler(g.log, g.ah) deleteUserLAHandler := api.NewDeleteUserLAHandler(g.log, g.ah) @@ -289,6 +290,7 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/users/{userref}", authForcedHandler(deleteUserHandler)).Methods("DELETE") apirouter.Handle("/user/createrun", authForcedHandler(userCreateRunHandler)).Methods("POST") apirouter.Handle("/user/orgs", authForcedHandler(userOrgsHandler)).Methods("GET") + apirouter.Handle("/user/projects", authForcedHandler(userProjectsHandler)).Methods("GET") apirouter.Handle("/users/{userref}/runs", authForcedHandler(userRunsHandler)).Methods("GET") apirouter.Handle("/users/{userref}/runs/{runnumber}", authOptionalHandler(userRunHandler)).Methods("GET") diff --git a/services/gateway/client/client.go b/services/gateway/client/client.go index c4162d3eb..7ca9915a9 100644 --- a/services/gateway/client/client.go +++ b/services/gateway/client/client.go @@ -637,3 +637,18 @@ func (c *Client) GetUserOrgs(ctx context.Context) ([]*gwapitypes.UserOrgsRespons resp, err := c.getParsedResponse(ctx, "GET", "/user/orgs", nil, jsonContent, nil, &userOrgs) return userOrgs, resp, errors.WithStack(err) } + +func (c *Client) GetUserProjects(ctx context.Context, page int, limit int) ([]*gwapitypes.ProjectResponse, *http.Response, error) { + userProjects := []*gwapitypes.ProjectResponse{} + + q := url.Values{} + if page > 0 { + q.Add("page", strconv.Itoa(page)) + } + if limit > 0 { + q.Add("limit", strconv.Itoa(limit)) + } + + resp, err := c.getParsedResponse(ctx, "GET", "/user/projects", q, jsonContent, nil, &userProjects) + return userProjects, resp, errors.WithStack(err) +} diff --git a/tests/setup_test.go b/tests/setup_test.go index 947b7c314..b8b5915f7 100644 --- a/tests/setup_test.go +++ b/tests/setup_test.go @@ -1928,3 +1928,66 @@ func TestUserOrgs(t *testing.T) { t.Fatalf("user orgs mismatch (-want +got):\n%s", diff) } } + +func TestUserProjects(t *testing.T) { + dir := t.TempDir() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tgitea, c := setup(ctx, t, dir, true) + defer shutdownGitea(tgitea) + + giteaToken, token := createLinkedAccount(ctx, t, tgitea, c) + + giteaAPIURL := fmt.Sprintf("http://%s:%s", tgitea.HTTPListenAddress, tgitea.HTTPPort) + giteaClient := gitea.NewClient(giteaAPIURL, giteaToken) + + giteaRepo, err := giteaClient.CreateRepo(gitea.CreateRepoOption{ + Name: "repo01", + Private: false, + }) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + t.Logf("created gitea repo: %s", giteaRepo.Name) + + gwClient := gwclient.NewClient(c.Gateway.APIExposedURL, token) + + project01, _, err := gwClient.CreateProject(ctx, &gwapitypes.CreateProjectRequest{ + Name: "Project01", + ParentRef: path.Join("user", agolaUser01), + RemoteSourceName: "gitea", + RepoPath: path.Join(giteaUser01, "repo01"), + Visibility: gwapitypes.VisibilityPublic, + }) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + _, _, err = gwClient.CreateOrg(ctx, &gwapitypes.CreateOrgRequest{Name: "org02", Visibility: gwapitypes.VisibilityPublic}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + project02, _, err := gwClient.CreateProject(ctx, &gwapitypes.CreateProjectRequest{ + Name: "Project02", + ParentRef: path.Join("org", "org02"), + RemoteSourceName: "gitea", + RepoPath: path.Join(giteaUser01, "repo01"), + Visibility: gwapitypes.VisibilityPublic, + }) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + userProjects, _, err := gwClient.GetUserProjects(ctx, 0, 10) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + expectedProjects := []*gwapitypes.ProjectResponse{project02, project01} + + if diff := cmp.Diff(expectedProjects, userProjects); diff != "" { + t.Fatalf("user projects mismatch (-want +got):\n%s", diff) + } +}