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..732e75b3 100644
--- a/client/src/components/Toast.vue
+++ b/client/src/components/Toast.vue
@@ -1,24 +1,26 @@
-
+
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..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"
@@ -18,34 +20,62 @@ 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
+type SetPricesInput struct {
+ Small float64 `json:"small"`
+ Medium float64 `json:"medium"`
+ Large float64 `json:"large"`
+ PublicIP float64 `json:"public_ip"`
+}
+
+type UserResponse struct {
+ *models.User
+ VMs []models.VM `json:"vms"`
+ K8S []models.K8sCluster `json:"k8s"`
+ Count models.DeploymentsCount `json:"count"`
}
// 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} []UserResponse
+// @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 {
@@ -60,13 +90,140 @@ 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()
+}
+
+// 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 +245,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 +270,86 @@ 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()
+// 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"))
}
- 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))
- }
+ if err = a.db.DeleteVMByID(id); err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
}
return ResponseMsg{
- Message: "Quota is reset successfully",
+ Message: "Virtual machine is deleted successfully",
}, Ok()
}
-// DeleteAllDeployments deletes all deployments
-func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) {
+// 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
+// @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 +364,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 +382,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 +407,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,58 +419,21 @@ func (a *App) DeleteAllDeployments(req *http.Request) (interface{}, Response) {
}, Ok()
}
-// ListDeployments lists all deployments
-func (a *App) ListDeployments(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.UserID)
- if err == gorm.ErrRecordNotFound || len(vms) == 0 {
- log.Error().Err(err).Str("userID", user.UserID).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.UserID)
- if err == gorm.ErrRecordNotFound || len(clusters) == 0 {
- log.Error().Err(err).Str("userID", user.UserID).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: map[string]interface{}{"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 +458,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 +518,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 +557,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 +565,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 +578,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 /email [post]
+func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) {
var emailUser EmailUser
err := json.NewDecoder(req.Body).Decode(&emailUser)
@@ -453,7 +619,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 +640,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 +678,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..2d455988 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,10 +114,12 @@ 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()
+ regionRouter := authRouter.PathPrefix("/region").Subrouter()
// sub routes with no authorization
unAuthUserRouter := versionRouter.PathPrefix("/user").Subrouter()
@@ -123,20 +137,32 @@ 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")
- quotaRouter.HandleFunc("", WrapFunc(a.GetQuotaHandler)).Methods("GET", "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")
+
+ 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")
+ 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")
@@ -156,21 +182,24 @@ 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("/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")
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("/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
new file mode 100644
index 00000000..5d1ff0af
--- /dev/null
+++ b/server/app/invoice_handler.go
@@ -0,0 +1,634 @@
+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" validate:"nonzero" 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))
+ }
+
+ 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",
+ }, 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. Pay invoices
+ invoices, err := a.db.ListUnpaidInvoices(user.ID.String())
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ for _, invoice := range invoices {
+ cards, err := a.db.GetUserCards(user.ID.String())
+ if 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
+ 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 {
+ monthStart := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local)
+
+ vms, err := a.db.GetAllSuccessfulVms(userID)
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return err
+ }
+
+ k8s, err := a.db.GetAllSuccessfulK8s(userID)
+ if err != nil && err != gorm.ErrRecordNotFound {
+ return err
+ }
+
+ var items []models.DeploymentItem
+ 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{
+ DeploymentResources: vm.Resources,
+ DeploymentType: "vm",
+ DeploymentID: vm.ID,
+ HasPublicIP: vm.Public,
+ PeriodInHours: time.Since(usageStart).Hours(),
+ Cost: cost,
+ })
+
+ total += cost
+ }
+
+ 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{
+ DeploymentResources: cluster.Master.Resources,
+ DeploymentType: "k8s",
+ DeploymentID: cluster.ID,
+ HasPublicIP: cluster.Master.Public,
+ PeriodInHours: time.Since(usageStart).Hours(),
+ Cost: cost,
+ })
+
+ total += cost
+ }
+
+ if len(items) > 0 {
+ if err = a.db.CreateInvoice(&models.Invoice{
+ UserID: userID,
+ Total: total,
+ Deployments: items,
+ }); err != nil {
+ return err
+ }
+ }
+
+ 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
+}
+
+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{
+ "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..70f59942 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" 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" validate:"nonzero"`
+}
+
// 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..829590a0
--- /dev/null
+++ b/server/app/payments_handler.go
@@ -0,0 +1,397 @@
+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 {
+ 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" validate:"nonzero"`
+}
+
+type ChargeBalance struct {
+ PaymentMethodID string `json:"payment_method_id" binding:"required" validate:"nonzero"`
+ Amount float64 `json:"amount" binding:"required" validate:"nonzero"`
+}
+
+// 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.TokenType, input.TokenID)
+ 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 {
+ 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.TokenType,
+ 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, paymentMethod.ID)
+ 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))
+ }
+ }
+
+ // 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{
+ 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)
+
+ 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()
+ 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.GetAllSuccessfulVms(userID)
+ if err != nil && err != gorm.ErrRecordNotFound {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ k8s, err = a.db.GetAllSuccessfulK8s(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"))
+ }
+
+ // 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 {
+ 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..121a4592 100644
--- a/server/app/setup.go
+++ b/server/app/setup.go
@@ -68,7 +68,16 @@ 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",
+ "voucher_balance": 10
}
`, dbPath)
@@ -88,7 +97,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..d0282cbb 100644
--- a/server/app/user_handler.go
+++ b/server/app/user_handler.go
@@ -22,38 +22,37 @@ 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
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"`
}
// 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"`
@@ -61,22 +60,52 @@ 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
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"`
+ Reason string `json:"reason" binding:"required" validate:"nonzero"`
}
// 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" validate:"nonzero"`
+}
+
+type AccessTokenResponse struct {
+ Token string `json:"access_token"`
+ Timeout int `json:"timeout"`
+}
+
+type RefreshTokenResponse struct {
+ Access string `json:"access_token"`
+ Refresh string `json:"refresh_token"`
+ Timeout int `json:"timeout"`
+}
+
+type clientSecretResponse struct {
+ ClientSecret string `json:"client_secret"`
}
// 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 +143,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 +157,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 +186,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 +236,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)
- 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()
-
- // token
- token, err := internal.CreateJWT(user.ID.String(), user.Email, a.config.Token.Secret, a.config.Token.Timeout)
+ 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).Inc()
- 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 +252,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} AccessTokenResponse
+// @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 +303,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: AccessTokenResponse{Token: token, Timeout: a.config.Token.Timeout},
+ }, 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} RefreshTokenResponse
+// @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 +348,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 +362,24 @@ 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: RefreshTokenResponse{Access: reqToken, Refresh: newToken, Timeout: a.config.Token.Timeout},
+ }, 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
+// @Param forgetPassword body EmailInput true "User forget password input"
+// @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 +403,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,13 +425,26 @@ 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
+// @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/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()
@@ -408,11 +481,25 @@ func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, R
return ResponseMsg{
Message: "Code is verified",
- Data: map[string]string{"access_token": token},
+ Data: AccessTokenResponse{Token: token, Timeout: a.config.Token.Timeout},
}, 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 +524,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 +540,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 +594,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 +617,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 +634,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 +669,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 +708,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: a.config.VoucherBalance,
+ Reason: input.Reason,
}
err = a.db.CreateVoucher(&voucher)
@@ -588,15 +719,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 +752,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 +770,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 +788,278 @@ 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)
+
+ // 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.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))
}
- middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc()
+
+ 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))
+ }
+
+ 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))
+ }
+
+ user.Balance += float64(input.Amount)
+
+ // 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))
+ }
+
+ // 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"))
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return ResponseMsg{
+ Message: "User is deleted successfully",
+ }, Ok()
+}
diff --git a/server/app/user_handler_test.go b/server/app/user_handler_test.go
index 908b91c4..0720441b 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) {
@@ -623,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"
}`)
@@ -870,8 +870,6 @@ func TestApplyForVoucherHandler(t *testing.T) {
assert.NoError(t, err)
voucherBody := []byte(`{
- "vms":10,
- "public_ips":1,
"reason":"strongReason"
}`)
@@ -889,7 +887,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 +913,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 +946,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 +1017,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 +1040,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 +1068,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..551293f0 100644
--- a/server/app/vm_handler.go
+++ b/server/app/vm_handler.go
@@ -9,16 +9,40 @@ 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"
)
+// 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" validate:"nonzero"`
+ 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 +55,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 +68,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 +111,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 +131,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 +171,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 +212,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 +247,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 +278,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 +302,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)
@@ -239,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/voucher_handler.go b/server/app/voucher_handler.go
index 90a24aca..ab65066c 100644
--- a/server/app/voucher_handler.go
+++ b/server/app/voucher_handler.go
@@ -17,17 +17,30 @@ 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" 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
+// 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,38 @@ 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/all/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 {
+ err = a.db.UpdateUserVoucherBalance(user.ID.String(), 0)
+ 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/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/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 fd489bf9..1410b3dd 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"
@@ -49,14 +48,17 @@ var (
type Deployer struct {
db models.DB
Redis streams.RedisClient
- tfPluginClient deployer.TFPluginClient
+ 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
@@ -105,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")
}
@@ -121,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")
}
@@ -141,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
@@ -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,124 @@ 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
+ now := time.Now()
+ monthStart := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local)
+
+ 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.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
+ }
+
+ return debt, nil
+}
+
+// UsagePercentageInMonth calculates percentage of hours till specific time during the month
+// according to total hours of the same month
+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/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..08abd22a 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
}
@@ -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)
}
@@ -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")
- }
- if err != nil {
- log.Error().Err(err).Send()
- return http.StatusInternalServerError, errors.New(internalServerErrorMsg)
+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
}
-
- 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)
- 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")
- }
+ k8sCluster, err := d.loadK8s(ctx, k8sDeployInput, node, networkContractID, k8sContractID)
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..37970bdc 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,
}
- nodeIDs, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, nil, 1)
+ 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
@@ -96,93 +97,56 @@ 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)
+ 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)
+ 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..6ec7941d
--- /dev/null
+++ b/server/docs/docs.go
@@ -0,0 +1,3657 @@
+// 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 announcement and sends it to all users as an email and notification",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Admin"
+ ],
+ "summary": "Creates a new administrator announcement and sends it to all users as an email and notification",
+ "parameters": [
+ {
+ "description": "announcement to be created",
+ "name": "announcement",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/app.AdminAnnouncement"
+ }
+ }
+ ],
+ "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": {
+ "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": {}
+ }
+ }
+ }
+ },
+ "/deployments/k8s/{id}": {
+ "delete": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Deletes a kubernetes cluster",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Admin"
+ ],
+ "summary": "Deletes a kubernetes cluster",
+ "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": {}
+ }
+ }
+ }
+ },
+ "/deployments/vm/{id}": {
+ "delete": {
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "description": "Deletes a virtual machine",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Admin"
+ ],
+ "summary": "Deletes a virtual machine",
+ "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": {}
+ }
+ }
+ }
+ },
+ "/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": [
+ {
+ "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": {}
+ }
+ }
+ }
+ },
+ "/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": [
+ {
+ "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": {}
+ }
+ }
+ },
+ "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": {
+ "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/app.UserResponse"
+ }
+ }
+ },
+ "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/forget_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",
+ "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.AccessTokenResponse"
+ }
+ },
+ "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",
+ "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.CodeTimeout"
+ }
+ },
+ "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.RefreshTokenResponse"
+ }
+ },
+ "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.AccessTokenResponse"
+ }
+ },
+ "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/all/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.AccessTokenResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "timeout": {
+ "type": "integer"
+ }
+ }
+ },
+ "app.AddCardInput": {
+ "type": "object",
+ "required": [
+ "token_id",
+ "token_type"
+ ],
+ "properties": {
+ "token_id": {
+ "type": "string"
+ },
+ "token_type": {
+ "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": [
+ "reason"
+ ],
+ "properties": {
+ "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.EmailInput": {
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "properties": {
+ "email": {
+ "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.PayInvoiceInput": {
+ "type": "object",
+ "required": [
+ "method"
+ ],
+ "properties": {
+ "card_payment_id": {
+ "type": "string"
+ },
+ "method": {
+ "$ref": "#/definitions/app.method"
+ }
+ }
+ },
+ "app.RefreshTokenResponse": {
+ "type": "object",
+ "properties": {
+ "access_token": {
+ "type": "string"
+ },
+ "refresh_token": {
+ "type": "string"
+ },
+ "timeout": {
+ "type": "integer"
+ }
+ }
+ },
+ "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.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": [
+ "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"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "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_default_payment_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..1cb21fca
--- /dev/null
+++ b/server/docs/swagger.yaml
@@ -0,0 +1,2426 @@
+definitions:
+ app.AccessTokenResponse:
+ properties:
+ access_token:
+ type: string
+ timeout:
+ type: integer
+ type: object
+ app.AddCardInput:
+ properties:
+ token_id:
+ type: string
+ token_type:
+ type: string
+ required:
+ - token_id
+ - token_type
+ 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:
+ reason:
+ type: string
+ required:
+ - 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.EmailInput:
+ properties:
+ email:
+ type: string
+ required:
+ - email
+ 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.PayInvoiceInput:
+ properties:
+ card_payment_id:
+ type: string
+ method:
+ $ref: '#/definitions/app.method'
+ required:
+ - method
+ type: object
+ app.RefreshTokenResponse:
+ properties:
+ access_token:
+ type: string
+ refresh_token:
+ type: string
+ timeout:
+ type: integer
+ 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.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:
+ 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
+ id:
+ type: integer
+ 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_default_payment_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 announcement and sends it to all users
+ as an email and notification
+ parameters:
+ - description: announcement to be created
+ in: body
+ name: announcement
+ required: true
+ schema:
+ $ref: '#/definitions/app.AdminAnnouncement'
+ 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 announcement and sends it to all users
+ 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
+ /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
+ /deployments/k8s/{id}:
+ delete:
+ consumes:
+ - application/json
+ 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: {}
+ "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 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:
+ 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:
+ - 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
+ /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:
+ - 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:
+ 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
+ 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/app.UserResponse'
+ 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/forget_password/verify_email:
+ post:
+ consumes:
+ - application/json
+ 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.AccessTokenResponse'
+ "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/forgot_password:
+ post:
+ consumes:
+ - application/json
+ 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.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/refresh_token:
+ post:
+ consumes:
+ - application/json
+ description: Generate a refresh token
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: Created
+ schema:
+ $ref: '#/definitions/app.RefreshTokenResponse'
+ "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.AccessTokenResponse'
+ "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/all/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..b8ad0fa7 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -1,9 +1,12 @@
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
+ 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
@@ -14,10 +17,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,8 +31,8 @@ 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
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.12 // 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,22 +62,25 @@ 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
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
+ 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
@@ -75,9 +88,11 @@ 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/sync v0.9.0 // indirect
- golang.org/x/sys v0.27.0 // 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
+ 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..fb46f78b 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=
@@ -121,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=
@@ -138,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=
@@ -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/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
-golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+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-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=
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..a594dbcd 100644
--- a/server/internal/config_parser.go
+++ b/server/internal/config_parser.go
@@ -21,6 +21,10 @@ 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"`
+ VoucherBalance uint64 `json:"voucher_balance" validate:"nonzero"`
}
// Server struct to hold server's information
@@ -57,6 +61,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/config_parser_test.go b/server/internal/config_parser_test.go
index 41e6d67a..f50ea67d 100644
--- a/server/internal/config_parser_test.go
+++ b/server/internal/config_parser_test.go
@@ -35,7 +35,15 @@ 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",
+ "voucher_balance": 10
}
`
@@ -331,7 +339,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 +439,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 +455,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 22b5a5f6..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))
@@ -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..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))
@@ -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/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
+}
diff --git a/server/internal/templates/adminAnnouncement.html b/server/internal/templates/adminAnnouncement.html
index b58d8661..a01b7f1b 100644
--- a/server/internal/templates/adminAnnouncement.html
+++ b/server/internal/templates/adminAnnouncement.html
@@ -212,7 +212,7 @@
>
Dear -name-,
- -announcement-
+ -body-
@@ -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/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..1521e57d
--- /dev/null
+++ b/server/models/card.go
@@ -0,0 +1,66 @@
+package models
+
+import (
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+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
+}
+
+// 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 52093922..c0b0fc4e 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{}, &DeploymentItem{}, &PaymentDetails{},
+ )
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..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"
@@ -37,12 +38,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 +51,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 +59,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 +74,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 +82,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 +104,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 +114,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 +130,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 +149,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 +163,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 +193,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 +202,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 +211,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 +225,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
@@ -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,97 +465,22 @@ 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)
})
}
-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"}
@@ -693,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",
@@ -712,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)
@@ -722,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) {
@@ -733,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",
@@ -746,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)
@@ -765,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) {
@@ -792,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)
@@ -810,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) {
@@ -832,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)
@@ -860,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) {
@@ -898,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/deployments_count.go b/server/models/deployments_count.go
new file mode 100644
index 00000000..d85a55fb
--- /dev/null
+++ b/server/models/deployments_count.go
@@ -0,0 +1,92 @@
+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").
+ Where("state != ?", StateFailed).Count(&vmsCount)
+ if result.Error != nil {
+ return DeploymentsCount{}, result.Error
+ }
+
+ var k8sCount int64
+ result = d.db.Table("k8s_clusters").
+ Where("state != ?", StateFailed).Count(&k8sCount)
+ if result.Error != nil {
+ return DeploymentsCount{}, result.Error
+ }
+
+ dlsCount := k8sCount + vmsCount
+
+ var vmIPsCount int64
+ 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("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
+ }
+ 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).
+ 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).
+ Where("state != ?", StateFailed).Count(&k8sCount)
+ if result.Error != nil {
+ return DeploymentsCount{}, result.Error
+ }
+
+ dlsCount := k8sCount + vmsCount
+
+ var vmIPsCount int64
+ 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).
+ Where("state != ?", StateFailed).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,
+ }, nil
+}
diff --git a/server/models/invoice.go b/server/models/invoice.go
new file mode 100644
index 00000000..1ccfb7df
--- /dev/null
+++ b/server/models/invoice.go
@@ -0,0 +1,103 @@
+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 {
+ ID int `json:"id" gorm:"primaryKey"`
+ 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
+ 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("paid_at", time.Now())
+
+ if result.RowsAffected == 0 {
+ return gorm.ErrRecordNotFound
+ }
+ return result.Error
+}
+
+// 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..221e4c36 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,120 @@ 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
+ if err = d.db.Model(&k8s).Association("Master").Find(&master); err != nil {
+ return K8sCluster{}, err
+ }
+
+ var workers []Worker
+ if err = d.db.Model(&k8s).Association("Workers").Find(&workers); err != nil {
+ return K8sCluster{}, nil
+ }
+
+ 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).
+ Where("state != ?", StateFailed).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
+}
+
+// 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
+ if err := d.db.First(&k8s, id).Error; 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..87987135
--- /dev/null
+++ b/server/models/state.go
@@ -0,0 +1,31 @@
+package models
+
+type state string
+
+const (
+ StateCreated = "CREATED"
+ 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..35467134 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_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"`
+ 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,98 @@ 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
+}
+
+// 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
+ 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 642882a0..d6729e4d 100644
--- a/server/models/vm.go
+++ b/server/models/vm.go
@@ -1,25 +1,100 @@
// 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).
+ Where("state != ?", StateFailed).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
+}
+
+// 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