├── .coveragerc ├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .env.template ├── .github └── workflows │ ├── cd_app_update.yml │ ├── ci_backend_lint_and_unit_tests.yml │ ├── docs_publish_vuepress.yml │ ├── ecr_backend.yml │ ├── ecr_backend_nginx.yml │ ├── ecr_frontend.yml │ ├── iac_cdk_actions.yml │ ├── iac_pulumi_actions.yml │ ├── iac_terraform_actions.yml │ ├── k6_load_test.yml │ └── release-please.yml ├── .gitignore ├── .vscode ├── .gitignore └── extension.json ├── CHANGELOG.md ├── Makefile ├── README.md ├── backend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── __init__.py ├── apps │ ├── accounts │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── authentication.py │ │ ├── forms.py │ │ ├── managers.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_customuser_profile_setup_complete_and_more.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── schema.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── templates │ │ │ ├── emails │ │ │ │ ├── account_activation_email.html │ │ │ │ └── email_admins.html │ │ │ ├── profile.html │ │ │ ├── register.html │ │ │ └── registration │ │ │ │ └── login.html │ │ ├── tests │ │ │ ├── test_accounts.py │ │ │ ├── test_accounts_gql.py │ │ │ ├── test_auth.py │ │ │ └── test_drf_fbv.py │ │ ├── tokens.py │ │ ├── urls │ │ │ ├── __init__.py │ │ │ ├── auth │ │ │ │ ├── api_urls.py │ │ │ │ └── mtv_urls.py │ │ │ ├── auth_urls.py │ │ │ └── drf_fbv_urls.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── drf_auth_views.py │ │ │ ├── drf_fbv_views.py │ │ │ └── mtv_auth_views.py │ ├── blog │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── factory.py │ │ ├── forms.py │ │ ├── management │ │ │ └── commands │ │ │ │ └── generate_posts.py │ │ ├── managers.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20210703_2332.py │ │ │ ├── 0003_auto_20210918_1404.py │ │ │ ├── 0004_alter_post_image.py │ │ │ ├── 0005_alter_post_created_by.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── permissions.py │ │ ├── schema.py │ │ ├── serializers.py │ │ ├── templates │ │ │ ├── blog │ │ │ │ ├── post_confirm_delete.html │ │ │ │ ├── post_create.html │ │ │ │ ├── post_detail.html │ │ │ │ ├── post_edit.html │ │ │ │ └── post_list.html │ │ │ ├── edit_post.html │ │ │ ├── new_post.html │ │ │ ├── post.html │ │ │ ├── post_likes.html │ │ │ ├── post_pagination.html │ │ │ └── posts.html │ │ ├── tests │ │ │ ├── test_blog.py │ │ │ ├── test_blog_cbv.py │ │ │ ├── test_blog_drf_fbvs.py │ │ │ └── test_blog_gql.py │ │ ├── urls │ │ │ ├── cbv_urls.py │ │ │ ├── drf_cbv_urls.py │ │ │ ├── drf_fbv_urls.py │ │ │ └── fbv_urls.py │ │ └── views │ │ │ ├── class_based_views.py │ │ │ ├── drf_cbv_views.py │ │ │ ├── drf_fbv_views.py │ │ │ └── views.py │ ├── chat │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ └── core │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── constants.py │ │ ├── db.py │ │ ├── management │ │ └── commands │ │ │ ├── create_database.py │ │ │ ├── pre_update.py │ │ │ ├── start_celery_beat.py │ │ │ └── start_celery_worker.py │ │ ├── middleware.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ │ ├── models.py │ │ ├── tasks.py │ │ ├── templates │ │ ├── index.html │ │ └── nav.html │ │ ├── tests.py │ │ ├── urls │ │ ├── api_urls.py │ │ └── mtv_urls.py │ │ ├── utils.py │ │ └── views.py ├── assets │ ├── postgres.png │ └── redis.png ├── backend │ ├── __init__.py │ ├── asgi.py │ ├── celery_app.py │ ├── context_processors.py │ ├── schema.py │ ├── settings │ │ ├── __init__.py │ │ ├── aws.py │ │ ├── base.py │ │ ├── development.py │ │ ├── ec2.py │ │ ├── production.py │ │ └── swarm_ec2.py │ ├── storage_backends.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── media │ └── .gitignore ├── notebooks │ ├── ORM_example.ipynb │ ├── README.md │ ├── celery_example.ipynb │ ├── email_debug.ipynb │ ├── m2m_examples.ipynb │ ├── media │ │ └── images │ │ │ └── postgres.png │ └── post_serializer_example.ipynb ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── requirements_dev.txt ├── scripts │ ├── aws │ │ ├── ecs_exec.sh │ │ ├── ssm_port_forward.sh │ │ └── ssm_start_session.sh │ └── start_prod.sh ├── static │ ├── favicon.ico │ ├── img │ │ ├── django.jpeg │ │ ├── drf_square.png │ │ ├── quasar.png │ │ └── vue3.png │ ├── main.css │ └── openapi │ │ └── schema.yml ├── staticfiles │ └── .gitkeep └── templates │ ├── 403.html │ ├── 404.html │ ├── base.html │ └── swagger-ui.html ├── cli └── main.py ├── cypress ├── cypress.json ├── cypress │ ├── fixtures │ │ ├── example.json │ │ └── redis.png │ ├── integration │ │ ├── createPost.spec.js │ │ ├── graphql.spec.js │ │ ├── quasar │ │ │ ├── createPost.spec.js │ │ │ ├── login.spec.js │ │ │ └── register.spec.js │ │ └── registration.spec.js │ ├── plugins │ │ └── index.js │ ├── screenshots │ │ └── .gitignore │ ├── support │ │ ├── commands.js │ │ ├── index.js │ │ └── utils │ │ │ └── index.js │ └── videos │ │ └── .gitignore └── package.json ├── docker-compose.devcontainer.yml ├── docker-compose.ec2.yml ├── docker-compose.yml ├── iac ├── README.md ├── cdk │ ├── .eslintrc.json │ ├── .gitattributes │ ├── .github │ │ ├── pull_request_template.md │ │ └── workflows │ │ │ ├── build.yml │ │ │ ├── pull-request-lint.yml │ │ │ └── upgrade.yml │ ├── .gitignore │ ├── .mergify.yml │ ├── .npmignore │ ├── .projen │ │ ├── deps.json │ │ ├── files.json │ │ └── tasks.json │ ├── .projenrc.js │ ├── LICENSE │ ├── README.md │ ├── cdk.json │ ├── package.json │ ├── src │ │ ├── ecs │ │ │ ├── configs │ │ │ │ ├── app │ │ │ │ │ └── alpha.json │ │ │ │ └── base │ │ │ │ │ └── dev.json │ │ │ └── index.ts │ │ ├── index.ts │ │ └── main.ts │ ├── test │ │ └── .gitignore │ ├── tsconfig.dev.json │ ├── tsconfig.json │ └── yarn.lock ├── pulumi │ └── live │ │ └── ecs │ │ ├── app │ │ ├── .gitignore │ │ ├── Pulumi.alpha.yaml │ │ ├── Pulumi.gamma.yaml │ │ ├── Pulumi.yaml │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── yarn.lock │ │ └── base │ │ ├── .gitignore │ │ ├── Pulumi.dev.yaml │ │ ├── Pulumi.yaml │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── yarn.lock └── terraform │ ├── .gitignore │ ├── bootstrap │ ├── .gitignore │ ├── README.md │ ├── bootstrap.tfvars.template │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf │ └── live │ └── ecs │ ├── app │ ├── .gitignore │ ├── README.md │ ├── backend.config.example │ ├── envs │ │ ├── .gitignore │ │ ├── alpha.tfvars │ │ ├── beta.tfvars │ │ ├── brian.tfvars │ │ ├── default.tf │ │ ├── sample.tfvars.template │ │ └── test.tfvars │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf │ └── base │ ├── .gitignore │ ├── envs │ ├── .gitignore │ ├── dev.tfvars │ └── dev.tfvars.example │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf ├── k6 └── script.js ├── nginx ├── backend │ └── prod │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── nginx.conf │ │ ├── script.sh │ │ ├── server.crt │ │ ├── server.csr │ │ ├── server.key │ │ └── v3.ext ├── dev │ ├── Dockerfile │ ├── dev.conf │ └── local.conf ├── devcontainer │ ├── Dockerfile │ └── nginx.conf ├── ec2 │ ├── docker-compose.ec2.init.yml │ ├── entrypoint.sh │ ├── index.html │ ├── init.conf │ ├── nginx.conf │ └── templates │ │ ├── app.conf.template │ │ └── init.conf.template ├── ecs │ ├── Dockerfile │ └── ecs.conf └── prod │ ├── Dockerfile │ └── prod.conf ├── nuxt-app ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app.vue ├── assets │ └── css │ │ └── tailwind.css ├── components.json ├── components │ ├── DarkMode.vue │ └── ui │ │ ├── button │ │ ├── Button.vue │ │ └── index.ts │ │ ├── card │ │ ├── Card.vue │ │ ├── CardContent.vue │ │ ├── CardDescription.vue │ │ ├── CardFooter.vue │ │ ├── CardHeader.vue │ │ ├── CardTitle.vue │ │ └── index.ts │ │ ├── dropdown-menu │ │ ├── DropdownMenu.vue │ │ ├── DropdownMenuCheckboxItem.vue │ │ ├── DropdownMenuContent.vue │ │ ├── DropdownMenuGroup.vue │ │ ├── DropdownMenuItem.vue │ │ ├── DropdownMenuLabel.vue │ │ ├── DropdownMenuRadioGroup.vue │ │ ├── DropdownMenuRadioItem.vue │ │ ├── DropdownMenuSeparator.vue │ │ ├── DropdownMenuShortcut.vue │ │ ├── DropdownMenuSub.vue │ │ ├── DropdownMenuSubContent.vue │ │ ├── DropdownMenuSubTrigger.vue │ │ ├── DropdownMenuTrigger.vue │ │ └── index.ts │ │ ├── form │ │ ├── FormControl.vue │ │ ├── FormDescription.vue │ │ ├── FormItem.vue │ │ ├── FormLabel.vue │ │ ├── FormMessage.vue │ │ ├── index.ts │ │ ├── injectionKeys.ts │ │ └── useFormField.ts │ │ ├── input │ │ ├── Input.vue │ │ └── index.ts │ │ ├── label │ │ ├── Label.vue │ │ └── index.ts │ │ ├── navigation-menu │ │ ├── NavigationMenu.vue │ │ ├── NavigationMenuContent.vue │ │ ├── NavigationMenuIndicator.vue │ │ ├── NavigationMenuItem.vue │ │ ├── NavigationMenuLink.vue │ │ ├── NavigationMenuList.vue │ │ ├── NavigationMenuTrigger.vue │ │ ├── NavigationMenuViewport.vue │ │ └── index.ts │ │ ├── popover │ │ ├── Popover.vue │ │ ├── PopoverContent.vue │ │ ├── PopoverTrigger.vue │ │ └── index.ts │ │ ├── table │ │ ├── Table.vue │ │ ├── TableBody.vue │ │ ├── TableCaption.vue │ │ ├── TableCell.vue │ │ ├── TableEmpty.vue │ │ ├── TableFooter.vue │ │ ├── TableHead.vue │ │ ├── TableHeader.vue │ │ ├── TableRow.vue │ │ └── index.ts │ │ ├── tabs │ │ ├── Tabs.vue │ │ ├── TabsContent.vue │ │ ├── TabsList.vue │ │ ├── TabsTrigger.vue │ │ └── index.ts │ │ ├── textarea │ │ ├── Textarea.vue │ │ └── index.ts │ │ └── toast │ │ ├── Toast.vue │ │ ├── ToastAction.vue │ │ ├── ToastClose.vue │ │ ├── ToastDescription.vue │ │ ├── ToastProvider.vue │ │ ├── ToastTitle.vue │ │ ├── ToastViewport.vue │ │ ├── Toaster.vue │ │ ├── index.ts │ │ └── use-toast.ts ├── composables │ ├── useAuth.ts │ ├── useChat.ts │ └── useProfile.ts ├── layouts │ ├── basic.vue │ └── default.vue ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages │ ├── Chat │ │ ├── [id].vue │ │ └── index.vue │ ├── Login │ │ └── index.vue │ ├── Profile │ │ └── index.vue │ ├── Signup │ │ └── index.vue │ ├── Verify │ │ └── [uid] │ │ │ └── [token].vue │ ├── Version │ │ └── index.vue │ ├── index.vue │ └── reset-password │ │ └── index.vue ├── plugins │ └── apiBase.ts ├── public │ ├── favicon.ico │ └── robots.txt ├── server │ └── tsconfig.json ├── stores │ ├── authStore.ts │ ├── chatStore.ts │ └── profileStore.ts ├── tailwind.config.js └── tsconfig.json ├── packages ├── app-update │ └── action.yml ├── ecr-run-task │ └── action.yml ├── run-task │ └── action.yml └── test │ └── action.yml ├── quasar-app ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── .vscode │ ├── extensions.json │ └── settings.json ├── Dockerfile ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── _redirects │ ├── bigsur.png │ ├── black.png │ ├── bootstrap.png │ ├── cdk.png │ ├── celery.png │ ├── circleci.jpeg │ ├── cloud-init.png │ ├── conventionalcommits.png │ ├── copilot.png │ ├── cypress.png │ ├── diagrams.png │ ├── django.jpeg │ ├── do.png │ ├── docker.png │ ├── drf_square.png │ ├── ecs.png │ ├── elephant.png │ ├── favicon.ico │ ├── focalfossa.png │ ├── ghactions.png │ ├── gitlab.png │ ├── gnu.png │ ├── gql.png │ ├── graphene.png │ ├── graphql.png │ ├── icons │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-167x167.png │ │ ├── apple-icon-180x180.png │ │ ├── favicon-128x128.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-192x192.png │ │ ├── icon-256x256.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── ms-icon-144x144.png │ │ └── safari-pinned-tab.svg │ ├── jazzband.png │ ├── jsii.png │ ├── jupyter.png │ ├── jwt.svg │ ├── k6.png │ ├── k8s.png │ ├── m1.jpeg │ ├── mailhog.png │ ├── minikube.png │ ├── netlify.png │ ├── nginx.png │ ├── openapi.png │ ├── pgadmin.svg │ ├── poetry.png │ ├── portainer.svg │ ├── postgres-alt.svg │ ├── projen.png │ ├── projen.svg │ ├── pulumi.png │ ├── pytest.png │ ├── python.png │ ├── quasar.png │ ├── raspberrypi.png │ ├── redis.png │ ├── sentry.png │ ├── swarm.png │ ├── systemd.png │ ├── terraform.webp │ ├── traefik.png │ ├── ts.png │ ├── ublog.png │ ├── vscode.png │ ├── vue3.png │ ├── vuepress.png │ ├── windows10.png │ └── wsl.png ├── quasar.conf.js ├── src-pwa │ ├── custom-service-worker.js │ ├── pwa-flag.d.ts │ └── register-service-worker.js ├── src-ssr │ └── ssr-flag.d.ts ├── src │ ├── App.vue │ ├── assets │ │ └── quasar-logo-vertical.svg │ ├── boot │ │ ├── .gitkeep │ │ └── axios.ts │ ├── classes │ │ └── index.ts │ ├── components │ │ ├── EssentialLink.vue │ │ ├── LoginForm │ │ │ └── index.vue │ │ ├── NavLink │ │ │ └── index.vue │ │ ├── Post │ │ │ └── index.vue │ │ ├── PostForm │ │ │ └── index.vue │ │ ├── RegistrationForm │ │ │ └── index.vue │ │ ├── TechCard │ │ │ └── index.vue │ │ └── models.ts │ ├── css │ │ ├── app.scss │ │ └── quasar.variables.scss │ ├── data │ │ └── technologies.ts │ ├── env.d.ts │ ├── index.template.html │ ├── layouts │ │ └── MainLayout.vue │ ├── modules │ │ ├── activate.ts │ │ ├── auth.ts │ │ ├── chat.ts │ │ ├── createPost.ts │ │ ├── darkMode.ts │ │ ├── debug.ts │ │ ├── pagination.ts │ │ ├── posts.ts │ │ ├── profile.ts │ │ └── registration.ts │ ├── pages │ │ ├── About │ │ │ └── index.vue │ │ ├── ActivateAccount │ │ │ └── index.vue │ │ ├── Chat │ │ │ └── index.vue │ │ ├── Chats │ │ │ └── index.vue │ │ ├── CreatePost │ │ │ └── index.vue │ │ ├── Debug │ │ │ └── index.vue │ │ ├── Error404.vue │ │ ├── Index.vue │ │ ├── Login │ │ │ └── index.vue │ │ ├── PostDetail │ │ │ └── index.vue │ │ ├── Posts │ │ │ └── index.vue │ │ ├── Profile │ │ │ └── index.vue │ │ ├── Register │ │ │ └── index.vue │ │ └── Technologies │ │ │ └── index.vue │ ├── router │ │ ├── index.ts │ │ └── routes.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── index.ts │ │ ├── module-example │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ └── state.ts │ │ └── store-flag.d.ts │ ├── types │ │ └── index.ts │ └── utils │ │ └── index.ts ├── start_dev.sh ├── tsconfig.json └── yarn.lock └── vuepress-docs ├── .gitignore ├── docs ├── .vuepress │ ├── .gitignore │ ├── config.ts │ ├── configs │ │ ├── index.ts │ │ └── navbar │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh.ts │ └── public │ │ ├── diagrams │ │ ├── django-cdk.png │ │ ├── django-ecs.png │ │ ├── docker-compose.png │ │ ├── docker-swarm-ec2.png │ │ └── jwt-authentication.png │ │ ├── images │ │ ├── docker-swarm-ec2-hero.png │ │ ├── screenshots │ │ │ ├── gh-pages-settings.png │ │ │ └── ublog-screenshot.png │ │ └── ublog.png │ │ └── manifest.webmanifest ├── README.md ├── deploy │ ├── app.md │ ├── aws.md │ ├── aws │ │ ├── cdk.md │ │ ├── pulumi.md │ │ └── terraform.md │ └── github-actions.md ├── intro.md ├── topics │ ├── README.md │ ├── django.md │ ├── docker-compose.md │ ├── nuxt.md │ └── vuepress.md └── zh │ ├── README.md │ └── intro.md ├── package.json └── yarn.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *__init__* 4 | *asgi.py 5 | *wsgi.py 6 | *manage.py -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerComposeFile": "../docker-compose.devcontainer.yaml", 3 | "service": "backend", 4 | "shutdownAction": "stopCompose", 5 | "features": { 6 | "ghcr.io/devcontainers/features/common-utils:2": { 7 | "version": "latest" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | media 2 | .local-env 3 | .github 4 | .pytest_cache 5 | .vscode 6 | cdk 7 | cdk.out 8 | cypress 9 | docs 10 | k8s 11 | notes 12 | pulumi 13 | vuepress-docs 14 | quasar-app/node_modules 15 | quasar-app/dist 16 | raspberrypi 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.py] 14 | indent_size = 4 15 | 16 | # 4 space indentation 17 | [*.ts] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk-proj-ABCD 2 | NVIDIA_API_KEY=nvapi-ABC-123 3 | -------------------------------------------------------------------------------- /.github/workflows/docs_publish_vuepress.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action for deploying documentation site to GitHub Pages 2 | name: '[DOCS] github pages' 3 | run-name: '[DOCS] github pages' 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: "20" 18 | 19 | - name: Cache dependencies 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.npm 23 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 24 | restore-keys: | 25 | ${{ runner.os }}-node- 26 | 27 | - run: cd vuepress-docs && yarn 28 | - run: cd vuepress-docs && yarn docs:build 29 | 30 | - name: deploy 31 | uses: peaceiris/actions-gh-pages@v3 32 | with: 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | publish_dir: ./vuepress-docs/docs/.vuepress/dist 35 | publish_branch: master 36 | -------------------------------------------------------------------------------- /.github/workflows/k6_load_test.yml: -------------------------------------------------------------------------------- 1 | # github action to create or update an ad hoc environment 2 | name: '[k6] load test' 3 | run-name: '[k6] load tests for ${{ inputs.url }}' 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | URL: 9 | description: 'URL to use for load test' 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | k6_load_test: 15 | name: k6 Load Test 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Run local k6 test 23 | uses: grafana/k6-action@v0.3.1 24 | env: 25 | BASE_URL: ${{ inputs.URL }} 26 | with: 27 | filename: k6/script.js 28 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | with: 12 | token: ${{ secrets.GH_PAT }} 13 | release-type: simple 14 | package-name: django-step-by-step 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .local-env/ 2 | .coverage 3 | htmlcov 4 | node_modules 5 | notes 6 | .env 7 | .venv 8 | cdk.out 9 | .raspberrypi.env 10 | cdk.context.json 11 | backend/celerybeat-schedule-* 12 | 13 | # Redis .rdb files 14 | *.rdb 15 | backend/.bash_history 16 | .terraform 17 | -------------------------------------------------------------------------------- /.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | dryrun.log 3 | configurationCache.log 4 | targets.log 5 | extension.json 6 | -------------------------------------------------------------------------------- /.vscode/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "octref.vetur" 6 | ], 7 | "unwantedRecommendations": [ 8 | "hookyqr.beautify", 9 | "dbaeumer.jshint", 10 | "ms-vscode.vscode-typescript-tslint-plugin" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | media -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13.3-slim AS base 2 | ENV PYTHONUNBUFFERED=1 \ 3 | PYTHONDONTWRITEBYTECODE=1 \ 4 | PIP_DISABLE_PIP_VERSION_CHECK=1 \ 5 | POETRY_VERSION='2.0.1' 6 | 7 | ARG SOURCE_TAG 8 | ENV SOURCE_TAG=$SOURCE_TAG 9 | 10 | RUN apt-get update && \ 11 | apt-get install -y --no-install-recommends \ 12 | build-essential \ 13 | curl && \ 14 | rm -rf /var/lib/apt/lists/* 15 | 16 | RUN useradd --create-home --home-dir /code --shell /bin/bash app 17 | 18 | WORKDIR /code 19 | RUN pip install "poetry==$POETRY_VERSION" 20 | COPY poetry.lock pyproject.toml /code/ 21 | 22 | FROM base AS prod 23 | RUN curl https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -o /usr/local/share/global-bundle.pem 24 | RUN POETRY_VIRTUALENVS_CREATE=false poetry install --only main 25 | COPY . /code 26 | RUN chown -R app:app /code 27 | USER app 28 | 29 | FROM base AS local 30 | RUN apt-get update && apt-get install procps -y # for pkill 31 | RUN POETRY_VIRTUALENVS_CREATE=false poetry install --with test,dev 32 | USER app 33 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/__init__.py -------------------------------------------------------------------------------- /backend/apps/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/accounts/__init__.py -------------------------------------------------------------------------------- /backend/apps/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = "apps.accounts" 6 | -------------------------------------------------------------------------------- /backend/apps/accounts/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework.authentication import BaseAuthentication 2 | from rest_framework.exceptions import AuthenticationFailed 3 | from django.conf import settings 4 | from rest_framework_simplejwt.tokens import AccessToken 5 | from django.contrib.auth import get_user_model 6 | 7 | User = get_user_model() 8 | 9 | 10 | class HttpOnlyJWTAuthentication(BaseAuthentication): 11 | def authenticate(self, request): 12 | access_token = request.COOKIES.get("access") 13 | if not access_token: 14 | return None # No token, return None to continue with other auth methods 15 | 16 | try: 17 | decoded_token = AccessToken(access_token) 18 | user = User.objects.get(id=decoded_token["user_id"]) 19 | except (User.DoesNotExist, Exception) as e: 20 | raise AuthenticationFailed("Invalid or expired token") 21 | 22 | return (user, None) 23 | -------------------------------------------------------------------------------- /backend/apps/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /backend/apps/accounts/templates/emails/account_activation_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | 3 | 4 | 5 | 11 | 12 | 13 |

14 | Your email address ({{ user.email }}) was just used to register an account 15 | on {{ domain }}. 16 |

17 | 18 |

Please click on the link below to confirm your registration:

19 | 20 | 25 | Confirm Email 26 | 27 | 28 | 29 | 30 | {% endautoescape %} 31 | -------------------------------------------------------------------------------- /backend/apps/accounts/templates/emails/email_admins.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | 3 | 4 | 5 | 6 | 7 | 8 |

{{ message }}

9 | 10 | 11 | 12 | 13 | {% endautoescape %} 14 | -------------------------------------------------------------------------------- /backend/apps/accounts/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block title %}Profile{% endblock %} 5 | 6 | 7 | 8 | {% block content %} 9 |

Your Profile

10 | 11 |

Your Email

12 | 13 |

{{ request.user.email }}

14 | 15 |
16 |

Your Posts ({{ posts.count }})

17 | View all of your posts 18 |
19 | 20 |
21 |

Your likes ({{ liked_posts.count }})

22 | View all of your likes 23 |
24 | 25 |
26 |

Likes on your posts ({{ post_likes.count }})

27 | View all likes on your posts 30 |
31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /backend/apps/accounts/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block title %}Register{% endblock %} 5 | 6 | 7 | 8 | {% block content %} 9 |
10 |
11 |

Register

12 | {% csrf_token %} 13 | 14 | {% for field in form %} 15 |
16 | 17 | {% if field.errors %} 18 |
{{ field.errors.as_text }}
19 | {% endif %} 20 | 21 | {{ field }} 22 | 23 | 24 | {% if field.help_text %} 25 |

{{ field.help_text|safe }}

26 | {% endif %} 27 |
28 | {% endfor %} 29 | 30 | {% if form.non_field_errors %} 31 |
{{form.non_field_errors.as_text}}
32 | {% endif %} 33 | 36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /backend/apps/accounts/tests/test_drf_fbv.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rest_framework.test import APIClient 4 | from rest_framework import status 5 | 6 | from django.contrib.auth import get_user_model 7 | from django.core import mail 8 | from django.shortcuts import reverse 9 | from django.test import override_settings 10 | 11 | # from apps.blog.factory import PostFactory 12 | # from apps.blog.models import Post 13 | 14 | User = get_user_model() 15 | 16 | 17 | @pytest.mark.django_db(transaction=True) 18 | @override_settings(CELERY_TASK_ALWAYS_EAGER=True) 19 | def test_register_user(client): 20 | """ 21 | Test register user. An email should be sent to the user upon registration. 22 | """ 23 | url = reverse("drf-fbv-register") 24 | data = {"email": "user@email.com", "password": "foobar123!"} 25 | 26 | client = APIClient() 27 | 28 | response = client.post(url, data, format="json") 29 | 30 | assert response.status_code == status.HTTP_201_CREATED 31 | assert User.objects.count() == 1 32 | assert User.objects.first().is_active is False 33 | assert len(mail.outbox) == 1 34 | -------------------------------------------------------------------------------- /backend/apps/accounts/tokens.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 2 | 3 | 4 | class AccountActivationTokenGenerator(PasswordResetTokenGenerator): 5 | def _make_hash_value(self, user, timestamp): 6 | return str(user.pk) + str(timestamp) + str(user.is_active) 7 | 8 | 9 | account_activation_token = AccountActivationTokenGenerator() 10 | -------------------------------------------------------------------------------- /backend/apps/accounts/urls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/accounts/urls/__init__.py -------------------------------------------------------------------------------- /backend/apps/accounts/urls/auth/api_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.accounts.views.drf_auth_views import ( 4 | CookieTokenRefreshView, 5 | CookieTokenObtainPairView, 6 | logout, 7 | ) 8 | 9 | urlpatterns = [ 10 | path( 11 | "auth/jwt/token/", 12 | CookieTokenObtainPairView.as_view(), 13 | name="jwt_token_obtain_pair", 14 | ), 15 | path( 16 | "auth/jwt/token/refresh/", 17 | CookieTokenRefreshView.as_view(), 18 | name="jwt_token_refresh", 19 | ), 20 | path( 21 | "auth/jwt/token/logout/", 22 | logout, 23 | name="jwt_token_logout", 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/apps/accounts/urls/auth/mtv_urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import path 3 | 4 | from apps.accounts.views import mtv_auth_views 5 | from apps.accounts.forms import UserLoginForm 6 | 7 | urlpatterns = [ 8 | path( 9 | "login", 10 | auth_views.LoginView.as_view(authentication_form=UserLoginForm), 11 | name="login", 12 | ), 13 | path("logout", mtv_auth_views.CustomLogoutView.as_view(), name="logout"), 14 | path("register", mtv_auth_views.register, name="register"), 15 | path( 16 | "activate///", 17 | mtv_auth_views.ActivateAccount.as_view(), 18 | name="activate", 19 | ), 20 | path("profile", mtv_auth_views.profile_view, name="profile"), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/apps/accounts/urls/drf_fbv_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.accounts.views.drf_fbv_views import ( 4 | get_user, 5 | get_users, 6 | get_profile, 7 | register, 8 | verify_email, 9 | update_user_details, 10 | ) 11 | 12 | urlpatterns = [ 13 | path( 14 | "activate///", 15 | verify_email, 16 | name="drf-fbv-activate", 17 | ), 18 | path("register/", register, name="drf-fbv-register"), 19 | path("users//", get_user, name="drf-fbv-get-user"), 20 | path("users/", get_users, name="drf-fbv-get-users"), 21 | # user profile 22 | path("profile/", get_profile, name="drf-fbv-get-profile"), 23 | path("update-user/", update_user_details, name="update-user-details"), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/apps/accounts/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/accounts/views/__init__.py -------------------------------------------------------------------------------- /backend/apps/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/blog/__init__.py -------------------------------------------------------------------------------- /backend/apps/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Post, PostLike 5 | 6 | 7 | class PostAdmin(admin.ModelAdmin): 8 | class Meta: 9 | model = Post 10 | 11 | search_fields = ("body",) 12 | 13 | list_display = ( 14 | "body", 15 | "created_on", 16 | "created_by", 17 | "image", 18 | ) 19 | 20 | 21 | class PostLikeAdmin(admin.ModelAdmin): 22 | class Meta: 23 | model = PostLike 24 | 25 | list_display = ("liked_by", "post", "liked_on") 26 | 27 | 28 | admin.site.register(Post, PostAdmin) 29 | admin.site.register(PostLike, PostLikeAdmin) 30 | -------------------------------------------------------------------------------- /backend/apps/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = "apps.blog" 6 | -------------------------------------------------------------------------------- /backend/apps/blog/factory.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from pathlib import Path 4 | 5 | from django.contrib.auth import get_user_model 6 | from factory.django import DjangoModelFactory 7 | 8 | from apps.blog.models import Post 9 | 10 | 11 | User = get_user_model() 12 | 13 | 14 | class UserFactory(DjangoModelFactory): 15 | class Meta: 16 | model = User 17 | 18 | # email = factory.Faker("email") 19 | 20 | 21 | # Another, different, factory for the same object 22 | class PostFactory(DjangoModelFactory): 23 | class Meta: 24 | model = Post 25 | 26 | body = factory.Faker("text") 27 | image = factory.django.FileField( 28 | from_path=Path(__file__).parent.parent.parent / "assets/postgres.png" 29 | ) 30 | -------------------------------------------------------------------------------- /backend/apps/blog/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ( 2 | ModelForm, 3 | CharField, 4 | Textarea, 5 | ImageField, 6 | FileInput, 7 | ) 8 | 9 | from apps.blog.models import Post 10 | 11 | 12 | class PostForm(ModelForm): 13 | 14 | body = CharField( 15 | widget=Textarea( 16 | attrs={ 17 | "class": "form-control", 18 | "placeholder": "Write your post here", 19 | "id": "post-body", 20 | "rows": 5, 21 | "autofocus": True, 22 | "type": "text", 23 | } 24 | ) 25 | ) 26 | 27 | image = ImageField( 28 | required=False, 29 | widget=FileInput( 30 | attrs={ 31 | # add the bootstrap class in the form 32 | "class": "form-control", 33 | "data-cy": "file-input", 34 | } 35 | ), 36 | ) 37 | 38 | class Meta: 39 | model = Post 40 | fields = ["body", "image"] 41 | -------------------------------------------------------------------------------- /backend/apps/blog/management/commands/generate_posts.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from apps.blog.factory import PostFactory 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Generate some posts" 8 | 9 | def handle(self, *args, **options): 10 | 11 | print("generating posts") 12 | for _ in range(20): 13 | PostFactory() 14 | 15 | print("finished generating posts") 16 | -------------------------------------------------------------------------------- /backend/apps/blog/migrations/0002_auto_20210703_2332.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.13 on 2021-07-04 03:32 2 | 3 | import backend.storage_backends 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("blog", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="post", 16 | name="image", 17 | field=models.ImageField( 18 | blank=True, 19 | storage=backend.storage_backends.PrivateMediaStorage(), 20 | upload_to="images", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/apps/blog/migrations/0003_auto_20210918_1404.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.13 on 2021-09-18 18:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("blog", "0002_auto_20210703_2332"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="post", 15 | name="image", 16 | field=models.ImageField(blank=True, upload_to="images"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/apps/blog/migrations/0004_alter_post_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-09-04 22:56 2 | 3 | import backend.storage_backends 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("blog", "0003_auto_20210918_1404"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="post", 16 | name="image", 17 | field=models.ImageField( 18 | blank=True, 19 | storage=backend.storage_backends.PrivateMediaStorage(), 20 | upload_to="images", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/apps/blog/migrations/0005_alter_post_created_by.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2025-01-14 04:14 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("blog", "0004_alter_post_image"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="post", 18 | name="created_by", 19 | field=models.ForeignKey( 20 | blank=True, 21 | default=None, 22 | editable=False, 23 | null=True, 24 | on_delete=django.db.models.deletion.SET_NULL, 25 | related_name="%(app_label)s_%(class)s_created_by", 26 | to=settings.AUTH_USER_MODEL, 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /backend/apps/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/blog/migrations/__init__.py -------------------------------------------------------------------------------- /backend/apps/blog/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class PostPermissions(permissions.BasePermission): 5 | def has_object_permission(self, request, view, obj): 6 | 7 | is_object_owner = request.user == obj.created_by 8 | if view.action == "retrieve": 9 | return True 10 | 11 | if view.action == "destroy": 12 | return is_object_owner 13 | 14 | if view.action in ["update", "partial_update"]: 15 | return is_object_owner 16 | 17 | if view.action == "like": 18 | print("like action...") 19 | return request.user.is_authenticated 20 | 21 | def has_permission(self, request, view): 22 | 23 | # anyone can list posts 24 | if view.action == "list": 25 | return True 26 | 27 | # anyone can create a post 28 | if view.action == "create": 29 | return True 30 | 31 | return True 32 | -------------------------------------------------------------------------------- /backend/apps/blog/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.blog.models import Post 4 | 5 | from apps.accounts.serializers import CustomUserSerializer 6 | 7 | 8 | class PostSerializer(serializers.ModelSerializer): 9 | liked = serializers.BooleanField(read_only=True) 10 | like_count = serializers.IntegerField(read_only=True) 11 | created_by = CustomUserSerializer(read_only=True) 12 | 13 | class Meta: 14 | model = Post 15 | fields = ( 16 | "id", 17 | "body", 18 | "created_by", 19 | "modified_on", 20 | "liked", 21 | "like_count", 22 | "image", 23 | ) 24 | -------------------------------------------------------------------------------- /backend/apps/blog/templates/blog/post_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}Delete post{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}Delete post{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 | 15 |
16 | {% csrf_token %} 17 |

Are you sure you want to delete this post?

18 | 24 | 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /backend/apps/blog/templates/blog/post_create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}New post{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}New post{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 | 15 |
16 |
17 | {% csrf_token %} 18 |

Create a new post - Class-based view

19 | {% for field in form %} 20 |
21 | 22 | {{ field.errors }} 23 | 24 | {{ field }} 25 | 26 | {% if field.help_text %} 27 |

{{ field.help_text|safe }}

28 | {% endif %} 29 |
30 | {% endfor %} 31 | 37 |
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /backend/apps/blog/templates/blog/post_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}Edit post{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}Edit post{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 |
15 |
16 | {% csrf_token %} 17 |

Edit this post

18 | 19 | {% for field in form %} 20 |
21 | 22 | {{ field.errors }} 23 | 24 | {{ field }} 25 | 26 | {% if field.help_text %} 27 |

{{ field.help_text|safe }}

28 | {% endif %} 29 |
30 | {% endfor %} 31 | 32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /backend/apps/blog/templates/edit_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}Edit post{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}Edit post{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 |
15 |
16 | {% csrf_token %} 17 |

Edit this post

18 | 19 | {% for field in form %} 20 |
21 | 22 | {{ field.errors }} 23 | 24 | {{ field }} 25 | 26 | {% if field.help_text %} 27 |

{{ field.help_text|safe }}

28 | {% endif %} 29 |
30 | {% endfor %} 31 | 32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /backend/apps/blog/templates/new_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}New post{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}New post{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 | 15 |
16 |
17 | {% csrf_token %} 18 |

Create a new post - Function-based view

19 | {% for field in form %} 20 |
21 | 22 | {{ field.errors }} 23 | 24 | {{ field }} 25 | 26 | {% if field.help_text %} 27 |

{{ field.help_text|safe }}

28 | {% endif %} 29 |
30 | {% endfor %} 31 | 37 |
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /backend/apps/blog/templates/post_pagination.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /backend/apps/blog/urls/cbv_urls.py: -------------------------------------------------------------------------------- 1 | # URLs for GCBVs 2 | from django.urls import path 3 | from apps.blog.views.class_based_views import ( 4 | PostListView, 5 | PostDetailView, 6 | PostCreateView, 7 | PostDeleteView, 8 | PostUpdateView, 9 | ) 10 | 11 | urlpatterns = [ 12 | path("posts/new", PostCreateView.as_view(), name="post-create-cbv"), 13 | path("posts//edit", PostUpdateView.as_view(), name="post-update-cbv"), 14 | path( 15 | "posts//delete", 16 | PostDeleteView.as_view(), 17 | name="post-delete-cbv", 18 | ), 19 | path("posts", PostListView.as_view(), name="post-list-cbv"), 20 | path("posts/", PostDetailView.as_view(), name="post-detail-cbv"), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/apps/blog/urls/drf_cbv_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework import routers 3 | 4 | from apps.blog.views.drf_cbv_views import PostViewSet 5 | 6 | 7 | router = routers.DefaultRouter() 8 | router.register(r"posts", PostViewSet, basename="drf-cbv-posts") 9 | 10 | # app_name = 'blog' 11 | 12 | urlpatterns = [ 13 | path("", include(router.urls)), 14 | ] 15 | -------------------------------------------------------------------------------- /backend/apps/blog/urls/drf_fbv_urls.py: -------------------------------------------------------------------------------- 1 | # URLs for DRF FBVs 2 | from django.urls import path 3 | 4 | from apps.blog.views.drf_fbv_views import ( 5 | get_post, 6 | list_posts, 7 | create_post, 8 | update_post, 9 | delete_post, 10 | like_post, 11 | ) 12 | 13 | urlpatterns = [ 14 | # create 15 | path("posts/new/", create_post, name="drf-fbv-create-post"), 16 | # update 17 | path("posts//update/", update_post, name="drf-fbv-update-post"), 18 | # read (detail) 19 | path("posts//", get_post, name="drf-fbv-get-post"), 20 | # read (list) 21 | path("posts/", list_posts, name="drf-fbv-list-posts"), 22 | # delete 23 | path("posts//delete/", delete_post, name="drf-fbv-delete-post"), 24 | # like post 25 | path("posts//like/", like_post, name="drf-fbv-like-post"), 26 | ] 27 | -------------------------------------------------------------------------------- /backend/apps/blog/urls/fbv_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.blog.views import views 4 | 5 | # url patterns for post CRUD (create, read, update, delete) 6 | urlpatterns = [ 7 | # api endpoint for liking a post 8 | path("api/posts//like", views.like_post, name="like-post"), 9 | # create 10 | path("posts/new", views.new_post, name="new-post"), 11 | # read (paginated list) 12 | path("posts", views.posts, name="list-posts"), 13 | # update 14 | path("posts//edit", views.edit_post, name="update-post"), 15 | # delete 16 | path("posts//delete", views.delete_post, name="delete-post"), 17 | # read (detail) 18 | path("posts/", views.post, name="post"), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/apps/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/chat/__init__.py -------------------------------------------------------------------------------- /backend/apps/chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.chat" 7 | -------------------------------------------------------------------------------- /backend/apps/chat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/chat/migrations/__init__.py -------------------------------------------------------------------------------- /backend/apps/chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path("get-sessions/", views.get_chat_sessions, name="get_chat_sessions"), 6 | path("sessions/", views.create_chat_session, name="create_chat_session"), 7 | path( 8 | "sessions//messages/", 9 | views.get_chat_messages, 10 | name="get_chat_messages", 11 | ), 12 | path( 13 | "sessions//messages/send/", 14 | views.send_message, 15 | name="send_message", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/apps/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/core/__init__.py -------------------------------------------------------------------------------- /backend/apps/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import RequestLog 4 | 5 | 6 | class RequestLogAdmin(admin.ModelAdmin): 7 | class Meta: 8 | model = RequestLog 9 | 10 | search_fields = ("full_path",) 11 | 12 | list_select_related = ("user",) 13 | 14 | list_filter = ("method", "response_code", "user") 15 | 16 | readonly_fields = ( 17 | "user", 18 | "date", 19 | "path", 20 | "full_path", 21 | "execution_time", 22 | "response_code", 23 | "method", 24 | "remote_address", 25 | ) 26 | list_display = ( 27 | "id", 28 | "user", 29 | "date", 30 | "path", 31 | "full_path", 32 | "execution_time", 33 | "response_code", 34 | "method", 35 | "remote_address", 36 | ) 37 | 38 | 39 | admin.site.register(RequestLog, RequestLogAdmin) 40 | -------------------------------------------------------------------------------- /backend/apps/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = "apps.core" 6 | -------------------------------------------------------------------------------- /backend/apps/core/constants.py: -------------------------------------------------------------------------------- 1 | BOOTSTRAP_ALERT_SUCCESS = "alert alert-success" 2 | BOOTSTRAP_ALERT_WARNING = "alert alert-warning" 3 | BOOTSTRAP_ALERT_DANGER = "alert alert-danger" 4 | -------------------------------------------------------------------------------- /backend/apps/core/management/commands/create_database.py: -------------------------------------------------------------------------------- 1 | """ 2 | This command is used to create databases for ad hoc environments 3 | """ 4 | 5 | import os 6 | 7 | from django.core.management.base import BaseCommand 8 | 9 | from apps.core.db import create_database 10 | 11 | 12 | class Command(BaseCommand): 13 | def handle(self, *args, **options): 14 | print(f"APP_NAME is {os.environ.get('APP_NAME', None)}") 15 | database_name = os.environ.get("APP_NAME", None) 16 | create_database(f"{database_name}-db") 17 | -------------------------------------------------------------------------------- /backend/apps/core/management/commands/pre_update.py: -------------------------------------------------------------------------------- 1 | """ 2 | This commands calls other Django management commands to perform pre-update tasks 3 | during the update process. 4 | 5 | This may include: 6 | 7 | - applying migrations to database with `migrate` 8 | - `collectstatic` 9 | - loading fixtures 10 | - clearing cache (redis) 11 | """ 12 | 13 | import os 14 | 15 | from django.core.management.base import BaseCommand 16 | from django.core.management import call_command 17 | 18 | from apps.core.db import create_database 19 | 20 | 21 | class Command(BaseCommand): 22 | def handle(self, *args, **options): 23 | 24 | # create database for the environment if it does not exist 25 | call_command("create_database") 26 | 27 | # collectstatic 28 | call_command("collectstatic", "--no-input") 29 | # migrate 30 | call_command("migrate") 31 | -------------------------------------------------------------------------------- /backend/apps/core/management/commands/start_celery_worker.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://avilpage.com/2017/05/how-to-auto-reload-celery-workers-in-development.html 3 | """ 4 | 5 | import shlex 6 | import subprocess 7 | 8 | from django.core.management.base import BaseCommand 9 | from django.utils import autoreload 10 | 11 | PIDFILE = "/code/celerybeat.pid" 12 | 13 | 14 | def restart_celery_worker(): 15 | cmd = "pkill -9 celery" 16 | subprocess.call(shlex.split(cmd)) 17 | # fmt: off 18 | cmd = 'celery --app=backend.celery_app:app worker -Q default --concurrency=1 --loglevel=INFO' # noqa 19 | # fmt: on 20 | subprocess.call(shlex.split(cmd)) 21 | 22 | 23 | class Command(BaseCommand): 24 | def handle(self, *args, **options): 25 | autoreload.run_with_reloader(restart_celery_worker) 26 | -------------------------------------------------------------------------------- /backend/apps/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/apps/core/migrations/__init__.py -------------------------------------------------------------------------------- /backend/apps/core/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from backend.celery_app import app 4 | 5 | from django.conf import settings 6 | from django.core.mail import EmailMessage 7 | from django.template.loader import render_to_string 8 | 9 | 10 | @app.task 11 | def debug_task(): 12 | time.sleep(5) 13 | return "Debug task slept for 5 second." 14 | 15 | 16 | @app.task 17 | def celery_beat_debug_task(): 18 | return "Celery beat debug task complete." 19 | 20 | 21 | @app.task 22 | def send_email_debug_task(): 23 | """ 24 | Sends an email to Django admins 25 | """ 26 | 27 | html_message = render_to_string( 28 | "emails/email_admins.html", 29 | { 30 | "message": "This email was sent successfully.", 31 | }, 32 | ) 33 | 34 | subject = "Debug Admin Email" 35 | email = EmailMessage( 36 | subject, 37 | html_message, 38 | settings.DEFAULT_FROM_EMAIL, 39 | [settings.ADMIN_EMAIL], 40 | ) 41 | email.content_subtype = "html" 42 | email.send() 43 | -------------------------------------------------------------------------------- /backend/apps/core/tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.contrib.auth import get_user_model 4 | 5 | from apps.core.models import RequestLog 6 | 7 | User = get_user_model() 8 | 9 | 10 | @pytest.mark.django_db(transaction=True) 11 | def test_request_log_middleware(client): 12 | header = {"HTTP_X_FORWARDED_FOR": "123.123.123.123"} 13 | client.get("/posts", follow=True, **header) 14 | 15 | assert RequestLog.objects.all().count() == 1 16 | 17 | request_log = RequestLog.objects.first() 18 | 19 | assert request_log.remote_address == "123.123.123.123" 20 | 21 | user = User.objects.create_user( 22 | email="user@email.com", password="abcd1234!", is_active=True 23 | ) 24 | 25 | client.force_login(user) 26 | 27 | client.get("admin/core/requestlog/") 28 | 29 | assert RequestLog.objects.all().count() == 2 30 | -------------------------------------------------------------------------------- /backend/apps/core/urls/api_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.core import views 4 | 5 | urlpatterns = [ 6 | path("health-check/", views.health_check, name="health_check"), 7 | path("exception/", views.trigger_exception, name="exception"), 8 | path("email-admins/", views.email_admins, name="email-admins"), 9 | path("version/", views.version, name="version"), 10 | ] 11 | -------------------------------------------------------------------------------- /backend/apps/core/urls/mtv_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.core import views 4 | 5 | urlpatterns = [ 6 | path("", views.index, name="index"), 7 | ] 8 | -------------------------------------------------------------------------------- /backend/apps/core/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import botocore 4 | from aws_secretsmanager_caching import ( 5 | InjectKeywordedSecretString, 6 | SecretCache, 7 | ) 8 | 9 | client = botocore.session.get_session().create_client( 10 | "secretsmanager", region_name=os.environ.get("AWS_REGION", "us-east-1") 11 | ) 12 | cache = SecretCache(client=client) 13 | 14 | DB_SECRET_NAME = os.environ.get("DB_SECRET_NAME", "") 15 | database_secret = cache.get_secret_string(DB_SECRET_NAME) 16 | 17 | 18 | # use this if the secret is JSON 19 | # @InjectKeywordedSecretString( 20 | # secret_id=DB_SECRET_NAME, cache=cache, func_password="password" 21 | # ) 22 | # def from_secret(func_password): 23 | # return func_password 24 | -------------------------------------------------------------------------------- /backend/assets/postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/assets/postgres.png -------------------------------------------------------------------------------- /backend/assets/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/assets/redis.png -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery_app import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /backend/backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/backend/celery_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | from django.conf import settings 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.development") 7 | 8 | app = Celery("backend") 9 | 10 | app.config_from_object("django.conf:settings", namespace="CELERY") 11 | 12 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 13 | -------------------------------------------------------------------------------- /backend/backend/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def template_vars(request): 5 | data = {} 6 | data["FRONTEND_URL"] = settings.FRONTEND_URL 7 | return data 8 | -------------------------------------------------------------------------------- /backend/backend/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | import graphql_jwt 3 | 4 | import apps.blog.schema 5 | import apps.accounts.schema 6 | 7 | 8 | class Query(apps.accounts.schema.Query, apps.blog.schema.Query, graphene.ObjectType): 9 | pass 10 | 11 | 12 | class Mutation( 13 | apps.accounts.schema.Mutation, 14 | apps.blog.schema.Mutation, 15 | graphene.ObjectType, 16 | ): 17 | token_auth = graphql_jwt.ObtainJSONWebToken.Field() 18 | verify_token = graphql_jwt.Verify.Field() 19 | refresh_token = graphql_jwt.Refresh.Field() 20 | 21 | 22 | schema = graphene.Schema(query=Query, mutation=Mutation) 23 | -------------------------------------------------------------------------------- /backend/backend/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/backend/settings/__init__.py -------------------------------------------------------------------------------- /backend/backend/settings/aws.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .production import * 4 | 5 | from apps.core.utils import database_secret 6 | 7 | DATABASES = { 8 | "default": { 9 | "ENGINE": "django.db.backends.postgresql_psycopg2", 10 | "NAME": os.environ.get("POSTGRES_NAME", "postgres"), 11 | "USER": os.environ.get("POSTGRES_USERNAME", "postgres"), 12 | # TODO: rename this function 13 | "PASSWORD": database_secret, 14 | "HOST": os.environ.get("POSTGRES_SERVICE_HOST", "localhost"), 15 | "PORT": os.environ.get("POSTGRES_SERVICE_PORT", 5432), 16 | } 17 | } 18 | 19 | ## Email settings 20 | 21 | EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com") 22 | EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "user@email.com") 23 | EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "password") 24 | EMAIL_PORT = os.environ.get("EMAIL_PORT", "587") 25 | EMAIL_USE_TLS = True 26 | DEFAULT_FROM_EMAIL = EMAIL_HOST_USER 27 | ADMINS = [("Admin", os.environ.get("ADMINS", ""))] 28 | 29 | SESSION_COOKIE_DOMAIN = os.environ.get("DOMAIN_NAME", "domain.com") 30 | -------------------------------------------------------------------------------- /backend/backend/settings/development.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .base import * 4 | 5 | ALLOWED_HOSTS = ["*"] 6 | 7 | INSTALLED_APPS += ["debug_toolbar", "django_extensions", "silk"] 8 | 9 | DEBUG_TOOLBAR_CONFIG = { 10 | "SHOW_COLLAPSED": True, 11 | } 12 | 13 | MIDDLEWARE += [ 14 | "debug_toolbar.middleware.DebugToolbarMiddleware", 15 | "silk.middleware.SilkyMiddleware", 16 | ] 17 | 18 | INTERNAL_IPS = ["127.0.0.1"] 19 | 20 | ADMIN_EMAIL = "user@email.com" 21 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 22 | 23 | NOTEBOOK_ARGUMENTS = [ 24 | "--ip", 25 | "0.0.0.0", 26 | "--allow-root", 27 | "--no-browser", 28 | ] 29 | 30 | # PRIVATE_MEDIA_STORAGE = "backend.storage_backends" 31 | DEFAULT_FILE_STORAGE = "backend.storage_backends.PrivateVolumeMediaStorage" 32 | -------------------------------------------------------------------------------- /backend/backend/settings/swarm_ec2.py: -------------------------------------------------------------------------------- 1 | # defines settings for applications using the DockerEc2 construct 2 | from .base import * # noqa 3 | 4 | DEBUG = False 5 | 6 | AWS_DEFAULT_ACL = None 7 | STATICFILES_STORAGE = "backend.storage_backends.StaticStorage" 8 | 9 | AWS_STATIC_LOCATION = "static" 10 | 11 | AWS_PRIVATE_MEDIA_LOCATION = "media/private" 12 | PRIVATE_FILE_STORAGE = "backend.storage_backends.PrivateMediaStorage" 13 | 14 | AWS_PRELOAD_METADATA = True 15 | AWS_STORAGE_BUCKET_NAME = os.environ["S3_BUCKET_NAME"] # noqa 16 | AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" 17 | STATIC_ROOT = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/" 18 | MEDIA_ROOT = f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/media/" 19 | -------------------------------------------------------------------------------- /backend/backend/storage_backends.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.files.storage import FileSystemStorage 3 | from storages.backends.s3boto3 import S3Boto3Storage 4 | 5 | 6 | class StaticStorage(S3Boto3Storage): 7 | location = settings.AWS_STATIC_LOCATION 8 | 9 | 10 | if settings.DEBUG: 11 | 12 | class PrivateMediaStorage(FileSystemStorage): 13 | location = settings.AWS_PRIVATE_MEDIA_LOCATION 14 | 15 | else: 16 | 17 | class PrivateMediaStorage(S3Boto3Storage): 18 | location = settings.AWS_PRIVATE_MEDIA_LOCATION 19 | default_acl = "private" 20 | file_overwrite = False 21 | custom_domain = False 22 | 23 | 24 | class PrivateVolumeMediaStorage(FileSystemStorage): 25 | location = settings.AWS_PRIVATE_MEDIA_LOCATION 26 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.development") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /backend/media/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /backend/notebooks/README.md: -------------------------------------------------------------------------------- 1 | # Example usage 2 | 3 | ```py 4 | %env DJANGO_ALLOW_ASYNC_UNSAFE=true 5 | from django.contrib.auth import get_user_model 6 | User = get_user_model() 7 | users = User.objects.all() 8 | users.count() 9 | ``` 10 | -------------------------------------------------------------------------------- /backend/notebooks/media/images/postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/notebooks/media/images/postgres.png -------------------------------------------------------------------------------- /backend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = backend.settings.development 3 | python_files = tests.py test_*.py *_tests.py 4 | filterwarnings = 5 | ignore::DeprecationWarning 6 | ignore::django.utils.deprecation.RemovedInDjango60Warning 7 | -------------------------------------------------------------------------------- /backend/scripts/aws/ecs_exec.sh: -------------------------------------------------------------------------------- 1 | : ' 2 | This script is used to gain access to a container running in an app environment 3 | 4 | The user is asked for the name of the ad hoc environment and then the script starts a shell in the gunicorn container 5 | ' 6 | 7 | read -p "App environment name: " APP_ENV_NAME 8 | 9 | TASK_ARN=$(aws ecs list-tasks \ 10 | --cluster $APP_ENV_NAME-cluster \ 11 | --service-name $APP_ENV_NAME-gunicorn | jq -r '.taskArns | .[0]' \ 12 | ) 13 | 14 | echo $TASK_ARN 15 | 16 | aws ecs execute-command --cluster $APP_ENV_NAME-cluster \ 17 | --task $TASK_ARN \ 18 | --container gunicorn \ 19 | --interactive \ 20 | --command "/bin/bash" 21 | -------------------------------------------------------------------------------- /backend/scripts/aws/ssm_port_forward.sh: -------------------------------------------------------------------------------- 1 | : ' 2 | This script is used to port forward to the bastion host instance using SSM 3 | 4 | The user is prompted for the name of the base environment and then a port forwarding session is opened to a bastion host 5 | 6 | You can then connect to postgres on you computer using localhost:5432 and via port forwarding it will connect to the RDS instance 7 | 8 | The bastion host runs a service with socat that forwards all traffic on port 5432 to the associated RDS instance 9 | ' 10 | 11 | read -p "Base environment name (dev): " BASE_ENV_NAME 12 | 13 | BASTION_INSTANCE_ID=$(aws ec2 describe-instances \ 14 | --filters "Name=tag:env,Values=$BASE_ENV_NAME" \ 15 | --filters "Name=instance-state-name,Values=running" \ 16 | --query "Reservations[*].Instances[*].InstanceId" \ 17 | --output text \ 18 | --region us-east-1 19 | ) 20 | 21 | aws ssm start-session \ 22 | --target $BASTION_INSTANCE_ID \ 23 | --document-name AWS-StartPortForwardingSession \ 24 | --parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}' \ 25 | --region us-east-1 26 | -------------------------------------------------------------------------------- /backend/scripts/aws/ssm_start_session.sh: -------------------------------------------------------------------------------- 1 | : ' 2 | This script is used to start a bash shell on the bastion host instance using SSM 3 | ' 4 | 5 | read -p "Base environment name (dev): " AD_HOC_BASE_ENV 6 | 7 | BASTION_INSTANCE_ID=$(aws ec2 describe-instances \ 8 | --filters "Name=tag:env,Values=$AD_HOC_BASE_ENV" \ 9 | --filters "Name=instance-state-name,Values=running" \ 10 | --query "Reservations[*].Instances[*].InstanceId" \ 11 | --output text \ 12 | --region us-east-1 13 | ) 14 | 15 | aws ssm start-session \ 16 | --target $BASTION_INSTANCE_ID 17 | -------------------------------------------------------------------------------- /backend/scripts/start_prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z $PORT ]]; then 4 | PORT=8000 5 | fi 6 | 7 | # start gunicorn process 8 | gunicorn -t 300 -w 4 -b 0.0.0.0:${PORT} backend.wsgi 9 | -------------------------------------------------------------------------------- /backend/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/static/favicon.ico -------------------------------------------------------------------------------- /backend/static/img/django.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/static/img/django.jpeg -------------------------------------------------------------------------------- /backend/static/img/drf_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/static/img/drf_square.png -------------------------------------------------------------------------------- /backend/static/img/quasar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/static/img/quasar.png -------------------------------------------------------------------------------- /backend/static/img/vue3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/static/img/vue3.png -------------------------------------------------------------------------------- /backend/static/main.css: -------------------------------------------------------------------------------- 1 | ul { 2 | padding-bottom: 0px !important; 3 | } 4 | 5 | .registration-form, 6 | .login-form { 7 | width: 100%; 8 | max-width: 330px; 9 | padding: 15px; 10 | margin: auto; 11 | } 12 | 13 | .post-image { 14 | max-height: 200px; 15 | margin: auto; 16 | padding: 15px; 17 | } 18 | 19 | .fieldWrapper { 20 | padding-bottom: 15px; 21 | } -------------------------------------------------------------------------------- /backend/staticfiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/backend/staticfiles/.gitkeep -------------------------------------------------------------------------------- /backend/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}Permission Denied{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}Permission Denied{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 |

You don't have permission to access this page.

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /backend/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block title %}Page Not Found{% endblock %} 6 | 7 | 8 | 9 | {% block nav_title %}Page not found{% endblock %} 10 | 11 | 12 | 13 | {% block content %} 14 |

This page cannot be found.

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /backend/templates/swagger-ui.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | Swagger 6 | 7 | 8 | 13 | 14 | 15 |
16 | 17 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /cypress/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8000", 3 | "frontendUrl": "http://localhost:8080", 4 | "defaultPassword": "Foobar123!" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/cypress/fixtures/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/cypress/cypress/fixtures/redis.png -------------------------------------------------------------------------------- /cypress/cypress/integration/graphql.spec.js: -------------------------------------------------------------------------------- 1 | describe("GraphQL demo", function () { 2 | it.skip("can query GraphiQL", function () { 3 | const query = `query { 4 | paginatedPosts { 5 | pages, 6 | count, 7 | objects { 8 | body, 9 | id, 10 | liked, 11 | likeCount, 12 | ` 13 | cy.visit("/graphql"); 14 | cy.get(":nth-child(31) > .CodeMirror-line").click().type(query, {delay:0}); 15 | cy.get(".execute-button").click(); 16 | }); 17 | }); -------------------------------------------------------------------------------- /cypress/cypress/integration/quasar/createPost.spec.js: -------------------------------------------------------------------------------- 1 | import { NewEmail } from '../../support/utils'; 2 | 3 | describe("Test Quasar CRUD functionality", function () { 4 | 5 | it("can create a new post as an anonymous user", function () { 6 | cy.createPost("Quasar test post content"); 7 | }); 8 | 9 | it("can create a new post as a logged-in user", function () { 10 | const emailAddress = new NewEmail().getEmail(); 11 | cy.registerUser(emailAddress); 12 | cy.createPost("This post was created by a logged-in user"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /cypress/cypress/integration/quasar/login.spec.js: -------------------------------------------------------------------------------- 1 | describe("Test Login functionality", function () { 2 | it("Test existing user can login", function () { 3 | cy.visit("http://localhost:8080/login"); 4 | cy.get('[data-cy="email-input"]').type("user@email.com") 5 | cy.get('[data-cy="password-input"]').type("password") 6 | cy.get('[data-cy="login-btn"]').click(); 7 | 8 | }); 9 | 10 | 11 | }); -------------------------------------------------------------------------------- /cypress/cypress/integration/quasar/register.spec.js: -------------------------------------------------------------------------------- 1 | describe("Test registration functionality", function () { 2 | it("Test new user can register", function () { 3 | cy.registerUser() 4 | }); 5 | }); -------------------------------------------------------------------------------- /cypress/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /cypress/cypress/screenshots/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /cypress/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/cypress/support/utils/index.js: -------------------------------------------------------------------------------- 1 | export class NewEmail { 2 | constructor(email) { 3 | let emailAddress; 4 | if (email) { 5 | this.emailAddress = email; 6 | } else { 7 | const timeStamp = Math.floor(Date.now() / 1000); 8 | emailAddress = `cypress${timeStamp}@email.com`; 9 | this.emailAddress = emailAddress; 10 | } 11 | } 12 | 13 | getEmail() { 14 | return this.emailAddress; 15 | } 16 | } -------------------------------------------------------------------------------- /cypress/cypress/videos/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /cypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-step-by-step", 3 | "version": "1.0.0", 4 | "description": "This is a Django project that focuses on demonstrating how to set up Django project, step-by-step. See the [Step-by-step Guide](/STEP_BY_STEP.md) to see each step of the project setup.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@gitlab.com-home:briancaffey/django-step-by-step.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "cypress": "^7.4.0", 17 | "cypress-file-upload": "^5.0.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iac/cdk/.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/build.yml linguist-generated 9 | /.github/workflows/pull-request-lint.yml linguist-generated 10 | /.github/workflows/upgrade.yml linguist-generated 11 | /.gitignore linguist-generated 12 | /.mergify.yml linguist-generated 13 | /.npmignore linguist-generated 14 | /.projen/** linguist-generated 15 | /.projen/deps.json linguist-generated 16 | /.projen/files.json linguist-generated 17 | /.projen/tasks.json linguist-generated 18 | /cdk.json linguist-generated 19 | /LICENSE linguist-generated 20 | /package.json linguist-generated 21 | /tsconfig.dev.json linguist-generated 22 | /tsconfig.json linguist-generated 23 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /iac/cdk/.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /iac/cdk/.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /iac/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | /test-reports/ 34 | junit.xml 35 | /coverage/ 36 | !/.github/workflows/build.yml 37 | !/.mergify.yml 38 | !/.github/workflows/upgrade.yml 39 | !/.github/pull_request_template.md 40 | !/test/ 41 | !/tsconfig.json 42 | !/tsconfig.dev.json 43 | !/src/ 44 | /lib 45 | /dist/ 46 | !/.eslintrc.json 47 | /assets/ 48 | !/cdk.json 49 | /cdk.out/ 50 | .cdk.staging/ 51 | .parcel-cache/ 52 | !/.projenrc.js 53 | -------------------------------------------------------------------------------- /iac/cdk/.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | merge_method: squash 11 | commit_message_template: |- 12 | {{ title }} (#{{ number }}) 13 | 14 | {{ body }} 15 | pull_request_rules: 16 | - name: Automatic merge on approval and successful build 17 | actions: 18 | delete_head_branch: {} 19 | queue: 20 | name: default 21 | conditions: 22 | - "#approved-reviews-by>=1" 23 | - -label~=(do-not-merge) 24 | - status-success=build 25 | -------------------------------------------------------------------------------- /iac/cdk/.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /.mergify.yml 8 | /test/ 9 | /tsconfig.dev.json 10 | /src/ 11 | !/lib/ 12 | !/lib/**/*.js 13 | !/lib/**/*.d.ts 14 | dist 15 | /tsconfig.json 16 | /.github/ 17 | /.vscode/ 18 | /.idea/ 19 | /.projenrc.js 20 | tsconfig.tsbuildinfo 21 | /.eslintrc.json 22 | !/assets/ 23 | cdk.out/ 24 | .cdk.staging/ 25 | /.gitattributes 26 | -------------------------------------------------------------------------------- /iac/cdk/.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/upgrade.yml", 9 | ".gitignore", 10 | ".mergify.yml", 11 | ".npmignore", 12 | ".projen/deps.json", 13 | ".projen/files.json", 14 | ".projen/tasks.json", 15 | "cdk.json", 16 | "LICENSE", 17 | "tsconfig.dev.json", 18 | "tsconfig.json" 19 | ], 20 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 21 | } 22 | -------------------------------------------------------------------------------- /iac/cdk/.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk } = require('projen'); 2 | const project = new awscdk.AwsCdkTypeScriptApp({ 3 | cdkVersion: '2.178.2', 4 | defaultReleaseBranch: 'main', 5 | name: 'cdk', 6 | 7 | deps: ['cdk-django@1.8.2'], /* Runtime dependencies of this module. */ 8 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 9 | // devDeps: [], /* Build dependencies for this module. */ 10 | // packageName: undefined, /* The "name" in package.json. */ 11 | }); 12 | project.synth(); 13 | -------------------------------------------------------------------------------- /iac/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Using `cdk-django` 2 | 3 | This directory provides an example of how to use the Infrastructure as Code library called [`cdk-django`](https://github.com/briancaffey/cdk-django). `cdk-django` was designed together with `django-step-by-step` to provide a complete of example of how to build infrastructure for running Django projects. 4 | 5 | ## Updating `cdk-django` version 6 | 7 | - Update the version of `cdk-django` in `.projenrc.js` to the latest version. 8 | 9 | - run `cd iac/cdk && npx projen` 10 | 11 | - commit and push changes 12 | 13 | - Run the `[IaC] CDK GitHub Action` 14 | -------------------------------------------------------------------------------- /iac/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/main.ts", 3 | "output": "cdk.out", 4 | "build": "npx projen bundle", 5 | "watch": { 6 | "include": [ 7 | "src/**/*.ts", 8 | "test/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "README.md", 12 | "cdk*.json", 13 | "**/*.d.ts", 14 | "**/*.js", 15 | "tsconfig.json", 16 | "package*.json", 17 | "yarn.lock", 18 | "node_modules" 19 | ] 20 | }, 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /iac/cdk/src/ecs/configs/app/alpha.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /iac/cdk/src/ecs/configs/base/dev.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /iac/cdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ecs/'; 2 | -------------------------------------------------------------------------------- /iac/cdk/src/main.ts: -------------------------------------------------------------------------------- 1 | export * from './ecs/'; 2 | -------------------------------------------------------------------------------- /iac/cdk/test/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/iac/cdk/test/.gitignore -------------------------------------------------------------------------------- /iac/cdk/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.js" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /iac/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2019" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/app/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/app/Pulumi.alpha.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | aws:region: us-east-1 3 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/app/Pulumi.gamma.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | aws:region: us-east-1 3 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/app/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: ecs-app 2 | runtime: nodejs 3 | description: A minimal AWS TypeScript Pulumi program 4 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-app", 3 | "license": "MIT", 4 | "main": "index.ts", 5 | "devDependencies": { 6 | "@types/node": "^20" 7 | }, 8 | "dependencies": { 9 | "@pulumi/aws": "^6.67.0", 10 | "@pulumi/awsx": "^2.21.0", 11 | "@pulumi/pulumi": "^3.147.0", 12 | "@pulumi/random": "^4.17.0", 13 | "typescript": "^5.7.3", 14 | "pulumi-aws-django": "1.28.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "bin", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "pretty": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "files": [ 16 | "index.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/base/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/base/Pulumi.dev.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | aws:region: us-east-1 3 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/base/Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: ecs-base 2 | runtime: nodejs 3 | description: A minimal AWS TypeScript Pulumi program 4 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/base/index.ts: -------------------------------------------------------------------------------- 1 | import { EcsBaseEnvComponent } from 'pulumi-aws-django'; 2 | 3 | const ecsBaseEnv = new EcsBaseEnvComponent('myEcsEnv', { 4 | certificateArn: process.env.CERTIFICATE_ARN || 'arn:aws:acm:us-east-1:111111111111:certificate/11111111-1111-1111-1111-111111111111', 5 | domainName: process.env.DOMAIN_NAME || 'example.com' 6 | }); 7 | 8 | export const vpcId = ecsBaseEnv.vpc.vpcId; 9 | export const assetsBucketName = ecsBaseEnv.assetsBucket.id; 10 | export const privateSubnetIds = ecsBaseEnv.vpc.privateSubnetIds; 11 | export const appSgId = ecsBaseEnv.appSecurityGroup.id; 12 | export const albSgId = ecsBaseEnv.albSecurityGroup.id; 13 | export const listenerArn = ecsBaseEnv.listener.arn; 14 | export const albDnsName = ecsBaseEnv.alb.dnsName; 15 | export const rdsAddress = ecsBaseEnv.databaseInstance.address; 16 | export const domainName = ecsBaseEnv.domainName; 17 | export const baseStackName = ecsBaseEnv.stackName; 18 | export const rdsPasswordSecretName = ecsBaseEnv.rdsPasswordSecretName; 19 | export const redisServiceHost = ecsBaseEnv.redisServiceHost; 20 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecs-base", 3 | "license": "MIT", 4 | "main": "index.ts", 5 | "devDependencies": { 6 | "@types/node": "^20" 7 | }, 8 | "dependencies": { 9 | "@pulumi/aws": "^6.67.0", 10 | "@pulumi/awsx": "^2.21.0", 11 | "@pulumi/pulumi": "^3.147.0", 12 | "@pulumi/random": "^4.17.0", 13 | "typescript": "^5.7.3", 14 | "pulumi-aws-django": "1.28.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iac/pulumi/live/ecs/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "bin", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "pretty": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "files": [ 16 | "index.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /iac/terraform/.gitignore: -------------------------------------------------------------------------------- 1 | NOTES.md 2 | .terraform 3 | .terraform.lock.hcl 4 | local.tfvars 5 | backend.config 6 | errored.tfstate 7 | -------------------------------------------------------------------------------- /iac/terraform/bootstrap/.gitignore: -------------------------------------------------------------------------------- 1 | bootstrap.tfvars 2 | terraform.tfstate 3 | terraform.tfstate.backup 4 | -------------------------------------------------------------------------------- /iac/terraform/bootstrap/README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap Terraform Backend Resources 2 | 3 | This directory sets up the following resources: 4 | 5 | - S3 bucket 6 | - DynamoDB table 7 | - ECR repositories (backend and frontend) 8 | 9 | These resources provide the Terraform S3 backend that will be used for the base/app stacks. 10 | 11 | ## TODO 12 | 13 | - Add IAM roles to be used with GitHub Actions for infrastructure and application deployments 14 | - The resources in `bootstrap` should be moved to a formal module in `terraform-aws-django` (called `foundation`, `prerequisites` or similar) 15 | - I might add the provisioning of the ACM certificate to this new module -------------------------------------------------------------------------------- /iac/terraform/bootstrap/bootstrap.tfvars.template: -------------------------------------------------------------------------------- 1 | # copy this file, replace with your values and name it bootstrap.tfvars 2 | # then run `terraform init && terraform plan -var-file=bootstrap.tfvars` 3 | # or simply `make terraform-bootstrap` 4 | region = "us-east-1" 5 | backend_name = "my-backend-name-prefix" 6 | -------------------------------------------------------------------------------- /iac/terraform/bootstrap/outputs.tf: -------------------------------------------------------------------------------- 1 | output "s3_backend_bucket" { 2 | value = aws_s3_bucket.this.id 3 | } 4 | output "s3_backend_region" { 5 | value = aws_s3_bucket.this.region 6 | } 7 | output "dynamodb_lock_table" { 8 | value = aws_dynamodb_table.this.id 9 | } 10 | 11 | # ECR 12 | 13 | output "ecr_be_repo_url" { 14 | value = aws_ecr_repository.backend.repository_url 15 | } 16 | 17 | output "ecr_fe_repo_url" { 18 | value = aws_ecr_repository.frontend.repository_url 19 | } 20 | 21 | output "ecr_be_nginx_url" { 22 | value = aws_ecr_repository.backend-nginx.repository_url 23 | } 24 | 25 | ################################################################################ 26 | # Used for the S3 backend (backend.config) 27 | ################################################################################ 28 | 29 | output "bucket" { 30 | value = aws_s3_bucket.this.id 31 | } 32 | 33 | output "key" { 34 | value = var.key 35 | } 36 | 37 | output "region" { 38 | value = var.region 39 | } 40 | -------------------------------------------------------------------------------- /iac/terraform/bootstrap/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">=1.10.5" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "5.84.0" 8 | } 9 | } 10 | } 11 | 12 | provider "aws" { 13 | region = var.region 14 | } 15 | -------------------------------------------------------------------------------- /iac/terraform/bootstrap/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-east-1" 3 | } 4 | 5 | variable "key" { 6 | default = "terraform-state" 7 | } 8 | 9 | variable "backend_name" {} 10 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/.gitignore: -------------------------------------------------------------------------------- 1 | #main.tf 2 | backend.config 3 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/backend.config.example: -------------------------------------------------------------------------------- 1 | # to deploy a development environment from your computer, 2 | # copy this file to `backend.config` (it is .gitignore'd) 3 | 4 | bucket = "your-tf-bucket" 5 | key = "terraform-state" 6 | region = "us-east-1" 7 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/.gitignore: -------------------------------------------------------------------------------- 1 | sample.tfvars 2 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/alpha.tfvars: -------------------------------------------------------------------------------- 1 | be_image_tag = "latest" 2 | fe_image_tag = "latest" 3 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/beta.tfvars: -------------------------------------------------------------------------------- 1 | be_image_tag = "v0.1.2" 2 | fe_image_tag = "latest" 3 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/brian.tfvars: -------------------------------------------------------------------------------- 1 | region = "us-east-1" 2 | instance_type = "t2.small" 3 | 4 | # additional required variables are defined with TF_VAR_ 5 | # variables and used in GitHub Actions workflow 6 | 7 | # ecr_be_repo_url = "111111111111.dkr.ecr.us-east-1.amazonaws.com/backend" 8 | # ecr_fe_repo_url = "111111111111.dkr.ecr.us-east-1.amazonaws.com/frontend" 9 | # acm_certificate_arn = "arn:aws:acm:us-east-1:111111111111:certificate/11111111-1111-1111-1111-111111111111" 10 | # frontend_url = "https://app.example.com" 11 | # domain_name = "example.com" 12 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/default.tf: -------------------------------------------------------------------------------- 1 | be_image_tag = "v0.1.1" 2 | fe_image_tag = "latest" 3 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/sample.tfvars.template: -------------------------------------------------------------------------------- 1 | region = "us-east-1" 2 | instance_type = "t2.small" 3 | 4 | # additional required variables are defined with TF_VAR_ 5 | # variables and used in GitHub Actions workflow 6 | 7 | # ecr_be_repo_url = "111111111111.dkr.ecr.us-east-1.amazonaws.com/backend" 8 | # ecr_fe_repo_url = "111111111111.dkr.ecr.us-east-1.amazonaws.com/frontend" 9 | # acm_certificate_arn = "arn:aws:acm:us-east-1:111111111111:certificate/11111111-1111-1111-1111-111111111111" 10 | # domain_name = "example.com" 11 | 12 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/envs/test.tfvars: -------------------------------------------------------------------------------- 1 | be_image_tag = "v0.1.0" 2 | fe_image_tag = "latest" 3 | 4 | # additional required variables are defined with TF_VAR_ 5 | # variables and used in GitHub Actions workflow 6 | 7 | # These TF_VAR_ variables are defined in the GitHub repository secrets 8 | 9 | # frontend_url = "https://app.example.com" 10 | # domain_name = "example.com" 11 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/outputs.tf: -------------------------------------------------------------------------------- 1 | output "backend_update_command" { 2 | value = module.main.backend_update_command 3 | description = "Command for running backend update commands with run-task" 4 | } 5 | 6 | output "ecs_exec_command" { 7 | value = module.main.ecs_exec_command 8 | } 9 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/app/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">=1.10.5" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "5.84.0" 8 | } 9 | } 10 | 11 | backend "s3" {} 12 | } 13 | 14 | provider "aws" { 15 | region = var.region 16 | default_tags { 17 | tags = { 18 | env = terraform.workspace 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | .terraform.lock.hcl 3 | terraform.tfvars 4 | terraform.tfstate.backup 5 | terraform.tfstate 6 | 7 | backend.config 8 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/envs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/envs/dev.tfvars: -------------------------------------------------------------------------------- 1 | # These values can be either included in this file or as environment variables 2 | # Environment variables are preferable for values that should not be committed, such as AWS ARNs 3 | 4 | # The following variables have been added to GitHub environment secrets for the environment called 'shared-resources-dev' 5 | # The variable is: TV_VARS_certificate_arn 6 | # certificate_arn = "arn:aws:acm:us-east-1:111111111111:certificate/11111111-1111-1111-1111-111111111111" 7 | region = "us-east-1" 8 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/envs/dev.tfvars.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/iac/terraform/live/ecs/base/envs/dev.tfvars.example -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/main.tf: -------------------------------------------------------------------------------- 1 | module "main" { 2 | # for local development 3 | # source = "../../../../../../terraform-aws-django/modules/ecs/base" 4 | # for production use use git URL pointing to the module (recommended to use tags instead of main branch) 5 | source = "git::https://github.com/briancaffey/terraform-aws-django.git//modules/ecs/base" # add ?ref= to use a branch 6 | certificate_arn = var.certificate_arn 7 | domain_name = var.domain_name 8 | } 9 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">=1.10.5" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "5.84.0" 8 | } 9 | } 10 | 11 | backend "s3" {} 12 | } 13 | 14 | provider "aws" { 15 | region = var.region 16 | default_tags { 17 | tags = { 18 | env = terraform.workspace 19 | shared_resources_env = terraform.workspace 20 | shared_resource = "true" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iac/terraform/live/ecs/base/variables.tf: -------------------------------------------------------------------------------- 1 | variable "certificate_arn" { 2 | type = string 3 | } 4 | 5 | variable "region" { 6 | type = string 7 | default = "us-east-1" 8 | } 9 | 10 | variable "domain_name" { 11 | type = string 12 | } 13 | -------------------------------------------------------------------------------- /k6/script.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | 3 | const baseUrl = __ENV.BASE_URL || 'http://0.0.0.0:8000'; 4 | 5 | export const options = { 6 | scenarios: { 7 | api: { 8 | // https://k6.io/docs/using-k6/scenarios/executors/ramping-arrival-rate 9 | executor: 'per-vu-iterations', 10 | 11 | vus: 100, 12 | iterations: 500, 13 | maxDuration: '10m', 14 | }, 15 | }, 16 | }; 17 | 18 | export default function () { 19 | const path = '/api/drf/cbv/posts/' 20 | const url = baseUrl + path; 21 | const params = { 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | }; 26 | const payload = JSON.stringify({ 27 | body: 'k6 blog post content ' + Math.random(), 28 | }); 29 | 30 | http.post(url, payload, params); 31 | } 32 | -------------------------------------------------------------------------------- /nginx/backend/prod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf 4 | 5 | COPY server.crt /etc/ssl/certs/server.crt 6 | COPY server.key /etc/ssl/private/server.key 7 | 8 | EXPOSE 443 9 | 10 | CMD ["nginx", "-g", "daemon off;"] 11 | -------------------------------------------------------------------------------- /nginx/backend/prod/README.md: -------------------------------------------------------------------------------- 1 | 2 | ``` 3 | openssl genrsa -aes256 -passout pass:foobar -out server.key 2048 4 | ``` 5 | 6 | ``` 7 | openssl req -new -key server.key -out server.csr -subj "/CN=Brian Caffey/OU=Brian Caffey/emailAddress=hello@briancaffey.com" 8 | ``` 9 | 10 | ``` 11 | openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 3650 -sha256 -extfile v3.ext 12 | ``` 13 | 14 | ``` 15 | openssl rsa -in server.key -out server.key 16 | ``` 17 | -------------------------------------------------------------------------------- /nginx/backend/prod/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | server { 29 | listen 443 ssl; 30 | 31 | ssl_certificate /etc/ssl/certs/server.crt; 32 | ssl_certificate_key /etc/ssl/private/server.key; 33 | 34 | location / { 35 | proxy_redirect off; 36 | proxy_pass http://localhost:8000; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_set_header Host $http_host; 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /nginx/backend/prod/script.sh: -------------------------------------------------------------------------------- 1 | openssl genrsa -aes256 -passout pass:foobar -out server.key 2048 2 | 3 | openssl req -new -key server.key -out server.csr -subj "/CN=Brian Caffey/OU=Brian Caffey/emailAddress=hello@briancaffey.com" 4 | 5 | openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 3650 -sha256 -extfile v3.ext 6 | 7 | openssl rsa -in server.key -out server.key 8 | -------------------------------------------------------------------------------- /nginx/backend/prod/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICmTCCAYECAQAwVDEVMBMGA1UEAwwMQnJpYW4gQ2FmZmV5MRUwEwYDVQQLDAxC 3 | cmlhbiBDYWZmZXkxJDAiBgkqhkiG9w0BCQEWFWhlbGxvQGJyaWFuY2FmZmV5LmNv 4 | bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOjEqm3J/w0AdDS5UWCc 5 | U6QPmOOyxE6/vbVzvWydclNGy43RCc5kQwBS2xByto9xt3nVIiNGoFniORb7yi6Q 6 | y4GkRQ/kt7v1sotg0FVDFHGZV/54fGZsvXhAsKHWNCL1mTXZnB38L7Egp57FKKCL 7 | 4jBi59OzQa62Awp8GhwtkAitGnyVwqg9gjoR654RtGt4Yx0ke2E+QF+yJm1Fw76A 8 | LlT0chVyaJAz39eHD/a9jQL9v27XzsLdz/bR+W+RlQuQjiCZY+9g6YJdYbCmWQbG 9 | CVO75WOFbpzrXoEs0zgStghpjm3NjcmyKAX2An8aE4Uf+lQntEFU6N46Jv3Ov8op 10 | egcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAUR3SRQ8FexzewbZaL+xsjGDEI 11 | tXyQ0e8u5oME1nQ35VhT9mvbDZP9hsEDTv1WYEx9B7AHkJ/1/bJmRFiM/h6vixh3 12 | S0/FRlL/sifJHsOqwQi2amXnNwp+E6NKClzYqnOIY9WC2/2kLRt2Py4JOtQJK1b8 13 | 416gPyvxQX8v9QDezI5RHuz7hbmNDnfg3qcRbOofxeypddacBngpVYzLImBpLlmc 14 | d0Oiw8o3Go8EOlaeYLAI09bA5j9EjJOsz92Kqg5yglErkpg3ZNAZns8nHnk3yjbw 15 | rYoDx8YOkZEJO2KT3NvFBO/+WkglH+eWU6vEVg8exhbxqPbrZcEQfy4Ixfuk 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /nginx/backend/prod/v3.ext: -------------------------------------------------------------------------------- 1 | subjectKeyIdentifier = hash 2 | authorityKeyIdentifier = keyid:always,issuer:always 3 | basicConstraints = CA:TRUE 4 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign 5 | subjectAltName = DNS:*.briancaffey.com 6 | issuerAltName = issuer:copy 7 | -------------------------------------------------------------------------------- /nginx/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.19.4-alpine 2 | COPY dev/dev.conf /etc/nginx/nginx.conf 3 | EXPOSE 80 4 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /nginx/dev/dev.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | http { 9 | include /etc/nginx/mime.types; 10 | client_max_body_size 100m; 11 | 12 | upstream backend { 13 | server backend:8000; 14 | } 15 | 16 | upstream frontend { 17 | server nuxt:3000; 18 | } 19 | 20 | server { 21 | listen 80; 22 | charset utf-8; 23 | 24 | # frontend urls 25 | location / { 26 | proxy_redirect off; 27 | proxy_pass http://frontend; 28 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 | proxy_set_header Host $http_host; 30 | 31 | # WebSocket support 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade $http_upgrade; 34 | proxy_set_header Connection "upgrade"; 35 | } 36 | 37 | 38 | # backend urls 39 | location ~ ^/(admin|api|mtv|graphql|media|static) { 40 | proxy_redirect off; 41 | proxy_pass http://backend; 42 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 43 | proxy_set_header Host $http_host; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /nginx/dev/local.conf: -------------------------------------------------------------------------------- 1 | # use this nginx conf file to run the backend from venv on the same hostname as the frontend 2 | 3 | user nobody; 4 | worker_processes 1; 5 | # run the process in the foreground 6 | daemon off; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | # include /etc/nginx/mime.types; 14 | client_max_body_size 100m; 15 | 16 | server { 17 | listen 80; 18 | charset utf-8; 19 | 20 | # frontend urls 21 | location / { 22 | proxy_redirect off; 23 | proxy_pass http://127.0.0.1:8081; 24 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 25 | proxy_set_header Host $http_host; 26 | } 27 | 28 | 29 | # backend urls 30 | location ~ ^/(admin|api|graphql|mtv|media|static) { 31 | proxy_redirect off; 32 | proxy_pass http://127.0.0.1:8000; 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header Host $http_host; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /nginx/devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.19.4-alpine 2 | COPY dev/dev.conf /etc/nginx/nginx.conf 3 | EXPOSE 80 4 | CMD ["nginx", "-g", "daemon off;"] 5 | -------------------------------------------------------------------------------- /nginx/ec2/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Ensure DOMAIN_NAME is set 5 | if [ -z "$DOMAIN_NAME" ]; then 6 | echo "Error: DOMAIN_NAME is not set" 7 | exit 1 8 | fi 9 | 10 | # Use envsubst to replace placeholders in the template 11 | envsubst '${DOMAIN_NAME}' < /templates/init.conf.template > /init/init.conf 12 | envsubst '${DOMAIN_NAME}' < /templates/app.conf.template > /output/app.conf 13 | 14 | 15 | echo "Generated Nginx config:" 16 | cat /init/init.conf 17 | cat /output/app.conf 18 | -------------------------------------------------------------------------------- /nginx/ec2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to nginx! 5 | 10 | 11 | 12 |

Brian, Welcome to nginx!

13 |

If you see this page, the nginx web server is successfully installed and 14 | working. Further configuration is required.

15 | 16 |

For online documentation and support please refer to 17 | nginx.org.
18 | Commercial support is available at 19 | nginx.com.

20 | 21 |

Thank you for using nginx.

22 | 23 | 24 | -------------------------------------------------------------------------------- /nginx/ec2/init.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-step-by-step/85bc5121c0df47b80c83a96813cb9be7fc9843ba/nginx/ec2/init.conf -------------------------------------------------------------------------------- /nginx/ec2/nginx.conf: -------------------------------------------------------------------------------- 1 | http { 2 | include /etc/nginx/mime.types; 3 | client_max_body_size 100m; 4 | 5 | upstream backend { 6 | server backend:8000; 7 | } 8 | 9 | upstream frontend { 10 | server nuxt-app:3000; 11 | } 12 | 13 | include /etc/nginx/conf.d/*.conf; # Ensure this line is present 14 | } -------------------------------------------------------------------------------- /nginx/ec2/templates/init.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name ${DOMAIN_NAME}; 4 | 5 | location /.well-known/acme-challenge/ { 6 | root /var/www/certbot; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nginx/ecs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as build-stage 2 | ARG VERSION 3 | ENV VERSION=$VERSION 4 | WORKDIR /app/ 5 | COPY quasar-app/package.json /app/ 6 | RUN npm cache verify 7 | RUN npm rebuild node-sass 8 | RUN npm install -g @quasar/cli 9 | RUN npm install --progress=false 10 | COPY quasar-app /app/ 11 | RUN quasar build -m spa 12 | 13 | FROM --platform=linux/amd64 nginx:1.13.12-alpine as production 14 | COPY nginx/ecs/ecs.conf /etc/nginx/nginx.conf 15 | COPY --from=build-stage /app/dist/spa /dist/ 16 | EXPOSE 80 17 | CMD ["nginx", "-g", "daemon off;"] 18 | -------------------------------------------------------------------------------- /nginx/ecs/ecs.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | http { 9 | include /etc/nginx/mime.types; 10 | client_max_body_size 100m; 11 | 12 | server { 13 | listen 80; 14 | charset utf-8; 15 | 16 | root /dist/; 17 | index index.html; 18 | 19 | # frontend 20 | location / { 21 | try_files $uri $uri/ @rewrites; 22 | } 23 | 24 | # frontend rewrites 25 | location @rewrites { 26 | rewrite ^(.+)$ /index.html last; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nginx/prod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 as build-stage 2 | ARG VERSION 3 | ENV VERSION=$VERSION 4 | WORKDIR /app/ 5 | COPY quasar-app/package.json /app/ 6 | RUN npm cache verify 7 | RUN npm rebuild node-sass 8 | RUN npm install -g @quasar/cli 9 | RUN npm install --progress=false 10 | COPY quasar-app /app/ 11 | RUN quasar build -m spa 12 | 13 | # ci stage 14 | FROM --platform=linux/amd64 nginx:1.13.12-alpine as production 15 | COPY nginx/prod/prod.conf /etc/nginx/nginx.conf 16 | COPY --from=build-stage /app/dist/spa /dist/ 17 | EXPOSE 80 18 | CMD ["nginx", "-g", "daemon off;"] 19 | -------------------------------------------------------------------------------- /nuxt-app/.dockerignore: -------------------------------------------------------------------------------- 1 | .nuxt 2 | .output 3 | node_modules 4 | 5 | .gitignore 6 | README.md 7 | -------------------------------------------------------------------------------- /nuxt-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /nuxt-app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base stage 2 | ARG NODE_VERSION=20.18.0 3 | FROM node:${NODE_VERSION}-slim AS base 4 | 5 | ARG SOURCE_TAG 6 | ENV SOURCE_TAG=$SOURCE_TAG 7 | ENV NUXT_PUBLIC_APP_VERSION=$SOURCE_TAG 8 | 9 | ARG PORT=3000 10 | ENV PORT=$PORT 11 | 12 | WORKDIR /src 13 | 14 | 15 | FROM base AS build-prod 16 | 17 | COPY --link package.json package-lock.json ./ 18 | RUN npm install 19 | 20 | COPY --link . . 21 | RUN npm run build 22 | 23 | 24 | FROM base AS prod 25 | 26 | ENV NODE_ENV=production 27 | 28 | COPY --from=build-prod /src/.output /src/.output 29 | 30 | CMD [ "node", ".output/server/index.mjs" ] 31 | 32 | 33 | FROM base AS build-dev 34 | 35 | ENV NODE_ENV=development 36 | 37 | COPY --link package.json package-lock.json ./ 38 | RUN npm install 39 | 40 | 41 | FROM base AS dev 42 | 43 | COPY --from=build-dev /src/node_modules /src/node_modules 44 | 45 | CMD [ "npm", "run", "dev" ] 46 | -------------------------------------------------------------------------------- /nuxt-app/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "typescript": true, 5 | "tsConfigPath": ".nuxt/tsconfig.json", 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "assets/css/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "framework": "nuxt", 14 | "aliases": { 15 | "components": "@/components/ui", 16 | "utils": "@/lib/utils" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardContent } from './CardContent.vue' 3 | export { default as CardDescription } from './CardDescription.vue' 4 | export { default as CardFooter } from './CardFooter.vue' 5 | export { default as CardHeader } from './CardHeader.vue' 6 | export { default as CardTitle } from './CardTitle.vue' 7 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/FormControl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/FormDescription.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/FormItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/FormLabel.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/FormMessage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FormControl } from './FormControl.vue' 2 | export { default as FormDescription } from './FormDescription.vue' 3 | export { default as FormItem } from './FormItem.vue' 4 | export { default as FormLabel } from './FormLabel.vue' 5 | export { default as FormMessage } from './FormMessage.vue' 6 | export { FORM_ITEM_INJECTION_KEY } from './injectionKeys' 7 | export { Field as FormField, Form } from 'vee-validate' 8 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/injectionKeys.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | 3 | export const FORM_ITEM_INJECTION_KEY 4 | = Symbol() as InjectionKey 5 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/form/useFormField.ts: -------------------------------------------------------------------------------- 1 | import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate' 2 | import { inject } from 'vue' 3 | import { FORM_ITEM_INJECTION_KEY } from './injectionKeys' 4 | 5 | export function useFormField() { 6 | const fieldContext = inject(FieldContextKey) 7 | const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY) 8 | 9 | if (!fieldContext) 10 | throw new Error('useFormField should be used within ') 11 | 12 | const { name } = fieldContext 13 | const id = fieldItemContext 14 | 15 | const fieldState = { 16 | valid: useIsFieldValid(name), 17 | isDirty: useIsFieldDirty(name), 18 | isTouched: useIsFieldTouched(name), 19 | error: useFieldError(name), 20 | } 21 | 22 | return { 23 | id, 24 | name, 25 | formItemId: `${id}-form-item`, 26 | formDescriptionId: `${id}-form-item-description`, 27 | formMessageId: `${id}-form-item-message`, 28 | ...fieldState, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue' 2 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/NavigationMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/NavigationMenuIndicator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/NavigationMenuItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/NavigationMenuLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/NavigationMenuList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/NavigationMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 35 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/navigation-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority' 2 | 3 | export { default as NavigationMenu } from './NavigationMenu.vue' 4 | export { default as NavigationMenuContent } from './NavigationMenuContent.vue' 5 | export { default as NavigationMenuItem } from './NavigationMenuItem.vue' 6 | export { default as NavigationMenuLink } from './NavigationMenuLink.vue' 7 | export { default as NavigationMenuList } from './NavigationMenuList.vue' 8 | export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue' 9 | export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue' 10 | 11 | export const navigationMenuTriggerStyle = cva( 12 | 'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50', 13 | ) 14 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/popover/Popover.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/popover/PopoverTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Popover } from './Popover.vue' 2 | export { default as PopoverContent } from './PopoverContent.vue' 3 | export { default as PopoverTrigger } from './PopoverTrigger.vue' 4 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/Table.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableBody.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableCaption.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableCell.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableEmpty.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableHead.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/TableRow.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Table } from './Table.vue' 2 | export { default as TableBody } from './TableBody.vue' 3 | export { default as TableCaption } from './TableCaption.vue' 4 | export { default as TableCell } from './TableCell.vue' 5 | export { default as TableEmpty } from './TableEmpty.vue' 6 | export { default as TableFooter } from './TableFooter.vue' 7 | export { default as TableHead } from './TableHead.vue' 8 | export { default as TableHeader } from './TableHeader.vue' 9 | export { default as TableRow } from './TableRow.vue' 10 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/tabs/TabsContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/tabs/TabsList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from './Tabs.vue' 2 | export { default as TabsContent } from './TabsContent.vue' 3 | export { default as TabsList } from './TabsList.vue' 4 | export { default as TabsTrigger } from './TabsTrigger.vue' 5 | -------------------------------------------------------------------------------- /nuxt-app/components/ui/textarea/Textarea.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 |