storj/satellite/console/consoleweb/consoleapi/projects.go
Wilfred Asomani 70f6b60d91 satellite/console: add get user projects http endpoint
This change adds an endpoint to get a user's projects, similar to
the MyProjects GraphQL query.

Issue: https://github.com/storj/storj/issues/6132

Change-Id: I91feb5a1ee8c1231a8a5e6de9b8dc5b256f857c5
2023-08-07 16:05:09 +00:00

283 lines
7.2 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/common/uuid"
"storj.io/storj/private/web"
"storj.io/storj/satellite/console"
)
// Projects is an api controller that exposes projects related functionality.
type Projects struct {
log *zap.Logger
service *console.Service
}
// NewProjects is a constructor for api analytics controller.
func NewProjects(log *zap.Logger, service *console.Service) *Projects {
return &Projects{
log: log,
service: service,
}
}
// GetUserProjects returns the user's projects.
func (p *Projects) GetUserProjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
projects, err := p.service.GetUsersProjects(ctx)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
type jsonProject struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
OwnerID uuid.UUID `json:"ownerId"`
Description string `json:"description"`
MemberCount int `json:"memberCount"`
CreatedAt time.Time `json:"createdAt"`
}
response := make([]jsonProject, 0)
for _, project := range projects {
response = append(response, jsonProject{
ID: project.PublicID,
Name: project.Name,
OwnerID: project.OwnerID,
Description: project.Description,
MemberCount: project.MemberCount,
CreatedAt: project.CreatedAt,
})
}
err = json.NewEncoder(w).Encode(response)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
}
}
// GetSalt returns the project's salt.
func (p *Projects) GetSalt(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
idParam, ok := mux.Vars(r)["id"]
if !ok {
p.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("missing id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
}
salt, err := p.service.GetSalt(ctx, id)
if err != nil {
p.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}
b64SaltString := base64.StdEncoding.EncodeToString(salt)
err = json.NewEncoder(w).Encode(b64SaltString)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
}
}
// InviteUsers sends invites to a given project(id) to the given users (emails).
func (p *Projects) InviteUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
idParam, ok := mux.Vars(r)["id"]
if !ok {
p.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("missing project id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
}
var data struct {
Emails []string `json:"emails"`
}
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
for i, email := range data.Emails {
data.Emails[i] = strings.TrimSpace(email)
}
_, err = p.service.InviteProjectMembers(ctx, id, data.Emails)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
}
}
// GetInviteLink returns a link to an invitation given project ID and invitee's email.
func (p *Projects) GetInviteLink(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
idParam, ok := mux.Vars(r)["id"]
if !ok {
p.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("missing project id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
}
email := r.URL.Query().Get("email")
if email == "" {
p.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("missing email query param"))
return
}
link, err := p.service.GetInviteLink(ctx, id, email)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
}
err = json.NewEncoder(w).Encode(link)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
}
}
// GetUserInvitations returns the user's pending project member invitations.
func (p *Projects) GetUserInvitations(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
invites, err := p.service.GetUserProjectInvitations(ctx)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
type jsonInvite struct {
ProjectID uuid.UUID `json:"projectID"`
ProjectName string `json:"projectName"`
ProjectDescription string `json:"projectDescription"`
InviterEmail string `json:"inviterEmail"`
CreatedAt time.Time `json:"createdAt"`
}
response := make([]jsonInvite, 0)
for _, invite := range invites {
proj, err := p.service.GetProjectNoAuth(ctx, invite.ProjectID)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
respInvite := jsonInvite{
ProjectID: proj.PublicID,
ProjectName: proj.Name,
ProjectDescription: proj.Description,
CreatedAt: invite.CreatedAt,
}
if invite.InviterID != nil {
inviter, err := p.service.GetUser(ctx, *invite.InviterID)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}
respInvite.InviterEmail = inviter.Email
}
response = append(response, respInvite)
}
err = json.NewEncoder(w).Encode(response)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
}
}
// RespondToInvitation handles accepting or declining a user's project member invitation.
func (p *Projects) RespondToInvitation(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var ok bool
var idParam string
if idParam, ok = mux.Vars(r)["id"]; !ok {
p.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("missing project id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
}
var payload struct {
Response console.ProjectInvitationResponse `json:"response"`
}
err = json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
err = p.service.RespondToProjectInvitation(ctx, id, payload.Response)
if err != nil {
status := http.StatusInternalServerError
switch {
case console.ErrAlreadyMember.Has(err):
status = http.StatusConflict
case console.ErrProjectInviteInvalid.Has(err):
status = http.StatusNotFound
case console.ErrValidation.Has(err):
status = http.StatusBadRequest
}
p.serveJSONError(ctx, w, status, err)
}
}
// serveJSONError writes JSON error to response output stream.
func (p *Projects) serveJSONError(ctx context.Context, w http.ResponseWriter, status int, err error) {
web.ServeJSONError(ctx, p.log, w, status, err)
}