apigen: project delete endpoint

Implemented project delete endpoint for REST API.
Added project usage status check service method to indicate if project can be deleted.
Updated project invoice status check method to indicate if project can be deleted.

Change-Id: I57dc96efb072517144252001ab5405446c9cdeb4
This commit is contained in:
Vitalii 2022-04-28 18:59:55 +03:00
parent 911cc1e163
commit f56504de2a
8 changed files with 343 additions and 166 deletions

View File

@ -58,6 +58,11 @@ func (eg *EndpointGroup) Post(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodPost, endpoint) eg.addEndpoint(path, http.MethodPost, endpoint)
} }
// Delete adds new DELETE endpoint to endpoints group.
func (eg *EndpointGroup) Delete(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodDelete, endpoint)
}
// addEndpoint adds new endpoint to endpoints list. // addEndpoint adds new endpoint to endpoints list.
func (eg *EndpointGroup) addEndpoint(path, method string, endpoint *Endpoint) { func (eg *EndpointGroup) addEndpoint(path, method string, endpoint *Endpoint) {
pathMethod := PathMethod{ pathMethod := PathMethod{

View File

@ -104,12 +104,17 @@ func (a *API) generateGo() ([]byte, error) {
for _, group := range a.EndpointGroups { for _, group := range a.EndpointGroups {
p("type %sService interface {", group.Name) p("type %sService interface {", group.Name)
for _, e := range group.Endpoints { for _, e := range group.Endpoints {
responseType := reflect.TypeOf(e.Response)
var params string var params string
for _, param := range e.Params { for _, param := range e.Params {
params += param.Type.String() + ", " params += param.Type.String() + ", "
} }
if e.Response != nil {
responseType := reflect.TypeOf(e.Response)
p("%s(context.Context, "+params+") (%s, api.HTTPError)", e.MethodName, a.handleTypesPackage(responseType)) p("%s(context.Context, "+params+") (%s, api.HTTPError)", e.MethodName, a.handleTypesPackage(responseType))
} else {
p("%s(context.Context, "+params+") (api.HTTPError)", e.MethodName)
}
} }
p("}") p("}")
p("") p("")
@ -184,68 +189,41 @@ func (a *API) generateGo() ([]byte, error) {
for _, param := range endpoint.Params { for _, param := range endpoint.Params {
switch param.Type { switch param.Type {
case reflect.TypeOf(uuid.UUID{}): case reflect.TypeOf(uuid.UUID{}):
p("%s, err := uuid.FromString(r.URL.Query().Get(\"%s\"))", param.Name, param.Name) handleUUIDQuery(p, param)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
continue continue
case reflect.TypeOf(time.Time{}): case reflect.TypeOf(time.Time{}):
p("%s, err := time.Parse(dateLayout, r.URL.Query().Get(\"%s\"))", param.Name, param.Name) handleTimeQuery(p, param)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
continue continue
case reflect.TypeOf(""): case reflect.TypeOf(""):
p("%s := r.URL.Query().Get(\"%s\")", param.Name, param.Name) handleStringQuery(p, param)
p("if %s == \"\" {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"parameter '%s' can't be empty\"))", param.Name)
p("return")
p("}")
p("")
continue continue
} }
} }
case http.MethodPatch: case http.MethodPatch:
for _, param := range endpoint.Params { for _, param := range endpoint.Params {
if param.Type == reflect.TypeOf(uuid.UUID{}) { if param.Type == reflect.TypeOf(uuid.UUID{}) {
p("%sParam, ok := mux.Vars(r)[\"%s\"]", param.Name, param.Name) handleUUIDParam(p, param)
p("if !ok {")
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"missing %s route param\"))", param.Name)
p("return")
p("}")
p("")
p("%s, err := uuid.FromString(%sParam)", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
} else { } else {
p("%s := &%s{}", param.Name, param.Type) handleBody(p, param)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
} }
} }
case http.MethodPost: case http.MethodPost:
for _, param := range endpoint.Params { for _, param := range endpoint.Params {
p("%s := &%s{}", param.Name, param.Type) handleBody(p, param)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name) }
p("api.ServeError(h.log, w, http.StatusBadRequest, err)") case http.MethodDelete:
p("return") for _, param := range endpoint.Params {
p("}") handleUUIDParam(p, param)
p("")
} }
} }
methodFormat := "retVal, httpErr := h.service.%s(ctx, " var methodFormat string
if endpoint.Response != nil {
methodFormat = "retVal, httpErr := h.service.%s(ctx, "
} else {
methodFormat = "httpErr := h.service.%s(ctx, "
}
switch pathMethod.Method { switch pathMethod.Method {
case http.MethodGet: case http.MethodGet:
for _, methodParam := range endpoint.Params { for _, methodParam := range endpoint.Params {
@ -263,22 +241,30 @@ func (a *API) generateGo() ([]byte, error) {
for _, methodParam := range endpoint.Params { for _, methodParam := range endpoint.Params {
methodFormat += "*" + methodParam.Name + ", " methodFormat += "*" + methodParam.Name + ", "
} }
case http.MethodDelete:
for _, methodParam := range endpoint.Params {
methodFormat += methodParam.Name + ", "
}
} }
methodFormat += ")" methodFormat += ")"
p(methodFormat, endpoint.MethodName) p(methodFormat, endpoint.MethodName)
p("if httpErr.Err != nil {") p("if httpErr.Err != nil {")
p("api.ServeError(h.log, w, httpErr.Status, httpErr.Err)") p("api.ServeError(h.log, w, httpErr.Status, httpErr.Err)")
if endpoint.Response == nil {
p("}")
p("}")
continue
}
p("return") p("return")
p("}") p("}")
p("")
p("")
p("err = json.NewEncoder(w).Encode(retVal)") p("err = json.NewEncoder(w).Encode(retVal)")
p("if err != nil {") p("if err != nil {")
p("h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))", endpoint.MethodName, cases.Title(language.Und).String(group.Prefix)) p("h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))", endpoint.MethodName, cases.Title(language.Und).String(group.Prefix))
p("}") p("}")
p("}") p("}")
p("")
} }
} }
@ -292,7 +278,7 @@ func (a *API) generateGo() ([]byte, error) {
// handleTypesPackage handles the way some type is used in generated code. // handleTypesPackage handles the way some type is used in generated code.
// If type is from the same package then we use only type's name. // If type is from the same package then we use only type's name.
// If type is from external package then we use type along with it's appropriate package name. // If type is from external package then we use type along with its appropriate package name.
func (a *API) handleTypesPackage(t reflect.Type) interface{} { func (a *API) handleTypesPackage(t reflect.Type) interface{} {
if strings.HasPrefix(t.String(), a.PackageName) { if strings.HasPrefix(t.String(), a.PackageName) {
return t.Elem().Name() return t.Elem().Name()
@ -300,3 +286,60 @@ func (a *API) handleTypesPackage(t reflect.Type) interface{} {
return t return t
} }
// handleStringQuery handles request query param of type string.
func handleStringQuery(p func(format string, a ...interface{}), param Param) {
p("%s := r.URL.Query().Get(\"%s\")", param.Name, param.Name)
p("if %s == \"\" {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"parameter '%s' can't be empty\"))", param.Name)
p("return")
p("}")
p("")
}
// handleUUIDQuery handles request query param of type uuid.UUID.
func handleUUIDQuery(p func(format string, a ...interface{}), param Param) {
p("%s, err := uuid.FromString(r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
// handleTimeQuery handles request query param of type time.Time.
func handleTimeQuery(p func(format string, a ...interface{}), param Param) {
p("%s, err := time.Parse(dateLayout, r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
// handleUUIDParam handles request inline param of type uuid.UUID.
func handleUUIDParam(p func(format string, a ...interface{}), param Param) {
p("%sParam, ok := mux.Vars(r)[\"%s\"]", param.Name, param.Name)
p("if !ok {")
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"missing %s route param\"))", param.Name)
p("return")
p("}")
p("")
p("%s, err := uuid.FromString(%sParam)", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
// handleBody handles request body.
func handleBody(p func(format string, a ...interface{}), param Param) {
p("%s := &%s{}", param.Name, param.Type)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}

View File

@ -26,11 +26,12 @@ var ErrApikeysAPI = errs.Class("consoleapi apikeys api")
var ErrUsersAPI = errs.Class("consoleapi users api") var ErrUsersAPI = errs.Class("consoleapi users api")
type ProjectManagementService interface { type ProjectManagementService interface {
GenGetUsersProjects(context.Context) ([]console.Project, api.HTTPError)
GenGetSingleBucketUsageRollup(context.Context, uuid.UUID, string, time.Time, time.Time) (*accounting.BucketUsageRollup, api.HTTPError) GenGetSingleBucketUsageRollup(context.Context, uuid.UUID, string, time.Time, time.Time) (*accounting.BucketUsageRollup, api.HTTPError)
GenGetBucketUsageRollups(context.Context, uuid.UUID, time.Time, time.Time) ([]accounting.BucketUsageRollup, api.HTTPError) GenGetBucketUsageRollups(context.Context, uuid.UUID, time.Time, time.Time) ([]accounting.BucketUsageRollup, api.HTTPError)
GenCreateProject(context.Context, console.ProjectInfo) (*console.Project, api.HTTPError) GenCreateProject(context.Context, console.ProjectInfo) (*console.Project, api.HTTPError)
GenUpdateProject(context.Context, uuid.UUID, console.ProjectInfo) (*console.Project, api.HTTPError) GenUpdateProject(context.Context, uuid.UUID, console.ProjectInfo) (*console.Project, api.HTTPError)
GenDeleteProject(context.Context, uuid.UUID) api.HTTPError
GenGetUsersProjects(context.Context) ([]console.Project, api.HTTPError)
} }
type APIKeyManagementService interface { type APIKeyManagementService interface {
@ -70,11 +71,12 @@ func NewProjectManagement(log *zap.Logger, service ProjectManagementService, rou
} }
projectsRouter := router.PathPrefix("/api/v0/projects").Subrouter() projectsRouter := router.PathPrefix("/api/v0/projects").Subrouter()
projectsRouter.HandleFunc("/bucket-rollups", handler.handleGenGetBucketUsageRollups).Methods("GET")
projectsRouter.HandleFunc("/create", handler.handleGenCreateProject).Methods("POST") projectsRouter.HandleFunc("/create", handler.handleGenCreateProject).Methods("POST")
projectsRouter.HandleFunc("/update/{id}", handler.handleGenUpdateProject).Methods("PATCH") projectsRouter.HandleFunc("/update/{id}", handler.handleGenUpdateProject).Methods("PATCH")
projectsRouter.HandleFunc("/delete/{id}", handler.handleGenDeleteProject).Methods("DELETE")
projectsRouter.HandleFunc("/", handler.handleGenGetUsersProjects).Methods("GET") projectsRouter.HandleFunc("/", handler.handleGenGetUsersProjects).Methods("GET")
projectsRouter.HandleFunc("/bucket-rollup", handler.handleGenGetSingleBucketUsageRollup).Methods("GET") projectsRouter.HandleFunc("/bucket-rollup", handler.handleGenGetSingleBucketUsageRollup).Methods("GET")
projectsRouter.HandleFunc("/bucket-rollups", handler.handleGenGetBucketUsageRollups).Methods("GET")
return handler return handler
} }
@ -105,6 +107,55 @@ func NewUserManagement(log *zap.Logger, service UserManagementService, router *m
return handler return handler
} }
func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
projectID, err := uuid.FromString(r.URL.Query().Get("projectID"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
bucket := r.URL.Query().Get("bucket")
if bucket == "" {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("parameter 'bucket' can't be empty"))
return
}
since, err := time.Parse(dateLayout, r.URL.Query().Get("since"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
before, err := time.Parse(dateLayout, r.URL.Query().Get("before"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
retVal, httpErr := h.service.GenGetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GenGetSingleBucketUsageRollup response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}
func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.Request) { func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var err error var err error
@ -222,6 +273,37 @@ func (h *ProjectManagementHandler) handleGenUpdateProject(w http.ResponseWriter,
} }
} }
func (h *ProjectManagementHandler) handleGenDeleteProject(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
idParam, ok := mux.Vars(r)["id"]
if !ok {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
httpErr := h.service.GenDeleteProject(ctx, id)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
}
}
func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWriter, r *http.Request) { func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var err error var err error
@ -247,55 +329,6 @@ func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWrit
} }
} }
func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
projectID, err := uuid.FromString(r.URL.Query().Get("projectID"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
bucket := r.URL.Query().Get("bucket")
if bucket == "" {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("parameter 'bucket' can't be empty"))
return
}
since, err := time.Parse(dateLayout, r.URL.Query().Get("since"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
before, err := time.Parse(dateLayout, r.URL.Query().Get("before"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
retVal, httpErr := h.service.GenGetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GenGetSingleBucketUsageRollup response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}
func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r *http.Request) { func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
var err error var err error

View File

@ -12,7 +12,7 @@ curl -i -L \
``` ```
## Successful responses ## Successful responses
All the requests a non-empty body for the resource that we're interacting with. All the requests (except DELETE) have a non-empty response body for the resource that you're interacting with.
Example: Example:
@ -184,6 +184,13 @@ A successful response body:
} }
``` ```
#### DELETE /api/v0/projects/delete/{uuid string}
Deletes project by id.
Note: all the buckets and access grants have to be deleted first and there should not be any usage during current month for paid tier users.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
### Macaroon API key API Endpoints ### Macaroon API key API Endpoints
#### POST /api/v0/apikeys/create #### POST /api/v0/apikeys/create
Creates new macaroon API key. Creates new macaroon API key.

View File

@ -25,6 +25,37 @@ func main() {
{ {
g := a.Group("ProjectManagement", "projects") g := a.Group("ProjectManagement", "projects")
g.Post("/create", &apigen.Endpoint{
Name: "Create new Project",
Description: "Creates new Project with given info",
MethodName: "GenCreateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
g.Patch("/update/{id}", &apigen.Endpoint{
Name: "Update Project",
Description: "Updates project with given info",
MethodName: "GenUpdateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}),
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
g.Delete("/delete/{id}", &apigen.Endpoint{
Name: "Delete Project",
Description: "Deletes project by id",
MethodName: "GenDeleteProject",
Response: nil,
Params: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}),
},
})
g.Get("/", &apigen.Endpoint{ g.Get("/", &apigen.Endpoint{
Name: "Get Projects", Name: "Get Projects",
Description: "Gets all projects user has", Description: "Gets all projects user has",
@ -56,27 +87,6 @@ func main() {
apigen.NewParam("before", time.Time{}), apigen.NewParam("before", time.Time{}),
}, },
}) })
g.Post("/create", &apigen.Endpoint{
Name: "Create new Project",
Description: "Creates new Project with given info",
MethodName: "GenCreateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
g.Patch("/update/{id}", &apigen.Endpoint{
Name: "Update Project",
Description: "Updates project with given info",
MethodName: "GenUpdateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}),
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
} }
{ {

View File

@ -538,19 +538,31 @@ func (payment Payments) checkOutstandingInvoice(ctx context.Context) (err error)
return nil return nil
} }
// checkProjectInvoicingStatus returns if for the given project there are outstanding project records and/or usage // checkProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe). // which have not been applied/invoiced yet (meaning sent over to stripe).
func (payment Payments) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) { func (payment Payments) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
_, err = payment.service.getAuthAndAuditLog(ctx, "project charges") _, err = payment.service.getAuthAndAuditLog(ctx, "project invoicing status")
if err != nil { if err != nil {
return false, Error.Wrap(err) return Error.Wrap(err)
} }
return payment.service.accounts.CheckProjectInvoicingStatus(ctx, projectID) return payment.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
} }
// checkProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
func (payment Payments) checkProjectUsageStatus(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
_, err = payment.service.getAuthAndAuditLog(ctx, "project usage status")
if err != nil {
return Error.Wrap(err)
}
return payment.service.accounts.CheckProjectUsageStatus(ctx, projectID)
}
// ApplyCouponCode applies a coupon code to a Stripe customer // ApplyCouponCode applies a coupon code to a Stripe customer
// and returns the coupon corresponding to the code. // and returns the coupon corresponding to the code.
func (payment Payments) ApplyCouponCode(ctx context.Context, couponCode string) (coupon *payments.Coupon, err error) { func (payment Payments) ApplyCouponCode(ctx context.Context, couponCode string) (coupon *payments.Coupon, err error) {
@ -1421,6 +1433,7 @@ func (s *Service) GenCreateProject(ctx context.Context, projectInfo ProjectInfo)
// DeleteProject is a method for deleting project by id. // DeleteProject is a method for deleting project by id.
func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err error) { func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
auth, err := s.getAuthAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String())) auth, err := s.getAuthAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String()))
if err != nil { if err != nil {
return Error.Wrap(err) return Error.Wrap(err)
@ -1431,7 +1444,7 @@ func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err e
return Error.Wrap(err) return Error.Wrap(err)
} }
err = s.checkProjectCanBeDeleted(ctx, projectID) err = s.checkProjectCanBeDeleted(ctx, auth.User, projectID)
if err != nil { if err != nil {
return Error.Wrap(err) return Error.Wrap(err)
} }
@ -1444,6 +1457,46 @@ func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err e
return nil return nil
} }
// GenDeleteProject is a method for deleting project by id for generated API.
func (s *Service) GenDeleteProject(ctx context.Context, projectID uuid.UUID) (httpError api.HTTPError) {
var err error
defer mon.Task()(&ctx)(&err)
auth, err := s.getAuthAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String()))
if err != nil {
return api.HTTPError{
Status: http.StatusUnauthorized,
Err: Error.Wrap(err),
}
}
_, err = s.isProjectOwner(ctx, auth.User.ID, projectID)
if err != nil {
return api.HTTPError{
Status: http.StatusUnauthorized,
Err: Error.Wrap(err),
}
}
err = s.checkProjectCanBeDeleted(ctx, auth.User, projectID)
if err != nil {
return api.HTTPError{
Status: http.StatusConflict,
Err: Error.Wrap(err),
}
}
err = s.store.Projects().Delete(ctx, projectID)
if err != nil {
return api.HTTPError{
Status: http.StatusInternalServerError,
Err: Error.Wrap(err),
}
}
return httpError
}
// UpdateProject is a method for updating project name and description by id. // UpdateProject is a method for updating project name and description by id.
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, projectInfo ProjectInfo) (p *Project, err error) { func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, projectInfo ProjectInfo) (p *Project, err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
@ -2452,10 +2505,10 @@ func (s *Service) keyAuth(ctx context.Context, r *http.Request) (context.Context
// checkProjectCanBeDeleted ensures that all data, api-keys and buckets are deleted and usage has been accounted. // checkProjectCanBeDeleted ensures that all data, api-keys and buckets are deleted and usage has been accounted.
// no error means the project status is clean. // no error means the project status is clean.
func (s *Service) checkProjectCanBeDeleted(ctx context.Context, project uuid.UUID) (err error) { func (s *Service) checkProjectCanBeDeleted(ctx context.Context, user User, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
buckets, err := s.buckets.CountBuckets(ctx, project) buckets, err := s.buckets.CountBuckets(ctx, projectID)
if err != nil { if err != nil {
return err return err
} }
@ -2463,20 +2516,28 @@ func (s *Service) checkProjectCanBeDeleted(ctx context.Context, project uuid.UUI
return ErrUsage.New("some buckets still exist") return ErrUsage.New("some buckets still exist")
} }
keys, err := s.store.APIKeys().GetPagedByProjectID(ctx, project, APIKeyCursor{Limit: 1, Page: 1}) keys, err := s.store.APIKeys().GetPagedByProjectID(ctx, projectID, APIKeyCursor{Limit: 1, Page: 1})
if err != nil { if err != nil {
return err return err
} }
if keys.TotalCount > 0 { if keys.TotalCount > 0 {
return ErrUsage.New("some api-keys still exist") return ErrUsage.New("some api keys still exist")
} }
outstanding, err := s.Payments().checkProjectInvoicingStatus(ctx, project) if user.PaidTier {
if outstanding { err = s.Payments().checkProjectUsageStatus(ctx, projectID)
return ErrUsage.New("there is outstanding usage that is not charged yet") if err != nil {
}
return ErrUsage.Wrap(err) return ErrUsage.Wrap(err)
} }
}
err = s.Payments().checkProjectInvoicingStatus(ctx, projectID)
if err != nil {
return ErrUsage.Wrap(err)
}
return nil
}
// checkProjectLimit is used to check if user is able to create a new project. // checkProjectLimit is used to check if user is able to create a new project.
func (s *Service) checkProjectLimit(ctx context.Context, userID uuid.UUID) (currentProjects int, err error) { func (s *Service) checkProjectLimit(ctx context.Context, userID uuid.UUID) (currentProjects int, err error) {

View File

@ -29,9 +29,12 @@ type Accounts interface {
// ProjectCharges returns how much money current user will be charged for each project. // ProjectCharges returns how much money current user will be charged for each project.
ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) ([]ProjectCharge, error) ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) ([]ProjectCharge, error)
// CheckProjectInvoicingStatus returns true if for the given project there are outstanding project records and/or usage // CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe). // which have not been applied/invoiced yet (meaning sent over to stripe).
CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) error
// CheckProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
CheckProjectUsageStatus(ctx context.Context, projectID uuid.UUID) error
// Charges returns list of all credit card charges related to account. // Charges returns list of all credit card charges related to account.
Charges(ctx context.Context, userID uuid.UUID) ([]Charge, error) Charges(ctx context.Context, userID uuid.UUID) ([]Charge, error)

View File

@ -5,10 +5,10 @@ package stripecoinpayments
import ( import (
"context" "context"
"errors"
"time" "time"
"github.com/stripe/stripe-go/v72" "github.com/stripe/stripe-go/v72"
"github.com/zeebo/errs"
"storj.io/common/uuid" "storj.io/common/uuid"
"storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments"
@ -147,50 +147,65 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID,
return charges, nil return charges, nil
} }
// CheckProjectInvoicingStatus returns true if for the given project there are outstanding project records and/or usage // CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe). // which have not been applied/invoiced yet (meaning sent over to stripe).
func (accounts *accounts) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) { func (accounts *accounts) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
// we do not want to delete projects that have usage for the current month.
year, month, _ := accounts.service.nowFn().UTC().Date() year, month, _ := accounts.service.nowFn().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
currentUsage, err := accounts.service.usageDB.GetProjectTotal(ctx, projectID, firstOfMonth, accounts.service.nowFn()) // Check if an invoice project record exists already
err = accounts.service.db.ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if errs.Is(err, ErrProjectRecordExists) {
record, err := accounts.service.db.ProjectRecords().Get(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if err != nil { if err != nil {
return false, err return err
}
if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.SegmentCount > 0 {
return true, errors.New("usage for current month exists")
}
// if usage of last month exist, make sure to look for billing records
lastMonthUsage, err := accounts.service.usageDB.GetProjectTotal(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
if err != nil {
return false, err
}
if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.SegmentCount > 0 {
// time passed into the check function need to be the UTC midnight dates of the first and last day of the month
err = accounts.service.db.ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour*24))
if errors.Is(err, ErrProjectRecordExists) {
record, err := accounts.service.db.ProjectRecords().Get(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour*24))
if err != nil {
return true, err
} }
// state = 0 means unapplied and not invoiced yet. // state = 0 means unapplied and not invoiced yet.
if record.State == 0 { if record.State == 0 {
return true, errors.New("unapplied project invoice record exist") return errs.New("unapplied project invoice record exist")
} }
// Record has been applied, so project can be deleted. // Record has been applied, so project can be deleted.
return false, nil return nil
} }
if err != nil { if err != nil {
return true, err return err
} }
return true, errors.New("usage for last month exist, but is not billed yet")
return nil
} }
return false, nil
// CheckProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
func (accounts *accounts) CheckProjectUsageStatus(ctx context.Context, projectID uuid.UUID) error {
var err error
defer mon.Task()(&ctx)(&err)
year, month, _ := accounts.service.nowFn().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
// check current month usage and do not allow deletion if usage exists
currentUsage, err := accounts.service.usageDB.GetProjectTotal(ctx, projectID, firstOfMonth, accounts.service.nowFn())
if err != nil {
return err
}
if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.SegmentCount > 0 {
return errs.New("usage for current month exists")
}
// check usage for last month, if exists, ensure we have an invoice item created.
lastMonthUsage, err := accounts.service.usageDB.GetProjectTotal(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
if err != nil {
return err
}
if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.SegmentCount > 0 {
err = accounts.service.db.ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if !errs.Is(err, ErrProjectRecordExists) {
return errs.New("usage for last month exist, but is not billed yet")
}
}
return nil
} }
// Charges returns list of all credit card charges related to account. // Charges returns list of all credit card charges related to account.