diff --git a/.env b/.env index 8538b56..f6e3b68 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL=postgres://postgres:postgres@localhost:5432/newsletter \ No newline at end of file +DATABASE_URL=postgres://postgres:postgres@localhost:5432/stacker \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..bf3ee4c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,155 @@ +name: Docker CICD + +on: + push: + branches: + - master + - testing + pull_request: + branches: + - master + +jobs: + cicd-linux-docker: + name: Cargo and npm build + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v3.0.7 + with: + path: ~/.cargo/registry + key: docker-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + docker-registry- + docker- + + - name: Cache cargo index + uses: actions/cache@v3.0.7 + with: + path: ~/.cargo/git + key: docker-index-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + docker-index- + docker- + + - name: Generate Secret Key + run: | + head -c16 /dev/urandom > src/secret.key + + - name: Cache cargo build + uses: actions/cache@v3.0.7 + with: + path: target + key: docker-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + docker-build- + docker- + + - name: Cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + - name: Cargo test + if: ${{ always() }} + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Rustfmt + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: rustfmt + command: fmt + args: --all -- --check + + - name: Rustfmt + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: clippy + command: clippy + args: -- -D warnings + + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release + + - name: npm install, build, and test + working-directory: ./web + run: | + npm install + npm run build + # npm test + + - name: Archive production artifacts + uses: actions/upload-artifact@v2 + with: + name: dist-without-markdown + path: | + web/dist + !web/dist/**/*.md + + - name: Display structure of downloaded files + run: ls -R web/dist + + - name: Copy app files and zip + run: | + mkdir -p app/stacker/dist + cp target/release/stacker app/stacker + cp -a web/dist/. app/stacker + cp docker/prod/Dockerfile app/Dockerfile + cd app + touch .env + tar -czvf ../app.tar.gz . + cd .. + + - name: Upload app archive for Docker job + uses: actions/upload-artifact@v2.2.2 + with: + name: artifact-linux-docker + path: app.tar.gz + + cicd-docker: + name: CICD Docker + runs-on: ubuntu-latest + needs: cicd-linux-docker + steps: + - name: Download app archive + uses: actions/download-artifact@v2 + with: + name: artifact-linux-docker + + - name: Extract app archive + run: tar -zxvf app.tar.gz + + - name: Display structure of downloaded files + run: ls -R + + - name: Docker build and publish + uses: docker/build-push-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: trydirect/stacker + add_git_labels: true + tag_with_ref: true + #no-cache: true \ No newline at end of file diff --git a/.github/workflows/notifier.yml b/.github/workflows/notifier.yml new file mode 100644 index 0000000..ba3ed81 --- /dev/null +++ b/.github/workflows/notifier.yml @@ -0,0 +1,19 @@ +name: Notifier +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + notifyTelegram: + runs-on: ubuntu-latest + steps: + - name: send custom message + uses: appleboy/telegram-action@master + with: + to: ${{ secrets.TELEGRAM_TO }} + token: ${{ secrets.TELEGRAM_TOKEN }} + message: | + "Issue ${{ github.event.action }}: \n${{ github.event.issue.html_url }}" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 33d7898..fc75567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "actix-cors" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + [[package]] name = "actix-http" version = "3.3.1" @@ -183,6 +198,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" +dependencies = [ + "actix-utils", + "actix-web", + "base64 0.21.0", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "adler" version = "1.0.2" @@ -259,7 +289,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -693,6 +723,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -932,6 +963,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1211,7 +1243,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1344,7 +1376,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1382,20 +1414,44 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1670,29 +1726,29 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.162" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.162" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -1711,6 +1767,50 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_valid" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a" +dependencies = [ + "indexmap", + "itertools", + "num-traits", + "once_cell", + "paste", + "regex", + "serde", + "serde_json", + "serde_valid_derive", + "serde_valid_literal", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "serde_valid_derive" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071237362e267e2a76ffe4434094e089dcd8b5e9d8423ada499e5550dcb0181d" +dependencies = [ + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "serde_valid_literal" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f57df292b1d64449f90794fc7a67efca0b21acca91493e64a46418a29bbe36b4" +dependencies = [ + "paste", + "regex", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1892,12 +1992,18 @@ dependencies = [ name = "stacker" version = "0.1.0" dependencies = [ + "actix-cors", "actix-web", + "actix-web-httpauth", "chrono", "config", "reqwest", "serde", + "serde_derive", + "serde_json", + "serde_valid", "sqlx", + "thiserror", "tokio", "tracing", "tracing-bunyan-formatter", @@ -1916,6 +2022,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -1935,9 +2047,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -1974,7 +2086,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -2067,7 +2179,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -2152,7 +2264,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -2350,7 +2462,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", "wasm-bindgen-shared", ] @@ -2384,7 +2496,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index b55616f..d1b3e87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,12 @@ tracing-bunyan-formatter = "0.3.8" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] } uuid = { version = "1.3.4", features = ["v4"] } +thiserror = "1.0" +serde_valid = "0.16.3" +serde_json = { version = "1.0.105", features = [] } +serde_derive = "1.0.188" +actix-web-httpauth = "0.8.1" +actix-cors = "0.6.4" [dependencies.sqlx] version = "0.6.3" diff --git a/README.md b/README.md index 0a33450..a708c47 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,51 @@ -# stacker +# Stacker + + +Stacker - is an application that helps users to create custom IT solutions based on dockerized open +source apps and user's custom applications docker containers. Users can build their own stack of applications, and +deploy the final result to their favorite clouds using TryDirect API. + +Application development will include: +- Web UI (Application Stack builder) +- Command line interface +- Back-end RESTful API, includes: + - [ ] Security module. + - [ ] User Authorization + - [ ] Application Management + - [ ] Application Key Management + - [ ] Cloud Provider Key Management + - [ ] docker-compose.yml generator + - [ ] TryDirect API Client + - [ ] Rating module + +## How to start + +#### Run db migration - -Run db migration ``` sqlx migrate run ``` -Down migration + +#### Down migration ``` sqlx migrate revert ``` -Add rating +## CURL examples +#### Rate Product + +``` + + curl -vX POST 'http://localhost:8000/rating' -d '{"obj_id": 1, "category": "application", "comment":"some comment", "rate": 10}' --header 'Content-Type: application/json' ``` - curl -vX POST 'http://localhost:8000/rating' -d '{"obj_id": 111, "category": "application", "comment":"some comment", "rate": 10}' --header 'Content-Type: application/json' +#### Deploy +``` +curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json http://127.0.0.1:8000/stack ``` \ No newline at end of file diff --git a/configuration.yaml b/configuration.yaml index 70fe16f..dbd16f2 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -4,4 +4,4 @@ database: port: 5432 username: postgres password: "postgres" - database_name: newsletter \ No newline at end of file + database_name: stacker \ No newline at end of file diff --git a/custom-stack-payload-2.json b/custom-stack-payload-2.json new file mode 100644 index 0000000..e64ec97 --- /dev/null +++ b/custom-stack-payload-2.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lltkpq6p347kystct","dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"0.6","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"

PostgREST description

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject","custom_stack_code":"smarty-bot","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}} diff --git a/custom-stack-payload.json b/custom-stack-payload.json new file mode 100644 index 0000000..a9ca754 --- /dev/null +++ b/custom-stack-payload.json @@ -0,0 +1,4 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration","stack_health_monitoring","stack_security_monitoring"],"save_token":true,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"smarty database","code":"smarty-database","domain":"smarty-db.example.com","sharedPorts":["6532"],"versions":[],"custom":true,"type":"feature","main":true,"_id":"lm0gdh732y2qrojfl","dockerhub_user":"trydirect","dockerhub_name":"smarty-db","ram_size":"1Gb","cpu":1,"disk_size":"1Gb"}],"feature":[{"_etag":null,"_id":235,"_created":"2023-08-11T07:07:12.123355","_updated":"2023-08-15T13:07:30.597485","name":"Nginx Proxy Manager","code":"nginx_proxy_manager","role":["nginx_proxy_manager"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["80","81","443"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":192,"height":192,"image":"205128e6-0303-4b62-b946-9810b61f3d04.png"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"

Nginx Proxy Manager is a user-friendly software application designed to effortlessly route traffic to your websites, whether they're hosted at home or elsewhere. It comes equipped with free SSL capabilities, eliminating the need for extensive Nginx or Letsencrypt knowledge. This tool proves especially handy for simplifying SSL generation and seamlessly proxying your docker containers.

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"0.3Gb","dockerhub_image":"nginx-proxy-manager","versions":[{"_etag":"599","_id":599,"_created":"2023-08-11T10:23:33","_updated":"2023-08-11T10:23:34.420583","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"601","_id":601,"_created":null,"_updated":"2023-08-15T08:11:19.703882","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"published","tag":"stable"},{"_etag":null,"_id":600,"_created":null,"_updated":"2023-08-11T07:08:43.944998","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"latest"}],"domain":"","sharedPorts":["443"],"main":true}],"service":[{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":null,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["5432"],"main":true}],"servers_count":3,"custom_stack_name":"SMBO","custom_stack_code":"sample-stack","custom_stack_git_url":"https://github.com/vsilent/smbo.git","custom_stack_category":["New","Marketing Automation"],"custom_stack_short_description":"Should be what is my project about shortly","custom_stack_description":"what is my project about more detailed","project_name":"sample stack","project_overview":"my short description, stack to marketplace, keep my token","project_description":"my full description, stack to marketplace, keep my token"}} + + + diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index c693139..4aaeb3f 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -9,8 +9,8 @@ CREATE TABLE product ( ); CREATE TABLE rating ( - id integer NOT NULL, PRIMARY KEY(id), - user_id uuid NOT NULL, + id serial, + user_id integer NOT NULL, product_id integer NOT NULL, category VARCHAR(255) NOT NULL, comment TEXT DEFAULT NULL, @@ -18,11 +18,10 @@ CREATE TABLE rating ( rate INTEGER, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, - CONSTRAINT fk_product - FOREIGN KEY(product_id) - REFERENCES product(id) + CONSTRAINT fk_product FOREIGN KEY(product_id) REFERENCES product(id), + CONSTRAINT rating_pk PRIMARY KEY (id) ); CREATE INDEX idx_category ON rating(category); CREATE INDEX idx_user_id ON rating(user_id); -CREATE INDEX idx_product_id_rating_id ON rating(product_id, rate); \ No newline at end of file +CREATE INDEX idx_product_id_rating_id ON rating(product_id, rate); diff --git a/migrations/20230905145525_creating_stack_tables.down.sql b/migrations/20230905145525_creating_stack_tables.down.sql new file mode 100644 index 0000000..203a95a --- /dev/null +++ b/migrations/20230905145525_creating_stack_tables.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here + +DROP TABLE user_stack; diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql new file mode 100644 index 0000000..eab6cf2 --- /dev/null +++ b/migrations/20230905145525_creating_stack_tables.up.sql @@ -0,0 +1,12 @@ +-- Add up migration script here +-- Add migration script here +CREATE TABLE user_stack ( + id integer NOT NULL, PRIMARY KEY(id), + stack_id integer NOT NULL, + user_id integer NOT NULL, + name TEXT NOT NULL, + body JSON NOT NULL, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL +) + diff --git a/migrations/20230917162549_creating_test_product.down.sql b/migrations/20230917162549_creating_test_product.down.sql new file mode 100644 index 0000000..f9f6339 --- /dev/null +++ b/migrations/20230917162549_creating_test_product.down.sql @@ -0,0 +1 @@ +delete from product where id=1; diff --git a/migrations/20230917162549_creating_test_product.up.sql b/migrations/20230917162549_creating_test_product.up.sql new file mode 100644 index 0000000..7a1d8d6 --- /dev/null +++ b/migrations/20230917162549_creating_test_product.up.sql @@ -0,0 +1 @@ +INSERT INTO public.product (id, obj_id, obj_type, created_at, updated_at) VALUES(1, 1, 'Application', '2023-09-17 10:30:02.579', '2023-09-17 10:30:02.579'); \ No newline at end of file diff --git a/scripts/init_db.sh b/scripts/init_db.sh index 9b13934..8d84403 100755 --- a/scripts/init_db.sh +++ b/scripts/init_db.sh @@ -14,7 +14,7 @@ fi DB_USER=${POSTGRES_USER:=postgres} DB_PASSWORD=${POSTGRES_PASSWORD:=postgres} -DB_NAME=${POSTGRES_DB:=newsletter} +DB_NAME=${POSTGRES_DB:=stacker} DB_PORT=${POSTGRES_PORT:=5432} docker run \ diff --git a/src/forms/mod.rs b/src/forms/mod.rs new file mode 100644 index 0000000..7a10a3e --- /dev/null +++ b/src/forms/mod.rs @@ -0,0 +1,3 @@ +mod rating; + +pub use rating::*; diff --git a/src/forms/rating.rs b/src/forms/rating.rs new file mode 100644 index 0000000..76efca4 --- /dev/null +++ b/src/forms/rating.rs @@ -0,0 +1,14 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Serialize, Deserialize, Debug, Validate)] +pub struct Rating { + pub obj_id: i32, // product external id + pub category: models::RateCategory, // rating of product | rating of service etc + #[validate(max_length = 1000)] + pub comment: Option, // always linked to a product + #[validate(minimum = 0)] + #[validate(maximum = 10)] + pub rate: i32, // +} diff --git a/src/lib.rs b/src/lib.rs index bae3244..9d3cc9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ +pub mod configuration; +pub mod forms; +mod middleware; +pub mod models; pub mod routes; +pub mod services; pub mod startup; -pub mod configuration; pub mod telemetry; -mod middleware; -mod models; -mod services; \ No newline at end of file diff --git a/src/models/mod.rs b/src/models/mod.rs index a4ece87..c3cbfed 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1,5 @@ -mod rating; -mod user; \ No newline at end of file +pub mod rating; +pub mod stack; +pub mod user; + +pub use rating::*; diff --git a/src/models/rating.rs b/src/models/rating.rs index 7b30c97..b1242dd 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,5 +1,6 @@ -use uuid::Uuid; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; pub struct Product { // Product - is an external object that we want to store in the database, @@ -9,29 +10,47 @@ pub struct Product { // rating - is a rating of the product // product type stack & app, // id is generated based on the product type and external obj_id - pub id: i32, //primary key, for better data management - pub obj_id: u32, // external product ID db, no autoincrement, example: 100 - pub obj_type: String, // stack | app, unique index - pub rating: Rating, // 0-10 - // pub rules: Rules, + pub id: i32, //primary key, for better data management + pub obj_id: i32, // external product ID db, no autoincrement, example: 100 + pub obj_type: String, // stack | app, unique index pub created_at: DateTime, pub updated_at: DateTime, } pub struct Rating { pub id: i32, - pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) - pub category: String, // rating of product | rating of service etc - pub comment: String, // always linked to a product - pub hidden: bool, // rating can be hidden for non-adequate user behaviour + pub user_id: Uuid, // external user_id, 100, taken using token (middleware?) + pub product_id: i32, //primary key, for better data management + pub category: String, // rating of product | rating of service etc + pub comment: String, // always linked to a product + pub hidden: bool, // rating can be hidden for non-adequate user behaviour pub rate: u32, pub created_at: DateTime, pub updated_at: DateTime, } +#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Copy)] +#[sqlx(rename_all = "lowercase", type_name = "varchar")] +pub enum RateCategory { + Application, // app, feature, extension + Cloud, // is user satisfied working with this cloud + Stack, // app stack + DeploymentSpeed, + Documentation, + Design, + TechSupport, + Price, + MemoryUsage, +} + +impl Into for RateCategory { + fn into(self) -> String { + format!("{:?}", self) + } +} + pub struct Rules { //-> Product.id // example: allow to add only a single comment comments_per_user: i32, // default = 1 } - diff --git a/src/models/stack.rs b/src/models/stack.rs new file mode 100644 index 0000000..ad8ebf8 --- /dev/null +++ b/src/models/stack.rs @@ -0,0 +1,255 @@ +use chrono::{DateTime, Utc}; +use serde_derive::Deserialize; +use serde_derive::Serialize; +use serde_json::Value; +use uuid::Uuid; + +pub struct Stack { + pub id: Uuid, // id - is a unique identifier for the app stack + pub stack_id: Uuid, // external stack ID + pub user_id: Uuid, // external unique identifier for the user + pub name: String, + pub body: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FormData { + pub common_domain: String, + pub domain_list: DomainList, + pub region: String, + pub zone: Value, + pub server: String, + pub os: String, + pub ssl: String, + pub vars: Vec, + #[serde(rename = "integrated_features")] + pub integrated_features: Vec, + #[serde(rename = "extended_features")] + pub extended_features: Vec, + pub subscriptions: Vec, + #[serde(rename = "save_token")] + pub save_token: bool, + #[serde(rename = "cloud_token")] + pub cloud_token: String, + pub provider: String, + #[serde(rename = "stack_code")] + pub stack_code: String, + #[serde(rename = "selected_plan")] + pub selected_plan: String, + pub custom: Custom, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DomainList {} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Custom { + pub web: Vec, + pub feature: Vec, + pub service: Vec, + #[serde(rename = "servers_count")] + pub servers_count: i64, + #[serde(rename = "custom_stack_name")] + pub custom_stack_name: String, + #[serde(rename = "custom_stack_code")] + pub custom_stack_code: String, + #[serde(rename = "custom_stack_git_url")] + pub custom_stack_git_url: String, + #[serde(rename = "custom_stack_category")] + pub custom_stack_category: Vec, + #[serde(rename = "custom_stack_short_description")] + pub custom_stack_short_description: String, + #[serde(rename = "custom_stack_description")] + pub custom_stack_description: String, + #[serde(rename = "project_name")] + pub project_name: String, + #[serde(rename = "project_overview")] + pub project_overview: String, + #[serde(rename = "project_description")] + pub project_description: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Web { + pub name: String, + pub code: String, + pub domain: String, + pub shared_ports: Vec, + pub versions: Vec, + pub custom: bool, + #[serde(rename = "type")] + pub type_field: String, + pub main: bool, + #[serde(rename = "_id")] + pub id: String, + #[serde(rename = "dockerhub_user")] + pub dockerhub_user: String, + #[serde(rename = "dockerhub_name")] + pub dockerhub_name: String, + #[serde(rename = "ram_size")] + pub ram_size: String, + pub cpu: i64, + #[serde(rename = "disk_size")] + pub disk_size: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Feature { + #[serde(rename = "_etag")] + pub etag: Value, + #[serde(rename = "_id")] + pub id: i64, + #[serde(rename = "_created")] + pub created: String, + #[serde(rename = "_updated")] + pub updated: String, + pub name: String, + pub code: String, + pub role: Vec, + #[serde(rename = "type")] + pub type_field: String, + pub default: Value, + pub popularity: Value, + pub descr: Value, + pub ports: Ports, + pub commercial: Value, + pub subscription: Value, + pub autodeploy: Value, + pub suggested: Value, + pub dependency: Value, + #[serde(rename = "avoid_render")] + pub avoid_render: Value, + pub price: Value, + pub icon: Icon, + #[serde(rename = "category_id")] + pub category_id: i64, + #[serde(rename = "parent_app_id")] + pub parent_app_id: Value, + #[serde(rename = "full_description")] + pub full_description: Value, + pub description: String, + #[serde(rename = "plan_type")] + pub plan_type: Value, + #[serde(rename = "ansible_var")] + pub ansible_var: Value, + #[serde(rename = "repo_dir")] + pub repo_dir: Value, + pub cpu: String, + #[serde(rename = "ram_size")] + pub ram_size: String, + #[serde(rename = "disk_size")] + pub disk_size: String, + #[serde(rename = "dockerhub_image")] + pub dockerhub_image: String, + pub versions: Vec, + pub domain: String, + pub shared_ports: Vec, + pub main: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Ports { + pub public: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Icon { + pub light: IconLight, + pub dark: IconDark, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IconLight { + pub width: i64, + pub height: i64, + pub image: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IconDark {} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Version { + #[serde(rename = "_etag")] + pub etag: Option, + #[serde(rename = "_id")] + pub id: i64, + #[serde(rename = "_created")] + pub created: Option, + #[serde(rename = "_updated")] + pub updated: String, + #[serde(rename = "app_id")] + pub app_id: i64, + pub name: String, + pub version: String, + #[serde(rename = "update_status")] + pub update_status: String, + pub tag: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Service { + #[serde(rename = "_etag")] + pub etag: Value, + #[serde(rename = "_id")] + pub id: i64, + #[serde(rename = "_created")] + pub created: String, + #[serde(rename = "_updated")] + pub updated: String, + pub name: String, + pub code: String, + pub role: Vec, + #[serde(rename = "type")] + pub type_field: String, + pub default: Value, + pub popularity: Value, + pub descr: Value, + pub ports: Value, + pub commercial: Value, + pub subscription: Value, + pub autodeploy: Value, + pub suggested: Value, + pub dependency: Value, + #[serde(rename = "avoid_render")] + pub avoid_render: Value, + pub price: Value, + pub icon: Icon, + #[serde(rename = "category_id")] + pub category_id: Value, + #[serde(rename = "parent_app_id")] + pub parent_app_id: Value, + #[serde(rename = "full_description")] + pub full_description: Value, + pub description: Value, + #[serde(rename = "plan_type")] + pub plan_type: Value, + #[serde(rename = "ansible_var")] + pub ansible_var: Value, + #[serde(rename = "repo_dir")] + pub repo_dir: Value, + pub cpu: Value, + #[serde(rename = "ram_size")] + pub ram_size: Value, + #[serde(rename = "disk_size")] + pub disk_size: Value, + #[serde(rename = "dockerhub_image")] + pub dockerhub_image: String, + pub versions: Vec, + pub domain: String, + pub shared_ports: Vec, + pub main: bool, +} diff --git a/src/models/user.rs b/src/models/user.rs index 9e86a34..f345e40 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,3 +1,6 @@ -pub struct User { +use serde::Deserialize; -} \ No newline at end of file +#[derive(Debug, Copy, Clone, Deserialize)] +pub struct User { + pub id: i32, +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 9416275..c9a46e5 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -3,3 +3,5 @@ mod rating; pub use health_checks::*; pub use rating::*; +pub(crate) mod stack; +pub use stack::*; \ No newline at end of file diff --git a/src/routes/rating.rs b/src/routes/rating.rs index 8e2b2ab..bc3995c 100644 --- a/src/routes/rating.rs +++ b/src/routes/rating.rs @@ -1,21 +1,141 @@ -use actix_web::{web, HttpResponse}; -use serde::{Deserialize, Serialize}; - +use crate::forms; +use crate::models; +use crate::models::user::User; +use crate::models::RateCategory; +use actix_web::{web, HttpResponse, Responder, Result}; +use serde_derive::Serialize; +use sqlx::PgPool; +use tracing::Instrument; +use uuid::Uuid; // workflow // add, update, list, get(user_id), ACL, // ACL - access to func for a user // ACL - access to objects for a user -#[derive(Serialize, Deserialize, Debug)] -pub struct RatingForm { - pub obj_id: u32, // product external id - pub category: String, // rating of product | rating of service etc - pub comment: String, // always linked to a product - pub rate: u32, // +#[derive(Serialize)] +struct JsonResponse { + status: String, + message: String, + code: u32, + id: Option, } -pub async fn rating(form: web::Json) -> HttpResponse { - println!("{:?}", form); - HttpResponse::Ok().finish() +pub async fn rating( + user: web::ReqData, + form: web::Json, + pool: web::Data, +) -> Result { + //TODO. check if there already exists a rating for this product committed by this user + let request_id = Uuid::new_v4(); + let query_span = tracing::info_span!("Check product existence by id."); + match sqlx::query_as!( + models::Product, + r"SELECT * FROM product WHERE obj_id = $1", + form.obj_id + ) + .fetch_one(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(product) => { + tracing::info!("req_id: {} Found product: {:?}", request_id, product.obj_id); + } + Err(e) => { + tracing::error!( + "req_id: {} Failed to fetch product: {:?}, error: {:?}", + request_id, + form.obj_id, + e + ); + // return HttpResponse::InternalServerError().finish(); + return Ok(web::Json(JsonResponse { + status: "Error".to_string(), + code: 404, + message: format!("Object not found {}", form.obj_id), + id: None, + })); + } + }; + + let query_span = tracing::info_span!("Search for existing vote."); + match sqlx::query!( + r"SELECT id FROM rating where user_id=$1 AND product_id=$2 AND category=$3 LIMIT 1", + user.id, + form.obj_id, + form.category as RateCategory + ) + .fetch_one(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(record) => { + tracing::info!( + "req_id: {} rating exists: {:?}, user: {}, product: {}, category: {:?}", + request_id, + record.id, + user.id, + form.obj_id, + form.category + ); + + return Ok(web::Json(JsonResponse { + status: "Error".to_string(), + code: 409, + message: format!("Already Rated"), + id: Some(record.id), + })); + } + Err(err) => { + // @todo, match the sqlx response + } + } + + let query_span = tracing::info_span!("Saving new rating details into the database"); + // Get product by id + // Insert rating + match sqlx::query!( + r#" + INSERT INTO rating (user_id, product_id, category, comment, hidden,rate, + created_at, + updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id + "#, + user.id, + form.obj_id, + form.category as models::RateCategory, + form.comment, + false, + form.rate + ) + .fetch_one(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(result) => { + println!("Query returned {:?}", result); + tracing::info!( + "req_id: {} New rating {} have been saved to database", + request_id, + result.id + ); + + Ok(web::Json(JsonResponse { + status: "ok".to_string(), + code: 200, + message: "Saved".to_string(), + id: Some(result.id), + })) + } + Err(e) => { + tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + Ok(web::Json(JsonResponse { + status: "error".to_string(), + code: 500, + message: "Failed to insert".to_string(), + id: None, + })) + } + } } diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs new file mode 100644 index 0000000..4ea14aa --- /dev/null +++ b/src/routes/stack/add.rs @@ -0,0 +1,93 @@ +use crate::models::stack::FormData; +use actix_web::error::{Error, JsonPayloadError, PayloadError}; +use actix_web::web::Form; +use actix_web::{ + web::{Bytes, Data, Json}, + HttpRequest, HttpResponse, Responder, Result, +}; +use chrono::Utc; +use sqlx::PgPool; +use std::io::Read; +use std::str; +use tracing::Instrument; +use uuid::Uuid; + +// pub async fn add(req: HttpRequest, app_state: Data, pool: +pub async fn add(body: Bytes) -> Result { + // None::.expect("my error"); + // return Err(JsonPayloadError::Payload(PayloadError::Overflow).into()); + // let content_type = req.headers().get("content-type"); + // println!("=================== Request Content-Type: {:?}", content_type); + + let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); + let body_str = str::from_utf8(&body_bytes).unwrap(); + // method 1 + // let app_state: AppState = serde_json::from_str(body_str).unwrap(); + // method 2 + // let app_state = serde_json::from_str::(body_str).unwrap(); + // println!("request: {:?}", app_state); + + let stack = serde_json::from_str::(body_str).unwrap(); + println!("app: {:?}", stack); + // println!("user_id: {:?}", data.user_id); + // tracing::info!("we are here"); + // match Json::::extract(&req).await { + // Ok(form) => println!("Hello from {:?}!", form), + // Err(err) => println!("error={:?}", err), + // }; + + // let user_id = app_state.user_id; + // let request_id = Uuid::new_v4(); + // let request_span = tracing::info_span!( + // "Validating a new stack", %request_id, + // commonDomain=?form.common_domain, + // region=?form.region, + // domainList=?form.domain_list + // ); + // + // // using `enter` is an async function + // let _request_span_guard = request_span.enter(); // ->exit + // + // tracing::info!( + // "request_id {} Adding '{}' '{}' as a new stack", + // request_id, + // form.common_domain, + // form.region + // ); + // + // let query_span = tracing::info_span!( + // "Saving new stack details into the database" + // ); + // + // // match sqlx::query!( + // // r#" + // // INSERT INTO user_stack (id, user_id, name, created_at, updated_at) + // // VALUES ($1, $2, $3, $4, $5) + // // "#, + // // 0_i32, + // // user_id, + // // form.common_domain, + // // Utc::now(), + // // Utc::now() + // // ) + // // .execute(pool.get_ref()) + // // .instrument(query_span) + // // .await + // // { + // // Ok(_) => { + // // tracing::info!( + // // "req_id: {} New stack details have been saved to database", + // // request_id + // // ); + // // HttpResponse::Ok().finish() + // // } + // // Err(e) => { + // // tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); + // // HttpResponse::InternalServerError().finish() + // // } + // // } + + // HttpResponse::Ok().finish() + Ok(Json(stack)) + // Ok(HttpResponse::Ok().finish()) +} diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs new file mode 100644 index 0000000..67edaa8 --- /dev/null +++ b/src/routes/stack/deploy.rs @@ -0,0 +1,5 @@ +use actix_web::HttpResponse; + +pub async fn deploy() -> HttpResponse { + unimplemented!() +} \ No newline at end of file diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs new file mode 100644 index 0000000..734f22b --- /dev/null +++ b/src/routes/stack/get.rs @@ -0,0 +1,38 @@ +use actix_web::{web, HttpResponse}; +// use chrono::Utc; +use sqlx::PgPool; +// use uuid::Uuid; + +pub async fn get( + id: web::Path, + pool: web::Data, +) -> HttpResponse { + let id = id.into_inner(); + tracing::info!("Get stack by id {:?}", id); + + match sqlx::query!( + r#" + SELECT id FROM user_stack + WHERE id=$1 + "#, + id.parse::().unwrap() + ) + .fetch_one(pool.get_ref()) + .await + { + Ok(_) => { + tracing::info!("Stack found by id {}", id); + HttpResponse::Ok().finish() + } + Err(e) => { + tracing::error!("Failed to execute query: {:?}", e); + HttpResponse::NotFound().finish() + } + } +} + + +pub async fn validate_stack () -> HttpResponse { + unimplemented!(); +} + diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs new file mode 100644 index 0000000..f3e5bc9 --- /dev/null +++ b/src/routes/stack/mod.rs @@ -0,0 +1,8 @@ +pub mod add; +pub mod deploy; +pub mod get; +pub mod update; +pub use add::*; +pub use update::*; +pub use deploy::*; +pub use get::*; diff --git a/src/routes/stack/update.rs b/src/routes/stack/update.rs new file mode 100644 index 0000000..5a4fa0c --- /dev/null +++ b/src/routes/stack/update.rs @@ -0,0 +1,4 @@ +use actix_web::HttpResponse; +pub async fn update() -> HttpResponse { + unimplemented!() +} diff --git a/src/services/mod.rs b/src/services/mod.rs index e69de29..4d551f8 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -0,0 +1 @@ +mod stack; \ No newline at end of file diff --git a/src/services/stack.rs b/src/services/stack.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/startup.rs b/src/startup.rs index 747922b..615fcab 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,32 +1,94 @@ -use actix_web::dev::Server; +use actix_cors::Cors; +use actix_web::dev::{Server, ServiceRequest}; use actix_web::middleware::Logger; +use actix_web::HttpMessage; use actix_web::{ - http::header::HeaderName, - web::{self, Form}, - App, HttpServer, + // http::header::HeaderName, + web::{self}, + App, + Error, + HttpServer, }; +use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; +use reqwest::header::{ACCEPT, CONTENT_TYPE}; use sqlx::PgPool; use std::net::TcpListener; +use crate::models::user::User; + +async fn bearer_guard( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result { + eprintln!("{credentials:?}"); + //todo check that credentials.token is a real. get in sync with auth server + //todo get user from auth server + + let client = reqwest::Client::new(); + let resp = client + .get("https://65190108818c4e98ac6000e4.mockapi.io/user/1") //todo add the right url + .bearer_auth(credentials.token()) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .send() + .await + .unwrap() //todo process the response rightly. At moment it's some of something + ; + eprintln!("{resp:?}"); + + let user: User = match resp.status() { + reqwest::StatusCode::OK => match resp.json().await { + Ok(user) => user, + Err(err) => panic!("can't parse the user from json {err:?}"), //todo + }, + other => { + //todo process the other status code accordingly + panic!("unexpected status code {other}"); + } + }; + + //let user = User { id: 1 }; + tracing::info!("authentication middleware. {user:?}"); + let existent_user = req.extensions_mut().insert(user); + if existent_user.is_some() { + tracing::error!("authentication middleware. already logged {existent_user:?}"); + //return Err(("".into(), req)); + } + Ok(req) +} + pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { let db_pool = web::Data::new(db_pool); let server = HttpServer::new(move || { App::new() .wrap(Logger::default()) + .wrap(HttpAuthentication::bearer(bearer_guard)) + .wrap(Cors::permissive()) .service( - web::resource("/health_check") - .route(web::get() - .to(crate::routes::health_check)), + web::resource("/health_check").route(web::get().to(crate::routes::health_check)), ) .service( web::resource("/rating") .route(web::get().to(crate::routes::rating)) .route(web::post().to(crate::routes::rating)), ) + // .service( + // web::resource("/stack/{id}") + // .route(web::get() + // .to(crate::routes::stack::get)) + // .route(web::post() + // .to(crate::routes::stack::update)) + // .route(web::post() + // .to(crate::routes::stack::add)), + // ) + .service(web::resource("/stack").route(web::post().to(crate::routes::stack::add::add))) + .service( + web::resource("/stack/deploy").route(web::post().to(crate::routes::stack::deploy)), + ) .app_data(db_pool.clone()) }) - .listen(listener)? - .run(); + .listen(listener)? + .run(); Ok(server) } diff --git a/src/telemetry.rs b/src/telemetry.rs index d950298..724381a 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,23 +1,22 @@ +use tracing::subscriber::{self, set_global_default}; use tracing::Subscriber; -use tracing::subscriber::{set_global_default, self}; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; -use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; use tracing_log::LogTracer; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; pub fn get_subscriber( name: String, - env_filter: String -// Subscriber is a trait for our spans, Send - trait for thread safety to send to another thread, Sync - trait for thread safety share between trheads -) -> impl Subscriber + Send + Sync { - + env_filter: String, // Subscriber is a trait for our spans, Send - trait for thread safety to send to another thread, Sync - trait for thread safety share between trheads +) -> impl Subscriber + Send + Sync { // when tracing_subscriber is used, env_logger is not needed // env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); let formatting_layer = BunyanFormattingLayer::new( - name, + name, // Output the formatted spans to stdout. - std::io::stdout + std::io::stdout, ); // the with method is provided by the SubscriberExt trait for Subscriber exposed by tracing_subscriber Registry::default() @@ -27,10 +26,10 @@ pub fn get_subscriber( } pub fn init_subscriber(subscriber: impl Subscriber + Send + Sync) { - // set_global_default + // set_global_default //redirect all log's events to the tracing subscriber LogTracer::init().expect("Failed to set logger."); // Result set_global_default(subscriber).expect("Failed to set subscriber."); -} \ No newline at end of file +} diff --git a/tests/health_check.rs b/tests/health_check.rs index fac89b7..f2a8323 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -87,59 +87,3 @@ async fn spawn_app() -> TestApp { db_pool: connection_pool, } } - -#[tokio::test] -async fn subscribe_returns_a_200_for_valid_form_data() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; // %20 - space, %40 - @ - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!(200, response.status().as_u16()); - - let saved = sqlx::query!("SELECT email, name FROM subscriptions",) - .fetch_one(&app.db_pool) - .await - .expect("Failed to fetch saved subscription."); - - assert_eq!(saved.email, "ursula_le_guin@gmail.com"); - assert_eq!(saved.name, "le guin"); -} - -#[tokio::test] -async fn subscribe_returns_a_400_when_data_is_missing() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let test_cases = vec![ - ("name=le%20guin", "missing the email"), - ("email=ursula_le_guin%40gmail.com", "missing the name"), - ("", "missing both name and email"), - ]; - - for (invalid_body, error_message) in test_cases { - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(invalid_body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!( - 400, - response.status().as_u16(), - "The API did not fail with 400 Bad Request when the payload was {}.", - error_message - ); - } -}