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/index.html b/client/index.html index fb72ab05..e2258ee5 100644 --- a/client/index.html +++ b/client/index.html @@ -1,16 +1,15 @@ - - - - - - - Cloud for Students - - - -
- - + + + + + + Cloud for All + + + +
+ + diff --git a/client/package.json b/client/package.json index aa93b3e1..2ec624ff 100644 --- a/client/package.json +++ b/client/package.json @@ -9,16 +9,15 @@ "lint": "eslint . --fix --ignore-path .gitignore" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.4.2", - "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@fortawesome/free-regular-svg-icons": "^6.4.2", - "@fortawesome/free-solid-svg-icons": "^6.4.2", - "@fortawesome/vue-fontawesome": "^3.0.8", "@mdi/font": "7.4.47", + "@stripe/stripe-js": "^5.4.0", + "@vue-stripe/vue-stripe": "^4.5.0", "axios": "^1.7.2", "core-js": "^3.33.2", - "mitt": "^3.0.1", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", "mosha-vue-toastify": "^1.0.23", + "pinia": "^2.3.1", "roboto-fontface": "*", "vite-plugin-eslint": "^1.8.1", "vue": "^3.5.10", 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/App.vue b/client/src/App.vue index 68e96bd2..08f54d07 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -4,18 +4,22 @@ - + diff --git a/client/src/assets/404_page.png b/client/src/assets/404_page.png new file mode 100644 index 00000000..b19e43c1 Binary files /dev/null and b/client/src/assets/404_page.png differ diff --git a/client/src/assets/about_us.png b/client/src/assets/about_us.png deleted file mode 100644 index 4a0030e2..00000000 Binary files a/client/src/assets/about_us.png and /dev/null differ diff --git a/client/src/assets/cards_logos/11.png b/client/src/assets/cards_logos/11.png new file mode 100644 index 00000000..590a7a80 Binary files /dev/null and b/client/src/assets/cards_logos/11.png differ diff --git a/client/src/assets/cards_logos/12.png b/client/src/assets/cards_logos/12.png new file mode 100644 index 00000000..b150aeab Binary files /dev/null and b/client/src/assets/cards_logos/12.png differ diff --git a/client/src/assets/cards_logos/13.png b/client/src/assets/cards_logos/13.png new file mode 100644 index 00000000..05e36461 Binary files /dev/null and b/client/src/assets/cards_logos/13.png differ diff --git a/client/src/assets/cards_logos/15.png b/client/src/assets/cards_logos/15.png new file mode 100644 index 00000000..f6aee5ac Binary files /dev/null and b/client/src/assets/cards_logos/15.png differ diff --git a/client/src/assets/cards_logos/17.png b/client/src/assets/cards_logos/17.png new file mode 100644 index 00000000..035126ef Binary files /dev/null and b/client/src/assets/cards_logos/17.png differ diff --git a/client/src/assets/cards_logos/18.png b/client/src/assets/cards_logos/18.png new file mode 100644 index 00000000..2f630a0e Binary files /dev/null and b/client/src/assets/cards_logos/18.png differ diff --git a/client/src/assets/cards_logos/19.png b/client/src/assets/cards_logos/19.png new file mode 100644 index 00000000..ed96da7c Binary files /dev/null and b/client/src/assets/cards_logos/19.png differ diff --git a/client/src/assets/cards_logos/20.png b/client/src/assets/cards_logos/20.png new file mode 100644 index 00000000..8573252e Binary files /dev/null and b/client/src/assets/cards_logos/20.png differ diff --git a/client/src/assets/cards_logos/21.png b/client/src/assets/cards_logos/21.png new file mode 100644 index 00000000..d21798c5 Binary files /dev/null and b/client/src/assets/cards_logos/21.png differ diff --git a/client/src/assets/cards_logos/23.png b/client/src/assets/cards_logos/23.png new file mode 100644 index 00000000..89a22ebd Binary files /dev/null and b/client/src/assets/cards_logos/23.png differ diff --git a/client/src/assets/cards_logos/24.png b/client/src/assets/cards_logos/24.png new file mode 100644 index 00000000..6ce333a6 Binary files /dev/null and b/client/src/assets/cards_logos/24.png differ diff --git a/client/src/assets/cards_logos/3.png b/client/src/assets/cards_logos/3.png new file mode 100644 index 00000000..5aba59ba Binary files /dev/null and b/client/src/assets/cards_logos/3.png differ diff --git a/client/src/assets/cards_logos/4.png b/client/src/assets/cards_logos/4.png new file mode 100644 index 00000000..406202f1 Binary files /dev/null and b/client/src/assets/cards_logos/4.png differ diff --git a/client/src/assets/cards_logos/5.png b/client/src/assets/cards_logos/5.png new file mode 100644 index 00000000..26a0f990 Binary files /dev/null and b/client/src/assets/cards_logos/5.png differ diff --git a/client/src/assets/cards_logos/6.png b/client/src/assets/cards_logos/6.png new file mode 100644 index 00000000..959d05c2 Binary files /dev/null and b/client/src/assets/cards_logos/6.png differ diff --git a/client/src/assets/cards_logos/7.png b/client/src/assets/cards_logos/7.png new file mode 100644 index 00000000..bbb391ce Binary files /dev/null and b/client/src/assets/cards_logos/7.png differ diff --git a/client/src/assets/cards_logos/8.png b/client/src/assets/cards_logos/8.png new file mode 100644 index 00000000..9ac70dc7 Binary files /dev/null and b/client/src/assets/cards_logos/8.png differ diff --git a/client/src/assets/cards_logos/9.png b/client/src/assets/cards_logos/9.png new file mode 100644 index 00000000..789e6a4a Binary files /dev/null and b/client/src/assets/cards_logos/9.png differ diff --git a/client/src/assets/cards_logos/amex.png b/client/src/assets/cards_logos/amex.png new file mode 100644 index 00000000..366f6e2b Binary files /dev/null and b/client/src/assets/cards_logos/amex.png differ diff --git a/client/src/assets/cards_logos/diners.png b/client/src/assets/cards_logos/diners.png new file mode 100644 index 00000000..0134bf0b Binary files /dev/null and b/client/src/assets/cards_logos/diners.png differ diff --git a/client/src/assets/cards_logos/discover.png b/client/src/assets/cards_logos/discover.png new file mode 100644 index 00000000..ac653f02 Binary files /dev/null and b/client/src/assets/cards_logos/discover.png differ diff --git a/client/src/assets/cards_logos/jcb.png b/client/src/assets/cards_logos/jcb.png new file mode 100644 index 00000000..f70bfd54 Binary files /dev/null and b/client/src/assets/cards_logos/jcb.png differ diff --git a/client/src/assets/cards_logos/mastercard.png b/client/src/assets/cards_logos/mastercard.png new file mode 100644 index 00000000..b274d7e7 Binary files /dev/null and b/client/src/assets/cards_logos/mastercard.png differ diff --git a/client/src/assets/cards_logos/visa.png b/client/src/assets/cards_logos/visa.png new file mode 100644 index 00000000..7d21c22c Binary files /dev/null and b/client/src/assets/cards_logos/visa.png differ diff --git a/client/src/assets/cpass.jpg b/client/src/assets/cpass.jpg deleted file mode 100644 index 0da13222..00000000 Binary files a/client/src/assets/cpass.jpg and /dev/null differ diff --git a/client/src/assets/cpass.png b/client/src/assets/cpass.png deleted file mode 100644 index 7301499d..00000000 Binary files a/client/src/assets/cpass.png and /dev/null differ diff --git a/client/src/assets/home_header.png b/client/src/assets/home_header.png new file mode 100644 index 00000000..ef2d94a2 Binary files /dev/null and b/client/src/assets/home_header.png differ diff --git a/client/src/assets/key.png b/client/src/assets/key.png new file mode 100644 index 00000000..c064313f Binary files /dev/null and b/client/src/assets/key.png differ diff --git a/client/src/assets/lock.png b/client/src/assets/lock.png new file mode 100644 index 00000000..d72f4410 Binary files /dev/null and b/client/src/assets/lock.png differ diff --git a/client/src/assets/login.png b/client/src/assets/login.png deleted file mode 100644 index 2c02ab49..00000000 Binary files a/client/src/assets/login.png and /dev/null differ diff --git a/client/src/assets/logo_c4all.png b/client/src/assets/logo_c4all.png new file mode 100644 index 00000000..68fef63d Binary files /dev/null and b/client/src/assets/logo_c4all.png differ diff --git a/client/src/assets/maintainence.png b/client/src/assets/maintainence.png new file mode 100644 index 00000000..4ca39654 Binary files /dev/null and b/client/src/assets/maintainence.png differ diff --git a/client/src/assets/next_launch.png b/client/src/assets/next_launch.png new file mode 100644 index 00000000..1de89cfd Binary files /dev/null and b/client/src/assets/next_launch.png differ diff --git a/client/src/assets/not_found.png b/client/src/assets/not_found.png deleted file mode 100644 index 8eeb2e21..00000000 Binary files a/client/src/assets/not_found.png and /dev/null differ diff --git a/client/src/assets/otp.png b/client/src/assets/otp.png index 0f38c066..2cafe26a 100644 Binary files a/client/src/assets/otp.png and b/client/src/assets/otp.png differ diff --git a/client/src/assets/sign-in.png b/client/src/assets/sign-in.png new file mode 100644 index 00000000..a0ad1caa Binary files /dev/null and b/client/src/assets/sign-in.png differ diff --git a/client/src/assets/sign-up.png b/client/src/assets/sign-up.png new file mode 100644 index 00000000..af7ea058 Binary files /dev/null and b/client/src/assets/sign-up.png differ diff --git a/client/src/assets/verification_code.png b/client/src/assets/verification_code.png deleted file mode 100644 index 31ffa086..00000000 Binary files a/client/src/assets/verification_code.png and /dev/null differ diff --git a/client/src/assets/welcome.png b/client/src/assets/welcome.png deleted file mode 100644 index a40bcc14..00000000 Binary files a/client/src/assets/welcome.png and /dev/null differ diff --git a/client/src/components/AddPayment.vue b/client/src/components/AddPayment.vue new file mode 100644 index 00000000..eba35f02 --- /dev/null +++ b/client/src/components/AddPayment.vue @@ -0,0 +1,27 @@ + + diff --git a/client/src/components/Alerts.vue b/client/src/components/Alerts.vue new file mode 100644 index 00000000..70917ad7 --- /dev/null +++ b/client/src/components/Alerts.vue @@ -0,0 +1,34 @@ + + + diff --git a/client/src/components/ApplyVoucher.vue b/client/src/components/ApplyVoucher.vue new file mode 100644 index 00000000..b3047cc0 --- /dev/null +++ b/client/src/components/ApplyVoucher.vue @@ -0,0 +1,93 @@ + + + diff --git a/client/src/components/Confirm.vue b/client/src/components/Confirm.vue index 8d3e9171..87f22ad3 100644 --- a/client/src/components/Confirm.vue +++ b/client/src/components/Confirm.vue @@ -1,57 +1,44 @@ - diff --git a/client/src/components/DeploymentCard.vue b/client/src/components/DeploymentCard.vue new file mode 100644 index 00000000..feb0470d --- /dev/null +++ b/client/src/components/DeploymentCard.vue @@ -0,0 +1,92 @@ + + + diff --git a/client/src/components/Footer.vue b/client/src/components/Footer.vue index 0c5c8815..e566a428 100644 --- a/client/src/components/Footer.vue +++ b/client/src/components/Footer.vue @@ -1,6 +1,7 @@ diff --git a/client/src/layouts/NoNavbar.vue b/client/src/layouts/NoNavbar.vue index 14ff5d16..5585aa91 100644 --- a/client/src/layouts/NoNavbar.vue +++ b/client/src/layouts/NoNavbar.vue @@ -1,9 +1,9 @@ diff --git a/client/src/layouts/default/AppBar.vue b/client/src/layouts/default/AppBar.vue index 5e0e86eb..eb253ab6 100644 --- a/client/src/layouts/default/AppBar.vue +++ b/client/src/layouts/default/AppBar.vue @@ -1,314 +1,196 @@ - diff --git a/client/src/layouts/default/Default.vue b/client/src/layouts/default/Default.vue index 697d1e79..d06b8a31 100644 --- a/client/src/layouts/default/Default.vue +++ b/client/src/layouts/default/Default.vue @@ -1,88 +1,19 @@ - - - diff --git a/client/src/layouts/default/View.vue b/client/src/layouts/default/View.vue index 64cc51f8..6ed5c663 100644 --- a/client/src/layouts/default/View.vue +++ b/client/src/layouts/default/View.vue @@ -4,16 +4,12 @@ - - diff --git a/client/src/views/Account.vue b/client/src/views/Account.vue new file mode 100644 index 00000000..851258a3 --- /dev/null +++ b/client/src/views/Account.vue @@ -0,0 +1,31 @@ + + diff --git a/client/src/views/Admin.vue b/client/src/views/Admin.vue index 59f323cb..5a1c1e22 100644 --- a/client/src/views/Admin.vue +++ b/client/src/views/Admin.vue @@ -1,282 +1,422 @@ diff --git a/client/src/views/Deploy.vue b/client/src/views/Deploy.vue new file mode 100644 index 00000000..a2c616e1 --- /dev/null +++ b/client/src/views/Deploy.vue @@ -0,0 +1,170 @@ + + diff --git a/client/src/views/Forgetpassword.vue b/client/src/views/Forgetpassword.vue index 859340c5..688bd33c 100644 --- a/client/src/views/Forgetpassword.vue +++ b/client/src/views/Forgetpassword.vue @@ -1,109 +1,90 @@ - - - diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index e5d41bf4..c23976e8 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,156 +1,121 @@ - diff --git a/client/src/views/HomeWrapper.vue b/client/src/views/HomeWrapper.vue new file mode 100644 index 00000000..e7ca8bb1 --- /dev/null +++ b/client/src/views/HomeWrapper.vue @@ -0,0 +1,11 @@ + + + diff --git a/client/src/views/K8s.vue b/client/src/views/K8s.vue index 28314c1a..2a91cf46 100644 --- a/client/src/views/K8s.vue +++ b/client/src/views/K8s.vue @@ -7,9 +7,7 @@
Kubernetes Clusters
-

- Deploy a new Kubernetes cluster -

+

Deploy a new Kubernetes cluster

@@ -27,7 +25,7 @@ placeholder="Resources" :modelValue="selectedResources" :items="resources" - :rules="[() => !!selectedResources || 'This field is required']" + :rules="[() => !!selectedResources || 'This field is required']" class="mt-3" @update:modelValue="selectedResources = $event" /> @@ -81,7 +79,10 @@ placeholder="Resources" :modelValue="workerSelResources" :items="workerResources" - :rules="[() => !!workerSelResources || 'This field is required']" + :rules="[ + () => + !!workerSelResources || 'This field is required', + ]" class="my-3" @update:modelValue="workerSelResources = $event" /> @@ -107,11 +108,11 @@ worker.resources }} @@ -122,7 +123,7 @@ @click="showInputs = true" class="d-flex ml-auto text-capitalize text-primary" > - + mdi-plus Add new worker @@ -166,13 +167,10 @@ {{ item.master.sru }}GB {{ item.master.mru }}GB {{ item.master.cru }} - + {{ item.master.ygg_ip }} - @@ -187,25 +185,27 @@ - - + > + mdi-delete + + - + >mdi-eye @@ -356,7 +356,7 @@ export default { title: "Mycelium IP", key: "mycelium_ip", sortable: false, - } + }, ]); const resources = ref([ { title: "Small K8s (1 CPU, 2GB, 25GB)", value: "small" }, @@ -402,8 +402,8 @@ export default { checked.value = false; selectedResources.value = ""; workerSelResources.value = ""; - workerName.value = ""; - savedWorkers.value = []; + workerName.value = ""; + savedWorkers.value = []; }; const deployK8s = () => { diff --git a/client/src/views/LandingPage.vue b/client/src/views/LandingPage.vue index 569692ca..fb22048f 100644 --- a/client/src/views/LandingPage.vue +++ b/client/src/views/LandingPage.vue @@ -1,80 +1,127 @@ - diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index 0a1b2d8c..95eb395e 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -1,170 +1,133 @@ - - diff --git a/client/src/views/Maintenance.vue b/client/src/views/Maintenance.vue index 837e92b9..964a2c58 100644 --- a/client/src/views/Maintenance.vue +++ b/client/src/views/Maintenance.vue @@ -1,34 +1,12 @@ - diff --git a/client/src/views/Newpassword.vue b/client/src/views/Newpassword.vue index da9c5eec..8478f715 100644 --- a/client/src/views/Newpassword.vue +++ b/client/src/views/Newpassword.vue @@ -1,180 +1,115 @@ - diff --git a/client/src/views/NextLaunch.vue b/client/src/views/NextLaunch.vue index 660e36dc..725c8e5c 100644 --- a/client/src/views/NextLaunch.vue +++ b/client/src/views/NextLaunch.vue @@ -1,35 +1,16 @@ - - - \ No newline at end of file + diff --git a/client/src/views/Otp.vue b/client/src/views/Otp.vue index 1e4547ba..fd7a2c67 100644 --- a/client/src/views/Otp.vue +++ b/client/src/views/Otp.vue @@ -1,231 +1,147 @@ - - diff --git a/client/src/views/PageNotFound.vue b/client/src/views/PageNotFound.vue index 8ec1ab77..ae3911ce 100644 --- a/client/src/views/PageNotFound.vue +++ b/client/src/views/PageNotFound.vue @@ -1,47 +1,27 @@ - - diff --git a/client/src/views/Profile.vue b/client/src/views/Profile.vue deleted file mode 100644 index aba6d062..00000000 --- a/client/src/views/Profile.vue +++ /dev/null @@ -1,338 +0,0 @@ - - - - - diff --git a/client/src/views/Signup.vue b/client/src/views/Signup.vue index cbcb8b4b..24ee0a4f 100644 --- a/client/src/views/Signup.vue +++ b/client/src/views/Signup.vue @@ -1,392 +1,206 @@ - - diff --git a/client/src/views/VM.vue b/client/src/views/VM.vue index 59abbd7b..42025b86 100644 --- a/client/src/views/VM.vue +++ b/client/src/views/VM.vue @@ -1,367 +1,246 @@ - diff --git a/client/src/views/tabs/AuditLogs.vue b/client/src/views/tabs/AuditLogs.vue new file mode 100644 index 00000000..3dd97eb0 --- /dev/null +++ b/client/src/views/tabs/AuditLogs.vue @@ -0,0 +1,84 @@ + + + diff --git a/client/src/views/tabs/ChangePassword.vue b/client/src/views/tabs/ChangePassword.vue new file mode 100644 index 00000000..1dcc9509 --- /dev/null +++ b/client/src/views/tabs/ChangePassword.vue @@ -0,0 +1,126 @@ + + diff --git a/client/src/views/tabs/DeleteAccount.vue b/client/src/views/tabs/DeleteAccount.vue new file mode 100644 index 00000000..a932b287 --- /dev/null +++ b/client/src/views/tabs/DeleteAccount.vue @@ -0,0 +1,70 @@ + + diff --git a/client/src/views/tabs/Invoices.vue b/client/src/views/tabs/Invoices.vue new file mode 100644 index 00000000..4f297079 --- /dev/null +++ b/client/src/views/tabs/Invoices.vue @@ -0,0 +1,167 @@ + + + diff --git a/client/src/views/tabs/Payments.vue b/client/src/views/tabs/Payments.vue new file mode 100644 index 00000000..c2be4af0 --- /dev/null +++ b/client/src/views/tabs/Payments.vue @@ -0,0 +1,52 @@ + + diff --git a/client/src/views/tabs/Profile.vue b/client/src/views/tabs/Profile.vue new file mode 100644 index 00000000..b06721da --- /dev/null +++ b/client/src/views/tabs/Profile.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/client/vite.config.js b/client/vite.config.js index 8c9ca19c..dac241cb 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -1,47 +1,39 @@ -// Plugins -import vue from '@vitejs/plugin-vue' -import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' +import vue from "@vitejs/plugin-vue"; +import vuetify, { transformAssetUrls } from "vite-plugin-vuetify"; +import { defineConfig, loadEnv } from "vite"; +import { fileURLToPath, URL } from "node:url"; +import eslintPlugin from "vite-plugin-eslint"; -// Utilities -import { defineConfig } from 'vite' -import { fileURLToPath, URL } from 'node:url' -import eslintPlugin from 'vite-plugin-eslint' +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); -// https://vitejs.dev/config/ -export default defineConfig({ + return { plugins: [ - [eslintPlugin({ cache: false })], - vue({ - template: { transformAssetUrls }, - }), - // https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin - vuetify({ - autoImport: true, - styles: { - configFile: 'src/styles/settings.scss', - }, - }), + [eslintPlugin({ cache: false })], + vue({ + template: { transformAssetUrls }, + }), + vuetify({ + autoImport: true, + styles: { + configFile: "src/styles/settings.scss", + }, + }), ], - define: { - 'process.env': {}, - __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true + define: { + "process.env": { + PUBLISHABLE_KEY: env.STRIPE_PUBLISHER_KEY, + }, + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true, }, resolve: { - alias: { - '@': fileURLToPath(new URL('./src', - import.meta.url)) - }, - extensions: [ - '.js', - '.json', - '.jsx', - '.mjs', - '.ts', - '.tsx', - '.vue', - ], + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"], }, server: { - port: 8080, + port: 8080, }, -}) \ No newline at end of file + }; +}); diff --git a/client/yarn.lock b/client/yarn.lock index bcdba6de..3b1e681d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -74,44 +74,6 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.54.0.tgz#4fab9a2ff7860082c304f750e94acd644cf984cf" integrity sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ== -"@fortawesome/fontawesome-common-types@6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" - integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA== - -"@fortawesome/fontawesome-svg-core@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz#37f4507d5ec645c8b50df6db14eced32a6f9be09" - integrity sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg== - dependencies: - "@fortawesome/fontawesome-common-types" "6.4.2" - -"@fortawesome/free-brands-svg-icons@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz#9b8e78066ea6dd563da5dfa686615791d0f7cc71" - integrity sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg== - dependencies: - "@fortawesome/fontawesome-common-types" "6.4.2" - -"@fortawesome/free-regular-svg-icons@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz#aee79ed76ce5dd04931352f9d83700761b8b1b25" - integrity sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q== - dependencies: - "@fortawesome/fontawesome-common-types" "6.4.2" - -"@fortawesome/free-solid-svg-icons@^6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz#33a02c4cb6aa28abea7bc082a9626b7922099df4" - integrity sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA== - dependencies: - "@fortawesome/fontawesome-common-types" "6.4.2" - -"@fortawesome/vue-fontawesome@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz#1e8032df151173d8174ac9f5a28da3c0f5a495e4" - integrity sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw== - "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -170,6 +132,16 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@stripe/stripe-js@^1.13.2": + version "1.54.2" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.2.tgz#0665848e22cbda936cfd05256facdfbba121438d" + integrity sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg== + +"@stripe/stripe-js@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-5.4.0.tgz#847e870ddfe9283432526867857a4c1fba9b11ed" + integrity sha512-3tfMbSvLGB+OsJ2MsjWjWo+7sp29dwx+3+9kG/TEnZQJt+EwbF/Nomm43cSK+6oXZA9uhspgyrB+BbrPRumx4g== + "@types/eslint@^8.4.5": version "8.37.0" resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz" @@ -198,6 +170,14 @@ resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz" integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw== +"@vue-stripe/vue-stripe@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@vue-stripe/vue-stripe/-/vue-stripe-4.5.0.tgz#2297b3a3f7cc984e7c80f3b7dac75f5e0239a758" + integrity sha512-BU449XT5zegjNQirl+SSztbzGIvPjhxlHv8ybomSZcI1jB6qEpLgpk2eHMFDKnOGZZRhqtg4C5FiErwSJ/yuRw== + dependencies: + "@stripe/stripe-js" "^1.13.2" + vue-coerce-props "^1.0.0" + "@vue/compiler-core@3.5.13": version "3.5.13" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05" @@ -240,7 +220,7 @@ "@vue/compiler-dom" "3.5.13" "@vue/shared" "3.5.13" -"@vue/devtools-api@^6.6.4": +"@vue/devtools-api@^6.6.3", "@vue/devtools-api@^6.6.4": version "6.6.4" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== @@ -442,6 +422,11 @@ core-js@^3.33.2: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.2.tgz#312bbf6996a3a517c04c99b9909cdd27138d1ceb" integrity sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -761,6 +746,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -890,6 +880,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immutable@^4.0.0: version "4.2.4" resolved "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz" @@ -916,7 +911,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -957,6 +952,11 @@ is-path-inside@^3.0.3: resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -979,6 +979,16 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" @@ -987,6 +997,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" @@ -1138,6 +1155,11 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -1180,6 +1202,14 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pinia@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.3.1.tgz#54c476675b72f5abcfafa24a7582531ea8c23d94" + integrity sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug== + dependencies: + "@vue/devtools-api" "^6.6.3" + vue-demi "^0.14.10" + pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" @@ -1218,6 +1248,11 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" @@ -1233,6 +1268,19 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -1285,6 +1333,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + sass@^1.69.4: version "1.69.4" resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.4.tgz#10c735f55e3ea0b7742c6efa940bce30e07fbca2" @@ -1304,6 +1357,11 @@ semver@^7.3.6, semver@^7.6.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -1326,6 +1384,13 @@ source-map-js@^1.2.0, source-map-js@^1.2.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -1386,7 +1451,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.2: +util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -1421,6 +1486,16 @@ vite@^3.1.9: optionalDependencies: fsevents "~2.3.2" +vue-coerce-props@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vue-coerce-props/-/vue-coerce-props-1.0.0.tgz#af3d00aa419f0de79c2b020824c6c0387070d66e" + integrity sha512-4fdRMXO6FHzmE7H4soAph6QmPg3sL/RiGdd+axuxuU07f02LNMns0jMM88fmt1bvSbN+2Wyd8raho6p6nXUzag== + +vue-demi@^0.14.10: + version "0.14.10" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04" + integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg== + vue-eslint-parser@^9.4.3: version "9.4.3" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz#9b04b22c71401f1e8bca9be7c3e3416a4bde76a8" 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..89f5c414 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,145 @@ 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 + } + + if err := a.logVMsPriceUpdate(req, a.config.PricesPerMonth); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + 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 +250,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 +275,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 +369,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 +387,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,70 +412,38 @@ 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)) } } - return ResponseMsg{ - Message: "Deployments are deleted successfully", - }, 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 { + if err := a.logAllDeploymentsDelete(req); 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}, + Message: "Deployments are deleted successfully", }, 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) @@ -263,32 +462,33 @@ func (a *App) UpdateMaintenanceHandler(req *http.Request) (interface{}, Response return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - return ResponseMsg{ - Message: "Maintenance is updated successfully", - Data: nil, - }, 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 { + if err := a.logMaintenanceUpdate(req, input.ON); 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, + Message: "Maintenance is updated successfully", + Data: nil, }, 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 { @@ -328,60 +528,32 @@ func (a *App) SetAdmin(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logAdminSet(req, user.ID.String(), input.Admin); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is updated successfully", }, 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,15 +577,15 @@ 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) + err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() 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() @@ -421,13 +593,32 @@ func (a *App) CreateNewAnnouncement(req *http.Request) (interface{}, Response) { } } + if err := a.logAnnouncementCreate(req, adminAnnouncement.Subject); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "new announcement is sent successfully", }, 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,9 +644,9 @@ 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) + err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -468,12 +659,31 @@ func (a *App) SendEmail(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logEmailSent(req, user.ID.String(), emailUser.Subject); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "new email is sent successfully", }, Created() } // 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) @@ -492,27 +702,60 @@ func (a *App) UpdateNextLaunchHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logNextLaunchUpdate(req, input.Launched); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Next Launch is updated successfully", Data: nil, }, 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 = a.mailer.SendMail(a.config.MailSender.Email, 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 = a.mailer.SendMail(a.config.MailSender.Email, 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..85eb2f47 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" ) @@ -23,6 +26,7 @@ type App struct { db models.DB redis streams.RedisClient deployer c4sDeployer.Deployer + mailer internal.Mailer } // NewApp creates new server app all configurations @@ -32,6 +36,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 +61,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 } @@ -68,6 +74,7 @@ func NewApp(ctx context.Context, configFile string) (app *App, err error) { db: db, redis: redis, deployer: newDeployer, + mailer: internal.NewMailer(config.MailSender.SendGridKey), }, nil } @@ -83,6 +90,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 +106,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 +116,14 @@ 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() + logRouter := userRouter.PathPrefix("/log").Subrouter() + eventRouter := userRouter.PathPrefix("/event").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,19 +141,38 @@ 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") + + cardRouter.HandleFunc("", WrapFunc(a.AddCardHandler)).Methods("POST", "OPTIONS") + cardRouter.HandleFunc("/{id}", WrapFunc(a.DeleteCardHandler)).Methods("DELETE", "OPTIONS") + cardRouter.HandleFunc("", WrapFunc(a.ListCardHandler)).Methods("GET", "OPTIONS") + cardRouter.HandleFunc("/default", WrapFunc(a.SetDefaultCardHandler)).Methods("PUT", "OPTIONS") - quotaRouter.HandleFunc("", WrapFunc(a.GetQuotaHandler)).Methods("GET", "OPTIONS") + logRouter.HandleFunc("", WrapFunc(a.ListLogsHandler)).Methods("GET", "OPTIONS") + + eventRouter.HandleFunc("", WrapFunc(a.ListEventsHandler)).Methods("GET", "OPTIONS") + + invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("/download/{id}", WrapFunc(a.DownloadInvoiceHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("/pay/{id}", WrapFunc(a.PayInvoiceHandler)).Methods("PUT", "OPTIONS") notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS") + notificationRouter.HandleFunc("/stream", a.sseNotificationsHandler).Methods("GET", "OPTIONS") notificationRouter.HandleFunc("/{id}", WrapFunc(a.UpdateNotificationsHandler)).Methods("PUT", "OPTIONS") + notificationRouter.HandleFunc("", WrapFunc(a.SeenNotificationsHandler)).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") @@ -156,27 +193,30 @@ 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) r.Use(middlewares.EnableCors) authRouter.Use(middlewares.Authorization(a.db, a.config.Token.Secret, a.config.Token.Timeout)) + authRouter.Use(middlewares.AuditLogMiddleware(a.db)) adminRouter.Use(middlewares.AdminAccess(a.db)) // prometheus registration diff --git a/server/app/audit_handler.go b/server/app/audit_handler.go new file mode 100644 index 00000000..6c49ef83 --- /dev/null +++ b/server/app/audit_handler.go @@ -0,0 +1,78 @@ +package app + +import ( + "errors" + "net/http" + + "github.com/codescalers/cloud4students/middlewares" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +// Example endpoint: List user's logs +// @Summary List user's logs +// @Description List user's logs +// @Tags Audit +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.AuditLog +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/log [get] +func (a *App) ListLogsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + logs, err := a.db.GetUserLogs(userID) + if err == gorm.ErrRecordNotFound || len(logs) == 0 { + return ResponseMsg{ + Message: "no logs found", + Data: logs, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Logs are found", + Data: logs, + }, Ok() +} + +// Example endpoint: List user's events +// @Summary List user's events +// @Description List user's events +// @Tags Audit +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.AuditEvent +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/event [get] +func (a *App) ListEventsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + events, err := a.db.GetUserEvents(userID) + if err == gorm.ErrRecordNotFound || len(events) == 0 { + return ResponseMsg{ + Message: "no events found", + Data: events, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Events are found", + Data: events, + }, Ok() +} diff --git a/server/app/events.go b/server/app/events.go new file mode 100644 index 00000000..1cafe8f2 --- /dev/null +++ b/server/app/events.go @@ -0,0 +1,552 @@ +package app + +import ( + "fmt" + "net/http" + "time" + + "github.com/codescalers/cloud4students/internal" + "github.com/codescalers/cloud4students/middlewares" + "github.com/codescalers/cloud4students/models" + "github.com/pkg/errors" +) + +type role string + +var ( + userRole role = "User" + adminRole role = "Admin" + systemRole role = "System" +) + +func (a *App) logUserDelete(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is deleted", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logBalanceCharge(userID, currency string, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_balance", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Balance is charged with %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserVoucherActivate(userID, currency, voucher string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "apply_voucher", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User activated a voucher %v with %v %v", voucher, balance, currency), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserVoucherApply(userID, currency string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "apply_voucher", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User applied for a voucher with %v %v", balance, currency), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserUpdate(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: "User data is updated successfully", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserPasswordUpdate(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_user_password", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: "Password is updated successfully", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserSignedIn(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "signin_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is signed in successfully", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserCreated(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is created", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherReset(userID string, voucherBalance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "reset_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher balance %v is reset", voucherBalance), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherUpdate(userID, voucher string, balance uint64, approved bool) error { + state := "Approved" + if approved { + state = "Rejected" + } + + event := models.AuditEvent{ + UserID: userID, + Action: "update_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher `%v` with balance %v, is %v", voucher, balance, state), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherCreate(userID, voucher string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher `%v` with balance %v, is created successfully", voucher, balance), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardDelete(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is deleted", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardDefaultSet(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "set_default_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is set as default", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardAdded(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "add_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is added", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logNotificationSeen(userID string, notificationID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "seen_notification", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Notification %v is seen", notificationID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherBalanceUpdate(userID, currency string, role role, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_voucher_balance", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Voucher balance is updated to %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logBalanceUpdate(userID, currency string, role role, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_balance", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Balance is updated to %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logK8sDelete(userID string, role role, k8sID int, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_k8s", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Kubernetes %v which created at %v, is deleted", k8sID, createdAt, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVMDelete(userID string, role role, vmID int, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_vm", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Virtual machine %v which created at %v, is deleted", + vmID, createdAt, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoiceCreate(userID, currency string, invoiceID int, invoiceTotal float64, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_invoice", + Role: string(systemRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Invoice %v with value: %v %v is created at %v", + invoiceID, invoiceTotal, currency, createdAt, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoicePayment(userID, currency string, invoiceTotal float64, paymentDetails models.PaymentDetails) error { + event := models.AuditEvent{ + UserID: userID, + Action: "pay_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Invoice %v with value: %v %v is paid using: {balance: %v, vouchers: %v, card:%v}", + paymentDetails.InvoiceID, invoiceTotal, currency, + paymentDetails.Balance, paymentDetails.VoucherBalance, paymentDetails.Card, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoicePDFUpdate(req *http.Request, invoiceID int) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Invoice %v pdf data is updated", invoiceID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoiceDownload(req *http.Request, invoiceID int) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "download_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Invoice %v is downloaded", invoiceID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logEmailSent(req *http.Request, targetUserID, subject string) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "send_email", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("An email is sent to: %v, with subject: %v", targetUserID, subject), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAnnouncementCreate(req *http.Request, subject string) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "create_announcement", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("An announcement is created with subject: %v", subject), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAdminSet(req *http.Request, adminID string, admin bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + metaData := fmt.Sprintf("A new admin %v is added", adminID) + if !admin { + metaData = fmt.Sprintf("An admin %v is removed", adminID) + } + + event := models.AuditEvent{ + UserID: userID, + Action: "set_admin", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: metaData, + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logNextLaunchUpdate(req *http.Request, on bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_next_launch", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Next launch value is updated to: %v", on), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logMaintenanceUpdate(req *http.Request, on bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_maintenance", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Maintenance value is updated to: %v", on), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAllDeploymentsDelete(req *http.Request) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "delete_all_deployments", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: "All virtual machines are deleted", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVMsPriceUpdate(req *http.Request, prices internal.Prices) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_vms_prices", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Virtual machines prices are updated {small: %v, medium: %v, large: %v, public IPs: %v}", + prices.SmallVM, prices.MediumVM, prices.LargeVM, prices.PublicIP, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} diff --git a/server/app/invoice_handler.go b/server/app/invoice_handler.go new file mode 100644 index 00000000..2a96c4e6 --- /dev/null +++ b/server/app/invoice_handler.go @@ -0,0 +1,759 @@ +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() +} + +// DownloadInvoiceHandler downloads user's invoice by ID +// Example endpoint: Downloads user's invoice by ID +// @Summary Downloads user's invoice by ID +// @Description Downloads user's invoice by ID +// @Tags Invoice +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "Invoice ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /invoice/download/{id} [get] +func (a *App) DownloadInvoiceHandler(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)) + } + + // Creating pdf for invoice if it doesn't have it + if len(invoice.FileData) == 0 { + 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)) + } + + pdfContent, err := internal.CreateInvoicePDF(invoice, user) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + invoice.FileData = pdfContent + if err := a.db.UpdateInvoicePDF(id, invoice.FileData); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if err := a.logInvoicePDFUpdate(req, invoice.ID); 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")) + } + + if err := a.logInvoiceDownload(req, invoice.ID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return invoice.FileData, Ok(). + WithHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID))). + WithHeader("Content-Type", "application/pdf") +} + +// 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, 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(user models.User, now time.Time) error { + monthStart := time.Date(now.Year(), now.Month(), 0, 0, 0, 0, 0, time.Local) + + vms, err := a.db.GetAllSuccessfulVms(user.ID.String()) + if err != nil && err != gorm.ErrRecordNotFound { + return err + } + + k8s, err := a.db.GetAllSuccessfulK8s(user.ID.String()) + 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, + DeploymentName: vm.Name, + DeploymentCreatedAt: vm.CreatedAt, + 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, + DeploymentName: cluster.Master.Name, + DeploymentCreatedAt: cluster.CreatedAt, + HasPublicIP: cluster.Master.Public, + PeriodInHours: time.Since(usageStart).Hours(), + Cost: cost, + }) + + total += cost + } + + if len(items) > 0 { + in := models.Invoice{ + UserID: user.ID.String(), + Total: total, + Deployments: items, + } + + // Creating pdf for invoice + pdfContent, err := internal.CreateInvoicePDF(in, user) + if err != nil { + return err + } + + in.FileData = pdfContent + + // Creating invoice in db + if err = a.db.CreateInvoice(&in); err != nil { + return err + } + + if err := a.logInvoiceCreate(user.ID.String(), a.config.Currency, in.ID, in.Total, in.CreatedAt); 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 err := a.logVMDelete(userID, systemRole, dl.DeploymentID, dl.DeploymentCreatedAt); 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() + } + + if err := a.logK8sDelete(userID, systemRole, dl.DeploymentID, dl.DeploymentCreatedAt); 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 before 7 days ago and + // invoice is not paid + // invoice.CreatedAt.Before(oneMonthsAgo) && + if 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) + if overDueDays > 0 { + 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 for being a valued customer." + + subject := "Unpaid Invoice Notification – Action Required" + subject, body := internal.AdminMailContent(subject, mailBody, a.config.Server.Host, userName) + + if err = a.mailer.SendMail( + a.config.MailSender.Email, userEmail, subject, body, internal.Attachment{ + FileName: fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID), + Data: invoice.FileData, + }, + ); 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)) + } + + if err := a.logVoucherBalanceUpdate(user.ID.String(), a.config.Currency, systemRole, 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)) + } + + if err := a.logBalanceUpdate(user.ID.String(), a.config.Currency, systemRole, 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)) + } + + if err := a.logInvoicePayment(user.ID.String(), a.config.Currency, invoiceTotal, paymentDetails); 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..ea2c4a51 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() @@ -189,6 +324,11 @@ func (a *App) K8sDeleteHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logK8sDelete(userID, userRole, cluster.ID, cluster.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // metrics middlewares.Deletions.WithLabelValues(userID, "k8s").Inc() @@ -199,6 +339,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) @@ -229,6 +382,11 @@ func (a *App) K8sDeleteAllHandler(req *http.Request) (interface{}, Response) { } for _, c := range clusters { + if err := a.logK8sDelete(userID, userRole, c.ID, c.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(c.UserID, "k8s").Inc() } diff --git a/server/app/notification_handler.go b/server/app/notification_handler.go index e820095d..3c9e19c4 100644 --- a/server/app/notification_handler.go +++ b/server/app/notification_handler.go @@ -2,9 +2,11 @@ package app import ( + "encoding/json" "errors" "net/http" "strconv" + "time" "github.com/codescalers/cloud4students/middlewares" "github.com/gorilla/mux" @@ -12,7 +14,87 @@ import ( "gorm.io/gorm" ) +// 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) { + 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 notification id")) + } + + err = a.db.UpdateNotification(id, true) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + if err := a.logNotificationSeen(userID, id); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Notifications are updated", + Data: nil, + }, Ok() +} + +// SeenNotificationsHandler updates notifications for a user to be seen +// 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 +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /notification [put] +func (a *App) SeenNotificationsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + err := a.db.UpdateUserNotification(userID, true) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Notifications are seen", + Data: nil, + }, Ok() +} + // 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) @@ -35,22 +117,73 @@ func (a *App) ListNotificationsHandler(req *http.Request) (interface{}, Response }, Ok() } -// UpdateNotificationsHandler updates notifications for a user -func (a *App) UpdateNotificationsHandler(req *http.Request) (interface{}, Response) { - id, err := strconv.Atoi(mux.Vars(req)["id"]) - if err != nil { - log.Error().Err(err).Send() - return nil, BadRequest(errors.New("failed to read notification id")) +// sseNotificationsHandler to stream notifications +// Example endpoint: Stream user's notifications +// @Summary Stream user's notifications +// @Description Stream user's notifications +// @Tags Notification +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.Notification +// @Failure 401 {object} Response +// @Failure 500 {object} Response +// @Router /notification/stream [get] +func (a *App) sseNotificationsHandler(w http.ResponseWriter, req *http.Request) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + // Flush the headers immediately + flusher, ok := w.(http.Flusher) + if !ok { + log.Error().Msg("Streaming unsupported") + internalServerError(w) + return } - err = a.db.UpdateNotification(id, true) - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) + // Sending notifications every 5 seconds + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + notifications, err := a.db.GetNewNotifications(userID) + if err != nil { + log.Error().Err(err).Send() + internalServerError(w) + return + } + + // Send each notification as a separate SSE message + for _, notification := range notifications { + if _, err := w.Write([]byte(notification.Msg)); err != nil { + log.Error().Err(err).Send() + internalServerError(w) + return + } + flusher.Flush() // Ensure the event is sent immediately + } + + case <-req.Context().Done(): + w.WriteHeader(http.StatusOK) + return + } } +} - return ResponseMsg{ - Message: "Notifications are updated", - Data: nil, - }, Ok() +func internalServerError(w http.ResponseWriter) { + w.WriteHeader(http.StatusInternalServerError) + object := struct { + Error string `json:"err"` + }{ + Error: "Internal server error", + } + + if err := json.NewEncoder(w).Encode(object); err != nil { + log.Error().Err(err).Msg("failed to encode return object") + } } diff --git a/server/app/payments_handler.go b/server/app/payments_handler.go new file mode 100644 index 00000000..1481e6c7 --- /dev/null +++ b/server/app/payments_handler.go @@ -0,0 +1,424 @@ +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 err := a.logCardAdded(userID, paymentMethod.Card.Last4); 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)) + } + + if err := a.logCardDefaultSet(userID, paymentMethod.Card.Last4); 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)) + } + + if err := a.logCardDefaultSet(userID, card.Last4); 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")) + } + + // TODO: deleting vms before the end of the month then deleting all cards case + + // 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 err := a.logCardDefaultSet(userID, card.Last4); 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)) + } + + if err := a.logCardDelete(userID, card.Last4); 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..ec6215de 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{ @@ -97,6 +106,7 @@ func SetUp(t testing.TB) *App { db: db, redis: streams.RedisClient{}, deployer: newDeployer, + mailer: internal.NewMailer(configuration.MailSender.SendGridKey), } return 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..93750aea 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,8 +143,8 @@ 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) - err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, signUp.Email, subject, body) + subject, body := internal.SignUpMailContent(code, a.config.MailSender.Timeout, fmt.Sprintf("%s %s", signUp.FirstName, signUp.LastName), a.config.Server.Host) + err = a.mailer.SendMail(a.config.MailSender.Email, signUp.Email, subject, body) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -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,34 +236,44 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) if user.UpdatedAt.Add(time.Duration(a.config.MailSender.Timeout) * time.Second).Before(time.Now()) { return nil, BadRequest(errors.New("code has expired")) } - err = a.db.UpdateVerification(user.ID.String(), true) + err = a.db.UpdateUserVerification(user.ID.String(), true) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - middlewares.UserCreations.WithLabelValues(user.ID.String(), user.Email, user.College, fmt.Sprint(user.TeamSize)).Inc() + middlewares.UserCreations.WithLabelValues(user.ID.String(), user.Email).Inc() - // token - token, err := internal.CreateJWT(user.ID.String(), user.Email, a.config.Token.Secret, a.config.Token.Timeout) + subject, body := internal.WelcomeMailContent(user.Name(), a.config.Server.Host) + err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - 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 { + if err := a.logUserCreated(user.ID.String()); err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } 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) @@ -267,13 +306,31 @@ func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserSignedIn(user.ID.String()); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + 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 +358,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 +372,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,8 +413,8 @@ 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) - err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, email.Email, subject, body) + subject, body := internal.ResetPasswordMailContent(code, a.config.MailSender.Timeout, user.Name(), a.config.Server.Host) + err = a.mailer.SendMail(a.config.MailSender.Email, email.Email, subject, body) if err != nil { log.Error().Err(err).Send() @@ -365,13 +435,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,12 +491,28 @@ 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) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + var data ChangePasswordInput err := json.NewDecoder(req.Body).Decode(&data) if err != nil { @@ -437,7 +536,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")) } @@ -446,6 +545,11 @@ func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserPasswordUpdate(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Password is updated successfully", Data: nil, @@ -453,9 +557,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 +611,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 +634,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(), @@ -526,13 +649,30 @@ func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserUpdate(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + 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 +691,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 +730,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 +741,34 @@ 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() + + if err := a.logUserVoucherApply(userID, a.config.Currency, a.config.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } 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 +779,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 +797,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 +815,328 @@ 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)) } - middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc() + + for _, invoice := range invoices { + response := a.payInvoice(&user, "", voucherAndBalance, invoice.Total, invoice.ID) + if response.Err() != nil { + log.Error().Err(response.Err()).Send() + } + } + + if err := a.logUserVoucherActivate(userID, a.config.Currency, voucherBalance.Voucher, voucherBalance.Balance); 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)) + } + + if err := a.logVoucherBalanceUpdate(userID, a.config.Currency, userRole, user.VoucherBalance); 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)) + } + + if err := a.logBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + middlewares.VoucherActivated.WithLabelValues(userID, voucherBalance.Voucher, fmt.Sprint(voucherBalance.Balance)).Inc() return ResponseMsg{ Message: "Voucher is applied successfully", Data: nil, }, Ok() } + +// Example endpoint: Charge user balance +// @Summary Charge user balance +// @Description Charge user balance +// @Tags User +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param balance body ChargeBalance true "Balance charging details" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/charge_balance [put] +func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + var input ChargeBalance + err := json.NewDecoder(req.Body).Decode(&input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("failed to read input data")) + } + + err = validator.Validate(input) + if err != nil { + log.Error().Err(err).Send() + return nil, BadRequest(errors.New("invalid input data")) + } + + user, err := a.db.GetUserByID(userID) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + 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) + + if err := a.logBalanceCharge(userID, a.config.Currency, input.Amount); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + // 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)) + } + + if err := a.logBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); 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)) + } + + if err := a.logVoucherBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); 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) { + // TODO: delete customer from stripe + 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(user, 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)) + } + + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + 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 err := a.logK8sDelete(userID, userRole, cluster.ID, cluster.CreatedAt); err != nil { + 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)) + } + + if err := a.logCardDelete(userID, card.Last4); 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)) + } + + if err := a.logUserDelete(userID); 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..5d894c4f 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() @@ -192,6 +294,11 @@ func (a *App) DeleteVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(userID, "vms").Inc() return ResponseMsg{ Message: "Virtual machine is deleted successfully", @@ -200,6 +307,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) @@ -230,6 +350,11 @@ func (a *App) DeleteAllVMsHandler(req *http.Request) (interface{}, Response) { // metrics for _, vm := range vms { + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(vm.UserID, "vms").Inc() } @@ -239,3 +364,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..92051728 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,8 +68,7 @@ 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 { + if err := a.logVoucherCreate(v.UserID, v.Voucher, v.Balance); err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } @@ -69,6 +80,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 +113,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,17 +176,22 @@ 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) + err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherUpdate(voucher.UserID, voucher.Voucher, voucher.Balance, input.Approved); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Update mail has been sent to the user", Data: nil, @@ -156,6 +199,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,12 +237,17 @@ 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) - err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body) + subject, body := internal.ApprovedVoucherMailContent(v.Voucher, user.Name(), a.config.Server.Host) + err = a.mailer.SendMail(a.config.MailSender.Email, user.Email, subject, body) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherUpdate(v.UserID, v.Voucher, v.Balance, true); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } return ResponseMsg{ @@ -196,3 +255,43 @@ 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)) + } + + if err := a.logVoucherReset(user.ID.String(), user.VoucherBalance); 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..7c34e4cc 100644 --- a/server/app/wrapper.go +++ b/server/app/wrapper.go @@ -69,7 +69,11 @@ func WrapFunc(a Handler) http.HandlerFunc { status = result.Status() } - if err := json.NewEncoder(w).Encode(object); err != nil { + if bytes, ok := object.([]byte); ok { + if _, err := w.Write(bytes); err != nil { + log.Error().Err(err).Msg("failed to write return object") + } + } else if err := json.NewEncoder(w).Encode(object); err != nil { log.Error().Err(err).Msg("failed to encode return object") } middlewares.Requests.WithLabelValues(r.Method, r.RequestURI, fmt.Sprint(status)).Inc() @@ -137,7 +141,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..51d9b43c 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,156 @@ 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 +} + +func logVMCreate(db models.DB, userID string, vmID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_vm", + Role: "User", + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Virtual machine %v is created successfully", vmID), + } + + if err := db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func logK8sCreate(db models.DB, userID string, k8sID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_k8s", + Role: "User", + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Kubernetes %v is created successfully", k8sID), + } + + if err := db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return 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..ab6fb980 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,57 @@ 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) + err = d.db.UpdateK8s(k8s) + if err != nil { + log.Error().Err(err).Send() + return models.K8sCluster{}, err } - k8sCluster := models.K8sCluster{ - UserID: userID, - NetworkContract: int(networkContractID), - ClusterContract: int(k8sContractID), - Master: master, - Workers: workers, + + if err := logK8sCreate(d.db, k8s.UserID, k8s.ID); 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 +205,70 @@ func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sDeployIn FreeSRU: freeSRU, FreeIPs: &ips, FarmIDs: []uint64{1}, - IPv6: &trueVal, + IPv4: &k.Master.Public, } - nodes, err := deployer.FilterNodes(ctx, d.tfPluginClient, filter, []uint64{*freeSRU}, nil, rootfs, 1) - if err != nil { - return 0, err + if len(strings.TrimSpace(k.Master.Region)) != 0 { + filter.Region = &k.Master.Region } - return uint32(nodes[0].NodeID), nil -} - -// ValidateK8sQuota validates the quota a k8s deployment need -func ValidateK8sQuota(k models.K8sDeployInput, availableResourcesQuota, availablePublicIPsQuota int) (int, error) { - neededQuota, err := calcNeededQuota(k.Resources) + nodes, err := deployer.FilterNodes(ctx, d.TFPluginClient, filter, []uint64{*freeSRU}, nil, rootfs, 1) if err != nil { return 0, err } - for _, worker := range k.Workers { - workerQuota, err := calcNeededQuota(worker.Resources) - if err != nil { - return 0, err - } - neededQuota += workerQuota - } - - if availableResourcesQuota < neededQuota { - return 0, fmt.Errorf("no available quota %d for kubernetes deployment, you can request a new voucher", availableResourcesQuota) - } - if k.Public && availablePublicIPsQuota < publicQuota { - return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) - } - - return neededQuota, nil + return uint32(nodes[0].NodeID), nil } -func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sDeployInput, adminSSHKey string) (int, error) { - // quota verification - quota, err := d.db.GetUserQuota(user.ID.String()) - if err == gorm.ErrRecordNotFound { - log.Error().Err(err).Send() - return http.StatusNotFound, errors.New("user quota is not found") +func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sCluster, adminSSHKey string) (int, error, error) { + _, err := d.CanDeployK8s(user.ID.String(), k8sDeployInput) + if errors.Is(err, ErrCannotDeploy) { + return http.StatusBadRequest, err, err } if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - - neededQuota, err := ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs) - if err != nil { - log.Error().Err(err).Send() - return http.StatusBadRequest, err + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } // deploy network and cluster node, networkContractID, k8sContractID, err := d.deployK8sClusterWithNetwork(ctx, k8sDeployInput, user.SSHKey, adminSSHKey) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - k8sCluster, err := d.loadK8s(ctx, k8sDeployInput, user.ID.String(), node, networkContractID, k8sContractID) + k8sCluster, err := d.loadK8s(ctx, k8sDeployInput, node, networkContractID, k8sContractID) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - publicIPsQuota := quota.PublicIPs - if k8sDeployInput.Public { - publicIPsQuota -= publicQuota - } - // update quota - err = d.db.UpdateUserQuota(user.ID.String(), quota.Vms-neededQuota, publicIPsQuota) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota is not found") - } - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - err = d.db.CreateK8s(&k8sCluster) + err = d.db.UpdateK8s(k8sCluster) if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } // metrics - middlewares.Deployments.WithLabelValues(user.ID.String(), k8sDeployInput.Resources, "master").Inc() + middlewares.Deployments.WithLabelValues(user.ID.String(), k8sDeployInput.Master.Resources, "master").Inc() for _, worker := range k8sDeployInput.Workers { middlewares.Deployments.WithLabelValues(user.ID.String(), worker.Resources, "worker").Inc() } - return 0, nil + return 0, nil, nil } -func convertGBToBytes(gb uint64) *uint64 { - bytes := gb * 1024 * 1024 * 1024 - return &bytes +// CanDeployK8s checks if user can deploy kubernetes +func (d *Deployer) CanDeployK8s(userID string, k8s models.K8sCluster) (float64, error) { + k8sPrice, err := calcPrice(d.prices, k8s.Master.Resources, k8s.Master.Public) + if err != nil { + return 0, errors.Wrap(err, "failed to calculate kubernetes master price") + } + + for _, worker := range k8s.Workers { + workerPrice, err := calcPrice(d.prices, worker.Resources, worker.Public) + if err != nil { + return 0, errors.Wrap(err, "failed to calculate kubernetes worker price") + } + + k8sPrice += workerPrice + } + + return k8sPrice, d.canDeploy(userID, k8sPrice) } diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index 8c8ce561..8cff1a91 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,60 @@ 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 nil, 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 + return &loadedDl.Vms[0], loadedDl.ContractID, loadedNet.NodeDeploymentID[nodeID], nil } -// ValidateVMQuota validates the quota a vm deployment need -func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota, availablePublicIPsQuota int) (int, error) { - neededQuota, err := calcNeededQuota(vm.Resources) +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 { - return 0, err + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - 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) + deployedVM, contractID, networkContractID, err := d.deployVM(ctx, vm, user.SSHKey, adminSSHKey) + if err != nil { + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } - return neededQuota, nil -} + // 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 -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") - } + 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) } - neededQuota, err := ValidateVMQuota(input, quota.Vms, quota.PublicIPs) - if err != nil { - return http.StatusBadRequest, err + if err := logVMCreate(d.db, vm.UserID, vm.ID); err != nil { + 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) - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } + middlewares.Deployments.WithLabelValues(user.ID.String(), vm.Resources, "vm").Inc() + return 0, nil, nil +} - 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") - } +// 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..55475f46 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,3942 @@ +// 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/download/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Downloads user's invoice by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Invoice" + ], + "summary": "Downloads user's invoice by ID", + "parameters": [ + { + "type": "string", + "description": "Invoice 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": {} + } + } + } + }, + "/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": {} + } + } + }, + "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", + "responses": { + "200": { + "description": "OK", + "schema": {} + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/notification/stream": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Stream user's notifications", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Stream user's notifications", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Notification" + } + } + }, + "401": { + "description": "Unauthorized", + "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/event": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List user's events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditEvent" + } + } + }, + "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/log": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's logs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List user's logs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditLog" + } + } + }, + "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.AuditEvent": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "metadata": { + "type": "string" + }, + "role": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.AuditLog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "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_created_at": { + "type": "string" + }, + "deployment_id": { + "type": "integer" + }, + "deployment_name": { + "type": "string" + }, + "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" + } + }, + "file_data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + }, + "last_reminder_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", + "notified", + "seen", + "type", + "user_id" + ], + "properties": { + "id": { + "type": "integer" + }, + "msg": { + "type": "string" + }, + "notified": { + "type": "boolean" + }, + "seen": { + "type": "boolean" + }, + "type": { + "description": "to allow redirecting from notifications to the right pages\nfor example if the type is ` + "`" + `vm` + "`" + ` it will be redirected to the vm page", + "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..bac1b9c9 --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,2616 @@ +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.AuditEvent: + properties: + action: + type: string + id: + type: integer + metadata: + type: string + role: + type: string + timestamp: + type: string + user_id: + type: string + type: object + models.AuditLog: + properties: + id: + type: integer + method: + type: string + status_code: + type: integer + success: + type: boolean + timestamp: + type: string + url: + type: string + user_id: + type: string + type: object + 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_created_at: + type: string + deployment_id: + type: integer + deployment_name: + type: string + 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 + file_data: + items: + type: integer + type: array + id: + type: integer + last_reminder_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 + notified: + type: boolean + seen: + type: boolean + type: + description: |- + to allow redirecting from notifications to the right pages + for example if the type is `vm` it will be redirected to the vm page + type: string + user_id: + type: string + required: + - msg + - notified + - 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/download/{id}: + get: + consumes: + - application/json + description: Downloads 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: {} + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Downloads user's invoice by ID + tags: + - Invoice + /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 + put: + consumes: + - application/json + description: Set user's notifications as seen + 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 + /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 + /notification/stream: + get: + consumes: + - application/json + description: Stream user's notifications + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Notification' + type: array + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: Stream user's notifications + 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/event: + get: + consumes: + - application/json + description: List user's events + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.AuditEvent' + 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 events + tags: + - Audit + /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/log: + get: + consumes: + - application/json + description: List user's logs + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.AuditLog' + 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 logs + tags: + - Audit + /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..c77dac94 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 @@ -12,12 +15,17 @@ require ( github.com/prometheus/client_golang v1.20.5 github.com/rs/zerolog v1.33.0 github.com/sendgrid/sendgrid-go v3.16.0+incompatible + github.com/signintech/gopdf v0.29.0 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 + github.com/urfave/negroni/v3 v3.1.1 + 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 +33,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 +46,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 +64,26 @@ 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/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // 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 +91,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..0757d794 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,10 +135,13 @@ 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/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no= +github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -138,8 +155,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= @@ -152,14 +169,25 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/signintech/gopdf v0.29.0 h1:ZwnHKvdgBtl1C2DUmbC9a29RCtQTehb11v/Z9w8xb3s= +github.com/signintech/gopdf v0.29.0/go.mod h1:d23eO35GpEliSrF22eJ4bsM3wVeQJTjXTHq5x5qGKjA= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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= @@ -176,28 +204,42 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= +github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= 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 +249,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..bc1ada62 100644 --- a/server/internal/email_sender.go +++ b/server/internal/email_sender.go @@ -3,6 +3,7 @@ package internal import ( _ "embed" + "encoding/base64" "fmt" "strings" @@ -39,27 +40,50 @@ var ( adminAnnouncement []byte ) +type Mailer struct { + client *sendgrid.Client +} + +type Attachment struct { + FileName string + Data []byte +} + +func NewMailer(sendGridKey string) Mailer { + return Mailer{ + client: sendgrid.NewSendClient(sendGridKey), + } +} + // SendMail sends verification mails -func SendMail(sender, sendGridKey, receiver, subject, body string) error { - from := mail.NewEmail("Cloud4Students", sender) +func (m *Mailer) SendMail(sender, receiver, subject, body string, attachments ...Attachment) error { + 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) - _, err = client.Send(message) + if len(attachments) > 0 { + attachment := mail.NewAttachment() + attachment = attachment.SetContent(base64.StdEncoding.EncodeToString(attachments[0].Data)) + attachment = attachment.SetType("application/pdf") + attachment = attachment.SetFilename(attachments[0].FileName) + attachment = attachment.SetDisposition("attachment") + message = message.AddAttachment(attachment) + } + + _, err = m.client.Send(message) return err } // 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 +96,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 +168,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..253ffa35 100644 --- a/server/internal/email_sender_test.go +++ b/server/internal/email_sender_test.go @@ -11,20 +11,22 @@ import ( ) func TestSendMail(t *testing.T) { + m := NewMailer("1234") + t.Run("send valid mail", func(t *testing.T) { - err := SendMail("sender@gmail.com", "1234", "receiver@gmail.com", "subject", "body") + err := m.SendMail("sender@gmail.com", "receiver@gmail.com", "subject", "body") assert.NoError(t, err) }) t.Run("send invalid mail", func(t *testing.T) { - err := SendMail("sender@gmail.com", "1234", "receiver", "subject", "body") + err := m.SendMail("sender@gmail.com", "receiver", "subject", "body") assert.Error(t, err) }) } 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 +100,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/fonts/Arial-Bold.ttf b/server/internal/fonts/Arial-Bold.ttf new file mode 100644 index 00000000..a6037e68 Binary files /dev/null and b/server/internal/fonts/Arial-Bold.ttf differ diff --git a/server/internal/fonts/Arial-Italic.ttf b/server/internal/fonts/Arial-Italic.ttf new file mode 100644 index 00000000..38019978 Binary files /dev/null and b/server/internal/fonts/Arial-Italic.ttf differ diff --git a/server/internal/fonts/Arial.ttf b/server/internal/fonts/Arial.ttf new file mode 100644 index 00000000..8682d946 Binary files /dev/null and b/server/internal/fonts/Arial.ttf differ 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/img/logo.png b/server/internal/img/logo.png new file mode 100644 index 00000000..7aed89c9 Binary files /dev/null and b/server/internal/img/logo.png differ diff --git a/server/internal/pdf_generator.go b/server/internal/pdf_generator.go new file mode 100644 index 00000000..5a6eb1b8 --- /dev/null +++ b/server/internal/pdf_generator.go @@ -0,0 +1,472 @@ +package internal + +import ( + "fmt" + "strconv" + "time" + + "github.com/codescalers/cloud4students/models" + "github.com/pkg/errors" + "github.com/signintech/gopdf" +) + +const ( + greyColor uint8 = 80 + darkGreyColor uint8 = 60 + + startX float64 = 25 + startY float64 = 30 + + logoPath = "internal/img/logo.png" + fontPath = "internal/fonts/Arial.ttf" + boldFontPath = "internal/fonts/Arial-Bold.ttf" + italicFontPath = "internal/fonts/Arial-Italic.ttf" +) + +type InvoicePDF struct { + invoice models.Invoice + user models.User + + pdf *gopdf.GoPdf + config gopdf.Config + startX float64 + startY float64 +} + +func CreateInvoicePDF( + invoice models.Invoice, user models.User, +) ([]byte, error) { + pdf := gopdf.GoPdf{} + config := gopdf.Config{PageSize: *gopdf.PageSizeA4} + pdf.Start(config) + + invoicePDF := InvoicePDF{ + invoice: invoice, + user: user, + pdf: &pdf, + config: config, + startX: startX, + startY: startY, + } + + if err := invoicePDF.setFonts(); err != nil { + return nil, errors.Wrap(err, "failed to set fonts") + } + + if err := invoicePDF.draw(); err != nil { + return nil, errors.Wrap(err, "failed to draw pdf") + } + + return pdf.GetBytesPdf(), nil +} + +func (in *InvoicePDF) setFonts() error { + if err := in.pdf.AddTTFFont("Arial", fontPath); err != nil { + return err + } + + if err := in.pdf.AddTTFFont("Arial-Bold", boldFontPath); err != nil { + return err + } + + return in.pdf.AddTTFFont("Arial-Italic", italicFontPath) +} + +func (in *InvoicePDF) draw() error { + in.pdf.AddPage() + + if err := in.setLogo(); err != nil { + return errors.Wrap(err, "failed to set logo") + } + + // space + in.startY += 35 + + if err := in.title(); err != nil { + return errors.Wrap(err, "failed to display title") + } + + // space + in.startY += 45 + + if err := in.companySection(); err != nil { + return errors.Wrap(err, "failed to display company section") + } + + if err := in.invoiceSection(); err != nil { + return errors.Wrap(err, "failed to display invoice section") + } + + // space + in.startY += 70 + + if err := in.userDetails(); err != nil { + return errors.Wrap(err, "failed to display user section") + } + + // space + in.startY += 90 + + if err := in.summary(); err != nil { + return errors.Wrap(err, "failed to display summary") + } + + // space + in.startY += 70 + + // Total due + if err := in.totalDue(); err != nil { + return errors.Wrap(err, "failed to display total due") + } + + // space + in.startY += 85 + + // Product usage charges + if err := in.usageCharges(); err != nil { + return errors.Wrap(err, "failed to display usage charges") + } + + // space + in.startY += 85 + + // Table Header + if err := in.tableHeader(); err != nil { + return errors.Wrap(err, "failed to display table header") + } + + // space + in.startY += 30 + + // Table content + if err := in.tableContent(); err != nil { + return errors.Wrap(err, "failed to display table content") + } + + return nil +} + +func (in *InvoicePDF) setLogo() error { + return in.pdf.Image(logoPath, in.startX, in.startY, nil) +} + +func (in *InvoicePDF) title() error { + if err := in.pdf.SetFont("Arial", "", 14); err != nil { + return err + } + + in.pdf.SetTextColor(greyColor, greyColor, greyColor) + in.pdf.SetXY(in.startX, in.startY) + + return in.pdf.Cell(nil, + fmt.Sprintf("Final invoice for %s %d billing period", in.invoice.CreatedAt.Month().String(), in.invoice.CreatedAt.Year()), + ) +} + +func (in *InvoicePDF) companySection() error { + if err := in.pdf.SetFont("Arial-Bold", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(darkGreyColor, darkGreyColor, darkGreyColor) + + in.pdf.SetXY(in.startX, in.startY) + if err := in.pdf.Cell(nil, "From"); err != nil { + return err + } + + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(greyColor, greyColor, greyColor) + + in.pdf.SetXY(in.startX, in.startY+15) + if err := in.pdf.Cell(nil, "Codescalers Egypt"); err != nil { + return err + } + + in.pdf.SetXY(in.startX, in.startY+27) + if err := in.pdf.Cell(nil, "9 Al Wardi street, El Hegaz St"); err != nil { + return err + } + + in.pdf.SetXY(in.startX, in.startY+39) + return in.pdf.Cell(nil, "Cairo Governorate 11341") +} + +func (in *InvoicePDF) invoiceSection() error { + marginRight := float64(250) + + if err := in.pdf.SetFont("Arial-Bold", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(darkGreyColor, darkGreyColor, darkGreyColor) + + in.pdf.SetXY(in.startX+marginRight, in.startY) + if err := in.pdf.Cell(nil, "Details"); err != nil { + return err + } + + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(greyColor, greyColor, greyColor) + + // Data labels + in.pdf.SetXY(in.startX+250, in.startY+15) + if err := in.pdf.Cell(nil, "Invoice number:"); err != nil { + return err + } + in.pdf.SetXY(in.startX+250, in.startY+30) + if err := in.pdf.Cell(nil, "Date of issue:"); err != nil { + return err + } + in.pdf.SetXY(in.startX+250, in.startY+45) + if err := in.pdf.Cell(nil, "Payment due on:"); err != nil { + return err + } + + // Data details + textWidth, err := in.pdf.MeasureTextWidth(fmt.Sprint(in.invoice.ID)) + if err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-textWidth, in.startY+20) + if err := in.pdf.Cell(nil, fmt.Sprint(in.invoice.ID)); err != nil { + return err + } + + textWidth, err = in.pdf.MeasureTextWidth(in.invoice.CreatedAt.Format("January 2, 2006")) + if err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-textWidth, in.startY+35) + if err := in.pdf.Cell(nil, in.invoice.CreatedAt.Format("January 2, 2006")); err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-textWidth, in.startY+50) + return in.pdf.Cell(nil, in.invoice.CreatedAt.Format("January 2, 2006")) +} + +func (in *InvoicePDF) userDetails() error { + if err := in.pdf.SetFont("Arial-Bold", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(darkGreyColor, darkGreyColor, darkGreyColor) + + in.pdf.SetXY(in.startX, in.startY) + if err := in.pdf.Cell(nil, "For"); err != nil { + return err + } + + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(greyColor, greyColor, greyColor) + + // name + in.pdf.SetXY(in.startX, in.startY+15) + if err := in.pdf.Cell(nil, fmt.Sprintf("%s %s", in.user.FirstName, in.user.LastName)); err != nil { + return err + } + + // email + in.pdf.SetXY(in.startX, in.startY+27) + return in.pdf.Cell(nil, fmt.Sprintf("<%s>", in.user.Email)) +} + +func (in *InvoicePDF) summary() error { + if err := in.pdf.SetFont("Arial", "", 14); err != nil { + return err + } + in.pdf.SetXY(in.startX, in.startY) + if err := in.pdf.Cell(nil, "Summary"); err != nil { + return err + } + + in.pdf.Line(in.startX, in.startY+25, in.startX+540, in.startY+25) + in.pdf.Line(in.startX, in.startY+55, in.startX+540, in.startY+55) + + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + + in.pdf.SetXY(in.startX, in.startY+35) + if err := in.pdf.Cell(nil, "Total usage charges"); err != nil { + return err + } + + totalText := formatFloat(in.invoice.Total) + totalTextWidth, err := in.pdf.MeasureTextWidth(totalText) + if err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-totalTextWidth, in.startY+35) + return in.pdf.Cell(nil, formatFloat(in.invoice.Total)) +} + +func (in *InvoicePDF) totalDue() error { + if err := in.pdf.SetFont("Arial-Bold", "", 14); err != nil { + return err + } + + in.pdf.SetXY(in.startX, in.startY) + if err := in.pdf.Cell(nil, "Total due"); err != nil { + return err + } + + totalText := formatFloat(in.invoice.Total) + totalTextWidth, err := in.pdf.MeasureTextWidth(totalText) + if err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-totalTextWidth, in.startY) + if err := in.pdf.Cell(nil, formatFloat(in.invoice.Total)); err != nil { + return err + } + + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + in.pdf.SetXY(in.startX, in.startY+25) + if err := in.pdf.Cell(nil, "If you have a credit card on your account, it will be automatically charged within 24 hours"); err != nil { + return err + } + + in.pdf.SetStrokeColor(200, 200, 200) + in.pdf.Line(in.startX, in.startY+60, in.startX+540, in.startY+60) + + return nil +} + +func (in *InvoicePDF) usageCharges() error { + if err := in.pdf.SetFont("Arial", "", 14); err != nil { + return err + } + in.pdf.SetXY(in.startX, in.startY) + if err := in.pdf.Cell(nil, "Product usage charges"); err != nil { + return err + } + + if err := in.pdf.SetFont("Arial-Italic", "", 10); err != nil { + return err + } + + in.pdf.SetXY(in.startX, in.startY+20) + return in.pdf.Cell(nil, "Detailed usage information ca n be downloaded from the invoices section of your account") +} + +func (in *InvoicePDF) tableHeader() error { + if err := in.pdf.SetFont("Arial-Bold", "", 10); err != nil { + return err + } + + in.pdf.SetTextColor(darkGreyColor, darkGreyColor, darkGreyColor) + in.pdf.SetXY(in.startX, in.startY) + if err := in.pdf.Cell(nil, "Virtual machines"); err != nil { + return err + } + + in.pdf.SetXY(in.startX+250, in.startY) + if err := in.pdf.Cell(nil, "Hours"); err != nil { + return err + } + + in.pdf.SetXY(in.startX+300, in.startY) + if err := in.pdf.Cell(nil, "Start"); err != nil { + return err + } + + in.pdf.SetXY(in.startX+380, in.startY) + if err := in.pdf.Cell(nil, "End"); err != nil { + return err + } + + totalText := formatFloat(in.invoice.Total) + totalTextWidth, err := in.pdf.MeasureTextWidth(totalText) + if err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-totalTextWidth, in.startY) + if err := in.pdf.Cell(nil, formatFloat(in.invoice.Total)); err != nil { + return err + } + + in.pdf.SetStrokeColor(darkGreyColor, darkGreyColor, darkGreyColor) + in.pdf.Line(in.startX, in.startY+15, in.startX+540, in.startY+15) + return nil +} + +func (in *InvoicePDF) tableContent() error { + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(greyColor, greyColor, greyColor) + + y := in.startY + for _, d := range in.invoice.Deployments { + in.pdf.SetXY(in.startX, y) + if err := in.pdf.Cell(nil, fmt.Sprintf("vm-%s-%s", d.DeploymentName, d.DeploymentResources)); err != nil { + return err + } + + in.pdf.SetXY(in.startX+250, y) + if err := in.pdf.Cell(nil, fmt.Sprint(d.PeriodInHours)); err != nil { + return err + } + + in.pdf.SetXY(in.startX+300, y) + if err := in.pdf.Cell(nil, d.DeploymentCreatedAt.Format("01-02 15:04")); err != nil { + return err + } + + in.pdf.SetXY(in.startX+380, y) + if err := in.pdf.Cell(nil, time.Now().Format("01-02 15:04")); err != nil { + return err + } + + costTextWidth, err := in.pdf.MeasureTextWidth(formatFloat(d.Cost)) + if err != nil { + return err + } + + in.pdf.SetXY(in.startX+540-costTextWidth, y) + if err := in.pdf.Cell(nil, formatFloat(d.Cost)); err != nil { + return err + } + + if y > in.config.PageSize.H-50 { + in.pdf.AddPage() + + in.startY = startY + y = in.startY + if err := in.tableHeader(); err != nil { + return err + } + + y += 10 + if err := in.pdf.SetFont("Arial", "", 10); err != nil { + return err + } + in.pdf.SetTextColor(greyColor, greyColor, greyColor) + } + + y += 15 + } + + return nil +} + +func formatFloat(f float64) string { + // Check if the number has a fractional part + if f == float64(int(f)) { + return strconv.Itoa(int(f)) + } + + return fmt.Sprintf("%.2f", f) +} 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/middlewares/logging.go b/server/middlewares/logging.go index 29f2fbbd..9f1829f8 100644 --- a/server/middlewares/logging.go +++ b/server/middlewares/logging.go @@ -3,14 +3,35 @@ package middlewares import ( "net/http" + "time" + "github.com/codescalers/cloud4students/models" "github.com/rs/zerolog/log" + "github.com/urfave/negroni/v3" ) -// LoggingMW logs all information of every request -func LoggingMW(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Info().Timestamp().Str("method", r.Method).Str("uri", r.RequestURI).Send() - h.ServeHTTP(w, r) - }) +func AuditLogMiddleware(db models.DB) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userID := r.Context().Value(UserIDKey("UserID")).(string) + + lrw := negroni.NewResponseWriter(w) + h.ServeHTTP(lrw, r) + + statusCode := lrw.Status() + + l := models.AuditLog{ + UserID: userID, + Method: r.Method, + URL: r.URL.Path, + Timestamp: time.Now(), + Success: statusCode >= 200 && statusCode < 300, + StatusCode: statusCode, + } + + if err := db.CreateAuditLog(&l); err != nil { + log.Error().Err(err).Msg("logging audit failed") + } + }) + } } 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/audit.go b/server/models/audit.go new file mode 100644 index 00000000..89c8c3ae --- /dev/null +++ b/server/models/audit.go @@ -0,0 +1,43 @@ +package models + +import ( + "time" +) + +// Struct to represent audit log details +type AuditLog struct { + ID uint `gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"not null"` + Method string `json:"method"` + URL string `json:"url"` + Timestamp time.Time `json:"timestamp"` + Success bool `json:"success"` + StatusCode int `json:"status_code"` +} + +func (d *DB) CreateAuditLog(l *AuditLog) error { + return d.db.Create(&l).Error +} + +func (d *DB) GetUserLogs(userID string) ([]AuditLog, error) { + var res []AuditLog + return res, d.db.Find(&res, "user_id = ?", userID).Error +} + +type AuditEvent struct { + ID uint `gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"not null"` + Action string `json:"action"` + Role string `json:"role"` + Timestamp time.Time `json:"timestamp"` + Metadata string `json:"metadata"` +} + +func (d *DB) CreateAuditEvent(e *AuditEvent) error { + return d.db.Create(&e).Error +} + +func (d *DB) GetUserEvents(userID string) ([]AuditEvent, error) { + var res []AuditEvent + return res, d.db.Find(&res, "user_id = ?", userID).Error +} 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..d376a1bf 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,19 @@ 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{}, + AuditLog{}, AuditEvent{}, + ) 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 +55,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..ffa77b5f --- /dev/null +++ b/server/models/deployments_count.go @@ -0,0 +1,84 @@ +package models + +// DeploymentsCount has the vms and ips reserved in the grid +type DeploymentsCount struct { + VMs int64 `json:"vms"` + IPs int64 `json:"ips"` +} + +// CountAllDeployments returns deployments and IPs count +func (d *DB) CountAllDeployments() (DeploymentsCount, error) { + var vmsCount int64 + result := d.db.Table("vms").Count(&vmsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sCount int64 + result = d.db.Table("masters").Count(&k8sCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + dlsCount := k8sCount + vmsCount + + var vmIPsCount int64 + result = d.db.Table("vms").Where("public = true").Count(&vmIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sIPsCount int64 + result = d.db.Table("masters").Where("public = true").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).Count(&vmsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sCount int64 + result = d.db.Table("k8s_clusters").Where("user_id = ?", userID).Count(&k8sCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + dlsCount := k8sCount + vmsCount + + var vmIPsCount int64 + result = d.db.Table("vms").Where("public = true").Where("user_id = ?", userID).Count(&vmIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + var k8sIPsCount int64 + result = d.db.Table("k8s_clusters").Joins("JOIN masters ON k8s_clusters.id = masters.cluster_id"). + Where("public = true").Where("user_id = ?", userID).Count(&k8sIPsCount) + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + if result.Error != nil { + return DeploymentsCount{}, result.Error + } + + ipsCount := k8sIPsCount + vmIPsCount + + return DeploymentsCount{ + dlsCount, ipsCount, + }, nil +} diff --git a/server/models/invoice.go b/server/models/invoice.go new file mode 100644 index 00000000..188d3785 --- /dev/null +++ b/server/models/invoice.go @@ -0,0 +1,110 @@ +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_reminder_at"` + CreatedAt time.Time `json:"created_at"` + PaidAt time.Time `json:"paid_at"` + FileData []byte `json:"file_data" gorm:"type:blob"` +} + +type DeploymentItem struct { + ID int `json:"id" gorm:"primaryKey"` + InvoiceID int `json:"invoice_id"` + DeploymentID int `json:"deployment_id"` + DeploymentName string `json:"deployment_name"` + DeploymentCreatedAt time.Time `json:"deployment_created_at"` + 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_reminder_at": time.Now()}).Error +} + +func (d *DB) UpdateInvoicePDF(id int, data []byte) error { + return d.db.Model(&Invoice{}).Where("id = ?", id).Updates(map[string]interface{}{"file_data": data}).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..bcc2e1c1 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,119 @@ 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).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..5fc1664d 100644 --- a/server/models/notification.go +++ b/server/models/notification.go @@ -10,10 +10,45 @@ const ( // Notification struct holds data of notifications type Notification struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id" binding:"required"` - Msg string `json:"msg" binding:"required"` - Seen bool `json:"seen" binding:"required"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" binding:"required"` + Msg string `json:"msg" binding:"required"` + Seen bool `json:"seen" binding:"required"` + Notified bool `json:"notified" binding:"required"` // to allow redirecting from notifications to the right pages + // for example if the type is `vm` it will be redirected to the vm page 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 +} + +// GetNewNotifications returns a list of new notifications for a user. +func (d *DB) GetNewNotifications(userID string) ([]Notification, error) { + var res []Notification + query := d.db.Where("user_id = ?", userID).Where("notified = ?", false).Find(&res) + if query.Error != nil { + return nil, query.Error + } + + return res, d.db.Model(&Notification{}).Where("user_id = ?", userID).Updates(map[string]interface{}{"notified": true}).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 +} + +// UpdateUserNotification updates seen field for user notifications +func (d *DB) UpdateUserNotification(userID string, seen bool) error { + return d.db.Model(&Notification{}).Where("user_id = ?", userID).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..c0384b9d 100644 --- a/server/models/vm.go +++ b/server/models/vm.go @@ -1,25 +1,99 @@ // Package models for database models package models +import ( + "time" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + // VM struct for vms data type VM struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id"` - Name string `json:"name" gorm:"unique" binding:"required"` - YggIP string `json:"ygg_ip"` - MyceliumIP string `json:"mycelium_ip"` - Public bool `json:"public"` - PublicIP string `json:"public_ip"` - Resources string `json:"resources"` - SRU uint64 `json:"sru"` - CRU uint64 `json:"cru"` - MRU uint64 `json:"mru"` - ContractID uint64 `json:"contractID"` - NetworkContractID uint64 `json:"networkContractID"` -} - -// DeploymentsCount has the vms and ips reserved in the grid -type DeploymentsCount struct { - VMs int64 `json:"vms"` - IPs int64 `json:"ips"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id"` + Name string `json:"name" gorm:"unique" binding:"required"` + YggIP string `json:"ygg_ip"` + MyceliumIP string `json:"mycelium_ip"` + Public bool `json:"public"` + PublicIP string `json:"public_ip"` + Resources string `json:"resources"` + Region string `json:"region"` + SRU uint64 `json:"sru"` + CRU uint64 `json:"cru"` + MRU uint64 `json:"mru"` + ContractID uint64 `json:"contractID"` + NetworkContractID uint64 `json:"networkContractID"` + State state `json:"state"` + Failure string `json:"failure"` + PricePerMonth float64 `json:"price"` + CreatedAt time.Time `json:"created_at"` +} + +// CreateVM creates new vm +func (d *DB) CreateVM(vm *VM) error { + return d.db.Create(&vm).Error +} + +// GetVMByID return vm by its id +func (d *DB) GetVMByID(id int) (VM, error) { + var vm VM + return vm, d.db.First(&vm, id).Error +} + +// GetAllVms returns all vms of user +func (d *DB) GetAllVms(userID string) ([]VM, error) { + var vms []VM + return vms, d.db.Where("user_id = ?", userID).Find(&vms).Error +} + +// 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