From 97e2ff47fbce179ee870a2d7a47268473687d8a0 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Tue, 17 Dec 2024 13:09:41 +0200 Subject: [PATCH 1/7] flat payment and swagger update --- Makefile | 4 - README.md | 1 + client/scripts/build-env.sh | 7 + client/scripts/build.sh | 6 + client/src/components/Toast.vue | 30 +- docker-compose.yml | 1 + docs/swagger.html | 53 - docs/user_stories.md | 7 +- server/Makefile | 7 +- server/README.md | 17 + server/app/admin_handler.go | 420 +- server/app/admin_handler_test.go | 27 +- server/app/app.go | 43 +- server/app/invoice_handler.go | 628 +++ server/app/k8s_handler.go | 174 +- server/app/notification_handler.go | 25 + server/app/payments_handler.go | 351 ++ server/app/quota_handler.go | 30 - server/app/quota_handler_test.go | 68 - server/app/setup.go | 12 +- server/app/stripe.go | 65 + server/app/unauth_handler.go | 71 + server/app/user_handler.go | 353 +- server/app/user_handler_test.go | 70 +- server/app/vm_handler.go | 131 +- server/app/voucher_handler.go | 112 +- server/deployer/deployer.go | 108 +- server/deployer/deployment_consumer.go | 28 +- server/deployer/k8s_deployer.go | 188 +- server/deployer/vms_deployer.go | 122 +- server/docs/docs.go | 3384 +++++++++++++++++ server/docs/swagger.yaml | 2244 +++++++++++ server/go.mod | 25 +- server/go.sum | 77 +- server/internal/config_parser.go | 11 + server/internal/email_sender.go | 9 +- server/internal/email_sender_test.go | 2 +- .../internal/templates/adminAnnouncement.html | 2 +- server/main.go | 8 + server/middlewares/admin_access.go | 2 +- server/middlewares/grafana_metrics.go | 6 +- server/models/api_inputs.go | 23 - server/models/card.go | 57 + server/models/database.go | 373 +- server/models/database_test.go | 107 +- server/models/deployments_count.go | 42 + server/models/invoice.go | 152 + server/models/k8s.go | 124 +- server/models/maintenance.go | 12 + server/models/nextlaunch.go | 12 + server/models/notification.go | 17 + server/models/quota.go | 9 - server/models/state.go | 31 + server/models/user.go | 119 +- server/models/vm.go | 106 +- server/models/voucher.go | 60 +- server/streams/types.go | 4 +- swagger.yml | 1182 ------ 58 files changed, 8897 insertions(+), 2462 deletions(-) delete mode 100644 docs/swagger.html create mode 100644 server/app/invoice_handler.go create mode 100644 server/app/payments_handler.go delete mode 100644 server/app/quota_handler.go delete mode 100644 server/app/quota_handler_test.go create mode 100644 server/app/stripe.go create mode 100644 server/app/unauth_handler.go create mode 100644 server/docs/docs.go create mode 100644 server/docs/swagger.yaml delete mode 100644 server/models/api_inputs.go create mode 100644 server/models/card.go create mode 100644 server/models/deployments_count.go create mode 100644 server/models/invoice.go delete mode 100644 server/models/quota.go create mode 100644 server/models/state.go delete mode 100644 swagger.yml diff --git a/Makefile b/Makefile index 5acb1ab1..3e3b7b70 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,4 @@ PWD := $(shell pwd) -swagger: - swagger validate swagger.yml - docker run -i yousan/swagger-yaml-to-html < swagger.yml > docs/swagger.html - run: docker compose up diff --git a/README.md b/README.md index 6763bd6d..f092addc 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ cloud4students aims to help students deploy their projects on Threefold Grid. - First create `config.json` check [configuration](#configuration) - Change `VITE_API_ENDPOINT` in docker-compose.yml to server api url for example `http://localhost:3000/v1` +- Change `STRIPE_PUBLISHER_KEY` in docker-compose.yml to your stripe publisher key (can get it from stripe dashboard) To build backend and frontend images diff --git a/client/scripts/build-env.sh b/client/scripts/build-env.sh index d7640682..6dcabadf 100755 --- a/client/scripts/build-env.sh +++ b/client/scripts/build-env.sh @@ -13,10 +13,17 @@ then exit 64 fi +if [ -z ${STRIPE_PUBLISHER_KEY+x} ] +then + echo 'Error! $STRIPE_PUBLISHER_KEY is required.' + exit 64 +fi + configs=" window.configs = window.configs || {}; window.configs.vite_app_endpoint = '$VITE_API_ENDPOINT'; +window.configs.stripe_publisher_key = '$STRIPE_PUBLISHER_KEY'; " if [ -e $file ] diff --git a/client/scripts/build.sh b/client/scripts/build.sh index e69b0d22..cce42fe7 100644 --- a/client/scripts/build.sh +++ b/client/scripts/build.sh @@ -12,10 +12,16 @@ then exit 64 fi +if [ -z ${STRIPE_PUBLISHER_KEY+x} ] +then + echo 'Error! $STRIPE_PUBLISHER_KEY is required.' + exit 64 +fi configs=" window.configs = window.configs || {}; window.configs.vite_app_endpoint = '$VITE_API_ENDPOINT'; +window.configs.stripe_publisher_key = '$STRIPE_PUBLISHER_KEY'; " if [ -e $file ] diff --git a/client/src/components/Toast.vue b/client/src/components/Toast.vue index e775f60e..5c7c8c2d 100644 --- a/client/src/components/Toast.vue +++ b/client/src/components/Toast.vue @@ -1,24 +1,24 @@ diff --git a/docker-compose.yml b/docker-compose.yml index 98b8a9c2..ad36f808 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: frontend: environment: - VITE_API_ENDPOINT=http://localhost:3000/v1 + - STRIPE_PUBLISHER_KEY="" build: context: client/. dockerfile: Dockerfile diff --git a/docs/swagger.html b/docs/swagger.html deleted file mode 100644 index ae00c0bd..00000000 --- a/docs/swagger.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - Swagger UI - - - - - -
- - - - - diff --git a/docs/user_stories.md b/docs/user_stories.md index 4b53d098..53964563 100644 --- a/docs/user_stories.md +++ b/docs/user_stories.md @@ -72,13 +72,12 @@ ## Scenario 8 - - As a user I expect to get all information about the voucher, used resources, and remaining quota + - As a user I expect to get all information about the voucher, used resources, and remaining balance ### Acceptance Criteria - - User should get all information about the voucher and its available resources (vms) - - Each user will have certain numbers of vms based on the voucher - - Each user should know how quota is calculated + - User should get all information about the voucher and its available balance + - Each user will have certain amount of money based on the voucher --- ## Scenario 9 diff --git a/server/Makefile b/server/Makefile index 3cc6786c..91d2ed23 100644 --- a/server/Makefile +++ b/server/Makefile @@ -7,7 +7,12 @@ build: @echo "Running $@" @go build -ldflags="-X 'github.com/codescalers/cloud4students/cmd.Commit=$(shell git rev-parse HEAD)'" -o bin/cloud4students main.go -run: build +swag: + @echo "Installing swag" && go install github.com/swaggo/swag/cmd/swag@latest + export PATH=${PATH}:${HOME}/go/bin + @swag init + +run: build swag @echo "Running $@" bin/cloud4students diff --git a/server/README.md b/server/README.md index f3e1a707..1dd6f193 100644 --- a/server/README.md +++ b/server/README.md @@ -69,3 +69,20 @@ make run ```bash docker run cloud4students ``` + +### Swagger + +- Install swag binary + +```bash +go install github.com/swaggo/swag/cmd/swag@latest +``` + +- Generate swagger docs + +```bash +swag init +``` + +- You can access swagger through `/swagger/index.html`. +- Example: if your port is `3000` and host is `localhost`, then you can access swagger using `http://localhost:3000/swagger/index.html` diff --git a/server/app/admin_handler.go b/server/app/admin_handler.go index 6a239b15..a0dc533a 100644 --- a/server/app/admin_handler.go +++ b/server/app/admin_handler.go @@ -45,7 +45,33 @@ type UpdateNextLaunchInput struct { Launched bool `json:"launched" binding:"required"` } +// SetPricesInput struct for setting prices as admins +type SetPricesInput struct { + Small float64 `json:"small"` + Medium float64 `json:"medium"` + Large float64 `json:"large"` + PublicIP float64 `json:"public_ip"` +} + +type ListDeploymentsResponse struct { + VMs []models.VM `json:"vms"` + K8S []models.K8sCluster `json:"k8s"` +} + // GetAllUsersHandler returns all users +// Example endpoint: List all users +// @Summary List all users +// @Description List all users in the system +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.User +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/all [get] func (a *App) GetAllUsersHandler(req *http.Request) (interface{}, Response) { users, err := a.db.ListAllUsers() if err == gorm.ErrRecordNotFound || len(users) == 0 { @@ -66,7 +92,103 @@ func (a *App) GetAllUsersHandler(req *http.Request) (interface{}, Response) { }, Ok() } +// GetAllInvoicesHandler returns all invoices +// Example endpoint: List all invoices +// @Summary List all invoices +// @Description List all invoices in the system +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.Invoice +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /invoice/all [get] +func (a *App) GetAllInvoicesHandler(req *http.Request) (interface{}, Response) { + invoices, err := a.db.ListInvoices() + if err == gorm.ErrRecordNotFound || len(invoices) == 0 { + return ResponseMsg{ + Message: "Invoices are not found", + Data: invoices, + }, Ok() + } + + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Invoices are found", + Data: invoices, + }, Ok() +} + +// SetPricesHandler set prices for vms and public ip +// Example endpoint: Set prices +// @Summary Set prices +// @Description Set vms and public ips prices prices +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param prices body SetPricesInput true "Prices to be set" +// @Success 200 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Router /set_prices [put] +func (a *App) SetPricesHandler(req *http.Request) (interface{}, Response) { + var input SetPricesInput + err := json.NewDecoder(req.Body).Decode(&input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read input data")) + } + + err = validator.Validate(input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("invalid input data")) + } + + if input.Small != 0 { + a.config.PricesPerMonth.SmallVM = input.Small + } + + if input.Medium != 0 { + a.config.PricesPerMonth.MediumVM = input.Medium + } + + if input.Large != 0 { + a.config.PricesPerMonth.LargeVM = input.Large + } + + if input.PublicIP != 0 { + a.config.PricesPerMonth.PublicIP = input.PublicIP + } + + return ResponseMsg{ + Message: "New prices are set", + Data: nil, + }, Ok() +} + // GetDlsCountHandler returns deployments count +// Example endpoint: Get users' deployments count +// @Summary Get users' deployments count +// @Description Get users' deployments count in the system +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.DeploymentsCount +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /deployments/count [get] func (a *App) GetDlsCountHandler(req *http.Request) (interface{}, Response) { count, err := a.db.CountAllDeployments() if err == gorm.ErrRecordNotFound { @@ -88,6 +210,18 @@ func (a *App) GetDlsCountHandler(req *http.Request) (interface{}, Response) { } // GetBalanceHandler return account balance information +// Example endpoint: Get main TF account balance +// @Summary Get main TF account balance +// @Description Get main TF account balance +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} float64 +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /balance [get] func (a *App) GetBalanceHandler(req *http.Request) (interface{}, Response) { balance, err := a.deployer.GetBalance() if err != nil { @@ -101,29 +235,21 @@ func (a *App) GetBalanceHandler(req *http.Request) (interface{}, Response) { }, Ok() } -func (a *App) ResetUsersQuota(req *http.Request) (interface{}, Response) { - users, err := a.db.ListAllUsers() - if err == gorm.ErrRecordNotFound || len(users) == 0 { - return ResponseMsg{ - Message: "Users are not found", - }, Ok() - } - - for _, user := range users { - err = a.db.UpdateUserQuota(user.UserID, 0, 0) - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - } - - return ResponseMsg{ - Message: "Quota is reset successfully", - }, Ok() -} - -// DeleteAllDeployments deletes all deployments -func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) { +// DeleteAllDeploymentsHandler deletes all users' deployments +// Example endpoint: Deletes all users' deployments +// @Summary Deletes all users' deployments +// @Description Deletes all users' deployments +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /deployments [delete] +func (a *App) DeleteAllDeploymentsHandler(req *http.Request) (interface{}, Response) { users, err := a.db.ListAllUsers() if err == gorm.ErrRecordNotFound || len(users) == 0 { return ResponseMsg{ @@ -138,9 +264,9 @@ func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) { for _, user := range users { // vms - vms, err := a.db.GetAllVms(user.UserID) + vms, err := a.db.GetAllVms(user.ID.String()) if err == gorm.ErrRecordNotFound || len(vms) == 0 { - log.Error().Err(err).Str("userID", user.UserID).Msg("Virtual machines are not found") + log.Error().Err(err).Str("userID", user.ID.String()).Msg("Virtual machines are not found") continue } if err != nil { @@ -156,16 +282,16 @@ func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) { } } - err = a.db.DeleteAllVms(user.UserID) + err = a.db.DeleteAllVms(user.ID.String()) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } // k8s clusters - clusters, err := a.db.GetAllK8s(user.UserID) + clusters, err := a.db.GetAllK8s(user.ID.String()) if err == gorm.ErrRecordNotFound || len(clusters) == 0 { - log.Error().Err(err).Str("userID", user.UserID).Msg("Kubernetes clusters are not found") + log.Error().Err(err).Str("userID", user.ID.String()).Msg("Kubernetes clusters are not found") continue } if err != nil { @@ -181,7 +307,7 @@ func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) { } } - err = a.db.DeleteAllK8s(user.UserID) + err = a.db.DeleteAllK8s(user.ID.String()) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -193,8 +319,21 @@ func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) { }, Ok() } -// ListDeployments lists all deployments -func (a *App) ListDeployments(req *http.Request) (interface{}, Response) { +// ListDeploymentsHandler lists all users' deployments +// Example endpoint: List all users' deployments +// @Summary List all users' deployments +// @Description List all users' deployments +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} ListDeploymentsResponse +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /deployments [get] +func (a *App) ListDeploymentsHandler(req *http.Request) (interface{}, Response) { users, err := a.db.ListAllUsers() if err == gorm.ErrRecordNotFound || len(users) == 0 { return ResponseMsg{ @@ -212,9 +351,9 @@ func (a *App) ListDeployments(req *http.Request) (interface{}, Response) { for _, user := range users { // vms - vms, err := a.db.GetAllVms(user.UserID) + vms, err := a.db.GetAllVms(user.ID.String()) if err == gorm.ErrRecordNotFound || len(vms) == 0 { - log.Error().Err(err).Str("userID", user.UserID).Msg("Virtual machines are not found") + log.Error().Err(err).Str("userID", user.ID.String()).Msg("Virtual machines are not found") continue } if err != nil { @@ -225,9 +364,9 @@ func (a *App) ListDeployments(req *http.Request) (interface{}, Response) { allVMs = append(allVMs, vms...) // k8s clusters - clusters, err := a.db.GetAllK8s(user.UserID) + clusters, err := a.db.GetAllK8s(user.ID.String()) if err == gorm.ErrRecordNotFound || len(clusters) == 0 { - log.Error().Err(err).Str("userID", user.UserID).Msg("Kubernetes clusters are not found") + log.Error().Err(err).Str("userID", user.ID.String()).Msg("Kubernetes clusters are not found") continue } if err != nil { @@ -240,11 +379,25 @@ func (a *App) ListDeployments(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "Deployments are listed successfully", - Data: map[string]interface{}{"vms": allVMs, "k8s": allClusters}, + Data: ListDeploymentsResponse{VMs: allVMs, K8S: allClusters}, }, Ok() } // UpdateMaintenanceHandler updates maintenance flag +// Example endpoint: Updates maintenance flag +// @Summary Updates maintenance flag +// @Description Updates maintenance flag +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param maintenance body UpdateMaintenanceInput true "Maintenance value to be set" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /maintenance [put] func (a *App) UpdateMaintenanceHandler(req *http.Request) (interface{}, Response) { var input UpdateMaintenanceInput err := json.NewDecoder(req.Body).Decode(&input) @@ -269,26 +422,22 @@ func (a *App) UpdateMaintenanceHandler(req *http.Request) (interface{}, Response }, Ok() } -// GetMaintenanceHandler updates maintenance flag -func (a *App) GetMaintenanceHandler(req *http.Request) (interface{}, Response) { - maintenance, err := a.db.GetMaintenance() - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("maintenance is not found")) - } - - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - return ResponseMsg{ - Message: fmt.Sprintf("Maintenance is set with %v", maintenance.Active), - Data: maintenance, - }, Ok() -} - -// SetAdmin sets a user as an admin -func (a *App) SetAdmin(req *http.Request) (interface{}, Response) { +// SetAdminHandler sets a user as an admin +// Example endpoint: Sets a user as an admin +// @Summary Sets a user as an admin +// @Description Sets a user as an admin +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param setAdmin body SetAdminInput true "User to be set as admin" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /set_admin [put] +func (a *App) SetAdminHandler(req *http.Request) (interface{}, Response) { input := SetAdminInput{} err := json.NewDecoder(req.Body).Decode(&input) if err != nil { @@ -333,55 +482,22 @@ func (a *App) SetAdmin(req *http.Request) (interface{}, Response) { }, Ok() } -// NotifyAdmins is used to notify admins that there are new vouchers requests -func (a *App) notifyAdmins() { - ticker := time.NewTicker(time.Hour * time.Duration(a.config.NotifyAdminsIntervalHours)) - - for range ticker.C { - // get admins - admins, err := a.db.ListAdmins() - if err != nil { - log.Error().Err(err).Send() - } - - // check pending voucher requests - pending, err := a.db.GetAllPendingVouchers() - if err != nil { - log.Error().Err(err).Send() - } - - if len(pending) > 0 { - subject, body := internal.NotifyAdminsMailContent(len(pending), a.config.Server.Host) - - for _, admin := range admins { - err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body) - if err != nil { - log.Error().Err(err).Send() - } - } - } - - // check account balance - balance, err := a.deployer.GetBalance() - if err != nil { - log.Error().Err(err).Send() - } - - if int(balance) < a.config.BalanceThreshold { - subject, body := internal.NotifyAdminsMailLowBalanceContent(balance, a.config.Server.Host) - - for _, admin := range admins { - err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body) - if err != nil { - log.Error().Err(err).Send() - } - } - } - } -} - -// CreateNewAnnouncement creates a new administrator announcement and sends it to all users as an email and notification -func (a *App) CreateNewAnnouncement(req *http.Request) (interface{}, Response) { +// CreateNewAnnouncementHandler creates a new administrator announcement and sends it to all users as an email and notification +// Example endpoint: Creates a new administrator announcement and sends it to all users as an email and notification +// @Summary Creates a new administrator announcement and sends it to all users as an email and notification +// @Description Creates a new administrator announcement and sends it to all users as an email and notification +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param announcement body AdminAnnouncement true "announcement to be created" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /announcement [post] +func (a *App) CreateNewAnnouncementHandler(req *http.Request) (interface{}, Response) { var adminAnnouncement AdminAnnouncement err := json.NewDecoder(req.Body).Decode(&adminAnnouncement) @@ -405,7 +521,7 @@ func (a *App) CreateNewAnnouncement(req *http.Request) (interface{}, Response) { } for _, user := range users { - subject, body := internal.AdminAnnouncementMailContent(adminAnnouncement.Subject, adminAnnouncement.Body, a.config.Server.Host, user.Name) + subject, body := internal.AdminAnnouncementMailContent(adminAnnouncement.Subject, adminAnnouncement.Body, a.config.Server.Host, user.Name()) err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body) if err != nil { @@ -413,7 +529,7 @@ func (a *App) CreateNewAnnouncement(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - notification := models.Notification{UserID: user.UserID, Msg: fmt.Sprintf("Announcement: %s", adminAnnouncement.Body)} + notification := models.Notification{UserID: user.ID.String(), Msg: fmt.Sprintf("Announcement: %s", adminAnnouncement.Body)} err = a.db.CreateNotification(¬ification) if err != nil { log.Error().Err(err).Send() @@ -426,8 +542,22 @@ func (a *App) CreateNewAnnouncement(req *http.Request) (interface{}, Response) { }, Created() } -// SendEmail creates a new administrator email and sends it to a specific user as an email and notification -func (a *App) SendEmail(req *http.Request) (interface{}, Response) { +// SendEmailHandler creates a new administrator email and sends it to a specific user as an email and notification +// Example endpoint: Creates a new administrator email and sends it to a specific user as an email and notification +// @Summary Creates a new administrator email and sends it to a specific user as an email and notification +// @Description Creates a new administrator email and sends it to a specific user as an email and notification +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param email body EmailUser true "email to be sent" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /announcement [post] +func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) { var emailUser EmailUser err := json.NewDecoder(req.Body).Decode(&emailUser) @@ -453,7 +583,7 @@ func (a *App) SendEmail(req *http.Request) (interface{}, Response) { return nil, BadRequest(errors.New("failed to get user")) } - subject, body := internal.AdminMailContent(emailUser.Subject, emailUser.Body, a.config.Server.Host, user.Name) + subject, body := internal.AdminMailContent(fmt.Sprintf("Hey! 📢 %s", emailUser.Subject), emailUser.Body, a.config.Server.Host, user.Name()) err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body) if err != nil { @@ -474,6 +604,20 @@ func (a *App) SendEmail(req *http.Request) (interface{}, Response) { } // UpdateNextLaunchHandler updates next launch flag +// Example endpoint: Updates next launch flag +// @Summary Updates next launch flag +// @Description Updates next launch flag +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param nextlaunch body UpdateNextLaunchInput true "Next launch value to be set" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /nextlaunch [put] func (a *App) UpdateNextLaunchHandler(req *http.Request) (interface{}, Response) { var input UpdateNextLaunchInput err := json.NewDecoder(req.Body).Decode(&input) @@ -498,21 +642,49 @@ func (a *App) UpdateNextLaunchHandler(req *http.Request) (interface{}, Response) }, Ok() } -// GetNextLaunchHandler returns next launch state -func (a *App) GetNextLaunchHandler(req *http.Request) (interface{}, Response) { - nextlaunch, err := a.db.GetNextLaunch() +// NotifyAdmins is used to notify admins that there are new vouchers requests +func (a *App) notifyAdmins() { + ticker := time.NewTicker(time.Hour * time.Duration(a.config.NotifyAdminsIntervalHours)) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("next launch is not found")) - } + for range ticker.C { + // get admins + admins, err := a.db.ListAdmins() + if err != nil { + log.Error().Err(err).Send() + } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } + // check pending voucher requests + pending, err := a.db.GetAllPendingVouchers() + if err != nil { + log.Error().Err(err).Send() + } - return ResponseMsg{ - Message: fmt.Sprintf("Next Launch is Launched with state: %v", nextlaunch.Launched), - Data: nextlaunch, - }, Ok() + if len(pending) > 0 { + subject, body := internal.NotifyAdminsMailContent(len(pending), a.config.Server.Host) + + for _, admin := range admins { + err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body) + if err != nil { + log.Error().Err(err).Send() + } + } + } + + // check account balance + balance, err := a.deployer.GetBalance() + if err != nil { + log.Error().Err(err).Send() + } + + if int(balance) < a.config.BalanceThreshold { + subject, body := internal.NotifyAdminsMailLowBalanceContent(balance, a.config.Server.Host) + + for _, admin := range admins { + err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, admin.Email, subject, body) + if err != nil { + log.Error().Err(err).Send() + } + } + } + } } diff --git a/server/app/admin_handler_test.go b/server/app/admin_handler_test.go index a21180be..68a095c4 100644 --- a/server/app/admin_handler_test.go +++ b/server/app/admin_handler_test.go @@ -16,10 +16,10 @@ func TestGetAllUsersHandler(t *testing.T) { app := SetUp(t) admin := models.User{ - Name: "admin", - Email: "admin@gmail.com", - Verified: true, - Admin: true, + FirstName: "admin", + Email: "admin@gmail.com", + Verified: true, + Admin: true, } err := app.db.CreateUser(&admin) assert.NoError(t, err) @@ -49,9 +49,10 @@ func TestGetAllUsersHandler(t *testing.T) { t.Run("Get all users: not admin", func(t *testing.T) { u := models.User{ - Name: "name", - Email: "name@gmail.com", - Verified: true, + FirstName: "name", + LastName: "last", + Email: "name@gmail.com", + Verified: true, } err := app.db.CreateUser(&u) assert.NoError(t, err) @@ -75,7 +76,7 @@ func TestGetAllUsersHandler(t *testing.T) { } response := adminHandler(req) - want := `{"err":"user 'name' doesn't have an admin access"}` + "\n" + want := fmt.Sprintf(`{"err":"user '%s %s' doesn't have an admin access"}`, u.FirstName, u.LastName) + "\n" assert.Equal(t, response.Body.String(), want) assert.Equal(t, response.Code, http.StatusUnauthorized) }) @@ -209,10 +210,10 @@ func TestGetAllUsersHandler(t *testing.T) { func TestCreateNewAnnouncement(t *testing.T) { app := SetUp(t) admin := models.User{ - Name: "admin", - Email: "admin@gmail.com", - Verified: true, - Admin: true, + FirstName: "admin", + Email: "admin@gmail.com", + Verified: true, + Admin: true, } err := app.db.CreateUser(&admin) assert.NoError(t, err) @@ -230,7 +231,7 @@ func TestCreateNewAnnouncement(t *testing.T) { req := authHandlerConfig{ unAuthHandlerConfig: unAuthHandlerConfig{ body: bytes.NewBuffer(adminAnnouncement), - handlerFunc: app.CreateNewAnnouncement, + handlerFunc: app.CreateNewAnnouncementHandler, api: fmt.Sprintf("/%s/announcement", app.config.Version), }, userID: user.ID.String(), diff --git a/server/app/app.go b/server/app/app.go index ff1da7d9..953e96ea 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -6,6 +6,7 @@ import ( "net/http" c4sDeployer "github.com/codescalers/cloud4students/deployer" + _ "github.com/codescalers/cloud4students/docs" "github.com/codescalers/cloud4students/internal" "github.com/codescalers/cloud4students/middlewares" "github.com/codescalers/cloud4students/models" @@ -13,6 +14,8 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stripe/stripe-go/v81" + httpSwagger "github.com/swaggo/http-swagger" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" ) @@ -32,6 +35,8 @@ func NewApp(ctx context.Context, configFile string) (app *App, err error) { return } + stripe.Key = config.StripeSecret + db := models.NewDB() err = db.Connect(config.Database.File) if err != nil { @@ -55,7 +60,7 @@ func NewApp(ctx context.Context, configFile string) (app *App, err error) { return } - newDeployer, err := c4sDeployer.NewDeployer(db, redis, tfPluginClient) + newDeployer, err := c4sDeployer.NewDeployer(db, redis, tfPluginClient, config.PricesPerMonth) if err != nil { return } @@ -83,6 +88,10 @@ func (a *App) startBackgroundWorkers(ctx context.Context) { // notify admins go a.notifyAdmins() + // Invoices + go a.monthlyInvoices() + go a.sendRemindersToPayInvoices() + // periodic deployments go a.deployer.PeriodicRequests(ctx, substrateBlockDiffInSeconds) go a.deployer.PeriodicDeploy(ctx, substrateBlockDiffInSeconds) @@ -95,6 +104,9 @@ func (a *App) startBackgroundWorkers(ctx context.Context) { func (a *App) registerHandlers() { r := mux.NewRouter() + // Setup Swagger UI route + r.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) + // version router versionRouter := r.PathPrefix("/" + a.config.Version).Subrouter() authRouter := versionRouter.NewRoute().Subrouter() @@ -102,7 +114,8 @@ func (a *App) registerHandlers() { // sub routes with authorization userRouter := authRouter.PathPrefix("/user").Subrouter() - quotaRouter := authRouter.PathPrefix("/quota").Subrouter() + invoiceRouter := authRouter.PathPrefix("/invoice").Subrouter() + cardRouter := userRouter.PathPrefix("/card").Subrouter() notificationRouter := authRouter.PathPrefix("/notification").Subrouter() vmRouter := authRouter.PathPrefix("/vm").Subrouter() k8sRouter := authRouter.PathPrefix("/k8s").Subrouter() @@ -131,8 +144,16 @@ func (a *App) registerHandlers() { userRouter.HandleFunc("", WrapFunc(a.GetUserHandler)).Methods("GET", "OPTIONS") userRouter.HandleFunc("/apply_voucher", WrapFunc(a.ApplyForVoucherHandler)).Methods("POST", "OPTIONS") userRouter.HandleFunc("/activate_voucher", WrapFunc(a.ActivateVoucherHandler)).Methods("PUT", "OPTIONS") + userRouter.HandleFunc("/charge_balance", WrapFunc(a.ChargeBalance)).Methods("PUT", "OPTIONS") + + cardRouter.HandleFunc("", WrapFunc(a.AddCardHandler)).Methods("POST", "OPTIONS") + cardRouter.HandleFunc("/{id}", WrapFunc(a.DeleteCardHandler)).Methods("DELETE", "OPTIONS") + cardRouter.HandleFunc("", WrapFunc(a.ListCardHandler)).Methods("GET", "OPTIONS") + cardRouter.HandleFunc("/default", WrapFunc(a.SetDefaultCardHandler)).Methods("PUT", "OPTIONS") - quotaRouter.HandleFunc("", WrapFunc(a.GetQuotaHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("/pay/{id}", WrapFunc(a.PayInvoiceHandler)).Methods("PUT", "OPTIONS") notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS") notificationRouter.HandleFunc("/{id}", WrapFunc(a.UpdateNotificationsHandler)).Methods("PUT", "OPTIONS") @@ -156,21 +177,23 @@ func (a *App) registerHandlers() { // ADMIN ACCESS adminRouter.HandleFunc("/user/all", WrapFunc(a.GetAllUsersHandler)).Methods("GET", "OPTIONS") - adminRouter.HandleFunc("/quota/reset", WrapFunc(a.ResetUsersQuota)).Methods("PUT", "OPTIONS") - adminRouter.HandleFunc("/deployment/count", WrapFunc(a.GetDlsCountHandler)).Methods("GET", "OPTIONS") - adminRouter.HandleFunc("/announcement", WrapFunc(a.CreateNewAnnouncement)).Methods("POST", "OPTIONS") - adminRouter.HandleFunc("/email", WrapFunc(a.SendEmail)).Methods("POST", "OPTIONS") - adminRouter.HandleFunc("/set_admin", WrapFunc(a.SetAdmin)).Methods("PUT", "OPTIONS") + adminRouter.HandleFunc("/invoice/all", WrapFunc(a.GetAllInvoicesHandler)).Methods("GET", "OPTIONS") + adminRouter.HandleFunc("/announcement", WrapFunc(a.CreateNewAnnouncementHandler)).Methods("POST", "OPTIONS") + adminRouter.HandleFunc("/email", WrapFunc(a.SendEmailHandler)).Methods("POST", "OPTIONS") + adminRouter.HandleFunc("/set_admin", WrapFunc(a.SetAdminHandler)).Methods("PUT", "OPTIONS") + adminRouter.HandleFunc("/set_prices", WrapFunc(a.SetPricesHandler)).Methods("PUT", "OPTIONS") balanceRouter.HandleFunc("", WrapFunc(a.GetBalanceHandler)).Methods("GET", "OPTIONS") maintenanceRouter.HandleFunc("", WrapFunc(a.UpdateMaintenanceHandler)).Methods("PUT", "OPTIONS") - deploymentsRouter.HandleFunc("", WrapFunc(a.DeleteAllDeployments)).Methods("DELETE", "OPTIONS") - deploymentsRouter.HandleFunc("", WrapFunc(a.ListDeployments)).Methods("GET", "OPTIONS") + deploymentsRouter.HandleFunc("", WrapFunc(a.DeleteAllDeploymentsHandler)).Methods("DELETE", "OPTIONS") + deploymentsRouter.HandleFunc("", WrapFunc(a.ListDeploymentsHandler)).Methods("GET", "OPTIONS") + deploymentsRouter.HandleFunc("/count", WrapFunc(a.GetDlsCountHandler)).Methods("GET", "OPTIONS") nextLaunchRouter.HandleFunc("", WrapFunc(a.UpdateNextLaunchHandler)).Methods("PUT", "OPTIONS") voucherRouter.HandleFunc("", WrapFunc(a.GenerateVoucherHandler)).Methods("POST", "OPTIONS") voucherRouter.HandleFunc("", WrapFunc(a.ListVouchersHandler)).Methods("GET", "OPTIONS") voucherRouter.HandleFunc("/{id}", WrapFunc(a.UpdateVoucherHandler)).Methods("PUT", "OPTIONS") voucherRouter.HandleFunc("", WrapFunc(a.ApproveAllVouchersHandler)).Methods("PUT", "OPTIONS") + voucherRouter.HandleFunc("/reset", WrapFunc(a.ResetUsersVoucherBalanceHandler)).Methods("PUT", "OPTIONS") // middlewares r.Use(middlewares.LoggingMW) diff --git a/server/app/invoice_handler.go b/server/app/invoice_handler.go new file mode 100644 index 00000000..35a90fb8 --- /dev/null +++ b/server/app/invoice_handler.go @@ -0,0 +1,628 @@ +package app + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/codescalers/cloud4students/deployer" + "github.com/codescalers/cloud4students/internal" + "github.com/codescalers/cloud4students/middlewares" + "github.com/codescalers/cloud4students/models" + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "gopkg.in/validator.v2" + "gorm.io/gorm" +) + +type method string + +const ( + card method = "card" + balance method = "balance" + voucher method = "voucher" + voucherAndBalance method = "voucher+balance" + voucherAndCard method = "voucher+card" + balanceAndCard method = "balance+card" + voucherAndBalanceAndCard method = "voucher+balance+card" +) + +var methods = []method{ + card, balance, voucher, + voucherAndBalance, voucherAndCard, balanceAndCard, + voucherAndBalanceAndCard, +} + +type PayInvoiceInput struct { + Method method `json:"method" binding:"required"` + CardPaymentID string `json:"card_payment_id"` +} + +// ListInvoicesHandler lists user's invoices +// Example endpoint: Lists user's invoices +// @Summary Lists user's invoices +// @Description Lists user's invoices +// @Tags Invoice +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.Invoice +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /invoice [get] +func (a *App) ListInvoicesHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + invoices, err := a.db.ListUserInvoices(userID) + if err == gorm.ErrRecordNotFound || len(invoices) == 0 { + return ResponseMsg{ + Message: "no invoices found", + Data: invoices, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Invoices are found", + Data: invoices, + }, Ok() +} + +// GetInvoiceHandler gets user's invoice by ID +// Example endpoint: Gets user's invoice by ID +// @Summary Gets user's invoice by ID +// @Description Gets user's invoice by ID +// @Tags Invoice +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Invoice ID" +// @Success 200 {object} models.Invoice +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /invoice/{id} [get] +func (a *App) GetInvoiceHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + id, err := strconv.Atoi(mux.Vars(req)["id"]) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read invoice id")) + } + + invoice, err := a.db.GetInvoice(id) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("invoice is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if userID != invoice.UserID { + return nil, NotFound(errors.New("invoice is not found")) + } + + return ResponseMsg{ + Message: "Invoice exists", + Data: invoice, + }, Ok() +} + +// PayInvoiceHandler pay user's invoice +// Example endpoint: Pay user's invoice +// @Summary Pay user's invoice +// @Description Pay user's invoice +// @Tags Invoice +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Invoice ID" +// @Param payment body PayInvoiceInput true "Payment method and ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /invoice/pay/{id} [put] +func (a *App) PayInvoiceHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + id, err := strconv.Atoi(mux.Vars(req)["id"]) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to invoice card id")) + } + + var input PayInvoiceInput + err = json.NewDecoder(req.Body).Decode(&input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read input data")) + } + + err = validator.Validate(input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("invalid input data")) + } + + invoice, err := a.db.GetInvoice(id) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("invoice is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if userID != invoice.UserID { + return nil, NotFound(errors.New("invoice is not found")) + } + + user, err := a.db.GetUserByID(userID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + var paymentDetails models.PaymentDetails + + switch input.Method { + case card: + _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) + } + + paymentDetails = models.PaymentDetails{Card: invoice.Total} + + case balance: + if user.Balance < invoice.Total { + return nil, BadRequest(errors.New("balance is not enough to pay the invoice")) + } + + paymentDetails = models.PaymentDetails{Balance: invoice.Total} + + user.Balance -= invoice.Total + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + case voucher: + if user.VoucherBalance < invoice.Total { + return nil, BadRequest(errors.New("voucher balance is not enough to pay the invoice")) + } + + paymentDetails = models.PaymentDetails{VoucherBalance: invoice.Total} + + user.VoucherBalance -= invoice.Total + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + case voucherAndBalance: + if user.VoucherBalance+user.Balance < invoice.Total { + return nil, BadRequest(errors.New("voucher balance and balance are not enough to pay the invoice")) + } + + if user.VoucherBalance > invoice.Total { + paymentDetails = models.PaymentDetails{VoucherBalance: invoice.Total} + user.VoucherBalance -= invoice.Total + } else { + paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Balance: (invoice.Total - user.VoucherBalance)} + user.Balance = (invoice.Total - user.VoucherBalance) + user.VoucherBalance = 0 + } + + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + case voucherAndCard: + if user.VoucherBalance > invoice.Total { + paymentDetails = models.PaymentDetails{VoucherBalance: invoice.Total} + user.VoucherBalance -= invoice.Total + } else { + paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Card: (invoice.Total - user.VoucherBalance)} + _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total-user.VoucherBalance) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) + } + user.VoucherBalance = 0 + } + + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + case balanceAndCard: + if user.Balance > invoice.Total { + paymentDetails = models.PaymentDetails{Balance: invoice.Total} + user.Balance -= invoice.Total + } else { + _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total-user.Balance) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) + } + paymentDetails = models.PaymentDetails{Balance: user.Balance, Card: (invoice.Total - user.Balance)} + user.Balance = 0 + } + + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + case voucherAndBalanceAndCard: + if user.VoucherBalance > invoice.Total { + paymentDetails = models.PaymentDetails{Balance: invoice.Total} + user.VoucherBalance -= invoice.Total + } else if user.Balance+user.VoucherBalance > invoice.Total { + paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Balance: (invoice.Total - user.VoucherBalance)} + user.Balance = (invoice.Total - user.VoucherBalance) + user.VoucherBalance = 0 + } else { + _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total-user.VoucherBalance-user.Balance) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) + } + paymentDetails = models.PaymentDetails{ + Balance: user.Balance, VoucherBalance: user.VoucherBalance, + Card: (invoice.Total - user.Balance - user.VoucherBalance), + } + user.VoucherBalance = 0 + user.Balance = 0 + } + + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + default: + return nil, BadRequest(fmt.Errorf("invalid payment method, only methods allowed %v", methods)) + } + + err = a.db.PayInvoice(id, paymentDetails) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("invoice is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Invoice is paid successfully", + Data: nil, + }, Ok() +} + +func (a *App) monthlyInvoices() { + for { + now := time.Now() + monthLastDay := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).AddDate(0, 0, -1) + timeTilLast := monthLastDay.Sub(now) + + if now.Day() != monthLastDay.Day() { + // Wait until the last day of the month + time.Sleep(timeTilLast) + } + + users, err := a.db.ListAllUsers() + if err == gorm.ErrRecordNotFound || len(users) == 0 { + log.Error().Err(err).Msg("Users are not found") + } + + if err != nil { + log.Error().Err(err).Send() + } + + // TODO: what if routine is killed + // Create invoices for all system users + for _, user := range users { + // 1. Create new monthly invoice + if err = a.createInvoice(user.ID.String(), now); err != nil { + log.Error().Err(err).Send() + } + + // 2. Use balance/voucher balance to pay invoices + user.Balance, user.VoucherBalance, err = a.db.PayUserInvoices(user.ID.String(), user.Balance, user.VoucherBalance) + if err != nil { + log.Error().Err(err).Send() + } else { + if err = a.db.UpdateUserByID(user); err != nil { + log.Error().Err(err).Send() + } + } + + // 3. Use cards to pay invoices + if err = a.payUserInvoicesUsingCards(user.ID.String(), user.StripeCustomerID, user.StripeDefaultPaymentID, true); err != nil { + log.Error().Err(err).Send() + } + + // 4. Delete expired deployments with invoices not paid for more than 3 months + if err = a.deleteInvoiceDeploymentsNotPaidSince3Months(user.ID.String(), now); err != nil { + log.Error().Err(err).Send() + } + } + + // Calculate the next last day of the month + nextMonthLastDay := time.Date(now.Year(), now.Month()+2, 1, 0, 0, 0, 0, now.Location()).AddDate(0, 0, -1) + timeTilNextLast := nextMonthLastDay.Sub(now) + + // Wait until the last day of the next month + time.Sleep(timeTilNextLast) + } +} + +func (a *App) createInvoice(userID string, now time.Time) error { + usagePercentageInMonth := deployer.UsagePercentageInMonth(now) + firstDayOfMonth := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local) + + vms, err := a.db.GetAllVms(userID) + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + + k8s, err := a.db.GetAllK8s(userID) + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + + var items []models.DeploymentItem + var total float64 + + for _, vm := range vms { + cost := float64(vm.PricePerMonth) * usagePercentageInMonth + + items = append(items, models.DeploymentItem{ + DeploymentResources: vm.Resources, + DeploymentType: "vm", + DeploymentID: vm.ID, + HasPublicIP: vm.Public, + PeriodInHours: time.Since(firstDayOfMonth).Hours(), + Cost: cost, + }) + + total += cost + } + + for _, cluster := range k8s { + cost := float64(cluster.PricePerMonth) * usagePercentageInMonth + + items = append(items, models.DeploymentItem{ + DeploymentResources: cluster.Master.Resources, + DeploymentType: "k8s", + DeploymentID: cluster.ID, + HasPublicIP: cluster.Master.Public, + PeriodInHours: time.Since(firstDayOfMonth).Hours(), + Cost: cost, + }) + + total += cost + } + + if err = a.db.CreateInvoice(&models.Invoice{ + UserID: userID, + Total: total, + Deployments: items, + }); err != nil { + return err + } + + return nil +} + +// payUserInvoicesUsingCards tries to pay invoices with user cards +func (a *App) payUserInvoicesUsingCards(userID, customerID, defaultPaymentMethod string, useOtherCards bool) error { + // get unpaid invoices + invoices, err := a.db.ListUnpaidInvoices(userID) + if err != nil { + return err + } + + cards, err := a.db.GetUserCards(userID) + if err != nil { + return err + } + + for _, invoice := range invoices { + // 1. use default payment method + if len(defaultPaymentMethod) != 0 { + _, err := createPaymentIntent(customerID, defaultPaymentMethod, a.config.Currency, invoice.Total) + if err != nil { + log.Error().Err(err).Send() + } else { + if err := a.db.PayInvoice(invoice.ID, models.PaymentDetails{Card: invoice.Total}); err != nil { + log.Error().Err(err).Send() + } + continue + } + } + + if !useOtherCards { + continue + } + + // 2. check other user cards + for _, card := range cards { + if defaultPaymentMethod != card.PaymentMethodID { + _, err := createPaymentIntent(customerID, card.PaymentMethodID, a.config.Currency, invoice.Total) + if err != nil { + log.Error().Err(err).Send() + } else { + if err := a.db.PayInvoice(invoice.ID, models.PaymentDetails{Card: invoice.Total}); err != nil { + log.Error().Err(err).Send() + } + break + } + } + } + } + + return nil +} + +func (a *App) deleteInvoiceDeploymentsNotPaidSince3Months(userID string, now time.Time) error { + invoices, err := a.db.ListUnpaidInvoices(userID) + if err != nil { + return err + } + + for _, invoice := range invoices { + threeMonthsAgo := now.AddDate(0, -3, 0) + + // check if the invoice created 3 months ago (not after it) and not paid + if !invoice.CreatedAt.After(threeMonthsAgo) && !invoice.Paid { + for _, dl := range invoice.Deployments { + if dl.DeploymentType == "vm" { + if err = a.db.DeleteVMByID(dl.DeploymentID); err != nil { + log.Error().Err(err).Send() + } + } + + if dl.DeploymentType == "k8s" { + if err = a.db.DeleteK8s(dl.DeploymentID); err != nil { + log.Error().Err(err).Send() + } + } + } + } + } + + return nil +} + +func (a *App) sendRemindersToPayInvoices() { + ticker := time.NewTicker(time.Hour * 24) + + for range ticker.C { + now := time.Now() + + users, err := a.db.ListAllUsers() + if err != nil { + log.Error().Err(err).Send() + } + + for _, u := range users { + if err = a.sendInvoiceReminderToUser(u.ID.String(), u.Email, u.Name(), now); err != nil { + log.Error().Err(err).Send() + } + } + } +} + +func (a *App) sendInvoiceReminderToUser(userID, userEmail, userName string, now time.Time) error { + invoices, err := a.db.ListUnpaidInvoices(userID) + if err != nil { + return err + } + + currencyName, err := getCurrencyName(a.config.Currency) + if err != nil { + return err + } + + for _, invoice := range invoices { + oneMonthsAgo := now.AddDate(0, -1, 0) + oneWeekAgo := now.AddDate(0, 0, -7) + + // check if the invoice created 1 months ago (not after it) and + // last remainder sent for this invoice was 7 days ago and + // invoice is not paid + if invoice.CreatedAt.Before(oneMonthsAgo) && + invoice.LastReminderAt.Before(oneWeekAgo) && + !invoice.Paid { + // overdue date starts after one month since invoice creation + overDueStart := invoice.CreatedAt.AddDate(0, 1, 0) + overDueDays := int(now.Sub(overDueStart).Hours() / 24) + + // 3 months as a grace period + deadline := invoice.CreatedAt.AddDate(0, 3, 0) + gracePeriod := int(deadline.Sub(now).Hours() / 24) + + mailBody := "We hope this message finds you well.\n" + mailBody += fmt.Sprintf("Our records show that there is an outstanding invoice for %v %s associated with your account (%d). ", invoice.Total, currencyName, invoice.ID) + mailBody += fmt.Sprintf("As of today, the payment for this invoice is %d days overdue.", overDueDays) + mailBody += "To avoid any interruptions to your services and the potential deletion of your deployments, " + mailBody += fmt.Sprintf("we kindly ask that you make the payment within the next %d days. If the invoice remains unpaid after this period, ", gracePeriod) + mailBody += "please be advised that the associated deployments will be deleted from our system.\n\n" + + mailBody += "You can easily pay your invoice by charging balance, activating voucher or using cards.\n\n" + mailBody += "If you have already made the payment or need any assistance, " + mailBody += "please don't hesitate to reach out to us.\n\n" + mailBody += "We appreciate your prompt attention to this matter and thank you fosr being a valued customer." + + subject := "Unpaid Invoice Notification – Action Required" + subject, body := internal.AdminMailContent(subject, mailBody, a.config.Server.Host, userName) + + if err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, userEmail, subject, body); err != nil { + log.Error().Err(err).Send() + } + + notification := models.Notification{UserID: userID, Msg: fmt.Sprintf("Reminder: %s", mailBody)} + err = a.db.CreateNotification(¬ification) + if err != nil { + log.Error().Err(err).Send() + } + + if err = a.db.UpdateInvoiceLastRemainderDate(invoice.ID); err != nil { + log.Error().Err(err).Send() + } + } + } + + return nil +} + +// getCurrencyName returns the full name of the currency based on the currency code. +func getCurrencyName(currencyCode string) (string, error) { + currencyMap := map[string]string{ + "USD": "US Dollar", + "EUR": "Euro", + "GBP": "British Pound", + "AUD": "Australian Dollar", + "CAD": "Canadian Dollar", + "JPY": "Japanese Yen", + "CNY": "Chinese Yuan", + "INR": "Indian Rupee", + "MXN": "Mexican Peso", + "BRL": "Brazilian Real", + "RUB": "Russian Ruble", + "KRW": "South Korean Won", + "CHF": "Swiss Franc", + "SEK": "Swedish Krona", + "NZD": "New Zealand Dollar", + } + + currencyCode = strings.ToUpper(currencyCode) + + if currencyName, exists := currencyMap[currencyCode]; exists { + return currencyName, nil + } + + return "", errors.New("unknown currency") +} diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go index 3ae5e66e..2b59e5e4 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -18,7 +18,36 @@ import ( "gorm.io/gorm" ) +// K8sDeployInput deploy k8s cluster input +type K8sDeployInput struct { + MasterName string `json:"master_name" validate:"min=3,max=20"` + MasterResources string `json:"resources"` + MasterPublic bool `json:"public"` + MasterRegion string `json:"region"` + Workers []WorkerInput `json:"workers"` +} + +// WorkerInput deploy k8s worker input +type WorkerInput struct { + Name string `json:"name" validate:"min=3,max=20"` + Resources string `json:"resources"` +} + // K8sDeployHandler deploy k8s handler +// Example endpoint: Deploy kubernetes +// @Summary Deploy kubernetes +// @Description Deploy kubernetes +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param kubernetes body K8sDeployInput true "Kubernetes deployment input" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /k8s [post] func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) user, err := a.db.GetUserByID(userID) @@ -30,42 +59,79 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - var k8sDeployInput models.K8sDeployInput - err = json.NewDecoder(req.Body).Decode(&k8sDeployInput) + var input K8sDeployInput + err = json.NewDecoder(req.Body).Decode(&input) if err != nil { log.Error().Err(err).Send() return nil, BadRequest(errors.New("failed to read k8s data")) } - err = validator.Validate(k8sDeployInput) + err = validator.Validate(input) if err != nil { log.Error().Err(err).Send() return nil, BadRequest(errors.New("invalid kubernetes data")) } - // quota verification - quota, err := a.db.GetUserQuota(user.ID.String()) - if err == gorm.ErrRecordNotFound { - log.Error().Err(err).Send() - return nil, NotFound(errors.New("user quota is not found")) - } + cru, mru, sru, _, err := deployer.CalcNodeResources(input.MasterResources, input.MasterPublic) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = deployer.ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs) + master := models.Master{ + CRU: cru, + MRU: mru, + SRU: sru, + Public: input.MasterPublic, + Name: input.MasterName, + Resources: input.MasterResources, + Region: input.MasterRegion, + } + + workers := []models.Worker{} + for _, worker := range input.Workers { + cru, mru, sru, _, err := deployer.CalcNodeResources(worker.Resources, false) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + workerModel := models.Worker{ + Name: worker.Name, + CRU: cru, + MRU: mru, + SRU: sru, + Public: input.MasterPublic, + Resources: worker.Resources, + Region: input.MasterRegion, + } + workers = append(workers, workerModel) + } + + k8sCluster := models.K8sCluster{ + UserID: userID, + Master: master, + Workers: workers, + } + + // check if user can deploy? cards verification or voucher balance exists + k8sPrice, err := a.deployer.CanDeployK8s(user.ID.String(), k8sCluster) + if errors.Is(err, deployer.ErrCannotDeploy) { + return nil, BadRequest(err) + } if err != nil { log.Error().Err(err).Send() - return nil, BadRequest(errors.New(err.Error())) + return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + k8sCluster.PricePerMonth = k8sPrice + if len(strings.TrimSpace(user.SSHKey)) == 0 { return nil, BadRequest(errors.New("ssh key is required")) } // unique names - available, err := a.db.AvailableK8sName(k8sDeployInput.MasterName) + available, err := a.db.AvailableK8sName(input.MasterName) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -75,7 +141,14 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { return nil, BadRequest(errors.New("kubernetes master name is not available, please choose a different name")) } - err = a.deployer.Redis.PushK8sRequest(streams.K8sDeployRequest{User: user, Input: k8sDeployInput, AdminSSHKey: a.config.AdminSSHKey}) + k8sCluster.State = models.StateInProgress + err = a.db.CreateK8s(&k8sCluster) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + err = a.deployer.Redis.PushK8sRequest(streams.K8sDeployRequest{User: user, Cluster: k8sCluster, AdminSSHKey: a.config.AdminSSHKey}) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -88,6 +161,19 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { } // ValidateK8sNameHandler validates a cluster name +// Example endpoint: Validate kubernetes name +// @Summary Validate kubernetes name +// @Description Validate kubernetes name +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param name path string true "Kubernetes name" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /k8s/validate/{name} [get] func (a *App) ValidateK8sNameHandler(req *http.Request) (interface{}, Response) { name := mux.Vars(req)["name"] @@ -115,6 +201,20 @@ func (a *App) ValidateK8sNameHandler(req *http.Request) (interface{}, Response) } // K8sGetHandler gets a cluster for a user +// Example endpoint: Get kubernetes deployment using ID +// @Summary Get kubernetes deployment using ID +// @Description Get kubernetes deployment using ID +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Kubernetes cluster ID" +// @Success 200 {object} models.K8sCluster +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /k8s/{id} [get] func (a *App) K8sGetHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) id, err := strconv.Atoi(mux.Vars(req)["id"]) @@ -132,6 +232,10 @@ func (a *App) K8sGetHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if userID != cluster.UserID { + return nil, NotFound(errors.New("cluster is not found")) + } + return ResponseMsg{ Message: "Kubernetes cluster is found", Data: cluster, @@ -139,6 +243,19 @@ func (a *App) K8sGetHandler(req *http.Request) (interface{}, Response) { } // K8sGetAllHandler gets all clusters for a user +// Example endpoint: Get user's kubernetes deployments +// @Summary Get user's kubernetes deployments +// @Description Get user's kubernetes deployments +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.K8sCluster +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /k8s [get] func (a *App) K8sGetAllHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) @@ -161,6 +278,20 @@ func (a *App) K8sGetAllHandler(req *http.Request) (interface{}, Response) { } // K8sDeleteHandler deletes a cluster for a user +// Example endpoint: Delete kubernetes deployment using ID +// @Summary Delete kubernetes deployment using ID +// @Description Delete kubernetes deployment using ID +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Kubernetes cluster ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /k8s/{id} [delete] func (a *App) K8sDeleteHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) id, err := strconv.Atoi(mux.Vars(req)["id"]) @@ -177,6 +308,10 @@ func (a *App) K8sDeleteHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if userID != cluster.UserID { + return nil, NotFound(errors.New("cluster is not found")) + } + err = a.deployer.CancelDeployment(uint64(cluster.ClusterContract), uint64(cluster.NetworkContract), "k8s", cluster.Master.Name) if err != nil && !strings.Contains(err.Error(), "ContractNotExists") { log.Error().Err(err).Send() @@ -199,6 +334,19 @@ func (a *App) K8sDeleteHandler(req *http.Request) (interface{}, Response) { } // K8sDeleteAllHandler deletes all clusters for a user +// Example endpoint: Delete all user's kubernetes deployments +// @Summary Delete all user's kubernetes deployments +// @Description Delete all user's kubernetes deployments +// @Tags Kubernetes +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /k8s [delete] func (a *App) K8sDeleteAllHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) diff --git a/server/app/notification_handler.go b/server/app/notification_handler.go index e820095d..c3e7dfc9 100644 --- a/server/app/notification_handler.go +++ b/server/app/notification_handler.go @@ -13,6 +13,18 @@ import ( ) // ListNotificationsHandler lists notifications for a user +// Example endpoint: Lists user's notifications +// @Summary Lists user's notifications +// @Description Lists user's notifications +// @Tags Notification +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.Notification +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /notification [get] func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) @@ -36,6 +48,19 @@ func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response } // UpdateNotificationsHandler updates notifications for a user +// Example endpoint: Set user's notifications as seen +// @Summary Set user's notifications as seen +// @Description Set user's notifications as seen +// @Tags Notification +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Notification ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /notification/{id} [put] func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Response) { id, err := strconv.Atoi(mux.Vars(req)["id"]) if err != nil { diff --git a/server/app/payments_handler.go b/server/app/payments_handler.go new file mode 100644 index 00000000..7c892012 --- /dev/null +++ b/server/app/payments_handler.go @@ -0,0 +1,351 @@ +package app + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + "strings" + + "github.com/codescalers/cloud4students/middlewares" + "github.com/codescalers/cloud4students/models" + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "gopkg.in/validator.v2" + "gorm.io/gorm" +) + +type AddCardInput struct { + PaymentMethodID string `json:"payment_method_id" binding:"required"` + CardType string `json:"card_type" binding:"required"` +} + +type SetDefaultCardInput struct { + PaymentMethodID string `json:"payment_method_id" binding:"required"` +} + +type ChargeBalance struct { + PaymentMethodID string `json:"payment_method_id" binding:"required"` + Amount float64 `json:"amount" binding:"required"` +} + +// Example endpoint: Add a new card +// @Summary Add a new card +// @Description Add a new card +// @Tags Card +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param card body AddCardInput true "Card input" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/card [post] +func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + var input AddCardInput + err := json.NewDecoder(req.Body).Decode(&input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read input data")) + } + + err = validator.Validate(input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("invalid input data")) + } + + user, err := a.db.GetUserByID(userID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // if user has no stipe customer ID then we create it + if len(strings.TrimSpace(user.StripeCustomerID)) == 0 { + customer, err := createCustomer(user.Name(), user.Email) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + user.StripeCustomerID = customer.ID + err = a.db.UpdateUserByID(user) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + paymentMethod, err := createPaymentMethod(input.CardType, input.PaymentMethodID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + unique, err := a.db.IsCardUnique(paymentMethod.Card.Fingerprint) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if !unique { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("card is added before")) + } + + err = attachPaymentMethod(user.StripeCustomerID, paymentMethod.ID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // Add payment method in DB + if err := a.db.AddCard( + &models.Card{ + UserID: userID, + PaymentMethodID: paymentMethod.ID, + CustomerID: user.StripeCustomerID, + CardType: input.CardType, + ExpMonth: paymentMethod.Card.ExpMonth, + ExpYear: paymentMethod.Card.ExpYear, + Last4: paymentMethod.Card.Last4, + Brand: string(paymentMethod.Card.Brand), + Fingerprint: paymentMethod.Card.Fingerprint, + }, + ); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // if no payment is added before then we update the user payment ID with it as a default + if len(strings.TrimSpace(user.StripeDefaultPaymentID)) == 0 { + // Update the default payment method for future payments + err = updateDefaultPaymentMethod(user.StripeCustomerID, input.PaymentMethodID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + user.StripeDefaultPaymentID = paymentMethod.ID + err = a.db.UpdateUserByID(user) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + // settle old invoices using the card + if err = a.payUserInvoicesUsingCards(user.ID.String(), user.StripeCustomerID, paymentMethod.ID, false); err != nil { + log.Error().Err(err).Send() + } + + return ResponseMsg{ + Message: "Card is added successfully", + Data: nil, + }, Created() +} + +// Example endpoint: Set card as default +// @Summary Set card as default +// @Description Set card as default +// @Tags Card +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param card body SetDefaultCardInput true "Card input" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/card/default [put] +func (a *App) SetDefaultCardHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + var input SetDefaultCardInput + err := json.NewDecoder(req.Body).Decode(&input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read input data")) + } + + err = validator.Validate(input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("invalid input data")) + } + + card, err := a.db.GetCardByPaymentMethod(input.PaymentMethodID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("card is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + err = attachPaymentMethod(card.CustomerID, card.PaymentMethodID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // Update the default payment method for future payments + err = updateDefaultPaymentMethod(card.CustomerID, input.PaymentMethodID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + err = a.db.UpdateUserByID(models.User{ID: uuid.MustParse(userID), StripeDefaultPaymentID: card.PaymentMethodID}) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Card is set as default successfully", + Data: nil, + }, Created() +} + +// Example endpoint: List user's cards +// @Summary List user's cards +// @Description List user's cards +// @Tags Card +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.Card +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/card [get] +func (a *App) ListCardHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + cards, err := a.db.GetUserCards(userID) + if err == gorm.ErrRecordNotFound || len(cards) == 0 { + return ResponseMsg{ + Message: "no cards found", + Data: cards, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Cards are found", + Data: cards, + }, Ok() +} + +// Example endpoint: Delete user card +// @Summary Delete user card +// @Description Delete user card +// @Tags Card +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Card ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/card/{id} [delete] +func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + id, err := strconv.Atoi(mux.Vars(req)["id"]) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read card id")) + } + + card, err := a.db.GetCard(id) + if err == gorm.ErrRecordNotFound { + return nil, BadRequest(errors.New("card is not found")) + } + + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if userID != card.UserID { + return nil, NotFound(errors.New("card is not found")) + } + + cards, err := a.db.GetUserCards(userID) + if err == gorm.ErrRecordNotFound || len(cards) == 0 { + return ResponseMsg{ + Message: "No cards found", + Data: nil, + }, Ok() + } + + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // check active deployments + var vms []models.VM + var k8s []models.K8sCluster + if len(cards) == 1 { + vms, err = a.db.GetAllVms(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + k8s, err = a.db.GetAllK8s(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + if len(vms) > 0 && len(k8s) > 0 { + return nil, BadRequest(errors.New("you have active deployment and cannot delete the card")) + } + + // If user has another cards or no active deployments, so can delete + err = detachPaymentMethod(card.PaymentMethodID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if err = a.db.DeleteCard(id); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Card is deleted successfully", + Data: nil, + }, Ok() +} diff --git a/server/app/quota_handler.go b/server/app/quota_handler.go deleted file mode 100644 index 193de4e1..00000000 --- a/server/app/quota_handler.go +++ /dev/null @@ -1,30 +0,0 @@ -// Package app for c4s backend app -package app - -import ( - "errors" - "net/http" - - "github.com/codescalers/cloud4students/middlewares" - "github.com/rs/zerolog/log" - "gorm.io/gorm" -) - -// GetQuotaHandler gets quota -func (a *App) GetQuotaHandler(req *http.Request) (interface{}, Response) { - userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) - - quota, err := a.db.GetUserQuota(userID) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota is not found")) - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - return ResponseMsg{ - Message: "Quota is found", - Data: quota, - }, Ok() -} diff --git a/server/app/quota_handler_test.go b/server/app/quota_handler_test.go deleted file mode 100644 index 97838e35..00000000 --- a/server/app/quota_handler_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Package app for c4s backend app -package app - -import ( - "fmt" - "net/http" - "testing" - - "github.com/codescalers/cloud4students/internal" - "github.com/codescalers/cloud4students/models" - "github.com/stretchr/testify/assert" -) - -func TestQuotaRouter(t *testing.T) { - app := SetUp(t) - - user.Verified = true - err := app.db.CreateUser(user) - assert.NoError(t, err) - - token, err := internal.CreateJWT(user.ID.String(), user.Email, app.config.Token.Secret, app.config.Token.Timeout) - assert.NoError(t, err) - - t.Run("get quota: not found", func(t *testing.T) { - req := authHandlerConfig{ - unAuthHandlerConfig: unAuthHandlerConfig{ - body: nil, - handlerFunc: app.GetQuotaHandler, - api: fmt.Sprintf("/%s/quota", app.config.Version), - }, - userID: user.ID.String(), - token: token, - config: app.config, - db: app.db, - } - - response := authorizedHandler(req) - want := `{"err":"user quota is not found"}` + "\n" - assert.Equal(t, response.Body.String(), want) - assert.Equal(t, response.Code, http.StatusNotFound) - }) - - t.Run("get quota: success", func(t *testing.T) { - err = app.db.CreateQuota( - &models.Quota{ - UserID: user.ID.String(), - Vms: 10, - PublicIPs: 1, - }, - ) - assert.NoError(t, err) - - req := authHandlerConfig{ - unAuthHandlerConfig: unAuthHandlerConfig{ - body: nil, - handlerFunc: app.GetQuotaHandler, - api: fmt.Sprintf("/%s/quota", app.config.Version), - }, - userID: user.ID.String(), - token: token, - config: app.config, - db: app.db, - } - - response := authorizedHandler(req) - assert.Equal(t, response.Code, http.StatusOK) - }) -} diff --git a/server/app/setup.go b/server/app/setup.go index 2d79af48..f4566ce5 100644 --- a/server/app/setup.go +++ b/server/app/setup.go @@ -68,7 +68,15 @@ func SetUp(t testing.TB) *App { "database": { "file": "%s" }, - "version": "v1" + "version": "v1", + "currency": "eur", + "prices": { + "public_ip": 2, + "small_vm": 10, + "medium_vm": 20, + "large_vm": 30 + }, + "stripe_secret": "sk_test" } `, dbPath) @@ -88,7 +96,7 @@ func SetUp(t testing.TB) *App { tfPluginClient, err := deployer.NewTFPluginClient(configuration.Account.Mnemonics, deployer.WithNetwork(configuration.Account.Network)) assert.NoError(t, err) - newDeployer, err := c4sDeployer.NewDeployer(db, streams.RedisClient{}, tfPluginClient) + newDeployer, err := c4sDeployer.NewDeployer(db, streams.RedisClient{}, tfPluginClient, configuration.PricesPerMonth) assert.NoError(t, err) app := &App{ diff --git a/server/app/stripe.go b/server/app/stripe.go new file mode 100644 index 00000000..c703f6ec --- /dev/null +++ b/server/app/stripe.go @@ -0,0 +1,65 @@ +package app + +import ( + "github.com/stripe/stripe-go/v81" + "github.com/stripe/stripe-go/v81/customer" + "github.com/stripe/stripe-go/v81/paymentintent" + "github.com/stripe/stripe-go/v81/paymentmethod" +) + +func createCustomer(name, email string) (*stripe.Customer, error) { + params := &stripe.CustomerParams{ + Name: stripe.String(name), + Email: stripe.String(email), + } + + return customer.New(params) +} + +func createPaymentIntent(customerID, paymentMethodID, currency string, amount float64) (*stripe.PaymentIntent, error) { + params := &stripe.PaymentIntentParams{ + Amount: stripe.Int64(int64(amount * 100)), + Currency: stripe.String(currency), + Customer: stripe.String(customerID), + PaymentMethod: stripe.String(paymentMethodID), + Confirm: stripe.Bool(true), // Automatically confirm the payment + AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{ + Enabled: stripe.Bool(true), + AllowRedirects: stripe.String("never"), + }, + } + + return paymentintent.New(params) +} + +func createPaymentMethod(cardType, paymentMethodID string) (*stripe.PaymentMethod, error) { + paymentMethodParams := &stripe.PaymentMethodParams{ + Type: stripe.String(cardType), + Card: &stripe.PaymentMethodCardParams{Token: stripe.String(paymentMethodID)}, + } + + return paymentmethod.New(paymentMethodParams) +} + +func attachPaymentMethod(customerID, paymentMethodID string) error { + paymentMethodAttachParams := &stripe.PaymentMethodAttachParams{ + Customer: stripe.String(customerID), + } + + _, err := paymentmethod.Attach(paymentMethodID, paymentMethodAttachParams) + return err +} + +func detachPaymentMethod(paymentMethodID string) error { + _, err := paymentmethod.Detach(paymentMethodID, nil) + return err +} + +func updateDefaultPaymentMethod(customerID, paymentMethodID string) error { + _, err := customer.Update(customerID, &stripe.CustomerParams{ + InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{ + DefaultPaymentMethod: stripe.String(paymentMethodID), + }, + }) + return err +} diff --git a/server/app/unauth_handler.go b/server/app/unauth_handler.go new file mode 100644 index 00000000..bea0b60c --- /dev/null +++ b/server/app/unauth_handler.go @@ -0,0 +1,71 @@ +package app + +import ( + "errors" + "fmt" + "net/http" + + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +// GetMaintenanceHandler gets maintenance flag +// Example endpoint: Gets maintenance flag +// @Summary Gets maintenance flag +// @Description Gets maintenance flag +// @Tags Unauthorized/Authorized +// @Accept json +// @Produce json +// @Success 200 {object} models.Maintenance +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /maintenance [get] +func (a *App) GetMaintenanceHandler(req *http.Request) (interface{}, Response) { + maintenance, err := a.db.GetMaintenance() + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("maintenance is not found")) + } + + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: fmt.Sprintf("Maintenance is set with %v", maintenance.Active), + Data: maintenance, + }, Ok() +} + +// GetNextLaunchHandler returns next launch state +// Example endpoint: Gets next launch state +// @Summary Gets next launch state +// @Description Gets next launch state +// @Tags Unauthorized/Authorized +// @Accept json +// @Produce json +// @Success 200 {object} models.NextLaunch +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /nextlaunch [get] +func (a *App) GetNextLaunchHandler(req *http.Request) (interface{}, Response) { + nextLaunch, err := a.db.GetNextLaunch() + + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("next launch is not found")) + } + + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: fmt.Sprintf("Next Launch is Launched with state: %v", nextLaunch.Launched), + Data: nextLaunch, + }, Ok() +} diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 18efa7d8..4030488c 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -22,14 +22,12 @@ import ( // SignUpInput struct for data needed when user creates account type SignUpInput struct { - Name string `json:"name" binding:"required" validate:"min=3,max=20"` + FirstName string `json:"first_name" binding:"required" validate:"min=3,max=20"` + LastName string `json:"last_name" binding:"required" validate:"min=3,max=20"` Email string `json:"email" binding:"required" validate:"mail"` Password string `json:"password" binding:"required" validate:"password"` ConfirmPassword string `json:"confirm_password" binding:"required" validate:"password"` - TeamSize int `json:"team_size" binding:"required" validate:"min=1,max=20"` - ProjectDesc string `json:"project_desc" binding:"required" validate:"nonzero"` - College string `json:"college" binding:"required" validate:"nonzero"` - SSHKey string `json:"ssh_key" binding:"required"` + SSHKey string `json:"ssh_key"` } // VerifyCodeInput struct takes verification code from user @@ -53,7 +51,8 @@ type ChangePasswordInput struct { // UpdateUserInput struct for user to updates his data type UpdateUserInput struct { - Name string `json:"name"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` Password string `json:"password"` ConfirmPassword string `json:"confirm_password"` SSHKey string `json:"ssh_key"` @@ -66,9 +65,8 @@ type EmailInput struct { // ApplyForVoucherInput struct for user to apply for voucher type ApplyForVoucherInput struct { - VMs int `json:"vms" binding:"required" validate:"min=0"` - PublicIPs int `json:"public_ips" binding:"required" validate:"min=0"` - Reason string `json:"reason" binding:"required" validate:"nonzero"` + Balance uint64 `json:"balance" binding:"required" validate:"min=0"` + Reason string `json:"reason" binding:"required" validate:"nonzero"` } // AddVoucherInput struct for voucher applied by user @@ -76,7 +74,33 @@ type AddVoucherInput struct { Voucher string `json:"voucher" binding:"required"` } +type CodeTimeout struct { + Timeout int `json:"timeout" binding:"required"` +} + +type AccessToken struct { + Token string `json:"access_token" binding:"required"` +} + +type RefreshToken struct { + Access string `json:"access_token" binding:"required"` + Refresh string `json:"refresh_token" binding:"required"` +} + // SignUpHandler creates account for user +// Example endpoint: Register a new user +// @Summary Register a new user +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce json +// @Param registration body SignUpInput true "User registration input" +// @Success 201 {object} CodeTimeout +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/signup [post] func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { var signUp SignUpInput err := json.NewDecoder(req.Body).Decode(&signUp) @@ -114,7 +138,7 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { // send verification code if user is not verified or not exist code := internal.GenerateRandomCode() - subject, body := internal.SignUpMailContent(code, a.config.MailSender.Timeout, signUp.Name, a.config.Server.Host) + subject, body := internal.SignUpMailContent(code, a.config.MailSender.Timeout, fmt.Sprintf("%s %s", signUp.FirstName, signUp.LastName), a.config.Server.Host) err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, signUp.Email, subject, body) if err != nil { log.Error().Err(err).Send() @@ -128,14 +152,12 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { } u := models.User{ - Name: signUp.Name, + FirstName: signUp.FirstName, + LastName: signUp.LastName, Email: signUp.Email, HashedPassword: hashedPassword, Code: code, SSHKey: signUp.SSHKey, - TeamSize: signUp.TeamSize, - ProjectDesc: signUp.ProjectDesc, - College: signUp.College, Admin: internal.Contains(a.config.Admins, signUp.Email), } @@ -159,26 +181,28 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - - // create empty quota - quota := models.Quota{ - UserID: u.ID.String(), - Vms: 0, - } - err = a.db.CreateQuota("a) - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } } return ResponseMsg{ Message: "Verification code has been sent to " + signUp.Email, - Data: map[string]int{"timeout": a.config.MailSender.Timeout}, + Data: CodeTimeout{Timeout: a.config.MailSender.Timeout}, }, Created() } // VerifySignUpCodeHandler gets verification code to create user +// Example endpoint: Verify new user's registration +// @Summary Verify new user's registration +// @Description Verify new user's registration +// @Tags User +// @Accept json +// @Produce json +// @Param code body VerifyCodeInput true "Verification code input" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/signup/verify_email [post] func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) { var data VerifyCodeInput err := json.NewDecoder(req.Body).Decode(&data) @@ -207,21 +231,14 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) if user.UpdatedAt.Add(time.Duration(a.config.MailSender.Timeout) * time.Second).Before(time.Now()) { return nil, BadRequest(errors.New("code has expired")) } - err = a.db.UpdateVerification(user.ID.String(), true) + err = a.db.UpdateUserVerification(user.ID.String(), true) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - middlewares.UserCreations.WithLabelValues(user.ID.String(), user.Email, user.College, fmt.Sprint(user.TeamSize)).Inc() + middlewares.UserCreations.WithLabelValues(user.ID.String(), user.Email).Inc() - // token - token, err := internal.CreateJWT(user.ID.String(), user.Email, a.config.Token.Secret, a.config.Token.Timeout) - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - subject, body := internal.WelcomeMailContent(user.Name, a.config.Server.Host) + subject, body := internal.WelcomeMailContent(user.Name(), a.config.Server.Host) err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() @@ -230,11 +247,23 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) return ResponseMsg{ Message: "Account is created successfully.", - Data: map[string]string{"user_id": user.ID.String(), "access_token": token}, - }, Ok() + }, Created() } // SignInHandler allows user to sign in to the system +// Example endpoint: Sign in user +// @Summary Sign in user +// @Description Sign in user +// @Tags User +// @Accept json +// @Produce json +// @Param login body SignInInput true "User login input" +// @Success 201 {object} AccessToken +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/signin [post] func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { var input SignInInput err := json.NewDecoder(req.Body).Decode(&input) @@ -269,11 +298,24 @@ func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "You are signed in successfully", - Data: map[string]string{"access_token": token}, - }, Ok() + Data: AccessToken{Token: token}, + }, Created() } // RefreshJWTHandler refreshes the user's token +// Example endpoint: Generate a refresh token +// @Summary Generate a refresh token +// @Description Generate a refresh token +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 201 {object} RefreshToken +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/refresh_token [post] func (a *App) RefreshJWTHandler(req *http.Request) (interface{}, Response) { reqToken := req.Header.Get("Authorization") splitToken := strings.Split(reqToken, "Bearer ") @@ -301,7 +343,7 @@ func (a *App) RefreshJWTHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "Access Token is valid", Data: map[string]string{"access_token": reqToken, "refresh_token": reqToken}, - }, Ok() + }, Created() } expirationTime := time.Now().Add(time.Duration(a.config.Token.Timeout) * time.Minute) @@ -315,11 +357,23 @@ func (a *App) RefreshJWTHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "Token is refreshed successfully", - Data: map[string]string{"access_token": reqToken, "refresh_token": newToken}, - }, Ok() + Data: RefreshToken{Access: reqToken, Refresh: newToken}, + }, Created() } // ForgotPasswordHandler sends user verification code +// Example endpoint: Send code to forget password email for verification +// @Summary Send code to forget password email for verification +// @Description Send code to forget password email for verification +// @Tags User +// @Accept json +// @Produce json +// @Success 201 {object} CodeTimeout +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/forgot_password [post] func (a *App) ForgotPasswordHandler(req *http.Request) (interface{}, Response) { var email EmailInput err := json.NewDecoder(req.Body).Decode(&email) @@ -343,7 +397,7 @@ func (a *App) ForgotPasswordHandler(req *http.Request) (interface{}, Response) { // send verification code code := internal.GenerateRandomCode() - subject, body := internal.ResetPasswordMailContent(code, a.config.MailSender.Timeout, user.Name, a.config.Server.Host) + subject, body := internal.ResetPasswordMailContent(code, a.config.MailSender.Timeout, user.Name(), a.config.Server.Host) err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, email.Email, subject, body) if err != nil { @@ -365,11 +419,23 @@ func (a *App) ForgotPasswordHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "Verification code has been sent to " + email.Email, - Data: map[string]int{"timeout": a.config.MailSender.Timeout}, + Data: CodeTimeout{Timeout: a.config.MailSender.Timeout}, }, Ok() } // VerifyForgetPasswordCodeHandler verifies code sent to user when forgetting password +// Example endpoint: Verify user's email to reset password +// @Summary Verify user's email to reset password +// @Description Verify user's email to reset password +// @Tags User +// @Accept json +// @Produce json +// @Success 201 {object} AccessToken +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/forgot_password/verify_email [post] func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, Response) { data := VerifyCodeInput{} err := json.NewDecoder(req.Body).Decode(&data) @@ -408,11 +474,25 @@ func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, R return ResponseMsg{ Message: "Code is verified", - Data: map[string]string{"access_token": token}, + Data: AccessToken{Token: token}, }, Ok() } // ChangePasswordHandler changes password of user +// Example endpoint: Change user password +// @Summary Change user password +// @Description Change user password +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param password body ChangePasswordInput true "New password" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/change_password [put] func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { var data ChangePasswordInput err := json.NewDecoder(req.Body).Decode(&data) @@ -437,7 +517,7 @@ func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdatePassword(data.Email, hashedPassword) + err = a.db.UpdateUserPassword(data.Email, hashedPassword) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user is not found")) } @@ -453,9 +533,23 @@ func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { } // UpdateUserHandler updates user's data +// Example endpoint: Change user data +// @Summary Change user data +// @Description Change user data +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param updates body UpdateUserInput true "User updates" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user [put] func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) - input := UpdateUserInput{} + var input UpdateUserInput err := json.NewDecoder(req.Body).Decode(&input) if err != nil { log.Error().Err(err).Send() @@ -493,7 +587,11 @@ func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { } } - if len(strings.TrimSpace(input.Name)) != 0 { + if len(strings.TrimSpace(input.FirstName)) != 0 { + updates++ + } + + if len(strings.TrimSpace(input.LastName)) != 0 { updates++ } @@ -512,7 +610,8 @@ func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { err = a.db.UpdateUserByID( models.User{ ID: userUUID, - Name: input.Name, + FirstName: input.FirstName, + LastName: input.LastName, HashedPassword: hashedPassword, SSHKey: input.SSHKey, UpdatedAt: time.Now(), @@ -528,11 +627,23 @@ func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "User is updated successfully", - Data: map[string]string{"user_id": userID}, }, Ok() } -// GetUserHandler returns user by its idx +// GetUserHandler returns user by its id +// Example endpoint: Get user +// @Summary Get user +// @Description Get user +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.User +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user [get] func (a *App) GetUserHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) user, err := a.db.GetUserByID(userID) @@ -551,6 +662,20 @@ func (a *App) GetUserHandler(req *http.Request) (interface{}, Response) { } // ApplyForVoucherHandler makes user apply for voucher that would be accepted by admin +// Example endpoint: Apply for a new voucher +// @Summary Apply for a new voucher +// @Description Apply for a new voucher +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param voucher body ApplyForVoucherInput true "New voucher details" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/apply_voucher [post] func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) { var input ApplyForVoucherInput err := json.NewDecoder(req.Body).Decode(&input) @@ -576,11 +701,10 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) // generate voucher for user but can't use it until admin approves it v := internal.GenerateRandomVoucher(5) voucher := models.Voucher{ - Voucher: v, - UserID: userID, - VMs: input.VMs, - Reason: input.Reason, - PublicIPs: input.PublicIPs, + Voucher: v, + UserID: userID, + Balance: input.Balance, + Reason: input.Reason, } err = a.db.CreateVoucher(&voucher) @@ -588,15 +712,29 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - middlewares.VoucherApplied.WithLabelValues(userID, voucher.Voucher, fmt.Sprint(voucher.VMs), fmt.Sprint(voucher.PublicIPs)).Inc() + middlewares.VoucherApplied.WithLabelValues(userID, voucher.Voucher, fmt.Sprint(voucher.Balance)).Inc() return ResponseMsg{ Message: "Voucher request is being reviewed, you'll receive a confirmation mail soon", Data: nil, - }, Ok() + }, Created() } // ActivateVoucherHandler makes user adds voucher to his account +// Example endpoint: Activate a voucher +// @Summary Activate a voucher +// @Description Activate a voucher +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param voucher body AddVoucherInput true "Voucher input" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/activate_voucher [put] func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) @@ -607,16 +745,16 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, BadRequest(errors.New("failed to read voucher data")) } - oldQuota, err := a.db.GetUserQuota(userID) + user, err := a.db.GetUserByID(userID) if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota is not found")) + return nil, NotFound(errors.New("user is not found")) } if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - voucherQuota, err := a.db.GetVoucher(input.Voucher) + voucherBalance, err := a.db.GetVoucher(input.Voucher) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user voucher is not found")) } @@ -625,15 +763,15 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - if voucherQuota.Rejected { + if voucherBalance.Rejected { return nil, BadRequest(errors.New("voucher is rejected")) } - if !voucherQuota.Approved { + if !voucherBalance.Approved { return nil, BadRequest(errors.New("voucher is not approved yet")) } - if voucherQuota.Used { + if voucherBalance.Used { return nil, BadRequest(errors.New("voucher is already used")) } @@ -643,15 +781,96 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserQuota(userID, oldQuota.Vms+voucherQuota.VMs, oldQuota.PublicIPs+voucherQuota.PublicIPs) + user.VoucherBalance += float64(voucherBalance.Balance) + + user.Balance, user.VoucherBalance, err = a.db.PayUserInvoices(userID, user.Balance, user.VoucherBalance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc() + + err = a.db.UpdateUserByID(user) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + middlewares.VoucherActivated.WithLabelValues(userID, voucherBalance.Voucher, fmt.Sprint(voucherBalance.Balance)).Inc() return ResponseMsg{ Message: "Voucher is applied successfully", Data: nil, }, Ok() } + +// Example endpoint: Charge user balance +// @Summary Charge user balance +// @Description Charge user balance +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param balance body ChargeBalance true "Balance charging details" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/charge_balance [put] +func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + var input ChargeBalance + err := json.NewDecoder(req.Body).Decode(&input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read input data")) + } + + err = validator.Validate(input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("invalid input data")) + } + + user, err := a.db.GetUserByID(userID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + _, err = createPaymentIntent(user.StripeCustomerID, input.PaymentMethodID, a.config.Currency, input.Amount) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + user.Balance += float64(input.Amount) + + user.Balance, user.VoucherBalance, err = a.db.PayUserInvoices(userID, user.Balance, user.VoucherBalance) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + err = a.db.UpdateUserByID(user) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Balance is charged successfully", + // Data: map[string]string{"client_secret": intent.ClientSecret}, + Data: nil, + }, Ok() +} diff --git a/server/app/user_handler_test.go b/server/app/user_handler_test.go index 908b91c4..027053a6 100644 --- a/server/app/user_handler_test.go +++ b/server/app/user_handler_test.go @@ -19,12 +19,9 @@ var salt = []byte("saltsaltsaltsalt") var password = "1234567" var hashedPassword = sha256.Sum256(append(salt, []byte(password)...)) var user = &models.User{ - Name: "name", + FirstName: "name", Email: "name@gmail.com", HashedPassword: append(salt, hashedPassword[:]...), - TeamSize: 5, - ProjectDesc: "desc", - College: "clg", SSHKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCSJYyNo6j1LxrjDTRGkbBgIyD/puMprzoepKr2zwbNobCEMfAx9DXBFstueQ9wYgcwO0Pu7/95BNgtGhjoRsNDEz5MBO0Iyhcr9hGYfoXrG2Ufr8IYu3i5DWLRmDERzuArZ6/aUWIpCfpheHX+/jH/R9vvnjO2phCutpkWrjx34/33U3pL+RRycA1uTsISZTyrcMZIXfABI4xBMFLundaBk6F4YFZaCjkUOLYld4KDxJ+N6cYnJ5pa5/hLzZQedn6h7SpMvSCghxOdCxqdEwF0m9odfsrXeKRBxRfL+HWxqytNKp9CgfLvE9Knmfn5GWhXYS6/7dY7GNUGxWSje6L1h9DFwhJLjTpEwoboNzveBmlcyDwduewFZZY+q1C/gKmJial3+0n6zkx4daQsiHc29KM5wiH8mvqpm5Ew9vWNOqw85sO7BaE1W5jMkZOuqIEJiz+KW6UicUBbv2YJ8kjvNtMLM1BiE3/WjVXQ3cMf1x1mUH4bFVgW7F42nnkuc2k= alaa@alaa-Inspiron-5537", } @@ -33,7 +30,8 @@ func TestSignUpHandler(t *testing.T) { // json Body of request signUpBody := []byte(`{ - "name": "name", + "first_name": "name", + "last_name": "last", "email": "name@gmail.com", "password": "1234567", "confirm_password": "1234567", @@ -99,7 +97,8 @@ func TestSignUpHandler(t *testing.T) { t.Run("Sign up: password and confirm_password don't match", func(t *testing.T) { body := []byte(`{ - "name": "name", + "first_name": "name", + "last_name": "last", "email": "name@gmail.com", "password": "12345679", "confirm_password": "1234567", @@ -124,7 +123,7 @@ func TestSignUpHandler(t *testing.T) { user, err := app.db.GetUserByEmail(user.Email) assert.NoError(t, err) - err = app.db.UpdateVerification(user.ID.String(), true) + err = app.db.UpdateUserVerification(user.ID.String(), true) assert.NoError(t, err) req := unAuthHandlerConfig{ @@ -159,7 +158,7 @@ func TestVerifySignUpCodeHandler(t *testing.T) { } response := unAuthorizedHandler(req) - assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Code, http.StatusCreated) }) t.Run("Verify sign up: add empty code", func(t *testing.T) { @@ -201,7 +200,7 @@ func TestVerifySignUpCodeHandler(t *testing.T) { }) t.Run("Verify sign up: wrong code", func(t *testing.T) { - err := app.db.UpdateVerification(user.ID.String(), false) + err := app.db.UpdateUserVerification(user.ID.String(), false) assert.NoError(t, err) body := []byte(fmt.Sprintf(`{"email": "%s", "code": %d}`, user.Email, 0)) @@ -254,7 +253,7 @@ func TestSignInHandler(t *testing.T) { } response := unAuthorizedHandler(req) - assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Code, http.StatusCreated) }) t.Run("Sign in: wrong password", func(t *testing.T) { @@ -310,7 +309,7 @@ func TestSignInHandler(t *testing.T) { }) t.Run("Sign in: user is not verified", func(t *testing.T) { - err := app.db.UpdateVerification(user.ID.String(), false) + err := app.db.UpdateUserVerification(user.ID.String(), false) assert.NoError(t, err) req := unAuthHandlerConfig{ @@ -351,7 +350,7 @@ func TestRefreshJWTHandler(t *testing.T) { } response := authorizedNoMiddlewareHandler(req) - assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Code, http.StatusCreated) }) t.Run("refresh token: not expired yet", func(t *testing.T) { @@ -367,7 +366,7 @@ func TestRefreshJWTHandler(t *testing.T) { } response := authorizedNoMiddlewareHandler(req) - assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Code, http.StatusCreated) }) t.Run("refresh token: add empty token", func(t *testing.T) { @@ -889,7 +888,7 @@ func TestApplyForVoucherHandler(t *testing.T) { } response := authorizedHandler(req) - assert.Equal(t, response.Code, http.StatusOK) + assert.Equal(t, response.Code, http.StatusCreated) }) t.Run("Apply voucher: failed to read voucher data", func(t *testing.T) { @@ -915,7 +914,7 @@ func TestApplyForVoucherHandler(t *testing.T) { v := models.Voucher{ UserID: user.ID.String(), Voucher: "voucher", - VMs: 10, + Balance: 10, Approved: false, Rejected: false, } @@ -948,16 +947,9 @@ func TestActivateVoucherHandler(t *testing.T) { err := app.db.CreateUser(user) assert.NoError(t, err) - err = app.db.CreateQuota( - &models.Quota{ - UserID: user.ID.String(), - }, - ) - assert.NoError(t, err) - v := models.Voucher{ Voucher: "voucher", - VMs: 10, + Balance: 10, Approved: true, } @@ -1026,34 +1018,6 @@ func TestActivateVoucherHandler(t *testing.T) { assert.Equal(t, response.Code, http.StatusBadRequest) }) - t.Run("Activate voucher: user quota not found", func(t *testing.T) { - newUser := user - newUser.Verified = true - newUser.Email = "test@example.com" - err := app.db.CreateUser(newUser) - assert.NoError(t, err) - - token, err := internal.CreateJWT(newUser.ID.String(), newUser.Email, app.config.Token.Secret, app.config.Token.Timeout) - assert.NoError(t, err) - - req := authHandlerConfig{ - unAuthHandlerConfig: unAuthHandlerConfig{ - body: bytes.NewBuffer(voucherBody), - handlerFunc: app.ActivateVoucherHandler, - api: fmt.Sprintf("/%s/user/activate_voucher", app.config.Version), - }, - userID: newUser.ID.String(), - token: token, - config: app.config, - db: app.db, - } - - response := authorizedHandler(req) - want := `{"err":"user quota is not found"}` + "\n" - assert.Equal(t, response.Body.String(), want) - assert.Equal(t, response.Code, http.StatusNotFound) - }) - t.Run("Activate voucher: voucher not found", func(t *testing.T) { body := []byte(`{"voucher" : "abcd"}`) req := authHandlerConfig{ @@ -1077,7 +1041,7 @@ func TestActivateVoucherHandler(t *testing.T) { t.Run("Activate voucher: voucher is rejected", func(t *testing.T) { v := models.Voucher{ Voucher: "rejected_voucher", - VMs: 10, + Balance: 10, Rejected: true, } err = app.db.CreateVoucher(&v) @@ -1105,7 +1069,7 @@ func TestActivateVoucherHandler(t *testing.T) { t.Run("Activate voucher: voucher is not approved yet", func(t *testing.T) { v := models.Voucher{ Voucher: "pending_voucher", - VMs: 10, + Balance: 10, Approved: false, Rejected: false, } diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index 17bfface..5f91becd 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -18,7 +18,29 @@ import ( "gorm.io/gorm" ) +// DeployVMInput struct takes input of vm from user +type DeployVMInput struct { + Name string `json:"name" binding:"required" validate:"min=3,max=20"` + Resources string `json:"resources" binding:"required"` + Public bool `json:"public"` + Region string `json:"region"` +} + // DeployVMHandler creates vm for user and deploy it +// Example endpoint: Deploy virtual machine +// @Summary Deploy virtual machine +// @Description Deploy virtual machine +// @Tags VM +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param vm body DeployVMInput true "virtual machine deployment input" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /vm [post] func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) user, err := a.db.GetUserByID(userID) @@ -31,7 +53,7 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - var input models.DeployVMInput + var input DeployVMInput err = json.NewDecoder(req.Body).Decode(&input) if err != nil { log.Error().Err(err).Send() @@ -44,21 +66,34 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { return nil, BadRequest(errors.New("invalid vm data")) } - // check quota of user - quota, err := a.db.GetUserQuota(user.ID.String()) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota is not found")) - } + cru, mru, sru, _, err := deployer.CalcNodeResources(input.Resources, input.Public) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = deployer.ValidateVMQuota(input, quota.Vms, quota.PublicIPs) + vm := models.VM{ + UserID: userID, + Name: input.Name, + Resources: input.Resources, + Public: input.Public, + SRU: sru, + CRU: cru, + MRU: mru * 1024, + Region: input.Region, + } + + vmPrice, err := a.deployer.CanDeployVM(user.ID.String(), vm) + if errors.Is(err, deployer.ErrCannotDeploy) { + return nil, BadRequest(err) + } if err != nil { - return nil, BadRequest(errors.New(err.Error())) + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + vm.PricePerMonth = vmPrice + if len(strings.TrimSpace(user.SSHKey)) == 0 { return nil, BadRequest(errors.New("ssh key is required")) } @@ -74,7 +109,14 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { return nil, BadRequest(errors.New("virtual machine name is not available, please choose a different name")) } - err = a.deployer.Redis.PushVMRequest(streams.VMDeployRequest{User: user, Input: input, AdminSSHKey: a.config.AdminSSHKey}) + vm.State = models.StateInProgress + err = a.db.CreateVM(&vm) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + err = a.deployer.Redis.PushVMRequest(streams.VMDeployRequest{User: user, VM: vm, AdminSSHKey: a.config.AdminSSHKey}) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -87,6 +129,19 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { } // ValidateVMNameHandler validates a vm name +// Example endpoint: Validate virtual machine name +// @Summary Validate virtual machine name +// @Description Validate virtual machine name +// @Tags VM +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param name path string true "Virtual machine name" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /vm/validate/{name} [get] func (a *App) ValidateVMNameHandler(req *http.Request) (interface{}, Response) { name := mux.Vars(req)["name"] @@ -114,6 +169,20 @@ func (a *App) ValidateVMNameHandler(req *http.Request) (interface{}, Response) { } // GetVMHandler returns vm by its id +// Example endpoint: Get virtual machine deployment using ID +// @Summary Get virtual machine deployment using ID +// @Description Get virtual machine deployment using ID +// @Tags VM +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Virtual machine ID" +// @Success 200 {object} models.VM +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /vm/{id} [get] func (a *App) GetVMHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) id, err := strconv.Atoi(mux.Vars(req)["id"]) @@ -141,6 +210,19 @@ func (a *App) GetVMHandler(req *http.Request) (interface{}, Response) { } // ListVMsHandler returns all vms of user +// Example endpoint: Get user's virtual machine deployments +// @Summary Get user's virtual machine deployments +// @Description Get user's virtual machine deployments +// @Tags VM +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.VM +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /vm [get] func (a *App) ListVMsHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) @@ -163,6 +245,20 @@ func (a *App) ListVMsHandler(req *http.Request) (interface{}, Response) { } // DeleteVMHandler deletes vm by its id +// Example endpoint: Delete virtual machine deployment using ID +// @Summary Delete virtual machine deployment using ID +// @Description Delete virtual machine deployment using ID +// @Tags VM +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Virtual machine ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /vm/{id} [delete] func (a *App) DeleteVMHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) id, err := strconv.Atoi(mux.Vars(req)["id"]) @@ -180,6 +276,10 @@ func (a *App) DeleteVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if vm.UserID != userID { + return nil, NotFound(errors.New("virtual machine is not found")) + } + err = a.deployer.CancelDeployment(vm.ContractID, vm.NetworkContractID, "vm", vm.Name) if err != nil && !strings.Contains(err.Error(), "ContractNotExists") { log.Error().Err(err).Send() @@ -200,6 +300,19 @@ func (a *App) DeleteVMHandler(req *http.Request) (interface{}, Response) { } // DeleteAllVMsHandler deletes all vms of user +// Example endpoint: Delete all user's virtual machine deployments +// @Summary Delete all user's virtual machine deployments +// @Description Delete all user's virtual machine deployments +// @Tags VM +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /vm [delete] func (a *App) DeleteAllVMsHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) vms, err := a.db.GetAllVms(userID) diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index 90a24aca..dc67051f 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -17,9 +17,8 @@ import ( // GenerateVoucherInput struct for data needed when user generate vouchers type GenerateVoucherInput struct { - Length int `json:"length" binding:"required" validate:"min=3,max=20"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` + Length int `json:"length" binding:"required" validate:"min=3,max=20"` + Balance uint64 `json:"balance" binding:"required"` } // UpdateVoucherInput struct for data needed when user update voucher @@ -28,6 +27,20 @@ type UpdateVoucherInput struct { } // GenerateVoucherHandler generates a voucher by admin +// Example endpoint: Generates a new voucher +// @Summary Generates a new voucher +// @Description Generates a new voucher +// @Tags Voucher (only admins) +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param voucher body GenerateVoucherInput true "Voucher details" +// @Success 201 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /voucher [post] func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) { var input GenerateVoucherInput err := json.NewDecoder(req.Body).Decode(&input) @@ -44,10 +57,9 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) voucher := internal.GenerateRandomVoucher(input.Length) v := models.Voucher{ - Voucher: voucher, - VMs: input.VMs, - PublicIPs: input.PublicIPs, - Approved: true, + Voucher: voucher, + Balance: input.Balance, + Approved: true, } err = a.db.CreateVoucher(&v) @@ -56,12 +68,6 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = a.db.UpdateVoucher(v.ID, true) - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - return ResponseMsg{ Message: "Voucher is generated successfully", Data: map[string]string{"voucher": voucher}, @@ -69,6 +75,18 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) } // ListVouchersHandler lists all vouchers by admin +// Example endpoint: Lists users' vouchers +// @Summary Lists users' vouchers +// @Description Lists users' vouchers +// @Tags Voucher (only admins) +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.Voucher +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /voucher [get] func (a *App) ListVouchersHandler(req *http.Request) (interface{}, Response) { vouchers, err := a.db.ListAllVouchers() if err == gorm.ErrRecordNotFound || len(vouchers) == 0 { @@ -90,6 +108,21 @@ func (a *App) ListVouchersHandler(req *http.Request) (interface{}, Response) { } // UpdateVoucherHandler approves/rejects a voucher by admin +// Example endpoint: Update (approve-reject) a voucher +// @Summary Update (approve-reject) a voucher +// @Description Update (approve-reject) a voucher +// @Tags Voucher (only admins) +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Voucher ID" +// @Param state body UpdateVoucherInput true "Voucher approval state" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /voucher/{id} [put] func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) { var input UpdateVoucherInput err := json.NewDecoder(req.Body).Decode(&input) @@ -138,9 +171,9 @@ func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) { var subject, body string if input.Approved { - subject, body = internal.ApprovedVoucherMailContent(updatedVoucher.Voucher, user.Name, a.config.Server.Host) + subject, body = internal.ApprovedVoucherMailContent(updatedVoucher.Voucher, user.Name(), a.config.Server.Host) } else { - subject, body = internal.RejectedVoucherMailContent(user.Name, a.config.Server.Host) + subject, body = internal.RejectedVoucherMailContent(user.Name(), a.config.Server.Host) } err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body) @@ -156,6 +189,17 @@ func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) { } // ApproveAllVouchersHandler approves all vouchers by admin +// Example endpoint: Approve all vouchers +// @Summary Approve all vouchers +// @Description Approve all vouchers +// @Tags Voucher (only admins) +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} Response +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /voucher [put] func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Response) { vouchers, err := a.db.ListAllVouchers() if err != nil { @@ -183,7 +227,7 @@ func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Respons return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - subject, body := internal.ApprovedVoucherMailContent(v.Voucher, user.Name, a.config.Server.Host) + subject, body := internal.ApprovedVoucherMailContent(v.Voucher, user.Name(), a.config.Server.Host) err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() @@ -196,3 +240,39 @@ func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Respons Data: nil, }, Ok() } + +// ResetUsersVoucherBalanceHandler resets all users voucher balance +// Example endpoint: Resets all users voucher balance +// @Summary Resets all users voucher balance +// @Description Resets all users voucher balance +// @Tags Voucher (only admins) +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} float64 +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /voucher/reset [put] +func (a *App) ResetUsersVoucherBalanceHandler(req *http.Request) (interface{}, Response) { + users, err := a.db.ListAllUsers() + if err == gorm.ErrRecordNotFound || len(users) == 0 { + return ResponseMsg{ + Message: "Users are not found", + }, Ok() + } + + for _, user := range users { + user.VoucherBalance = 0 + err = a.db.UpdateUserByID(user) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + return ResponseMsg{ + Message: "Voucher balance is reset successfully", + }, Ok() +} diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index fd489bf9..63ecf104 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -7,18 +7,23 @@ import ( "net" "time" + "github.com/codescalers/cloud4students/internal" "github.com/codescalers/cloud4students/models" "github.com/codescalers/cloud4students/streams" "github.com/codescalers/cloud4students/validators" + "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "gopkg.in/validator.v2" + "gorm.io/gorm" ) const internalServerErrorMsg = "Something went wrong" var ( + ErrCannotDeploy = errors.New("cannot proceed with deployment, either add a valid card or apply for a new voucher") + vmEntryPoint = "/init.sh" k8sFlist = "https://hub.grid.tf/tf-official-apps/threefoldtech-k3s-latest.flist" @@ -34,12 +39,6 @@ var ( largeMemory = uint64(8) largeDisk = uint64(100) - smallQuota = 1 - mediumQuota = 2 - largeQuota = 3 - publicQuota = 1 - - trueVal = true statusUp = "up" token = "random" @@ -50,13 +49,16 @@ type Deployer struct { db models.DB Redis streams.RedisClient tfPluginClient deployer.TFPluginClient + prices internal.Prices vmDeployed chan bool k8sDeployed chan bool } // NewDeployer create new deployer -func NewDeployer(db models.DB, redis streams.RedisClient, tfPluginClient deployer.TFPluginClient) (Deployer, error) { +func NewDeployer( + db models.DB, redis streams.RedisClient, tfPluginClient deployer.TFPluginClient, prices internal.Prices, +) (Deployer, error) { // validations err := validator.SetValidationFunc("ssh", validators.ValidateSSHKey) if err != nil { @@ -75,6 +77,7 @@ func NewDeployer(db models.DB, redis streams.RedisClient, tfPluginClient deploye db, redis, tfPluginClient, + prices, make(chan bool), make(chan bool), }, nil @@ -182,7 +185,7 @@ func buildNetwork(node uint32, name string) (workloads.ZNet, error) { }, nil } -func calcNodeResources(resources string, public bool) (uint64, uint64, uint64, uint64, error) { +func CalcNodeResources(resources string, public bool) (uint64, uint64, uint64, uint64, error) { var cru uint64 var mru uint64 var sru uint64 @@ -209,18 +212,95 @@ func calcNodeResources(resources string, public bool) (uint64, uint64, uint64, u return cru, mru, sru, ips, nil } -func calcNeededQuota(resources string) (int, error) { - var neededQuota int +func calcPrice(prices internal.Prices, resources string, public bool) (float64, error) { + var price float64 switch resources { case "small": - neededQuota += smallQuota + price += prices.SmallVM case "medium": - neededQuota += mediumQuota + price += prices.MediumVM case "large": - neededQuota += largeQuota + price += prices.LargeVM default: return 0, fmt.Errorf("unknown resource type %s", resources) } - return neededQuota, nil + if public { + price += prices.PublicIP + } + return price, nil +} + +func convertGBToBytes(gb uint64) *uint64 { + bytes := gb * 1024 * 1024 * 1024 + return &bytes +} + +// canDeploy checks if user has a valid card so can deploy or has enough voucher money +func (d *Deployer) canDeploy(userID string, costPerMonth float64) error { + // check if user has a valid card + _, err := d.db.GetUserCards(userID) + if err == gorm.ErrRecordNotFound { + // If no? check if user has enough voucher balance respecting his active deployments (debt) + user, err := d.db.GetUserByID(userID) + if err != nil { + return err + } + + // calculate new debt during the current month (for active deployments) + newDebt, err := d.calculateUserDebtInMonth(userID) + if err != nil { + return err + } + + userDebt, err := d.db.CalcUserDebt(userID) + if err != nil { + return err + } + + debt := userDebt + newDebt + // if user has enough money for new cost and his debt then can deploy + if user.VoucherBalance > debt+costPerMonth { + return nil + } + + return ErrCannotDeploy + } + + return err +} + +// calculateUserDebtInMonth calculates how much money does user have used +// from the start of current month +func (d *Deployer) calculateUserDebtInMonth(userID string) (float64, error) { + var debt float64 + usagePercentageInMonth := UsagePercentageInMonth(time.Now()) + + vms, err := d.db.GetAllVms(userID) + if err != nil { + return 0, err + } + + for _, vm := range vms { + debt += float64(vm.PricePerMonth) * usagePercentageInMonth + } + + clusters, err := d.db.GetAllK8s(userID) + if err != nil { + return 0, err + } + + for _, c := range clusters { + debt += float64(c.PricePerMonth) * usagePercentageInMonth + } + + return debt, nil +} + +// UsagePercentageInMonth calculates percentage of hours till specific time during the month +// according to total hours of the same month +func UsagePercentageInMonth(end time.Time) float64 { + start := time.Date(end.Year(), end.Month(), 0, 0, 0, 0, 0, time.UTC) + endMonth := time.Date(end.Year(), end.Month()+1, 0, 0, 0, 0, 0, time.UTC) + return end.Sub(start).Hours() / endMonth.Sub(start).Hours() } diff --git a/server/deployer/deployment_consumer.go b/server/deployer/deployment_consumer.go index 95ca2abf..71901192 100644 --- a/server/deployer/deployment_consumer.go +++ b/server/deployer/deployment_consumer.go @@ -37,7 +37,7 @@ func (d *Deployer) ConsumeVMRequest(ctx context.Context, pending bool) { defer vmWG.Done() var codeErr int - var resErr error + var resErr, backendErr error var req streams.VMDeployRequest for _, v := range message.Values { @@ -47,9 +47,14 @@ func (d *Deployer) ConsumeVMRequest(ctx context.Context, pending bool) { continue } - codeErr, resErr = d.deployVMRequest(ctx, req.User, req.Input, req.AdminSSHKey) + codeErr, backendErr, resErr = d.deployVMRequest(ctx, req.User, req.VM, req.AdminSSHKey) if resErr != nil { - log.Error().Err(resErr).Msg("failed to deploy vm request") + log.Error().Err(backendErr).Msg("failed to deploy vm request") + + updateErr := d.db.UpdateVMState(req.VM.ID, backendErr.Error(), models.StateFailed) + if updateErr != nil { + log.Error().Err(updateErr).Msg("failed to update vm state") + } continue } } @@ -60,9 +65,9 @@ func (d *Deployer) ConsumeVMRequest(ctx context.Context, pending bool) { codeErr = http.StatusInternalServerError } - msg := fmt.Sprintf("Your virtual machine '%s' failed to be deployed with error: %s", req.Input.Name, resErr) + msg := fmt.Sprintf("Your virtual machine '%s' failed to be deployed with error: %s", req.VM.Name, resErr) if codeErr == 0 { - msg = fmt.Sprintf("Your virtual machine '%s' is deployed successfully 🎆", req.Input.Name) + msg = fmt.Sprintf("Your virtual machine '%s' is deployed successfully 🎆", req.VM.Name) } notification := models.Notification{ @@ -101,7 +106,7 @@ func (d *Deployer) ConsumeK8sRequest(ctx context.Context, pending bool) { defer k8sWG.Done() var codeErr int - var resErr error + var resErr, backendErr error var req streams.K8sDeployRequest for _, v := range message.Values { @@ -111,9 +116,14 @@ func (d *Deployer) ConsumeK8sRequest(ctx context.Context, pending bool) { continue } - codeErr, resErr = d.deployK8sRequest(ctx, req.User, req.Input, req.AdminSSHKey) + codeErr, backendErr, resErr = d.deployK8sRequest(ctx, req.User, req.Cluster, req.AdminSSHKey) if resErr != nil { log.Error().Err(resErr).Msg("failed to deploy k8s request") + + updateErr := d.db.UpdateK8sState(req.Cluster.ID, backendErr.Error(), models.StateFailed) + if updateErr != nil { + log.Error().Err(updateErr).Msg("failed to update kubernetes state") + } continue } } @@ -124,9 +134,9 @@ func (d *Deployer) ConsumeK8sRequest(ctx context.Context, pending bool) { codeErr = http.StatusInternalServerError } - msg := fmt.Sprintf("Your kubernetes cluster '%s' failed to be deployed with error: %s", req.Input.MasterName, resErr) + msg := fmt.Sprintf("Your kubernetes cluster '%s' failed to be deployed with error: %s", req.Cluster.Master.Name, resErr) if codeErr == 0 { - msg = fmt.Sprintf("Your kubernetes cluster '%s' is deployed successfully 🎆", req.Input.MasterName) + msg = fmt.Sprintf("Your kubernetes cluster '%s' is deployed successfully 🎆", req.Cluster.Master.Name) } notification := models.Notification{ diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index 7e19d7a3..8721739d 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "net/http" + "strings" "github.com/codescalers/cloud4students/middlewares" "github.com/codescalers/cloud4students/models" @@ -14,10 +15,9 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" - "gorm.io/gorm" ) -func buildK8sCluster(node uint32, sshKey, network string, k models.K8sDeployInput) (workloads.K8sCluster, error) { +func buildK8sCluster(node uint32, sshKey, network string, k models.K8sCluster) (workloads.K8sCluster, error) { myceliumIPSeed, err := workloads.RandomMyceliumIPSeed() if err != nil { return workloads.K8sCluster{}, err @@ -25,7 +25,7 @@ func buildK8sCluster(node uint32, sshKey, network string, k models.K8sDeployInpu master := workloads.K8sNode{ VM: &workloads.VM{ - Name: k.MasterName, + Name: k.Master.Name, Flist: k8sFlist, Planetary: true, MyceliumIPSeed: myceliumIPSeed, @@ -34,7 +34,7 @@ func buildK8sCluster(node uint32, sshKey, network string, k models.K8sDeployInpu }, } - cru, mru, sru, ips, err := calcNodeResources(k.Resources, k.Public) + cru, mru, sru, ips, err := CalcNodeResources(k.Master.Resources, k.Master.Public) if err != nil { return workloads.K8sCluster{}, err } @@ -64,7 +64,7 @@ func buildK8sCluster(node uint32, sshKey, network string, k models.K8sDeployInpu }, } - cru, mru, sru, _, err := calcNodeResources(k.Resources, false) + cru, mru, sru, _, err := CalcNodeResources(worker.Resources, false) if err != nil { return workloads.K8sCluster{}, err } @@ -81,13 +81,13 @@ func buildK8sCluster(node uint32, sshKey, network string, k models.K8sDeployInpu NetworkName: network, Token: token, SSHKey: sshKey, - SolutionType: k.MasterName, + SolutionType: k.Master.Name, } return k8sCluster, nil } -func (d *Deployer) deployK8sClusterWithNetwork(ctx context.Context, k8sDeployInput models.K8sDeployInput, sshKey string, adminSSHKey string) (uint32, uint64, uint64, error) { +func (d *Deployer) deployK8sClusterWithNetwork(ctx context.Context, k8sDeployInput models.K8sCluster, sshKey string, adminSSHKey string) (uint32, uint64, uint64, error) { // get available nodes node, err := d.getK8sAvailableNode(ctx, k8sDeployInput) if err != nil { @@ -95,7 +95,7 @@ func (d *Deployer) deployK8sClusterWithNetwork(ctx context.Context, k8sDeployInp } // build network - network, err := buildNetwork(node, fmt.Sprintf("%sk8sNet", k8sDeployInput.MasterName)) + network, err := buildNetwork(node, fmt.Sprintf("%sk8sNet", k8sDeployInput.Master.Name)) if err != nil { return 0, 0, 0, err } @@ -137,72 +137,52 @@ func (d *Deployer) deployK8sClusterWithNetwork(ctx context.Context, k8sDeployInp return node, loadedNet.NodeDeploymentID[node], loadedCluster.NodeDeploymentID[node], nil } -func (d *Deployer) loadK8s(ctx context.Context, k8sDeployInput models.K8sDeployInput, userID string, node uint32, networkContractID uint64, k8sContractID uint64) (models.K8sCluster, error) { +func (d *Deployer) loadK8s( + ctx context.Context, + k8s models.K8sCluster, + node uint32, + networkContractID uint64, k8sContractID uint64, +) (models.K8sCluster, error) { // load cluster - resCluster, err := d.tfPluginClient.State.LoadK8sFromGrid(ctx, []uint32{node}, k8sDeployInput.MasterName) + resCluster, err := d.tfPluginClient.State.LoadK8sFromGrid(ctx, []uint32{node}, k8s.Master.Name) if err != nil { return models.K8sCluster{}, err } - // save to db - cru, mru, sru, _, err := calcNodeResources(k8sDeployInput.Resources, k8sDeployInput.Public) - if err != nil { - return models.K8sCluster{}, err - } + // Updates after deployment + k8s.Master.PublicIP = resCluster.Master.ComputedIP + k8s.Master.YggIP = resCluster.Master.PlanetaryIP + k8s.Master.MyceliumIP = resCluster.Master.MyceliumIP - master := models.Master{ - CRU: cru, - MRU: mru, - SRU: sru, - Public: k8sDeployInput.Public, - PublicIP: resCluster.Master.ComputedIP, - Name: k8sDeployInput.MasterName, - YggIP: resCluster.Master.PlanetaryIP, - MyceliumIP: resCluster.Master.MyceliumIP, - Resources: k8sDeployInput.Resources, + for i := range k8s.Workers { + k8s.Workers[i].PublicIP = resCluster.Workers[i].ComputedIP + k8s.Workers[i].YggIP = resCluster.Workers[i].PlanetaryIP + k8s.Workers[i].MyceliumIP = resCluster.Workers[i].MyceliumIP } - workers := []models.Worker{} - for i, worker := range k8sDeployInput.Workers { - cru, mru, sru, _, err := calcNodeResources(worker.Resources, false) - if err != nil { - return models.K8sCluster{}, err - } + k8s.NetworkContract = int(networkContractID) + k8s.ClusterContract = int(k8sContractID) + k8s.State = models.StateCreated - workerModel := models.Worker{ - Name: worker.Name, - CRU: cru, - MRU: mru, - SRU: sru, - Public: k8sDeployInput.Public, - PublicIP: resCluster.Workers[i].ComputedIP, - YggIP: resCluster.Workers[i].PlanetaryIP, - MyceliumIP: resCluster.Workers[i].MyceliumIP, - Resources: worker.Resources, - } - workers = append(workers, workerModel) - } - k8sCluster := models.K8sCluster{ - UserID: userID, - NetworkContract: int(networkContractID), - ClusterContract: int(k8sContractID), - Master: master, - Workers: workers, + err = d.db.UpdateK8s(k8s) + if err != nil { + log.Error().Err(err).Send() + return models.K8sCluster{}, err } - return k8sCluster, nil + return k8s, nil } -func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sDeployInput) (uint32, error) { +func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sCluster) (uint32, error) { rootfs := make([]uint64, len(k.Workers)+1) - _, mru, sru, ips, err := calcNodeResources(k.Resources, k.Public) + _, mru, sru, ips, err := CalcNodeResources(k.Master.Resources, k.Master.Public) if err != nil { return 0, err } for _, worker := range k.Workers { - _, m, s, _, err := calcNodeResources(worker.Resources, false) + _, m, s, _, err := CalcNodeResources(worker.Resources, false) if err != nil { return 0, err } @@ -220,102 +200,70 @@ func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sDeployIn FreeSRU: freeSRU, FreeIPs: &ips, FarmIDs: []uint64{1}, - IPv6: &trueVal, + IPv4: &k.Master.Public, } - nodes, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, rootfs, 1) - if err != nil { - return 0, err + if len(strings.TrimSpace(k.Master.Region)) != 0 { + filter.Region = &k.Master.Region } - return uint32(nodes[0].NodeID), nil -} - -// ValidateK8sQuota validates the quota a k8s deployment need -func ValidateK8sQuota(k models.K8sDeployInput, availableResourcesQuota, availablePublicIPsQuota int) (int, error) { - neededQuota, err := calcNeededQuota(k.Resources) + nodes, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, rootfs, 1) if err != nil { return 0, err } - for _, worker := range k.Workers { - workerQuota, err := calcNeededQuota(worker.Resources) - if err != nil { - return 0, err - } - neededQuota += workerQuota - } - - if availableResourcesQuota < neededQuota { - return 0, fmt.Errorf("no available quota %d for kubernetes deployment, you can request a new voucher", availableResourcesQuota) - } - if k.Public && availablePublicIPsQuota < publicQuota { - return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) - } - - return neededQuota, nil + return uint32(nodes[0].NodeID), nil } -func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sDeployInput, adminSSHKey string) (int, error) { - // quota verification - quota, err := d.db.GetUserQuota(user.ID.String()) - if err == gorm.ErrRecordNotFound { - log.Error().Err(err).Send() - return http.StatusNotFound, errors.New("user quota is not found") +func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sCluster, adminSSHKey string) (int, error, error) { + _, err := d.CanDeployK8s(user.ID.String(), k8sDeployInput) + if errors.Is(err, ErrCannotDeploy) { + return http.StatusBadRequest, err, err } if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - - neededQuota, err := ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs) - if err != nil { - log.Error().Err(err).Send() - return http.StatusBadRequest, err + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } // deploy network and cluster node, networkContractID, k8sContractID, err := d.deployK8sClusterWithNetwork(ctx, k8sDeployInput, user.SSHKey, adminSSHKey) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - k8sCluster, err := d.loadK8s(ctx, k8sDeployInput, user.ID.String(), node, networkContractID, k8sContractID) + k8sCluster, err := d.loadK8s(ctx, k8sDeployInput, node, networkContractID, k8sContractID) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - publicIPsQuota := quota.PublicIPs - if k8sDeployInput.Public { - publicIPsQuota -= publicQuota - } - // update quota - err = d.db.UpdateUserQuota(user.ID.String(), quota.Vms-neededQuota, publicIPsQuota) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota is not found") - } - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - err = d.db.CreateK8s(&k8sCluster) + err = d.db.UpdateK8s(k8sCluster) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } // metrics - middlewares.Deployments.WithLabelValues(user.ID.String(), k8sDeployInput.Resources, "master").Inc() + middlewares.Deployments.WithLabelValues(user.ID.String(), k8sDeployInput.Master.Resources, "master").Inc() for _, worker := range k8sDeployInput.Workers { middlewares.Deployments.WithLabelValues(user.ID.String(), worker.Resources, "worker").Inc() } - return 0, nil + return 0, nil, nil } -func convertGBToBytes(gb uint64) *uint64 { - bytes := gb * 1024 * 1024 * 1024 - return &bytes +// CanDeployK8s checks if user can deploy kubernetes +func (d *Deployer) CanDeployK8s(userID string, k8s models.K8sCluster) (float64, error) { + k8sPrice, err := calcPrice(d.prices, k8s.Master.Resources, k8s.Master.Public) + if err != nil { + return 0, errors.Wrap(err, "failed to calculate kubernetes master price") + } + + for _, worker := range k8s.Workers { + workerPrice, err := calcPrice(d.prices, worker.Resources, worker.Public) + if err != nil { + return 0, errors.Wrap(err, "failed to calculate kubernetes worker price") + } + + k8sPrice += workerPrice + } + + return k8sPrice, d.canDeploy(userID, k8sPrice) } diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index 8c8ce561..c4e44859 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -5,23 +5,22 @@ import ( "context" "fmt" "net/http" + "strings" "github.com/codescalers/cloud4students/middlewares" "github.com/codescalers/cloud4students/models" "github.com/codescalers/cloud4students/streams" "github.com/pkg/errors" - "github.com/rs/zerolog/log" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer" "github.com/threefoldtech/tfgrid-sdk-go/grid-client/workloads" "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" - "gorm.io/gorm" ) -func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, sshKey string, adminSSHKey string) (*workloads.VM, uint64, uint64, uint64, error) { +func (d *Deployer) deployVM(ctx context.Context, vmInput models.VM, sshKey string, adminSSHKey string) (*workloads.VM, uint64, uint64, error) { // filter nodes - cru, mru, sru, ips, err := calcNodeResources(vmInput.Resources, vmInput.Public) + cru, mru, sru, ips, err := CalcNodeResources(vmInput.Resources, vmInput.Public) if err != nil { - return nil, 0, 0, 0, err + return nil, 0, 0, err } freeSRU := convertGBToBytes(sru) @@ -31,21 +30,24 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, s FreeSRU: freeSRU, FreeMRU: convertGBToBytes(mru), FreeIPs: &ips, - IPv4: &trueVal, Status: []string{statusUp}, - IPv6: &trueVal, + IPv4: &vmInput.Public, + } + + if len(strings.TrimSpace(vmInput.Region)) != 0 { + filter.Region = &vmInput.Region } nodeIDs, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, nil, 1) if err != nil { - return nil, 0, 0, 0, err + return nil, 0, 0, err } nodeID := uint32(nodeIDs[0].NodeID) // create network workload network, err := buildNetwork(nodeID, fmt.Sprintf("%svmNet", vmInput.Name)) if err != nil { - return nil, 0, 0, 0, err + return nil, 0, 0, err } // create disk @@ -56,7 +58,7 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, s myceliumIPSeed, err := workloads.RandomMyceliumIPSeed() if err != nil { - return nil, 0, 0, 0, err + return nil, 0, 0, err } // create vm workload @@ -79,13 +81,12 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, s NodeID: nodeID, } - dl := workloads.NewDeployment(vmInput.Name, nodeID, "", nil, network.Name, []workloads.Disk{disk}, nil, []workloads.VM{vm}, nil, nil, nil) - dl.SolutionType = vmInput.Name + dl := workloads.NewDeployment(vmInput.Name, nodeID, vmInput.Name, nil, network.Name, []workloads.Disk{disk}, nil, []workloads.VM{vm}, nil, nil, nil) // add network and deployment to be deployed err = d.Redis.PushVM(streams.VMDeployment{Net: &network, DL: &dl}) if err != nil { - return nil, 0, 0, 0, err + return nil, 0, 0, err } // wait for deployments @@ -98,91 +99,54 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, s // checks that network and vm are deployed successfully loadedNet, err := d.tfPluginClient.State.LoadNetworkFromGrid(ctx, dl.NetworkName) if err != nil { - return nil, 0, 0, 0, errors.Wrapf(err, "failed to load network '%s' on node %v", dl.NetworkName, dl.NodeID) + return nil, 0, 0, errors.Wrapf(err, "failed to load network '%s' on node %v", dl.NetworkName, dl.NodeID) } loadedDl, err := d.tfPluginClient.State.LoadDeploymentFromGrid(ctx, nodeID, dl.Name) if err != nil { - return nil, 0, 0, 0, errors.Wrapf(err, "failed to load vm '%s' on node %v", dl.Name, dl.NodeID) - } - - return &loadedDl.Vms[0], loadedDl.ContractID, loadedNet.NodeDeploymentID[nodeID], disk.SizeGB, nil -} - -// ValidateVMQuota validates the quota a vm deployment need -func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota, availablePublicIPsQuota int) (int, error) { - neededQuota, err := calcNeededQuota(vm.Resources) - if err != nil { - return 0, err - } - - if availableResourcesQuota < neededQuota { - return 0, fmt.Errorf("no available quota %d for deployment for resources %s, you can request a new voucher", availableResourcesQuota, vm.Resources) - } - if vm.Public && availablePublicIPsQuota < publicQuota { - return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) + return nil, 0, 0, errors.Wrapf(err, "failed to load vm '%s' on node %v", dl.Name, dl.NodeID) } - return neededQuota, nil + return &loadedDl.Vms[0], loadedDl.ContractID, loadedNet.NodeDeploymentID[nodeID], nil } -func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input models.DeployVMInput, adminSSHKey string) (int, error) { - // check quota of user - quota, err := d.db.GetUserQuota(user.ID.String()) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota is not found") +func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, vm models.VM, adminSSHKey string) (int, error, error) { + _, err := d.CanDeployVM(user.ID.String(), vm) + if errors.Is(err, ErrCannotDeploy) { + return http.StatusBadRequest, err, err } if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - neededQuota, err := ValidateVMQuota(input, quota.Vms, quota.PublicIPs) + deployedVM, contractID, networkContractID, err := d.deployVM(ctx, vm, user.SSHKey, adminSSHKey) if err != nil { - return http.StatusBadRequest, err + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - vm, contractID, networkContractID, diskSize, err := d.deployVM(ctx, input, user.SSHKey, adminSSHKey) - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - - userVM := models.VM{ - UserID: user.ID.String(), - Name: vm.Name, - YggIP: vm.PlanetaryIP, - MyceliumIP: vm.MyceliumIP, - Resources: input.Resources, - Public: input.Public, - PublicIP: vm.ComputedIP, - SRU: diskSize, - CRU: uint64(vm.CPU), - MRU: vm.MemoryMB, - ContractID: contractID, - NetworkContractID: networkContractID, - } - - err = d.db.CreateVM(&userVM) + // Updates after deployment + vm.YggIP = deployedVM.PlanetaryIP + vm.MyceliumIP = deployedVM.MyceliumIP + vm.PublicIP = deployedVM.ComputedIP + vm.ContractID = contractID + vm.NetworkContractID = networkContractID + vm.State = models.StateCreated + + err = d.db.UpdateVM(vm) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - publicIPsQuota := quota.PublicIPs - if input.Public { - publicIPsQuota -= publicQuota - } - // update quota of user - err = d.db.UpdateUserQuota(user.ID.String(), quota.Vms-neededQuota, publicIPsQuota) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("User quota is not found") - } + middlewares.Deployments.WithLabelValues(user.ID.String(), vm.Resources, "vm").Inc() + return 0, nil, nil +} + +// CanDeployVM checks if user can deploy a vm according to its price +func (d *Deployer) CanDeployVM(userID string, vm models.VM) (float64, error) { + vmPrice, err := calcPrice(d.prices, vm.Resources, vm.Public) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return 0, err } - middlewares.Deployments.WithLabelValues(user.ID.String(), input.Resources, "vm").Inc() - return 0, nil + return vmPrice, d.canDeploy(userID, vmPrice) } diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 00000000..b3980ae4 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,3384 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/announcement": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new administrator email and sends it to a specific user as an email and notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Creates a new administrator email and sends it to a specific user as an email and notification", + "parameters": [ + { + "description": "email to be sent", + "name": "email", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.EmailUser" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/balance": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get main TF account balance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Get main TF account balance", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "number" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/deployments": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all users' deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "List all users' deployments", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/app.ListDeploymentsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes all users' deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Deletes all users' deployments", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/deployments/count": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get users' deployments count in the system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Get users' deployments count", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DeploymentsCount" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/invoice": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists user's invoices", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Invoice" + ], + "summary": "Lists user's invoices", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/invoice/all": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all invoices in the system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "List all invoices", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/invoice/pay/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Pay user's invoice", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Invoice" + ], + "summary": "Pay user's invoice", + "parameters": [ + { + "type": "string", + "description": "Invoice ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Payment method and ID", + "name": "payment", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.PayInvoiceInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/invoice/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Gets user's invoice by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Invoice" + ], + "summary": "Gets user's invoice by ID", + "parameters": [ + { + "type": "string", + "description": "Invoice ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Invoice" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/k8s": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get user's kubernetes deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Get user's kubernetes deployments", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.K8sCluster" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deploy kubernetes", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Deploy kubernetes", + "parameters": [ + { + "description": "Kubernetes deployment input", + "name": "kubernetes", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.K8sDeployInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete all user's kubernetes deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Delete all user's kubernetes deployments", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/k8s/validate/{name}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Validate kubernetes name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Validate kubernetes name", + "parameters": [ + { + "type": "string", + "description": "Kubernetes name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/k8s/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get kubernetes deployment using ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Get kubernetes deployment using ID", + "parameters": [ + { + "type": "string", + "description": "Kubernetes cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.K8sCluster" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete kubernetes deployment using ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Kubernetes" + ], + "summary": "Delete kubernetes deployment using ID", + "parameters": [ + { + "type": "string", + "description": "Kubernetes cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/maintenance": { + "get": { + "description": "Gets maintenance flag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Unauthorized/Authorized" + ], + "summary": "Gets maintenance flag", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Maintenance" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates maintenance flag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Updates maintenance flag", + "parameters": [ + { + "description": "Maintenance value to be set", + "name": "maintenance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateMaintenanceInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/nextlaunch": { + "get": { + "description": "Gets next launch state", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Unauthorized/Authorized" + ], + "summary": "Gets next launch state", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.NextLaunch" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates next launch flag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Updates next launch flag", + "parameters": [ + { + "description": "Next launch value to be set", + "name": "nextlaunch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateNextLaunchInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/notification": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists user's notifications", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Lists user's notifications", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Notification" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/notification/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Set user's notifications as seen", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Set user's notifications as seen", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/set_admin": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Sets a user as an admin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Sets a user as an admin", + "parameters": [ + { + "description": "User to be set as admin", + "name": "setAdmin", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SetAdminInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/set_prices": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Set vms and public ips prices prices", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Set prices", + "parameters": [ + { + "description": "Prices to be set", + "name": "prices", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SetPricesInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + } + } + } + }, + "/user": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Change user data", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Change user data", + "parameters": [ + { + "description": "User updates", + "name": "updates", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateUserInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/activate_voucher": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Activate a voucher", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Activate a voucher", + "parameters": [ + { + "description": "Voucher input", + "name": "voucher", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.AddVoucherInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/all": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all users in the system", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "List all users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/apply_voucher": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Apply for a new voucher", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Apply for a new voucher", + "parameters": [ + { + "description": "New voucher details", + "name": "voucher", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.ApplyForVoucherInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/card": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's cards", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Card" + ], + "summary": "List user's cards", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Card" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add a new card", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Card" + ], + "summary": "Add a new card", + "parameters": [ + { + "description": "Card input", + "name": "card", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.AddCardInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/card/default": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Set card as default", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Card" + ], + "summary": "Set card as default", + "parameters": [ + { + "description": "Card input", + "name": "card", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SetDefaultCardInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/card/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete user card", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Card" + ], + "summary": "Delete user card", + "parameters": [ + { + "type": "string", + "description": "Card ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/change_password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Change user password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Change user password", + "parameters": [ + { + "description": "New password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.ChangePasswordInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/charge_balance": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Charge user balance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Charge user balance", + "parameters": [ + { + "description": "Balance charging details", + "name": "balance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.ChargeBalance" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/forgot_password": { + "post": { + "description": "Send code to forget password email for verification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Send code to forget password email for verification", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app.CodeTimeout" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/forgot_password/verify_email": { + "post": { + "description": "Verify user's email to reset password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Verify user's email to reset password", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app.AccessToken" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/refresh_token": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generate a refresh token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Generate a refresh token", + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app.RefreshToken" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/signin": { + "post": { + "description": "Sign in user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Sign in user", + "parameters": [ + { + "description": "User login input", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SignInInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app.AccessToken" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/signup": { + "post": { + "description": "Register a new user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Register a new user", + "parameters": [ + { + "description": "User registration input", + "name": "registration", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SignUpInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/app.CodeTimeout" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/user/signup/verify_email": { + "post": { + "description": "Verify new user's registration", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Verify new user's registration", + "parameters": [ + { + "description": "Verification code input", + "name": "code", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.VerifyCodeInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/vm": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get user's virtual machine deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VM" + ], + "summary": "Get user's virtual machine deployments", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.VM" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deploy virtual machine", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VM" + ], + "summary": "Deploy virtual machine", + "parameters": [ + { + "description": "virtual machine deployment input", + "name": "vm", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.DeployVMInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete all user's virtual machine deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VM" + ], + "summary": "Delete all user's virtual machine deployments", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/vm/validate/{name}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Validate virtual machine name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VM" + ], + "summary": "Validate virtual machine name", + "parameters": [ + { + "type": "string", + "description": "Virtual machine name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/vm/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get virtual machine deployment using ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VM" + ], + "summary": "Get virtual machine deployment using ID", + "parameters": [ + { + "type": "string", + "description": "Virtual machine ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.VM" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete virtual machine deployment using ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VM" + ], + "summary": "Delete virtual machine deployment using ID", + "parameters": [ + { + "type": "string", + "description": "Virtual machine ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/voucher": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists users' vouchers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Voucher (only admins)" + ], + "summary": "Lists users' vouchers", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Voucher" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Approve all vouchers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Voucher (only admins)" + ], + "summary": "Approve all vouchers", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generates a new voucher", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Voucher (only admins)" + ], + "summary": "Generates a new voucher", + "parameters": [ + { + "description": "Voucher details", + "name": "voucher", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.GenerateVoucherInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/voucher/reset": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Resets all users voucher balance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Voucher (only admins)" + ], + "summary": "Resets all users voucher balance", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "number" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/voucher/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update (approve-reject) a voucher", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Voucher (only admins)" + ], + "summary": "Update (approve-reject) a voucher", + "parameters": [ + { + "type": "string", + "description": "Voucher ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Voucher approval state", + "name": "state", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.UpdateVoucherInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + } + }, + "definitions": { + "app.AccessToken": { + "type": "object", + "required": [ + "access_token" + ], + "properties": { + "access_token": { + "type": "string" + } + } + }, + "app.AddCardInput": { + "type": "object", + "required": [ + "card_type", + "payment_method_id" + ], + "properties": { + "card_type": { + "type": "string" + }, + "payment_method_id": { + "type": "string" + } + } + }, + "app.AddVoucherInput": { + "type": "object", + "required": [ + "voucher" + ], + "properties": { + "voucher": { + "type": "string" + } + } + }, + "app.AdminAnnouncement": { + "type": "object", + "required": [ + "announcement", + "subject" + ], + "properties": { + "announcement": { + "type": "string" + }, + "subject": { + "type": "string" + } + } + }, + "app.ApplyForVoucherInput": { + "type": "object", + "required": [ + "balance", + "reason" + ], + "properties": { + "balance": { + "type": "integer", + "minimum": 0 + }, + "reason": { + "type": "string" + } + } + }, + "app.ChangePasswordInput": { + "type": "object", + "required": [ + "confirm_password", + "email", + "password" + ], + "properties": { + "confirm_password": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "app.ChargeBalance": { + "type": "object", + "required": [ + "amount", + "payment_method_id" + ], + "properties": { + "amount": { + "type": "number" + }, + "payment_method_id": { + "type": "string" + } + } + }, + "app.CodeTimeout": { + "type": "object", + "required": [ + "timeout" + ], + "properties": { + "timeout": { + "type": "integer" + } + } + }, + "app.DeployVMInput": { + "type": "object", + "required": [ + "name", + "resources" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 20, + "minLength": 3 + }, + "public": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "string" + } + } + }, + "app.EmailUser": { + "type": "object", + "required": [ + "body", + "email", + "subject" + ], + "properties": { + "body": { + "type": "string" + }, + "email": { + "type": "string" + }, + "subject": { + "type": "string" + } + } + }, + "app.GenerateVoucherInput": { + "type": "object", + "required": [ + "balance", + "length" + ], + "properties": { + "balance": { + "type": "integer" + }, + "length": { + "type": "integer", + "maximum": 20, + "minimum": 3 + } + } + }, + "app.K8sDeployInput": { + "type": "object", + "properties": { + "master_name": { + "type": "string", + "maxLength": 20, + "minLength": 3 + }, + "public": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "string" + }, + "workers": { + "type": "array", + "items": { + "$ref": "#/definitions/app.WorkerInput" + } + } + } + }, + "app.ListDeploymentsResponse": { + "type": "object", + "properties": { + "k8s": { + "type": "array", + "items": { + "$ref": "#/definitions/models.K8sCluster" + } + }, + "vms": { + "type": "array", + "items": { + "$ref": "#/definitions/models.VM" + } + } + } + }, + "app.PayInvoiceInput": { + "type": "object", + "required": [ + "method" + ], + "properties": { + "card_payment_id": { + "type": "string" + }, + "method": { + "$ref": "#/definitions/app.method" + } + } + }, + "app.RefreshToken": { + "type": "object", + "required": [ + "access_token", + "refresh_token" + ], + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "app.SetAdminInput": { + "type": "object", + "required": [ + "admin", + "email" + ], + "properties": { + "admin": { + "type": "boolean" + }, + "email": { + "type": "string" + } + } + }, + "app.SetDefaultCardInput": { + "type": "object", + "required": [ + "payment_method_id" + ], + "properties": { + "payment_method_id": { + "type": "string" + } + } + }, + "app.SetPricesInput": { + "type": "object", + "properties": { + "large": { + "type": "number" + }, + "medium": { + "type": "number" + }, + "public_ip": { + "type": "number" + }, + "small": { + "type": "number" + } + } + }, + "app.SignInInput": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "app.SignUpInput": { + "type": "object", + "required": [ + "confirm_password", + "email", + "first_name", + "last_name", + "password" + ], + "properties": { + "confirm_password": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string", + "maxLength": 20, + "minLength": 3 + }, + "last_name": { + "type": "string", + "maxLength": 20, + "minLength": 3 + }, + "password": { + "type": "string" + }, + "ssh_key": { + "type": "string" + } + } + }, + "app.UpdateMaintenanceInput": { + "type": "object", + "required": [ + "on" + ], + "properties": { + "on": { + "type": "boolean" + } + } + }, + "app.UpdateNextLaunchInput": { + "type": "object", + "required": [ + "launched" + ], + "properties": { + "launched": { + "type": "boolean" + } + } + }, + "app.UpdateUserInput": { + "type": "object", + "properties": { + "confirm_password": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "ssh_key": { + "type": "string" + } + } + }, + "app.UpdateVoucherInput": { + "type": "object", + "required": [ + "approved" + ], + "properties": { + "approved": { + "type": "boolean" + } + } + }, + "app.VerifyCodeInput": { + "type": "object", + "required": [ + "code", + "email" + ], + "properties": { + "code": { + "type": "integer" + }, + "email": { + "type": "string" + } + } + }, + "app.WorkerInput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 20, + "minLength": 3 + }, + "resources": { + "type": "string" + } + } + }, + "app.method": { + "type": "string", + "enum": [ + "card", + "balance", + "voucher", + "voucher+balance", + "voucher+card", + "balance+card", + "voucher+balance+card" + ], + "x-enum-varnames": [ + "card", + "balance", + "voucher", + "voucherAndBalance", + "voucherAndCard", + "balanceAndCard", + "voucherAndBalanceAndCard" + ] + }, + "models.Card": { + "type": "object", + "required": [ + "card_type", + "customer_id", + "fingerprint", + "payment_method_id", + "user_id" + ], + "properties": { + "brand": { + "type": "string" + }, + "card_type": { + "type": "string" + }, + "customer_id": { + "type": "string" + }, + "exp_month": { + "type": "integer" + }, + "exp_year": { + "type": "integer" + }, + "fingerprint": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_4": { + "type": "string" + }, + "payment_method_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.DeploymentItem": { + "type": "object", + "properties": { + "cost": { + "type": "number" + }, + "deployment_id": { + "type": "integer" + }, + "has_public_ip": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "invoice_id": { + "type": "integer" + }, + "period": { + "type": "number" + }, + "resources": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "models.DeploymentsCount": { + "type": "object", + "properties": { + "ips": { + "type": "integer" + }, + "vms": { + "type": "integer" + } + } + }, + "models.Invoice": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "created_at": { + "type": "string" + }, + "deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/models.DeploymentItem" + } + }, + "id": { + "type": "integer" + }, + "last_remainder_at": { + "type": "string" + }, + "paid": { + "type": "boolean" + }, + "paid_at": { + "type": "string" + }, + "payment_details": { + "$ref": "#/definitions/models.PaymentDetails" + }, + "tax": { + "description": "TODO:", + "type": "number" + }, + "total": { + "type": "number" + }, + "user_id": { + "type": "string" + } + } + }, + "models.K8sCluster": { + "type": "object", + "properties": { + "contract_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "failure": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "master": { + "$ref": "#/definitions/models.Master" + }, + "network_contract_id": { + "type": "integer" + }, + "price": { + "type": "number" + }, + "state": { + "type": "string" + }, + "userID": { + "type": "string" + }, + "workers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Worker" + } + } + } + }, + "models.Maintenance": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.Master": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "clusterID": { + "type": "integer" + }, + "cru": { + "type": "integer" + }, + "mru": { + "type": "integer" + }, + "mycelium_ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "public_ip": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "string" + }, + "sru": { + "type": "integer" + }, + "ygg_ip": { + "type": "string" + } + } + }, + "models.NextLaunch": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "launched": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.Notification": { + "type": "object", + "required": [ + "msg", + "seen", + "type", + "user_id" + ], + "properties": { + "id": { + "type": "integer" + }, + "msg": { + "type": "string" + }, + "seen": { + "type": "boolean" + }, + "type": { + "description": "to allow redirecting from notifications to the right pages", + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.PaymentDetails": { + "type": "object", + "properties": { + "balance": { + "type": "number" + }, + "card": { + "type": "number" + }, + "invoice_id": { + "type": "integer" + }, + "voucher_balance": { + "type": "number" + } + } + }, + "models.User": { + "type": "object", + "required": [ + "email", + "first_name", + "hashed_password", + "last_name" + ], + "properties": { + "admin": { + "description": "checks if user type is admin", + "type": "boolean" + }, + "balance": { + "type": "number" + }, + "code": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "hashed_password": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "ssh_key": { + "type": "string" + }, + "stripe_customer_id": { + "type": "string" + }, + "stripe_payment_method_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "verified": { + "type": "boolean" + }, + "voucher_balance": { + "type": "number" + } + } + }, + "models.VM": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "contractID": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "cru": { + "type": "integer" + }, + "failure": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "mru": { + "type": "integer" + }, + "mycelium_ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "networkContractID": { + "type": "integer" + }, + "price": { + "type": "number" + }, + "public": { + "type": "boolean" + }, + "public_ip": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "string" + }, + "sru": { + "type": "integer" + }, + "state": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "ygg_ip": { + "type": "string" + } + } + }, + "models.Voucher": { + "type": "object", + "required": [ + "approved", + "balance", + "reason", + "rejected", + "used", + "user_id" + ], + "properties": { + "approved": { + "type": "boolean" + }, + "balance": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "reason": { + "type": "string" + }, + "rejected": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + }, + "used": { + "type": "boolean" + }, + "user_id": { + "type": "string" + }, + "voucher": { + "type": "string" + } + } + }, + "models.Worker": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "clusterID": { + "type": "integer" + }, + "cru": { + "type": "integer" + }, + "mru": { + "type": "integer" + }, + "mycelium_ip": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "public_ip": { + "type": "string" + }, + "region": { + "type": "string" + }, + "resources": { + "type": "string" + }, + "sru": { + "type": "integer" + }, + "ygg_ip": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "C4All API", + Description: "This is C4All API documentation using Swagger in Golang", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 00000000..45469305 --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,2244 @@ +definitions: + app.AccessToken: + properties: + access_token: + type: string + required: + - access_token + type: object + app.AddCardInput: + properties: + card_type: + type: string + payment_method_id: + type: string + required: + - card_type + - payment_method_id + type: object + app.AddVoucherInput: + properties: + voucher: + type: string + required: + - voucher + type: object + app.AdminAnnouncement: + properties: + announcement: + type: string + subject: + type: string + required: + - announcement + - subject + type: object + app.ApplyForVoucherInput: + properties: + balance: + minimum: 0 + type: integer + reason: + type: string + required: + - balance + - reason + type: object + app.ChangePasswordInput: + properties: + confirm_password: + type: string + email: + type: string + password: + type: string + required: + - confirm_password + - email + - password + type: object + app.ChargeBalance: + properties: + amount: + type: number + payment_method_id: + type: string + required: + - amount + - payment_method_id + type: object + app.CodeTimeout: + properties: + timeout: + type: integer + required: + - timeout + type: object + app.DeployVMInput: + properties: + name: + maxLength: 20 + minLength: 3 + type: string + public: + type: boolean + region: + type: string + resources: + type: string + required: + - name + - resources + type: object + app.EmailUser: + properties: + body: + type: string + email: + type: string + subject: + type: string + required: + - body + - email + - subject + type: object + app.GenerateVoucherInput: + properties: + balance: + type: integer + length: + maximum: 20 + minimum: 3 + type: integer + required: + - balance + - length + type: object + app.K8sDeployInput: + properties: + master_name: + maxLength: 20 + minLength: 3 + type: string + public: + type: boolean + region: + type: string + resources: + type: string + workers: + items: + $ref: '#/definitions/app.WorkerInput' + type: array + type: object + app.ListDeploymentsResponse: + properties: + k8s: + items: + $ref: '#/definitions/models.K8sCluster' + type: array + vms: + items: + $ref: '#/definitions/models.VM' + type: array + type: object + app.PayInvoiceInput: + properties: + card_payment_id: + type: string + method: + $ref: '#/definitions/app.method' + required: + - method + type: object + app.RefreshToken: + properties: + access_token: + type: string + refresh_token: + type: string + required: + - access_token + - refresh_token + type: object + app.SetAdminInput: + properties: + admin: + type: boolean + email: + type: string + required: + - admin + - email + type: object + app.SetDefaultCardInput: + properties: + payment_method_id: + type: string + required: + - payment_method_id + type: object + app.SetPricesInput: + properties: + large: + type: number + medium: + type: number + public_ip: + type: number + small: + type: number + type: object + app.SignInInput: + properties: + email: + type: string + password: + type: string + required: + - email + - password + type: object + app.SignUpInput: + properties: + confirm_password: + type: string + email: + type: string + first_name: + maxLength: 20 + minLength: 3 + type: string + last_name: + maxLength: 20 + minLength: 3 + type: string + password: + type: string + ssh_key: + type: string + required: + - confirm_password + - email + - first_name + - last_name + - password + type: object + app.UpdateMaintenanceInput: + properties: + "on": + type: boolean + required: + - "on" + type: object + app.UpdateNextLaunchInput: + properties: + launched: + type: boolean + required: + - launched + type: object + app.UpdateUserInput: + properties: + confirm_password: + type: string + first_name: + type: string + last_name: + type: string + password: + type: string + ssh_key: + type: string + type: object + app.UpdateVoucherInput: + properties: + approved: + type: boolean + required: + - approved + type: object + app.VerifyCodeInput: + properties: + code: + type: integer + email: + type: string + required: + - code + - email + type: object + app.WorkerInput: + properties: + name: + maxLength: 20 + minLength: 3 + type: string + resources: + type: string + type: object + app.method: + enum: + - card + - balance + - voucher + - voucher+balance + - voucher+card + - balance+card + - voucher+balance+card + type: string + x-enum-varnames: + - card + - balance + - voucher + - voucherAndBalance + - voucherAndCard + - balanceAndCard + - voucherAndBalanceAndCard + models.Card: + properties: + brand: + type: string + card_type: + type: string + customer_id: + type: string + exp_month: + type: integer + exp_year: + type: integer + fingerprint: + type: string + id: + type: integer + last_4: + type: string + payment_method_id: + type: string + user_id: + type: string + required: + - card_type + - customer_id + - fingerprint + - payment_method_id + - user_id + type: object + models.DeploymentItem: + properties: + cost: + type: number + deployment_id: + type: integer + has_public_ip: + type: boolean + id: + type: integer + invoice_id: + type: integer + period: + type: number + resources: + type: string + type: + type: string + type: object + models.DeploymentsCount: + properties: + ips: + type: integer + vms: + type: integer + type: object + models.Invoice: + properties: + created_at: + type: string + deployments: + items: + $ref: '#/definitions/models.DeploymentItem' + type: array + id: + type: integer + last_remainder_at: + type: string + paid: + type: boolean + paid_at: + type: string + payment_details: + $ref: '#/definitions/models.PaymentDetails' + tax: + description: 'TODO:' + type: number + total: + type: number + user_id: + type: string + required: + - user_id + type: object + models.K8sCluster: + properties: + contract_id: + type: integer + created_at: + type: string + failure: + type: string + id: + type: integer + master: + $ref: '#/definitions/models.Master' + network_contract_id: + type: integer + price: + type: number + state: + type: string + userID: + type: string + workers: + items: + $ref: '#/definitions/models.Worker' + type: array + type: object + models.Maintenance: + properties: + active: + type: boolean + id: + type: integer + updated_at: + type: string + type: object + models.Master: + properties: + clusterID: + type: integer + cru: + type: integer + mru: + type: integer + mycelium_ip: + type: string + name: + type: string + public: + type: boolean + public_ip: + type: string + region: + type: string + resources: + type: string + sru: + type: integer + ygg_ip: + type: string + required: + - name + type: object + models.NextLaunch: + properties: + id: + type: integer + launched: + type: boolean + updated_at: + type: string + type: object + models.Notification: + properties: + id: + type: integer + msg: + type: string + seen: + type: boolean + type: + description: to allow redirecting from notifications to the right pages + type: string + user_id: + type: string + required: + - msg + - seen + - type + - user_id + type: object + models.PaymentDetails: + properties: + balance: + type: number + card: + type: number + invoice_id: + type: integer + voucher_balance: + type: number + type: object + models.User: + properties: + admin: + description: checks if user type is admin + type: boolean + balance: + type: number + code: + type: integer + email: + type: string + first_name: + type: string + hashed_password: + items: + type: integer + type: array + id: + type: string + last_name: + type: string + ssh_key: + type: string + stripe_customer_id: + type: string + stripe_payment_method_id: + type: string + updated_at: + type: string + verified: + type: boolean + voucher_balance: + type: number + required: + - email + - first_name + - hashed_password + - last_name + type: object + models.VM: + properties: + contractID: + type: integer + created_at: + type: string + cru: + type: integer + failure: + type: string + id: + type: integer + mru: + type: integer + mycelium_ip: + type: string + name: + type: string + networkContractID: + type: integer + price: + type: number + public: + type: boolean + public_ip: + type: string + region: + type: string + resources: + type: string + sru: + type: integer + state: + type: string + user_id: + type: string + ygg_ip: + type: string + required: + - name + type: object + models.Voucher: + properties: + approved: + type: boolean + balance: + type: integer + created_at: + type: string + id: + type: integer + reason: + type: string + rejected: + type: boolean + updated_at: + type: string + used: + type: boolean + user_id: + type: string + voucher: + type: string + required: + - approved + - balance + - reason + - rejected + - used + - user_id + type: object + models.Worker: + properties: + clusterID: + type: integer + cru: + type: integer + mru: + type: integer + mycelium_ip: + type: string + name: + type: string + public: + type: boolean + public_ip: + type: string + region: + type: string + resources: + type: string + sru: + type: integer + ygg_ip: + type: string + required: + - name + type: object +info: + contact: + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: This is C4All API documentation using Swagger in Golang + license: + name: Apache + url: https://www.apache.org/licenses/LICENSE-2.0 + title: C4All API + version: "1.0" +paths: + /announcement: + post: + consumes: + - application/json + description: Creates a new administrator email and sends it to a specific user + as an email and notification + parameters: + - description: email to be sent + in: body + name: email + required: true + schema: + $ref: '#/definitions/app.EmailUser' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Creates a new administrator email and sends it to a specific user as + an email and notification + tags: + - Admin + /balance: + get: + consumes: + - application/json + description: Get main TF account balance + produces: + - application/json + responses: + "200": + description: OK + schema: + type: number + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get main TF account balance + tags: + - Admin + /deployments: + delete: + consumes: + - application/json + description: Deletes all users' deployments + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Deletes all users' deployments + tags: + - Admin + get: + consumes: + - application/json + description: List all users' deployments + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/app.ListDeploymentsResponse' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List all users' deployments + tags: + - Admin + /deployments/count: + get: + consumes: + - application/json + description: Get users' deployments count in the system + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.DeploymentsCount' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get users' deployments count + tags: + - Admin + /invoice: + get: + consumes: + - application/json + description: Lists user's invoices + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Invoice' + type: array + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Lists user's invoices + tags: + - Invoice + /invoice/{id}: + get: + consumes: + - application/json + description: Gets user's invoice by ID + parameters: + - description: Invoice ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Invoice' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Gets user's invoice by ID + tags: + - Invoice + /invoice/all: + get: + consumes: + - application/json + description: List all invoices in the system + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Invoice' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List all invoices + tags: + - Admin + /invoice/pay/{id}: + put: + consumes: + - application/json + description: Pay user's invoice + parameters: + - description: Invoice ID + in: path + name: id + required: true + type: string + - description: Payment method and ID + in: body + name: payment + required: true + schema: + $ref: '#/definitions/app.PayInvoiceInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Pay user's invoice + tags: + - Invoice + /k8s: + delete: + consumes: + - application/json + description: Delete all user's kubernetes deployments + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Delete all user's kubernetes deployments + tags: + - Kubernetes + get: + consumes: + - application/json + description: Get user's kubernetes deployments + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.K8sCluster' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get user's kubernetes deployments + tags: + - Kubernetes + post: + consumes: + - application/json + description: Deploy kubernetes + parameters: + - description: Kubernetes deployment input + in: body + name: kubernetes + required: true + schema: + $ref: '#/definitions/app.K8sDeployInput' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Deploy kubernetes + tags: + - Kubernetes + /k8s/{id}: + delete: + consumes: + - application/json + description: Delete kubernetes deployment using ID + parameters: + - description: Kubernetes cluster ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Delete kubernetes deployment using ID + tags: + - Kubernetes + get: + consumes: + - application/json + description: Get kubernetes deployment using ID + parameters: + - description: Kubernetes cluster ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.K8sCluster' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get kubernetes deployment using ID + tags: + - Kubernetes + /k8s/validate/{name}: + get: + consumes: + - application/json + description: Validate kubernetes name + parameters: + - description: Kubernetes name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Validate kubernetes name + tags: + - Kubernetes + /maintenance: + get: + consumes: + - application/json + description: Gets maintenance flag + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Maintenance' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Gets maintenance flag + tags: + - Unauthorized/Authorized + put: + consumes: + - application/json + description: Updates maintenance flag + parameters: + - description: Maintenance value to be set + in: body + name: maintenance + required: true + schema: + $ref: '#/definitions/app.UpdateMaintenanceInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Updates maintenance flag + tags: + - Admin + /nextlaunch: + get: + consumes: + - application/json + description: Gets next launch state + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.NextLaunch' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Gets next launch state + tags: + - Unauthorized/Authorized + put: + consumes: + - application/json + description: Updates next launch flag + parameters: + - description: Next launch value to be set + in: body + name: nextlaunch + required: true + schema: + $ref: '#/definitions/app.UpdateNextLaunchInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Updates next launch flag + tags: + - Admin + /notification: + get: + consumes: + - application/json + description: Lists user's notifications + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Notification' + type: array + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Lists user's notifications + tags: + - Notification + /notification/{id}: + put: + consumes: + - application/json + description: Set user's notifications as seen + parameters: + - description: Notification ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Set user's notifications as seen + tags: + - Notification + /set_admin: + put: + consumes: + - application/json + description: Sets a user as an admin + parameters: + - description: User to be set as admin + in: body + name: setAdmin + required: true + schema: + $ref: '#/definitions/app.SetAdminInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Sets a user as an admin + tags: + - Admin + /set_prices: + put: + consumes: + - application/json + description: Set vms and public ips prices prices + parameters: + - description: Prices to be set + in: body + name: prices + required: true + schema: + $ref: '#/definitions/app.SetPricesInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + security: + - BearerAuth: [] + summary: Set prices + tags: + - Admin + /user: + get: + consumes: + - application/json + description: Get user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get user + tags: + - User + put: + consumes: + - application/json + description: Change user data + parameters: + - description: User updates + in: body + name: updates + required: true + schema: + $ref: '#/definitions/app.UpdateUserInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Change user data + tags: + - User + /user/activate_voucher: + put: + consumes: + - application/json + description: Activate a voucher + parameters: + - description: Voucher input + in: body + name: voucher + required: true + schema: + $ref: '#/definitions/app.AddVoucherInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Activate a voucher + tags: + - User + /user/all: + get: + consumes: + - application/json + description: List all users in the system + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.User' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List all users + tags: + - Admin + /user/apply_voucher: + post: + consumes: + - application/json + description: Apply for a new voucher + parameters: + - description: New voucher details + in: body + name: voucher + required: true + schema: + $ref: '#/definitions/app.ApplyForVoucherInput' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Apply for a new voucher + tags: + - User + /user/card: + get: + consumes: + - application/json + description: List user's cards + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Card' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List user's cards + tags: + - Card + post: + consumes: + - application/json + description: Add a new card + parameters: + - description: Card input + in: body + name: card + required: true + schema: + $ref: '#/definitions/app.AddCardInput' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Add a new card + tags: + - Card + /user/card/{id}: + delete: + consumes: + - application/json + description: Delete user card + parameters: + - description: Card ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Delete user card + tags: + - Card + /user/card/default: + put: + consumes: + - application/json + description: Set card as default + parameters: + - description: Card input + in: body + name: card + required: true + schema: + $ref: '#/definitions/app.SetDefaultCardInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Set card as default + tags: + - Card + /user/change_password: + put: + consumes: + - application/json + description: Change user password + parameters: + - description: New password + in: body + name: password + required: true + schema: + $ref: '#/definitions/app.ChangePasswordInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Change user password + tags: + - User + /user/charge_balance: + put: + consumes: + - application/json + description: Charge user balance + parameters: + - description: Balance charging details + in: body + name: balance + required: true + schema: + $ref: '#/definitions/app.ChargeBalance' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Charge user balance + tags: + - User + /user/forgot_password: + post: + consumes: + - application/json + description: Send code to forget password email for verification + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/app.CodeTimeout' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Send code to forget password email for verification + tags: + - User + /user/forgot_password/verify_email: + post: + consumes: + - application/json + description: Verify user's email to reset password + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/app.AccessToken' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Verify user's email to reset password + tags: + - User + /user/refresh_token: + post: + consumes: + - application/json + description: Generate a refresh token + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/app.RefreshToken' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Generate a refresh token + tags: + - User + /user/signin: + post: + consumes: + - application/json + description: Sign in user + parameters: + - description: User login input + in: body + name: login + required: true + schema: + $ref: '#/definitions/app.SignInInput' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/app.AccessToken' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Sign in user + tags: + - User + /user/signup: + post: + consumes: + - application/json + description: Register a new user + parameters: + - description: User registration input + in: body + name: registration + required: true + schema: + $ref: '#/definitions/app.SignUpInput' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/app.CodeTimeout' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Register a new user + tags: + - User + /user/signup/verify_email: + post: + consumes: + - application/json + description: Verify new user's registration + parameters: + - description: Verification code input + in: body + name: code + required: true + schema: + $ref: '#/definitions/app.VerifyCodeInput' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Verify new user's registration + tags: + - User + /vm: + delete: + consumes: + - application/json + description: Delete all user's virtual machine deployments + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Delete all user's virtual machine deployments + tags: + - VM + get: + consumes: + - application/json + description: Get user's virtual machine deployments + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.VM' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get user's virtual machine deployments + tags: + - VM + post: + consumes: + - application/json + description: Deploy virtual machine + parameters: + - description: virtual machine deployment input + in: body + name: vm + required: true + schema: + $ref: '#/definitions/app.DeployVMInput' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Deploy virtual machine + tags: + - VM + /vm/{id}: + delete: + consumes: + - application/json + description: Delete virtual machine deployment using ID + parameters: + - description: Virtual machine ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Delete virtual machine deployment using ID + tags: + - VM + get: + consumes: + - application/json + description: Get virtual machine deployment using ID + parameters: + - description: Virtual machine ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.VM' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Get virtual machine deployment using ID + tags: + - VM + /vm/validate/{name}: + get: + consumes: + - application/json + description: Validate virtual machine name + parameters: + - description: Virtual machine name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Validate virtual machine name + tags: + - VM + /voucher: + get: + consumes: + - application/json + description: Lists users' vouchers + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Voucher' + type: array + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Lists users' vouchers + tags: + - Voucher (only admins) + post: + consumes: + - application/json + description: Generates a new voucher + parameters: + - description: Voucher details + in: body + name: voucher + required: true + schema: + $ref: '#/definitions/app.GenerateVoucherInput' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Generates a new voucher + tags: + - Voucher (only admins) + put: + consumes: + - application/json + description: Approve all vouchers + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Approve all vouchers + tags: + - Voucher (only admins) + /voucher/{id}: + put: + consumes: + - application/json + description: Update (approve-reject) a voucher + parameters: + - description: Voucher ID + in: path + name: id + required: true + type: string + - description: Voucher approval state + in: body + name: state + required: true + schema: + $ref: '#/definitions/app.UpdateVoucherInput' + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Update (approve-reject) a voucher + tags: + - Voucher (only admins) + /voucher/reset: + put: + consumes: + - application/json + description: Resets all users voucher balance + produces: + - application/json + responses: + "200": + description: OK + schema: + type: number + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Resets all users voucher balance + tags: + - Voucher (only admins) +swagger: "2.0" diff --git a/server/go.mod b/server/go.mod index c3731c1b..29d73c46 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,8 @@ module github.com/codescalers/cloud4students -go 1.21 +go 1.22.0 + +toolchain go1.23.4 require ( github.com/caitlin615/nist-password-validator v0.0.0-20190321104149-45ab5d3140de @@ -14,10 +16,13 @@ require ( github.com/sendgrid/sendgrid-go v3.16.0+incompatible github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 + github.com/stripe/stripe-go/v81 v81.1.1 + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.4 github.com/threefoldtech/tfgrid-sdk-go/grid-client v0.16.0 github.com/threefoldtech/tfgrid-sdk-go/grid-proxy v0.16.0 - golang.org/x/crypto v0.29.0 - golang.org/x/text v0.20.0 + golang.org/x/crypto v0.30.0 + golang.org/x/text v0.21.0 gopkg.in/validator.v2 v2.0.1 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.11 @@ -25,6 +30,7 @@ require ( require ( github.com/ChainSafe/go-schnorrkel v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect @@ -38,6 +44,10 @@ require ( github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/ethereum/go-ethereum v1.11.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect @@ -52,7 +62,9 @@ require ( github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect @@ -68,6 +80,7 @@ require ( github.com/rs/cors v1.10.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/swaggo/files v1.0.1 // indirect github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 // indirect github.com/threefoldtech/tfgrid-sdk-go/rmb-sdk-go v0.15.18 // indirect github.com/threefoldtech/zos v0.5.6-0.20240902110349-172a0a29a6ee // indirect @@ -76,8 +89,10 @@ require ( github.com/vedhavyas/go-subkey v1.0.3 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/tools v0.28.0 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b // indirect gonum.org/v1/gonum v0.15.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/server/go.sum b/server/go.sum index 4c1f80e9..43c9a991 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,5 +1,7 @@ github.com/ChainSafe/go-schnorrkel v1.1.0 h1:rZ6EU+CZFCjB4sHUE1jIu8VDoB/wRKZxoe1tkcO71Wk= github.com/ChainSafe/go-schnorrkel v1.1.0/go.mod h1:ABkENxiP+cvjFiByMIZ9LYbRoNNLeBLiakC1XeTFxfE= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= @@ -42,6 +44,14 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= @@ -89,6 +99,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -99,6 +111,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -158,8 +172,17 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stripe/stripe-go/v81 v81.1.1 h1:5wpVhqvkHkZyYOpve5LOoQUw6YeDj6g2a8RLI1dsk14= +github.com/stripe/stripe-go/v81 v81.1.1/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 h1:XIXVdFrum50Wnxv62sS+cEgqHtvdInWB2Co8AJVJ8xs= github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4/go.mod h1:cOL5YgHUmDG5SAXrsZxFjUECRQQuAqOoqvXhZG5sEUw= github.com/threefoldtech/tfgrid-sdk-go/grid-client v0.16.0 h1:Tou1RTyeH5M4qDl1QjqddRFTLr9UPINDpf38cJPqJfw= @@ -178,26 +201,38 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/vedhavyas/go-subkey v1.0.3 h1:iKR33BB/akKmcR2PMlXPBeeODjWLM90EL98OrOGs8CA= github.com/vedhavyas/go-subkey v1.0.3/go.mod h1:CloUaFQSSTdWnINfBRFjVMkWXZANW+nd8+TI5jYcl6Y= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -207,20 +242,38 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b h1:l4mBVCYinjzZuR5DtxHuBD6wyd4348TGiavJ5vLrhEc= diff --git a/server/internal/config_parser.go b/server/internal/config_parser.go index 678de081..ae951709 100644 --- a/server/internal/config_parser.go +++ b/server/internal/config_parser.go @@ -21,6 +21,9 @@ type Configuration struct { NotifyAdminsIntervalHours int `json:"notifyAdminsIntervalHours"` AdminSSHKey string `json:"adminSSHKey"` BalanceThreshold int `json:"balanceThreshold"` + PricesPerMonth Prices `json:"prices"` + Currency string `json:"currency" validate:"nonzero"` + StripeSecret string `json:"stripe_secret" validate:"nonzero"` } // Server struct to hold server's information @@ -57,6 +60,14 @@ type GridAccount struct { Network string `json:"network" validate:"nonzero"` } +// Prices struct to hold vm types prices +type Prices struct { + SmallVM float64 `json:"small_vm" validate:"nonzero"` + MediumVM float64 `json:"medium_vm" validate:"nonzero"` + LargeVM float64 `json:"large_vm" validate:"nonzero"` + PublicIP float64 `json:"public_ip" validate:"nonzero"` +} + // ReadConfFile read configurations of json file func ReadConfFile(path string) (Configuration, error) { config := Configuration{NotifyAdminsIntervalHours: 6, BalanceThreshold: 2000} diff --git a/server/internal/email_sender.go b/server/internal/email_sender.go index 22b5a5f6..f48da153 100644 --- a/server/internal/email_sender.go +++ b/server/internal/email_sender.go @@ -144,18 +144,17 @@ func AdminAnnouncementMailContent(adminSubject, announcement, host, username str subject := "New Announcement! 📢 " + adminSubject body := string(adminAnnouncement) body = strings.ReplaceAll(body, "-subject-", adminSubject) - body = strings.ReplaceAll(body, "-announcement-", strings.ReplaceAll(announcement, "\n", "
")) + body = strings.ReplaceAll(body, "-body-", strings.ReplaceAll(announcement, "\n", "
")) body = strings.ReplaceAll(body, "-name-", cases.Title(language.Und).String(username)) body = strings.ReplaceAll(body, "-host-", host) return subject, body } // AdminMailContent gets the email content for administrator emails -func AdminMailContent(adminSubject, email, host, username string) (string, string) { - subject := "Hey! 📢 " + adminSubject +func AdminMailContent(subject, email, host, username string) (string, string) { body := string(adminAnnouncement) - body = strings.ReplaceAll(body, "-subject-", adminSubject) - body = strings.ReplaceAll(body, "-announcement-", strings.ReplaceAll(email, "\n", "
")) + body = strings.ReplaceAll(body, "-subject-", subject) + body = strings.ReplaceAll(body, "-body-", strings.ReplaceAll(email, "\n", "
")) body = strings.ReplaceAll(body, "-name-", cases.Title(language.Und).String(username)) body = strings.ReplaceAll(body, "-host-", host) return subject, body diff --git a/server/internal/email_sender_test.go b/server/internal/email_sender_test.go index 821289e4..9153fc4c 100644 --- a/server/internal/email_sender_test.go +++ b/server/internal/email_sender_test.go @@ -98,7 +98,7 @@ func TestAdminAnnouncementMailContent(t *testing.T) { assert.Equal(t, subject, "New Announcement! 📢 subject!") want := string(adminAnnouncement) want = strings.ReplaceAll(want, "-subject-", "subject!") - want = strings.ReplaceAll(want, "-announcement-", "announcement!") + want = strings.ReplaceAll(want, "-body-", "announcement!") want = strings.ReplaceAll(want, "-host-", "") want = strings.ReplaceAll(want, "-name-", "") assert.Equal(t, body, want) diff --git a/server/internal/templates/adminAnnouncement.html b/server/internal/templates/adminAnnouncement.html index b58d8661..6c98741a 100644 --- a/server/internal/templates/adminAnnouncement.html +++ b/server/internal/templates/adminAnnouncement.html @@ -212,7 +212,7 @@ > Dear -name-, -

-announcement-

+

-body-

diff --git a/server/main.go b/server/main.go index 74bf1979..2e320505 100644 --- a/server/main.go +++ b/server/main.go @@ -28,6 +28,14 @@ func init() { } } +// @title C4All API +// @version 1.0 +// @description This is C4All API documentation using Swagger in Golang +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io +// @license.name Apache +// @license.url https://www.apache.org/licenses/LICENSE-2.0 func main() { cmd.Execute() } diff --git a/server/middlewares/admin_access.go b/server/middlewares/admin_access.go index ecad4998..d6c3c522 100644 --- a/server/middlewares/admin_access.go +++ b/server/middlewares/admin_access.go @@ -27,7 +27,7 @@ func AdminAccess(db models.DB) func(http.Handler) http.Handler { } if !user.Admin { - writeErrResponse(r, w, http.StatusUnauthorized, fmt.Sprintf("user '%s' doesn't have an admin access", user.Name)) + writeErrResponse(r, w, http.StatusUnauthorized, fmt.Sprintf("user '%s %s' doesn't have an admin access", user.FirstName, user.LastName)) return } h.ServeHTTP(w, r) diff --git a/server/middlewares/grafana_metrics.go b/server/middlewares/grafana_metrics.go index b7e0285d..50924953 100644 --- a/server/middlewares/grafana_metrics.go +++ b/server/middlewares/grafana_metrics.go @@ -18,7 +18,7 @@ var UserCreations = prometheus.NewCounterVec( Name: "http_request_create_user", // metric name Help: "Count of users registered.", }, - []string{"user", "email", "college", "team"}, // labels + []string{"user", "email"}, // labels ) // VoucherActivated metrics @@ -27,7 +27,7 @@ var VoucherActivated = prometheus.NewCounterVec( Name: "http_request_activate_voucher", // metric name Help: "Count of activated voucher.", }, - []string{"user", "voucher", "vms", "public_ips"}, // labels + []string{"user", "voucher", "balance"}, // labels ) // VoucherApplied metrics @@ -36,7 +36,7 @@ var VoucherApplied = prometheus.NewCounterVec( Name: "http_request_apply_voucher", // metric name Help: "Count of applied voucher.", }, - []string{"user", "voucher", "vms", "public_ips"}, // labels + []string{"user", "voucher", "balance"}, // labels ) // Deployments metrics diff --git a/server/models/api_inputs.go b/server/models/api_inputs.go deleted file mode 100644 index dd42792b..00000000 --- a/server/models/api_inputs.go +++ /dev/null @@ -1,23 +0,0 @@ -// Package models for database models -package models - -// DeployVMInput struct takes input of vm from user -type DeployVMInput struct { - Name string `json:"name" binding:"required" validate:"min=3,max=20"` - Resources string `json:"resources" binding:"required"` - Public bool `json:"public"` -} - -// K8sDeployInput deploy k8s cluster input -type K8sDeployInput struct { - MasterName string `json:"master_name" validate:"min=3,max=20"` - Resources string `json:"resources"` - Public bool `json:"public"` - Workers []Worker `json:"workers"` -} - -// WorkerInput deploy k8s worker input -type WorkerInput struct { - Name string `json:"name" validate:"min=3,max=20"` - Resources string `json:"resources"` -} diff --git a/server/models/card.go b/server/models/card.go new file mode 100644 index 00000000..ba34208d --- /dev/null +++ b/server/models/card.go @@ -0,0 +1,57 @@ +package models + +import "gorm.io/gorm" + +type Card struct { + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" binding:"required"` + PaymentMethodID string `json:"payment_method_id" gorm:"unique" binding:"required"` + CustomerID string `json:"customer_id" binding:"required"` + Fingerprint string `json:"fingerprint" gorm:"unique" binding:"required"` + CardType string `json:"card_type" binding:"required"` + ExpMonth int64 `json:"exp_month"` + ExpYear int64 `json:"exp_year"` + Last4 string `json:"last_4"` + Brand string `json:"brand"` +} + +// AddCard adds a new card +func (d *DB) AddCard(c *Card) error { + result := d.db.Create(&c) + return result.Error +} + +// GetCard gets a user card using ID +func (d *DB) GetCard(id int) (Card, error) { + var res Card + return res, d.db.First(&res, &id).Error +} + +// GetCardByPaymentMethod gets a user card using stripe payment method ID +func (d *DB) GetCardByPaymentMethod(paymentMethodID string) (Card, error) { + var res Card + return res, d.db.First(&res, "payment_method_id = ?", paymentMethodID).Error +} + +// IsCardUnique gets checks if the entered card is not a duplicate +func (d *DB) IsCardUnique(fingerprint string) (bool, error) { + var res []Card + err := d.db.Find(&res, "fingerprint = ?", fingerprint).Error + if err == gorm.ErrRecordNotFound || len(res) == 0 { + return true, nil + } + + return false, err +} + +// GetUserCards gets user cards +func (d *DB) GetUserCards(userID string) ([]Card, error) { + var res []Card + return res, d.db.Find(&res, "user_id = ?", userID).Error +} + +// DeleteCard deletes card by its id +func (d *DB) DeleteCard(id int) error { + var card Card + return d.db.Delete(&card, id).Error +} diff --git a/server/models/database.go b/server/models/database.go index 52093922..ab8ba3fa 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -2,10 +2,7 @@ package models import ( - "time" - "gorm.io/driver/sqlite" - "gorm.io/gorm/clause" "gorm.io/gorm" ) @@ -32,11 +29,18 @@ func (d *DB) Connect(file string) error { // Migrate migrates db schema func (d *DB) Migrate() error { - err := d.db.AutoMigrate(&User{}, &Quota{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}) + err := d.db.AutoMigrate( + &User{}, &State{}, &Card{}, &Invoice{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, + &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}, + ) if err != nil { return err } + if err := d.CreateState(); err != nil { + return err + } + // add maintenance if err := d.db.Delete(&Maintenance{}, "1 = 1").Error; err != nil { return err @@ -50,364 +54,3 @@ func (d *DB) Migrate() error { } return d.db.Create(&Maintenance{}).Error } - -// CreateUser creates new user -func (d *DB) CreateUser(u *User) error { - result := d.db.Create(&u) - return result.Error -} - -// GetUserByEmail returns user by its email -func (d *DB) GetUserByEmail(email string) (User, error) { - var res User - query := d.db.First(&res, "email = ?", email) - return res, query.Error -} - -// GetUserByID returns user by its id -func (d *DB) GetUserByID(id string) (User, error) { - var res User - query := d.db.First(&res, "id = ?", id) - return res, query.Error -} - -// ListAllUsers returns all users to admin -func (d *DB) ListAllUsers() ([]UserUsedQuota, error) { - var res []UserUsedQuota - query := d.db.Table("users"). - Select("*, users.id as user_id, sum(vouchers.vms) as vms, sum(vouchers.public_ips) as public_ips, sum(vouchers.vms) - quota.vms as used_vms, sum(vouchers.public_ips) - quota.public_ips as used_public_ips"). - Joins("left join quota on quota.user_id = users.id"). - Joins("left join vouchers on vouchers.used = true and vouchers.user_id = users.id"). - Where("verified = true"). - Group("users.id"). - Scan(&res) - return res, query.Error -} - -// CountAllDeployments returns deployments and IPs count -func (d *DB) CountAllDeployments() (DeploymentsCount, error) { - var vmsCount int64 - result := d.db.Table("vms").Count(&vmsCount) - if result.Error != nil { - return DeploymentsCount{}, result.Error - } - - var k8sCount int64 - result = d.db.Table("masters").Count(&k8sCount) - if result.Error != nil { - return DeploymentsCount{}, result.Error - } - - dlsCount := k8sCount + vmsCount - - var vmIPsCount int64 - result = d.db.Table("vms").Where("public_ip = true").Count(&vmIPsCount) - if result.Error != nil { - return DeploymentsCount{}, result.Error - } - - var k8sIPsCount int64 - result = d.db.Table("masters").Where("public_ip = true").Count(&k8sIPsCount) - if result.Error != nil { - return DeploymentsCount{}, result.Error - } - - ipsCount := k8sIPsCount + vmIPsCount - - return DeploymentsCount{ - dlsCount, ipsCount, - }, result.Error -} - -// ListAdmins gets all admins -func (d *DB) ListAdmins() ([]User, error) { - var admins []User - return admins, d.db.Where("admin = true and verified = true").Find(&admins).Error -} - -// GetCodeByEmail returns verification code for unit testing -func (d *DB) GetCodeByEmail(email string) (int, error) { - var res User - query := d.db.First(&res, "email = ?", email) - if query.Error != nil { - return 0, query.Error - } - return res.Code, nil -} - -// UpdatePassword updates password of user -func (d *DB) UpdatePassword(email string, password []byte) error { - var res User - result := d.db.Model(&res).Where("email = ?", email).Update("hashed_password", password) - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return result.Error -} - -// UpdateUserByID updates information of user. empty and unchanged fields are not updated. -func (d *DB) UpdateUserByID(user User) error { - result := d.db.Model(&User{}).Where("id = ?", user.ID.String()).Updates(user) - return result.Error -} - -// UpdateAdminUserByID updates admin information of user. -func (d *DB) UpdateAdminUserByID(id string, admin bool) error { - return d.db.Model(&User{}).Where("id = ?", id).Updates(map[string]interface{}{"admin": admin, "updated_at": time.Now()}).Error -} - -// UpdateVerification updates if user is verified or not -func (d *DB) UpdateVerification(id string, verified bool) error { - var res User - result := d.db.Model(&res).Where("id=?", id).Update("verified", verified) - return result.Error -} - -// GetNotUsedVoucherByUserID returns not used voucher by its user id -func (d *DB) GetNotUsedVoucherByUserID(id string) (Voucher, error) { - var res Voucher - query := d.db.Last(&res, "user_id = ? AND used = false", id) - return res, query.Error -} - -// CreateVM creates new vm -func (d *DB) CreateVM(vm *VM) error { - result := d.db.Create(&vm) - return result.Error - -} - -// GetVMByID return vm by its id -func (d *DB) GetVMByID(id int) (VM, error) { - var vm VM - query := d.db.First(&vm, id) - return vm, query.Error -} - -// GetAllVms returns all vms of user -func (d *DB) GetAllVms(userID string) ([]VM, error) { - var vms []VM - result := d.db.Where("user_id = ?", userID).Find(&vms) - if result.Error != nil { - return []VM{}, result.Error - } - return vms, result.Error -} - -// AvailableVMName returns if name available -func (d *DB) AvailableVMName(name string) (bool, error) { - var names []string - query := d.db.Table("vms"). - Select("name"). - Where("name = ?", name). - Scan(&names) - - if query.Error != nil { - return false, query.Error - } - return len(names) == 0, query.Error -} - -// DeleteVMByID deletes vm by its id -func (d *DB) DeleteVMByID(id int) error { - var vm VM - result := d.db.Delete(&vm, id) - return result.Error -} - -// DeleteAllVms deletes all vms of user -func (d *DB) DeleteAllVms(userID string) error { - var vms []VM - result := d.db.Clauses(clause.Returning{}).Where("user_id = ?", userID).Delete(&vms) - return result.Error -} - -// CreateQuota creates a new quota -func (d *DB) CreateQuota(q *Quota) error { - result := d.db.Create(&q) - return result.Error -} - -// UpdateUserQuota updates quota -func (d *DB) UpdateUserQuota(userID string, vms int, publicIPs int) error { - return d.db.Model(&Quota{}).Where("user_id = ?", userID).Updates(map[string]interface{}{"vms": vms, "public_ips": publicIPs}).Error -} - -// GetUserQuota gets user quota available vms (vms will be used for both vms and k8s clusters) -func (d *DB) GetUserQuota(userID string) (Quota, error) { - var res Quota - query := d.db.First(&res, "user_id = ?", userID) - return res, query.Error -} - -// CreateVoucher creates a new voucher -func (d *DB) CreateVoucher(v *Voucher) error { - result := d.db.Create(&v) - return result.Error -} - -// GetVoucher gets voucher -func (d *DB) GetVoucher(voucher string) (Voucher, error) { - var res Voucher - query := d.db.First(&res, "voucher = ?", voucher) - return res, query.Error -} - -// GetVoucherByID gets voucher by ID -func (d *DB) GetVoucherByID(id int) (Voucher, error) { - var res Voucher - query := d.db.First(&res, id) - return res, query.Error -} - -// ListAllVouchers returns all vouchers to admin -func (d *DB) ListAllVouchers() ([]Voucher, error) { - var res []Voucher - query := d.db.Find(&res) - return res, query.Error -} - -// UpdateVoucher approves voucher by voucher id -func (d *DB) UpdateVoucher(id int, approved bool) (Voucher, error) { - var voucher Voucher - query := d.db.First(&voucher, id) - if query.Error != nil { - return voucher, query.Error - } - - query = d.db.Model(&voucher).Clauses(clause.Returning{}).Updates(map[string]interface{}{"approved": approved, "rejected": !approved}) - return voucher, query.Error -} - -// GetAllPendingVouchers gets all pending vouchers -func (d *DB) GetAllPendingVouchers() ([]Voucher, error) { - var vouchers []Voucher - return vouchers, d.db.Where("approved = false and rejected = false").Find(&vouchers).Error -} - -// DeactivateVoucher if it is used -func (d *DB) DeactivateVoucher(userID string, voucher string) error { - return d.db.Model(Voucher{}).Where("voucher = ?", voucher).Updates(map[string]interface{}{"used": true, "user_id": userID}).Error -} - -// CreateK8s creates a new k8s cluster -func (d *DB) CreateK8s(k *K8sCluster) error { - result := d.db.Create(&k) - return result.Error -} - -// GetK8s gets a k8s cluster -func (d *DB) GetK8s(id int) (K8sCluster, error) { - var k8s K8sCluster - err := d.db.First(&k8s, id).Error - if err != nil { - return K8sCluster{}, err - } - var master Master - err = d.db.Model(&k8s).Association("Master").Find(&master) - if err != nil { - return K8sCluster{}, err - } - var workers []Worker - err = d.db.Model(&k8s).Association("Workers").Find(&workers) - if err != nil { - return K8sCluster{}, err - } - k8s.Master = master - k8s.Workers = workers - - return k8s, nil -} - -// GetAllK8s gets all k8s clusters -func (d *DB) GetAllK8s(userID string) ([]K8sCluster, error) { - var k8sClusters []K8sCluster - err := d.db.Find(&k8sClusters, "user_id = ?", userID).Error - if err != nil { - return nil, err - } - for i := range k8sClusters { - k8sClusters[i], err = d.GetK8s(k8sClusters[i].ID) - if err != nil { - return nil, err - } - } - return k8sClusters, nil -} - -// DeleteK8s deletes a k8s cluster -func (d *DB) DeleteK8s(id int) error { - var k8s K8sCluster - err := d.db.First(&k8s, id).Error - if err != nil { - return err - } - return d.db.Select("Master", "Workers").Delete(&k8s).Error -} - -// DeleteAllK8s deletes all k8s clusters -func (d *DB) DeleteAllK8s(userID string) error { - var k8sClusters []K8sCluster - err := d.db.Find(&k8sClusters, "user_id = ?", userID).Error - if err != nil { - return err - } - return d.db.Select("Master", "Workers").Delete(&k8sClusters).Error -} - -// AvailableK8sName returns if name available -func (d *DB) AvailableK8sName(name string) (bool, error) { - var names []string - query := d.db.Table("masters"). - Select("name"). - Where("name = ?", name). - Scan(&names) - - if query.Error != nil { - return false, query.Error - } - return len(names) == 0, query.Error -} - -// UpdateMaintenance updates if maintenance is on or off -func (d *DB) UpdateMaintenance(on bool) error { - return d.db.Model(&Maintenance{}).Where("active = ?", !on).Updates(map[string]interface{}{"active": on, "updated_at": time.Now()}).Error -} - -// GetMaintenance gets if maintenance is on or off -func (d *DB) GetMaintenance() (Maintenance, error) { - var res Maintenance - query := d.db.First(&res) - return res, query.Error -} - -// notifications - -// ListNotifications returns a list of notifications for a user. -func (d *DB) ListNotifications(userID string) ([]Notification, error) { - var res []Notification - query := d.db.Where("user_id = ?", userID).Find(&res) - return res, query.Error -} - -// UpdateNotification updates seen field for notification -func (d *DB) UpdateNotification(id int, seen bool) error { - return d.db.Model(&Notification{}).Where("id = ?", id).Updates(map[string]interface{}{"seen": seen}).Error -} - -// CreateNotification adds a new notification for a user -func (d *DB) CreateNotification(n *Notification) error { - return d.db.Create(&n).Error -} - -// UpdateNextLaunch updates the launched state of NextLaunch -func (d *DB) UpdateNextLaunch(on bool) error { - return d.db.Model(&NextLaunch{}).Where("launched = ?", !on).Updates(map[string]interface{}{"launched": on, "updated_at": time.Now()}).Error -} - -// GetNextLaunch queries on NextLaunch in db -func (d *DB) GetNextLaunch() (NextLaunch, error) { - var res NextLaunch - query := d.db.First(&res) - return res, query.Error -} diff --git a/server/models/database_test.go b/server/models/database_test.go index 4b6f8fb7..57bb7cd9 100644 --- a/server/models/database_test.go +++ b/server/models/database_test.go @@ -37,12 +37,12 @@ func TestConnect(t *testing.T) { func TestCreateUser(t *testing.T) { db := setupDB(t) err := db.CreateUser(&User{ - Name: "test", + FirstName: "test", }) require.NoError(t, err) var user User err = db.db.First(&user).Error - require.Equal(t, user.Name, "test") + require.Equal(t, user.FirstName, "test") require.NoError(t, err) } @@ -50,7 +50,7 @@ func TestGetUserByEmail(t *testing.T) { db := setupDB(t) t.Run("user not found", func(t *testing.T) { err := db.CreateUser(&User{ - Name: "test", + FirstName: "test", }) require.NoError(t, err) _, err = db.GetUserByEmail("email") @@ -58,12 +58,12 @@ func TestGetUserByEmail(t *testing.T) { }) t.Run("user found", func(t *testing.T) { err := db.CreateUser(&User{ - Name: "test", - Email: "email", + FirstName: "test", + Email: "email", }) require.NoError(t, err) u, err := db.GetUserByEmail("email") - require.Equal(t, u.Name, "test") + require.Equal(t, u.FirstName, "test") require.Equal(t, u.Email, "email") require.NoError(t, err) }) @@ -73,7 +73,7 @@ func TestGetUserByID(t *testing.T) { db := setupDB(t) t.Run("user not found", func(t *testing.T) { err := db.CreateUser(&User{ - Name: "test", + FirstName: "test", }) require.NoError(t, err) _, err = db.GetUserByID("not-uuid") @@ -81,13 +81,13 @@ func TestGetUserByID(t *testing.T) { }) t.Run("user found", func(t *testing.T) { user := User{ - Name: "test", - Email: "email", + FirstName: "test", + Email: "email", } err := db.CreateUser(&user) require.NoError(t, err) u, err := db.GetUserByID(user.ID.String()) - require.Equal(t, u.Name, "test") + require.Equal(t, u.FirstName, "test") require.Equal(t, u.Email, "email") require.NoError(t, err) }) @@ -103,7 +103,7 @@ func TestListAllUsers(t *testing.T) { t.Run("list all users for admin", func(t *testing.T) { user1 := User{ - Name: "user1", + FirstName: "user1", Email: "user1@gmail.com", HashedPassword: []byte{}, Verified: true, @@ -113,7 +113,7 @@ func TestListAllUsers(t *testing.T) { require.NoError(t, err) users, err := db.ListAllUsers() require.NoError(t, err) - require.Equal(t, users[0].Name, user1.Name) + require.Equal(t, users[0].FirstName, user1.FirstName) require.Equal(t, users[0].Email, user1.Email) require.Equal(t, users[0].HashedPassword, user1.HashedPassword) @@ -129,7 +129,7 @@ func TestGetCodeByEmail(t *testing.T) { t.Run("get code of user", func(t *testing.T) { user := User{ - Name: "user", + FirstName: "user", Email: "user@gmail.com", HashedPassword: []byte{}, Verified: true, @@ -148,7 +148,7 @@ func TestGetCodeByEmail(t *testing.T) { func TestUpdatePassword(t *testing.T) { db := setupDB(t) t.Run("user not found so nothing updated", func(t *testing.T) { - err := db.UpdatePassword("email", []byte("new-pass")) + err := db.UpdateUserPassword("email", []byte("new-pass")) require.Error(t, err) var user User err = db.db.First(&user).Error @@ -162,7 +162,7 @@ func TestUpdatePassword(t *testing.T) { } err := db.CreateUser(&user) require.NoError(t, err) - err = db.UpdatePassword("email", []byte("new-pass")) + err = db.UpdateUserPassword("email", []byte("new-pass")) require.NoError(t, err) u, err := db.GetUserByEmail("email") require.Equal(t, u.Email, "email") @@ -192,7 +192,7 @@ func TestUpdateUserByID(t *testing.T) { ID: user.ID, Email: "", HashedPassword: []byte("new-pass"), - Name: "name", + FirstName: "name", }) require.NoError(t, err) var u User @@ -201,7 +201,7 @@ func TestUpdateUserByID(t *testing.T) { require.Equal(t, u.Email, user.Email) // should change require.Equal(t, u.HashedPassword, []byte("new-pass")) - require.Equal(t, u.Name, "name") + require.Equal(t, u.FirstName, "name") require.NoError(t, err) }) @@ -210,7 +210,7 @@ func TestUpdateUserByID(t *testing.T) { func TestUpdateVerification(t *testing.T) { db := setupDB(t) t.Run("user not found so nothing updated", func(t *testing.T) { - err := db.UpdateVerification("id", true) + err := db.UpdateUserVerification("id", true) require.NoError(t, err) var user User err = db.db.First(&user).Error @@ -224,7 +224,7 @@ func TestUpdateVerification(t *testing.T) { err := db.CreateUser(&user) require.Equal(t, user.Verified, false) require.NoError(t, err) - err = db.UpdateVerification(user.ID.String(), true) + err = db.UpdateUserVerification(user.ID.String(), true) require.NoError(t, err) var u User err = db.db.First(&u).Error @@ -470,75 +470,6 @@ func TestDeleteAllVMs(t *testing.T) { }) } -func TestCreateQuota(t *testing.T) { - db := setupDB(t) - quota := Quota{UserID: "user"} - err := db.CreateQuota("a) - require.NoError(t, err) - var q Quota - err = db.db.First(&q).Error - require.NoError(t, err) - require.Equal(t, q, quota) -} - -func TestUpdateUserQuota(t *testing.T) { - db := setupDB(t) - t.Run("quota not found so no updates", func(t *testing.T) { - err := db.UpdateUserQuota("user", 5, 0) - require.NoError(t, err) - }) - t.Run("quota found", func(t *testing.T) { - quota1 := Quota{UserID: "user"} - quota2 := Quota{UserID: "new-user"} - - err := db.CreateQuota("a1) - require.NoError(t, err) - err = db.CreateQuota("a2) - require.NoError(t, err) - - err = db.UpdateUserQuota("user", 5, 10) - require.NoError(t, err) - - var q Quota - err = db.db.First(&q, "user_id = 'user'").Error - require.NoError(t, err) - require.Equal(t, q.Vms, 5) - - err = db.db.First(&q, "user_id = 'new-user'").Error - require.NoError(t, err) - require.Equal(t, q.Vms, 0) - - }) - - t.Run("quota found with zero values", func(t *testing.T) { - quota := Quota{UserID: "1"} - err := db.CreateQuota("a) - require.NoError(t, err) - err = db.UpdateUserQuota("1", 0, 0) - require.NoError(t, err) - }) -} -func TestGetUserQuota(t *testing.T) { - db := setupDB(t) - t.Run("quota not found", func(t *testing.T) { - _, err := db.GetUserQuota("user") - require.Equal(t, err, gorm.ErrRecordNotFound) - }) - t.Run("quota found", func(t *testing.T) { - quota1 := Quota{UserID: "user"} - quota2 := Quota{UserID: "new-user"} - - err := db.CreateQuota("a1) - require.NoError(t, err) - err = db.CreateQuota("a2) - require.NoError(t, err) - - quota, err := db.GetUserQuota("user") - require.NoError(t, err) - require.Equal(t, quota, quota1) - }) -} - func TestCreateVoucher(t *testing.T) { db := setupDB(t) voucher := Voucher{UserID: "user"} diff --git a/server/models/deployments_count.go b/server/models/deployments_count.go new file mode 100644 index 00000000..dc79ffbd --- /dev/null +++ b/server/models/deployments_count.go @@ -0,0 +1,42 @@ +package models + +// DeploymentsCount has the vms and ips reserved in the grid +type DeploymentsCount struct { + VMs int64 `json:"vms"` + IPs int64 `json:"ips"` +} + +// CountAllDeployments returns deployments and IPs count +func (d *DB) CountAllDeployments() (DeploymentsCount, error) { + var vmsCount int64 + result := d.db.Table("vms").Count(&vmsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sCount int64 + result = d.db.Table("masters").Count(&k8sCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + dlsCount := k8sCount + vmsCount + + var vmIPsCount int64 + result = d.db.Table("vms").Where("public_ip = true").Count(&vmIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sIPsCount int64 + result = d.db.Table("masters").Where("public_ip = true").Count(&k8sIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + ipsCount := k8sIPsCount + vmIPsCount + + return DeploymentsCount{ + dlsCount, ipsCount, + }, result.Error +} diff --git a/server/models/invoice.go b/server/models/invoice.go new file mode 100644 index 00000000..f1549b1b --- /dev/null +++ b/server/models/invoice.go @@ -0,0 +1,152 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Invoice struct { + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" binding:"required"` + Total float64 `json:"total"` + Deployments []DeploymentItem `json:"deployments" gorm:"foreignKey:invoice_id"` + // TODO: + Tax float64 `json:"tax"` + Paid bool `json:"paid"` + PaymentDetails PaymentDetails `json:"payment_details" gorm:"foreignKey:invoice_id"` + LastReminderAt time.Time `json:"last_remainder_at"` + CreatedAt time.Time `json:"created_at"` + PaidAt time.Time `json:"paid_at"` +} + +type DeploymentItem struct { + ID int `json:"id" gorm:"primaryKey"` + InvoiceID int `json:"invoice_id"` + DeploymentID int `json:"deployment_id"` + DeploymentType string `json:"type"` + DeploymentResources string `json:"resources"` + HasPublicIP bool `json:"has_public_ip"` + PeriodInHours float64 `json:"period"` + Cost float64 `json:"cost"` +} + +type PaymentDetails struct { + InvoiceID int `json:"invoice_id"` + Card float64 `json:"card"` + Balance float64 `json:"balance"` + VoucherBalance float64 `json:"voucher_balance"` +} + +// CreateInvoice creates new invoice +func (d *DB) CreateInvoice(invoice *Invoice) error { + return d.db.Create(&invoice).Error +} + +// GetInvoice returns an invoice by ID +func (d *DB) GetInvoice(id int) (Invoice, error) { + var invoice Invoice + return invoice, d.db.First(&invoice, id).Error +} + +// ListUserInvoices returns all invoices of user +func (d *DB) ListUserInvoices(userID string) ([]Invoice, error) { + var invoices []Invoice + return invoices, d.db.Where("user_id = ?", userID).Find(&invoices).Error +} + +// ListInvoices returns all invoices (admin) +func (d *DB) ListInvoices() ([]Invoice, error) { + var invoices []Invoice + return invoices, d.db.Find(&invoices).Error +} + +// ListUnpaidInvoices returns unpaid user invoices +func (d *DB) ListUnpaidInvoices(userID string) ([]Invoice, error) { + var invoices []Invoice + return invoices, d.db.Order("total desc").Where("user_id = ?", userID).Where("paid = ?", false).Find(&invoices).Error +} + +func (d *DB) UpdateInvoiceLastRemainderDate(id int) error { + return d.db.Model(&Invoice{}).Where("id = ?", id).Updates(map[string]interface{}{"last_remainder_at": time.Now()}).Error +} + +// PayInvoice updates paid with true and paid at field with current time in the invoice +func (d *DB) PayInvoice(id int, payment PaymentDetails) error { + var invoice Invoice + result := d.db.Model(&invoice). + Where("id = ?", id). + Update("paid", true). + Update("payment_details", payment). + Update("paid_at", time.Now()) + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return result.Error +} + +// PayUserInvoices tries to pay invoices with a given balance +func (d *DB) PayUserInvoices(userID string, balance, voucherBalance float64) (float64, float64, error) { + // get unpaid invoices + var invoices []Invoice + if err := d.db. + Order("total desc"). + Where("user_id = ?", userID). + Where("paid = ?", false). + Find(&invoices).Error; err != nil && err != gorm.ErrRecordNotFound { + return 0, 0, err + } + + for _, invoice := range invoices { + if balance == 0 && voucherBalance == 0 { + break + } + + // 1. check voucher balance + if invoice.Total <= voucherBalance { + if err := d.PayInvoice(invoice.ID, PaymentDetails{VoucherBalance: invoice.Total}); err != nil { + return 0, 0, err + } + voucherBalance -= invoice.Total + continue + } + + // 2. check balance + if invoice.Total <= balance { + if err := d.PayInvoice(invoice.ID, PaymentDetails{Balance: invoice.Total}); err != nil { + return 0, 0, err + } + balance -= invoice.Total + continue + } + + // 3. check both (total is more than both balance and voucher balance) + if invoice.Total <= balance+voucherBalance { + if err := d.PayInvoice( + invoice.ID, + PaymentDetails{VoucherBalance: voucherBalance, Balance: (invoice.Total - voucherBalance)}, + ); err != nil { + return 0, 0, err + } + + // use voucher first + balance -= (invoice.Total - voucherBalance) + voucherBalance = 0 + } + } + + return balance, voucherBalance, nil +} + +// CalcUserDebt calculates the user debt according to invoices +func (d *DB) CalcUserDebt(userID string) (float64, error) { + var debt float64 + result := d.db.Model(&Invoice{}). + Select("sum(total)"). + Where("user_id = ?", userID). + Where("paid = ?", false). + Scan(&debt) + + return debt, result.Error +} diff --git a/server/models/k8s.go b/server/models/k8s.go index f67a62f1..9f8ebe09 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -1,14 +1,24 @@ // Package models for database models package models +import ( + "time" + + "gorm.io/gorm" +) + // K8sCluster holds all cluster data type K8sCluster struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"userID"` - NetworkContract int `json:"network_contract_id"` - ClusterContract int `json:"contract_id"` - Master Master `json:"master" gorm:"foreignKey:ClusterID"` - Workers []Worker `json:"workers" gorm:"foreignKey:ClusterID"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"userID"` + NetworkContract int `json:"network_contract_id"` + ClusterContract int `json:"contract_id"` + Master Master `json:"master" gorm:"foreignKey:ClusterID"` + Workers []Worker `json:"workers" gorm:"foreignKey:ClusterID"` + State state `json:"state"` + Failure string `json:"failure"` + PricePerMonth float64 `json:"price"` + CreatedAt time.Time `json:"created_at"` } // Master struct for kubernetes master data @@ -23,12 +33,13 @@ type Master struct { Public bool `json:"public"` PublicIP string `json:"public_ip"` Resources string `json:"resources"` + Region string `json:"region"` } // Worker struct for k8s workers data type Worker struct { ClusterID int `json:"clusterID"` - Name string `json:"name"` + Name string `json:"name" gorm:"unique" binding:"required"` CRU uint64 `json:"cru"` MRU uint64 `json:"mru"` SRU uint64 `json:"sru"` @@ -37,4 +48,103 @@ type Worker struct { Public bool `json:"public"` PublicIP string `json:"public_ip"` Resources string `json:"resources"` + Region string `json:"region"` +} + +// CreateK8s creates a new k8s cluster +func (d *DB) CreateK8s(k *K8sCluster) error { + return d.db.Create(&k).Error +} + +// UpdateK8s updates information of k8s cluster. empty and unchanged fields are not updated. +func (d *DB) UpdateK8s(k8s K8sCluster) error { + return d.db.Model(&K8sCluster{}).Where("id = ?", k8s.ID).Updates(k8s).Error +} + +// GetK8s gets a k8s cluster +func (d *DB) GetK8s(id int) (K8sCluster, error) { + var k8s K8sCluster + err := d.db.First(&k8s, id).Error + if err != nil { + return K8sCluster{}, err + } + + var master Master + err = d.db.Model(&k8s).Association("Master").Find(&master) + if err != nil { + return K8sCluster{}, err + } + var workers []Worker + err = d.db.Model(&k8s).Association("Workers").Find(&workers) + if err != nil { + return K8sCluster{}, err + } + k8s.Master = master + k8s.Workers = workers + + return k8s, nil +} + +// GetAllK8s gets all k8s clusters +func (d *DB) GetAllK8s(userID string) ([]K8sCluster, error) { + var k8sClusters []K8sCluster + err := d.db.Find(&k8sClusters, "user_id = ?", userID).Error + if err != nil { + return nil, err + } + for i := range k8sClusters { + k8sClusters[i], err = d.GetK8s(k8sClusters[i].ID) + if err != nil { + return nil, err + } + } + return k8sClusters, nil +} + +// DeleteK8s deletes a k8s cluster +func (d *DB) DeleteK8s(id int) error { + var k8s K8sCluster + err := d.db.First(&k8s, id).Error + if err != nil { + return err + } + return d.db.Select("Master", "Workers").Delete(&k8s).Error +} + +// DeleteAllK8s deletes all k8s clusters +func (d *DB) DeleteAllK8s(userID string) error { + var k8sClusters []K8sCluster + err := d.db.Find(&k8sClusters, "user_id = ?", userID).Error + if err != nil { + return err + } + return d.db.Select("Master", "Workers").Delete(&k8sClusters).Error +} + +// AvailableK8sName returns if name available +func (d *DB) AvailableK8sName(name string) (bool, error) { + var names []string + query := d.db.Table("masters"). + Select("name"). + Where("name = ?", name). + Scan(&names) + + if query.Error != nil { + return false, query.Error + } + return len(names) == 0, query.Error +} + +// UpdateK8sState updates state of k8s cluster +func (d *DB) UpdateK8sState(id int, failure string, state state) error { + var k8s K8sCluster + result := d.db.Model(&k8s).Where("id = ?", id).Update("state", state) + if state == StateFailed { + result.Update("failure", failure) + } + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return result.Error } diff --git a/server/models/maintenance.go b/server/models/maintenance.go index 0b8d8deb..a7568ea2 100644 --- a/server/models/maintenance.go +++ b/server/models/maintenance.go @@ -9,3 +9,15 @@ type Maintenance struct { Active bool `json:"active"` UpdatedAt time.Time `json:"updated_at"` } + +// UpdateMaintenance updates if maintenance is on or off +func (d *DB) UpdateMaintenance(on bool) error { + return d.db.Model(&Maintenance{}).Where("active = ?", !on).Updates(map[string]interface{}{"active": on, "updated_at": time.Now()}).Error +} + +// GetMaintenance gets if maintenance is on or off +func (d *DB) GetMaintenance() (Maintenance, error) { + var res Maintenance + query := d.db.First(&res) + return res, query.Error +} diff --git a/server/models/nextlaunch.go b/server/models/nextlaunch.go index 43021970..f776bb7d 100644 --- a/server/models/nextlaunch.go +++ b/server/models/nextlaunch.go @@ -8,3 +8,15 @@ type NextLaunch struct { Launched bool `json:"launched"` UpdatedAt time.Time `json:"updated_at"` } + +// UpdateNextLaunch updates the launched state of NextLaunch +func (d *DB) UpdateNextLaunch(on bool) error { + return d.db.Model(&NextLaunch{}).Where("launched = ?", !on).Updates(map[string]interface{}{"launched": on, "updated_at": time.Now()}).Error +} + +// GetNextLaunch queries on NextLaunch in db +func (d *DB) GetNextLaunch() (NextLaunch, error) { + var res NextLaunch + query := d.db.First(&res) + return res, query.Error +} diff --git a/server/models/notification.go b/server/models/notification.go index 14e1b179..1bf0f4ae 100644 --- a/server/models/notification.go +++ b/server/models/notification.go @@ -17,3 +17,20 @@ type Notification struct { // to allow redirecting from notifications to the right pages Type string `json:"type" binding:"required"` } + +// ListNotifications returns a list of notifications for a user. +func (d *DB) ListNotifications(userID string) ([]Notification, error) { + var res []Notification + query := d.db.Where("user_id = ?", userID).Find(&res) + return res, query.Error +} + +// UpdateNotification updates seen field for notification +func (d *DB) UpdateNotification(id int, seen bool) error { + return d.db.Model(&Notification{}).Where("id = ?", id).Updates(map[string]interface{}{"seen": seen}).Error +} + +// CreateNotification adds a new notification for a user +func (d *DB) CreateNotification(n *Notification) error { + return d.db.Create(&n).Error +} diff --git a/server/models/quota.go b/server/models/quota.go deleted file mode 100644 index 35a1c934..00000000 --- a/server/models/quota.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package models for database models -package models - -// Quota struct holds available vms for each user -type Quota struct { - UserID string `json:"user_id"` - Vms int `json:"vms"` - PublicIPs int `json:"public_ips"` -} diff --git a/server/models/state.go b/server/models/state.go new file mode 100644 index 00000000..dbd289be --- /dev/null +++ b/server/models/state.go @@ -0,0 +1,31 @@ +package models + +type state string + +const ( + StateCreated = "CREATED" // TODO: + StateFailed = "FAILED" + StateInProgress = "INPROGRESS" +) + +type State struct { + ID int `json:"id" gorm:"primaryKey"` + Value string `json:"value"` +} + +// CreateState creates state table values +func (d *DB) CreateState() error { + if err := d.db.Create(&State{Value: StateCreated}).Error; err != nil { + return err + } + + if err := d.db.Create(&State{Value: StateFailed}).Error; err != nil { + return err + } + + if err := d.db.Create(&State{Value: StateInProgress}).Error; err != nil { + return err + } + + return nil +} diff --git a/server/models/user.go b/server/models/user.go index 9e6fd236..7915e0ea 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -2,6 +2,7 @@ package models import ( + "fmt" "time" "github.com/google/uuid" @@ -10,22 +11,24 @@ import ( // User struct holds data of users type User struct { - ID uuid.UUID `gorm:"primary_key; unique; type:uuid; column:id"` - Name string `json:"name" binding:"required"` - Email string `json:"email" gorm:"unique" binding:"required"` - HashedPassword []byte `json:"hashed_password" binding:"required"` - UpdatedAt time.Time `json:"updated_at"` - Code int `json:"code"` - SSHKey string `json:"ssh_key"` - Verified bool `json:"verified"` - TeamSize int `json:"team_size" binding:"required"` - ProjectDesc string `json:"project_desc" binding:"required"` - College string `json:"college" binding:"required"` + ID uuid.UUID `gorm:"primary_key; unique; type:uuid; column:id"` + StripeCustomerID string `json:"stripe_customer_id"` + StripeDefaultPaymentID string `json:"stripe_payment_method_id"` + FirstName string `json:"first_name" binding:"required"` + LastName string `json:"last_name" binding:"required"` + Email string `json:"email" gorm:"unique" binding:"required"` + HashedPassword []byte `json:"hashed_password" binding:"required"` + UpdatedAt time.Time `json:"updated_at"` + Code int `json:"code"` + SSHKey string `json:"ssh_key"` + Verified bool `json:"verified"` // checks if user type is admin - Admin bool `json:"admin"` + Admin bool `json:"admin"` + Balance float64 `json:"balance"` + VoucherBalance float64 `json:"voucher_balance"` } -// BeforeCreate generates a new uuid +// BeforeCreate generates a new uuid per user func (user *User) BeforeCreate(tx *gorm.DB) (err error) { id, err := uuid.NewUUID() if err != nil { @@ -36,23 +39,75 @@ func (user *User) BeforeCreate(tx *gorm.DB) (err error) { return } -// UserUsedQuota has user data + voucher quota and used quota -type UserUsedQuota struct { - UserID string `json:"user_id"` - Name string `json:"name"` - Email string `json:"email"` - HashedPassword []byte `json:"hashed_password"` - Voucher string `json:"voucher"` - UpdatedAt time.Time `json:"updated_at"` - Code int `json:"code"` - SSHKey string `json:"ssh_key"` - Verified bool `json:"verified"` - TeamSize int `json:"team_size"` - ProjectDesc string `json:"project_desc"` - College string `json:"college"` - Admin bool `json:"admin"` - Vms int `json:"vms"` - PublicIPs int `json:"public_ips"` - UsedVms int `json:"used_vms"` - UsedPublicIPs int `json:"used_public_ips"` +func (user *User) Name() string { + return fmt.Sprintf("%s %s", user.FirstName, user.LastName) +} + +// CreateUser creates new user +func (d *DB) CreateUser(u *User) error { + result := d.db.Create(&u) + return result.Error +} + +// GetUserByEmail returns user by its email +func (d *DB) GetUserByEmail(email string) (User, error) { + var res User + query := d.db.First(&res, "email = ?", email) + return res, query.Error +} + +// GetUserByID returns user by its id +func (d *DB) GetUserByID(id string) (User, error) { + var res User + query := d.db.First(&res, "id = ?", id) + return res, query.Error +} + +// ListAllUsers returns all users to admin +func (d *DB) ListAllUsers() ([]User, error) { + var res []User + return res, d.db.Where("verified = true").Find(&res).Error +} + +// ListAdmins gets all admins +func (d *DB) ListAdmins() ([]User, error) { + var admins []User + return admins, d.db.Where("admin = true and verified = true").Find(&admins).Error +} + +// GetCodeByEmail returns verification code for unit testing +func (d *DB) GetCodeByEmail(email string) (int, error) { + var res User + query := d.db.First(&res, "email = ?", email) + if query.Error != nil { + return 0, query.Error + } + return res.Code, nil +} + +// UpdateUserPassword updates password of user +func (d *DB) UpdateUserPassword(email string, password []byte) error { + var res User + result := d.db.Model(&res).Where("email = ?", email).Update("hashed_password", password) + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return result.Error +} + +// UpdateUserByID updates information of user. empty and unchanged fields are not updated. +func (d *DB) UpdateUserByID(user User) error { + return d.db.Model(&User{}).Where("id = ?", user.ID.String()).Updates(user).Error +} + +// UpdateAdminUserByID updates admin information of user. +func (d *DB) UpdateAdminUserByID(id string, admin bool) error { + return d.db.Model(&User{}).Where("id = ?", id).Updates(map[string]interface{}{"admin": admin, "updated_at": time.Now()}).Error +} + +// UpdateUserVerification updates if user is verified or not +func (d *DB) UpdateUserVerification(id string, verified bool) error { + var res User + result := d.db.Model(&res).Where("id=?", id).Update("verified", verified) + return result.Error } diff --git a/server/models/vm.go b/server/models/vm.go index 642882a0..69918584 100644 --- a/server/models/vm.go +++ b/server/models/vm.go @@ -1,25 +1,93 @@ // Package models for database models package models +import ( + "time" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + // VM struct for vms data type VM struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id"` - Name string `json:"name" gorm:"unique" binding:"required"` - YggIP string `json:"ygg_ip"` - MyceliumIP string `json:"mycelium_ip"` - Public bool `json:"public"` - PublicIP string `json:"public_ip"` - Resources string `json:"resources"` - SRU uint64 `json:"sru"` - CRU uint64 `json:"cru"` - MRU uint64 `json:"mru"` - ContractID uint64 `json:"contractID"` - NetworkContractID uint64 `json:"networkContractID"` -} - -// DeploymentsCount has the vms and ips reserved in the grid -type DeploymentsCount struct { - VMs int64 `json:"vms"` - IPs int64 `json:"ips"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id"` + Name string `json:"name" gorm:"unique" binding:"required"` + YggIP string `json:"ygg_ip"` + MyceliumIP string `json:"mycelium_ip"` + Public bool `json:"public"` + PublicIP string `json:"public_ip"` + Resources string `json:"resources"` + Region string `json:"region"` + SRU uint64 `json:"sru"` + CRU uint64 `json:"cru"` + MRU uint64 `json:"mru"` + ContractID uint64 `json:"contractID"` + NetworkContractID uint64 `json:"networkContractID"` + State state `json:"state"` + Failure string `json:"failure"` + PricePerMonth float64 `json:"price"` + CreatedAt time.Time `json:"created_at"` +} + +// CreateVM creates new vm +func (d *DB) CreateVM(vm *VM) error { + return d.db.Create(&vm).Error +} + +// GetVMByID return vm by its id +func (d *DB) GetVMByID(id int) (VM, error) { + var vm VM + return vm, d.db.First(&vm, id).Error +} + +// GetAllVms returns all vms of user +func (d *DB) GetAllVms(userID string) ([]VM, error) { + var vms []VM + return vms, d.db.Where("user_id = ?", userID).Find(&vms).Error +} + +// UpdateVM updates information of vm. empty and unchanged fields are not updated. +func (d *DB) UpdateVM(vm VM) error { + return d.db.Model(&VM{}).Where("id = ?", vm.ID).Updates(vm).Error +} + +// UpdateVMState updates state of vm +func (d *DB) UpdateVMState(id int, failure string, state state) error { + var vm VM + result := d.db.Model(&vm).Where("id = ?", id).Update("state", state) + if state == StateFailed { + result.Update("failure", failure) + } + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return result.Error +} + +// AvailableVMName returns if name available +func (d *DB) AvailableVMName(name string) (bool, error) { + var names []string + query := d.db.Table("vms"). + Select("name"). + Where("name = ?", name). + Scan(&names) + + if query.Error != nil { + return false, query.Error + } + return len(names) == 0, query.Error +} + +// DeleteVMByID deletes vm by its id +func (d *DB) DeleteVMByID(id int) error { + var vm VM + return d.db.Delete(&vm, id).Error +} + +// DeleteAllVms deletes all vms of user +func (d *DB) DeleteAllVms(userID string) error { + var vms []VM + return d.db.Clauses(clause.Returning{}).Where("user_id = ?", userID).Delete(&vms).Error } diff --git a/server/models/voucher.go b/server/models/voucher.go index 8c3c8f56..01f20f2c 100644 --- a/server/models/voucher.go +++ b/server/models/voucher.go @@ -1,15 +1,18 @@ // Package models for database models package models -import "time" +import ( + "time" + + "gorm.io/gorm/clause" +) // Voucher struct holds data of vouchers type Voucher struct { ID int `json:"id" gorm:"primaryKey"` UserID string `json:"user_id" binding:"required"` Voucher string `json:"voucher" gorm:"unique"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` + Balance uint64 `json:"balance" binding:"required"` Reason string `json:"reason" binding:"required"` Used bool `json:"used" binding:"required"` Approved bool `json:"approved" binding:"required"` @@ -17,3 +20,54 @@ type Voucher struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } + +// CreateVoucher creates a new voucher +func (d *DB) CreateVoucher(v *Voucher) error { + return d.db.Create(&v).Error +} + +// GetVoucher gets voucher +func (d *DB) GetVoucher(voucher string) (Voucher, error) { + var res Voucher + return res, d.db.First(&res, "voucher = ?", voucher).Error +} + +// GetVoucherByID gets voucher by ID +func (d *DB) GetVoucherByID(id int) (Voucher, error) { + var res Voucher + return res, d.db.First(&res, id).Error +} + +// ListAllVouchers returns all vouchers to admin +func (d *DB) ListAllVouchers() ([]Voucher, error) { + var res []Voucher + return res, d.db.Find(&res).Error +} + +// UpdateVoucher approves voucher by voucher id +func (d *DB) UpdateVoucher(id int, approved bool) (Voucher, error) { + var voucher Voucher + query := d.db.First(&voucher, id) + if query.Error != nil { + return voucher, query.Error + } + + return voucher, d.db.Model(&voucher).Clauses(clause.Returning{}).Updates(map[string]interface{}{"approved": approved, "rejected": !approved}).Error +} + +// GetAllPendingVouchers gets all pending vouchers +func (d *DB) GetAllPendingVouchers() ([]Voucher, error) { + var vouchers []Voucher + return vouchers, d.db.Where("approved = false and rejected = false").Find(&vouchers).Error +} + +// DeactivateVoucher if it is used +func (d *DB) DeactivateVoucher(userID string, voucher string) error { + return d.db.Model(Voucher{}).Where("voucher = ?", voucher).Updates(map[string]interface{}{"used": true, "user_id": userID}).Error +} + +// GetNotUsedVoucherByUserID returns not used voucher by its user id +func (d *DB) GetNotUsedVoucherByUserID(id string) (Voucher, error) { + var res Voucher + return res, d.db.Last(&res, "user_id = ? AND used = false", id).Error +} diff --git a/server/streams/types.go b/server/streams/types.go index 888952ba..9fc515e6 100644 --- a/server/streams/types.go +++ b/server/streams/types.go @@ -31,14 +31,14 @@ const ( // VMDeployRequest type for redis vm deployment request type VMDeployRequest struct { User models.User - Input models.DeployVMInput + VM models.VM AdminSSHKey string } // K8sDeployRequest type for redis k8s deployment request type K8sDeployRequest struct { User models.User - Input models.K8sDeployInput + Cluster models.K8sCluster AdminSSHKey string } diff --git a/swagger.yml b/swagger.yml deleted file mode 100644 index 7e54f2bf..00000000 --- a/swagger.yml +++ /dev/null @@ -1,1182 +0,0 @@ -consumes: -- application/json -info: - description: HTTP server in Go with Swagger endpoints definition. - title: cloud4students - version: 0.1.0 -produces: -- application/json -schemes: -- http -securityDefinitions: - Bearer: - type: apiKey - name: Authorization - in: header - description: >- - Enter the token with the `Bearer: ` prefix, e.g. "Bearer abcde12345". -swagger: "2.0" - -paths: - /user/signup: - post: - description: A user sign up - consumes: - - application/json - parameters: - - in: body - name: user - description: the user info to sign up. - schema: - $ref: '#/definitions/SingUp' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Verification Code has been sent to email@email.com - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/signup/verify_email: - post: - description: Verifying sign up email - consumes: - - application/json - parameters: - - in: body - name: code - description: the code and email of the user to verify - schema: - $ref: '#/definitions/VerifyCode' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Account Created Successfully - data: - type: object - properties: - id: - type: string - - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/forget_password: - post: - description: A user forgets password - consumes: - - application/json - parameters: - - in: body - name: email - description: the email of the user to change password - schema: - type: object - properties: - email: - type: string - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Verification Code has been sent to email@email.com - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/forget_password/verify_email: - post: - description: Verifying forget password email - consumes: - - application/json - parameters: - - in: body - name: code - description: the code and email of the user to verify - schema: - $ref: '#/definitions/VerifyCode' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Code is verified - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/signin: - post: - description: A user sign in - consumes: - - application/json - parameters: - - in: body - name: user - description: the user info to sign in. - schema: - $ref: '#/definitions/SingIn' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - example: User signed in successfully - data: - type: object - properties: - access_token: - type: string - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/refresh_token: - post: - description: A token to refresh - security: - - Bearer: [] - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Token is refreshed successfully - data: - type: object - properties: - access_token: - type: string - refresh_token: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - - /user/change_password: - put: - description: A user to change their password - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: password - description: the user's password to update - schema: - $ref: '#/definitions/ChangePassword' - responses: - 203: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Password is changed - 404: - $ref: '#/responses/UnfoundError' - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/activate_voucher: - put: - description: A user to add voucher - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: voucher - description: the voucher to add - schema: - type: object - properties: - voucher: - type: string - responses: - 203: - description: OK - schema: - type: object - properties: - msg: - type: string - example: Voucher is added successfully. - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/apply_voucher: - post: - description: A user to apply for a voucher - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: voucher - description: the voucher to apply for - schema: - type: object - properties: - vms: - type: integer - public_ips: - type: integer - reason: - type: string - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user: - get: - description: getting a user - security: - - Bearer: [] - consumes: - - application/json - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - user: - $ref: '#/definitions/User' - - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - put: - description: A user to update data - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: data - description: the user data to update - schema: - $ref: '#/definitions/UpdateData' - responses: - 203: - description: OK - schema: - type: object - properties: - msg: - type: string - example: User is updated successfully - 404: - $ref: '#/responses/UnfoundError' - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - - /quota: - get: - description: getting a quota - security: - - Bearer: [] - consumes: - - application/json - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - quota: - $ref: '#/definitions/Quota' - - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /vm: - post: - description: deploy a vm - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: vm - description: the vm to deploy - schema: - $ref: '#/definitions/DeployVM' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - id: - type: integer - - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - get: - description: get all vms for a user - security: - - Bearer: [] - consumes: - - application/json - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - vm: - $ref: '#/definitions/Vms' - - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - delete: - description: delete all vms for a user - security: - - Bearer: [] - consumes: - - application/json - responses: - 204: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /vm/{id}: - get: - description: get a vm - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: path - name: id - description: vm ID - required: true - type: string - format: integer - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - vm: - $ref: '#/definitions/Vm' - - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - delete: - description: delete a vm - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: path - name: id - description: vm ID - required: true - type: string - format: integer - responses: - 204: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - - /k8s: - post: - description: deploy a k8s - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: k8s - description: the k8s to deploy - schema: - $ref: '#/definitions/DeployK8s' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - id: - type: integer - - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - get: - description: get all k8s for a user - security: - - Bearer: [] - consumes: - - application/json - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - k8s: - $ref: '#/definitions/Kubernetes_list' - - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - delete: - description: delete all k8s for a user - security: - - Bearer: [] - consumes: - - application/json - responses: - 204: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /k8s/{id}: - get: - description: get a k8s - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: path - name: id - description: k8s ID - required: true - type: string - format: integer - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - k8s: - $ref: '#/definitions/Kubernetes' - - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - delete: - description: delete a k8s - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: path - name: id - description: k8s ID - required: true - type: string - format: integer - responses: - 204: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /voucher: - get: - description: getting all vouchers - security: - - Bearer: [] - consumes: - - application/json - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - users: - $ref: '#/definitions/Vouchers' - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - put: - description: approve all vouchers - security: - - Bearer: [] - consumes: - - application/json - responses: - 203: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - post: - description: generate a voucher - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: body - name: voucher - description: the voucher to generate - schema: - $ref: '#/definitions/GenerateVoucher' - responses: - 201: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - users: - $ref: '#/definitions/Voucher' - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /voucher/{id}: - put: - description: approve a voucher - security: - - Bearer: [] - consumes: - - application/json - parameters: - - in: path - name: id - description: voucher ID - required: true - type: string - format: integer - - in: body - name: voucher - description: approval status - schema: - type: object - properties: - approved: - type: boolean - responses: - 203: - description: OK - schema: - type: object - properties: - msg: - type: string - 401: - $ref: '#/responses/UnauthorizedError' - 404: - $ref: '#/responses/UnfoundError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - - /user/all: - get: - description: getting all user - security: - - Bearer: [] - consumes: - - application/json - responses: - 200: - description: OK - schema: - type: object - properties: - msg: - type: string - data: - type: object - properties: - users: - $ref: '#/definitions/Users' - 401: - $ref: '#/responses/UnauthorizedError' - 500: - description: Unexpected error - schema: - $ref: '#/responses/ErrorResponse' - -definitions: - Users: - type: array - items: - $ref: '#/definitions/UserUsedQuota' - - Vouchers: - type: array - items: - $ref: '#/definitions/Voucher' - - Vms: - type: array - items: - $ref: '#/definitions/Vm' - - Kubernetes_list: - type: array - items: - $ref: '#/definitions/Kubernetes' - - User: - type: object - required: - - id - properties: - id: - type: string - format: uuid - name: - type: string - email: - type: string - format: email - hashed_password: - type: string - voucher: - type: string - ssh_key: - type: string - admin: - type: boolean - verified: - type: boolean - team_size: - type: integer - project_desc: - type: string - college: - type: string - - UserUsedQuota: - type: object - properties: - user_id: - type: string - format: uuid - name: - type: string - email: - type: string - format: email - hashed_password: - type: string - voucher: - type: string - ssh_key: - type: string - admin: - type: boolean - verified: - type: boolean - team_size: - type: integer - project_desc: - type: string - college: - type: string - vms: - type: integer - public_ips: - type: integer - used_vms: - type: integer - used_public_ips: - type: integer - - Quota: - type: object - required: - - id - properties: - id: - type: integer - userID: - type: string - format: uuid - vms: - type: integer - public_ips: - type: integer - - Voucher: - type: object - required: - - id - properties: - id: - type: integer - userID: - type: string - format: uuid - voucher: - type: string - vms: - type: integer - public_ips: - type: integer - reason: - type: string - used: - type: boolean - approved: - type: boolean - - Vm: - type: object - required: - - id - properties: - id: - type: integer - userID: - type: string - format: uuid - ygg_ip: - type: string - public: - type: boolean - public_ip: - type: string - cru: - type: string - sru: - type: string - mru: - type: string - contract_id: - type: string - network_contract_id: - type: string - - Kubernetes: - type: object - required: - - id - properties: - id: - type: integer - userID: - type: string - format: uuid - contract_id: - type: string - network_contract_id: - type: string - master: - type: object - $ref: '#/definitions/Master' - worker: - type: array - items: - $ref: '#/definitions/Worker' - - Master: - type: object - properties: - cluster_id: - type: integer - name: - type: string - ygg_ip: - type: string - public: - type: boolean - public_ip: - type: string - cru: - type: string - sru: - type: string - mru: - type: string - - Worker: - type: object - properties: - cluster_id: - type: integer - name: - type: string - cru: - type: string - sru: - type: string - mru: - type: string - - SingUp: - type: object - required: - - name - - email - - password - - confirm_password - - team_size - - project_desc - - college - properties: - name: - type: string - email: - type: string - format: email - password: - type: string - confirm_password: - type: string - team_size: - type: integer - project_desc: - type: string - college: - type: string - - VerifyCode: - type: object - required: - - code - - email - properties: - code: - type: integer - email: - type: string - format: email - - SingIn: - type: object - required: - - password - - email - properties: - email: - type: string - format: email - password: - type: string - - ChangePassword: - type: object - required: - - email - - password - - confirm_password - properties: - email: - type: string - format: email - password: - type: string - confirm_password: - type: string - - UpdateData: - type: object - properties: - name: - type: string - password: - type: string - ssh_key: - type: string - confirm_password: - type: string - - DeployVM: - type: object - properties: - name: - type: string - resources: - type: string - format: small-medium-large - public: - type: boolean - - DeployK8s: - type: object - properties: - master_name: - type: string - resources: - type: string - format: small-medium-large - public: - type: boolean - workers: - type: array - items: - $ref: '#/definitions/DeployWorker' - - DeployWorker: - type: object - properties: - name: - type: string - resources: - type: string - format: small-medium-large - - GenerateVoucher: - type: object - properties: - length: - type: string - vms: - type: integer - public_ips: - type: integer - -responses: - ErrorResponse: - description: Unexpected error - schema: - type: object - properties: - err: - type: string - - UnfoundError: - description: Unfound error - schema: - type: object - properties: - err: - type: string - description: Object is not found - - UnauthorizedError: - description: Unauthorized error - schema: - type: object - properties: - err: - type: string - description: Access token is missing or invalid - \ No newline at end of file From 7855fb1b21800d4443570d906441bd2429bf0e96 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Thu, 19 Dec 2024 17:09:38 +0200 Subject: [PATCH 2/7] fix tests --- server/models/database_test.go | 85 ++++++++++++++++++++++------------ server/models/k8s.go | 10 ++-- server/models/state.go | 2 +- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/server/models/database_test.go b/server/models/database_test.go index 57bb7cd9..a48daa4a 100644 --- a/server/models/database_test.go +++ b/server/models/database_test.go @@ -3,6 +3,7 @@ package models import ( "testing" + "time" "github.com/stretchr/testify/require" "gorm.io/gorm" @@ -325,9 +326,12 @@ func TestCreateVM(t *testing.T) { vm := VM{Name: "vm"} err := db.CreateVM(&vm) require.NoError(t, err) + var v VM - err = db.db.First(&v).Error - require.NoError(t, err) + require.NoError(t, db.db.First(&v).Error) + + v.CreatedAt = time.Now().In(time.Local).Truncate(time.Second) + vm.CreatedAt = time.Now().Truncate(time.Second) require.Equal(t, v, vm) } @@ -343,10 +347,14 @@ func TestGetVMByID(t *testing.T) { require.NoError(t, err) v, err := db.GetVMByID(vm.ID) - require.Equal(t, v, vm) require.NoError(t, err) + + v.CreatedAt = time.Now().In(time.Local).Truncate(time.Second) + vm.CreatedAt = time.Now().Truncate(time.Second) + require.Equal(t, v, vm) }) } + func TestGetAllVMs(t *testing.T) { db := setupDB(t) t.Run("no vms with user", func(t *testing.T) { @@ -367,14 +375,22 @@ func TestGetAllVMs(t *testing.T) { require.NoError(t, err) vms, err := db.GetAllVms("user") + + vms[0].CreatedAt = time.Now().In(time.Local).Truncate(time.Second) + vms[1].CreatedAt = time.Now().In(time.Local).Truncate(time.Second) + vm1.CreatedAt = time.Now().Truncate(time.Second) + vm2.CreatedAt = time.Now().Truncate(time.Second) + require.Equal(t, vms, []VM{vm1, vm2}) require.NoError(t, err) vms, err = db.GetAllVms("new-user") + vms[0].CreatedAt = time.Now().In(time.Local).Truncate(time.Second) + vm3.CreatedAt = time.Now().Truncate(time.Second) + require.Equal(t, vms, []VM{vm3}) require.NoError(t, err) }) - } func TestAvailableVMName(t *testing.T) { @@ -406,8 +422,8 @@ func TestAvailableVMName(t *testing.T) { require.Equal(t, true, valid) }) - } + func TestDeleteVMByID(t *testing.T) { db := setupDB(t) t.Run("delete non existing vm", func(t *testing.T) { @@ -436,6 +452,7 @@ func TestDeleteAllVMs(t *testing.T) { err := db.DeleteAllVms("user") require.NoError(t, err) }) + t.Run("delete existing vms", func(t *testing.T) { vm1 := VM{UserID: "user", Name: "vm1"} vm2 := VM{UserID: "user", Name: "vm2"} @@ -448,23 +465,17 @@ func TestDeleteAllVMs(t *testing.T) { err = db.CreateVM(&vm3) require.NoError(t, err) - vms, err := db.GetAllVms("user") - require.Equal(t, vms, []VM{vm1, vm2}) - require.NoError(t, err) - - vms, err = db.GetAllVms("new-user") - require.Equal(t, vms, []VM{vm3}) - require.NoError(t, err) - err = db.DeleteAllVms("user") require.NoError(t, err) - vms, err = db.GetAllVms("user") + vms, err := db.GetAllVms("user") require.NoError(t, err) require.Empty(t, vms) // other users unaffected vms, err = db.GetAllVms("new-user") + vms[0].CreatedAt = time.Now().In(time.Local).Truncate(time.Second) + vm3.CreatedAt = time.Now().Truncate(time.Second) require.Equal(t, vms, []VM{vm3}) require.NoError(t, err) }) @@ -624,12 +635,14 @@ func TestCreateK8s(t *testing.T) { require.Equal(t, w[1].Name, "worker2") require.Equal(t, w[1].ClusterID, 1) } + func TestGetK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { _, err := db.GetK8s(1) require.Equal(t, err, gorm.ErrRecordNotFound) }) + t.Run("K8s found", func(t *testing.T) { k8s := K8sCluster{ UserID: "user", @@ -643,7 +656,7 @@ func TestGetK8s(t *testing.T) { Master: Master{ Name: "master2", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker3"}, {Name: "worker4"}}, } err := db.CreateK8s(&k8s) @@ -653,10 +666,12 @@ func TestGetK8s(t *testing.T) { k, err := db.GetK8s(k8s.ID) require.NoError(t, err) - require.Equal(t, k, k8s) + require.Equal(t, len(k.Workers), len(k8s.Workers)) + require.Equal(t, k.Master.ClusterID, k8s.Master.ClusterID) require.NotEqual(t, k, k8s2) }) } + func TestGetAllK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -664,6 +679,7 @@ func TestGetAllK8s(t *testing.T) { require.NoError(t, err) require.Empty(t, c) }) + t.Run("K8s found", func(t *testing.T) { k8s1 := K8sCluster{ UserID: "user", @@ -677,14 +693,14 @@ func TestGetAllK8s(t *testing.T) { Master: Master{ Name: "master2", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker3"}, {Name: "worker4"}}, } k8s3 := K8sCluster{ UserID: "new-user", Master: Master{ Name: "master3", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker5"}, {Name: "worker6"}}, } err := db.CreateK8s(&k8s1) @@ -696,14 +712,19 @@ func TestGetAllK8s(t *testing.T) { k, err := db.GetAllK8s("user") require.NoError(t, err) - require.Equal(t, k, []K8sCluster{k8s1, k8s2}) + require.Equal(t, len(k[0].Workers), len(k8s1.Workers)) + require.Equal(t, k[0].Master.ClusterID, k8s1.Master.ClusterID) + require.Equal(t, len(k[1].Workers), len(k8s2.Workers)) + require.Equal(t, k[1].Master.ClusterID, k8s2.Master.ClusterID) k, err = db.GetAllK8s("new-user") require.NoError(t, err) - require.Equal(t, k, []K8sCluster{k8s3}) + require.Equal(t, len(k[0].Workers), len(k8s3.Workers)) + require.Equal(t, k[0].Master.ClusterID, k8s3.Master.ClusterID) }) } + func TestDeleteK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -723,9 +744,9 @@ func TestDeleteK8s(t *testing.T) { k8s2 := K8sCluster{ UserID: "new-user", Master: Master{ - Name: "master", + Name: "master2", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker3"}, {Name: "worker4"}}, } err := db.CreateK8s(&k8s1) @@ -741,9 +762,11 @@ func TestDeleteK8s(t *testing.T) { k, err := db.GetK8s(k8s2.ID) require.NoError(t, err) - require.Equal(t, k, k8s2) + require.Equal(t, len(k.Workers), len(k8s2.Workers)) + require.Equal(t, k.Master.ClusterID, k8s2.Master.ClusterID) }) } + func TestDeleteAllK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -763,16 +786,16 @@ func TestDeleteAllK8s(t *testing.T) { k8s2 := K8sCluster{ UserID: "user", Master: Master{ - Name: "master", + Name: "master2", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker3"}, {Name: "worker4"}}, } k8s3 := K8sCluster{ UserID: "new-user", Master: Master{ - Name: "master", + Name: "master3", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker5"}, {Name: "worker6"}}, } err := db.CreateK8s(&k8s1) @@ -791,7 +814,9 @@ func TestDeleteAllK8s(t *testing.T) { k, err = db.GetAllK8s("new-user") require.NoError(t, err) - require.Equal(t, k, []K8sCluster{k8s3}) + + require.Equal(t, len(k[0].Workers), len(k8s3.Workers)) + require.Equal(t, k[0].Master.ClusterID, k8s3.Master.ClusterID) }) t.Run("test with no id", func(t *testing.T) { @@ -829,9 +854,9 @@ func TestAvailableK8sName(t *testing.T) { k8s := K8sCluster{ UserID: "user", Master: Master{ - Name: "master", + Name: "master2", }, - Workers: []Worker{{Name: "worker1"}, {Name: "worker2"}}, + Workers: []Worker{{Name: "worker3"}, {Name: "worker4"}}, } err := db.CreateK8s(&k8s) require.NoError(t, err) diff --git a/server/models/k8s.go b/server/models/k8s.go index 9f8ebe09..0ffb58c1 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -70,15 +70,15 @@ func (d *DB) GetK8s(id int) (K8sCluster, error) { } var master Master - err = d.db.Model(&k8s).Association("Master").Find(&master) - if err != nil { + if err = d.db.Model(&k8s).Association("Master").Find(&master); err != nil { return K8sCluster{}, err } + var workers []Worker - err = d.db.Model(&k8s).Association("Workers").Find(&workers) - if err != nil { - return K8sCluster{}, err + if err = d.db.Model(&k8s).Association("Workers").Find(&workers); err != nil { + return K8sCluster{}, nil } + k8s.Master = master k8s.Workers = workers diff --git a/server/models/state.go b/server/models/state.go index dbd289be..87987135 100644 --- a/server/models/state.go +++ b/server/models/state.go @@ -3,7 +3,7 @@ package models type state string const ( - StateCreated = "CREATED" // TODO: + StateCreated = "CREATED" StateFailed = "FAILED" StateInProgress = "INPROGRESS" ) From 4a180e7517de879f92bfc249faee5a537ff0c600 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Sun, 22 Dec 2024 12:40:52 +0200 Subject: [PATCH 3/7] support listing regions --- server/app/app.go | 3 + server/app/vm_handler.go | 44 +++++++ server/app/wrapper.go | 2 +- server/deployer/balance.go | 2 +- server/deployer/deployer.go | 20 +-- server/deployer/k8s_deployer.go | 8 +- server/deployer/vms_deployer.go | 6 +- server/docs/docs.go | 39 ++++++ server/docs/swagger.yaml | 25 ++++ server/internal/graphql.go | 219 ++++++++++++++++++++++++++++++++ 10 files changed, 349 insertions(+), 19 deletions(-) create mode 100644 server/internal/graphql.go diff --git a/server/app/app.go b/server/app/app.go index 953e96ea..644255c4 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -119,6 +119,7 @@ func (a *App) registerHandlers() { notificationRouter := authRouter.PathPrefix("/notification").Subrouter() vmRouter := authRouter.PathPrefix("/vm").Subrouter() k8sRouter := authRouter.PathPrefix("/k8s").Subrouter() + regionRouter := authRouter.PathPrefix("/region").Subrouter() // sub routes with no authorization unAuthUserRouter := versionRouter.PathPrefix("/user").Subrouter() @@ -158,6 +159,8 @@ func (a *App) registerHandlers() { notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS") notificationRouter.HandleFunc("/{id}", WrapFunc(a.UpdateNotificationsHandler)).Methods("PUT", "OPTIONS") + regionRouter.HandleFunc("", WrapFunc(a.ListRegionsHandler)).Methods("GET", "OPTIONS") + vmRouter.HandleFunc("", WrapFunc(a.DeployVMHandler)).Methods("POST", "OPTIONS") vmRouter.HandleFunc("/validate/{name}", WrapFunc(a.ValidateVMNameHandler)).Methods("Get", "OPTIONS") vmRouter.HandleFunc("/{id}", WrapFunc(a.GetVMHandler)).Methods("GET", "OPTIONS") diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index 5f91becd..2e21af72 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -9,11 +9,13 @@ import ( "strings" "github.com/codescalers/cloud4students/deployer" + "github.com/codescalers/cloud4students/internal" "github.com/codescalers/cloud4students/middlewares" "github.com/codescalers/cloud4students/models" "github.com/codescalers/cloud4students/streams" "github.com/gorilla/mux" "github.com/rs/zerolog/log" + "github.com/threefoldtech/tfgrid-sdk-go/grid-proxy/pkg/types" "gopkg.in/validator.v2" "gorm.io/gorm" ) @@ -352,3 +354,45 @@ func (a *App) DeleteAllVMsHandler(req *http.Request) (interface{}, Response) { Data: nil, }, Ok() } + +// ListRegionsHandler returns all supported regions +// Example endpoint: List all supported regions +// @Summary List all supported regions +// @Description List all supported regions +// @Tags Region +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []string +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /region [get] +func (a *App) ListRegionsHandler(req *http.Request) (interface{}, Response) { + stats, err := a.deployer.TFPluginClient.GridProxyClient.Stats(req.Context(), types.StatsFilter{}) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + graphql, err := internal.NewGraphQl(a.config.Account.Network) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + var countries []string + for country := range stats.NodesDistribution { + countries = append(countries, country) + } + + regions, err := graphql.ListRegions(countries) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Regions are found", + Data: regions, + }, Ok() +} diff --git a/server/app/wrapper.go b/server/app/wrapper.go index afe97832..69d0bc42 100644 --- a/server/app/wrapper.go +++ b/server/app/wrapper.go @@ -137,7 +137,7 @@ func BadRequest(err error) Response { // InternalServerError result func InternalServerError(err error) Response { - return Error(err, 0) + return Error(err) } // NotFound response diff --git a/server/deployer/balance.go b/server/deployer/balance.go index 5cb5019b..2c62eec9 100644 --- a/server/deployer/balance.go +++ b/server/deployer/balance.go @@ -7,7 +7,7 @@ import ( // GetBalance returns the current balance of the deployer account func (d *Deployer) GetBalance() (float64, error) { - balance, err := d.tfPluginClient.SubstrateConn.GetBalance(d.tfPluginClient.Identity) + balance, err := d.TFPluginClient.SubstrateConn.GetBalance(d.TFPluginClient.Identity) if err != nil { return 0, errors.Wrap(err, "failed to get account balance with the given mnemonics") } diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index 63ecf104..cd16b8e4 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -48,7 +48,7 @@ var ( type Deployer struct { db models.DB Redis streams.RedisClient - tfPluginClient deployer.TFPluginClient + TFPluginClient deployer.TFPluginClient prices internal.Prices vmDeployed chan bool @@ -108,12 +108,12 @@ func (d *Deployer) PeriodicDeploy(ctx context.Context, sec int) { } if len(vms) > 0 { - err := d.tfPluginClient.NetworkDeployer.BatchDeploy(ctx, vmNets) + err := d.TFPluginClient.NetworkDeployer.BatchDeploy(ctx, vmNets) if err != nil { log.Error().Err(err).Msg("failed to batch deploy network") } - err = d.tfPluginClient.DeploymentDeployer.BatchDeploy(ctx, vms) + err = d.TFPluginClient.DeploymentDeployer.BatchDeploy(ctx, vms) if err != nil { log.Error().Err(err).Msg("failed to batch deploy vm") } @@ -124,12 +124,12 @@ func (d *Deployer) PeriodicDeploy(ctx context.Context, sec int) { } if len(clusters) > 0 { - err := d.tfPluginClient.NetworkDeployer.BatchDeploy(ctx, k8sNets) + err := d.TFPluginClient.NetworkDeployer.BatchDeploy(ctx, k8sNets) if err != nil { log.Error().Err(err).Msg("failed to batch deploy network") } - err = d.tfPluginClient.K8sDeployer.BatchDeploy(ctx, clusters) + err = d.TFPluginClient.K8sDeployer.BatchDeploy(ctx, clusters) if err != nil { log.Error().Err(err).Msg("failed to batch deploy clusters") } @@ -144,24 +144,24 @@ func (d *Deployer) PeriodicDeploy(ctx context.Context, sec int) { // CancelDeployment cancel deployments from grid func (d *Deployer) CancelDeployment(contractID uint64, netContractID uint64, dlType string, dlName string) error { // cancel deployment - err := d.tfPluginClient.SubstrateConn.CancelContract(d.tfPluginClient.Identity, contractID) + err := d.TFPluginClient.SubstrateConn.CancelContract(d.TFPluginClient.Identity, contractID) if err != nil { return err } // cancel network - err = d.tfPluginClient.SubstrateConn.CancelContract(d.tfPluginClient.Identity, netContractID) + err = d.TFPluginClient.SubstrateConn.CancelContract(d.TFPluginClient.Identity, netContractID) if err != nil { return err } // update state - for node, contracts := range d.tfPluginClient.State.CurrentNodeDeployments { + for node, contracts := range d.TFPluginClient.State.CurrentNodeDeployments { contracts = workloads.Delete(contracts, contractID) contracts = workloads.Delete(contracts, netContractID) - d.tfPluginClient.State.CurrentNodeDeployments[node] = contracts + d.TFPluginClient.State.CurrentNodeDeployments[node] = contracts - d.tfPluginClient.State.Networks.DeleteNetwork(fmt.Sprintf("%s%sNet", dlType, dlName)) + d.TFPluginClient.State.Networks.DeleteNetwork(fmt.Sprintf("%s%sNet", dlType, dlName)) } return nil diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index 8721739d..08abd22a 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -124,12 +124,12 @@ func (d *Deployer) deployK8sClusterWithNetwork(ctx context.Context, k8sDeployInp } // checks that network and k8s are deployed successfully - loadedNet, err := d.tfPluginClient.State.LoadNetworkFromGrid(ctx, cluster.NetworkName) + loadedNet, err := d.TFPluginClient.State.LoadNetworkFromGrid(ctx, cluster.NetworkName) if err != nil { return 0, 0, 0, errors.Wrapf(err, "failed to load network '%s' on nodes %v", cluster.NetworkName, network.Nodes) } - loadedCluster, err := d.tfPluginClient.State.LoadK8sFromGrid(ctx, []uint32{node}, cluster.Master.Name) + loadedCluster, err := d.TFPluginClient.State.LoadK8sFromGrid(ctx, []uint32{node}, cluster.Master.Name) if err != nil { return 0, 0, 0, errors.Wrapf(err, "failed to load kubernetes cluster '%s' on nodes %v", cluster.Master.Name, network.Nodes) } @@ -144,7 +144,7 @@ func (d *Deployer) loadK8s( networkContractID uint64, k8sContractID uint64, ) (models.K8sCluster, error) { // load cluster - resCluster, err := d.tfPluginClient.State.LoadK8sFromGrid(ctx, []uint32{node}, k8s.Master.Name) + resCluster, err := d.TFPluginClient.State.LoadK8sFromGrid(ctx, []uint32{node}, k8s.Master.Name) if err != nil { return models.K8sCluster{}, err } @@ -207,7 +207,7 @@ func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sCluster) filter.Region = &k.Master.Region } - nodes, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, rootfs, 1) + nodes, err := deployer.FilterNodes(ctx, d.TFPluginClient, filter, []uint64{*freeSRU}, nil, rootfs, 1) if err != nil { return 0, err } diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index c4e44859..37970bdc 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -38,7 +38,7 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.VM, sshKey strin filter.Region = &vmInput.Region } - nodeIDs, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, nil, 1) + nodeIDs, err := deployer.FilterNodes(ctx, d.TFPluginClient, filter, []uint64{*freeSRU}, nil, nil, 1) if err != nil { return nil, 0, 0, err } @@ -97,12 +97,12 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.VM, sshKey strin } // checks that network and vm are deployed successfully - loadedNet, err := d.tfPluginClient.State.LoadNetworkFromGrid(ctx, dl.NetworkName) + loadedNet, err := d.TFPluginClient.State.LoadNetworkFromGrid(ctx, dl.NetworkName) if err != nil { return nil, 0, 0, errors.Wrapf(err, "failed to load network '%s' on node %v", dl.NetworkName, dl.NodeID) } - loadedDl, err := d.tfPluginClient.State.LoadDeploymentFromGrid(ctx, nodeID, dl.Name) + loadedDl, err := d.TFPluginClient.State.LoadDeploymentFromGrid(ctx, nodeID, dl.Name) if err != nil { return nil, 0, 0, errors.Wrapf(err, "failed to load vm '%s' on node %v", dl.Name, dl.NodeID) } diff --git a/server/docs/docs.go b/server/docs/docs.go index b3980ae4..9f75e000 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -1004,6 +1004,45 @@ const docTemplate = `{ } } }, + "/region": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all supported regions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Region" + ], + "summary": "List all supported regions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/set_admin": { "put": { "security": [ diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 45469305..047a5138 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1282,6 +1282,31 @@ paths: summary: Set user's notifications as seen tags: - Notification + /region: + get: + consumes: + - application/json + description: List all supported regions + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List all supported regions + tags: + - Region /set_admin: put: consumes: diff --git a/server/internal/graphql.go b/server/internal/graphql.go new file mode 100644 index 00000000..a47a7e29 --- /dev/null +++ b/server/internal/graphql.go @@ -0,0 +1,219 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "slices" + "time" + + "github.com/cenkalti/backoff" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +var ( + DevNetwork = "dev" + QaNetwork = "qa" + TestNetwork = "test" + MainNetwork = "main" + + // GraphQlURLs for graphql urls + GraphQlURLs = map[string][]string{ + DevNetwork: { + "https://graphql.dev.grid.tf/graphql", + "https://graphql.02.dev.grid.tf/graphql", + }, + TestNetwork: { + "https://graphql.test.grid.tf/graphql", + "https://graphql.02.test.grid.tf/graphql", + }, + QaNetwork: { + "https://graphql.qa.grid.tf/graphql", + "https://graphql.02.qa.grid.tf/graphql", + }, + MainNetwork: { + "https://graphql.grid.tf/graphql", + "https://graphql.02.grid.tf/graphql", + }, + } +) + +// GraphQl for tf graphql +type GraphQl struct { + urls []string + activeStackIdx int +} + +// NewGraphQl new tf graphql +func NewGraphQl(network string) (GraphQl, error) { + if len(network) == 0 { + return GraphQl{}, errors.New("network is required") + } + + return GraphQl{urls: GraphQlURLs[network], activeStackIdx: 0}, nil +} + +// ListContractsByTwinID returns contracts for a twinID +func (g *GraphQl) ListRegions(countries []string) ([]string, error) { + options := fmt.Sprintf("(orderBy: region_ASC, where: {name_in: %q})", countries) + countriesCount, err := g.getItemTotalCount("countries", options) + if err != nil { + return nil, err + } + + countriesData, err := g.query(`query getRegions($countriesCount: Int!){ + countries(limit: $countriesCount) { + region + } + }`, + map[string]interface{}{ + "countriesCount": countriesCount, + }) + if err != nil { + return nil, err + } + + countriesJSONData, err := json.Marshal(countriesData) + if err != nil { + return nil, err + } + + var listCountries struct { + Countries []struct { + Region string + } + } + err = json.Unmarshal(countriesJSONData, &listCountries) + if err != nil { + return nil, err + } + + var regions []string + for _, c := range listCountries.Countries { + if !slices.Contains(regions, c.Region) { + regions = append(regions, c.Region) + } + } + + return regions, nil +} + +// getItemTotalCount return count of items +func (g *GraphQl) getItemTotalCount(itemName string, options string) (float64, error) { + countBody := fmt.Sprintf(`query { items: %vConnection%v { count: totalCount } }`, itemName, options) + requestBody := map[string]interface{}{"query": countBody} + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return 0, err + } + + bodyReader := bytes.NewReader(jsonBody) + + countResponse, err := g.httpPost(bodyReader) + if err != nil { + return 0, err + } + + queryData, err := parseHTTPResponse(countResponse) + if err != nil { + return 0, err + } + + countMap := queryData["data"].(map[string]interface{}) + countItems := countMap["items"].(map[string]interface{}) + count := countItems["count"].(float64) + + return count, nil +} + +// query queries graphql +func (g *GraphQl) query(body string, variables map[string]interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + requestBody := map[string]interface{}{"query": body, "variables": variables} + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return result, err + } + + bodyReader := bytes.NewReader(jsonBody) + + resp, err := g.httpPost(bodyReader) + if err != nil { + return result, err + } + + queryData, err := parseHTTPResponse(resp) + if err != nil { + return result, err + } + + result = queryData["data"].(map[string]interface{}) + return result, nil +} + +func parseHTTPResponse(resp *http.Response) (map[string]interface{}, error) { + resBody, err := io.ReadAll(resp.Body) + if err != nil { + return map[string]interface{}{}, err + } + + defer resp.Body.Close() + + var data map[string]interface{} + err = json.Unmarshal(resBody, &data) + if err != nil { + return map[string]interface{}{}, err + } + + if resp.StatusCode >= 400 { + return map[string]interface{}{}, errors.Errorf("request failed with status code: %d with error %v", resp.StatusCode, data) + } + + return data, nil +} + +func (g *GraphQl) httpPost(body io.Reader) (*http.Response, error) { + cl := &http.Client{ + Timeout: 10 * time.Second, + } + + var ( + endpoint string + reqErr error + resp *http.Response + ) + + backoffCfg := backoff.WithMaxRetries( + backoff.NewConstantBackOff(1*time.Millisecond), + 2, + ) + + err := backoff.RetryNotify(func() error { + endpoint = g.urls[g.activeStackIdx] + log.Debug().Str("url", endpoint).Msg("checking") + + resp, reqErr = cl.Post(endpoint, "application/json", body) + if reqErr != nil && + (errors.Is(reqErr, http.ErrAbortHandler) || + errors.Is(reqErr, http.ErrHandlerTimeout) || + errors.Is(reqErr, http.ErrServerClosed)) { + g.activeStackIdx = (g.activeStackIdx + 1) % len(g.urls) + return reqErr + } + + return nil + }, backoffCfg, func(err error, _ time.Duration) { + log.Error().Err(err).Msg("failed to connect to endpoint, retrying") + }) + + if err != nil { + log.Error().Err(err).Msg("failed to connect to endpoint") + } + + return resp, reqErr +} From 8f8673034eb0938c30e87fe9264bac56b92ac9ba Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Mon, 23 Dec 2024 18:44:56 +0200 Subject: [PATCH 4/7] add delete user endpoint --- client/src/components/Toast.vue | 2 + server/app/admin_handler.go | 18 +- server/app/app.go | 4 +- server/app/invoice_handler.go | 414 +++++++++--------- server/app/k8s_handler.go | 8 +- server/app/payments_handler.go | 72 ++- server/app/user_handler.go | 252 +++++++++-- server/app/user_handler_test.go | 1 + server/app/vm_handler.go | 2 +- server/app/voucher_handler.go | 9 +- server/app/voucher_handler_test.go | 6 +- server/deployer/deployer.go | 43 +- server/docs/docs.go | 184 ++++++-- server/docs/swagger.yaml | 141 ++++-- server/go.mod | 8 +- server/go.sum | 12 +- server/internal/config_parser_test.go | 96 +++- server/internal/email_sender.go | 8 +- server/internal/email_sender_test.go | 2 +- .../internal/templates/adminAnnouncement.html | 2 +- server/internal/templates/signup.html | 2 +- server/models/card.go | 11 +- server/models/database.go | 2 +- server/models/invoice.go | 59 +-- server/models/k8s.go | 17 + server/models/user.go | 29 +- server/models/vm.go | 6 + 27 files changed, 992 insertions(+), 418 deletions(-) diff --git a/client/src/components/Toast.vue b/client/src/components/Toast.vue index 5c7c8c2d..732e75b3 100644 --- a/client/src/components/Toast.vue +++ b/client/src/components/Toast.vue @@ -8,12 +8,14 @@ import { createToast, clearToasts } from "mosha-vue-toastify"; export default { setup() { const toast = (title, color = "#217dbb") => { + if (title.length > 0) { createToast(title.charAt(0).toUpperCase() + title.slice(1), { position: "bottom-right", hideProgressBar: true, toastBackgroundColor: color, timeout: 8000, }); + } }; const clear = () => { clearToasts(); diff --git a/server/app/admin_handler.go b/server/app/admin_handler.go index a0dc533a..f0799139 100644 --- a/server/app/admin_handler.go +++ b/server/app/admin_handler.go @@ -18,31 +18,31 @@ import ( // AdminAnnouncement struct for data needed when admin sends new announcement type AdminAnnouncement struct { - Subject string `json:"subject" binding:"required"` - Body string `json:"announcement" binding:"required"` + Subject string `json:"subject" validate:"nonzero" binding:"required"` + Body string `json:"announcement" validate:"nonzero" binding:"required"` } // EmailUser struct for data needed when admin sends new email to a user type EmailUser struct { - Subject string `json:"subject" binding:"required"` - Body string `json:"body" binding:"required"` + Subject string `json:"subject" validate:"nonzero" binding:"required"` + Body string `json:"body" validate:"nonzero" binding:"required"` Email string `json:"email" binding:"required" validate:"mail"` } // UpdateMaintenanceInput struct for data needed when user update maintenance type UpdateMaintenanceInput struct { - ON bool `json:"on" binding:"required"` + ON bool `json:"on" validate:"nonzero" binding:"required"` } // SetAdminInput struct for setting users as admins type SetAdminInput struct { - Email string `json:"email" binding:"required"` - Admin bool `json:"admin" binding:"required"` + Email string `json:"email" binding:"required" validate:"mail"` + Admin bool `json:"admin" validate:"nonzero" binding:"required"` } // UpdateNextLaunchInput struct for data needed when updating next launch state type UpdateNextLaunchInput struct { - Launched bool `json:"launched" binding:"required"` + Launched bool `json:"launched" validate:"nonzero" binding:"required"` } // SetPricesInput struct for setting prices as admins @@ -556,7 +556,7 @@ func (a *App) CreateNewAnnouncementHandler(req *http.Request) (interface{}, Resp // @Failure 401 {object} Response // @Failure 404 {object} Response // @Failure 500 {object} Response -// @Router /announcement [post] +// @Router /email [post] func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) { var emailUser EmailUser err := json.NewDecoder(req.Body).Decode(&emailUser) diff --git a/server/app/app.go b/server/app/app.go index 644255c4..da38e776 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -137,12 +137,14 @@ func (a *App) registerHandlers() { unAuthUserRouter.HandleFunc("/signup/verify_email", WrapFunc(a.VerifySignUpCodeHandler)).Methods("POST", "OPTIONS") unAuthUserRouter.HandleFunc("/signin", WrapFunc(a.SignInHandler)).Methods("POST", "OPTIONS") unAuthUserRouter.HandleFunc("/refresh_token", WrapFunc(a.RefreshJWTHandler)).Methods("POST", "OPTIONS") + // TODO: rename it unAuthUserRouter.HandleFunc("/forgot_password", WrapFunc(a.ForgotPasswordHandler)).Methods("POST", "OPTIONS") unAuthUserRouter.HandleFunc("/forget_password/verify_email", WrapFunc(a.VerifyForgetPasswordCodeHandler)).Methods("POST", "OPTIONS") userRouter.HandleFunc("/change_password", WrapFunc(a.ChangePasswordHandler)).Methods("PUT", "OPTIONS") userRouter.HandleFunc("", WrapFunc(a.UpdateUserHandler)).Methods("PUT", "OPTIONS") userRouter.HandleFunc("", WrapFunc(a.GetUserHandler)).Methods("GET", "OPTIONS") + userRouter.HandleFunc("", WrapFunc(a.DeleteUserHandler)).Methods("DELETE", "OPTIONS") userRouter.HandleFunc("/apply_voucher", WrapFunc(a.ApplyForVoucherHandler)).Methods("POST", "OPTIONS") userRouter.HandleFunc("/activate_voucher", WrapFunc(a.ActivateVoucherHandler)).Methods("PUT", "OPTIONS") userRouter.HandleFunc("/charge_balance", WrapFunc(a.ChargeBalance)).Methods("PUT", "OPTIONS") @@ -196,7 +198,7 @@ func (a *App) registerHandlers() { voucherRouter.HandleFunc("", WrapFunc(a.ListVouchersHandler)).Methods("GET", "OPTIONS") voucherRouter.HandleFunc("/{id}", WrapFunc(a.UpdateVoucherHandler)).Methods("PUT", "OPTIONS") voucherRouter.HandleFunc("", WrapFunc(a.ApproveAllVouchersHandler)).Methods("PUT", "OPTIONS") - voucherRouter.HandleFunc("/reset", WrapFunc(a.ResetUsersVoucherBalanceHandler)).Methods("PUT", "OPTIONS") + voucherRouter.HandleFunc("/all/reset", WrapFunc(a.ResetUsersVoucherBalanceHandler)).Methods("PUT", "OPTIONS") // middlewares r.Use(middlewares.LoggingMW) diff --git a/server/app/invoice_handler.go b/server/app/invoice_handler.go index 35a90fb8..5d1ff0af 100644 --- a/server/app/invoice_handler.go +++ b/server/app/invoice_handler.go @@ -38,7 +38,7 @@ var methods = []method{ } type PayInvoiceInput struct { - Method method `json:"method" binding:"required"` + Method method `json:"method" validate:"nonzero" binding:"required"` CardPaymentID string `json:"card_payment_id"` } @@ -179,144 +179,13 @@ func (a *App) PayInvoiceHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - var paymentDetails models.PaymentDetails - - switch input.Method { - case card: - _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total) - if err != nil { - log.Error().Err(err).Send() - return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) - } - - paymentDetails = models.PaymentDetails{Card: invoice.Total} - - case balance: - if user.Balance < invoice.Total { - return nil, BadRequest(errors.New("balance is not enough to pay the invoice")) - } - - paymentDetails = models.PaymentDetails{Balance: invoice.Total} - - user.Balance -= invoice.Total - if err = a.db.UpdateUserByID(user); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - case voucher: - if user.VoucherBalance < invoice.Total { - return nil, BadRequest(errors.New("voucher balance is not enough to pay the invoice")) - } - - paymentDetails = models.PaymentDetails{VoucherBalance: invoice.Total} - - user.VoucherBalance -= invoice.Total - if err = a.db.UpdateUserByID(user); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - case voucherAndBalance: - if user.VoucherBalance+user.Balance < invoice.Total { - return nil, BadRequest(errors.New("voucher balance and balance are not enough to pay the invoice")) - } - - if user.VoucherBalance > invoice.Total { - paymentDetails = models.PaymentDetails{VoucherBalance: invoice.Total} - user.VoucherBalance -= invoice.Total - } else { - paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Balance: (invoice.Total - user.VoucherBalance)} - user.Balance = (invoice.Total - user.VoucherBalance) - user.VoucherBalance = 0 - } - - if err = a.db.UpdateUserByID(user); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - case voucherAndCard: - if user.VoucherBalance > invoice.Total { - paymentDetails = models.PaymentDetails{VoucherBalance: invoice.Total} - user.VoucherBalance -= invoice.Total - } else { - paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Card: (invoice.Total - user.VoucherBalance)} - _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total-user.VoucherBalance) - if err != nil { - log.Error().Err(err).Send() - return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) - } - user.VoucherBalance = 0 - } - - if err = a.db.UpdateUserByID(user); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - case balanceAndCard: - if user.Balance > invoice.Total { - paymentDetails = models.PaymentDetails{Balance: invoice.Total} - user.Balance -= invoice.Total - } else { - _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total-user.Balance) - if err != nil { - log.Error().Err(err).Send() - return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) - } - paymentDetails = models.PaymentDetails{Balance: user.Balance, Card: (invoice.Total - user.Balance)} - user.Balance = 0 - } - - if err = a.db.UpdateUserByID(user); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - case voucherAndBalanceAndCard: - if user.VoucherBalance > invoice.Total { - paymentDetails = models.PaymentDetails{Balance: invoice.Total} - user.VoucherBalance -= invoice.Total - } else if user.Balance+user.VoucherBalance > invoice.Total { - paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Balance: (invoice.Total - user.VoucherBalance)} - user.Balance = (invoice.Total - user.VoucherBalance) - user.VoucherBalance = 0 - } else { - _, err := createPaymentIntent(user.StripeCustomerID, input.CardPaymentID, a.config.Currency, invoice.Total-user.VoucherBalance-user.Balance) - if err != nil { - log.Error().Err(err).Send() - return nil, BadRequest(errors.New("payment failed, please try again later or report the problem")) - } - paymentDetails = models.PaymentDetails{ - Balance: user.Balance, VoucherBalance: user.VoucherBalance, - Card: (invoice.Total - user.Balance - user.VoucherBalance), - } - user.VoucherBalance = 0 - user.Balance = 0 - } - - if err = a.db.UpdateUserByID(user); err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - default: - return nil, BadRequest(fmt.Errorf("invalid payment method, only methods allowed %v", methods)) - } - - err = a.db.PayInvoice(id, paymentDetails) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("invoice is not found")) - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) + response := a.payInvoice(&user, input.CardPaymentID, input.Method, invoice.Total, id) + if response.Err() != nil { + return nil, response } return ResponseMsg{ Message: "Invoice is paid successfully", - Data: nil, }, Ok() } @@ -348,19 +217,47 @@ func (a *App) monthlyInvoices() { log.Error().Err(err).Send() } - // 2. Use balance/voucher balance to pay invoices - user.Balance, user.VoucherBalance, err = a.db.PayUserInvoices(user.ID.String(), user.Balance, user.VoucherBalance) + // 2. Pay invoices + invoices, err := a.db.ListUnpaidInvoices(user.ID.String()) if err != nil { log.Error().Err(err).Send() - } else { - if err = a.db.UpdateUserByID(user); err != nil { + } + + for _, invoice := range invoices { + cards, err := a.db.GetUserCards(user.ID.String()) + if err != nil { log.Error().Err(err).Send() } - } - // 3. Use cards to pay invoices - if err = a.payUserInvoicesUsingCards(user.ID.String(), user.StripeCustomerID, user.StripeDefaultPaymentID, true); err != nil { - log.Error().Err(err).Send() + // No cards option + if len(cards) == 0 { + response := a.payInvoice(&user, "", voucherAndBalance, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } + continue + } + + // Use default card + response := a.payInvoice(&user, user.StripeDefaultPaymentID, voucherAndBalanceAndCard, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } else { + continue + } + + for _, card := range cards { + if card.PaymentMethodID == user.StripeDefaultPaymentID { + continue + } + + response := a.payInvoice(&user, card.PaymentMethodID, voucherAndBalanceAndCard, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + continue + } + break + } } // 4. Delete expired deployments with invoices not paid for more than 3 months @@ -379,15 +276,14 @@ func (a *App) monthlyInvoices() { } func (a *App) createInvoice(userID string, now time.Time) error { - usagePercentageInMonth := deployer.UsagePercentageInMonth(now) - firstDayOfMonth := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local) + monthStart := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local) - vms, err := a.db.GetAllVms(userID) + vms, err := a.db.GetAllSuccessfulVms(userID) if err != nil && err != gorm.ErrRecordNotFound { return err } - k8s, err := a.db.GetAllK8s(userID) + k8s, err := a.db.GetAllSuccessfulK8s(userID) if err != nil && err != gorm.ErrRecordNotFound { return err } @@ -396,6 +292,16 @@ func (a *App) createInvoice(userID string, now time.Time) error { var total float64 for _, vm := range vms { + usageStart := monthStart + if vm.CreatedAt.After(monthStart) { + usageStart = vm.CreatedAt + } + + usagePercentageInMonth, err := deployer.UsagePercentageInMonth(usageStart, now) + if err != nil { + return err + } + cost := float64(vm.PricePerMonth) * usagePercentageInMonth items = append(items, models.DeploymentItem{ @@ -403,7 +309,7 @@ func (a *App) createInvoice(userID string, now time.Time) error { DeploymentType: "vm", DeploymentID: vm.ID, HasPublicIP: vm.Public, - PeriodInHours: time.Since(firstDayOfMonth).Hours(), + PeriodInHours: time.Since(usageStart).Hours(), Cost: cost, }) @@ -411,6 +317,16 @@ func (a *App) createInvoice(userID string, now time.Time) error { } for _, cluster := range k8s { + usageStart := monthStart + if cluster.CreatedAt.After(monthStart) { + usageStart = cluster.CreatedAt + } + + usagePercentageInMonth, err := deployer.UsagePercentageInMonth(usageStart, now) + if err != nil { + return err + } + cost := float64(cluster.PricePerMonth) * usagePercentageInMonth items = append(items, models.DeploymentItem{ @@ -418,68 +334,20 @@ func (a *App) createInvoice(userID string, now time.Time) error { DeploymentType: "k8s", DeploymentID: cluster.ID, HasPublicIP: cluster.Master.Public, - PeriodInHours: time.Since(firstDayOfMonth).Hours(), + PeriodInHours: time.Since(usageStart).Hours(), Cost: cost, }) total += cost } - if err = a.db.CreateInvoice(&models.Invoice{ - UserID: userID, - Total: total, - Deployments: items, - }); err != nil { - return err - } - - return nil -} - -// payUserInvoicesUsingCards tries to pay invoices with user cards -func (a *App) payUserInvoicesUsingCards(userID, customerID, defaultPaymentMethod string, useOtherCards bool) error { - // get unpaid invoices - invoices, err := a.db.ListUnpaidInvoices(userID) - if err != nil { - return err - } - - cards, err := a.db.GetUserCards(userID) - if err != nil { - return err - } - - for _, invoice := range invoices { - // 1. use default payment method - if len(defaultPaymentMethod) != 0 { - _, err := createPaymentIntent(customerID, defaultPaymentMethod, a.config.Currency, invoice.Total) - if err != nil { - log.Error().Err(err).Send() - } else { - if err := a.db.PayInvoice(invoice.ID, models.PaymentDetails{Card: invoice.Total}); err != nil { - log.Error().Err(err).Send() - } - continue - } - } - - if !useOtherCards { - continue - } - - // 2. check other user cards - for _, card := range cards { - if defaultPaymentMethod != card.PaymentMethodID { - _, err := createPaymentIntent(customerID, card.PaymentMethodID, a.config.Currency, invoice.Total) - if err != nil { - log.Error().Err(err).Send() - } else { - if err := a.db.PayInvoice(invoice.ID, models.PaymentDetails{Card: invoice.Total}); err != nil { - log.Error().Err(err).Send() - } - break - } - } + if len(items) > 0 { + if err = a.db.CreateInvoice(&models.Invoice{ + UserID: userID, + Total: total, + Deployments: items, + }); err != nil { + return err } } @@ -598,6 +466,144 @@ func (a *App) sendInvoiceReminderToUser(userID, userEmail, userName string, now return nil } +func (a *App) pay(user *models.User, cardPaymentID string, method method, invoiceTotal float64) (models.PaymentDetails, error) { + var paymentDetails models.PaymentDetails + + switch method { + case card: + _, err := createPaymentIntent(user.StripeCustomerID, cardPaymentID, a.config.Currency, invoiceTotal) + if err != nil { + log.Error().Err(err).Send() + return paymentDetails, errors.New("payment failed, please try again later or report the problem") + } + + paymentDetails = models.PaymentDetails{Card: invoiceTotal} + + case balance: + if user.Balance < invoiceTotal { + return paymentDetails, errors.New("balance is not enough to pay the invoice") + } + + paymentDetails = models.PaymentDetails{Balance: invoiceTotal} + user.Balance -= invoiceTotal + + case voucher: + if user.VoucherBalance < invoiceTotal { + return paymentDetails, errors.New("voucher balance is not enough to pay the invoice") + } + + paymentDetails = models.PaymentDetails{VoucherBalance: invoiceTotal} + user.VoucherBalance -= invoiceTotal + + case voucherAndBalance: + if user.VoucherBalance+user.Balance < invoiceTotal { + return paymentDetails, errors.New("voucher balance and balance are not enough to pay the invoice") + } + + if user.VoucherBalance >= invoiceTotal { + paymentDetails = models.PaymentDetails{VoucherBalance: invoiceTotal} + user.VoucherBalance -= invoiceTotal + } else { + paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Balance: (invoiceTotal - user.VoucherBalance)} + user.Balance = (invoiceTotal - user.VoucherBalance) + user.VoucherBalance = 0 + } + + case voucherAndCard: + if user.VoucherBalance >= invoiceTotal { + paymentDetails = models.PaymentDetails{VoucherBalance: invoiceTotal} + user.VoucherBalance -= invoiceTotal + } else { + paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Card: (invoiceTotal - user.VoucherBalance)} + _, err := createPaymentIntent(user.StripeCustomerID, cardPaymentID, a.config.Currency, invoiceTotal-user.VoucherBalance) + if err != nil { + log.Error().Err(err).Send() + return paymentDetails, errors.New("payment failed, please try again later or report the problem") + } + user.VoucherBalance = 0 + } + + case balanceAndCard: + if user.Balance >= invoiceTotal { + paymentDetails = models.PaymentDetails{Balance: invoiceTotal} + user.Balance -= invoiceTotal + } else { + _, err := createPaymentIntent(user.StripeCustomerID, cardPaymentID, a.config.Currency, invoiceTotal-user.Balance) + if err != nil { + log.Error().Err(err).Send() + return paymentDetails, errors.New("payment failed, please try again later or report the problem") + } + paymentDetails = models.PaymentDetails{Balance: user.Balance, Card: (invoiceTotal - user.Balance)} + user.Balance = 0 + } + + case voucherAndBalanceAndCard: + if user.VoucherBalance >= invoiceTotal { + paymentDetails = models.PaymentDetails{Balance: invoiceTotal} + user.VoucherBalance -= invoiceTotal + + } else if user.Balance+user.VoucherBalance >= invoiceTotal { + paymentDetails = models.PaymentDetails{VoucherBalance: user.VoucherBalance, Balance: (invoiceTotal - user.VoucherBalance)} + user.Balance = (invoiceTotal - user.VoucherBalance) + user.VoucherBalance = 0 + + } else { + _, err := createPaymentIntent(user.StripeCustomerID, cardPaymentID, a.config.Currency, invoiceTotal-user.VoucherBalance-user.Balance) + if err != nil { + log.Error().Err(err).Send() + return paymentDetails, errors.New("payment failed, please try again later or report the problem") + } + + paymentDetails = models.PaymentDetails{ + Balance: user.Balance, VoucherBalance: user.VoucherBalance, + Card: (invoiceTotal - user.Balance - user.VoucherBalance), + } + user.VoucherBalance = 0 + user.Balance = 0 + } + + default: + return paymentDetails, fmt.Errorf("invalid payment method, only methods allowed %v", methods) + } + + return paymentDetails, nil +} + +func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, invoiceTotal float64, invoiceID int) Response { + paymentDetails, err := a.pay(user, cardPaymentID, method, invoiceTotal) + if err != nil { + return BadRequest(errors.New(internalServerErrorMsg)) + } + + // invoice used voucher balance + if paymentDetails.VoucherBalance != 0 { + if err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + // invoice used balance + if paymentDetails.Balance != 0 { + if err = a.db.UpdateUserBalance(user.ID.String(), user.Balance); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + paymentDetails.InvoiceID = invoiceID + err = a.db.PayInvoice(invoiceID, paymentDetails) + if err == gorm.ErrRecordNotFound { + return NotFound(errors.New("invoice is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } + + return nil +} + // getCurrencyName returns the full name of the currency based on the currency code. func getCurrencyName(currencyCode string) (string, error) { currencyMap := map[string]string{ diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go index 2b59e5e4..70f59942 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -21,16 +21,16 @@ import ( // K8sDeployInput deploy k8s cluster input type K8sDeployInput struct { MasterName string `json:"master_name" validate:"min=3,max=20"` - MasterResources string `json:"resources"` - MasterPublic bool `json:"public"` - MasterRegion string `json:"region"` + MasterResources string `json:"resources" validate:"nonzero"` + MasterPublic bool `json:"public" validate:"nonzero"` + MasterRegion string `json:"region" validate:"nonzero"` Workers []WorkerInput `json:"workers"` } // WorkerInput deploy k8s worker input type WorkerInput struct { Name string `json:"name" validate:"min=3,max=20"` - Resources string `json:"resources"` + Resources string `json:"resources" validate:"nonzero"` } // K8sDeployHandler deploy k8s handler diff --git a/server/app/payments_handler.go b/server/app/payments_handler.go index 7c892012..829590a0 100644 --- a/server/app/payments_handler.go +++ b/server/app/payments_handler.go @@ -17,17 +17,17 @@ import ( ) type AddCardInput struct { - PaymentMethodID string `json:"payment_method_id" binding:"required"` - CardType string `json:"card_type" binding:"required"` + TokenID string `json:"token_id" binding:"required" validate:"nonzero"` + TokenType string `json:"token_type" binding:"required" validate:"nonzero"` } type SetDefaultCardInput struct { - PaymentMethodID string `json:"payment_method_id" binding:"required"` + PaymentMethodID string `json:"payment_method_id" binding:"required" validate:"nonzero"` } type ChargeBalance struct { - PaymentMethodID string `json:"payment_method_id" binding:"required"` - Amount float64 `json:"amount" binding:"required"` + PaymentMethodID string `json:"payment_method_id" binding:"required" validate:"nonzero"` + Amount float64 `json:"amount" binding:"required" validate:"nonzero"` } // Example endpoint: Add a new card @@ -88,7 +88,7 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { } } - paymentMethod, err := createPaymentMethod(input.CardType, input.PaymentMethodID) + paymentMethod, err := createPaymentMethod(input.TokenType, input.TokenID) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -101,7 +101,6 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { } if !unique { - log.Error().Err(err).Send() return nil, BadRequest(errors.New("card is added before")) } @@ -117,7 +116,7 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { UserID: userID, PaymentMethodID: paymentMethod.ID, CustomerID: user.StripeCustomerID, - CardType: input.CardType, + CardType: input.TokenType, ExpMonth: paymentMethod.Card.ExpMonth, ExpYear: paymentMethod.Card.ExpYear, Last4: paymentMethod.Card.Last4, @@ -132,7 +131,7 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { // if no payment is added before then we update the user payment ID with it as a default if len(strings.TrimSpace(user.StripeDefaultPaymentID)) == 0 { // Update the default payment method for future payments - err = updateDefaultPaymentMethod(user.StripeCustomerID, input.PaymentMethodID) + err = updateDefaultPaymentMethod(user.StripeCustomerID, paymentMethod.ID) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -149,9 +148,18 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { } } - // settle old invoices using the card - if err = a.payUserInvoicesUsingCards(user.ID.String(), user.StripeCustomerID, paymentMethod.ID, false); err != nil { + // try to settle old invoices using the card + invoices, err := a.db.ListUnpaidInvoices(user.ID.String()) + if err != nil { log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + for _, invoice := range invoices { + response := a.payInvoice(&user, paymentMethod.ID, voucherAndBalanceAndCard, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } } return ResponseMsg{ @@ -278,6 +286,15 @@ func (a *App) ListCardHandler(req *http.Request) (interface{}, Response) { func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + user, err := a.db.GetUserByID(userID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + id, err := strconv.Atoi(mux.Vars(req)["id"]) if err != nil { log.Error().Err(err).Send() @@ -315,13 +332,13 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { var vms []models.VM var k8s []models.K8sCluster if len(cards) == 1 { - vms, err = a.db.GetAllVms(userID) + vms, err = a.db.GetAllSuccessfulVms(userID) if err != nil && err != gorm.ErrRecordNotFound { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - k8s, err = a.db.GetAllK8s(userID) + k8s, err = a.db.GetAllSuccessfulK8s(userID) if err != nil && err != gorm.ErrRecordNotFound { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -332,6 +349,35 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { return nil, BadRequest(errors.New("you have active deployment and cannot delete the card")) } + // Update the default payment method for future payments (if deleted card is the default) + if card.PaymentMethodID == user.StripeDefaultPaymentID { + var newPaymentMethod string + // no more cards + if len(cards) == 1 { + newPaymentMethod = "" + } + + for _, c := range cards { + if c.PaymentMethodID != user.StripeDefaultPaymentID { + newPaymentMethod = c.PaymentMethodID + if err = updateDefaultPaymentMethod(card.CustomerID, c.PaymentMethodID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + break + } + } + + err = a.db.UpdateUserPaymentMethod(userID, newPaymentMethod) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + // If user has another cards or no active deployments, so can delete err = detachPaymentMethod(card.PaymentMethodID) if err != nil { diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 4030488c..5f32a936 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -32,19 +32,19 @@ type SignUpInput struct { // VerifyCodeInput struct takes verification code from user type VerifyCodeInput struct { - Email string `json:"email" binding:"required"` - Code int `json:"code" binding:"required"` + Email string `json:"email" binding:"required" validate:"mail"` + Code int `json:"code" binding:"required" validate:"nonzero"` } // SignInInput struct for data needed when user sign in type SignInInput struct { - Email string `json:"email" binding:"required"` - Password string `json:"password" binding:"required"` + Email string `json:"email" binding:"required" validate:"mail"` + Password string `json:"password" binding:"required" validate:"password"` } // ChangePasswordInput struct for user to change password type ChangePasswordInput struct { - Email string `json:"email" binding:"required"` + Email string `json:"email" binding:"required" validate:"mail"` Password string `json:"password" binding:"required" validate:"password"` ConfirmPassword string `json:"confirm_password" binding:"required" validate:"password"` } @@ -60,7 +60,7 @@ type UpdateUserInput struct { // EmailInput struct for user when forgetting password type EmailInput struct { - Email string `json:"email" binding:"required"` + Email string `json:"email" binding:"required" validate:"mail"` } // ApplyForVoucherInput struct for user to apply for voucher @@ -71,20 +71,26 @@ type ApplyForVoucherInput struct { // AddVoucherInput struct for voucher applied by user type AddVoucherInput struct { - Voucher string `json:"voucher" binding:"required"` + Voucher string `json:"voucher" binding:"required" validate:"nonzero"` } type CodeTimeout struct { - Timeout int `json:"timeout" binding:"required"` + Timeout int `json:"timeout" binding:"required" validate:"nonzero"` +} + +type AccessTokenResponse struct { + Token string `json:"access_token"` + Timeout int `json:"timeout"` } -type AccessToken struct { - Token string `json:"access_token" binding:"required"` +type RefreshTokenResponse struct { + Access string `json:"access_token"` + Refresh string `json:"refresh_token"` + Timeout int `json:"timeout"` } -type RefreshToken struct { - Access string `json:"access_token" binding:"required"` - Refresh string `json:"refresh_token" binding:"required"` +type clientSecretResponse struct { + ClientSecret string `json:"client_secret"` } // SignUpHandler creates account for user @@ -258,7 +264,7 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) // @Accept json // @Produce json // @Param login body SignInInput true "User login input" -// @Success 201 {object} AccessToken +// @Success 201 {object} AccessTokenResponse // @Failure 400 {object} Response // @Failure 401 {object} Response // @Failure 404 {object} Response @@ -298,7 +304,7 @@ func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "You are signed in successfully", - Data: AccessToken{Token: token}, + Data: AccessTokenResponse{Token: token, Timeout: a.config.Token.Timeout}, }, Created() } @@ -310,7 +316,7 @@ func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { // @Accept json // @Produce json // @Security BearerAuth -// @Success 201 {object} RefreshToken +// @Success 201 {object} RefreshTokenResponse // @Failure 400 {object} Response // @Failure 401 {object} Response // @Failure 404 {object} Response @@ -357,7 +363,7 @@ func (a *App) RefreshJWTHandler(req *http.Request) (interface{}, Response) { return ResponseMsg{ Message: "Token is refreshed successfully", - Data: RefreshToken{Access: reqToken, Refresh: newToken}, + Data: RefreshTokenResponse{Access: reqToken, Refresh: newToken, Timeout: a.config.Token.Timeout}, }, Created() } @@ -368,6 +374,7 @@ func (a *App) RefreshJWTHandler(req *http.Request) (interface{}, Response) { // @Tags User // @Accept json // @Produce json +// @Param forgetPassword body EmailInput true "User forget password input" // @Success 201 {object} CodeTimeout // @Failure 400 {object} Response // @Failure 401 {object} Response @@ -430,14 +437,15 @@ func (a *App) ForgotPasswordHandler(req *http.Request) (interface{}, Response) { // @Tags User // @Accept json // @Produce json -// @Success 201 {object} AccessToken +// @Param forgetPassword body VerifyCodeInput true "User Verify forget password input" +// @Success 201 {object} AccessTokenResponse // @Failure 400 {object} Response // @Failure 401 {object} Response // @Failure 404 {object} Response // @Failure 500 {object} Response -// @Router /user/forgot_password/verify_email [post] +// @Router /user/forget_password/verify_email [post] func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, Response) { - data := VerifyCodeInput{} + var data VerifyCodeInput err := json.NewDecoder(req.Body).Decode(&data) if err != nil { log.Error().Err(err).Send() @@ -474,7 +482,7 @@ func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, R return ResponseMsg{ Message: "Code is verified", - Data: AccessToken{Token: token}, + Data: AccessTokenResponse{Token: token, Timeout: a.config.Token.Timeout}, }, Ok() } @@ -783,16 +791,27 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) user.VoucherBalance += float64(voucherBalance.Balance) - user.Balance, user.VoucherBalance, err = a.db.PayUserInvoices(userID, user.Balance, user.VoucherBalance) + // try to settle old invoices + invoices, err := a.db.ListUnpaidInvoices(user.ID.String()) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserByID(user) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user is not found")) + for _, invoice := range invoices { + response := a.payInvoice(&user, "", voucherAndBalance, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } + } + + err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + err = a.db.UpdateUserBalance(user.ID.String(), user.Balance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -821,6 +840,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) // @Failure 500 {object} Response // @Router /user/charge_balance [put] func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) var input ChargeBalance @@ -845,7 +865,7 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = createPaymentIntent(user.StripeCustomerID, input.PaymentMethodID, a.config.Currency, input.Amount) + intent, err := createPaymentIntent(user.StripeCustomerID, input.PaymentMethodID, a.config.Currency, input.Amount) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -853,13 +873,185 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { user.Balance += float64(input.Amount) - user.Balance, user.VoucherBalance, err = a.db.PayUserInvoices(userID, user.Balance, user.VoucherBalance) + // try to settle old invoices + invoices, err := a.db.ListUnpaidInvoices(user.ID.String()) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + for _, invoice := range invoices { + response := a.payInvoice(&user, "", voucherAndBalance, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } + } + + err = a.db.UpdateUserBalance(user.ID.String(), user.Balance) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Balance is charged successfully", + Data: clientSecretResponse{ClientSecret: intent.ClientSecret}, + }, Ok() +} + +// DeleteUserHandler deletes account for user +// Example endpoint: Deletes account for user +// @Summary Deletes account for user +// @Description Deletes account for user +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user [delete] +func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + user, err := a.db.GetUserByID(userID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // 1. Create last invoice to pay if there were active deployments + if err := a.createInvoice(userID, time.Now()); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + invoices, err := a.db.ListUnpaidInvoices(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // 2. Try to pay invoices + for _, invoice := range invoices { + cards, err := a.db.GetUserCards(user.ID.String()) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // No cards option + if len(cards) == 0 { + response := a.payInvoice(&user, "", voucherAndBalance, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + continue + } + + // Use default card + response := a.payInvoice(&user, user.StripeDefaultPaymentID, voucherAndBalanceAndCard, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } else { + continue + } + + for _, card := range cards { + if card.PaymentMethodID == user.StripeDefaultPaymentID { + continue + } + + response := a.payInvoice(&user, card.PaymentMethodID, voucherAndBalanceAndCard, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + continue + } + break + } + + return nil, BadRequest(errors.New("failed to pay your invoices, please pay them first before deleting your account")) + } + + // 3. Delete user vms + vms, err := a.db.GetAllVms(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + for _, vm := range vms { + err = a.deployer.CancelDeployment(vm.ContractID, vm.NetworkContractID, "vm", vm.Name) + if err != nil && !strings.Contains(err.Error(), "ContractNotExists") { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + err = a.db.DeleteAllVms(userID) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserByID(user) + // 4. Delete user k8s + clusters, err := a.db.GetAllK8s(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + for _, cluster := range clusters { + err = a.deployer.CancelDeployment(uint64(cluster.ClusterContract), uint64(cluster.NetworkContract), "k8s", cluster.Master.Name) + if err != nil && !strings.Contains(err.Error(), "ContractNotExists") { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + if len(clusters) > 0 { + err = a.db.DeleteAllK8s(userID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + // 5. Remove cards + cards, err := a.db.GetUserCards(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + for _, card := range cards { + err = detachPaymentMethod(card.PaymentMethodID) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + } + + err = a.db.DeleteAllCards(userID) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // 6. TODO: should invoices be deleted? + + // 7. Remove cards + err = a.db.DeleteUser(userID) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user is not found")) } @@ -869,8 +1061,6 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { } return ResponseMsg{ - Message: "Balance is charged successfully", - // Data: map[string]string{"client_secret": intent.ClientSecret}, - Data: nil, + Message: "User is deleted successfully", }, Ok() } diff --git a/server/app/user_handler_test.go b/server/app/user_handler_test.go index 027053a6..3e74685f 100644 --- a/server/app/user_handler_test.go +++ b/server/app/user_handler_test.go @@ -622,6 +622,7 @@ func TestChangePasswordHandler(t *testing.T) { t.Run("change password: user not found", func(t *testing.T) { body := []byte(`{ "password":"1234567", + "email":"notfound@gmail.com", "confirm_password":"1234567" }`) diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index 2e21af72..551293f0 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -23,7 +23,7 @@ import ( // DeployVMInput struct takes input of vm from user type DeployVMInput struct { Name string `json:"name" binding:"required" validate:"min=3,max=20"` - Resources string `json:"resources" binding:"required"` + Resources string `json:"resources" binding:"required" validate:"nonzero"` Public bool `json:"public"` Region string `json:"region"` } diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index dc67051f..ab65066c 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -18,12 +18,12 @@ import ( // GenerateVoucherInput struct for data needed when user generate vouchers type GenerateVoucherInput struct { Length int `json:"length" binding:"required" validate:"min=3,max=20"` - Balance uint64 `json:"balance" binding:"required"` + Balance uint64 `json:"balance" binding:"required" validate:"nonzero"` } // UpdateVoucherInput struct for data needed when user update voucher type UpdateVoucherInput struct { - Approved bool `json:"approved" binding:"required"` + Approved bool `json:"approved" binding:"required" validate:"nonzero"` } // GenerateVoucherHandler generates a voucher by admin @@ -254,7 +254,7 @@ func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Respons // @Failure 401 {object} Response // @Failure 404 {object} Response // @Failure 500 {object} Response -// @Router /voucher/reset [put] +// @Router /voucher/all/reset [put] func (a *App) ResetUsersVoucherBalanceHandler(req *http.Request) (interface{}, Response) { users, err := a.db.ListAllUsers() if err == gorm.ErrRecordNotFound || len(users) == 0 { @@ -264,8 +264,7 @@ func (a *App) ResetUsersVoucherBalanceHandler(req *http.Request) (interface{}, R } for _, user := range users { - user.VoucherBalance = 0 - err = a.db.UpdateUserByID(user) + err = a.db.UpdateUserVoucherBalance(user.ID.String(), 0) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) diff --git a/server/app/voucher_handler_test.go b/server/app/voucher_handler_test.go index a9763126..02ce4f4a 100644 --- a/server/app/voucher_handler_test.go +++ b/server/app/voucher_handler_test.go @@ -25,8 +25,7 @@ func TestGenerateVoucherHandler(t *testing.T) { voucherBody := []byte(`{ "length": 5, - "vms": 10, - "public_ips": 1 + "balance": 10 }`) t.Run("Generate voucher: success", func(t *testing.T) { @@ -49,8 +48,7 @@ func TestGenerateVoucherHandler(t *testing.T) { t.Run("Generate voucher: invalid data", func(t *testing.T) { body := []byte(`{ "length": 2, - "vms": 10, - "public_ips": 1 + "balance": 1 }`) req := authHandlerConfig{ diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index cd16b8e4..1410b3dd 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -274,23 +274,44 @@ func (d *Deployer) canDeploy(userID string, costPerMonth float64) error { // from the start of current month func (d *Deployer) calculateUserDebtInMonth(userID string) (float64, error) { var debt float64 - usagePercentageInMonth := UsagePercentageInMonth(time.Now()) + now := time.Now() + monthStart := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local) - vms, err := d.db.GetAllVms(userID) + vms, err := d.db.GetAllSuccessfulVms(userID) if err != nil { return 0, err } for _, vm := range vms { + usageStart := monthStart + if vm.CreatedAt.After(monthStart) { + usageStart = vm.CreatedAt + } + + usagePercentageInMonth, err := UsagePercentageInMonth(usageStart, now) + if err != nil { + return 0, err + } + debt += float64(vm.PricePerMonth) * usagePercentageInMonth } - clusters, err := d.db.GetAllK8s(userID) + clusters, err := d.db.GetAllSuccessfulK8s(userID) if err != nil { return 0, err } for _, c := range clusters { + usageStart := monthStart + if c.CreatedAt.After(monthStart) { + usageStart = c.CreatedAt + } + + usagePercentageInMonth, err := UsagePercentageInMonth(usageStart, now) + if err != nil { + return 0, err + } + debt += float64(c.PricePerMonth) * usagePercentageInMonth } @@ -299,8 +320,16 @@ func (d *Deployer) calculateUserDebtInMonth(userID string) (float64, error) { // UsagePercentageInMonth calculates percentage of hours till specific time during the month // according to total hours of the same month -func UsagePercentageInMonth(end time.Time) float64 { - start := time.Date(end.Year(), end.Month(), 0, 0, 0, 0, 0, time.UTC) - endMonth := time.Date(end.Year(), end.Month()+1, 0, 0, 0, 0, 0, time.UTC) - return end.Sub(start).Hours() / endMonth.Sub(start).Hours() +func UsagePercentageInMonth(start time.Time, end time.Time) (float64, error) { + if start.Month() != end.Month() || start.Year() != end.Year() { + return 0, errors.New("start and end time should be the same month and year") + } + + startMonth := time.Date(start.Year(), start.Month(), 0, 0, 0, 0, 0, time.UTC) + endMonth := time.Date(start.Year(), start.Month()+1, 0, 0, 0, 0, 0, time.UTC) + + totalHoursInMonth := endMonth.Sub(startMonth).Hours() + usedHours := end.Sub(start).Hours() + + return usedHours / totalHoursInMonth, nil } diff --git a/server/docs/docs.go b/server/docs/docs.go index 9f75e000..f497c792 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -30,7 +30,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Creates a new administrator email and sends it to a specific user as an email and notification", + "description": "Creates a new administrator announcement and sends it to all users as an email and notification", "consumes": [ "application/json" ], @@ -40,15 +40,15 @@ const docTemplate = `{ "tags": [ "Admin" ], - "summary": "Creates a new administrator email and sends it to a specific user as an email and notification", + "summary": "Creates a new administrator announcement and sends it to all users as an email and notification", "parameters": [ { - "description": "email to be sent", - "name": "email", + "description": "announcement to be created", + "name": "announcement", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/app.EmailUser" + "$ref": "#/definitions/app.AdminAnnouncement" } } ], @@ -244,6 +244,59 @@ const docTemplate = `{ } } }, + "/email": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new administrator email and sends it to a specific user as an email and notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Creates a new administrator email and sends it to a specific user as an email and notification", + "parameters": [ + { + "description": "email to be sent", + "name": "email", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.EmailUser" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/invoice": { "get": { "security": [ @@ -1234,6 +1287,42 @@ const docTemplate = `{ "schema": {} } } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes account for user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Deletes account for user", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } } }, "/user/activate_voucher": { @@ -1697,9 +1786,9 @@ const docTemplate = `{ } } }, - "/user/forgot_password": { + "/user/forget_password/verify_email": { "post": { - "description": "Send code to forget password email for verification", + "description": "Verify user's email to reset password", "consumes": [ "application/json" ], @@ -1709,12 +1798,23 @@ const docTemplate = `{ "tags": [ "User" ], - "summary": "Send code to forget password email for verification", + "summary": "Verify user's email to reset password", + "parameters": [ + { + "description": "User Verify forget password input", + "name": "forgetPassword", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.VerifyCodeInput" + } + } + ], "responses": { "201": { "description": "Created", "schema": { - "$ref": "#/definitions/app.CodeTimeout" + "$ref": "#/definitions/app.AccessTokenResponse" } }, "400": { @@ -1736,9 +1836,9 @@ const docTemplate = `{ } } }, - "/user/forgot_password/verify_email": { + "/user/forgot_password": { "post": { - "description": "Verify user's email to reset password", + "description": "Send code to forget password email for verification", "consumes": [ "application/json" ], @@ -1748,12 +1848,23 @@ const docTemplate = `{ "tags": [ "User" ], - "summary": "Verify user's email to reset password", + "summary": "Send code to forget password email for verification", + "parameters": [ + { + "description": "User forget password input", + "name": "forgetPassword", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.EmailInput" + } + } + ], "responses": { "201": { "description": "Created", "schema": { - "$ref": "#/definitions/app.AccessToken" + "$ref": "#/definitions/app.CodeTimeout" } }, "400": { @@ -1797,7 +1908,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/app.RefreshToken" + "$ref": "#/definitions/app.RefreshTokenResponse" } }, "400": { @@ -1847,7 +1958,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/app.AccessToken" + "$ref": "#/definitions/app.AccessTokenResponse" } }, "400": { @@ -2380,7 +2491,7 @@ const docTemplate = `{ } } }, - "/voucher/reset": { + "/voucher/all/reset": { "put": { "security": [ { @@ -2486,28 +2597,28 @@ const docTemplate = `{ } }, "definitions": { - "app.AccessToken": { + "app.AccessTokenResponse": { "type": "object", - "required": [ - "access_token" - ], "properties": { "access_token": { "type": "string" + }, + "timeout": { + "type": "integer" } } }, "app.AddCardInput": { "type": "object", "required": [ - "card_type", - "payment_method_id" + "token_id", + "token_type" ], "properties": { - "card_type": { + "token_id": { "type": "string" }, - "payment_method_id": { + "token_type": { "type": "string" } } @@ -2622,6 +2733,17 @@ const docTemplate = `{ } } }, + "app.EmailInput": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, "app.EmailUser": { "type": "object", "required": [ @@ -2714,18 +2836,17 @@ const docTemplate = `{ } } }, - "app.RefreshToken": { + "app.RefreshTokenResponse": { "type": "object", - "required": [ - "access_token", - "refresh_token" - ], "properties": { "access_token": { "type": "string" }, "refresh_token": { "type": "string" + }, + "timeout": { + "type": "integer" } } }, @@ -3190,6 +3311,9 @@ const docTemplate = `{ "card": { "type": "number" }, + "id": { + "type": "integer" + }, "invoice_id": { "type": "integer" }, @@ -3241,7 +3365,7 @@ const docTemplate = `{ "stripe_customer_id": { "type": "string" }, - "stripe_payment_method_id": { + "stripe_default_payment_id": { "type": "string" }, "updated_at": { diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index 047a5138..ba534271 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1,20 +1,20 @@ definitions: - app.AccessToken: + app.AccessTokenResponse: properties: access_token: type: string - required: - - access_token + timeout: + type: integer type: object app.AddCardInput: properties: - card_type: + token_id: type: string - payment_method_id: + token_type: type: string required: - - card_type - - payment_method_id + - token_id + - token_type type: object app.AddVoucherInput: properties: @@ -90,6 +90,13 @@ definitions: - name - resources type: object + app.EmailInput: + properties: + email: + type: string + required: + - email + type: object app.EmailUser: properties: body: @@ -152,15 +159,14 @@ definitions: required: - method type: object - app.RefreshToken: + app.RefreshTokenResponse: properties: access_token: type: string refresh_token: type: string - required: - - access_token - - refresh_token + timeout: + type: integer type: object app.SetAdminInput: properties: @@ -474,6 +480,8 @@ definitions: type: number card: type: number + id: + type: integer invoice_id: type: integer voucher_balance: @@ -504,7 +512,7 @@ definitions: type: string stripe_customer_id: type: string - stripe_payment_method_id: + stripe_default_payment_id: type: string updated_at: type: string @@ -632,15 +640,15 @@ paths: post: consumes: - application/json - description: Creates a new administrator email and sends it to a specific user + description: Creates a new administrator announcement and sends it to all users as an email and notification parameters: - - description: email to be sent + - description: announcement to be created in: body - name: email + name: announcement required: true schema: - $ref: '#/definitions/app.EmailUser' + $ref: '#/definitions/app.AdminAnnouncement' produces: - application/json responses: @@ -661,8 +669,8 @@ paths: schema: {} security: - BearerAuth: [] - summary: Creates a new administrator email and sends it to a specific user as - an email and notification + summary: Creates a new administrator announcement and sends it to all users + as an email and notification tags: - Admin /balance: @@ -776,6 +784,43 @@ paths: summary: Get users' deployments count tags: - Admin + /email: + post: + consumes: + - application/json + description: Creates a new administrator email and sends it to a specific user + as an email and notification + parameters: + - description: email to be sent + in: body + name: email + required: true + schema: + $ref: '#/definitions/app.EmailUser' + produces: + - application/json + responses: + "201": + description: Created + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Creates a new administrator email and sends it to a specific user as + an email and notification + tags: + - Admin /invoice: get: consumes: @@ -1372,6 +1417,30 @@ paths: tags: - Admin /user: + delete: + consumes: + - application/json + description: Deletes account for user + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Deletes account for user + tags: + - User get: consumes: - application/json @@ -1739,18 +1808,25 @@ paths: summary: Charge user balance tags: - User - /user/forgot_password: + /user/forget_password/verify_email: post: consumes: - application/json - description: Send code to forget password email for verification + description: Verify user's email to reset password + parameters: + - description: User Verify forget password input + in: body + name: forgetPassword + required: true + schema: + $ref: '#/definitions/app.VerifyCodeInput' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/app.CodeTimeout' + $ref: '#/definitions/app.AccessTokenResponse' "400": description: Bad Request schema: {} @@ -1763,21 +1839,28 @@ paths: "500": description: Internal Server Error schema: {} - summary: Send code to forget password email for verification + summary: Verify user's email to reset password tags: - User - /user/forgot_password/verify_email: + /user/forgot_password: post: consumes: - application/json - description: Verify user's email to reset password + description: Send code to forget password email for verification + parameters: + - description: User forget password input + in: body + name: forgetPassword + required: true + schema: + $ref: '#/definitions/app.EmailInput' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/app.AccessToken' + $ref: '#/definitions/app.CodeTimeout' "400": description: Bad Request schema: {} @@ -1790,7 +1873,7 @@ paths: "500": description: Internal Server Error schema: {} - summary: Verify user's email to reset password + summary: Send code to forget password email for verification tags: - User /user/refresh_token: @@ -1804,7 +1887,7 @@ paths: "201": description: Created schema: - $ref: '#/definitions/app.RefreshToken' + $ref: '#/definitions/app.RefreshTokenResponse' "400": description: Bad Request schema: {} @@ -1840,7 +1923,7 @@ paths: "201": description: Created schema: - $ref: '#/definitions/app.AccessToken' + $ref: '#/definitions/app.AccessTokenResponse' "400": description: Bad Request schema: {} @@ -2237,7 +2320,7 @@ paths: summary: Update (approve-reject) a voucher tags: - Voucher (only admins) - /voucher/reset: + /voucher/all/reset: put: consumes: - application/json diff --git a/server/go.mod b/server/go.mod index 29d73c46..b8ad0fa7 100644 --- a/server/go.mod +++ b/server/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.4 require ( github.com/caitlin615/nist-password-validator v0.0.0-20190321104149-45ab5d3140de + github.com/cenkalti/backoff v2.2.1+incompatible github.com/go-redis/redis v6.15.9+incompatible github.com/golang-jwt/jwt/v4 v4.5.1 github.com/google/uuid v1.6.0 @@ -32,7 +33,6 @@ require ( github.com/ChainSafe/go-schnorrkel v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.12 // indirect @@ -70,13 +70,13 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/gomega v1.33.1 // indirect + github.com/onsi/gomega v1.34.2 // indirect github.com/pierrec/xxHash v0.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/cors v1.10.1 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -88,7 +88,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/vedhavyas/go-subkey v1.0.3 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.32.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect diff --git a/server/go.sum b/server/go.sum index 43c9a991..fb46f78b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -135,8 +135,8 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -152,8 +152,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -211,8 +211,8 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= diff --git a/server/internal/config_parser_test.go b/server/internal/config_parser_test.go index 41e6d67a..b640280e 100644 --- a/server/internal/config_parser_test.go +++ b/server/internal/config_parser_test.go @@ -35,7 +35,14 @@ var rightConfig = ` "file": "testing.db" }, "version": "v1", - "salt": "salt" + "currency": "eur", + "prices": { + "public_ip": 2, + "small_vm": 10, + "medium_vm": 20, + "large_vm": 30 + }, + "stripe_secret": "sk_test" } ` @@ -331,7 +338,82 @@ func TestParseConf(t *testing.T) { }) - t.Run("no salt configuration", func(t *testing.T) { + t.Run("no currency configuration", func(t *testing.T) { + config := + ` +{ + "server": { + "host": "localhost", + "port": ":3000" + }, + "mailSender": { + "email": "email", + "sendgrid_key": "my sendgrid_key", + "timeout": 60 + }, + "account": { + "mnemonics": "my mnemonics", + "network": "my network" + }, + "database": { + "file": "testing.db" + }, + "token": { + "secret": "secret", + "timeout": 10 + }, + "version": "v1", +} + ` + dir := t.TempDir() + configPath := filepath.Join(dir, "/config.json") + + err := os.WriteFile(configPath, []byte(config), 0644) + assert.NoError(t, err) + + _, err = ReadConfFile(configPath) + assert.Error(t, err, "currency is required") + }) + + t.Run("no prices configuration", func(t *testing.T) { + config := + ` +{ + "server": { + "host": "localhost", + "port": ":3000" + }, + "mailSender": { + "email": "email", + "sendgrid_key": "my sendgrid_key", + "timeout": 60 + }, + "account": { + "mnemonics": "my mnemonics", + "network": "my network" + }, + "database": { + "file": "testing.db" + }, + "token": { + "secret": "secret", + "timeout": 10 + }, + "version": "v1", + "currency": "eur", +} + ` + dir := t.TempDir() + configPath := filepath.Join(dir, "/config.json") + + err := os.WriteFile(configPath, []byte(config), 0644) + assert.NoError(t, err) + + _, err = ReadConfFile(configPath) + assert.Error(t, err, "prices is required") + }) + + t.Run("no stripe secret configuration", func(t *testing.T) { config := ` { @@ -356,7 +438,13 @@ func TestParseConf(t *testing.T) { "timeout": 10 }, "version": "v1", - "salt": "" + "currency": "eur", + "prices": { + "public_ip": 2, + "small_vm": 10, + "medium_vm": 20, + "large_vm": 30 + }, } ` dir := t.TempDir() @@ -366,7 +454,7 @@ func TestParseConf(t *testing.T) { assert.NoError(t, err) _, err = ReadConfFile(configPath) - assert.Error(t, err, "salt is required") + assert.Error(t, err, "stripe_secret is required") }) } diff --git a/server/internal/email_sender.go b/server/internal/email_sender.go index f48da153..b8ec2c08 100644 --- a/server/internal/email_sender.go +++ b/server/internal/email_sender.go @@ -41,14 +41,14 @@ var ( // SendMail sends verification mails func SendMail(sender, sendGridKey, receiver, subject, body string) error { - from := mail.NewEmail("Cloud4Students", sender) + from := mail.NewEmail("Cloud4All", sender) err := validators.ValidMail(receiver) if err != nil { return fmt.Errorf("email %v is not valid", receiver) } - to := mail.NewEmail("Cloud4Students User", receiver) + to := mail.NewEmail("Cloud4All User", receiver) message := mail.NewSingleEmail(from, subject, to, "", body) client := sendgrid.NewSendClient(sendGridKey) @@ -59,7 +59,7 @@ func SendMail(sender, sendGridKey, receiver, subject, body string) error { // SignUpMailContent gets the email content for sign up func SignUpMailContent(code int, timeout int, username, host string) (string, string) { - subject := "Welcome to Cloud4Students 🎉" + subject := "Welcome to Cloud4All 🎉" body := string(signUpMail) body = strings.ReplaceAll(body, "-code-", fmt.Sprint(code)) @@ -72,7 +72,7 @@ func SignUpMailContent(code int, timeout int, username, host string) (string, st // WelcomeMailContent gets the email content for welcome messages func WelcomeMailContent(username, host string) (string, string) { - subject := "Welcome to Cloud4Students 🎉" + subject := "Welcome to Cloud4All 🎉" body := string(welcomeMail) body = strings.ReplaceAll(body, "-name-", cases.Title(language.Und).String(username)) diff --git a/server/internal/email_sender_test.go b/server/internal/email_sender_test.go index 9153fc4c..8d424d64 100644 --- a/server/internal/email_sender_test.go +++ b/server/internal/email_sender_test.go @@ -24,7 +24,7 @@ func TestSendMail(t *testing.T) { func TestSignUpMailContent(t *testing.T) { subject, body := SignUpMailContent(1234, 60, "user", "") - assert.Equal(t, subject, "Welcome to Cloud4Students 🎉") + assert.Equal(t, subject, "Welcome to Cloud4All 🎉") want := string(signUpMail) want = strings.ReplaceAll(want, "-code-", fmt.Sprint(1234)) diff --git a/server/internal/templates/adminAnnouncement.html b/server/internal/templates/adminAnnouncement.html index 6c98741a..a01b7f1b 100644 --- a/server/internal/templates/adminAnnouncement.html +++ b/server/internal/templates/adminAnnouncement.html @@ -266,7 +266,7 @@ " >

- You received this email because you are a cloud4students user. + You received this email because you are a cloud4all user.

-host- diff --git a/server/internal/templates/signup.html b/server/internal/templates/signup.html index da52f479..1dd1db70 100644 --- a/server/internal/templates/signup.html +++ b/server/internal/templates/signup.html @@ -198,7 +198,7 @@ Welcome, -name-!

- Thank you for signing up with cloud4students. We are so glad + Thank you for signing up with cloud4all. We are so glad to have you here. We strive to produce efficient virtual machines and kubernetes clusters that you can use for your cloud or deployment needs. diff --git a/server/models/card.go b/server/models/card.go index ba34208d..1521e57d 100644 --- a/server/models/card.go +++ b/server/models/card.go @@ -1,6 +1,9 @@ package models -import "gorm.io/gorm" +import ( + "gorm.io/gorm" + "gorm.io/gorm/clause" +) type Card struct { ID int `json:"id" gorm:"primaryKey"` @@ -55,3 +58,9 @@ func (d *DB) DeleteCard(id int) error { var card Card return d.db.Delete(&card, id).Error } + +// DeleteAllCards deletes all cards of user +func (d *DB) DeleteAllCards(userID string) error { + var cards []Card + return d.db.Clauses(clause.Returning{}).Where("user_id = ?", userID).Delete(&cards).Error +} diff --git a/server/models/database.go b/server/models/database.go index ab8ba3fa..c0b0fc4e 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -31,7 +31,7 @@ func (d *DB) Connect(file string) error { func (d *DB) Migrate() error { err := d.db.AutoMigrate( &User{}, &State{}, &Card{}, &Invoice{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, - &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}, + &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}, &DeploymentItem{}, &PaymentDetails{}, ) if err != nil { return err diff --git a/server/models/invoice.go b/server/models/invoice.go index f1549b1b..1ccfb7df 100644 --- a/server/models/invoice.go +++ b/server/models/invoice.go @@ -32,6 +32,7 @@ type DeploymentItem struct { } type PaymentDetails struct { + ID int `json:"id" gorm:"primaryKey"` InvoiceID int `json:"invoice_id"` Card float64 `json:"card"` Balance float64 `json:"balance"` @@ -74,10 +75,13 @@ func (d *DB) UpdateInvoiceLastRemainderDate(id int) error { // PayInvoice updates paid with true and paid at field with current time in the invoice func (d *DB) PayInvoice(id int, payment PaymentDetails) error { var invoice Invoice + if err := d.db.Model(&invoice).Association("PaymentDetails").Append(&payment); err != nil { + return err + } + result := d.db.Model(&invoice). Where("id = ?", id). Update("paid", true). - Update("payment_details", payment). Update("paid_at", time.Now()) if result.RowsAffected == 0 { @@ -86,59 +90,6 @@ func (d *DB) PayInvoice(id int, payment PaymentDetails) error { return result.Error } -// PayUserInvoices tries to pay invoices with a given balance -func (d *DB) PayUserInvoices(userID string, balance, voucherBalance float64) (float64, float64, error) { - // get unpaid invoices - var invoices []Invoice - if err := d.db. - Order("total desc"). - Where("user_id = ?", userID). - Where("paid = ?", false). - Find(&invoices).Error; err != nil && err != gorm.ErrRecordNotFound { - return 0, 0, err - } - - for _, invoice := range invoices { - if balance == 0 && voucherBalance == 0 { - break - } - - // 1. check voucher balance - if invoice.Total <= voucherBalance { - if err := d.PayInvoice(invoice.ID, PaymentDetails{VoucherBalance: invoice.Total}); err != nil { - return 0, 0, err - } - voucherBalance -= invoice.Total - continue - } - - // 2. check balance - if invoice.Total <= balance { - if err := d.PayInvoice(invoice.ID, PaymentDetails{Balance: invoice.Total}); err != nil { - return 0, 0, err - } - balance -= invoice.Total - continue - } - - // 3. check both (total is more than both balance and voucher balance) - if invoice.Total <= balance+voucherBalance { - if err := d.PayInvoice( - invoice.ID, - PaymentDetails{VoucherBalance: voucherBalance, Balance: (invoice.Total - voucherBalance)}, - ); err != nil { - return 0, 0, err - } - - // use voucher first - balance -= (invoice.Total - voucherBalance) - voucherBalance = 0 - } - } - - return balance, voucherBalance, nil -} - // CalcUserDebt calculates the user debt according to invoices func (d *DB) CalcUserDebt(userID string) (float64, error) { var debt float64 diff --git a/server/models/k8s.go b/server/models/k8s.go index 0ffb58c1..3be657a2 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -101,6 +101,22 @@ func (d *DB) GetAllK8s(userID string) ([]K8sCluster, error) { return k8sClusters, nil } +// GetAllSuccessfulK8s returns all K8s of user that have a state succeeded +func (d *DB) GetAllSuccessfulK8s(userID string) ([]K8sCluster, error) { + var k8sClusters []K8sCluster + err := d.db.Find(&k8sClusters, "user_id = ? and state = 'CREATED'", userID).Error + if err != nil { + return nil, err + } + for i := range k8sClusters { + k8sClusters[i], err = d.GetK8s(k8sClusters[i].ID) + if err != nil { + return nil, err + } + } + return k8sClusters, nil +} + // DeleteK8s deletes a k8s cluster func (d *DB) DeleteK8s(id int) error { var k8s K8sCluster @@ -118,6 +134,7 @@ func (d *DB) DeleteAllK8s(userID string) error { if err != nil { return err } + return d.db.Select("Master", "Workers").Delete(&k8sClusters).Error } diff --git a/server/models/user.go b/server/models/user.go index 7915e0ea..35467134 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -13,7 +13,7 @@ import ( type User struct { ID uuid.UUID `gorm:"primary_key; unique; type:uuid; column:id"` StripeCustomerID string `json:"stripe_customer_id"` - StripeDefaultPaymentID string `json:"stripe_payment_method_id"` + StripeDefaultPaymentID string `json:"stripe_default_payment_id"` FirstName string `json:"first_name" binding:"required"` LastName string `json:"last_name" binding:"required"` Email string `json:"email" gorm:"unique" binding:"required"` @@ -105,9 +105,32 @@ func (d *DB) UpdateAdminUserByID(id string, admin bool) error { return d.db.Model(&User{}).Where("id = ?", id).Updates(map[string]interface{}{"admin": admin, "updated_at": time.Now()}).Error } +// UpdateUserPaymentMethod updates user payment method ID +func (d *DB) UpdateUserPaymentMethod(id string, paymentID string) error { + var res User + return d.db.Model(&res).Where("id = ?", id).Update("stripe_default_payment_id", paymentID).Error +} + +// UpdateUserBalance updates user balance +func (d *DB) UpdateUserBalance(id string, balance float64) error { + var res User + return d.db.Model(&res).Where("id = ?", id).Update("balance", balance).Error +} + +// UpdateUserVoucherBalance updates user voucher balance +func (d *DB) UpdateUserVoucherBalance(id string, balance float64) error { + var res User + return d.db.Model(&res).Where("id = ?", id).Update("voucher_balance", balance).Error +} + // UpdateUserVerification updates if user is verified or not func (d *DB) UpdateUserVerification(id string, verified bool) error { var res User - result := d.db.Model(&res).Where("id=?", id).Update("verified", verified) - return result.Error + return d.db.Model(&res).Where("id = ?", id).Update("verified", verified).Error +} + +// DeleteUser deletes user by its id +func (d *DB) DeleteUser(id string) error { + var user User + return d.db.Where("id = ?", id).Delete(&user).Error } diff --git a/server/models/vm.go b/server/models/vm.go index 69918584..c0384b9d 100644 --- a/server/models/vm.go +++ b/server/models/vm.go @@ -47,6 +47,12 @@ func (d *DB) GetAllVms(userID string) ([]VM, error) { return vms, d.db.Where("user_id = ?", userID).Find(&vms).Error } +// GetAllSuccessfulVms returns all vms of user that have a state succeeded +func (d *DB) GetAllSuccessfulVms(userID string) ([]VM, error) { + var vms []VM + return vms, d.db.Where("user_id = ? and state = 'CREATED'", userID).Find(&vms).Error +} + // UpdateVM updates information of vm. empty and unchanged fields are not updated. func (d *DB) UpdateVM(vm VM) error { return d.db.Model(&VM{}).Where("id = ?", vm.ID).Updates(vm).Error From dcfeca38a946f7cc0963ca32ca235f7d164fda43 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Mon, 10 Feb 2025 16:14:06 +0200 Subject: [PATCH 5/7] add vms and clusters per user while listing all --- server/app/admin_handler.go | 174 +++++++++++++++++----------- server/app/app.go | 3 +- server/docs/docs.go | 177 ++++++++++++++++++++++++----- server/docs/swagger.yaml | 120 +++++++++++++++---- server/models/deployments_count.go | 44 ++++++- server/models/k8s.go | 3 +- 6 files changed, 396 insertions(+), 125 deletions(-) diff --git a/server/app/admin_handler.go b/server/app/admin_handler.go index f0799139..ac94d145 100644 --- a/server/app/admin_handler.go +++ b/server/app/admin_handler.go @@ -6,11 +6,13 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "time" "github.com/codescalers/cloud4students/internal" "github.com/codescalers/cloud4students/models" + "github.com/gorilla/mux" "github.com/rs/zerolog/log" "gopkg.in/validator.v2" "gorm.io/gorm" @@ -53,9 +55,11 @@ type SetPricesInput struct { PublicIP float64 `json:"public_ip"` } -type ListDeploymentsResponse struct { - VMs []models.VM `json:"vms"` - K8S []models.K8sCluster `json:"k8s"` +type UserResponse struct { + *models.User + VMs []models.VM `json:"vms"` + K8S []models.K8sCluster `json:"k8s"` + Count models.DeploymentsCount `json:"count"` } // GetAllUsersHandler returns all users @@ -66,7 +70,7 @@ type ListDeploymentsResponse struct { // @Accept json // @Produce json // @Security BearerAuth -// @Success 200 {object} []models.User +// @Success 200 {object} []UserResponse // @Failure 400 {object} Response // @Failure 401 {object} Response // @Failure 404 {object} Response @@ -86,9 +90,40 @@ func (a *App) GetAllUsersHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + var allUsers []UserResponse + + for _, user := range users { + // vms + vms, err := a.db.GetAllVms(user.ID.String()) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // k8s clusters + clusters, err := a.db.GetAllK8s(user.ID.String()) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + count, err := a.db.CountUserDeployments(user.ID.String()) + if err != nil && err != gorm.ErrRecordNotFound { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + allUsers = append(allUsers, UserResponse{ + User: &user, + VMs: vms, + K8S: clusters, + Count: count, + }) + } + return ResponseMsg{ Message: "Users are found", - Data: users, + Data: allUsers, }, Ok() } @@ -235,6 +270,71 @@ func (a *App) GetBalanceHandler(req *http.Request) (interface{}, Response) { }, Ok() } +// DeleteVMDeploymentHandler deletes a virtual machine +// Example endpoint: Deletes a virtual machine +// @Summary Deletes a virtual machine +// @Description Deletes a virtual machine +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Virtual machine ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /deployments/vm/{id} [delete] +func (a *App) DeleteVMDeploymentHandler(req *http.Request) (interface{}, Response) { + id, err := strconv.Atoi(mux.Vars(req)["id"]) + if err != nil { + return nil, BadRequest(errors.New("failed to read deployment id")) + } + + if err = a.db.DeleteVMByID(id); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Virtual machine is deleted successfully", + }, Ok() +} + +// DeleteK8sDeploymentHandler deletes a kubernetes cluster +// Example endpoint: Deletes a kubernetes cluster +// @Summary Deletes a kubernetes cluster +// @Description Deletes a kubernetes cluster +// @Tags Admin +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Kubernetes cluster ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /deployments/k8s/{id} [delete] +func (a *App) DeleteK8sDeploymentHandler(req *http.Request) (interface{}, Response) { + id, err := strconv.Atoi(mux.Vars(req)["id"]) + if err != nil { + return nil, BadRequest(errors.New("failed to read deployment id")) + } + + if err = a.db.DeleteK8s(id); err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("kubernetes cluster is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Kubernetes cluster is deleted successfully", + }, Ok() +} + // DeleteAllDeploymentsHandler deletes all users' deployments // Example endpoint: Deletes all users' deployments // @Summary Deletes all users' deployments @@ -319,70 +419,6 @@ func (a *App) DeleteAllDeploymentsHandler(req *http.Request) (interface{}, Respo }, Ok() } -// ListDeploymentsHandler lists all users' deployments -// Example endpoint: List all users' deployments -// @Summary List all users' deployments -// @Description List all users' deployments -// @Tags Admin -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} ListDeploymentsResponse -// @Failure 400 {object} Response -// @Failure 401 {object} Response -// @Failure 404 {object} Response -// @Failure 500 {object} Response -// @Router /deployments [get] -func (a *App) ListDeploymentsHandler(req *http.Request) (interface{}, Response) { - users, err := a.db.ListAllUsers() - if err == gorm.ErrRecordNotFound || len(users) == 0 { - return ResponseMsg{ - Message: "Users are not found", - }, Ok() - } - - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - var allVMs []models.VM - var allClusters []models.K8sCluster - - for _, user := range users { - // vms - vms, err := a.db.GetAllVms(user.ID.String()) - if err == gorm.ErrRecordNotFound || len(vms) == 0 { - log.Error().Err(err).Str("userID", user.ID.String()).Msg("Virtual machines are not found") - continue - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - allVMs = append(allVMs, vms...) - - // k8s clusters - clusters, err := a.db.GetAllK8s(user.ID.String()) - if err == gorm.ErrRecordNotFound || len(clusters) == 0 { - log.Error().Err(err).Str("userID", user.ID.String()).Msg("Kubernetes clusters are not found") - continue - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - allClusters = append(allClusters, clusters...) - } - - return ResponseMsg{ - Message: "Deployments are listed successfully", - Data: ListDeploymentsResponse{VMs: allVMs, K8S: allClusters}, - }, Ok() -} - // UpdateMaintenanceHandler updates maintenance flag // Example endpoint: Updates maintenance flag // @Summary Updates maintenance flag diff --git a/server/app/app.go b/server/app/app.go index da38e776..2d455988 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -190,7 +190,8 @@ func (a *App) registerHandlers() { balanceRouter.HandleFunc("", WrapFunc(a.GetBalanceHandler)).Methods("GET", "OPTIONS") maintenanceRouter.HandleFunc("", WrapFunc(a.UpdateMaintenanceHandler)).Methods("PUT", "OPTIONS") deploymentsRouter.HandleFunc("", WrapFunc(a.DeleteAllDeploymentsHandler)).Methods("DELETE", "OPTIONS") - deploymentsRouter.HandleFunc("", WrapFunc(a.ListDeploymentsHandler)).Methods("GET", "OPTIONS") + deploymentsRouter.HandleFunc("/vm/{id}", WrapFunc(a.DeleteVMDeploymentHandler)).Methods("DELETE", "OPTIONS") + deploymentsRouter.HandleFunc("/k8s/{id}", WrapFunc(a.DeleteK8sDeploymentHandler)).Methods("DELETE", "OPTIONS") deploymentsRouter.HandleFunc("/count", WrapFunc(a.GetDlsCountHandler)).Methods("GET", "OPTIONS") nextLaunchRouter.HandleFunc("", WrapFunc(a.UpdateNextLaunchHandler)).Methods("PUT", "OPTIONS") diff --git a/server/docs/docs.go b/server/docs/docs.go index f497c792..67d8ea7f 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -117,13 +117,55 @@ const docTemplate = `{ } }, "/deployments": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes all users' deployments", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Deletes all users' deployments", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/deployments/count": { "get": { "security": [ { "BearerAuth": [] } ], - "description": "List all users' deployments", + "description": "Get users' deployments count in the system", "consumes": [ "application/json" ], @@ -133,12 +175,12 @@ const docTemplate = `{ "tags": [ "Admin" ], - "summary": "List all users' deployments", + "summary": "Get users' deployments count", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/app.ListDeploymentsResponse" + "$ref": "#/definitions/models.DeploymentsCount" } }, "400": { @@ -158,14 +200,16 @@ const docTemplate = `{ "schema": {} } } - }, + } + }, + "/deployments/k8s/{id}": { "delete": { "security": [ { "BearerAuth": [] } ], - "description": "Deletes all users' deployments", + "description": "Deletes a kubernetes cluster", "consumes": [ "application/json" ], @@ -175,7 +219,16 @@ const docTemplate = `{ "tags": [ "Admin" ], - "summary": "Deletes all users' deployments", + "summary": "Deletes a kubernetes cluster", + "parameters": [ + { + "type": "string", + "description": "Kubernetes cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -200,14 +253,14 @@ const docTemplate = `{ } } }, - "/deployments/count": { - "get": { + "/deployments/vm/{id}": { + "delete": { "security": [ { "BearerAuth": [] } ], - "description": "Get users' deployments count in the system", + "description": "Deletes a virtual machine", "consumes": [ "application/json" ], @@ -217,13 +270,20 @@ const docTemplate = `{ "tags": [ "Admin" ], - "summary": "Get users' deployments count", + "summary": "Deletes a virtual machine", + "parameters": [ + { + "type": "string", + "description": "Virtual machine ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", - "schema": { - "$ref": "#/definitions/models.DeploymentsCount" - } + "schema": {} }, "400": { "description": "Bad Request", @@ -1402,7 +1462,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.User" + "$ref": "#/definitions/app.UserResponse" } } }, @@ -2805,23 +2865,6 @@ const docTemplate = `{ } } }, - "app.ListDeploymentsResponse": { - "type": "object", - "properties": { - "k8s": { - "type": "array", - "items": { - "$ref": "#/definitions/models.K8sCluster" - } - }, - "vms": { - "type": "array", - "items": { - "$ref": "#/definitions/models.VM" - } - } - } - }, "app.PayInvoiceInput": { "type": "object", "required": [ @@ -2995,6 +3038,78 @@ const docTemplate = `{ } } }, + "app.UserResponse": { + "type": "object", + "required": [ + "email", + "first_name", + "hashed_password", + "last_name" + ], + "properties": { + "admin": { + "description": "checks if user type is admin", + "type": "boolean" + }, + "balance": { + "type": "number" + }, + "code": { + "type": "integer" + }, + "count": { + "$ref": "#/definitions/models.DeploymentsCount" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "hashed_password": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "string" + }, + "k8s": { + "type": "array", + "items": { + "$ref": "#/definitions/models.K8sCluster" + } + }, + "last_name": { + "type": "string" + }, + "ssh_key": { + "type": "string" + }, + "stripe_customer_id": { + "type": "string" + }, + "stripe_default_payment_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "verified": { + "type": "boolean" + }, + "vms": { + "type": "array", + "items": { + "$ref": "#/definitions/models.VM" + } + }, + "voucher_balance": { + "type": "number" + } + } + }, "app.VerifyCodeInput": { "type": "object", "required": [ diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index ba534271..c93889ae 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -139,17 +139,6 @@ definitions: $ref: '#/definitions/app.WorkerInput' type: array type: object - app.ListDeploymentsResponse: - properties: - k8s: - items: - $ref: '#/definitions/models.K8sCluster' - type: array - vms: - items: - $ref: '#/definitions/models.VM' - type: array - type: object app.PayInvoiceInput: properties: card_payment_id: @@ -265,6 +254,55 @@ definitions: required: - approved type: object + app.UserResponse: + properties: + admin: + description: checks if user type is admin + type: boolean + balance: + type: number + code: + type: integer + count: + $ref: '#/definitions/models.DeploymentsCount' + email: + type: string + first_name: + type: string + hashed_password: + items: + type: integer + type: array + id: + type: string + k8s: + items: + $ref: '#/definitions/models.K8sCluster' + type: array + last_name: + type: string + ssh_key: + type: string + stripe_customer_id: + type: string + stripe_default_payment_id: + type: string + updated_at: + type: string + verified: + type: boolean + vms: + items: + $ref: '#/definitions/models.VM' + type: array + voucher_balance: + type: number + required: + - email + - first_name + - hashed_password + - last_name + type: object app.VerifyCodeInput: properties: code: @@ -727,17 +765,18 @@ paths: summary: Deletes all users' deployments tags: - Admin + /deployments/count: get: consumes: - application/json - description: List all users' deployments + description: Get users' deployments count in the system produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/app.ListDeploymentsResponse' + $ref: '#/definitions/models.DeploymentsCount' "400": description: Bad Request schema: {} @@ -752,21 +791,26 @@ paths: schema: {} security: - BearerAuth: [] - summary: List all users' deployments + summary: Get users' deployments count tags: - Admin - /deployments/count: - get: + /deployments/k8s/{id}: + delete: consumes: - application/json - description: Get users' deployments count in the system + description: Deletes a kubernetes cluster + parameters: + - description: Kubernetes cluster ID + in: path + name: id + required: true + type: string produces: - application/json responses: "200": description: OK - schema: - $ref: '#/definitions/models.DeploymentsCount' + schema: {} "400": description: Bad Request schema: {} @@ -781,7 +825,41 @@ paths: schema: {} security: - BearerAuth: [] - summary: Get users' deployments count + summary: Deletes a kubernetes cluster + tags: + - Admin + /deployments/vm/{id}: + delete: + consumes: + - application/json + description: Deletes a virtual machine + parameters: + - description: Virtual machine ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Deletes a virtual machine tags: - Admin /email: @@ -1550,7 +1628,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/models.User' + $ref: '#/definitions/app.UserResponse' type: array "400": description: Bad Request diff --git a/server/models/deployments_count.go b/server/models/deployments_count.go index dc79ffbd..db923b1e 100644 --- a/server/models/deployments_count.go +++ b/server/models/deployments_count.go @@ -33,10 +33,52 @@ func (d *DB) CountAllDeployments() (DeploymentsCount, error) { if result.Error != nil { return DeploymentsCount{}, result.Error } + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + ipsCount := k8sIPsCount + vmIPsCount + + return DeploymentsCount{ + dlsCount, ipsCount, + }, nil +} + +// CountUserDeployments returns deployments and IPs count per user +func (d *DB) CountUserDeployments(userID string) (DeploymentsCount, error) { + var vmsCount int64 + result := d.db.Table("vms").Where("user_id = ?", userID).Count(&vmsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sCount int64 + result = d.db.Table("k8s_clusters").Where("user_id = ?", userID).Count(&k8sCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + dlsCount := k8sCount + vmsCount + + var vmIPsCount int64 + result = d.db.Table("vms").Where("public_ip = true").Where("user_id = ?", userID).Count(&vmIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sIPsCount int64 + result = d.db.Table("k8s_clusters").Joins("JOIN masters ON k8s_clusters.id = masters.cluster_id"). + Where("public_ip = true").Where("user_id = ?", userID).Count(&k8sIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + if result.Error != nil { + return DeploymentsCount{}, result.Error + } ipsCount := k8sIPsCount + vmIPsCount return DeploymentsCount{ dlsCount, ipsCount, - }, result.Error + }, nil } diff --git a/server/models/k8s.go b/server/models/k8s.go index 3be657a2..bcc2e1c1 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -120,8 +120,7 @@ func (d *DB) GetAllSuccessfulK8s(userID string) ([]K8sCluster, error) { // DeleteK8s deletes a k8s cluster func (d *DB) DeleteK8s(id int) error { var k8s K8sCluster - err := d.db.First(&k8s, id).Error - if err != nil { + if err := d.db.First(&k8s, id).Error; err != nil { return err } return d.db.Select("Master", "Workers").Delete(&k8s).Error From c75afcd9d065678750ffdf5b29cdf2474dc3d78b Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Tue, 11 Feb 2025 16:47:40 +0200 Subject: [PATCH 6/7] make voucher balance fixed --- server/app/setup.go | 3 ++- server/app/user_handler.go | 5 ++--- server/app/user_handler_test.go | 2 -- server/docs/docs.go | 5 ----- server/docs/swagger.yaml | 4 ---- server/internal/config_parser.go | 1 + server/internal/config_parser_test.go | 3 ++- server/models/deployments_count.go | 8 ++++---- 8 files changed, 11 insertions(+), 20 deletions(-) diff --git a/server/app/setup.go b/server/app/setup.go index f4566ce5..121a4592 100644 --- a/server/app/setup.go +++ b/server/app/setup.go @@ -76,7 +76,8 @@ func SetUp(t testing.TB) *App { "medium_vm": 20, "large_vm": 30 }, - "stripe_secret": "sk_test" + "stripe_secret": "sk_test", + "voucher_balance": 10 } `, dbPath) diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 5f32a936..d0282cbb 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -65,8 +65,7 @@ type EmailInput struct { // ApplyForVoucherInput struct for user to apply for voucher type ApplyForVoucherInput struct { - Balance uint64 `json:"balance" binding:"required" validate:"min=0"` - Reason string `json:"reason" binding:"required" validate:"nonzero"` + Reason string `json:"reason" binding:"required" validate:"nonzero"` } // AddVoucherInput struct for voucher applied by user @@ -711,7 +710,7 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) voucher := models.Voucher{ Voucher: v, UserID: userID, - Balance: input.Balance, + Balance: a.config.VoucherBalance, Reason: input.Reason, } diff --git a/server/app/user_handler_test.go b/server/app/user_handler_test.go index 3e74685f..0720441b 100644 --- a/server/app/user_handler_test.go +++ b/server/app/user_handler_test.go @@ -870,8 +870,6 @@ func TestApplyForVoucherHandler(t *testing.T) { assert.NoError(t, err) voucherBody := []byte(`{ - "vms":10, - "public_ips":1, "reason":"strongReason" }`) diff --git a/server/docs/docs.go b/server/docs/docs.go index 67d8ea7f..6ec7941d 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -2712,14 +2712,9 @@ const docTemplate = `{ "app.ApplyForVoucherInput": { "type": "object", "required": [ - "balance", "reason" ], "properties": { - "balance": { - "type": "integer", - "minimum": 0 - }, "reason": { "type": "string" } diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index c93889ae..1cb21fca 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -35,13 +35,9 @@ definitions: type: object app.ApplyForVoucherInput: properties: - balance: - minimum: 0 - type: integer reason: type: string required: - - balance - reason type: object app.ChangePasswordInput: diff --git a/server/internal/config_parser.go b/server/internal/config_parser.go index ae951709..a594dbcd 100644 --- a/server/internal/config_parser.go +++ b/server/internal/config_parser.go @@ -24,6 +24,7 @@ type Configuration struct { PricesPerMonth Prices `json:"prices"` Currency string `json:"currency" validate:"nonzero"` StripeSecret string `json:"stripe_secret" validate:"nonzero"` + VoucherBalance uint64 `json:"voucher_balance" validate:"nonzero"` } // Server struct to hold server's information diff --git a/server/internal/config_parser_test.go b/server/internal/config_parser_test.go index b640280e..f50ea67d 100644 --- a/server/internal/config_parser_test.go +++ b/server/internal/config_parser_test.go @@ -42,7 +42,8 @@ var rightConfig = ` "medium_vm": 20, "large_vm": 30 }, - "stripe_secret": "sk_test" + "stripe_secret": "sk_test", + "voucher_balance": 10 } ` diff --git a/server/models/deployments_count.go b/server/models/deployments_count.go index db923b1e..ffa77b5f 100644 --- a/server/models/deployments_count.go +++ b/server/models/deployments_count.go @@ -23,13 +23,13 @@ func (d *DB) CountAllDeployments() (DeploymentsCount, error) { dlsCount := k8sCount + vmsCount var vmIPsCount int64 - result = d.db.Table("vms").Where("public_ip = true").Count(&vmIPsCount) + result = d.db.Table("vms").Where("public = true").Count(&vmIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } var k8sIPsCount int64 - result = d.db.Table("masters").Where("public_ip = true").Count(&k8sIPsCount) + result = d.db.Table("masters").Where("public = true").Count(&k8sIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } @@ -61,14 +61,14 @@ func (d *DB) CountUserDeployments(userID string) (DeploymentsCount, error) { dlsCount := k8sCount + vmsCount var vmIPsCount int64 - result = d.db.Table("vms").Where("public_ip = true").Where("user_id = ?", userID).Count(&vmIPsCount) + result = d.db.Table("vms").Where("public = true").Where("user_id = ?", userID).Count(&vmIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } var k8sIPsCount int64 result = d.db.Table("k8s_clusters").Joins("JOIN masters ON k8s_clusters.id = masters.cluster_id"). - Where("public_ip = true").Where("user_id = ?", userID).Count(&k8sIPsCount) + Where("public = true").Where("user_id = ?", userID).Count(&k8sIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } From e0371591943780d5b9bf01eb345a11ecd6a17c58 Mon Sep 17 00:00:00 2001 From: rawdaGastan Date: Thu, 13 Feb 2025 14:04:50 +0200 Subject: [PATCH 7/7] remove failed deployments from listing --- server/models/deployments_count.go | 24 ++++++++++++++++-------- server/models/k8s.go | 3 ++- server/models/vm.go | 3 ++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/server/models/deployments_count.go b/server/models/deployments_count.go index ffa77b5f..d85a55fb 100644 --- a/server/models/deployments_count.go +++ b/server/models/deployments_count.go @@ -9,13 +9,15 @@ type DeploymentsCount struct { // CountAllDeployments returns deployments and IPs count func (d *DB) CountAllDeployments() (DeploymentsCount, error) { var vmsCount int64 - result := d.db.Table("vms").Count(&vmsCount) + result := d.db.Table("vms"). + Where("state != ?", StateFailed).Count(&vmsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } var k8sCount int64 - result = d.db.Table("masters").Count(&k8sCount) + result = d.db.Table("k8s_clusters"). + Where("state != ?", StateFailed).Count(&k8sCount) if result.Error != nil { return DeploymentsCount{}, result.Error } @@ -23,13 +25,15 @@ func (d *DB) CountAllDeployments() (DeploymentsCount, error) { dlsCount := k8sCount + vmsCount var vmIPsCount int64 - result = d.db.Table("vms").Where("public = true").Count(&vmIPsCount) + result = d.db.Table("vms").Where("public = true"). + Where("state != ?", StateFailed).Count(&vmIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } var k8sIPsCount int64 - result = d.db.Table("masters").Where("public = true").Count(&k8sIPsCount) + result = d.db.Table("k8s_clusters").Joins("JOIN masters ON k8s_clusters.id = masters.cluster_id"). + Where("public = true").Where("state != ?", StateFailed).Count(&k8sIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } @@ -47,13 +51,15 @@ func (d *DB) CountAllDeployments() (DeploymentsCount, error) { // CountUserDeployments returns deployments and IPs count per user func (d *DB) CountUserDeployments(userID string) (DeploymentsCount, error) { var vmsCount int64 - result := d.db.Table("vms").Where("user_id = ?", userID).Count(&vmsCount) + result := d.db.Table("vms").Where("user_id = ?", userID). + Where("state != ?", StateFailed).Count(&vmsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } var k8sCount int64 - result = d.db.Table("k8s_clusters").Where("user_id = ?", userID).Count(&k8sCount) + result = d.db.Table("k8s_clusters").Where("user_id = ?", userID). + Where("state != ?", StateFailed).Count(&k8sCount) if result.Error != nil { return DeploymentsCount{}, result.Error } @@ -61,14 +67,16 @@ func (d *DB) CountUserDeployments(userID string) (DeploymentsCount, error) { dlsCount := k8sCount + vmsCount var vmIPsCount int64 - result = d.db.Table("vms").Where("public = true").Where("user_id = ?", userID).Count(&vmIPsCount) + result = d.db.Table("vms").Where("public = true").Where("user_id = ?", userID). + Where("state != ?", StateFailed).Count(&vmIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } var k8sIPsCount int64 result = d.db.Table("k8s_clusters").Joins("JOIN masters ON k8s_clusters.id = masters.cluster_id"). - Where("public = true").Where("user_id = ?", userID).Count(&k8sIPsCount) + Where("public = true").Where("user_id = ?", userID). + Where("state != ?", StateFailed).Count(&k8sIPsCount) if result.Error != nil { return DeploymentsCount{}, result.Error } diff --git a/server/models/k8s.go b/server/models/k8s.go index bcc2e1c1..221e4c36 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -88,7 +88,8 @@ func (d *DB) GetK8s(id int) (K8sCluster, error) { // GetAllK8s gets all k8s clusters func (d *DB) GetAllK8s(userID string) ([]K8sCluster, error) { var k8sClusters []K8sCluster - err := d.db.Find(&k8sClusters, "user_id = ?", userID).Error + err := d.db.Find(&k8sClusters, "user_id = ?", userID). + Where("state != ?", StateFailed).Error if err != nil { return nil, err } diff --git a/server/models/vm.go b/server/models/vm.go index c0384b9d..d6729e4d 100644 --- a/server/models/vm.go +++ b/server/models/vm.go @@ -44,7 +44,8 @@ func (d *DB) GetVMByID(id int) (VM, error) { // GetAllVms returns all vms of user func (d *DB) GetAllVms(userID string) ([]VM, error) { var vms []VM - return vms, d.db.Where("user_id = ?", userID).Find(&vms).Error + return vms, d.db.Where("user_id = ?", userID). + Where("state != ?", StateFailed).Find(&vms).Error } // GetAllSuccessfulVms returns all vms of user that have a state succeeded