├── .autoenv ├── .bashrc ├── .credo.exs ├── .github └── workflows │ ├── pull-request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── .rubocop.yml ├── .slim-lint.yml ├── .stylelintrc.yml ├── Makefile ├── README.md ├── TODO.md ├── ansible.cfg ├── ansible ├── development.yml ├── development │ ├── group_vars │ │ └── all │ │ │ ├── vars.yml │ │ │ └── vault.yml │ └── inventory.yml ├── group_vars │ ├── all.yml │ └── inventory.yml ├── production │ ├── group_vars │ │ └── all │ │ │ ├── vars.yml │ │ │ └── vault.yml │ └── inventory.yml ├── templates │ ├── environment.j2 │ └── secrets.auto.tfvars.j2 └── terraform.yml ├── docker-compose.yml ├── k8s ├── Makefile ├── app-chart │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── autoscaler.yaml │ │ ├── deployment.yaml │ │ ├── ingress-nginx.yaml │ │ └── service.yaml │ └── values.yaml ├── exercises-css.job.yml ├── exercises-elixir.job.yml ├── exercises-go.job.yml ├── exercises-html.job.yml ├── exercises-java.job.yml ├── exercises-javascript.job.yml ├── exercises-php.job.yml ├── exercises-python.job.yml ├── exercises-racket.job.yml ├── exercises-ruby.job.yml ├── nginx-config.yaml ├── values-external-dns.yml ├── values-gcloud-proxy.yml └── values-nginx-ingress.yml ├── make-compose.mk ├── make-services-caddy.mk ├── make-services-web.mk ├── mix.exs ├── mix.lock ├── package-lock.json ├── package.json ├── services ├── caddy │ ├── Dockerfile.development │ ├── Dockerfile.production │ └── files │ │ ├── development │ │ └── Caddyfile │ │ └── production │ │ └── Caddyfile ├── static │ └── index.html └── web │ ├── .dockerignore │ ├── .env.docker │ ├── .eslintignore │ ├── .eslintrc.yml │ ├── .formatter.exs │ ├── .iex.exs │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── assets │ ├── babel.config.js │ ├── css │ │ ├── app.scss │ │ ├── hexlet-basics.scss │ │ └── variables.scss │ ├── js │ │ ├── app.js │ │ ├── lesson │ │ │ ├── EntityContext.js │ │ │ ├── components │ │ │ │ ├── App.jsx │ │ │ │ ├── Console.jsx │ │ │ │ ├── ControlBox.jsx │ │ │ │ ├── Editor.jsx │ │ │ │ ├── HTMLPreview.jsx │ │ │ │ ├── Solution.jsx │ │ │ │ └── TabsBox.jsx │ │ │ ├── connect.js │ │ │ ├── index.js │ │ │ ├── init.jsx │ │ │ ├── routes.js │ │ │ └── slices │ │ │ │ ├── checkInfo.js │ │ │ │ ├── currentTabInfo.js │ │ │ │ ├── editor.js │ │ │ │ ├── index.js │ │ │ │ ├── lessonState.js │ │ │ │ └── solutionState.js │ │ ├── lib │ │ │ ├── ansi_up.js │ │ │ ├── configureStore.js │ │ │ ├── data-fns-locale.js │ │ │ ├── i18n.js │ │ │ └── markdown.js │ │ ├── shared.js │ │ └── socket.js │ ├── locales │ │ ├── en │ │ │ └── translation.json │ │ └── ru │ │ │ └── translation.json │ ├── package-lock.json │ ├── package.json │ ├── static │ │ ├── favicon.ico │ │ └── images │ │ │ ├── css.png │ │ │ ├── elixir.png │ │ │ ├── fake_output_topbar.png │ │ │ ├── favicons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-114x114.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-144x144.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-167x167.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── apple-touch-icon-57x57.png │ │ │ ├── apple-touch-icon-72x72.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-precomposed.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-150x150.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ │ │ ├── flag-en.svg │ │ │ ├── flag-ru.svg │ │ │ ├── go.png │ │ │ ├── hexlet_logo.png │ │ │ ├── html.png │ │ │ ├── java.png │ │ │ ├── javascript.png │ │ │ ├── logo.png │ │ │ ├── party_emoj.png │ │ │ ├── php.png │ │ │ ├── prof_icons │ │ │ ├── frontend.svg │ │ │ ├── layout-designer.svg │ │ │ ├── php.svg │ │ │ └── python.svg │ │ │ ├── python.png │ │ │ ├── racket.png │ │ │ ├── ruby.png │ │ │ ├── smm_cover.jpg │ │ │ └── smm_cover.png │ └── webpack.config.js │ ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ ├── prod.secret.exs │ └── test.exs │ ├── deploy-notify.sh │ ├── docker-compose.test.yml │ ├── lib │ ├── ext_enum.ex │ ├── hexlet_basics.ex │ ├── hexlet_basics │ │ ├── application.ex │ │ ├── email.ex │ │ ├── language.ex │ │ ├── language │ │ │ ├── module.ex │ │ │ └── module │ │ │ │ ├── description.ex │ │ │ │ ├── lesson.ex │ │ │ │ ├── lesson │ │ │ │ ├── description.ex │ │ │ │ └── scope.ex │ │ │ │ └── scope.ex │ │ ├── mailer.ex │ │ ├── notifier.ex │ │ ├── page_title.ex │ │ ├── repo.ex │ │ ├── state_machines │ │ │ ├── user │ │ │ │ └── email_delivery_state_machine.ex │ │ │ └── user_state_machine.ex │ │ ├── upload.ex │ │ ├── user.ex │ │ ├── user │ │ │ ├── account.ex │ │ │ ├── finished_lesson.ex │ │ │ └── scope.ex │ │ ├── user_manager.ex │ │ └── user_manager │ │ │ ├── error_handler.ex │ │ │ ├── guardian.ex │ │ │ └── pipeline.ex │ ├── hexlet_basics_web.ex │ ├── hexlet_basics_web │ │ ├── channels │ │ │ └── user_socket.ex │ │ ├── controllers │ │ │ ├── api │ │ │ │ ├── lesson │ │ │ │ │ └── check_controller.ex │ │ │ │ ├── lesson_controller.ex │ │ │ │ └── webhooks │ │ │ │ │ └── sparkpost_controller.ex │ │ │ ├── auth_controller.ex │ │ │ ├── language │ │ │ │ ├── module │ │ │ │ │ └── lesson_controller.ex │ │ │ │ └── module_controller.ex │ │ │ ├── language_controller.ex │ │ │ ├── lesson_controller.ex │ │ │ ├── locale_controller.ex │ │ │ ├── page_controller.ex │ │ │ ├── password_controller.ex │ │ │ ├── profile_controller.ex │ │ │ ├── remind_password_controller.ex │ │ │ ├── session_controller.ex │ │ │ └── user_controller.ex │ │ ├── endpoint.ex │ │ ├── gettext.ex │ │ ├── helpers │ │ │ ├── auth.ex │ │ │ ├── custom_url.ex │ │ │ └── language_styles.ex │ │ ├── plugs │ │ │ ├── api_require_auth.ex │ │ │ ├── assign_current_user.ex │ │ │ ├── assign_globals.ex │ │ │ ├── change_locale.ex │ │ │ ├── check_authentication.ex │ │ │ ├── detect_domain_for_root.ex │ │ │ ├── detect_locale_by_host.ex │ │ │ ├── require_auth.ex │ │ │ ├── set_locale.ex │ │ │ └── set_url.ex │ │ ├── router.ex │ │ ├── schemas │ │ │ ├── company_schema.ex │ │ │ └── language │ │ │ │ ├── module │ │ │ │ └── lesson_schema.ex │ │ │ │ └── module_schema.ex │ │ ├── serializers │ │ │ └── prev_lesson_serializer.ex │ │ ├── templates │ │ │ ├── email │ │ │ │ ├── confirmation.en.html.slime │ │ │ │ ├── confirmation.ru.html.slime │ │ │ │ ├── reset_password.en.html.slime │ │ │ │ └── reset_password.ru.html.slime │ │ │ ├── language │ │ │ │ ├── module │ │ │ │ │ ├── lesson │ │ │ │ │ │ ├── index.html.slime │ │ │ │ │ │ └── show.html.slime │ │ │ │ │ └── show.html.slime │ │ │ │ └── show.html.slime │ │ │ ├── layout │ │ │ │ ├── app.html.slime │ │ │ │ ├── email.html.slime │ │ │ │ ├── email.text.slime │ │ │ │ ├── lesson.html.slime │ │ │ │ └── shared │ │ │ │ │ ├── auth.html.slime │ │ │ │ │ ├── head.html.slime │ │ │ │ │ ├── header.html.slime │ │ │ │ │ └── start_body_scripts.html.slime │ │ │ ├── page │ │ │ │ ├── about.html.slime │ │ │ │ ├── index.html.slime │ │ │ │ ├── privacy.html.slim │ │ │ │ ├── robots.txt.eex │ │ │ │ └── tos.html.slim │ │ │ ├── password │ │ │ │ └── edit.html.slime │ │ │ ├── profile │ │ │ │ └── show.html.slime │ │ │ ├── remind_password │ │ │ │ └── new.html.slime │ │ │ ├── session │ │ │ │ └── new.html.slime │ │ │ ├── shared │ │ │ │ └── social_sign_in.html.slime │ │ │ └── user │ │ │ │ └── new.html.slime │ │ └── views │ │ │ ├── email_view.ex │ │ │ ├── error_helpers.ex │ │ │ ├── error_view.ex │ │ │ ├── language │ │ │ ├── module │ │ │ │ └── lesson_view.ex │ │ │ └── module_view.ex │ │ │ ├── language_view.ex │ │ │ ├── layout │ │ │ └── shared.ex │ │ │ ├── layout_view.ex │ │ │ ├── page_view.ex │ │ │ ├── password_view.ex │ │ │ ├── profile_view.ex │ │ │ ├── remind_password_view.ex │ │ │ ├── session_view.ex │ │ │ ├── shared_view.ex │ │ │ └── user_view.ex │ └── mix │ │ └── tasks │ │ └── exercises.ex │ ├── mix.exs │ ├── mix.lock │ ├── package-lock.json │ ├── postcss.config.js │ ├── priv │ ├── gettext │ │ ├── default.pot │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ ├── default.po │ │ │ │ └── errors.po │ │ ├── errors.pot │ │ └── ru │ │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ └── repo │ │ ├── migrations │ │ └── 20190911072926_migrate_email_deliviery_state_for_user.exs │ │ ├── seeds.exs │ │ └── structure.sql │ ├── test │ ├── hexlet_basics │ │ └── user_manager_test.exs │ ├── hexlet_basics_web │ │ ├── controllers │ │ │ ├── api │ │ │ │ ├── lesson │ │ │ │ │ └── check_controller_test.exs │ │ │ │ └── sparkpost_controller_test.exs │ │ │ ├── auth_controller_test.exs │ │ │ ├── language │ │ │ │ ├── module │ │ │ │ │ └── lesson_controller_test.exs │ │ │ │ └── module_controller_test.exs │ │ │ ├── language_controller_test.exs │ │ │ ├── lesson_controller_test.exs │ │ │ ├── locale_controller_test.exs │ │ │ ├── page_controller_test.exs │ │ │ ├── password_controller_test.exs │ │ │ ├── profile_controller_test.exs │ │ │ ├── remind_password_controller_test.exs │ │ │ ├── session_controller_test.exs │ │ │ └── user_controller_test.exs │ │ └── views │ │ │ ├── error_view_test.exs │ │ │ ├── layout_view_test.exs │ │ │ └── page_view_test.exs │ ├── support │ │ ├── channel_case.ex │ │ ├── conn_case.ex │ │ ├── data_case.ex │ │ ├── factories │ │ │ ├── language_factory.ex │ │ │ ├── language_module_description_factory.ex │ │ │ ├── language_module_factory.ex │ │ │ ├── language_module_lesson_description_factory.ex │ │ │ ├── language_module_lesson_factory.ex │ │ │ ├── upload_factory.ex │ │ │ ├── user_account_factory.ex │ │ │ ├── user_factory.ex │ │ │ └── user_finished_lesson_factory.ex │ │ └── factory.ex │ └── test_helper.exs │ └── tsconfig.json └── terraform ├── Makefile ├── backend.tf ├── cloudbuild.tf ├── cloudflare.tf ├── dok8s.tf ├── kubernetes.tf ├── project.tf ├── source.tf ├── sql.tf ├── variables.tf └── versions.tf /.autoenv: -------------------------------------------------------------------------------- 1 | KUBECONFIG=~/hexlet_basics/.kube/config3 2 | -------------------------------------------------------------------------------- /.bashrc: -------------------------------------------------------------------------------- 1 | set -o vi 2 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # config/.credo.exs 2 | %{ 3 | configs: [ 4 | %{ 5 | name: "default", 6 | files: %{ 7 | included: [ 8 | "services/web/lib/", 9 | "services/web/test/", 10 | "services/web/config/" 11 | ], 12 | excluded: [] 13 | }, 14 | # checks: [ 15 | # {Credo.Check.Consistency.TabsOrSpaces}, 16 | 17 | # # For some checks, like AliasUsage, you can only customize the priority 18 | # # Priority values are: `low, normal, high, higher` 19 | # {Credo.Check.Design.AliasUsage, priority: :low}, 20 | 21 | # # For others you can also set parameters 22 | # {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, 23 | 24 | # # You can also customize the exit_status of each check. 25 | # # If you don't want TODO comments to cause `mix credo` to fail, just 26 | # # set this value to 0 (zero). 27 | # {Credo.Check.Design.TagTODO, exit_status: 2}, 28 | 29 | # # To deactivate a check: 30 | # # Put `false` as second element: 31 | # {Credo.Check.Design.TagFIXME, false}, 32 | 33 | # ... several checks omitted for readability ... 34 | # ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: On Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Run web tests 17 | run: | 18 | make web-ci-test 19 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: On Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | 10 | build: 11 | if: github.repository == 'hexlet-basics/hexlet_basics' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Run web tests 16 | run: | 17 | make web-ci-test 18 | - name: Login to Docker Hub 19 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 20 | - name: Build Web 21 | run: make web-docker-build-production 22 | - name: Push Web 23 | run: make web-docker-push 24 | - name: Build Caddy 25 | run: make caddy-docker-build-production 26 | - name: Push Caddy 27 | run: make caddy-docker-push 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: On Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | 10 | build: 11 | if: github.repository == 'hexlet-basics/hexlet_basics' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Run web tests 16 | run: | 17 | make web-ci-test 18 | - name: Login to Docker Hub 19 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 20 | - name: Set Tag To The Env 21 | run: echo ::set-env name=RELEASE_VERSION::$(echo ${GITHUB_REF:10}) 22 | - name: Build Web 23 | run: make web-docker-build-production VERSION=$RELEASE_VERSION 24 | - name: Push Web 25 | run: make web-docker-push VERSION=$RELEASE_VERSION 26 | - name: Build Caddy 27 | run: make caddy-docker-build-production VERSION=$RELEASE_VERSION 28 | - name: Push Caddy 29 | run: make caddy-docker-push VERSION=$RELEASE_VERSION 30 | - uses: rtCamp/action-slack-notify@v2.0.0 31 | env: 32 | SLACK_CHANNEL: '#basics-code-auto' 33 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | _build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /services/web/deps/ 9 | /deps/ 10 | 11 | # Where 3rd-party dependencies like ExDoc output generated docs. 12 | /doc/ 13 | 14 | # Ignore .fetch files in case you like to edit your project deps locally. 15 | /.fetch 16 | 17 | # If the VM crashes, it generates a dump, let's ignore it too. 18 | erl_crash.dump 19 | 20 | # Also ignore archive artifacts (built via "mix archive.build"). 21 | *.ez 22 | 23 | # Ignore package tarball (built via "mix hex.build"). 24 | hello-*.tar 25 | 26 | # If NPM crashes, it generates a log, let's ignore it too. 27 | npm-debug.log 28 | 29 | # The directory NPM downloads your dependencies sources to. 30 | node_modules 31 | 32 | # Since we are building assets from assets/, 33 | # we ignore priv/static. You may want to comment 34 | # this depending on your deployment strategy. 35 | /services/web/priv/static/ 36 | 37 | # HEXLET 38 | 39 | *.backup 40 | .terraform 41 | secrets.auto.tfvars 42 | *.tfstate 43 | google.key.json 44 | .env 45 | .elixir_ls 46 | tmp 47 | .kube 48 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Lint/Syntax: 2 | Enabled: false 3 | -------------------------------------------------------------------------------- /.slim-lint.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | LineLength: 3 | enabled: false 4 | -------------------------------------------------------------------------------- /.stylelintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: 4 | - stylelint-config-recommended 5 | - stylelint-config-standard 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | S := web 2 | VERSION := latest 3 | PROJECT := hexlet-basics 4 | 5 | include make-compose.mk 6 | include make-services-web.mk 7 | include make-services-caddy.mk 8 | include k8s/Makefile 9 | 10 | project-setup: project-files-touch project-env-generate compose-setup 11 | npm install 12 | git clone git://github.com/inishchith/autoenv.git ~/.autoenv || true 13 | grep -qxF 'source ~/.autoenv/activate.sh' ~/.bash_profile || echo 'source ~/.autoenv/activate.sh' >> ~/.bash_profile 14 | grep -qxF 'export AUTOENV_ENV_FILENAME=.autoenv' ~/.bash_profile || echo 'export AUTOENV_ENV_FILENAME=.autoenv' >> ~/.bash_profile 15 | grep -qxF 'export AUTOENV_ENV_LEAVE_FILENAME=.autoenv.leave' ~/.bash_profile || echo 'export AUTOENV_ENV_LEAVE_FILENAME=.autoenv.leave' >> ~/.bash_profile 16 | grep -qxF 'export AUTOENV_ENABLE_LEAVE=true' ~/.bash_profile || echo 'export AUTOENV_ENABLE_LEAVE=true' >> ~/.bash_profile 17 | export AUTOENV_ENV_FILENAME=.autoenv 18 | mkdir .kube 19 | 20 | cluster-setup: 21 | doctl auth init 22 | doctl kubernetes cluster kubeconfig save hexlet-basics-3 23 | kubectx do-fra1-hexlet-basics-3 24 | 25 | project-files-touch: 26 | mkdir -p tmp 27 | if [ ! -f tmp/ansible-vault-password ]; then echo 'jopa' > tmp/ansible-vault-password; fi; 28 | 29 | project-env-generate: 30 | docker run --rm -e RUNNER_PLAYBOOK=ansible/development.yml \ 31 | -v $(CURDIR)/ansible/development:/runner/inventory \ 32 | -v $(CURDIR):/runner/project \ 33 | ansible/ansible-runner 34 | 35 | terraform-vars-generate: 36 | docker run --rm -e RUNNER_PLAYBOOK=ansible/terraform.yml \ 37 | -v $(CURDIR)/ansible/production:/runner/inventory \ 38 | -v $(CURDIR):/runner/project \ 39 | ansible/ansible-runner 40 | 41 | ansible-vaults-edit: 42 | # docker run -it -v $(CURDIR):/web -w /web ansible ansible-vault edit ansible/production/group_vars/all/vault.yml --vault-password-file=tmp/ansible-vault-password 43 | docker run -it --rm \ 44 | -v $(CURDIR):/runner/project \ 45 | ansible/ansible-runner ansible-vault edit project/ansible/production/group_vars/all/vault.yml 46 | tag: 47 | git tag $(TAG) && git push --tags 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![On Push](https://github.com/hexlet-basics/hexlet_basics/workflows/On%20Push/badge.svg?branch=master)](https://github.com/hexlet-basics/hexlet_basics/actions) 2 | 3 | # This repository is outdated. See the new version on [hexlet-basics](https://github.com/hexlet-basics/hexlet-basics) 4 | 5 | ## 6 | [![Hexlet Ltd. logo](https://raw.githubusercontent.com/Hexlet/hexletguides.github.io/master/images/hexlet_logo128.png)](https://ru.hexlet.io/pages/about?utm_source=github&utm_medium=link&utm_campaign=hexlet-basics) 7 | 8 | This repository is created and maintained by the team and the community of Hexlet, an educational project. [Read more about Hexlet (in Russian)](https://ru.hexlet.io/pages/about?utm_source=github&utm_medium=link&utm_campaign=hexlet-basics). 9 | ## 10 | 11 | ## Development 12 | 13 | ### Requirements 14 | 15 | * Mac / Linux 16 | * Docker 17 | * Docker Compose 18 | * Node & npm 19 | 20 | ### Install 21 | 22 | ```sh 23 | $ make project-setup 24 | ``` 25 | 26 | Add to _/etc/hosts_: 27 | 28 | 127.0.0.1 code-basics.test ru.code-basics.test en.code-basics.test 29 | 30 | ### Load content 31 | 32 | ```sh 33 | $ make web-exercises-load-php 34 | ``` 35 | 36 | ### Run 37 | 38 | ```sh 39 | $ make compose 40 | ``` 41 | 42 | Go to [https://ru.code-basics.test](https://ru.code-basics.test) 43 | Go to [https://en.code-basics.test](https://en.code-basics.test) 44 | 45 | 46 | ## Kubernetes (Production) 47 | 48 | ### Requirements 49 | 50 | * doctl 51 | * kubectl 52 | * [kubectx](https://github.com/ahmetb/kubectx) 53 | * [autoenv](https://github.com/inishchith/autoenv) 54 | 55 | ### Setup 56 | 57 | * doctl kubernetes cluster kubeconfig save 58 | 59 | ```sh 60 | $ make cluster-setup 61 | ``` 62 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | kubectl create serviceaccount --namespace kube-system tiller 2 | kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller 3 | kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}' 4 | 5 | helm repo add rimusz https://charts.rimusz.net 6 | helm repo update 7 | 8 | https://medium.com/@at_ishikawa/enable-https-by-cert-manager-on-gke-3996f84fffe5 9 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | force_color = true 3 | roles_path = tmp/ansible-roles 4 | gathering = smart 5 | fact_caching = jsonfile 6 | fact_caching_connection = tmp/ansible-cached-facts 7 | fact_caching_timeout = 86400 8 | host_key_checking = False 9 | vault_password_file = tmp/ansible-vault-password 10 | 11 | [ssh_connection] 12 | ssh_args=-o ForwardAgent=yes 13 | # pipelining=True 14 | # scp_if_ssh=True 15 | 16 | [inventory] 17 | enable_plugins = yaml 18 | -------------------------------------------------------------------------------- /ansible/development.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | gather_facts: false 5 | tasks: 6 | - template: 7 | src: environment.j2 8 | dest: '/runner/project/.env' 9 | tags: env 10 | 11 | # TODO: add https://cloud.google.com/sdk/docs/quickstart-debian-ubuntu 12 | # - lineinfile: 13 | # path: /etc/hosts 14 | # regexp: 'hexlet-basics' 15 | # line: '127.0.0.1 ru.hexlet-basics.test en.hexlet-basics.test # hexlet-basics' 16 | # become: yes 17 | -------------------------------------------------------------------------------- /ansible/development/group_vars/all/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | hexlet_basics_mix_env: dev 4 | hexlet_basics_run_user: "{{ ansible_ssh_user }}" 5 | hexlet_basics_run_user_home: "{{ lookup('env','HOME') }}" 6 | hexlet_basics_env_file: "{{ lookup('env','PWD') }}/.env" 7 | hexlet_basics_node_env: development 8 | hexlet_basics_app_host: "code-basics.test" 9 | hexlet_basics_app_ru_host: "ru.code-basics.test" 10 | hexlet_basics_app_scheme: "https" 11 | -------------------------------------------------------------------------------- /ansible/development/group_vars/all/vault.yml: -------------------------------------------------------------------------------- 1 | # -*- mode: yaml; vault: true; -*- 2 | --- 3 | 4 | hexlet_basics_vault_github_client_id: "4c8040727fface68d59a" 5 | hexlet_basics_vault_github_client_secret: "3bd561a6859ba09b4427d6960a89e4a9060cf605" 6 | hexlet_basics_vault_facebook_client_id: "421592815347082" 7 | hexlet_basics_vault_facebook_client_secret: "05e0272e68a45aac9e96129a3f89ebc1" 8 | hexlet_basics_vault_db_hostname: "db" 9 | hexlet_basics_vault_db_username: "postgres" 10 | hexlet_basics_vault_db_pool_size: 2 11 | hexlet_basics_vault_db_password: "" 12 | hexlet_basics_vault_db_name: "hexlet_basics_dev" 13 | hexlet_basics_vault_db_port: "5432" 14 | hexlet_basics_vault_db_ssl_mode: "false" 15 | hexlet_basics_vault_secret_key_base: "as df;lkajsdf ;alkjsdf ;alksdjf ;alksdjf ;alskdjf ;alskdjf a;lsdjkf ;alsdkjfa ;lskdjf a;lskdj f;alksdjf ;alksjdfal;sdjfqwerty" 16 | hexlet_basics: 17 | vault_rollbar_access_token: "token" 18 | 19 | hexlet_basics_vault_sparkpost_smtp_username: "sparkpost" 20 | hexlet_basics_vault_sparkpost_smtp_password: "password" 21 | hexlet_basics_vault_guardian_secret_key: "asdfasdfasdfasdf" 22 | -------------------------------------------------------------------------------- /ansible/development/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | all: 4 | hosts: 5 | localhost: 6 | gather_facts: false 7 | ansible_connection: local 8 | -------------------------------------------------------------------------------- /ansible/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | hexlet_basics_repository: "git@github.com:hexlet-basics/hexlet_basics.git" 4 | hexlet_basics_directory_to_clone: /var/tmp/hexlet_basics 5 | hexlet_basics_port: 4000 6 | hexlet_basics_docker_network: "hexlet-basics" 7 | 8 | docker_install_compose: false 9 | docker_package_state: latest 10 | -------------------------------------------------------------------------------- /ansible/group_vars/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | all: 4 | vars: 5 | ansible_connection: local 6 | -------------------------------------------------------------------------------- /ansible/production/group_vars/all/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | hexlet_basics_mix_env: prod 4 | hexlet_basics_run_user: ubuntu 5 | hexlet_basics_run_user_home: /home/ubuntu 6 | hexlet_basics_env_file: "{{ hexlet_basics_run_user_home }}/.env" 7 | hexlet_basics_node_env: production 8 | hexlet_basics_app_host: "code-basics.com" 9 | hexlet_basics_app_ru_host: "ru.code-basics.com" 10 | hexlet_basics_app_scheme: "https" 11 | -------------------------------------------------------------------------------- /ansible/production/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | all: 4 | hosts: 5 | localhost: 6 | gather_facts: false 7 | ansible_connection: local 8 | -------------------------------------------------------------------------------- /ansible/templates/environment.j2: -------------------------------------------------------------------------------- 1 | GITHUB_CLIENT_ID={{ hexlet_basics_vault_github_client_id }} 2 | GITHUB_CLIENT_SECRET={{ hexlet_basics_vault_github_client_secret }} 3 | FACEBOOK_CLIENT_ID={{ hexlet_basics_vault_facebook_client_id }} 4 | FACEBOOK_CLIENT_SECRET={{ hexlet_basics_vault_facebook_client_secret }} 5 | SPARKPOST_SMTP_USERNAME={{ hexlet_basics_vault_sparkpost_smtp_username }} 6 | SPARKPOST_SMTP_PASSWORD={{ hexlet_basics_vault_sparkpost_smtp_password }} 7 | ROLLBAR_ACCESS_TOKEN={{ hexlet_basics.vault_rollbar_access_token }} 8 | PORT={{ hexlet_basics_port }} 9 | DB_NAME={{ hexlet_basics_vault_db_name }} 10 | DB_USERNAME={{ hexlet_basics_vault_db_username }} 11 | DB_PASSWORD={{ hexlet_basics_vault_db_password }} 12 | DB_HOSTNAME={{ hexlet_basics_vault_db_hostname }} 13 | DB_PORT={{ hexlet_basics_vault_db_port }} 14 | DB_SSL_MODE={{ hexlet_basics_vault_db_ssl_mode }} 15 | DB_POOL_SIZE={{ hexlet_basics_vault_db_pool_size }} 16 | SECRET_KEY_BASE={{ hexlet_basics_vault_secret_key_base }} 17 | GUARDIAN_SECRET_KEY={{ hexlet_basics_vault_guardian_secret_key }} 18 | NODE_ENV={{ hexlet_basics_node_env }} 19 | APP_HOST={{ hexlet_basics_app_host }} 20 | APP_SCHEME={{ hexlet_basics_app_scheme }} 21 | APP_RU_HOST={{ hexlet_basics_app_ru_host }} 22 | -------------------------------------------------------------------------------- /ansible/templates/secrets.auto.tfvars.j2: -------------------------------------------------------------------------------- 1 | db_username = "{{ hexlet_basics_vault_db_username }}" 2 | db_password = "{{ hexlet_basics_vault_db_password }}" 3 | db_name = "{{ hexlet_basics_vault_db_name }}" 4 | db_hostname = "{{ hexlet_basics_vault_db_hostname }}" 5 | db_port = "{{ hexlet_basics_vault_db_port }}" 6 | github_oauth_token = "{{ hexlet_basics_vault_github_oauth_token }}" 7 | digitalocean_token = "{{ hexlet_basics_vault_digitalocean_token }}" 8 | cloudflare_api_key = "{{ hexlet_basics_vault_cloudflare_api_key }}" 9 | cloudflare_email = "{{ hexlet_basics_vault_cloudflare_email }}" 10 | github_client_id = "{{ hexlet_basics_vault_github_client_id }}" 11 | github_client_secret = "{{ hexlet_basics_vault_github_client_secret }}" 12 | facebook_client_id = "{{ hexlet_basics_vault_facebook_client_id }}" 13 | facebook_client_secret = "{{ hexlet_basics_vault_facebook_client_secret }}" 14 | secret_key_base = "{{ hexlet_basics_vault_secret_key_base }}" 15 | slack_codebuild_webhook = "{{ hexlet_basics.slack_codebuild_webhook }}" 16 | rollbar_access_token = "{{ hexlet_basics.vault_rollbar_access_token }}" 17 | sparkpost_smtp_username = "{{ hexlet_basics_vault_sparkpost_smtp_username }}" 18 | sparkpost_smtp_password = "{{ hexlet_basics_vault_sparkpost_smtp_password }}" 19 | guardian_secret_key = "{{ hexlet_basics_vault_guardian_secret_key }}" 20 | app_host = "{{ hexlet_basics_app_host }}" 21 | app_ru_host = "{{ hexlet_basics_app_ru_host }}" 22 | app_scheme = "{{ hexlet_basics_app_scheme }}" 23 | -------------------------------------------------------------------------------- /ansible/terraform.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | gather_facts: false 5 | tasks: 6 | - template: 7 | src: secrets.auto.tfvars.j2 8 | dest: '../terraform/secrets.auto.tfvars' 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: '3' 4 | 5 | services: 6 | 7 | db: 8 | image: postgres:12.2-alpine 9 | environment: 10 | POSTGRES_HOST_AUTH_METHOD: trust 11 | container_name: postgres 12 | volumes: 13 | - /var/tmp/db:/var/lib/postgresql/data 14 | networks: 15 | - default 16 | - hexlet-basics 17 | 18 | caddy: 19 | build: 20 | context: services/caddy 21 | dockerfile: Dockerfile.development 22 | ports: 23 | - "443:2015" 24 | - "80:80" 25 | volumes: 26 | - "./services/caddy/files/development/Caddyfile:/etc/Caddyfile" 27 | depends_on: 28 | - web 29 | 30 | web: 31 | build: 32 | context: services/web 33 | dockerfile: Dockerfile 34 | command: mix phx.server 35 | ports: 36 | - "${PORT}:${PORT}" 37 | env_file: '.env' 38 | volumes: 39 | - "web_static:/web/priv/static" 40 | - "./services/web:/app" 41 | - "~/.bash_history:/root/.bash_history:cached" 42 | - "./tmp/hexletbasics:/hexletbasics" 43 | - ".bashrc:/root/.bashrc:cached" 44 | - '/var/run/docker.sock:/var/run/docker.sock' 45 | # - "/var/tmp:/var/tmp:cached" 46 | - "/tmp:/tmp" 47 | depends_on: 48 | - db 49 | 50 | volumes: 51 | pgdata: 52 | # web_node_modules: 53 | web_static: 54 | # app_build: 55 | # app_deps: 56 | # 57 | networks: 58 | hexlet-basics: 59 | external: 60 | name: hexlet-basics_hexlet-basics 61 | -------------------------------------------------------------------------------- /k8s/app-chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /k8s/app-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: HexletBasics 4 | name: app-chart 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /k8s/app-chart/templates/autoscaler.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v1 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: "{{ .Release.Name }}-autoscaler" 5 | spec: 6 | scaleTargetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: "{{ .Release.Name }}-deployment" 10 | minReplicas: 3 11 | maxReplicas: 3 12 | targetCPUUtilizationPercentage: 50 13 | -------------------------------------------------------------------------------- /k8s/app-chart/templates/ingress-nginx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: networking.k8s.io/v1beta1 4 | kind: Ingress 5 | metadata: 6 | name: nginx-ingress 7 | annotations: 8 | kubernetes.io/ingress.class: "nginx" 9 | # https://stackoverflow.com/questions/40791459/upstream-sent-too-big-header-while-reading-response-header-from-upstream 10 | nginx.ingress.kubernetes.io/proxy-buffers-number: "4 256k" 11 | nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" 12 | spec: 13 | rules: 14 | - host: "*.code-basics.com" 15 | http: 16 | paths: 17 | - backend: 18 | serviceName: "{{.Release.Name }}-frontend-service" 19 | servicePort: 8080 20 | - host: "code-basics.com" 21 | http: 22 | paths: 23 | - backend: 24 | serviceName: "{{.Release.Name }}-frontend-service" 25 | servicePort: 8080 26 | - host: "code-basics.ru" 27 | http: 28 | paths: 29 | - backend: 30 | serviceName: "{{.Release.Name }}-frontend-service" 31 | servicePort: 8080 32 | -------------------------------------------------------------------------------- /k8s/app-chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: "{{ .Release.Name }}-frontend-service" 6 | labels: 7 | app.kubernetes.io/name: "{{ .Chart.Name }}" 8 | app.kubernetes.io/instance: "{{ .Release.Name }}" 9 | app.kubernetes.io/version: "{{ .Chart.AppVersion }}" 10 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 11 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" 12 | spec: 13 | selector: 14 | app.kubernetes.io/name: "{{ .Release.Name }}-web-pod" 15 | type: NodePort 16 | ports: 17 | - protocol: TCP 18 | port: 8080 19 | targetPort: 8080 20 | -------------------------------------------------------------------------------- /k8s/app-chart/values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: v283 4 | 5 | replicaCount: 3 6 | 7 | caddy: 8 | image: 9 | repository: hexletbasics/services-caddy 10 | imagePullPolicy: Always 11 | 12 | web: 13 | image: 14 | repository: hexletbasics/services-web 15 | imagePullPolicy: Always 16 | 17 | slack_notification: ['/app/deploy-notify.sh'] 18 | 19 | resources: {} 20 | # We usually recommend not to specify default resources and to leave this as a conscious 21 | # choice for the user. This also increases chances charts run on environments with little 22 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 23 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 24 | # limits: 25 | # cpu: 100m 26 | # memory: 128Mi 27 | # requests: 28 | # cpu: 100m 29 | # memory: 128Mi 30 | 31 | nodeSelector: {} 32 | 33 | tolerations: [] 34 | 35 | affinity: {} 36 | -------------------------------------------------------------------------------- /k8s/exercises-css.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-css-job 7 | spec: 8 | template: 9 | spec: 10 | restartPolicy: Never 11 | volumes: 12 | - name: pod-data 13 | emptyDir: {} 14 | initContainers: 15 | - name: exercises-css 16 | image: hexletbasics/exercises-css:latest 17 | command: 18 | - cp 19 | - "-r" 20 | - "/exercises-css/." 21 | - "/out" 22 | volumeMounts: 23 | - mountPath: /out 24 | name: pod-data 25 | containers: 26 | - name: app 27 | imagePullPolicy: Always 28 | image: hexletbasics/services-web:v271 29 | command: 30 | - mix 31 | - x.exercises.load 32 | - css 33 | volumeMounts: 34 | - mountPath: /hexletbasics/exercises-css 35 | name: pod-data 36 | resources: 37 | requests: 38 | cpu: 10m 39 | envFrom: 40 | - secretRef: 41 | name: hexlet-basics-secrets 42 | - secretRef: 43 | name: sparkpost-credentials 44 | - secretRef: 45 | name: github-credentials 46 | - configMapRef: 47 | name: hexlet-basics-config-map 48 | -------------------------------------------------------------------------------- /k8s/exercises-elixir.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-elixir-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-elixir 19 | image: hexletbasics/exercises-elixir 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-elixir/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - elixir 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-elixir 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | 54 | -------------------------------------------------------------------------------- /k8s/exercises-go.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-go-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-go 19 | image: hexletbasics/exercises-go 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-go/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - go 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-go 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-html.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-html-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-html 19 | image: hexletbasics/exercises-html 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-html/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - html 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-html 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-java.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-java-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-java 19 | image: hexletbasics/exercises-java 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-java/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - java 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-java 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-javascript.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-javascript-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-javascript 19 | image: hexletbasics/exercises-javascript 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-javascript/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - javascript 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-javascript 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-php.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-php-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-php 19 | image: hexletbasics/exercises-php 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-php/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - php 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-php 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-python.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-python-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-python 19 | image: hexletbasics/exercises-python:latest 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-python/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - python 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-python 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: github-credentials 49 | - secretRef: 50 | name: sparkpost-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-racket.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-racket-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-racket 19 | image: hexletbasics/exercises-racket 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-racket/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - racket 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-racket 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/exercises-ruby.job.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: exercises-ruby-job 7 | spec: 8 | 9 | template: 10 | spec: 11 | restartPolicy: Never 12 | 13 | volumes: 14 | - name: pod-data 15 | emptyDir: {} 16 | 17 | initContainers: 18 | - name: exercises-ruby 19 | image: hexletbasics/exercises-ruby 20 | command: 21 | - cp 22 | - "-r" 23 | - "/exercises-ruby/." 24 | - "/out" 25 | volumeMounts: 26 | - mountPath: /out 27 | name: pod-data 28 | 29 | containers: 30 | 31 | - name: app 32 | imagePullPolicy: Always 33 | image: hexletbasics/services-web:v271 34 | command: 35 | - mix 36 | - x.exercises.load 37 | - ruby 38 | volumeMounts: 39 | - mountPath: /hexletbasics/exercises-ruby 40 | name: pod-data 41 | resources: 42 | requests: 43 | cpu: 10m 44 | envFrom: 45 | - secretRef: 46 | name: hexlet-basics-secrets 47 | - secretRef: 48 | name: sparkpost-credentials 49 | - secretRef: 50 | name: github-credentials 51 | - configMapRef: 52 | name: hexlet-basics-config-map 53 | -------------------------------------------------------------------------------- /k8s/nginx-config.yaml: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/40791459/upstream-sent-too-big-header-while-reading-response-header-from-upstream 2 | # NOTE kubectl replace nginx-ingress-nginx-ingress -f app-chart/templates/nginx-config.yaml 3 | apiVersion: v1 4 | data: 5 | proxy-buffers: "4 256k" 6 | proxy-buffer-size: "128k" 7 | proxy-busy-buffers-size: "256k" 8 | 9 | kind: ConfigMap 10 | metadata: 11 | name: nginx-ingress-nginx-ingress 12 | namespace: default 13 | -------------------------------------------------------------------------------- /k8s/values-external-dns.yml: -------------------------------------------------------------------------------- 1 | provider: cloudflare 2 | 3 | rbac: 4 | create: true 5 | 6 | resources: 7 | requests: 8 | cpu: 10m 9 | 10 | extraEnv: 11 | # - envFrom: 12 | # - secretRef: 13 | # name: cloudflare-credentials 14 | - name: CF_API_KEY 15 | valueFrom: 16 | secretKeyRef: 17 | name: cloudflare-credentials 18 | key: CF_API_KEY 19 | - name: CF_API_EMAIL 20 | valueFrom: 21 | secretKeyRef: 22 | name: cloudflare-credentials 23 | key: CF_API_EMAIL 24 | -------------------------------------------------------------------------------- /k8s/values-gcloud-proxy.yml: -------------------------------------------------------------------------------- 1 | rbac: 2 | create: true 3 | 4 | cloudsql: 5 | instances: 6 | - instance: "master3" 7 | project: "hexlet-basics" 8 | region: "europe-west3" 9 | port: 5432 10 | 11 | resources: 12 | requests: 13 | cpu: 10m 14 | 15 | existingSecret: cloudsql-instance-credentials 16 | -------------------------------------------------------------------------------- /k8s/values-nginx-ingress.yml: -------------------------------------------------------------------------------- 1 | controller: 2 | replicaCount: 3 3 | config: 4 | use-forwarded-headers: "true" 5 | 6 | affinity: 7 | podAntiAffinity: 8 | requiredDuringSchedulingIgnoredDuringExecution: 9 | - labelSelector: 10 | matchExpressions: 11 | - key: "app" 12 | operator: In 13 | values: 14 | - nginx-ingress 15 | - key: "component" 16 | operator: In 17 | values: 18 | - controller 19 | 20 | topologyKey: "kubernetes.io/hostname" 21 | -------------------------------------------------------------------------------- /make-compose.mk: -------------------------------------------------------------------------------- 1 | compose-build: 2 | docker-compose build 3 | 4 | compose: 5 | docker-compose up -d 6 | 7 | compose-down: 8 | docker-compose down -v || true 9 | 10 | compose-setup: compose-down compose-build web-install web-db-prepare 11 | -------------------------------------------------------------------------------- /make-services-caddy.mk: -------------------------------------------------------------------------------- 1 | caddy-docker-build-production: 2 | docker build --file services/caddy/Dockerfile.production --tag hexletbasics/services-caddy:$(VERSION) services/caddy 3 | 4 | caddy-docker-push: 5 | docker push hexletbasics/services-caddy:$(VERSION) 6 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hexlet_basics, 7 | version: "0.0.1", 8 | elixir: "~> 1.7", 9 | deps: deps(), 10 | dialyzer: [paths: ["services/app/_build/dev"]] 11 | ] 12 | end 13 | 14 | defp deps do 15 | [ 16 | {:credo, "~> 1.2.2", only: [:dev, :test], runtime: false}, 17 | {:dialyxir, "~> 1.0.0-rc.7", only: [:dev], runtime: false} 18 | ] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "credo": {:hex, :credo, "1.2.2", "f57faf60e0a12b0ba9fd4bad07966057fde162b33496c509b95b027993494aab", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8f2623cd8c895a6f4a55ef10f3fdf6a55a9ca7bef09676bd835551687bf8a740"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "506294d6c543e4e5282d4852aead19ace8a35bedeb043f9256a06a6336827122"}, 5 | "dogma": {:hex, :dogma, "0.1.16", "3c1532e2f63ece4813fe900a16704b8e33264da35fdb0d8a1d05090a3022eef9", [], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm", "756d3e19b056339af674b715fdd752c5dac468cf9d0e2d1a03abf4574e99fbf8"}, 7 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, 8 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-eslint": "^10.0.3", 4 | "eslint": "^6.8.0", 5 | "eslint-config-airbnb": "^18.0.1", 6 | "eslint-plugin-import": "^2.20.0", 7 | "eslint-plugin-jest": "^23.6.0", 8 | "eslint-plugin-jsx-a11y": "^6.2.3", 9 | "eslint-plugin-react": "^7.18.0", 10 | "eslint-plugin-react-hooks": "^3.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/caddy/Dockerfile.development: -------------------------------------------------------------------------------- 1 | FROM abiosoft/caddy:1.0.3 2 | 3 | COPY files/development/Caddyfile /etc/Caddyfile 4 | -------------------------------------------------------------------------------- /services/caddy/Dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM abiosoft/caddy:1.0.3 2 | 3 | COPY files/production/Caddyfile /etc/Caddyfile 4 | -------------------------------------------------------------------------------- /services/caddy/files/development/Caddyfile: -------------------------------------------------------------------------------- 1 | localhost, ru.code-basics.test, en.code-basics.test, code-basics.test { 2 | gzip 3 | tls self_signed 4 | proxy / web:4000 { 5 | insecure_skip_verify 6 | transparent 7 | websocket 8 | header_upstream X-Forwarded-Host {host} 9 | } 10 | } 11 | 12 | # Redir example 13 | # ru.code-basics.test { 14 | # tls self_signed 15 | # redir https://code-basics.ru{uri} 16 | #} 17 | -------------------------------------------------------------------------------- /services/caddy/files/production/Caddyfile: -------------------------------------------------------------------------------- 1 | code-basics.com:8080, ru.code-basics.com:8080 { 2 | tls off 3 | gzip 4 | proxy / {$CADDY_SERVER_ADDRESS}:4000 { 5 | transparent 6 | websocket 7 | header_upstream X-Forwarded-Host {host} 8 | } 9 | } 10 | 11 | code-basics.ru:8080 { 12 | tls off 13 | redir https://ru.code-basics.com{uri} 14 | } 15 | 16 | :8080 17 | -------------------------------------------------------------------------------- /services/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | code-basics.ru 4 | 5 | 6 |

Maintaince mode

7 |

Something happens. We already trying to come back!

8 | 9 | 10 | -------------------------------------------------------------------------------- /services/web/.dockerignore: -------------------------------------------------------------------------------- 1 | **/_build 2 | **/deps 3 | **/node_modules 4 | **/priv/static 5 | .cache-loader 6 | **/.git 7 | -------------------------------------------------------------------------------- /services/web/.env.docker: -------------------------------------------------------------------------------- 1 | SPARKPOST_SMTP_USERNAME=none 2 | SPARKPOST_SMTP_PASSWORD=none 3 | ROLLBAR_ACCESS_TOKEN=none 4 | DB_NAME=hexlet_basics_test 5 | DB_USERNAME=postgres 6 | DB_PASSWORD= 7 | DB_HOSTNAME=db 8 | DB_PORT=5432 9 | DB_SSL_MODE=FALSE 10 | SECRET_KEY_BASE=small-secret 11 | GUARDIAN_SECRET_KEY=none 12 | APP_HOST=code-basics.com 13 | APP_RU_HOST=ru.code-basics.com 14 | APP_SCHEME=https 15 | GITHUB_CLIENT_ID=none 16 | GITHUB_CLIENT_SECRET=none 17 | FACEBOOK_CLIENT_ID=none 18 | FACEBOOK_CLIENT_SECRET=none 19 | 20 | NODE_ENV=development 21 | -------------------------------------------------------------------------------- /services/web/.eslintignore: -------------------------------------------------------------------------------- 1 | priv 2 | node_modules 3 | deps 4 | -------------------------------------------------------------------------------- /services/web/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: 4 | - airbnb 5 | - "plugin:jest/recommended" 6 | - "plugin:react/recommended" 7 | - "plugin:react-hooks/recommended" 8 | 9 | parser: babel-eslint 10 | 11 | env: 12 | browser: true 13 | jest: true 14 | 15 | rules: 16 | no-console: 0 17 | import/no-unresolved: 0 18 | import/extensions: 0 19 | react/prop-types: 0 20 | -------------------------------------------------------------------------------- /services/web/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,priv,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /services/web/.iex.exs: -------------------------------------------------------------------------------- 1 | import Ecto.Query 2 | alias HexletBasics.Repo 3 | alias HexletBasics.Language 4 | alias HexletBasics.User 5 | alias HexletBasics.UserManager 6 | -------------------------------------------------------------------------------- /services/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:1.10.2 2 | 3 | ENV HOME /home/shared 4 | ENV LANG C.UTF-8 5 | 6 | WORKDIR /app 7 | 8 | RUN curl -sL https://deb.nodesource.com/setup_13.x | bash - 9 | 10 | RUN apt-get update && apt-get install -y inotify-tools 11 | RUN apt-get update && apt-get install -y nodejs 12 | RUN apt-get update && apt-get install -yqq postgresql-client 13 | RUN npm install -g npm-check-updates env-cmd 14 | 15 | # Install hex (Elixir package manager) 16 | RUN mix local.hex --force 17 | # Install rebar (Erlang build tool) 18 | RUN mix local.rebar --force 19 | RUN mix archive.install --force hex phx_new 1.4.16 20 | 21 | ENV DOCKER_CHANNEL edge 22 | ENV DOCKER_VERSION 19.03.8 23 | RUN curl -fsSL "https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz" \ 24 | | tar -xzC /usr/local/bin --strip=1 docker/docker 25 | 26 | COPY mix.exs mix.lock ./ 27 | RUN mix deps.get 28 | COPY config . 29 | RUN MIX_ENV=prod mix deps.compile 30 | 31 | COPY assets/package.json ./assets/package.json 32 | COPY assets/package-lock.json ./assets/package-lock.json 33 | 34 | RUN npm ci --prefix ./assets 35 | 36 | COPY . . 37 | 38 | RUN MIX_ENV=prod env-cmd -f .env.docker mix compile 39 | RUN NODE_ENV=production npm run deploy --prefix ./assets 40 | RUN MIX_ENV=prod env-cmd -f .env.docker mix phx.digest 41 | 42 | CMD ["mix", "phx.server"] 43 | -------------------------------------------------------------------------------- /services/web/Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | mix ecto.create 3 | mix ecto.migrate 4 | mix phx.server 5 | 6 | update: 7 | mix deps.update --all 8 | ncu -u 9 | npm update 10 | 11 | console: 12 | iex -S mix 13 | 14 | locales-build: 15 | mix gettext.merge priv/gettext 16 | -------------------------------------------------------------------------------- /services/web/README.md: -------------------------------------------------------------------------------- 1 | # HexletBasics 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Install Node.js dependencies with `cd assets && npm install` 8 | * Start Phoenix endpoint with `mix phx.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 13 | 14 | ## Learn more 15 | 16 | * Official website: http://www.phoenixframework.org/ 17 | * Guides: http://phoenixframework.org/docs/overview 18 | * Docs: https://hexdocs.pm/phoenix 19 | * Mailing list: http://groups.google.com/group/phoenix-talk 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /services/web/assets/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | ], 6 | plugins: [ 7 | ['@babel/plugin-proposal-decorators', { legacy: true }], 8 | '@babel/plugin-proposal-class-properties', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /services/web/assets/css/hexlet-basics.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/css/hexlet-basics.scss -------------------------------------------------------------------------------- /services/web/assets/css/variables.scss: -------------------------------------------------------------------------------- 1 | $breadcrumb-divider: quote("→"); 2 | $font-size-base: 1rem; 3 | $code-font-size: 0.8rem; 4 | -------------------------------------------------------------------------------- /services/web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import '../css/app.scss'; 4 | import './shared'; 5 | // import 'jquery'; 6 | // import '@fortawesome/react-fontawesome'; 7 | // import brands from '@fortawesome/fontawesome-free-brands'; 8 | // import solid from '@fortawesome/fontawesome-free-solid'; 9 | // import 'jquery-ujs'; 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | 15 | // Import local files 16 | // 17 | // Local files can be imported directly using relative 18 | // paths "./socket" or full ones "web/static/js/socket". 19 | 20 | // import socket from "./socket" 21 | // import '../css/app.scss'; 22 | // import '../css/lesson.scss'; 23 | 24 | // import 'react'; 25 | // import 'react-dom'; 26 | // import 'react-redux'; 27 | // import 'redux'; 28 | // import 'redux-actions'; 29 | // import 'prop-types'; 30 | // import 'classnames'; 31 | // import 'react-monaco-editor'; 32 | // import 'react-tabs'; 33 | // import 'react-bs-notifier'; 34 | // import 'axios'; 35 | // import 'ansi_up'; 36 | // import 'redux-thunk'; 37 | // import 'react-i18next'; 38 | // import 'markdown-it'; 39 | // import 'tether'; 40 | // import 'popper.js'; 41 | // import 'bootstrap'; 42 | 43 | // fontawesome.library.add(brands); 44 | // fontawesome.library.add(solid); 45 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/EntityContext.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from 'react'; 4 | 5 | export default React.createContext({}); 6 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/components/App.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import TabsBox from './TabsBox'; 6 | import HTMLPreview from './HTMLPreview'; 7 | import ControlBox from './ControlBox'; 8 | 9 | const getViewOptions = (languageName) => { 10 | const { editor } = useSelector((state) => state); 11 | 12 | switch (languageName) { 13 | case 'css': 14 | case 'html': 15 | return { 16 | tabsBoxClassName: 'h-50', 17 | component: , 18 | }; 19 | default: 20 | return { 21 | tabsBoxClassName: 'h-100', 22 | component: null, 23 | }; 24 | } 25 | }; 26 | 27 | const App = (props) => { 28 | const { 29 | startTime, 30 | language, 31 | userFinishedLesson, 32 | } = props; 33 | 34 | const { currentTabInfo } = useSelector((state) => state); 35 | 36 | const currentViewOptions = getViewOptions(language.name); 37 | 38 | return ( 39 | <> 40 | 46 | {currentTabInfo.title === 'editor' && currentViewOptions.component} 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/components/Console.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from 'react'; 4 | import cn from 'classnames'; 5 | import { useTranslation } from 'react-i18next'; 6 | import ansiUp from '../../lib/ansi_up'; 7 | 8 | const Console = ({ checkInfo }) => { 9 | const html = ansiUp(checkInfo.output); 10 | const { t } = useTranslation(); 11 | 12 | const message = checkInfo.result ? t(`check.${checkInfo.result}.message`) : null; 13 | const alertClassName = cn('mt-auto text-center alert', { 14 | 'alert-success': checkInfo.passed, 15 | 'alert-warning': !checkInfo.passed, 16 | }); 17 | return ( 18 |
19 |
20 |         
21 |       
22 | {message &&
{message}
} 23 |
24 | ); 25 | }; 26 | 27 | export default Console; 28 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/components/Editor.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from 'react'; 4 | import MonacoEditor from 'react-monaco-editor'; 5 | import { get } from 'lodash'; 6 | 7 | 8 | export const languageMapping = { 9 | racket: 'scheme', 10 | css: 'html', 11 | }; 12 | 13 | const langToTabSizeMapping = { 14 | javascript: 2, 15 | ruby: 2, 16 | yml: 2, 17 | py: 2, 18 | rkt: 2, 19 | erlang: 2, 20 | elixir: 2, 21 | }; 22 | 23 | const defaultTabSize = 4; 24 | 25 | export default class Editor extends React.Component { 26 | componentDidUpdate() { 27 | const { current } = this.props; 28 | if (this.editor && current) { 29 | this.editor.layout(); 30 | this.editor.focus(); 31 | } 32 | } 33 | 34 | handleResize = () => this.editor.layout(); 35 | 36 | handleChange = (content) => { 37 | const { onCodeChange } = this.props; 38 | onCodeChange({ content }); 39 | } 40 | 41 | editorDidMount = (editor, monaco) => { 42 | const { language } = this.props; 43 | this.editor = editor; 44 | this.monaco = monaco; 45 | this.editor.focus(); 46 | const model = this.editor.getModel(); 47 | model.updateOptions({ tabSize: get(langToTabSizeMapping, language, defaultTabSize)}); 48 | window.addEventListener('resize', this.handleResize); 49 | } 50 | 51 | render() { 52 | const options = { 53 | fontSize: 14, 54 | minimap: { 55 | enabled: false, 56 | }, 57 | // scrollBeyondLastLine: false, 58 | // selectOnLineNumbers: true, 59 | // automaticLayout: true, 60 | }; 61 | 62 | const { language, defaultValue } = this.props; 63 | 64 | return ( 65 | 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/components/HTMLPreview.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from 'react'; 4 | import Frame from 'react-frame-component'; 5 | 6 | const HTMLPreview = (props) => { 7 | const { html } = props; 8 | return ( 9 |
10 | 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default HTMLPreview; 18 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/connect.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { connect } from 'react-redux'; 4 | import { actions } from './slices'; 5 | 6 | export default () => (Component) => connect(null, { ...actions })(Component); 7 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import 'core-js/stable'; 4 | import 'regenerator-runtime/runtime'; 5 | import hljs from 'highlight.js'; 6 | import gon from 'gon'; 7 | import run from './init'; 8 | import '../../css/app.scss'; 9 | import '../shared'; 10 | import '../lib/i18n'; 11 | 12 | const currentUser = gon.getAsset('current_user'); 13 | hljs.initHighlightingOnLoad(); 14 | if (!currentUser.guest) { 15 | run(); 16 | } 17 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/init.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | 5 | import gon from 'gon'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { Provider } from 'react-redux'; 9 | import { configureStore } from '@reduxjs/toolkit'; 10 | import { toast } from 'react-toastify'; 11 | import reducer, { actions } from './slices'; 12 | 13 | // import configureStore from '../lib/configureStore'; 14 | import App from './components/App'; 15 | import EntityContext from './EntityContext'; 16 | 17 | const lesson = gon.getAsset('lesson'); 18 | const language = gon.getAsset('language'); 19 | const lessonDescription = gon.getAsset('lesson_description'); 20 | const userFinishedLesson = gon.getAsset('user_finished_lesson'); 21 | const prevLesson = gon.getAsset('prev_lesson'); 22 | 23 | export default () => { 24 | const store = configureStore({ 25 | reducer, 26 | // code: lesson.prepared_code, 27 | }); 28 | 29 | store.dispatch(actions.initLessonState({ 30 | userFinishedLesson, 31 | })); 32 | 33 | const entities = { 34 | prevLesson, 35 | language, 36 | lesson, 37 | lessonDescription, 38 | }; 39 | 40 | ReactDOM.render( 41 | 42 | 43 | 48 | 49 | , 50 | document.getElementById('basics-lesson-container'), 51 | ); 52 | toast.configure(); 53 | }; 54 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | export default { 4 | nextLessonPath: (lesson) => `/lessons/${lesson.id}/redirect-to-next`, 5 | languageModuleLessonPath: (language, module, lesson) => `/languages/${language.slug}/modules/${module.slug}/lessons/${lesson.slug}`, 6 | lessonChecksPath: (lesson) => `/api/lessons/${lesson.id}/checks`, 7 | }; 8 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/slices/checkInfo.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-param-reassign */ 3 | 4 | import { createSlice } from '@reduxjs/toolkit'; 5 | import { useDispatch } from 'react-redux'; 6 | import i18next from 'i18next'; 7 | import { toast } from 'react-toastify'; 8 | import axios from 'axios'; 9 | import routes from '../routes'; 10 | 11 | const slice = createSlice({ 12 | name: 'checkInfo', 13 | initialState: { 14 | result: null, 15 | passed: false, 16 | processing: false, 17 | output: '', 18 | }, 19 | reducers: { 20 | runCheckRequest(state) { 21 | state.output = ''; 22 | state.processing = true; 23 | state.passed = false; 24 | state.result = null; 25 | }, 26 | runCheckSuccess(state, { payload }) { 27 | const { check: { data: { attributes } } } = payload; 28 | state.processing = false; 29 | state.result = attributes.result; 30 | state.passed = attributes.passed; 31 | state.output = attributes.output; 32 | }, 33 | runCheckFailure(state) { 34 | state.processing = false; 35 | }, 36 | }, 37 | }); 38 | 39 | const { runCheckRequest, runCheckSuccess, runCheckFailure } = slice.actions; 40 | 41 | const useCheckInfoActions = () => { 42 | const dispatch = useDispatch(); 43 | 44 | const runCheck = async ({ lesson, editor }) => { 45 | dispatch(runCheckRequest()); 46 | const url = routes.lessonChecksPath(lesson); 47 | const data = { 48 | type: 'check', 49 | attributes: { 50 | code: editor.content, 51 | }, 52 | }; 53 | try { 54 | const response = await axios.post(url, { data }); 55 | dispatch(runCheckSuccess({ check: response.data })); 56 | } catch (e) { 57 | dispatch(runCheckFailure({ code: e.response.status })); 58 | const key = e.response ? 'server' : 'network'; 59 | toast(i18next.t(`errors.${key}`)); 60 | throw e; 61 | } 62 | }; 63 | 64 | return { 65 | runCheck, 66 | }; 67 | }; 68 | 69 | const actions = { ...slice.actions }; 70 | export { actions, useCheckInfoActions }; 71 | export default slice.reducer; 72 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/slices/currentTabInfo.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-param-reassign */ 3 | 4 | import { createSlice } from '@reduxjs/toolkit'; 5 | import { actions as checkInfoActions } from './checkInfo'; 6 | 7 | const slice = createSlice({ 8 | name: 'currentTabInfo', 9 | initialState: { title: 'editor', clicksCount: 0 }, 10 | reducers: { 11 | selectTab(state, { payload }) { 12 | state.clicksCount = state.title === payload ? state.clicksCount + 1 : 0; 13 | state.title = payload; 14 | }, 15 | }, 16 | extraReducers: { 17 | [checkInfoActions.runCheckRequest](state) { 18 | state.title = 'console'; 19 | state.clicksCount = 0; 20 | }, 21 | }, 22 | }); 23 | 24 | export const { actions } = slice; 25 | export default slice.reducer; 26 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/slices/editor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-param-reassign */ 3 | 4 | import { createSlice } from '@reduxjs/toolkit'; 5 | 6 | const slice = createSlice({ 7 | name: 'editor', 8 | initialState: { content: '' }, 9 | reducers: { 10 | changeCode(state, { payload }) { 11 | const { content } = payload; 12 | state.content = content; 13 | }, 14 | }, 15 | }); 16 | 17 | export const { actions } = slice; 18 | export default slice.reducer; 19 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/slices/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { combineReducers } from 'redux'; 4 | 5 | import checkInfo, { actions as checkInfoActions, useCheckInfoActions } from './checkInfo'; 6 | import currentTabInfo, { actions as currentTabInfoActions } from './currentTabInfo'; 7 | import lessonState, { actions as lessonActions } from './lessonState'; 8 | import solutionState, { actions as solutionActions } from './solutionState'; 9 | import editor, { actions as editorActions } from './editor'; 10 | 11 | export default combineReducers({ 12 | editor, 13 | checkInfo, 14 | currentTabInfo, 15 | lessonState, 16 | solutionState, 17 | }); 18 | 19 | const actions = { 20 | ...editorActions, 21 | ...checkInfoActions, 22 | ...lessonActions, 23 | ...solutionActions, 24 | ...currentTabInfoActions, 25 | }; 26 | 27 | const asyncActions = { 28 | useCheckInfoActions, 29 | }; 30 | 31 | export { 32 | actions, 33 | asyncActions, 34 | }; 35 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/slices/lessonState.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-param-reassign */ 3 | 4 | import { createSlice } from '@reduxjs/toolkit'; 5 | import { actions as checkInfoActions } from './checkInfo'; 6 | 7 | const slice = createSlice({ 8 | name: 'lesson', 9 | initialState: { finished: null }, 10 | reducers: { 11 | initLessonState: (state, { payload }) => { 12 | const { userFinishedLesson } = payload; 13 | state.finished = !!userFinishedLesson; 14 | }, 15 | }, 16 | extraReducers: { 17 | [checkInfoActions.runCheckSuccess]: (state, { payload }) => { 18 | if (state.finished) { 19 | return; 20 | } 21 | const { check: { data: { attributes } } } = payload; 22 | state.finished = attributes.passed; 23 | }, 24 | }, 25 | }); 26 | 27 | export const { actions } = slice; 28 | export default slice.reducer; 29 | -------------------------------------------------------------------------------- /services/web/assets/js/lesson/slices/solutionState.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable no-param-reassign */ 3 | 4 | import { createSlice } from '@reduxjs/toolkit'; 5 | import { actions as checkInfoActions } from './checkInfo'; 6 | import { actions as lessonStateActions } from './lessonState'; 7 | 8 | const slice = createSlice({ 9 | name: 'solution', 10 | initialState: { canBeShown: false, shown: false }, 11 | reducers: { 12 | showSolution: (state) => { 13 | state.shown = true; 14 | }, 15 | makeSolutionAvailable: (state) => { 16 | state.canBeShown = true; 17 | }, 18 | }, 19 | extraReducers: { 20 | [checkInfoActions.runCheckSuccess]: (state, { payload }) => { 21 | const { check: { data: { attributes } } } = payload; 22 | if (attributes.passed) { 23 | state.canBeShown = true; 24 | state.shown = true; 25 | } 26 | }, 27 | [lessonStateActions.initLessonState]: (state, { payload }) => { 28 | const { userFinishedLesson } = payload; 29 | const lessonFinished = !!userFinishedLesson; 30 | state.canBeShown = lessonFinished; 31 | state.shown = lessonFinished; 32 | }, 33 | }, 34 | }); 35 | 36 | export const { actions } = slice; 37 | export default slice.reducer; 38 | -------------------------------------------------------------------------------- /services/web/assets/js/lib/ansi_up.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import AnsiUp from 'ansi_up'; 4 | 5 | const obj = new AnsiUp(); 6 | export default content => obj.ansi_to_html(content); 7 | -------------------------------------------------------------------------------- /services/web/assets/js/lib/configureStore.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | // import promise from 'redux-promise'; 6 | 7 | const middlewares = [ 8 | thunk, 9 | // promise, 10 | ]; 11 | 12 | export default function configureStore(reducer, initialState) { 13 | /* eslint-disable no-underscore-dangle */ 14 | const store = createStore(reducer, initialState, compose( 15 | applyMiddleware(...middlewares), 16 | window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f, 17 | )); 18 | /* eslint-enable */ 19 | return store; 20 | } 21 | -------------------------------------------------------------------------------- /services/web/assets/js/lib/data-fns-locale.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import gon from 'gon'; 4 | import { ru, enUS } from 'date-fns/locale'; 5 | 6 | const locales = { ru, en: enUS }; 7 | 8 | export default locales[gon.getAsset('locale')]; 9 | -------------------------------------------------------------------------------- /services/web/assets/js/lib/i18n.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import i18n from 'i18next'; 4 | import { initReactI18next } from 'react-i18next'; 5 | import Gon from 'gon'; 6 | // import XHR from 'i18next-xhr-backend'; 7 | import ruTranslation from '../../locales/ru/translation.json'; 8 | import enTranslation from '../../locales/en/translation.json'; 9 | 10 | const resources = { 11 | ru: { 12 | translation: ruTranslation, 13 | }, 14 | en: { 15 | translation: enTranslation, 16 | }, 17 | }; 18 | 19 | i18n 20 | .use(initReactI18next) 21 | .init({ 22 | resources, 23 | load: 'languageOnly', 24 | fallbackLng: false, 25 | lng: Gon.getAsset('locale'), 26 | debug: process.env.NODE_ENV !== 'production', 27 | // react i18next special options (optional) 28 | // keySeparator: false, 29 | react: { 30 | wait: true, 31 | }, 32 | }); 33 | 34 | 35 | export default i18n; 36 | -------------------------------------------------------------------------------- /services/web/assets/js/lib/markdown.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import MarkdownIt from 'markdown-it'; 4 | import hljs from 'highlight.js'; 5 | 6 | const md = new MarkdownIt({ 7 | highlight: (str, lang) => { 8 | if (lang && hljs.getLanguage(lang)) { 9 | try { 10 | const text = hljs.highlight(lang, str).value; 11 | return `
${text}
`; 12 | } catch (__) { 13 | return hljs.highlightAuto(str); 14 | } 15 | } 16 | 17 | return ''; 18 | }, 19 | }); 20 | 21 | export default text => md.render(text); 22 | -------------------------------------------------------------------------------- /services/web/assets/js/shared.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import 'bootstrap'; 4 | import 'phoenix_html'; 5 | -------------------------------------------------------------------------------- /services/web/assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/web/endpoint.ex": 6 | import { Socket } from 'phoenix'; 7 | 8 | const socket = new Socket('/socket', { params: { token: window.userToken } }); 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "lib/web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "lib/web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect(); 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | const channel = socket.channel('topic:subtopic', {}); 58 | channel.join() 59 | .receive('ok', (resp) => { console.log('Joined successfully', resp); }) 60 | .receive('error', (resp) => { console.log('Unable to join', resp); }); 61 | 62 | export default socket; 63 | -------------------------------------------------------------------------------- /services/web/assets/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "reset_code": "Reset code", 3 | "run": "Run", 4 | "editor": "Editor", 5 | "console": "Console", 6 | "solution": "Solution", 7 | "teacher_solution": "Teacher's soltuion:", 8 | "user_code": "Your solution:", 9 | "user_code_instructions": "(start writing in Editor, your code will appear here and you'll be able to compare it to the teacher's solution)", 10 | "solution_instructions": "Teacher's solution will be available in {{remainingTime}} or after you finish the task yourself.", 11 | "solution_notice": "It's best to solve the problem yourself, but if you're stuck for a long time, feel free to check out the solution. But make sure to study it thoroughly to truly understand it.", 12 | "show_solution": "Show solution", 13 | "lesson": "Lesson", 14 | "discuss": "Discussion", 15 | "instructions": "Instructions", 16 | "next_lesson": "Next Lesson", 17 | "prev_lesson": "Previous Lesson", 18 | "alert": { 19 | "error": { 20 | "message": "something went wrong, try one more time, please", 21 | "headline": "Oops!" 22 | }, 23 | "passed": { 24 | "message": "Whoa! You did it! Proceed.", 25 | "headline": "Tests passed" 26 | }, 27 | "failed": { 28 | "message": "Fix errros and run again", 29 | "headline": "Tests Failed" 30 | }, 31 | "failed-infinity": { 32 | "message": "Code was running too long. Check it for infinity loops.", 33 | "headline": "Tests Failed (Infinity Loop!)" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /services/web/assets/locales/ru/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": { 3 | "network": "Возникла сетевая проблема. Попробуйте повторить еще раз. Если не получилось, убедитесь в наличии хорошего интернета и отсутствии блокировщиков.", 4 | "server": "Ошибка на сервере. Возможно скоро отпустит, а возможно нет. Попробуйте узнать что произошло в https://slack-ru.hexlet.io/" 5 | }, 6 | "reset_code": "Сбросить код", 7 | "run": "Проверить", 8 | "confirm": "Вы уверены?", 9 | "editor": "Редактор", 10 | "console": "Консоль", 11 | "solution": "Решение", 12 | "teacher_solution": "Решение учителя:", 13 | "user_code": "Ваше решение:", 14 | "user_code_instructions": "(когда вы начнёте писать решение в Редакторе, оно появится тут для сравнения с учительским)", 15 | "solution_instructions": "Решение учителя станет доступно через {{remainingTime}} или после успешного выполнения задачи.", 16 | "solution_notice": "Желательно решить задачу самостоятельно, но если вы застряли и долгое время ничего не получается, то посмотрите решение учителя, но обязательно хорошенько в нём разберитесь.", 17 | "show_solution": "Показать решение", 18 | "lesson": "Урок", 19 | "discuss": "Обсуждение", 20 | "instructions": "Инструкции", 21 | "next_lesson": "Следующий", 22 | "prev_lesson": "Предыдущий", 23 | "check": { 24 | "passed": { 25 | "message": "Ура! Всё получилось! Сравните свое решение с решением учителя и затем переходите к следующему уроку", 26 | "headline": "Тесты пройдены" 27 | }, 28 | "failed": { 29 | "message": "Поправьте ошибки и запустите код на проверку снова", 30 | "headline": "Тесты не прошли" 31 | }, 32 | "failed-infinity": { 33 | "message": "Вероятно в вашем коде есть бесконечный цикл. Убедитесь что у вас верное условие остановки и вы учитываете пограничные случаи", 34 | "headline": "Долгое выполнение" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/web/assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/favicon.ico -------------------------------------------------------------------------------- /services/web/assets/static/images/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/css.png -------------------------------------------------------------------------------- /services/web/assets/static/images/elixir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/elixir.png -------------------------------------------------------------------------------- /services/web/assets/static/images/fake_output_topbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/fake_output_topbar.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-167x167.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #b91d47 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /services/web/assets/static/images/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Code Basics", 3 | "short_name": "Code Basics", 4 | "icons": [ 5 | { 6 | "src": "/images/favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/images/favicons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /services/web/assets/static/images/flag-ru.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /services/web/assets/static/images/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/go.png -------------------------------------------------------------------------------- /services/web/assets/static/images/hexlet_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/hexlet_logo.png -------------------------------------------------------------------------------- /services/web/assets/static/images/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/html.png -------------------------------------------------------------------------------- /services/web/assets/static/images/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/java.png -------------------------------------------------------------------------------- /services/web/assets/static/images/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/javascript.png -------------------------------------------------------------------------------- /services/web/assets/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/logo.png -------------------------------------------------------------------------------- /services/web/assets/static/images/party_emoj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/party_emoj.png -------------------------------------------------------------------------------- /services/web/assets/static/images/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/php.png -------------------------------------------------------------------------------- /services/web/assets/static/images/prof_icons/frontend.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /services/web/assets/static/images/prof_icons/php.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 10 | 11 | 16 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /services/web/assets/static/images/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/python.png -------------------------------------------------------------------------------- /services/web/assets/static/images/racket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/racket.png -------------------------------------------------------------------------------- /services/web/assets/static/images/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/ruby.png -------------------------------------------------------------------------------- /services/web/assets/static/images/smm_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/smm_cover.jpg -------------------------------------------------------------------------------- /services/web/assets/static/images/smm_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-basics/hexlet_basics/d43133b5d0eb7eea83b78880940071d62041b408/services/web/assets/static/images/smm_cover.png -------------------------------------------------------------------------------- /services/web/config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | secret_key_base = 8 | System.fetch_env!("SECRET_KEY_BASE") || 9 | raise """ 10 | environment variable SECRET_KEY_BASE is missing. 11 | You can generate one by calling: mix phx.gen.secret 12 | """ 13 | 14 | config :hexlet_basics, HexletBasicsWeb.Endpoint, 15 | http: [:inet6, port: String.to_integer(System.get_env("PORT", "4000"))], 16 | secret_key_base: secret_key_base 17 | 18 | guardian_secret_key = 19 | System.fetch_env!("GUARDIAN_SECRET_KEY") || 20 | raise """ 21 | environment variable GUARDIAN_SECRET_KEY is missing. 22 | You can generate one by calling:mix guardian.gen.secret 23 | """ 24 | 25 | config :hexlet_basics, HexletBasics.UserManager.Guardian, 26 | issuer: "hexlet_basics", 27 | secret_key: guardian_secret_key 28 | 29 | # ## Using releases (Elixir v1.9+) 30 | # 31 | # If you are doing OTP releases, you need to instruct Phoenix 32 | # to start each relevant endpoint: 33 | # 34 | # config :<%= web_app_name %>, <%= endpoint_module %>, server: true 35 | # 36 | # Then you can assemble a release by calling `mix release`. 37 | # See `mix help release` for more information. 38 | -------------------------------------------------------------------------------- /services/web/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | 4 | # We don't run a server during test. If one is required, 5 | # you can enable the server option below. 6 | config :hexlet_basics, HexletBasicsWeb.Endpoint, 7 | secret_key_base: "secret-key-base-secret-key-base-secret-key-base-secret-key-basesecret-key-base-secret-key-base-secret-key-base-secret-key-basesecret-key-base-secret-key-base-secret-key-base-secret-key-base", 8 | http: [port: 4001], 9 | server: false 10 | 11 | config :hexlet_basics, 12 | docker_command_template: "echo 'docker run --rm ~s ~s timeout -t 1 make --silent -C ~s test'", 13 | code_directory: "/tmp/hexlet-basics-test/code", 14 | langs: %{}, 15 | ga: "gtag", 16 | gtm: "gtm" 17 | 18 | # config :rollbax, enabled: false 19 | 20 | # Print only warnings and errors during test 21 | config :logger, level: :warn 22 | # config :logger, :console, format: "[$level] $message\n" 23 | 24 | config :hexlet_basics, HexletBasics.UserManager.Guardian, 25 | issuer: "hexlet_basics", 26 | secret_key: "asdf" 27 | 28 | # Configure your database 29 | config :hexlet_basics, HexletBasics.Repo, 30 | adapter: Ecto.Adapters.Postgres, 31 | username: "postgres", 32 | password: "", 33 | database: "hexlet_basics_test", 34 | hostname: "db", 35 | pool: Ecto.Adapters.SQL.Sandbox 36 | -------------------------------------------------------------------------------- /services/web/deploy-notify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_ACCESS_TOKEN -F environment=production -F revision=$CODE_BASICS_VERSION -F local_username=code_basics_production 3 | 4 | -------------------------------------------------------------------------------- /services/web/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: '3.7' 4 | 5 | services: 6 | db: 7 | image: postgres:11.6 8 | test: 9 | build: 10 | cache_from: 11 | - hexletbasics/services-web 12 | context: . 13 | command: mix test 14 | env_file: "./.env.docker" 15 | depends_on: 16 | - db 17 | -------------------------------------------------------------------------------- /services/web/lib/ext_enum.ex: -------------------------------------------------------------------------------- 1 | defmodule ExtEnum do 2 | def key_by(enumerable, keyword) do 3 | enumerable 4 | |> Enum.reduce(%{}, fn element, acc -> 5 | Map.put(acc, Map.get(element, keyword), element) 6 | end) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics do 2 | @moduledoc """ 3 | HexletBasics keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/application.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Application do 2 | use Application 3 | 4 | # See https://hexdocs.pm/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | # Define workers and child supervisors to be supervised 8 | children = [ 9 | # Start the Ecto repository 10 | HexletBasics.Repo, 11 | # Start the endpoint when the application starts 12 | HexletBasicsWeb.Endpoint, 13 | # Start your own worker by calling: HexletBasics.Worker.start_link(arg1, arg2, arg3) 14 | # worker(HexletBasics.Worker, [arg1, arg2, arg3]), 15 | ] 16 | 17 | # See https://hexdocs.pm/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: HexletBasics.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | 23 | # Tell Phoenix to update the endpoint configuration 24 | # whenever the application is updated. 25 | def config_change(changed, _new, removed) do 26 | HexletBasicsWeb.Endpoint.config_change(changed, removed) 27 | :ok 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/email.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Email do 2 | use Bamboo.Phoenix, view: HexletBasicsWeb.EmailView 3 | import HexletBasicsWeb.Gettext 4 | 5 | defp base_email(email_address, subject, user) do 6 | meta = Jason.encode!(%{metadata: %{user_id: user.id}}) 7 | 8 | new_email() 9 | |> to(email_address) 10 | |> from({"Code Basics", sending_from()}) 11 | |> subject(subject) 12 | |> put_header("X-MSYS-API", meta) 13 | |> put_html_layout({HexletBasicsWeb.LayoutView, "email.html"}) 14 | end 15 | 16 | def confirmation_html_email(conn, user, url) do 17 | subject = gettext("Confirm registration") 18 | %{assigns: %{locale: locale_from_conn}} = conn 19 | 20 | locale = user.locale || locale_from_conn 21 | email_address = user.email 22 | 23 | email_address 24 | |>base_email(subject, user) 25 | |> render( 26 | "confirmation.#{locale}.html", 27 | confirmation_url: url, 28 | subject: subject 29 | ) 30 | end 31 | 32 | def reset_password_html_email(conn, user, url) do 33 | subject = gettext("Reset password") 34 | %{assigns: %{locale: locale_from_conn}} = conn 35 | 36 | locale = user.locale || locale_from_conn 37 | email_address = user.email 38 | 39 | email_address 40 | |>base_email(subject, user) 41 | |> render( 42 | "reset_password.#{locale}.html", 43 | reset_password_url: url, 44 | subject: subject 45 | ) 46 | end 47 | 48 | defp sending_from do 49 | "basics.info@hexlet.io" 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.Language 5 | 6 | @derive {Jason.Encoder, only: [:name, :slug]} 7 | 8 | schema "languages" do 9 | field :name, :string 10 | field :slug, :string 11 | field :extension, :string 12 | field :docker_image, :string 13 | field :exercise_filename, :string 14 | field :exercise_test_filename, :string 15 | 16 | belongs_to :upload, HexletBasics.Upload 17 | has_many :modules, Language.Module 18 | has_many :module_descriptions, Language.Module.Description 19 | has_many :lesson_descriptions, Language.Module.Lesson.Description 20 | has_many :lessons, Language.Module.Lesson 21 | 22 | timestamps() 23 | end 24 | 25 | @doc false 26 | def changeset(%Language{} = language, attrs) do 27 | language 28 | |> cast(attrs, [:name, :slug, :upload_id, :extension, :exercise_filename, :exercise_test_filename, :docker_image]) 29 | # |> cast_assoc(:upload) 30 | |> validate_required([:name, :slug, :extension, :exercise_filename, :exercise_test_filename, :docker_image]) 31 | # |> unique_constraint(:slug, message: "JOPA!") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language/module.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language.Module do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.Language.Module 5 | 6 | @derive {Jason.Encoder, only: [:id, :slug]} 7 | 8 | schema "language_modules" do 9 | field(:slug, :string) 10 | field(:state, :string) 11 | field(:order, :integer) 12 | 13 | belongs_to(:upload, HexletBasics.Upload) 14 | belongs_to(:language, HexletBasics.Language) 15 | has_many(:descriptions, Module.Description) 16 | has_many(:lessons, Module.Lesson) 17 | 18 | timestamps() 19 | end 20 | 21 | @doc false 22 | def changeset(%Module{} = module, attrs) do 23 | module 24 | |> cast(attrs, [:slug, :state, :order, :language_id, :upload_id]) 25 | |> validate_required([:slug, :order]) 26 | end 27 | 28 | def get_directory(module) do 29 | "#{module.order}-#{module.slug}" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language/module/description.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language.Module.Description do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.Language 5 | alias HexletBasics.Language.Module 6 | 7 | 8 | schema "language_module_descriptions" do 9 | field :description, :string 10 | field :locale, :string 11 | field :name, :string 12 | belongs_to :module, Module 13 | belongs_to :language, Language 14 | 15 | timestamps() 16 | end 17 | 18 | @doc false 19 | def changeset(%Module.Description{} = description, attrs) do 20 | description 21 | |> cast(attrs, [:name, :description, :locale, :language_id, :module_id]) 22 | |> cast_assoc(:module) 23 | |> validate_required([:name, :description, :locale, :language_id, :module_id]) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language/module/lesson.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language.Module.Lesson do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.Language.Module.Lesson 5 | 6 | @derive {Jason.Encoder, only: [:id, :slug, :prepared_code, :original_code]} 7 | 8 | schema "language_module_lessons" do 9 | field(:order, :integer) 10 | field(:natural_order, :integer) 11 | field(:slug, :string) 12 | field(:state, :string) 13 | field(:original_code, :string) 14 | field(:prepared_code, :string) 15 | field(:test_code, :string) 16 | field(:path_to_code, :string) 17 | 18 | belongs_to(:language, HexletBasics.Language) 19 | belongs_to(:module, HexletBasics.Language.Module) 20 | belongs_to(:upload, HexletBasics.Upload) 21 | has_many(:descriptions, Lesson.Description) 22 | 23 | has_many(:user_finished_lessons, HexletBasics.User.FinishedLesson, 24 | foreign_key: :language_module_lesson_id 25 | ) 26 | 27 | timestamps() 28 | end 29 | 30 | @doc false 31 | def changeset(%Lesson{} = lesson, attrs) do 32 | lesson 33 | |> cast(attrs, [ 34 | :slug, 35 | :order, 36 | :natural_order, 37 | :original_code, 38 | :prepared_code, 39 | :test_code, 40 | :language_id, 41 | :module_id, 42 | :upload_id, 43 | :path_to_code 44 | ]) 45 | |> validate_required([ 46 | :slug, 47 | :order, 48 | :original_code, 49 | :natural_order, 50 | :test_code, 51 | :path_to_code 52 | ]) 53 | end 54 | 55 | def file_name_for_exercise(lesson) do 56 | "#{lesson.id}.#{lesson.language.extension}" 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language/module/lesson/description.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language.Module.Lesson.Description do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.Language 5 | alias HexletBasics.Language.Module.Lesson.Description 6 | alias HexletBasics.Language.Module.Lesson 7 | 8 | @derive {Jason.Encoder, only: [:instructions, :theory, :name]} 9 | 10 | schema "language_module_lesson_descriptions" do 11 | field(:instructions, :string) 12 | field(:locale, :string) 13 | field(:name, :string) 14 | field(:theory, :string) 15 | field(:tips, {:array, :string}, default: []) 16 | field(:definitions, {:array, :map}, default: []) 17 | belongs_to(:lesson, Lesson) 18 | belongs_to(:language, Language) 19 | 20 | timestamps() 21 | end 22 | 23 | @doc false 24 | def changeset(%Description{} = description, attrs) do 25 | description 26 | |> cast(attrs, [ 27 | :theory, 28 | :instructions, 29 | :locale, 30 | :name, 31 | :lesson_id, 32 | :language_id, 33 | :tips, 34 | :definitions 35 | ]) 36 | # |> cast_embed(attrs, :tips) 37 | |> validate_required([:theory, :instructions, :locale, :name, :lesson_id, :language_id]) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language/module/lesson/scope.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language.Module.Lesson.Scope do 2 | import Ecto.Query 3 | 4 | def web(query, language, locale) do 5 | query = web_without_sorting(query, language, locale) 6 | 7 | query 8 | |> order_by([l], asc: l.natural_order) 9 | end 10 | 11 | def web_without_sorting(query, language, locale) do 12 | query 13 | |> for_upload(language.upload_id) 14 | |> for_locale(locale) 15 | end 16 | 17 | def for_locale(query, locale) do 18 | from(m in query, 19 | join: d in assoc(m, :descriptions), 20 | where: d.locale == ^locale 21 | ) 22 | end 23 | 24 | def for_upload(query, upload_id) do 25 | from(l in query, 26 | where: l.upload_id == ^upload_id 27 | ) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/language/module/scope.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Language.Module.Scope do 2 | import Ecto.Query 3 | 4 | def web(query, language, locale) do 5 | query 6 | |> for_upload(language.upload_id) 7 | |> for_locale(locale) 8 | |> order_by([m], [asc: m.order]) 9 | end 10 | 11 | def for_locale(query, locale) do 12 | from m in query, 13 | join: d in assoc(m, :descriptions), 14 | where: d.locale == ^locale 15 | end 16 | 17 | def for_upload(query, upload_id) do 18 | from m in query, 19 | where: m.upload_id == ^upload_id 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Mailer do 2 | use Bamboo.Mailer, otp_app: :hexlet_basics 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Notifier do 2 | alias HexletBasics.{Mailer, User} 3 | 4 | def send_email(email, recipient) do 5 | if User.enabled_delivery?(recipient) do 6 | email 7 | |> Mailer.deliver_later() 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/page_title.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.PageTitle do 2 | import Phoenix.Controller, only: [action_name: 1, view_module: 1] 3 | alias HexletBasicsWeb.{LanguageView, Language} 4 | 5 | @suffix "Code Basics (By Hexlet)" 6 | @separator " - " 7 | 8 | def page_title(assigns), do: assigns |> get |> put_suffix 9 | 10 | defp put_suffix(nil), do: @suffix 11 | defp put_suffix(title), do: Enum.join([title, @suffix], @separator) 12 | 13 | defp get(conn) do 14 | action = action_name(conn) 15 | view = view_module(conn) 16 | 17 | get_by_view(view, action, conn.assigns) 18 | end 19 | 20 | defp get_by_view(Language.Module.LessonView, action, assigns) do 21 | %{language: language, module_description: module_description} = assigns 22 | case action do 23 | :show -> Enum.join([assigns.lesson_description.name, module_description.name, language.name], @separator) 24 | end 25 | end 26 | 27 | defp get_by_view(LanguageView, action, assigns) do 28 | case action do 29 | :show -> assigns.language.name 30 | end 31 | end 32 | defp get_by_view(_, _, _), do: nil 33 | # defp get(%{ view_module: ArticleView, view_template: "show.html", article: article }) do 34 | # article.title <> " - " <> article.series 35 | # end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Repo do 2 | use Ecto.Repo, 3 | otp_app: :hexlet_basics, 4 | adapter: Ecto.Adapters.Postgres 5 | 6 | @doc """ 7 | Dynamically loads the repository url from the 8 | DATABASE_URL environment variable. 9 | """ 10 | def init(_, opts) do 11 | if url = System.get_env("DATABASE_URL") do 12 | {:ok, Keyword.put(opts, :url, url)} 13 | else 14 | {:ok, opts} 15 | end 16 | end 17 | 18 | def get_assoc_by!(model, associationName, query) do 19 | get_by!(Ecto.assoc(model, associationName), query) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/state_machines/user/email_delivery_state_machine.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.StateMachines.User.EmailDeliveryStateMachine do 2 | use Machinery, 3 | field: :email_delivery_state, 4 | states: ["enabled", "disabled"], 5 | transitions: %{ 6 | "disabled" => "enabled", 7 | "*" => "disabled" 8 | } 9 | end 10 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/state_machines/user_state_machine.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.StateMachines.UserStateMachine do 2 | use Machinery, 3 | states: ["initial", "waiting_confirmation", "active", "removed"], 4 | transitions: %{ 5 | "initial" => ["waiting_confirmation", "active"], 6 | "waiting_confirmation" => "active", 7 | "active" => "active", 8 | "*" => "removed" 9 | } 10 | end 11 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/upload.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Upload do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.Upload 5 | 6 | 7 | schema "uploads" do 8 | field :language_name, :string 9 | 10 | timestamps() 11 | end 12 | 13 | @doc false 14 | def changeset(%Upload{} = upload, attrs) do 15 | upload 16 | |> cast(attrs, [:language_name]) 17 | |> validate_required([:language_name]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/user/account.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.User.Account do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "user_accounts" do 6 | field(:provider, :string) 7 | field(:uid, :string) 8 | belongs_to(:user, HexletBasics.User) 9 | 10 | timestamps() 11 | end 12 | 13 | @doc false 14 | def changeset(account, attrs) do 15 | account 16 | |> cast(attrs, [:provider, :uid, :user_id]) 17 | |> validate_required([:provider, :uid, :user_id]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/user/finished_lesson.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.User.FinishedLesson do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | alias HexletBasics.User.FinishedLesson 5 | 6 | @derive {Jason.Encoder, only: [:id]} 7 | 8 | schema "user_finished_lessons" do 9 | belongs_to(:user, HexletBasics.User) 10 | belongs_to(:language_module_lesson, HexletBasics.Language.Module.Lesson) 11 | # field :language_module_lesson_id, :id 12 | 13 | timestamps() 14 | end 15 | 16 | @doc false 17 | def changeset(%FinishedLesson{} = finished_lesson, attrs) do 18 | finished_lesson 19 | |> cast(attrs, [:user_id, :language_module_lesson_id]) 20 | |> validate_required([:user_id, :language_module_lesson_id]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/user/scope.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.User.Scope do 2 | import Ecto.Query 3 | 4 | def web(query) do 5 | query 6 | |> active 7 | end 8 | 9 | def active(query) do 10 | from q in query, where: q.state == "active" 11 | end 12 | 13 | def not_removed(query) do 14 | from q in query, where: q.state != "removed" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/user_manager/error_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UserManager.ErrorHandler do 2 | use HexletBasicsWeb, :controller 3 | # import Plug.Conn 4 | import HexletBasicsWeb.Gettext 5 | alias HexletBasicsWeb.Router.Helpers, as: Routes 6 | 7 | @behaviour Guardian.Plug.ErrorHandler 8 | 9 | @impl Guardian.Plug.ErrorHandler 10 | def auth_error(conn, {_type = :unauthenticated, _reason}, _opts) do 11 | conn 12 | |> put_flash(:error, gettext("Sorry, you have to sign in.")) 13 | |> redirect(to: Routes.page_path(conn, :index)) 14 | end 15 | 16 | def auth_error(conn, {_type, _reason}, _opts) do 17 | conn 18 | |> HexletBasics.UserManager.Guardian.Plug.sign_out() 19 | |> redirect(to: "/") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/user_manager/guardian.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UserManager.Guardian do 2 | use Guardian, otp_app: :hexlet_basics 3 | 4 | alias HexletBasics.UserManager 5 | 6 | def subject_for_token(user, _claims) do 7 | {:ok, to_string(user.id)} 8 | end 9 | 10 | def resource_from_claims(%{"sub" => id}) do 11 | case UserManager.get_user!(id) do 12 | nil -> {:error, :resource_not_found} 13 | user -> {:ok, user} 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics/user_manager/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UserManager.Pipeline do 2 | use Guardian.Plug.Pipeline, 3 | otp_app: :hexlet_basics, 4 | error_handler: HexletBasics.UserManager.ErrorHandler, 5 | module: HexletBasics.UserManager.Guardian 6 | 7 | # If there is a session token, restrict it to an access token and validate it 8 | plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"} 9 | # If there is an authorization header, restrict it to an access token and validate it 10 | plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"} 11 | # Load the user if either of the verifications worked 12 | plug Guardian.Plug.LoadResource, allow_blank: true 13 | end 14 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use HexletBasicsWeb, :controller 9 | use HexletBasicsWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: HexletBasicsWeb 23 | import Plug.Conn 24 | # TODO: remove after switching to Routes 25 | # import HexletBasicsWeb.Router.Helpers 26 | import HexletBasicsWeb.Gettext 27 | alias HexletBasicsWeb.Router.Helpers, as: Routes 28 | end 29 | end 30 | 31 | def view do 32 | quote do 33 | use Phoenix.View, 34 | root: "lib/hexlet_basics_web/templates", 35 | namespace: HexletBasicsWeb, 36 | pattern: "*" 37 | 38 | # Import convenience functions from controllers 39 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 40 | 41 | # Use all HTML functionality (forms, tags, etc) 42 | use Phoenix.HTML 43 | 44 | import HexletBasicsWeb.ErrorHelpers 45 | import HexletBasicsWeb.Gettext 46 | alias HexletBasicsWeb.Router.Helpers, as: Routes 47 | import HexletBasicsWeb.Helpers.Auth, only: [signed_in?: 1] 48 | import Formulator 49 | import HexletBasicsWeb.Helpers.CustomUrl 50 | import HexletBasicsWeb.Helpers.LanguageStyles 51 | 52 | def current_user(conn) do 53 | conn.assigns[:current_user] 54 | end 55 | end 56 | end 57 | 58 | def router do 59 | quote do 60 | use Phoenix.Router 61 | import Plug.Conn 62 | import Phoenix.Controller 63 | end 64 | end 65 | 66 | def channel do 67 | quote do 68 | use Phoenix.Channel 69 | import HexletBasicsWeb.Gettext 70 | end 71 | end 72 | 73 | @doc """ 74 | When used, dispatch to the appropriate controller/view/etc. 75 | """ 76 | defmacro __using__(which) when is_atom(which) do 77 | apply(__MODULE__, which, []) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", HexletBasicsWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | def connect(_params, socket) do 19 | {:ok, socket} 20 | end 21 | 22 | # Socket id's are topics that allow you to identify all sockets for a given user: 23 | # 24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 25 | # 26 | # Would allow you to broadcast a "disconnect" event and terminate 27 | # all active sockets and channels for a given user: 28 | # 29 | # HexletBasicsWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 30 | # 31 | # Returning `nil` makes this socket anonymous. 32 | def id(_socket), do: nil 33 | end 34 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/api/lesson_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Api.LessonController do 2 | use HexletBasicsWeb, :controller 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/api/webhooks/sparkpost_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Api.Webhooks.SparkpostController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.UserManager 4 | 5 | def process(conn, _params) do 6 | try do 7 | events = conn.params["_json"] 8 | 9 | events 10 | |> Enum.filter(fn event -> !Enum.empty?(event["msys"]) end) 11 | |> Enum.each( 12 | fn 13 | %{"msys" => %{"message_event"=> body}} -> message_event_process(body) 14 | _ -> true 15 | end) 16 | rescue 17 | error in _ -> 18 | Rollbax.report(:error, error, __STACKTRACE__) 19 | end 20 | 21 | conn 22 | |> put_status(:ok) 23 | |> json("") 24 | end 25 | 26 | defp message_event_process(%{"rcpt_meta" => meta, "type" => type} = _body) do 27 | user = UserManager.get_user(meta["user_id"]) 28 | if user do 29 | case type do 30 | "delivery" -> true 31 | "bounce" -> UserManager.disable_delivery!(user) 32 | "spam_complaint" -> UserManager.disable_delivery!(user) 33 | "policy_rejection" -> true 34 | _ -> nil 35 | end 36 | end 37 | end 38 | defp message_event_process(_), do: nil 39 | end 40 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/auth_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.AuthController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.{UserManager, UserManager.Guardian} 4 | alias HexletBasicsWeb.Plugs.ChangeLocale 5 | alias Ueberauth.Strategy.Helpers 6 | 7 | plug Ueberauth 8 | plug ChangeLocale when action in [:request, :callback] 9 | 10 | def request(conn, _params) do 11 | render(conn, callback_url: Helpers.callback_url(conn)) 12 | end 13 | 14 | def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do 15 | conn 16 | |> put_flash(:error, gettext("Failed to authenticate.")) 17 | |> redirect(to: Routes.page_path(conn, :index)) 18 | end 19 | 20 | def callback( 21 | %{assigns: %{ueberauth_auth: %{info: %{email: nil}, provider: provider}}} = 22 | conn, 23 | _params 24 | ) do 25 | 26 | conn 27 | |> put_flash( 28 | :error, 29 | gettext("Please confirm the email in your %{provider} account, then try again", 30 | provider: provider 31 | ) 32 | ) 33 | |> redirect(to: Routes.page_path(conn, :index)) 34 | end 35 | 36 | def callback(%{assigns: %{ueberauth_auth: auth, current_user: current_user}} = conn, _params) do 37 | if current_user.guest do 38 | case UserManager.authenticate_user_by_social_network(auth) do 39 | {:ok, user} -> 40 | conn 41 | |> put_flash(:info, gettext("Successfully authenticated.")) 42 | |> Guardian.Plug.sign_in(user) 43 | |> redirect(to: Routes.page_path(conn, :index)) 44 | {:error, _} -> 45 | conn 46 | |> put_flash(:error, gettext("Failed to authenticate.")) 47 | |> redirect(to: Routes.page_path(conn, :index)) 48 | end 49 | else 50 | case UserManager.link_user_social_network_account(auth, current_user) do 51 | {:ok, _} -> 52 | conn 53 | |> put_flash(:info, gettext("Account successfully added.")) 54 | |> redirect(to: Routes.profile_path(conn, :show)) 55 | {:error, _} -> 56 | conn 57 | |> put_flash(:error, gettext("Error adding account.")) 58 | |> redirect(to: Routes.profile_path(conn, :show)) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/language/module_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Language.ModuleController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.Repo 4 | alias HexletBasics.Language 5 | import Ecto.Query 6 | 7 | def show(conn, %{"id" => id, "language_id" => language_id}) do 8 | language = Repo.get_by(Language, slug: language_id) 9 | module = Repo.get_by(Language.Module, language_id: language.id, slug: id) 10 | 11 | query = from l in Language.Module.Lesson, 12 | where: l.language_id == ^language.id and l.upload_id == ^language.upload_id and l.module_id == ^module.id, 13 | order_by: [asc: l.order] 14 | lessons = Repo.all(query) 15 | render conn, language: language, module: module, lessons: lessons 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/lesson_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LessonController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.Repo 4 | alias HexletBasics.Language.Module.Lesson 5 | require Logger 6 | import Ecto.Query 7 | import HexletBasicsWeb.Helpers.CustomUrl, only: [hexlet_link: 1] 8 | 9 | plug(HexletBasicsWeb.Plugs.RequireAuth) 10 | 11 | def next(conn, %{"lesson_id" => id}) do 12 | %{assigns: %{locale: locale, current_user: current_user}} = conn 13 | lesson = Repo.get!(Lesson, id) 14 | lesson = lesson |> Repo.preload([:language]) 15 | language = lesson.language 16 | 17 | lessons_query = Lesson.Scope.web(Lesson, language, locale) 18 | 19 | next_lesson_query = 20 | from(l in lessons_query, 21 | where: l.natural_order > ^lesson.natural_order, 22 | limit: 1 23 | ) 24 | 25 | next_not_finished_lesson_query = 26 | from(l in lessons_query, 27 | left_join: fl in assoc(l, :user_finished_lessons), 28 | on: fl.user_id == ^current_user.id, 29 | where: is_nil(fl.id), 30 | limit: 1 31 | ) 32 | 33 | case Repo.one(next_lesson_query) || Repo.one(next_not_finished_lesson_query) do 34 | nil -> 35 | path = Routes.language_path(conn, :show, language.slug) 36 | redirect(conn, to: path) 37 | 38 | next_lesson -> 39 | next_lesson = next_lesson |> Repo.preload([:module]) 40 | Logger.debug(inspect(next_lesson)) 41 | module = next_lesson.module 42 | 43 | path = 44 | Routes.language_module_lesson_path(conn, :show, language.slug, module.slug, next_lesson.slug) 45 | 46 | redirect(conn, to: path) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/locale_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LocaleController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.UserManager 4 | alias HexletBasicsWeb.Helpers.CustomUrl 5 | 6 | def switch(conn, %{"redirect_url" => redirect_url, "locale" => locale}) do 7 | %{assigns: %{current_user: current_user}} = conn 8 | 9 | if !current_user.guest && current_user.locale != locale do 10 | UserManager.set_locale!(current_user, locale) 11 | end 12 | 13 | conn 14 | |> put_router_url(CustomUrl.url_by_lang(locale)) 15 | |> put_session(:locale, locale) 16 | |> redirect(external: redirect_url) 17 | end 18 | 19 | def switch(conn, _) do 20 | conn |> redirect(to: "/") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/password_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.PasswordController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.Repo 4 | alias HexletBasics.{User, Email, Notifier, UserManager} 5 | 6 | def edit(conn, %{"reset_password_token" => reset_password_token}) do 7 | user = UserManager.user_get_by(reset_password_token: reset_password_token) 8 | 9 | if user do 10 | changeset = User.reset_password_changeset(%User{}, %{}) 11 | render(conn, "edit.html", changeset: changeset, reset_password_token: reset_password_token) 12 | else 13 | conn 14 | |> put_flash(:error, gettext("User not found")) 15 | |> redirect(to: Routes.remind_password_path(conn, :new)) 16 | end 17 | end 18 | 19 | def edit(conn, _) do 20 | conn 21 | |> put_flash(:error, gettext("User not found")) 22 | |> redirect(to: Routes.remind_password_path(conn, :new)) 23 | end 24 | 25 | def update(conn, params) do 26 | user = UserManager.user_get_by(reset_password_token: params["reset_password_token"]) 27 | 28 | if user do 29 | user 30 | |> User.reset_password_changeset(params["user"]) 31 | |> Repo.update() 32 | 33 | conn 34 | |> put_flash(:info, gettext("Password was change")) 35 | |> redirect(to: Routes.session_path(conn, :new)) 36 | else 37 | conn 38 | |> put_flash(:error, gettext("User not found")) 39 | |> redirect(to: Routes.remind_password_path(conn, :new)) 40 | end 41 | end 42 | 43 | def reset_password(%{assigns: %{current_user: current_user}} = conn, params) do 44 | redirect_to = params["redirect_to"] || Routes.page_path(conn, :index) 45 | 46 | user = 47 | current_user 48 | |> User.generate_token(:reset_password_token) 49 | |> Repo.update!() 50 | 51 | email = 52 | Email.reset_password_html_email( 53 | conn, 54 | user, 55 | Routes.password_url(conn, :edit, reset_password_token: user.reset_password_token) 56 | ) 57 | 58 | email 59 | |> Notifier.send_email(user) 60 | 61 | conn 62 | |> put_flash(:info, gettext("Message with instructions for reset password was sent")) 63 | |> redirect(to: redirect_to) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/profile_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ProfileController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.Repo 4 | alias HexletBasicsWeb.Plugs.{DetectDomainForRoot} 5 | alias HexletBasics.{User, UserManager, UserManager.Guardian} 6 | import Ecto 7 | plug DetectDomainForRoot when action in [:show] 8 | 9 | def show(%{assigns: %{current_user: current_user}} = conn, _params) do 10 | github_account = assoc(current_user, :accounts) |> Repo.get_by(provider: "github") 11 | 12 | render(conn, "show.html", 13 | user: current_user, 14 | github_account: github_account, 15 | user_active?: User.active?(current_user), 16 | user_delivery_state_disabled?: User.disabled_delivery?(current_user) 17 | ) 18 | end 19 | 20 | def delete(%{assigns: %{current_user: current_user}} = conn, _) do 21 | case UserManager.remove_user!(current_user) do 22 | {:ok, _} -> 23 | conn 24 | |> Guardian.Plug.sign_out() 25 | |> put_flash(:info, gettext("Your account has been successfully deleted")) 26 | |> redirect(to: Routes.page_path(conn, :index)) 27 | {:error, _} -> 28 | conn 29 | |> put_flash( 30 | :error, 31 | gettext( 32 | "Something went wrong! Please contact support support@hexlet.io ." 33 | ) 34 | ) 35 | |> redirect(to: Routes.page_path(conn, :index)) 36 | end 37 | end 38 | 39 | def delete_account( 40 | %{assigns: %{current_user: current_user}} = conn, 41 | %{"account_id" => account_id, "redirect_to" => redirect_to, "provider" => provider} = 42 | _params 43 | ) do 44 | case UserManager.delete_account(current_user, account_id) do 45 | {:ok, _} -> 46 | conn 47 | |> put_flash( 48 | :info, 49 | gettext("Your %{provider} account has been successfully deleted", provider: provider) 50 | ) 51 | |> redirect(to: redirect_to) 52 | 53 | {:error, _} -> 54 | conn 55 | |> put_flash( 56 | :error, 57 | gettext( 58 | "Something went wrong! Please contact support support@hexlet.io ." 59 | ) 60 | ) 61 | |> redirect(to: redirect_to) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/remind_password_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.RemindPasswordController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.{User, Repo, UserManager} 4 | alias HexletBasics.{Notifier, Email} 5 | 6 | def new(conn, _params) do 7 | changeset = User.changeset(%User{}, %{}) 8 | meta_attrs = [ 9 | %{property: "og:type", content: 'website'}, 10 | %{property: "og:title", content: gettext("Code Basics Remind Password Title")}, 11 | %{property: "og:description", content: gettext("Code Basics Remind Password Description")}, 12 | %{property: "og:image", content: Routes.static_url(conn, "/images/logo.png")}, 13 | %{property: "og:url", content: Routes.page_url(conn, :index)}, 14 | %{property: "description", content: gettext("Code Basics Remind Password Description")}, 15 | %{property: "image", content: Routes.static_url(conn, "/images/logo.png")} 16 | ] 17 | link_attrs = [ 18 | %{rel: "canonical", href: Routes.page_url(conn, :index)}, 19 | %{rel: 'image_src', href: Routes.static_url(conn, "/images/logo.png")} 20 | ] 21 | 22 | title_text = gettext("Code Basics Remind Password Title") 23 | 24 | render(conn, "new.html", changeset: changeset, meta_attrs: meta_attrs, link_attrs: link_attrs, title: title_text) 25 | end 26 | 27 | def create(conn, %{"user" => params}) do 28 | user = UserManager.user_get_by(email: params["email"]) 29 | if user do 30 | {:ok, user} = user 31 | |> User.generate_token(:reset_password_token) 32 | |> Repo.update() 33 | 34 | email = Email.reset_password_html_email( 35 | conn, 36 | user, 37 | Routes.password_url(conn, :edit, reset_password_token: user.reset_password_token) 38 | ) 39 | 40 | email 41 | |> Notifier.send_email(user) 42 | 43 | conn 44 | |> put_flash(:info, gettext("Message with instructions for reset password was sent")) 45 | |> redirect(to: "/") 46 | else 47 | conn 48 | |> put_flash(:error, gettext("User not found")) 49 | |> redirect(to: Routes.remind_password_path(conn, :new)) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.SessionController do 2 | use HexletBasicsWeb, :controller 3 | alias HexletBasics.{UserManager, UserManager.Guardian} 4 | alias HexletBasicsWeb.Plugs.CheckAuthentication 5 | alias HexletBasicsWeb.Plugs.DetectLocaleByHost 6 | 7 | plug DetectLocaleByHost when action in [:new] 8 | plug CheckAuthentication when action in [:new, :create] 9 | 10 | def new(conn, _params) do 11 | meta_attrs = [ 12 | %{property: "og:type", content: 'website'}, 13 | %{property: "og:title", content: gettext("Code Basics Login Title")}, 14 | %{property: "og:description", content: gettext("Code Basics Login Description")}, 15 | %{property: "og:image", content: Routes.static_url(conn, "/images/logo.png")}, 16 | %{property: "og:url", content: Routes.page_url(conn, :index)}, 17 | %{property: "description", content: gettext("Code Basics Login Description")}, 18 | %{property: "image", content: Routes.static_url(conn, "/images/logo.png")} 19 | ] 20 | link_attrs = [ 21 | %{rel: "canonical", href: Routes.page_url(conn, :index)}, 22 | %{rel: 'image_src', href: Routes.static_url(conn, "/images/logo.png")} 23 | ] 24 | 25 | title_text = gettext("Code Basics Login Title") 26 | 27 | render(conn, "new.html", link_attrs: link_attrs, meta_attrs: meta_attrs, title: title_text) 28 | end 29 | 30 | def create(conn, %{"session" => %{"email" => email, "password" => password}}) do 31 | auth = UserManager.authenticate_user(String.trim(email), password) 32 | 33 | auth 34 | |> login_reply(conn) 35 | end 36 | 37 | def delete(conn, _params) do 38 | conn 39 | |> Guardian.Plug.sign_out() 40 | |> clear_session 41 | |> put_flash(:info, gettext("You have been logged out!")) 42 | |> redirect(to: "/") 43 | end 44 | 45 | defp login_reply({:ok, user}, conn) do 46 | conn 47 | |> put_flash(:info, gettext("Signed in successfully.")) 48 | |> Guardian.Plug.sign_in(user) 49 | |> redirect(to: Routes.page_path(conn, :index)) 50 | end 51 | 52 | defp login_reply({:error, _reason}, conn) do 53 | conn 54 | |> put_flash(:error, gettext("There was a problem with your email/password")) 55 | |> new(%{}) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hexlet_basics 3 | require Logger 4 | 5 | socket("/socket", HexletBasicsWeb.UserSocket, 6 | # or list of options 7 | websocket: true 8 | # longpoll: [check_origin: ...] 9 | ) 10 | 11 | # Serve at "/" the static files from "priv/static" directory. 12 | # 13 | # You should set gzip to true if you are running phoenix.digest 14 | # when deploying your static files in production. 15 | plug(Plug.Static, 16 | at: "/", 17 | from: :hexlet_basics, 18 | gzip: false, 19 | only: ~w(css js fonts images favicon.ico) 20 | ) 21 | 22 | # Code reloading can be explicitly enabled under the 23 | # :code_reloader configuration of your endpoint. 24 | if code_reloading? do 25 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 26 | plug(Phoenix.LiveReloader) 27 | plug(Phoenix.CodeReloader) 28 | end 29 | 30 | plug(Plug.RequestId) 31 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 32 | 33 | plug(Plug.Parsers, 34 | parsers: [:urlencoded, :multipart, :json], 35 | pass: ["*/*"], 36 | json_decoder: Phoenix.json_library() 37 | ) 38 | 39 | plug(Plug.MethodOverride) 40 | plug(Plug.Head) 41 | 42 | # The session will be stored in the cookie and signed, 43 | # this means its contents can be read but not tampered with. 44 | # Set :encryption_salt if you would also like to encrypt it. 45 | plug(Plug.Session, 46 | store: :cookie, 47 | key: "_hexlet_basics_key", 48 | # FIXME extract to env! 49 | signing_salt: "mKGV92uB", 50 | domain: System.fetch_env!("APP_HOST") 51 | ) 52 | 53 | plug(HexletBasicsWeb.Router) 54 | 55 | # Callback invoked for dynamically configuring the endpoint. 56 | 57 | # It receives the endpoint configuration and checks if 58 | # configuration should be loaded from the system environment. 59 | # def init(_key, config) do 60 | # if config[:load_from_system_env] do 61 | # port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 62 | # {:ok, Keyword.put(config, :http, [:inet6, port: port])} 63 | # else 64 | # {:ok, config} 65 | # end 66 | # end 67 | end 68 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import HexletBasicsWeb.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :hexlet_basics 24 | end 25 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/helpers/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Helpers.Auth do 2 | def signed_in?(conn) do 3 | current_user = conn.assigns[:current_user] 4 | !current_user.guest 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/helpers/custom_url.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Helpers.CustomUrl do 2 | use HexletBasicsWeb, :controller 3 | 4 | def url_by_lang(locale) do 5 | hosts_by_langs = %{"ru" => System.fetch_env!("APP_RU_HOST"), "en" => System.fetch_env!("APP_HOST")} 6 | Routes.url(%URI{scheme: System.fetch_env!("APP_SCHEME"), host: hosts_by_langs[locale]}) 7 | end 8 | 9 | def redirect_current_url(conn, locale) do 10 | cur_path = current_path(conn) 11 | url_by_lang(locale) <> cur_path 12 | end 13 | 14 | def redirect_current_path(conn) do 15 | current_path(conn) 16 | end 17 | 18 | def hexlet_link(path \\ "/") do 19 | utm = %{utm_campaign: "general", utm_source: "code-basics", utm_medium: "referral"} 20 | utm_query = URI.encode_query(utm) 21 | uri = %URI{ host: "hexlet.io", query: utm_query, scheme: "https", port: 443, path: path } 22 | 23 | URI.to_string(uri) 24 | end 25 | 26 | def facebook_curl, do: "https://www.facebook.com/codebasicsru/" 27 | def youtube_curl, do: "https://www.youtube.com/user/HexletUniversity" 28 | def twitter_curl, do: "https://twitter.com/HexletHQ" 29 | def telegram_curl, do: "https://t.me/hexlet_ru" 30 | def slack_curl, do: "https://slack-ru.hexlet.io/" 31 | end 32 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/helpers/language_styles.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Helpers.LanguageStyles do 2 | @mapping %{ 3 | php: %{ 4 | bg: "bg-blue", 5 | }, 6 | javascript: %{ 7 | bg: "bg-yellow", 8 | }, 9 | java: %{ 10 | bg: "bg-azure", 11 | }, 12 | python: %{ 13 | bg: "bg-orange", 14 | }, 15 | html: %{ 16 | bg: "bg-orange", 17 | }, 18 | css: %{ 19 | bg: "bg-azure", 20 | }, 21 | racket: %{ 22 | bg: "bg-red", 23 | }, 24 | ruby: %{ 25 | bg: "bg-red", 26 | }, 27 | elixir: %{ 28 | bg: "bg-indigo", 29 | }, 30 | go: %{ 31 | bg: "bg-cyan", 32 | }, 33 | } 34 | 35 | def style_for_lang(lang, style) do 36 | @mapping[lang][style] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/api_require_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.ApiRequireAuth do 2 | import Plug.Conn 3 | # import Phoenix.Controller 4 | # import HexletBasicsWeb.Gettext 5 | # alias HexletBasicsWeb.Router.Helpers, as: RouteHelpers 6 | 7 | def init(options), do: options 8 | 9 | def call(conn, _) do 10 | if conn.assigns.current_user.guest do 11 | conn 12 | |> send_resp(403, "Forbidden") 13 | |> halt() 14 | else 15 | conn 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/assign_current_user.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.AssignCurrentUser do 2 | import Plug.Conn 3 | 4 | alias HexletBasics.User 5 | alias HexletBasics.UserManager 6 | 7 | @spec init(Keyword.t()) :: Keyword.t() 8 | def init(opts), do: opts 9 | 10 | @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() 11 | def call(conn, _opts) do 12 | # FIXME: Это нужно будет убрать когда вернем аутентификацию для соцсетей 13 | user_from_session = get_session(conn, :current_user) 14 | conn = cond do 15 | user_from_session && !user_from_session.guest -> 16 | renew_user = UserManager.get_user!(user_from_session.id) 17 | conn 18 | |> put_session(:current_user, renew_user) 19 | true -> 20 | conn 21 | end 22 | 23 | maybe_user = get_session(conn, :current_user) || Guardian.Plug.current_resource(conn) 24 | 25 | user = 26 | case maybe_user do 27 | nil -> %User{guest: true, nickname: nil, locale: nil} 28 | u -> u 29 | end 30 | 31 | conn |> assign(:current_user, user) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/assign_globals.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.AssignGlobals do 2 | @moduledoc false 3 | import PhoenixGon.Controller 4 | import Plug.Conn 5 | require Logger 6 | 7 | @spec init(Keyword.t) :: Keyword.t 8 | def init(opts), do: opts 9 | 10 | @spec call(Plug.Conn.t, Keyword.t) :: Plug.Conn.t 11 | def call(conn, _opts) do 12 | %{assigns: %{current_user: current_user, locale: locale}} = conn 13 | 14 | configuration1 = [:ga, :gtm] 15 | |> Enum.reduce(%{}, fn key, acc -> 16 | value = Application.fetch_env!(:hexlet_basics, key) 17 | Map.put(acc, key, value) 18 | end) 19 | 20 | disqus_local_key = String.to_atom("disqus_#{locale}") 21 | disqus_value = Application.fetch_env!(:hexlet_basics, disqus_local_key) 22 | 23 | configuration2 = %{locale: locale, current_user: current_user, disqus: disqus_value} 24 | configuration = Map.merge(configuration1, configuration2) 25 | Logger.info inspect ["Params For Gon", configuration] 26 | 27 | conn 28 | |> put_gon(configuration) 29 | |> assign(:ga, configuration1.ga) 30 | |> assign(:gtm, configuration1.gtm) 31 | |> assign(:meta_attrs, []) 32 | |> assign(:link_attrs, []) 33 | |> assign(:title, Gettext.gettext(HexletBasicsWeb.Gettext, "Code Basics Title")) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/change_locale.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.ChangeLocale do 2 | import Plug.Conn 3 | 4 | def init(options), do: options 5 | 6 | def call(conn, _) do 7 | %{assigns: %{locale: default_locale, current_user: current_user}} = conn 8 | 9 | locale_from_session = get_session(conn, :locale) 10 | 11 | locale = current_user.locale || locale_from_session || default_locale 12 | 13 | Gettext.put_locale(HexletBasicsWeb.Gettext, locale) 14 | conn 15 | |> assign(:locale, locale) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/check_authentication.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.CheckAuthentication do 2 | import Plug.Conn 3 | use HexletBasicsWeb, :controller 4 | 5 | @spec init(Keyword.t()) :: Keyword.t() 6 | def init(opts), do: opts 7 | 8 | @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() 9 | def call(conn, _opts) do 10 | %{assigns: %{current_user: current_user}} = conn 11 | 12 | if current_user.guest do 13 | conn 14 | else 15 | conn 16 | |> redirect(to: "/") 17 | |> halt() 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/detect_domain_for_root.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.DetectDomainForRoot do 2 | import Plug.Conn 3 | alias HexletBasicsWeb.Helpers.CustomUrl 4 | use HexletBasicsWeb, :controller 5 | 6 | def init(options), do: options 7 | 8 | def call(conn, _) do 9 | %{assigns: %{locale: default_locale, current_user: current_user}} = conn 10 | 11 | locale_from_session = get_session(conn, :locale) 12 | 13 | locale = current_user.locale || locale_from_session || default_locale 14 | cond do 15 | conn.host == System.fetch_env!("APP_RU_HOST") -> 16 | conn 17 | |> put_session(:locale, "ru") 18 | locale == "ru" -> 19 | conn 20 | |> redirect(external: CustomUrl.redirect_current_url(conn, locale)) 21 | |> halt() 22 | true -> 23 | conn 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/detect_locale_by_host.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.DetectLocaleByHost do 2 | import Plug.Conn 3 | 4 | @spec init(Keyword.t()) :: Keyword.t() 5 | def init(options), do: options 6 | 7 | @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() 8 | def call(conn, _) do 9 | cond do 10 | conn.host == System.fetch_env!("APP_RU_HOST") -> 11 | conn 12 | |> put_session(:locale, "ru") 13 | true -> 14 | conn 15 | |> put_session(:locale, "en") 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/require_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.RequireAuth do 2 | import Plug.Conn 3 | import Phoenix.Controller 4 | import HexletBasicsWeb.Gettext 5 | alias HexletBasicsWeb.Router.Helpers, as: RouteHelpers 6 | 7 | def init(options), do: options 8 | 9 | def call(conn, _) do 10 | if conn.assigns.current_user.guest do 11 | conn 12 | |> put_flash(:error, gettext "Require auth") 13 | |> redirect(to: RouteHelpers.page_path(conn, :index)) 14 | |> halt 15 | else 16 | conn 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/set_locale.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.SetLocale do 2 | import Plug.Conn 3 | 4 | def init(options), do: options 5 | 6 | def call(conn, _) do 7 | langs = Application.fetch_env!(:hexlet_basics, :langs) 8 | locale = Map.get(langs, conn.host, "en") 9 | 10 | Gettext.put_locale(HexletBasicsWeb.Gettext, locale) 11 | conn 12 | |> assign(:locale, locale) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/plugs/set_url.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Plugs.SetUrl do 2 | import Plug.Conn 3 | alias HexletBasicsWeb.Helpers.CustomUrl 4 | use HexletBasicsWeb, :controller 5 | 6 | def init(options), do: options 7 | 8 | def call(conn, _) do 9 | %{assigns: %{locale: locale}} = conn 10 | 11 | conn 12 | |> put_router_url(CustomUrl.url_by_lang(locale)) 13 | |> put_static_url(CustomUrl.url_by_lang(locale)) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/schemas/company_schema.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Schemas.CompanySchema do 2 | alias HexletBasicsWeb.Router.Helpers, as: Routes 3 | 4 | def build(conn) do 5 | %{ 6 | "@context": "https://schema.org", 7 | "@type": "Organization", 8 | url: "http://hexlet.io", 9 | name: "Hexlet", 10 | legalName: "Hexlet Ltd.", 11 | vatID: "VAT ID: FI26641607", 12 | telephone: "+7 499 609 12 31", 13 | sameAs: [ 14 | "https://www.facebook.com/Hexlet", 15 | "https://www.youtube.com/user/HexletUniversity", 16 | "https://twitter.com/HexletHQ", 17 | "https://soundcloud.com/hexlet" 18 | ], 19 | address: %{ 20 | "@type": "PostalAddress", 21 | name: "UMA Esplanadi, Pohjoisesplanadi 39, 00100 Helsinki, Finland" 22 | }, 23 | logo: %{ 24 | "@type": "ImageObject", 25 | url: Routes.static_url(conn, "/images/hexlet_logo.png") 26 | }, 27 | email: %{ 28 | "@type": "Text", 29 | "@id": "mailto:support@hexlet.io" 30 | } 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/schemas/language/module/lesson_schema.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Schemas.Language.Module.LessonSchema do 2 | alias HexletBasicsWeb.Schemas.CompanySchema 3 | alias HexletBasicsWeb.Router.Helpers, as: Routes 4 | 5 | def build(conn, lesson, lesson_description, lesson_theory_html) do 6 | %{ 7 | "@context": "https://schema.org", 8 | "@type": "TechArticle", 9 | author: Gettext.gettext(HexletBasicsWeb.Gettext, "Code Basics Author"), 10 | headline: lesson_description.name, 11 | datePublished: lesson.inserted_at, 12 | dateModified: lesson.updated_at, 13 | image: Routes.static_url(conn, "/images/smm_cover.jpg"), 14 | accessMode: "textOnVisual", 15 | publisher: CompanySchema.build(conn), 16 | hasPart: %{ 17 | "@context": "https://schema.org", 18 | "@type": "WebPageElement", 19 | isAccessibleForFree: "https://schema.org/True", 20 | value: HtmlSanitizeEx.strip_tags(lesson_theory_html) 21 | } 22 | } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/schemas/language/module_schema.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Schemas.Language.ModuleSchema do 2 | alias HexletBasicsWeb.Schemas.CompanySchema 3 | alias HexletBasicsWeb.Router.Helpers, as: Routes 4 | 5 | def build(conn, _, nil, _) do 6 | %{} 7 | end 8 | 9 | def build(conn, module, module_description, language) do 10 | %{ 11 | "@context": "https://schema.org", 12 | "@type": "Course", 13 | name: module_description.name, 14 | description: module_description.description, 15 | accessMode: "textOnVisual", 16 | isAccessibleForFree: "https://schema.org/True", 17 | url: Routes.language_module_lesson_path(conn, :index, language.slug, module.slug), 18 | provider: CompanySchema.build(conn) 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/serializers/prev_lesson_serializer.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Serializers.PrevLessonSerializer do 2 | use Remodel 3 | 4 | attributes([:id, :slug, :module]) 5 | 6 | def module(record) do 7 | record.module 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/language/module/lesson/index.html.slime: -------------------------------------------------------------------------------- 1 | = if @module_description do 2 | div 3 | .row.mt-5.d-flex.flex-row 4 | .col-12 5 | - module_description = @module_description 6 | h2 7 | = module_description.name 8 | .show id="module_#{@module.id}" 9 | .row 10 | .col-12.col-md-6.mt-4 11 | ul class="list-group" 12 | = for lesson <- @lessons do 13 | - lesson_description = @descriptions_by_lesson[lesson.id] 14 | - finished_lesson = @user_finished_lessons_by_lesson[lesson.id] 15 | li class="list-group-item d-flex" 16 | - url = Routes.language_module_lesson_path(@conn, :show, @language.slug, @module.slug, lesson.slug) 17 | = link to: url, class: "stretched-link text-decoration-none" do 18 | = lesson.natural_order 19 | | .  20 | = lesson_description.name 21 | = if finished_lesson do 22 | .ml-auto 23 | i class="text-primary fas fa-check" 24 | 25 | .col-12.col-md-6.mt-4 26 | = module_description.description 27 | - else 28 | .d-flex.justify-content-center 29 | p.lead 30 | = gettext("Module available in ") 31 | - switch_map = locales_switch_map() 32 | = link to: Routes.locale_path(@conn, :switch, redirect_url: redirect_current_url(@conn, switch_map[@locale]), locale: switch_map[@locale]) do 33 | = gettext("Russian only") 34 | i.fas.fa-arrow-right.ml-1 35 | 36 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/language/module/show.html.slime: -------------------------------------------------------------------------------- 1 | h1 <%= @language.slug %> 2 | h2 3 | = @module.slug 4 | | ( 5 | = length(@lessons) 6 | | ) 7 | 8 | ol 9 | <%= for lesson <- @lessons do %> 10 | li= link lesson.slug, to: Routes.language_module_lesson_path(@conn, :show, @language.slug, @module.slug, lesson.slug) 11 | <% end %> 12 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/layout/email.html.slime: -------------------------------------------------------------------------------- 1 | doctype html 2 | html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" 3 | head 4 | meta content=("text/html; charset=UTF-8") http-equiv="Content-Type" / 5 | /! NAME: 1 COLUMN - BANDED 6 | /![if gte mso 15] 7 | | 96 7 | = input f, :password, as: :password, placeholder: gettext("Password"), class: "form-control", label: false, required: true 8 | .form-group.text-center 9 | = submit gettext("Update Password"), class: "btn btn-primary" 10 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/remind_password/new.html.slime: -------------------------------------------------------------------------------- 1 | .container-fluid 2 | .row.justify-content-center.pb-5.mt-5 3 | .col-12.col-sm-8.col-md-7.col-lg-4 4 | h1.h3.mb-4.mt-3.text-center 5 | = gettext("Forgot Password") 6 | = form_for @conn, Routes.remind_password_path(@conn, :create), [as: :user], fn f -> 7 | = input f, :email, placeholder: gettext("Email"), class: "form-control", label: false, required: true 8 | .form-group.text-center 9 | = submit gettext("Remind Password"), class: "btn btn-primary w-100" 10 | 11 | .text-center 12 | = gettext("Trying sign in?") 13 | = link gettext("Sign In"), to: Routes.session_path(@conn, :new), class: "ml-1" 14 | .text-center.mt-2 15 | = gettext("Dont have account?") 16 | = link gettext("Sign Up"), to: Routes.user_path(@conn, :new), class: "ml-1" 17 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/session/new.html.slime: -------------------------------------------------------------------------------- 1 | .row.justify-content-center.pb-5.mt-5 2 | .col-12.col-sm-8.col-md-7.col-lg-4 3 | .card.border-top 4 | .card-body 5 | h1.mb-4.text-center= gettext("Sign In") 6 | = form_for @conn, Routes.session_path(@conn, :create), [as: :session], fn f -> 7 | = input f, :email, placeholder: gettext("Email"), class: "form-control mb-3", label: false 8 | = input f, :password, as: :password, placeholder: gettext("Password"), class: "form-control mb-3", label: false 9 | .text-right.mb-3 10 | = link gettext("Forgot password?"), to: Routes.remind_password_path(@conn, :new) 11 | 12 | .form-group 13 | = submit gettext("Sign In"), class: "btn btn-primary mb-2 btn-block" 14 | = render HexletBasicsWeb.SharedView, "social_sign_in.html", assigns 15 | 16 | .card-footer.text-center 17 | span.mr-1 18 | = gettext("Dont have an account?") 19 | = link gettext("Sign Up"), to: Routes.user_path(@conn, :new) 20 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/shared/social_sign_in.html.slime: -------------------------------------------------------------------------------- 1 | = if @current_user.guest do 2 | = link to: Routes.auth_path(@conn, :request, "github"), class: "btn btn-secondary btn-block" do 3 | i.fab.mr-2.fa-github 4 | = gettext("Sign In Github") 5 | / .row.mt-3 6 | / .col-12 7 | / = link to: Routes.auth_path(@conn, :request, "facebook"), class: "btn btn-primary w-100" do 8 | / i.fab.mr-2.fa-facebook 9 | / = gettext("Sign In Facebook") 10 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/templates/user/new.html.slime: -------------------------------------------------------------------------------- 1 | .row.justify-content-center.pb-5.mt-5 2 | .col-12.col-sm-8.col-md-7.col-lg-4 3 | .card.border-top 4 | .card-body 5 | h1.mb-4.text-center= gettext("Sign Up") 6 | = form_for @changeset, Routes.user_path(@conn, :create), fn form -> 7 | = if @changeset.action do 8 | .alert.alert-danger 9 | p 10 | = gettext("Oops, something went wrong! Please check the errors below.") 11 | = input form, :email, as: :email, class: "form-control mb-3", placeholder: gettext("Email"), label: false, required: true 12 | = input form, :password, as: :password, class: "form-control mb-3", placeholder: gettext("Password"), label: false, required: true 13 | 14 | .form-group 15 | = submit gettext("Registration"), class: "btn btn-primary mb-2 w-100" 16 | = render HexletBasicsWeb.SharedView, "social_sign_in.html", assigns 17 | .card-footer.text-center 18 | span.mr-1 19 | = gettext("Have an account?") 20 | = link gettext("Sign In"), to: Routes.session_path(@conn, :new) 21 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/email_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.EmailView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) -> 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(HexletBasicsWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(HexletBasicsWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ErrorView do 2 | use HexletBasicsWeb, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/language/module/lesson_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Language.Module.LessonView do 2 | use HexletBasicsWeb, :view 3 | 4 | # TODO Extract to shared 5 | def locales_switch_map, do: %{"ru" => "en", "en" => "ru"} 6 | 7 | def description_on_github_cpath(conn, lesson) do 8 | repository_name = "exercises-#{lesson.module.language.slug}" 9 | module_dir_name = "#{lesson.module.order}-#{lesson.module.slug}" 10 | lesson_dir_name = "#{lesson.order}-#{lesson.slug}" 11 | locale = conn.assigns.locale 12 | 13 | "https://github.com/hexlet-basics/#{repository_name}/blob/master/modules/#{module_dir_name}/#{lesson_dir_name}/description.#{locale}.yml" 14 | end 15 | 16 | end 17 | 18 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/language/module_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Language.ModuleView do 2 | use HexletBasicsWeb, :view 3 | end 4 | 5 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/language_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LanguageView do 2 | use HexletBasicsWeb, :view 3 | 4 | def hexlet_professions_map(slug) do 5 | map = %{ 6 | "javascript" => "frontend", 7 | "python" => "python", 8 | "php" => "php", 9 | "html" => "layout-designer", 10 | "css" => "layout-designer", 11 | "java" => "java" 12 | } 13 | if Map.has_key?(map, slug), do: map[slug], else: nil 14 | end 15 | 16 | def hexlet_profession_cpath(language) do 17 | lang = hexlet_professions_map(language) 18 | if lang, do: "/professions/#{lang}", else: "/professions" 19 | end 20 | 21 | def profession_static_cpath(language) do 22 | lang = hexlet_professions_map(language) 23 | if lang do 24 | "/images/prof_icons/#{lang}.svg" 25 | else 26 | "/images/hexlet_logo.png" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/layout/shared.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Layout.SharedView do 2 | use HexletBasicsWeb, :view 3 | import PhoenixGon.View 4 | 5 | def meta_tags(attrs_list) do 6 | Enum.map(attrs_list, &meta_tag/1) 7 | end 8 | 9 | def meta_tag(attrs) do 10 | tag(:meta, Enum.into(attrs, [])) 11 | end 12 | 13 | def link_tags(attrs_list) do 14 | Enum.map(attrs_list, &link_tag/1) 15 | end 16 | 17 | def link_tag(attrs) do 18 | tag(:link, Enum.into(attrs, [])) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LayoutView do 2 | use HexletBasicsWeb, :view 3 | # import HexletBasics.PageTitle 4 | 5 | def locales_switch_map, do: %{"ru" => "en", "en" => "ru"} 6 | 7 | def alert_name_by_flash(name) do 8 | map = %{ 9 | info: 'info', 10 | error: 'danger' 11 | } 12 | 13 | map[name] 14 | end 15 | 16 | def render_schema(schema) do 17 | Jason.encode!(schema) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.PageView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/password_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.PasswordView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/profile_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ProfileView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/remind_password_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.RemindPasswordView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.SessionView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/shared_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.SharedView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/lib/hexlet_basics_web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.UserView do 2 | use HexletBasicsWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /services/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // parser: 'sugarss', 3 | plugins: { 4 | 'postcss-import': {}, 5 | 'postcss-preset-env': {}, 6 | cssnano: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /services/web/priv/gettext/ru/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: ru_RU\n" 12 | 13 | msgid "can't be blank" 14 | msgstr "не может быть пустым" 15 | 16 | msgid "has already been taken" 17 | msgstr "уже существует" 18 | 19 | msgid "is invalid" 20 | msgstr "" 21 | 22 | msgid "must be accepted" 23 | msgstr "" 24 | 25 | msgid "has invalid format" 26 | msgstr "имеет неверный формат" 27 | 28 | msgid "has an invalid entry" 29 | msgstr "" 30 | 31 | msgid "is reserved" 32 | msgstr "" 33 | 34 | msgid "does not match confirmation" 35 | msgstr "" 36 | 37 | msgid "is still associated with this entry" 38 | msgstr "" 39 | 40 | msgid "are still associated with this entry" 41 | msgstr "" 42 | 43 | msgid "should be %{count} character(s)" 44 | msgid_plural "should be %{count} character(s)" 45 | msgstr[0] "" 46 | msgstr[1] "" 47 | 48 | msgid "should have %{count} item(s)" 49 | msgid_plural "should have %{count} item(s)" 50 | msgstr[0] "" 51 | msgstr[1] "" 52 | 53 | msgid "should be at least %{count} character(s)" 54 | msgid_plural "should be at least %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have at least %{count} item(s)" 59 | msgid_plural "should have at least %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at most %{count} character(s)" 64 | msgid_plural "should be at most %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at most %{count} item(s)" 69 | msgid_plural "should have at most %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "must be less than %{number}" 74 | msgstr "" 75 | 76 | msgid "must be greater than %{number}" 77 | msgstr "" 78 | 79 | msgid "must be less than or equal to %{number}" 80 | msgstr "" 81 | 82 | msgid "must be greater than or equal to %{number}" 83 | msgstr "" 84 | 85 | msgid "must be equal to %{number}" 86 | msgstr "" 87 | -------------------------------------------------------------------------------- /services/web/priv/repo/migrations/20190911072926_migrate_email_deliviery_state_for_user.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Repo.Migrations.MigrateEmailDelivieryStateForUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | HexletBasics.Repo.update_all(HexletBasics.User, set: [email_delivery_state: "enabled"]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /services/web/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # HexletBasics.Repo.insert!(%HexletBasics.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/api/lesson/check_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Api.Lesson.CheckControllerTest do 2 | import Plug.Conn 3 | use HexletBasicsWeb.ConnCase 4 | alias HexletBasics.UserManager.Guardian 5 | 6 | test "create", %{conn: conn} do 7 | lesson = insert(:language_module_lesson) 8 | data = %{ 9 | attributes: %{ 10 | code: %{ content: " Guardian.Plug.sign_in(user) 20 | |> post(lesson_check_path(conn, :create, lesson.id), data: data) 21 | 22 | assert json_response(conn, 200) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/api/sparkpost_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Api.SparkpostControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | alias HexletBasics.{User, UserManager} 4 | 5 | setup [:prepare_events] 6 | 7 | test "process", %{conn: conn, disabled_delivery_user: disabled_delivery_user, delivery_user: delivery_user, spam_user: spam_user, events: events} do 8 | conn = 9 | conn 10 | |> put_req_header("content-type", "application/json") 11 | |> post(sparkpost_path(conn, :process), Jason.encode!(events)) 12 | 13 | disabled_delivery_user = UserManager.get_user!(disabled_delivery_user.id) 14 | spam_user = UserManager.get_user!(spam_user.id) 15 | delivery_user = UserManager.get_user!(delivery_user.id) 16 | 17 | assert json_response(conn, 200) 18 | assert User.disabled_delivery?(disabled_delivery_user) 19 | assert User.disabled_delivery?(spam_user) 20 | assert User.enabled_delivery?(delivery_user) 21 | end 22 | 23 | def prepare_events(_) do 24 | disabled_delivery_user = insert(:user) 25 | delivery_user = insert(:user) 26 | spam_user = insert(:user) 27 | events = %{"_json" => 28 | [ 29 | %{ 30 | msys: %{ 31 | message_event: %{ 32 | type: "bounce", 33 | rcpt_meta: %{ 34 | user_id: disabled_delivery_user.id 35 | } 36 | } 37 | } 38 | }, 39 | %{ 40 | msys: %{ 41 | message_event: %{ 42 | type: "spam_complaint", 43 | rcpt_meta: %{ 44 | user_id: spam_user.id 45 | } 46 | } 47 | } 48 | }, 49 | %{ msys: %{ 50 | message_event: %{ 51 | type: "delivery", 52 | rcpt_meta: %{ 53 | user_id: delivery_user.id 54 | } 55 | } 56 | } 57 | } 58 | ] 59 | } 60 | {:ok, disabled_delivery_user: disabled_delivery_user, delivery_user: delivery_user, events: events, spam_user: spam_user} 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/auth_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.AuthControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | 4 | test "/auth/github/callback ueberauth failure", %{conn: conn} do 5 | failure_conn = Map.put(conn, :assigns, %{ueberauth_failure: %{}}) 6 | conn = get(failure_conn, "/auth/github/callback") 7 | 8 | assert conn.state == :sent 9 | assert redirected_to(conn) == page_path(conn, :index) 10 | end 11 | 12 | test "when callback succeeds" , %{conn: conn} do 13 | auth = %Ueberauth.Auth{ 14 | provider: :github, 15 | uid: 12345, 16 | info: %{ 17 | nickname: "johndoe", 18 | email: "john.doe@example.com", 19 | } 20 | } 21 | 22 | conn = 23 | conn 24 | |> bypass_through(HexletBasicsWeb.Router, [:browser]) 25 | |> get("/auth/github/callback", code: "12345") 26 | |> assign(:ueberauth_auth, auth) 27 | |> HexletBasicsWeb.AuthController.callback(%{}) 28 | 29 | assert redirected_to(conn) == page_path(conn, :index) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/language/module/lesson_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Language.Module.LessonControllerTest do 2 | use HexletBasicsWeb.ConnCase 3 | alias HexletBasics.UserManager.Guardian 4 | 5 | test "show", %{conn: conn} do 6 | lesson = insert(:language_module_lesson) 7 | module = lesson.module 8 | language = lesson.language 9 | 10 | conn = conn 11 | |> get(language_module_lesson_path(conn, :show, language.slug, module.slug, lesson.slug)) 12 | assert html_response(conn, 200) 13 | end 14 | 15 | test "show for signed user", %{conn: conn} do 16 | user = insert(:user) 17 | lesson = insert(:language_module_lesson) 18 | module = lesson.module 19 | language = lesson.language 20 | 21 | conn = conn 22 | |> Guardian.Plug.sign_in(user) 23 | |> get(language_module_lesson_path(conn, :show, language.slug, module.slug, lesson.slug)) 24 | assert html_response(conn, 200) 25 | end 26 | 27 | test 'index', %{conn: conn} do 28 | module = insert(:language_module) 29 | language = module.language 30 | conn = get conn, language_module_lesson_path(conn, :index, language.slug, module.slug) 31 | assert html_response(conn, 200) 32 | end 33 | end 34 | 35 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/language/module_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.Language.ModuleControllerTest do 2 | use HexletBasicsWeb.ConnCase 3 | 4 | test "show", %{conn: conn} do 5 | module = insert(:language_module) 6 | language = module.language 7 | conn = get conn, language_module_path(conn, :show, language.slug, module.slug) 8 | assert html_response(conn, 200) 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/language_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LanguageControllerTest do 2 | use HexletBasicsWeb.ConnCase 3 | alias HexletBasics.UserManager.Guardian 4 | 5 | test "index", %{conn: conn} do 6 | conn = get conn, "/" 7 | assert html_response(conn, 200) 8 | end 9 | 10 | test "show", %{conn: conn} do 11 | lesson = insert(:language_module_lesson) 12 | language = lesson.language 13 | conn = get conn, language_path(conn, :show, language.slug) 14 | assert html_response(conn, 200) 15 | end 16 | 17 | test "show for signed user", %{conn: conn} do 18 | user = insert(:user) 19 | lesson = insert(:language_module_lesson) 20 | language = lesson.language 21 | conn = conn 22 | |> Guardian.Plug.sign_in(user) 23 | |> get(language_path(conn, :show, language.slug)) 24 | assert html_response(conn, 200) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/lesson_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LessonControllerTest do 2 | use HexletBasicsWeb.ConnCase 3 | alias HexletBasics.{UserManager.Guardian} 4 | 5 | test "next not authorized user", %{conn: conn} do 6 | lesson = insert(:language_module_lesson) 7 | conn = get conn, lesson_member_path(conn, :next, lesson.id) 8 | assert html_response(conn, 302) 9 | end 10 | 11 | test "next", %{conn: conn} do 12 | user = insert(:user) 13 | lesson = insert(:language_module_lesson) 14 | next_lesson = insert(:language_module_lesson, %{language: lesson.language, module: lesson.module, natural_order: 110, upload: lesson.language.upload}) 15 | 16 | conn = conn 17 | |> Guardian.Plug.sign_in(user) 18 | |>get(lesson_member_path(conn, :next, lesson.id)) 19 | language = next_lesson.language 20 | module = next_lesson.module 21 | path = 22 | language_module_lesson_path(conn, :show, language.slug, module.slug, next_lesson.slug) 23 | 24 | assert redirected_to(conn) == path 25 | end 26 | 27 | test "next when not finished lesson exists", %{conn: conn} do 28 | not_finished_lesson = insert(:language_module_lesson) 29 | finished_lesson = insert(:language_module_lesson, 30 | %{language: not_finished_lesson.language, 31 | module: not_finished_lesson.module, 32 | upload: not_finished_lesson.language.upload, 33 | natural_order: 110 34 | }) 35 | 36 | user_with_all_finished_lessons = insert(:user) 37 | _first_finished_lesson = insert(:user_finished_lesson, %{language_module_lesson: finished_lesson, user: user_with_all_finished_lessons}) 38 | _second_finished_lesson = insert(:user_finished_lesson, %{language_module_lesson: not_finished_lesson, user: user_with_all_finished_lessons}) 39 | 40 | user_with_last_finished_lesson = insert(:user) 41 | _last_finished_lesson = insert(:user_finished_lesson, %{language_module_lesson: finished_lesson, user: user_with_last_finished_lesson}) 42 | 43 | conn = conn 44 | |> Guardian.Plug.sign_in(user_with_last_finished_lesson) 45 | |>get(lesson_member_path(conn, :next, finished_lesson.id)) 46 | language = not_finished_lesson.language 47 | module = not_finished_lesson.module 48 | path = 49 | language_module_lesson_path(conn, :show, language.slug, module.slug, not_finished_lesson.slug) 50 | 51 | assert redirected_to(conn) == path 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/locale_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LocaleControllerTest do 2 | use HexletBasicsWeb.ConnCase 3 | alias HexletBasics.UserManager.Guardian 4 | alias HexletBasics.UserManager 5 | alias HexletBasicsWeb.Helpers.CustomUrl 6 | 7 | test "switch to ru", %{conn: conn} do 8 | locale = "ru" 9 | url = CustomUrl.redirect_current_url(conn, locale) 10 | user = insert(:user) 11 | 12 | conn = conn 13 | |> Guardian.Plug.sign_in(user) 14 | |>get(locale_path(conn, :switch, redirect_url: url, locale: locale)) 15 | 16 | new_locale = get_session(conn, :locale) 17 | user = UserManager.get_user!(user.id) 18 | assert redirected_to(conn) == url 19 | assert new_locale == locale 20 | assert user.locale == locale 21 | end 22 | 23 | test "switch en", %{conn: conn} do 24 | locale = "en" 25 | url = CustomUrl.redirect_current_url(conn, locale) 26 | user = insert(:user) 27 | 28 | conn = conn 29 | |> Guardian.Plug.sign_in(user) 30 | |>get(locale_path(conn, :switch, redirect_url: url, locale: locale)) 31 | 32 | new_locale = get_session(conn, :locale) 33 | user = UserManager.get_user!(user.id) 34 | assert redirected_to(conn) == url 35 | assert new_locale == locale 36 | assert user.locale == locale 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.PageControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, page_path(conn, :index) 6 | assert html_response(conn, 200) 7 | end 8 | 9 | test "show about", %{conn: conn} do 10 | conn = get conn, page_path(conn, :show, "about") 11 | assert html_response(conn, 200) 12 | end 13 | 14 | test "must be 404", %{conn: conn} do 15 | assert_raise Phoenix.Router.NoRouteError, fn -> 16 | get conn, page_path(conn, :show, "bla-bla-bla") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/password_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.PasswordControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | alias HexletBasics.{UserManager.Guardian, UserManager} 4 | 5 | @reset_password_token "123456" 6 | @create_attrs %{ 7 | encrypted_password: Bcrypt.hash_pwd_salt("password"), 8 | email: "user@mail.ru", 9 | reset_password_token: @reset_password_token 10 | } 11 | @update_attrs %{encrypted_password: Bcrypt.hash_pwd_salt("new_password")} 12 | 13 | setup [:create_user] 14 | 15 | test "#edit", %{conn: conn} do 16 | conn = get conn, password_path(conn, :edit, reset_password_token: @reset_password_token) 17 | assert html_response(conn, 200) 18 | end 19 | 20 | test "#edit without params", %{conn: conn} do 21 | conn = get conn, password_path(conn, :edit) 22 | assert redirected_to(conn) == remind_password_path(conn, :new) 23 | end 24 | 25 | test "#edit when user not found", %{conn: conn} do 26 | conn = get conn, password_path(conn, :edit, reset_password_token: "123") 27 | assert redirected_to(conn) == remind_password_path(conn, :new) 28 | end 29 | 30 | 31 | test "#update", %{conn: conn} do 32 | conn = patch conn, password_path(conn, :update, reset_password_token: @reset_password_token), user: @update_attrs 33 | 34 | assert redirected_to(conn) == session_path(conn, :new) 35 | end 36 | 37 | test "#update when user not found", %{conn: conn} do 38 | conn = patch conn, password_path(conn, :update, reset_password_token: "123"), user: @update_attrs 39 | 40 | assert redirected_to(conn) == remind_password_path(conn, :new) 41 | end 42 | 43 | test "reset_password", %{conn: conn, user: user} do 44 | conn = 45 | conn 46 | |> Guardian.Plug.sign_in(user) 47 | |> post(password_path(conn, :reset_password, redirect_to: profile_path(conn, :show))) 48 | 49 | reset_password_user = UserManager.get_user!(user.id) 50 | assert redirected_to(conn) == profile_path(conn, :show) 51 | assert user.reset_password_token != reset_password_user.reset_password_token 52 | end 53 | 54 | defp create_user(_) do 55 | user = insert(:user, @create_attrs) 56 | {:ok, user: user} 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/remind_password_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.RemindPasswordControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | 4 | 5 | @create_attrs %{ 6 | encrypted_password: Bcrypt.hash_pwd_salt("password"), 7 | email: "user@mail.ru", 8 | reset_password_token: "reset_password_token" 9 | } 10 | 11 | setup [:create_user] 12 | 13 | test "#new", %{conn: conn} do 14 | conn = get conn, remind_password_path(conn, :new) 15 | assert html_response(conn, 200) 16 | end 17 | 18 | test "#create", %{conn: conn, user: user} do 19 | conn = post conn, remind_password_path(conn, :create), user: %{email: user.email} 20 | 21 | assert redirected_to(conn) == page_path(conn, :index) 22 | end 23 | 24 | test "#update when user not found", %{conn: conn} do 25 | conn = post conn, remind_password_path(conn, :create), user: %{email: "notexist@mail.ru"} 26 | 27 | assert redirected_to(conn) == remind_password_path(conn, :new) 28 | end 29 | 30 | defp create_user(_) do 31 | user = insert(:user, @create_attrs) 32 | {:ok, user: user} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/session_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.SessionControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | alias HexletBasics.UserManager.Guardian 4 | 5 | @create_attrs %{encrypted_password: Bcrypt.hash_pwd_salt("password"), email: "user@mail.ru"} 6 | @session_attrs %{password: "password", email: "user@mail.ru"} 7 | 8 | test "#new", %{conn: conn} do 9 | conn = get conn, user_path(conn, :new) 10 | assert html_response(conn, 200) 11 | end 12 | 13 | test "#create", %{conn: conn} do 14 | _user = insert(:user, @create_attrs) 15 | conn = post conn, session_path(conn, :create), session: @session_attrs 16 | 17 | assert redirected_to(conn) == page_path(conn, :index) 18 | end 19 | 20 | test "#create when user doesnt have encrypted_password", %{conn: conn} do 21 | _user = insert(:user, %{email: "user@mail.ru"}) 22 | conn = post conn, session_path(conn, :create), session: @session_attrs 23 | 24 | assert html_response(conn, 200) 25 | end 26 | 27 | test "#delete", %{conn: conn} do 28 | user = insert(:user, @create_attrs) 29 | conn = conn 30 | |> Guardian.Plug.sign_in(user) 31 | |> delete(session_path(conn, :delete)) 32 | 33 | assert redirected_to(conn) == page_path(conn, :index) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.UserControllerTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | alias HexletBasics.{UserManager.Guardian, UserManager} 4 | 5 | @create_attrs %{password: "password", email: "user@mail.ru", confirmation_token: "1234"} 6 | @waiting_confirmation_attrs %{password: "password", email: "user@mail.ru", confirmation_token: "1234", state: "waiting_confirmation"} 7 | @invalid_attrs %{password: nil, email: nil} 8 | 9 | test "render new", %{conn: conn} do 10 | conn = get conn, user_path(conn, :new) 11 | assert html_response(conn, 200) 12 | end 13 | 14 | test "create user", %{conn: conn} do 15 | conn = post conn, user_path(conn, :create), user: @create_attrs 16 | 17 | assert redirected_to(conn) == page_path(conn, :index) 18 | end 19 | 20 | test "renders errors when data is invalid", %{conn: conn} do 21 | conn = post conn, user_path(conn, :create), user: @invalid_attrs 22 | assert html_response(conn, 200) 23 | end 24 | 25 | test "confirm user", %{conn: conn} do 26 | user = insert(:user, @waiting_confirmation_attrs) 27 | 28 | conn = get conn, user_path(conn, :confirm, confirmation_token: user.confirmation_token) 29 | 30 | confirmed_user = UserManager.get_user!(user.id) 31 | assert redirected_to(conn) == page_path(conn, :index) 32 | assert confirmed_user.state == "active" 33 | end 34 | 35 | test "confirm user when user not found", %{conn: conn} do 36 | user = insert(:user, @waiting_confirmation_attrs) 37 | 38 | conn = get conn, user_path(conn, :confirm, confirmation_token: "abcd") 39 | 40 | confirmed_user = UserManager.get_user!(user.id) 41 | assert redirected_to(conn) == page_path(conn, :index) 42 | assert confirmed_user.state == "waiting_confirmation" 43 | end 44 | 45 | test "resend_confirmation", %{conn: conn} do 46 | user = insert(:user, @waiting_confirmation_attrs) 47 | conn = 48 | conn 49 | |> Guardian.Plug.sign_in(user) 50 | |> post(user_path(conn, :resend_confirmation, user, redirect_to: profile_path(conn, :show))) 51 | 52 | not_confirmed_user = UserManager.get_user!(user.id) 53 | assert not_confirmed_user.confirmation_token != user.confirmation_token 54 | assert redirected_to(conn) == profile_path(conn, :show) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ErrorViewTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(HexletBasicsWeb.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(HexletBasicsWeb.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(HexletBasicsWeb.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.LayoutViewTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /services/web/test/hexlet_basics_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.PageViewTest do 2 | use HexletBasicsWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /services/web/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint HexletBasicsWeb.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(HexletBasics.Repo) 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(HexletBasics.Repo, {:shared, self()}) 33 | end 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /services/web/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasicsWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | @session Plug.Session.init( 19 | store: :cookie, 20 | key: "_app", 21 | encryption_salt: "yadayada", 22 | signing_salt: "yadayada" 23 | ) 24 | 25 | using do 26 | quote do 27 | # Import conveniences for testing with connections 28 | use Phoenix.ConnTest 29 | import HexletBasicsWeb.Router.Helpers 30 | import HexletBasics.Factory 31 | 32 | # The default endpoint for testing 33 | @endpoint HexletBasicsWeb.Endpoint 34 | end 35 | end 36 | 37 | 38 | setup tags do 39 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(HexletBasics.Repo) 40 | unless tags[:async] do 41 | Ecto.Adapters.SQL.Sandbox.mode(HexletBasics.Repo, {:shared, self()}) 42 | end 43 | conn = Phoenix.ConnTest.build_conn() 44 | |> Plug.Session.call(@session) 45 | |> Plug.Conn.fetch_session() 46 | {:ok, conn: conn } 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /services/web/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias HexletBasics.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import HexletBasics.DataCase 25 | import HexletBasics.Factory 26 | end 27 | end 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(HexletBasics.Repo) 31 | 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(HexletBasics.Repo, {:shared, self()}) 34 | end 35 | 36 | :ok 37 | end 38 | 39 | @doc """ 40 | A helper that transform changeset errors to a map of messages. 41 | 42 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 43 | assert "password is too short" in errors_on(changeset).password 44 | assert %{password: ["password is too short"]} = errors_on(changeset) 45 | 46 | """ 47 | def errors_on(changeset) do 48 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 49 | Enum.reduce(opts, message, fn {key, value}, acc -> 50 | String.replace(acc, "%{#{key}}", to_string(value)) 51 | end) 52 | end) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /services/web/test/support/factories/language_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.LanguageFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def language_factory do 5 | %HexletBasics.Language{ 6 | slug: Faker.Internet.slug, 7 | name: Faker.Internet.slug, 8 | upload: build(:upload), 9 | docker_image: "hexlet/hexlet-basics-exercises-php", 10 | extension: "php", 11 | exercise_filename: "index.php", 12 | exercise_test_filename: "Test.php" 13 | } 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /services/web/test/support/factories/language_module_description_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.LanguageModuleDescriptionFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def language_module_description_factory do 5 | %HexletBasics.Language.Module.Description{ 6 | # module: build(:language_module), 7 | name: Faker.Lorem.word, 8 | description: Faker.Lorem.paragraph, 9 | locale: "ru" 10 | } 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /services/web/test/support/factories/language_module_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.LanguageModuleFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def language_module_factory do 5 | language = insert(:language) 6 | %HexletBasics.Language.Module{ 7 | slug: Faker.Internet.slug, 8 | language: language, 9 | upload: language.upload, 10 | order: 100, 11 | descriptions: [ 12 | build(:language_module_description, language: language), 13 | build(:language_module_description, locale: "en", language: language) 14 | ] 15 | } 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /services/web/test/support/factories/language_module_lesson_description_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.LanguageModuleLessonDescriptionFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def language_module_lesson_description_factory do 5 | %HexletBasics.Language.Module.Lesson.Description{ 6 | # lesson: build(:lesson), 7 | theory: Faker.Lorem.paragraph, 8 | instructions: Faker.Lorem.paragraph, 9 | locale: "ru" 10 | } 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /services/web/test/support/factories/language_module_lesson_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.LanguageModuleLessonFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def language_module_lesson_factory do 5 | module = insert(:language_module) 6 | language = module.language 7 | 8 | %HexletBasics.Language.Module.Lesson{ 9 | slug: Faker.Internet.slug(), 10 | order: 100, 11 | natural_order: 100, 12 | module: module, 13 | language: language, 14 | upload: module.upload, 15 | path_to_code: "/exercises/modules/10-basics/10-hello-world", 16 | descriptions: [ 17 | build(:language_module_lesson_description, language: language), 18 | build(:language_module_lesson_description, locale: "en", language: language) 19 | ] 20 | } 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /services/web/test/support/factories/upload_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UploadFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def upload_factory do 5 | %HexletBasics.Upload{} 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /services/web/test/support/factories/user_account_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UserAccountFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def user_account_factory do 5 | user = insert(:user) 6 | 7 | %HexletBasics.User.Account{ 8 | uid: to_string(System.unique_integer([:monotonic, :positive])), 9 | provider: "github", 10 | user: user 11 | } 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /services/web/test/support/factories/user_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UserFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def user_factory do 5 | %HexletBasics.User{ 6 | github_uid: System.unique_integer([:monotonic, :positive]), 7 | nickname: Faker.Internet.slug, 8 | state: "active" 9 | } 10 | end 11 | 12 | def will_removed_user_factory do 13 | %HexletBasics.User{ 14 | email: "user@user.ru", 15 | github_uid: System.unique_integer([:monotonic, :positive]), 16 | facebook_uid: to_string(System.unique_integer([:monotonic, :positive])), 17 | nickname: Faker.Internet.slug, 18 | first_name: Faker.Name.En.first_name(), 19 | last_name: Faker.Name.En.first_name(), 20 | encrypted_password: Bcrypt.hash_pwd_salt("password"), 21 | reset_password_token: Bcrypt.hash_pwd_salt("reset_token"), 22 | confirmation_token: Bcrypt.hash_pwd_salt("confirmation_token"), 23 | state: "active" 24 | } 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /services/web/test/support/factories/user_finished_lesson_factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.UserFinishedLessonFactory do 2 | defmacro __using__(_opts) do 3 | quote do 4 | def user_finished_lesson_factory do 5 | user = insert(:user) 6 | lesson = insert(:language_module_lesson) 7 | 8 | %HexletBasics.User.FinishedLesson{ 9 | user: user, 10 | language_module_lesson: lesson 11 | } 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /services/web/test/support/factory.ex: -------------------------------------------------------------------------------- 1 | defmodule HexletBasics.Factory do 2 | use ExMachina.Ecto, repo: HexletBasics.Repo 3 | use HexletBasics.LanguageFactory 4 | use HexletBasics.UploadFactory 5 | use HexletBasics.LanguageModuleFactory 6 | use HexletBasics.LanguageModuleDescriptionFactory 7 | use HexletBasics.LanguageModuleLessonFactory 8 | use HexletBasics.LanguageModuleLessonDescriptionFactory 9 | use HexletBasics.UserFactory 10 | use HexletBasics.UserAccountFactory 11 | use HexletBasics.UserFinishedLessonFactory 12 | end 13 | -------------------------------------------------------------------------------- /services/web/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Faker.start() 3 | 4 | Ecto.Adapters.SQL.Sandbox.mode(HexletBasics.Repo, :manual) 5 | 6 | -------------------------------------------------------------------------------- /services/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "assets/js/**/*" 4 | ], 5 | "compilerOptions": { 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "allowJs": true, 9 | "jsx": "react" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /terraform/Makefile: -------------------------------------------------------------------------------- 1 | export GOOGLE_CREDENTIALS=~/.config/gcloud/terraform-admin.json 2 | init: 3 | @echo $(GOOGLE_CREDENTIALS) 4 | 5 | PHONY: init 6 | -------------------------------------------------------------------------------- /terraform/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "gcs" { 3 | bucket = "hexlet-basics-terraform-state" 4 | prefix = "production" 5 | credentials = "google.key.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /terraform/cloudflare.tf: -------------------------------------------------------------------------------- 1 | variable "domain" { 2 | default = "code-basics.com" 3 | } 4 | 5 | resource "cloudflare_record" "bounces" { 6 | domain = var.domain 7 | name = "bounces" 8 | value = "sparkpostmail.com" 9 | type = "CNAME" 10 | proxied = false 11 | } 12 | 13 | resource "cloudflare_record" "txt" { 14 | domain = var.domain 15 | name = "scph0819._domainkey" 16 | value = "v=DKIM1; k=rsa; h=sha256; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCaY5OgnrfYY/bD07hyyqiVtk4Pxs9iQuN7u7SCNbD2d1JQyGOXcSD7t/A6VUZum6HlgOegSdi3p9gMb4wc9C6e/RQV5EblIdwABvLMYmC0CN+DDarNrF93Sejn44vjSY+kK6jEbqFBOc7qqO9k4Nep/sXb6gEsq6a9YvOHaeivFQIDAQAB" 17 | type = "TXT" 18 | } 19 | 20 | resource "cloudflare_record" "yandex-verification" { 21 | domain = var.domain 22 | name = var.domain 23 | value = "yandex-verification=a1675c88bd4b0ade" 24 | type = "TXT" 25 | } 26 | 27 | resource "cloudflare_record" "yandex-verification-ru" { 28 | domain = var.domain 29 | name = "ru.${var.domain}" 30 | value = "yandex-verification=a1675c88bd4b0ade" 31 | type = "TXT" 32 | } 33 | 34 | resource "cloudflare_record" "google-verification-ru" { 35 | domain = var.domain 36 | name = "ru.${var.domain}" 37 | value = "google-site-verification=AdJxboarIC6NOwJ9CEkIeZXdNE7DqamnPo0P7J4DJDw" 38 | type = "TXT" 39 | } 40 | 41 | resource "cloudflare_record" "google-verification-com" { 42 | domain = var.domain 43 | name = var.domain 44 | value = "google-site-verification=0kk3DdOciLvoog-nVFcbRzmZH65FWmNW-_aYElP0gJk" 45 | type = "TXT" 46 | } 47 | 48 | resource "cloudflare_record" "facebook-domain-verification" { 49 | domain = var.domain 50 | name = var.domain 51 | value = "facebook-domain-verification=d7d3em3a29yebcswwq8aa57shrc1m6" 52 | type = "TXT" 53 | } 54 | 55 | -------------------------------------------------------------------------------- /terraform/dok8s.tf: -------------------------------------------------------------------------------- 1 | resource "digitalocean_kubernetes_cluster" "hexlet_basics_3" { 2 | version = "1.18.8-do.0" 3 | 4 | name = "hexlet-basics-3" 5 | region = "fra1" 6 | 7 | node_pool { 8 | name = "hexlet-basics-node-pool-2" 9 | size = "c-2" 10 | auto_scale = true 11 | 12 | min_nodes = 3 13 | max_nodes = 3 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /terraform/project.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | version = "~> 2.7" 3 | region = var.region 4 | credentials = file("google.key.json") 5 | } 6 | 7 | provider "cloudflare" { 8 | version = "~> 1.15.0" 9 | email = var.cloudflare_email 10 | token = var.cloudflare_api_key 11 | } 12 | 13 | provider "kubernetes" { 14 | version = "~> 1.7" 15 | 16 | # region = var.region 17 | } 18 | 19 | resource "google_project" "hexlet_basics" { 20 | name = var.project_name 21 | project_id = var.project_name 22 | billing_account = var.billing_account 23 | org_id = var.org_id 24 | } 25 | 26 | provider "digitalocean" { 27 | version = "~> 1.16.0" 28 | token = "4700689d4645fd83cd49145e4d870644f7b8d538444d537d8316261cadb099d3" 29 | } 30 | 31 | output "project_id" { 32 | value = google_project.hexlet_basics.project_id 33 | } 34 | -------------------------------------------------------------------------------- /terraform/source.tf: -------------------------------------------------------------------------------- 1 | # resource "google_sourcerepo_repository" "exercises_php" { 2 | # project = "${var.project_name}" 3 | # name = "github_hexlet-basics_exercises-php" 4 | # } 5 | # resource "google_sourcerepo_repository" "exercises_java" { 6 | # project = "${var.project_name}" 7 | # name = "github_hexlet-basics_exercises-java" 8 | # } 9 | # resource "google_sourcerepo_repository" "exercises_javascript" { 10 | # project = "${var.project_name}" 11 | # name = "github_hexlet-basics_exercises-javascript" 12 | # } 13 | # resource "google_sourcerepo_repository" "exercises_python" { 14 | # project = "${var.project_name}" 15 | # name = "github_hexlet-basics_exercises-python" 16 | # } 17 | # resource "google_sourcerepo_repository" "hexlet_basics" { 18 | # project = "${var.project_name}" 19 | # name = "github_hexlet-basics_hexlet_5Fbasics" 20 | # } 21 | 22 | -------------------------------------------------------------------------------- /terraform/sql.tf: -------------------------------------------------------------------------------- 1 | resource "google_sql_database_instance" "master" { 2 | project = var.project_name 3 | name = "master3" 4 | database_version = "POSTGRES_9_6" 5 | region = var.region 6 | 7 | settings { 8 | tier = "db-f1-micro" 9 | availability_type = "REGIONAL" 10 | 11 | backup_configuration { 12 | enabled = true 13 | } 14 | } 15 | } 16 | 17 | resource "google_sql_user" "hexlet_basics" { 18 | project = var.project_name 19 | name = "hexlet_basics" 20 | instance = google_sql_database_instance.master.name 21 | password = var.db_password 22 | } 23 | 24 | resource "google_sql_database" "hexlet_basics_prod" { 25 | project = var.project_name 26 | name = var.db_name 27 | instance = google_sql_database_instance.master.name 28 | } 29 | 30 | resource "digitalocean_database_cluster" "hexlet_basics" { 31 | name = "master3" 32 | engine = "pg" 33 | version = "11" 34 | 35 | size = "db-s-1vcpu-1gb" 36 | region = "fra1" 37 | node_count = 1 38 | } 39 | 40 | resource "digitalocean_database_user" "hexlet_basics" { 41 | name = "hexlet_basics" 42 | cluster_id = digitalocean_database_cluster.hexlet_basics.id 43 | # password = var.db_password 44 | } 45 | 46 | resource "digitalocean_database_db" "hexlet_basics_prod" { 47 | name = var.db_name 48 | cluster_id = digitalocean_database_cluster.hexlet_basics.id 49 | } 50 | 51 | resource "digitalocean_database_db" "postgres" { 52 | name = "postgres" 53 | cluster_id = digitalocean_database_cluster.hexlet_basics.id 54 | } 55 | 56 | resource "digitalocean_database_firewall" "k8s" { 57 | depends_on = [digitalocean_kubernetes_cluster.hexlet_basics_3] 58 | 59 | cluster_id = digitalocean_database_cluster.hexlet_basics.id 60 | rule { 61 | type = "k8s" 62 | value = digitalocean_kubernetes_cluster.hexlet_basics_3.id 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_name" { 2 | default = "hexlet-basics" 3 | } 4 | 5 | variable "gke_cluster_name" { 6 | default = "hexlet-basics-5" 7 | } 8 | 9 | variable "gke_cluster_name_2" { 10 | default = "hexlet-basics-6" 11 | } 12 | 13 | variable "billing_account" { 14 | default = "01730C-0A5BE7-686C51" 15 | } 16 | 17 | variable "org_id" { 18 | default = "431020544079" 19 | } 20 | 21 | variable "region" { 22 | default = "europe-west3" 23 | } 24 | 25 | variable "zone" { 26 | default = "europe-west3-a" 27 | } 28 | 29 | variable "additional_zones" { 30 | default = ["europe-west3-a", "europe-west3-c"] 31 | } 32 | 33 | variable "db_name" {} 34 | variable "db_username" {} 35 | variable "db_password" {} 36 | variable "db_hostname" {} 37 | variable "db_port" {} 38 | variable "github_oauth_token" {} 39 | variable "cloudflare_api_key" {} 40 | variable "cloudflare_email" {} 41 | variable "github_client_id" {} 42 | variable "github_client_secret" {} 43 | variable "facebook_client_id" {} 44 | variable "facebook_client_secret" {} 45 | variable "secret_key_base" {} 46 | variable "slack_codebuild_webhook" {} 47 | variable "rollbar_access_token" {} 48 | variable "sparkpost_smtp_username" {} 49 | variable "sparkpost_smtp_password" {} 50 | variable "guardian_secret_key" {} 51 | variable "digitalocean_token" {} 52 | variable "app_host" {} 53 | variable "app_ru_host" {} 54 | variable "app_scheme" {} 55 | 56 | variable "repositories" { 57 | type = map 58 | 59 | default = { 60 | "exercises_php" = "github_hexlet-basics_exercises-php" 61 | "exercises_java" = "github_hexlet-basics_exercises-java" 62 | "exercises_javascript" = "github_hexlet-basics_exercises-javascript" 63 | "exercises_python" = "github_hexlet-basics_exercises-python" 64 | "hexlet_basics" = "github_hexlet-basics_hexlet_5Fbasics" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /terraform/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | cloudflare = { 4 | source = "terraform-providers/cloudflare" 5 | } 6 | digitalocean = { 7 | source = "terraform-providers/digitalocean" 8 | } 9 | google = { 10 | source = "hashicorp/google" 11 | } 12 | kubernetes = { 13 | source = "hashicorp/kubernetes" 14 | } 15 | } 16 | required_version = ">= 0.13" 17 | } 18 | --------------------------------------------------------------------------------