├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── cookiecutter.json
├── dev-fsfcb-back.sh
├── dev-fsfcb-config.yml
├── dev-fsfcb.sh
├── hooks
└── post_gen_project.py
├── screenshot.png
├── scripts
├── discard-dev-files.sh
└── generate_cookiecutter_config.py
├── test.sh
├── testing-config.yml
└── {{cookiecutter.project_slug}}
├── .env
├── .gitignore
├── .gitlab-ci.yml
├── README.md
├── backend
├── .gitignore
├── app
│ ├── Pipfile
│ ├── app
│ │ ├── __init__.py
│ │ ├── api
│ │ │ ├── __init__.py
│ │ │ ├── api_v1
│ │ │ │ ├── __init__.py
│ │ │ │ ├── api.py
│ │ │ │ └── endpoints
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── items.py
│ │ │ │ │ ├── login.py
│ │ │ │ │ ├── roles.py
│ │ │ │ │ ├── users.py
│ │ │ │ │ └── utils.py
│ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ └── security.py
│ │ ├── backend_pre_start.py
│ │ ├── celeryworker_pre_start.py
│ │ ├── core
│ │ │ ├── __init__.py
│ │ │ ├── celery_app.py
│ │ │ ├── config.py
│ │ │ ├── jwt.py
│ │ │ └── security.py
│ │ ├── crud
│ │ │ ├── __init__.py
│ │ │ ├── item.py
│ │ │ ├── user.py
│ │ │ └── utils.py
│ │ ├── db
│ │ │ ├── __init__.py
│ │ │ ├── couchbase_utils.py
│ │ │ ├── database.py
│ │ │ ├── full_text_search_utils.py
│ │ │ └── init_db.py
│ │ ├── email-templates
│ │ │ ├── build
│ │ │ │ ├── new_account.html
│ │ │ │ ├── reset_password.html
│ │ │ │ └── test_email.html
│ │ │ └── src
│ │ │ │ ├── new_account.mjml
│ │ │ │ ├── reset_password.mjml
│ │ │ │ └── test_email.mjml
│ │ ├── main.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── item.py
│ │ │ ├── msg.py
│ │ │ ├── role.py
│ │ │ ├── token.py
│ │ │ └── user.py
│ │ ├── search_index_definitions
│ │ │ ├── items.json
│ │ │ ├── items_01.json
│ │ │ ├── users.json
│ │ │ └── users_01.json
│ │ ├── tests
│ │ │ ├── .gitignore
│ │ │ ├── __init__.py
│ │ │ ├── api
│ │ │ │ ├── __init__.py
│ │ │ │ └── api_v1
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── test_celery.py
│ │ │ │ │ ├── test_items.py
│ │ │ │ │ ├── test_login.py
│ │ │ │ │ └── test_users.py
│ │ │ ├── conftest.py
│ │ │ ├── crud
│ │ │ │ ├── __init__.py
│ │ │ │ ├── test_get_ids.py
│ │ │ │ ├── test_item.py
│ │ │ │ └── test_user.py
│ │ │ └── utils
│ │ │ │ ├── __init__.py
│ │ │ │ ├── item.py
│ │ │ │ ├── user.py
│ │ │ │ └── utils.py
│ │ ├── tests_pre_start.py
│ │ ├── utils.py
│ │ └── worker.py
│ ├── prestart.sh
│ ├── scripts
│ │ └── lint.sh
│ ├── tests-start.sh
│ └── worker-start.sh
├── backend.dockerfile
├── celeryworker.dockerfile
└── tests.dockerfile
├── cookiecutter-config-file.yml
├── docker-compose.deploy.build.yml
├── docker-compose.deploy.command.yml
├── docker-compose.deploy.images.yml
├── docker-compose.deploy.labels.yml
├── docker-compose.deploy.networks.yml
├── docker-compose.deploy.volumes-placement.yml
├── docker-compose.dev.build.yml
├── docker-compose.dev.command.yml
├── docker-compose.dev.env.yml
├── docker-compose.dev.labels.yml
├── docker-compose.dev.networks.yml
├── docker-compose.dev.ports.yml
├── docker-compose.dev.volumes.yml
├── docker-compose.shared.admin.yml
├── docker-compose.shared.base-images.yml
├── docker-compose.shared.depends.yml
├── docker-compose.shared.env.yml
├── docker-compose.test.yml
├── env-backend.env
├── env-couchbase.env
├── env-flower.env
├── env-sync-gateway.env
├── frontend
├── .dockerignore
├── .env
├── .gitignore
├── Dockerfile
├── README.md
├── babel.config.js
├── nginx-backend-not-found.conf
├── package.json
├── public
│ ├── favicon.ico
│ ├── img
│ │ └── icons
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── apple-touch-icon-120x120.png
│ │ │ ├── apple-touch-icon-152x152.png
│ │ │ ├── apple-touch-icon-180x180.png
│ │ │ ├── apple-touch-icon-60x60.png
│ │ │ ├── apple-touch-icon-76x76.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── msapplication-icon-144x144.png
│ │ │ ├── mstile-150x150.png
│ │ │ └── safari-pinned-tab.svg
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.vue
│ ├── api.ts
│ ├── assets
│ │ └── logo.png
│ ├── component-hooks.ts
│ ├── components
│ │ ├── NotificationsManager.vue
│ │ ├── RouterComponent.vue
│ │ └── UploadButton.vue
│ ├── env.ts
│ ├── interfaces
│ │ └── index.ts
│ ├── main.ts
│ ├── plugins
│ │ ├── vee-validate.ts
│ │ └── vuetify.ts
│ ├── registerServiceWorker.ts
│ ├── router.ts
│ ├── shims-tsx.d.ts
│ ├── shims-vue.d.ts
│ ├── store
│ │ ├── admin
│ │ │ ├── actions.ts
│ │ │ ├── getters.ts
│ │ │ ├── index.ts
│ │ │ ├── mutations.ts
│ │ │ └── state.ts
│ │ ├── index.ts
│ │ ├── main
│ │ │ ├── actions.ts
│ │ │ ├── getters.ts
│ │ │ ├── index.ts
│ │ │ ├── mutations.ts
│ │ │ └── state.ts
│ │ └── state.ts
│ ├── utils.ts
│ └── views
│ │ ├── Login.vue
│ │ ├── PasswordRecovery.vue
│ │ ├── ResetPassword.vue
│ │ └── main
│ │ ├── Dashboard.vue
│ │ ├── Main.vue
│ │ ├── Start.vue
│ │ ├── admin
│ │ ├── Admin.vue
│ │ ├── AdminUsers.vue
│ │ ├── CreateUser.vue
│ │ └── EditUser.vue
│ │ └── profile
│ │ ├── UserProfile.vue
│ │ ├── UserProfileEdit.vue
│ │ └── UserProfileEditPassword.vue
├── tests
│ └── unit
│ │ └── upload-button.spec.ts
├── tsconfig.json
├── tslint.json
└── vue.config.js
├── scripts
├── build-push.sh
├── build.sh
├── deploy.sh
├── test-local.sh
└── test.sh
└── sync-gateway
├── Dockerfile
├── create_config.py
├── entrypoint.sh
└── sync
└── sync-function.js
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | on:
2 | schedule:
3 | - cron: "0 0 * * *"
4 |
5 | jobs:
6 | issue-manager:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: tiangolo/issue-manager@master
10 | with:
11 | token: ${{ secrets.GITHUB_TOKEN }}
12 | config: >
13 | {
14 | "answered": {
15 | "users": ["tiangolo"],
16 | "delay": 864000,
17 | "message": "Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues."
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | testing-project
3 | .mypy_cache
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 |
3 | language: python
4 |
5 | install:
6 | - pip install cookiecutter
7 |
8 | services:
9 | - docker
10 |
11 | script:
12 | - bash ./test.sh
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Sebastián Ramírez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/cookiecutter.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_name": "Base Project",
3 | "project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}",
4 | "domain_main": "{{cookiecutter.project_slug}}.com",
5 | "domain_staging": "stag.{{cookiecutter.domain_main}}",
6 |
7 | "docker_swarm_stack_name_main": "{{cookiecutter.domain_main|replace('.', '-')}}",
8 | "docker_swarm_stack_name_staging": "{{cookiecutter.domain_staging|replace('.', '-')}}",
9 |
10 | "secret_key": "changethis",
11 | "first_superuser": "admin@{{cookiecutter.domain_main}}",
12 | "first_superuser_password": "changethis",
13 | "backend_cors_origins": "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, https://localhost, https://localhost:4200, https://localhost:3000, https://localhost:8080, http://dev.{{cookiecutter.domain_main}}, https://{{cookiecutter.domain_staging}}, https://{{cookiecutter.domain_main}}, http://local.dockertoolbox.tiangolo.com, http://localhost.tiangolo.com",
14 | "smtp_port": "587",
15 | "smtp_host": "",
16 | "smtp_user": "",
17 | "smtp_password": "",
18 | "smtp_emails_from_email": "info@{{cookiecutter.domain_main}}",
19 |
20 | "couchbase_user": "admin",
21 | "couchbase_password": "changethis",
22 |
23 | "couchbase_sync_gateway_cors": "http://localhost:4984, http://localhost:4985, http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://dev.{{cookiecutter.domain_main}}, https://{{cookiecutter.domain_staging}}, https://db.{{cookiecutter.domain_staging}}, https://{{cookiecutter.domain_main}}, https://db.{{cookiecutter.domain_main}}, http://local.dockertoolbox.tiangolo.com, http://local.dockertoolbox.tiangolo.com:4984, http://localhost.tiangolo.com, http://localhost.tiangolo.com:4984",
24 | "couchbase_sync_gateway_user": "sync",
25 | "couchbase_sync_gateway_password": "changethis",
26 |
27 | "traefik_constraint_tag": "{{cookiecutter.domain_main}}",
28 | "traefik_constraint_tag_staging": "{{cookiecutter.domain_staging}}",
29 | "traefik_public_network": "traefik-public",
30 | "traefik_public_constraint_tag": "traefik-public",
31 |
32 | "flower_auth": "admin:{{cookiecutter.first_superuser_password}}",
33 |
34 | "sentry_dsn": "",
35 |
36 | "docker_image_prefix": "",
37 |
38 | "docker_image_backend": "{{cookiecutter.docker_image_prefix}}backend",
39 | "docker_image_celeryworker": "{{cookiecutter.docker_image_prefix}}celeryworker",
40 | "docker_image_frontend": "{{cookiecutter.docker_image_prefix}}frontend",
41 | "docker_image_sync_gateway": "{{cookiecutter.docker_image_prefix}}sync-gateway",
42 |
43 | "_copy_without_render": [
44 | "frontend/src/**/*.html",
45 | "frontend/src/**/*.vue",
46 | "frontend/node_modules/*",
47 | "backend/app/app/email-templates/**"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/dev-fsfcb-back.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Run this script from outside the project, to integrate a dev-fsfcb project with changes and review modifications
4 |
5 | # Exit in case of error
6 | set -e
7 |
8 | if [ $(uname -s) = "Linux" ]; then
9 | echo "Remove __pycache__ files"
10 | sudo find ./dev-fsfcb/ -type d -name __pycache__ -exec rm -r {} \+
11 | fi
12 |
13 | rm -rf ./full-stack-fastapi-couchbase/\{\{cookiecutter.project_slug\}\}/*
14 |
15 | rsync -a --exclude=node_modules ./dev-fsfcb/* ./full-stack-fastapi-couchbase/\{\{cookiecutter.project_slug\}\}/
16 |
17 | rsync -a ./dev-fsfcb/{.env,.gitignore,.gitlab-ci.yml} ./full-stack-fastapi-couchbase/\{\{cookiecutter.project_slug\}\}/
18 |
--------------------------------------------------------------------------------
/dev-fsfcb-config.yml:
--------------------------------------------------------------------------------
1 | default_context:
2 | "project_name": "Dev FSFCB"
3 |
--------------------------------------------------------------------------------
/dev-fsfcb.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Run this script from outside the project, to generate a dev-fsfcb project
4 |
5 | # Exit in case of error
6 | set -e
7 |
8 | rm -rf ./dev-fsfcb
9 |
10 | cookiecutter --config-file ./full-stack-fastapi-couchbase/dev-fsfcb-config.yml --no-input -f ./full-stack-fastapi-couchbase
11 |
--------------------------------------------------------------------------------
/hooks/post_gen_project.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | path: Path
5 | for path in Path(".").glob("**/*.sh"):
6 | data = path.read_bytes()
7 | lf_data = data.replace(b"\r\n", b"\n")
8 | path.write_bytes(lf_data)
9 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/screenshot.png
--------------------------------------------------------------------------------
/scripts/discard-dev-files.sh:
--------------------------------------------------------------------------------
1 | rm -rf \{\{cookiecutter.project_slug\}\}/.git
2 | rm -rf \{\{cookiecutter.project_slug\}\}/frontend/node_modules
3 | rm -rf \{\{cookiecutter.project_slug\}\}/frontend/dist
4 | git checkout \{\{cookiecutter.project_slug\}\}/README.md
5 | git checkout \{\{cookiecutter.project_slug\}\}/.gitlab-ci.yml
6 | git checkout \{\{cookiecutter.project_slug\}\}/cookiecutter-config-file.yml
7 | git checkout \{\{cookiecutter.project_slug\}\}/docker-compose.deploy.networks.yml
8 | git checkout \{\{cookiecutter.project_slug\}\}/env-backend.env
9 | git checkout \{\{cookiecutter.project_slug\}\}/env-couchbase.env
10 | git checkout \{\{cookiecutter.project_slug\}\}/env-flower.env
11 | git checkout \{\{cookiecutter.project_slug\}\}/.env
12 | git checkout \{\{cookiecutter.project_slug\}\}/frontend/.env
13 | git checkout \{\{cookiecutter.project_slug\}\}/env-sync-gateway.env
14 |
--------------------------------------------------------------------------------
/scripts/generate_cookiecutter_config.py:
--------------------------------------------------------------------------------
1 | import json
2 | from collections import OrderedDict
3 | import oyaml as yaml
4 | from pathlib import Path
5 | cookie_path = Path('./cookiecutter.json')
6 | out_path = Path('./{{cookiecutter.project_slug}}/cookiecutter-config-file.yml')
7 |
8 | with open(cookie_path) as f:
9 | cookie_config = json.load(f)
10 | config_out = OrderedDict()
11 |
12 | for key, value in cookie_config.items():
13 | if key.startswith('_'):
14 | config_out[key] = value
15 | else:
16 | config_out[key] = '{{ cookiecutter.' + key + ' }}'
17 | config_out['_template'] = './'
18 |
19 | with open(out_path, 'w') as out_f:
20 | out_f.write(yaml.dump({'default_context': config_out}, line_break=None, width=200))
21 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | rm -rf ./testing-project
7 |
8 | cookiecutter --config-file ./testing-config.yml --no-input -f ./
9 |
10 | cd ./testing-project
11 |
12 | bash ./scripts/test.sh
13 |
14 | cd ../
15 |
--------------------------------------------------------------------------------
/testing-config.yml:
--------------------------------------------------------------------------------
1 | default_context:
2 | "project_name": "Testing Project"
3 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/.env:
--------------------------------------------------------------------------------
1 | COMPOSE_PATH_SEPARATOR=:
2 | COMPOSE_FILE=docker-compose.test.yml:docker-compose.shared.admin.yml:docker-compose.shared.base-images.yml:docker-compose.shared.depends.yml:docker-compose.shared.env.yml:docker-compose.dev.build.yml:docker-compose.dev.command.yml:docker-compose.dev.env.yml:docker-compose.dev.labels.yml:docker-compose.dev.networks.yml:docker-compose.dev.ports.yml:docker-compose.dev.volumes.yml
3 |
4 | DOMAIN=localhost
5 | # DOMAIN=local.dockertoolbox.tiangolo.com
6 | # DOMAIN=localhost.tiangolo.com
7 | # DOMAIN=dev.{{cookiecutter.domain_main}}
8 |
9 | TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}
10 | TRAEFIK_PUBLIC_NETWORK={{cookiecutter.traefik_public_network}}
11 | TRAEFIK_PUBLIC_TAG={{cookiecutter.traefik_public_constraint_tag}}
12 |
13 | DOCKER_IMAGE_BACKEND={{cookiecutter.docker_image_backend}}
14 | DOCKER_IMAGE_CELERYWORKER={{cookiecutter.docker_image_celeryworker}}
15 | DOCKER_IMAGE_FRONTEND={{cookiecutter.docker_image_frontend}}
16 | DOCKER_IMAGE_SYNC_GATEWAY={{cookiecutter.docker_image_sync_gateway}}
17 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .mypy_cache
3 | docker-stack.yml
4 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: tiangolo/docker-with-compose
2 |
3 | before_script:
4 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
5 | - pip install docker-auto-labels
6 |
7 | stages:
8 | - test
9 | - build
10 | - deploy
11 |
12 | tests:
13 | stage: test
14 | script:
15 | - sh ./scripts/test.sh
16 | tags:
17 | - build
18 | - test
19 |
20 | build-stag:
21 | stage: build
22 | script:
23 | - TAG=stag FRONTEND_ENV=staging sh ./scripts/build-push.sh
24 | only:
25 | - master
26 | tags:
27 | - build
28 | - test
29 |
30 | build-prod:
31 | stage: build
32 | script:
33 | - TAG=prod FRONTEND_ENV=production sh ./scripts/build-push.sh
34 | only:
35 | - production
36 | tags:
37 | - build
38 | - test
39 |
40 | deploy-stag:
41 | stage: deploy
42 | script:
43 | - >
44 | DOMAIN={{cookiecutter.domain_staging}}
45 | TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag_staging}}
46 | STACK_NAME={{cookiecutter.docker_swarm_stack_name_staging}}
47 | TAG=stag
48 | sh ./scripts/deploy.sh
49 | environment:
50 | name: staging
51 | url: https://{{cookiecutter.domain_staging}}
52 | only:
53 | - master
54 | tags:
55 | - swarm
56 | - stag
57 |
58 | deploy-prod:
59 | stage: deploy
60 | script:
61 | - >
62 | DOMAIN={{cookiecutter.domain_main}}
63 | TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}
64 | STACK_NAME={{cookiecutter.docker_swarm_stack_name_main}}
65 | TAG=prod
66 | sh ./scripts/deploy.sh
67 | environment:
68 | name: production
69 | url: https://{{cookiecutter.domain_main}}
70 | only:
71 | - production
72 | tags:
73 | - swarm
74 | - prod
75 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | mypy = "*"
8 | black = "*"
9 | jupyter = "*"
10 | isort = "*"
11 | autoflake = "*"
12 | flake8 = "*"
13 | pytest = "*"
14 | vulture = "*"
15 |
16 | [packages]
17 | fastapi = "*"
18 | uvicorn = "*"
19 | pyjwt = "*"
20 | python-multipart = "*"
21 | email-validator = "*"
22 | requests = "*"
23 | celery = "~=4.3"
24 | passlib = {extras = ["bcrypt"],version = "*"}
25 | tenacity = "*"
26 | pydantic = "*"
27 | couchbase = "*"
28 | emails = "*"
29 | raven = "*"
30 | jinja2 = "*"
31 |
32 | [requires]
33 | python_version = "3.6"
34 |
35 | [pipenv]
36 | allow_prereleases = true
37 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/api/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/api.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 |
3 | from app.api.api_v1.endpoints import items, login, roles, users, utils
4 |
5 | api_router = APIRouter()
6 | api_router.include_router(login.router, tags=["login"])
7 | api_router.include_router(roles.router, prefix="/roles", tags=["roles"])
8 | api_router.include_router(users.router, prefix="/users", tags=["users"])
9 | api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
10 | api_router.include_router(items.router, prefix="/items", tags=["items"])
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/items.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from fastapi import APIRouter, Depends, HTTPException
4 |
5 | from app import crud
6 | from app.api.utils.security import get_current_active_user
7 | from app.db.database import get_default_bucket
8 | from app.models.item import Item, ItemCreate, ItemUpdate
9 | from app.models.user import UserInDB
10 |
11 | router = APIRouter()
12 |
13 |
14 | @router.get("/", response_model=List[Item])
15 | def read_items(
16 | skip: int = 0,
17 | limit: int = 100,
18 | current_user: UserInDB = Depends(get_current_active_user),
19 | ):
20 | """
21 | Retrieve items.
22 |
23 | If superuser, all the items.
24 |
25 | If normal user, the items owned by this user.
26 | """
27 | bucket = get_default_bucket()
28 | if crud.user.is_superuser(current_user):
29 | docs = crud.item.get_multi(bucket, skip=skip, limit=limit)
30 | else:
31 | docs = crud.item.get_multi_by_owner(
32 | bucket=bucket, owner_username=current_user.username, skip=skip, limit=limit
33 | )
34 | return docs
35 |
36 |
37 | @router.get("/search/", response_model=List[Item])
38 | def search_items(
39 | q: str,
40 | skip: int = 0,
41 | limit: int = 100,
42 | current_user: UserInDB = Depends(get_current_active_user),
43 | ):
44 | """
45 | Search items, use Bleve Query String syntax:
46 | http://blevesearch.com/docs/Query-String-Query/
47 |
48 | For typeahead suffix with `*`. For example, a query with: `title:foo*` will match
49 | items containing `football`, `fool proof`, etc.
50 | """
51 | bucket = get_default_bucket()
52 | if crud.user.is_superuser(current_user):
53 | docs = crud.item.search(bucket=bucket, query_string=q, skip=skip, limit=limit)
54 | else:
55 | docs = crud.item.search_with_owner(
56 | bucket=bucket,
57 | query_string=q,
58 | username=current_user.username,
59 | skip=skip,
60 | limit=limit,
61 | )
62 | return docs
63 |
64 |
65 | @router.post("/", response_model=Item)
66 | def create_item(
67 | *, item_in: ItemCreate, current_user: UserInDB = Depends(get_current_active_user)
68 | ):
69 | """
70 | Create new item.
71 | """
72 | bucket = get_default_bucket()
73 | id = crud.utils.generate_new_id()
74 | doc = crud.item.upsert(
75 | bucket=bucket, id=id, doc_in=item_in, owner_username=current_user.username
76 | )
77 | return doc
78 |
79 |
80 | @router.put("/{id}", response_model=Item)
81 | def update_item(
82 | *,
83 | id: str,
84 | item_in: ItemUpdate,
85 | current_user: UserInDB = Depends(get_current_active_user),
86 | ):
87 | """
88 | Update an item.
89 | """
90 | bucket = get_default_bucket()
91 | doc = crud.item.get(bucket=bucket, id=id)
92 | if not doc:
93 | raise HTTPException(status_code=404, detail="Item not found")
94 | if not crud.user.is_superuser(current_user) and (
95 | doc.owner_username != current_user.username
96 | ):
97 | raise HTTPException(status_code=400, detail="Not enough permissions")
98 | doc = crud.item.update(
99 | bucket=bucket, id=id, doc_in=item_in, owner_username=doc.owner_username
100 | )
101 | return doc
102 |
103 |
104 | @router.get("/{id}", response_model=Item)
105 | def read_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
106 | """
107 | Get item by ID.
108 | """
109 | bucket = get_default_bucket()
110 | doc = crud.item.get(bucket=bucket, id=id)
111 | if not doc:
112 | raise HTTPException(status_code=404, detail="Item not found")
113 | if not crud.user.is_superuser(current_user) and (
114 | doc.owner_username != current_user.username
115 | ):
116 | raise HTTPException(status_code=400, detail="Not enough permissions")
117 | return doc
118 |
119 |
120 | @router.delete("/{id}", response_model=Item)
121 | def delete_item(id: str, current_user: UserInDB = Depends(get_current_active_user)):
122 | """
123 | Delete an item by ID.
124 | """
125 | bucket = get_default_bucket()
126 | doc = crud.item.get(bucket=bucket, id=id)
127 | if not doc:
128 | raise HTTPException(status_code=404, detail="Item not found")
129 | if not crud.user.is_superuser(current_user) and (
130 | doc.owner_username != current_user.username
131 | ):
132 | raise HTTPException(status_code=400, detail="Not enough permissions")
133 | doc = crud.item.remove(bucket=bucket, id=id)
134 | return doc
135 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/login.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from fastapi import APIRouter, Body, Depends, HTTPException
4 | from fastapi.security import OAuth2PasswordRequestForm
5 |
6 | from app import crud
7 | from app.api.utils.security import get_current_user
8 | from app.core import config
9 | from app.core.jwt import create_access_token
10 | from app.db.database import get_default_bucket
11 | from app.models.msg import Msg
12 | from app.models.token import Token
13 | from app.models.user import User, UserInDB, UserUpdate
14 | from app.utils import (
15 | generate_password_reset_token,
16 | send_reset_password_email,
17 | verify_password_reset_token,
18 | )
19 |
20 | router = APIRouter()
21 |
22 |
23 | @router.post("/login/access-token", response_model=Token)
24 | def login(form_data: OAuth2PasswordRequestForm = Depends()):
25 | """
26 | OAuth2 compatible token login, get an access token for future requests.
27 | """
28 | bucket = get_default_bucket()
29 | user = crud.user.authenticate(
30 | bucket, username=form_data.username, password=form_data.password
31 | )
32 | if not user:
33 | raise HTTPException(status_code=400, detail="Incorrect email or password")
34 | elif not crud.user.is_active(user):
35 | raise HTTPException(status_code=400, detail="Inactive user")
36 | access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
37 | return {
38 | "access_token": create_access_token(
39 | data={"username": user.username}, expires_delta=access_token_expires
40 | ),
41 | "token_type": "bearer",
42 | }
43 |
44 |
45 | @router.post("/login/test-token", response_model=User)
46 | def test_token(current_user: UserInDB = Depends(get_current_user)):
47 | """
48 | Test access token.
49 | """
50 | return current_user
51 |
52 |
53 | @router.post("/password-recovery/{username}", response_model=Msg)
54 | def recover_password(username: str):
55 | """
56 | Password Recovery.
57 | """
58 | bucket = get_default_bucket()
59 | user = crud.user.get(bucket, username=username)
60 |
61 | if not user:
62 | raise HTTPException(
63 | status_code=404,
64 | detail="The user with this username does not exist in the system.",
65 | )
66 | password_reset_token = generate_password_reset_token(username=username)
67 | send_reset_password_email(
68 | email_to=user.email, username=username, token=password_reset_token
69 | )
70 | return {"msg": "Password recovery email sent"}
71 |
72 |
73 | @router.post("/reset-password/", response_model=Msg)
74 | def reset_password(token: str = Body(...), new_password: str = Body(...)):
75 | """
76 | Reset password.
77 | """
78 | username = verify_password_reset_token(token)
79 | if not username:
80 | raise HTTPException(status_code=400, detail="Invalid token")
81 | bucket = get_default_bucket()
82 | user = crud.user.get(bucket, username=username)
83 | if not user:
84 | raise HTTPException(
85 | status_code=404,
86 | detail="The user with this username does not exist in the system.",
87 | )
88 | elif not crud.user.is_active(user):
89 | raise HTTPException(status_code=400, detail="Inactive user")
90 | user_in = UserUpdate(name=username, password=new_password)
91 | user = crud.user.update(bucket, username=username, user_in=user_in)
92 | return {"msg": "Password updated successfully"}
93 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/roles.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends
2 |
3 | from app import crud
4 | from app.api.utils.security import get_current_active_superuser
5 | from app.models.role import RoleEnum, Roles
6 | from app.models.user import UserInDB
7 |
8 | router = APIRouter()
9 |
10 |
11 | @router.get("/", response_model=Roles)
12 | def read_roles(current_user: UserInDB = Depends(get_current_active_superuser)):
13 | """
14 | Retrieve roles.
15 | """
16 | roles = crud.utils.ensure_enums_to_strs(RoleEnum)
17 | return {"roles": roles}
18 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/users.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from fastapi import APIRouter, Body, Depends, HTTPException
4 | from pydantic.networks import EmailStr
5 |
6 | from app import crud
7 | from app.api.utils.security import get_current_active_superuser, get_current_active_user
8 | from app.core import config
9 | from app.db.database import get_default_bucket
10 | from app.models.user import User, UserCreate, UserInDB, UserUpdate
11 | from app.utils import send_new_account_email
12 |
13 | router = APIRouter()
14 |
15 |
16 | @router.get("/", response_model=List[User])
17 | def read_users(
18 | skip: int = 0,
19 | limit: int = 100,
20 | current_user: UserInDB = Depends(get_current_active_superuser),
21 | ):
22 | """
23 | Retrieve users.
24 | """
25 | bucket = get_default_bucket()
26 | users = crud.user.get_multi(bucket, skip=skip, limit=limit)
27 | return users
28 |
29 |
30 | @router.get("/search/", response_model=List[User])
31 | def search_users(
32 | q: str,
33 | skip: int = 0,
34 | limit: int = 100,
35 | current_user: UserInDB = Depends(get_current_active_superuser),
36 | ):
37 | """
38 | Search users, use Bleve Query String syntax:
39 | http://blevesearch.com/docs/Query-String-Query/
40 |
41 | For typeahead suffix with `*`. For example, a query with: `email:johnd*` will match
42 | users with email `johndoe@example.com`, `johndid@example.net`, etc.
43 | """
44 | bucket = get_default_bucket()
45 | users = crud.user.search(bucket=bucket, query_string=q, skip=skip, limit=limit)
46 | return users
47 |
48 |
49 | @router.post("/", response_model=User)
50 | def create_user(
51 | *,
52 | user_in: UserCreate,
53 | current_user: UserInDB = Depends(get_current_active_superuser),
54 | ):
55 | """
56 | Create new user.
57 | """
58 | bucket = get_default_bucket()
59 | user = crud.user.get(bucket, username=user_in.username)
60 | if user:
61 | raise HTTPException(
62 | status_code=400,
63 | detail="The user with this username already exists in the system.",
64 | )
65 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
66 | if config.EMAILS_ENABLED and user_in.email:
67 | send_new_account_email(
68 | email_to=user_in.email, username=user_in.username, password=user_in.password
69 | )
70 | return user
71 |
72 |
73 | @router.put("/me", response_model=User)
74 | def update_user_me(
75 | *,
76 | password: str = Body(None),
77 | full_name: str = Body(None),
78 | email: EmailStr = Body(None),
79 | current_user: UserInDB = Depends(get_current_active_user),
80 | ):
81 | """
82 | Update own user.
83 | """
84 | user_in = UserUpdate(**current_user.dict())
85 | if password is not None:
86 | user_in.password = password
87 | if full_name is not None:
88 | user_in.full_name = full_name
89 | if email is not None:
90 | user_in.email = email
91 | bucket = get_default_bucket()
92 | user = crud.user.update(bucket, username=current_user.username, user_in=user_in)
93 | return user
94 |
95 |
96 | @router.get("/me", response_model=User)
97 | def read_user_me(current_user: UserInDB = Depends(get_current_active_user)):
98 | """
99 | Get current user.
100 | """
101 | return current_user
102 |
103 |
104 | @router.post("/open", response_model=User)
105 | def create_user_open(
106 | *,
107 | username: str = Body(...),
108 | password: str = Body(...),
109 | email: EmailStr = Body(None),
110 | full_name: str = Body(None),
111 | ):
112 | """
113 | Create new user without the need to be logged in.
114 | """
115 | if not config.USERS_OPEN_REGISTRATION:
116 | raise HTTPException(
117 | status_code=403,
118 | detail="Open user resistration is forbidden on this server",
119 | )
120 | bucket = get_default_bucket()
121 | user = crud.user.get(bucket, username=username)
122 | if user:
123 | raise HTTPException(
124 | status_code=400,
125 | detail="The user with this username already exists in the system",
126 | )
127 | user_in = UserCreate(
128 | username=username, password=password, email=email, full_name=full_name
129 | )
130 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
131 | if config.EMAILS_ENABLED and user_in.email:
132 | send_new_account_email(
133 | email_to=user_in.email, username=user_in.username, password=user_in.password
134 | )
135 | return user
136 |
137 |
138 | @router.get("/{username}", response_model=User)
139 | def read_user(username: str, current_user: UserInDB = Depends(get_current_active_user)):
140 | """
141 | Get a specific user by username (email).
142 | """
143 | bucket = get_default_bucket()
144 | user = crud.user.get(bucket, username=username)
145 | if user == current_user:
146 | return user
147 | if not crud.user.is_superuser(current_user):
148 | raise HTTPException(
149 | status_code=400, detail="The user doesn't have enough privileges"
150 | )
151 | return user
152 |
153 |
154 | @router.put("/{username}", response_model=User)
155 | def update_user(
156 | *,
157 | username: str,
158 | user_in: UserUpdate,
159 | current_user: UserInDB = Depends(get_current_active_superuser),
160 | ):
161 | """
162 | Update a user.
163 | """
164 | bucket = get_default_bucket()
165 | user = crud.user.get(bucket, username=username)
166 |
167 | if not user:
168 | raise HTTPException(
169 | status_code=404,
170 | detail="The user with this username does not exist in the system",
171 | )
172 | user = crud.user.update(bucket, username=username, user_in=user_in)
173 | return user
174 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/api_v1/endpoints/utils.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends
2 | from pydantic.networks import EmailStr
3 |
4 | from app.api.utils.security import get_current_active_superuser
5 | from app.core.celery_app import celery_app
6 | from app.models.msg import Msg
7 | from app.models.user import UserInDB
8 | from app.utils import send_test_email
9 |
10 | router = APIRouter()
11 |
12 |
13 | @router.post("/test-celery/", response_model=Msg, status_code=201)
14 | def test_celery(
15 | msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
16 | ):
17 | """
18 | Test Celery worker.
19 | """
20 | celery_app.send_task("app.worker.test_celery", args=[msg.msg])
21 | return {"msg": "Word received"}
22 |
23 |
24 | @router.post("/test-email/", response_model=Msg, status_code=201)
25 | def test_email(
26 | email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
27 | ):
28 | """
29 | Test emails.
30 | """
31 | send_test_email(email_to=email_to)
32 | return {"msg": "Test email sent"}
33 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/api/utils/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/api/utils/security.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | from fastapi import HTTPException, Security
3 | from fastapi.security import OAuth2PasswordBearer
4 | from jwt import PyJWTError
5 | from starlette.status import HTTP_403_FORBIDDEN
6 |
7 | from app import crud
8 | from app.core import config
9 | from app.core.jwt import ALGORITHM
10 | from app.db.database import get_default_bucket
11 | from app.models.token import TokenPayload
12 | from app.models.user import UserInDB
13 |
14 | reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
15 |
16 |
17 | def get_current_user(token: str = Security(reusable_oauth2)):
18 | try:
19 | payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
20 | token_data = TokenPayload(**payload)
21 | except PyJWTError:
22 | raise HTTPException(
23 | status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
24 | )
25 | bucket = get_default_bucket()
26 | user = crud.user.get(bucket, username=token_data.username)
27 | if not user:
28 | raise HTTPException(status_code=404, detail="User not found")
29 | return user
30 |
31 |
32 | def get_current_active_user(current_user: UserInDB = Security(get_current_user)):
33 | if not crud.user.is_active(current_user):
34 | raise HTTPException(status_code=400, detail="Inactive user")
35 | return current_user
36 |
37 |
38 | def get_current_active_superuser(current_user: UserInDB = Security(get_current_user)):
39 | if not crud.user.is_superuser(current_user):
40 | raise HTTPException(
41 | status_code=400, detail="The user doesn't have enough privileges"
42 | )
43 | return current_user
44 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/backend_pre_start.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
4 |
5 | from app.db.init_db import init_db
6 |
7 | logging.basicConfig(level=logging.INFO)
8 | logger = logging.getLogger(__name__)
9 |
10 | max_tries = 60 * 5 # 5 minutes
11 | wait_seconds = 1
12 |
13 |
14 | @retry(
15 | stop=stop_after_attempt(max_tries),
16 | wait=wait_fixed(wait_seconds),
17 | before=before_log(logger, logging.INFO),
18 | after=after_log(logger, logging.WARN),
19 | )
20 | def init():
21 | try:
22 | init_db()
23 | except Exception as e:
24 | logger.error(e)
25 | raise e
26 |
27 |
28 | def main():
29 | logger.info("Initializing service")
30 | init()
31 | logger.info("Service finished initializing")
32 |
33 |
34 | if __name__ == "__main__":
35 | main()
36 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/celeryworker_pre_start.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
4 |
5 | from app.db.database import get_default_bucket
6 |
7 | logging.basicConfig(level=logging.INFO)
8 | logger = logging.getLogger(__name__)
9 |
10 | max_tries = 60 * 5 # 5 minutes
11 | wait_seconds = 1
12 |
13 |
14 | @retry(
15 | stop=stop_after_attempt(max_tries),
16 | wait=wait_fixed(wait_seconds),
17 | before=before_log(logger, logging.INFO),
18 | after=after_log(logger, logging.WARN),
19 | )
20 | def init():
21 | try:
22 | # Check Couchbase is awake
23 | bucket = get_default_bucket()
24 | logger.info(
25 | f"Database bucket connection established with bucket object: {bucket}"
26 | )
27 | except Exception as e:
28 | logger.error(e)
29 | raise e
30 |
31 |
32 | def main():
33 | logger.info("Initializing service")
34 | init()
35 | logger.info("Service finished initializing")
36 |
37 |
38 | if __name__ == "__main__":
39 | main()
40 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/core/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/core/celery_app.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 |
3 | celery_app = Celery("worker", broker="amqp://guest@queue//")
4 |
5 | celery_app.conf.task_routes = {"app.worker.test_celery": "main-queue"}
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/core/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def getenv_boolean(var_name, default_value=False):
5 | result = default_value
6 | env_value = os.getenv(var_name)
7 | if env_value is not None:
8 | result = env_value.upper() in ("TRUE", "1")
9 | return result
10 |
11 |
12 | API_V1_STR = "/api/v1"
13 |
14 | SECRET_KEY = os.getenvb(b"SECRET_KEY")
15 | if not SECRET_KEY:
16 | SECRET_KEY = os.urandom(32)
17 |
18 | ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days
19 |
20 | SERVER_NAME = os.getenv("SERVER_NAME")
21 | SERVER_HOST = os.getenv("SERVER_HOST")
22 | BACKEND_CORS_ORIGINS = os.getenv(
23 | "BACKEND_CORS_ORIGINS"
24 | ) # a string of origins separated by commas, e.g: "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://dev.couchbase-project.com, https://stag.couchbase-project.com, https://couchbase-project.com, http://local.dockertoolbox.tiangolo.com"
25 | PROJECT_NAME = os.getenv("PROJECT_NAME")
26 | SENTRY_DSN = os.getenv("SENTRY_DSN")
27 |
28 | # Couchbase server settings
29 | COUCHBASE_MEMORY_QUOTA_MB = os.getenv("COUCHBASE_MEMORY_QUOTA_MB", "256")
30 | COUCHBASE_INDEX_MEMORY_QUOTA_MB = os.getenv("COUCHBASE_INDEX_MEMORY_QUOTA_MB" "256")
31 | COUCHBASE_FTS_MEMORY_QUOTA_MB = os.getenv("COUCHBASE_FTS_MEMORY_QUOTA_MB", "256")
32 | COUCHBASE_HOST = os.getenv("COUCHBASE_HOST", "couchbase")
33 | COUCHBASE_PORT = os.getenv("COUCHBASE_PORT", "8091")
34 | COUCHBASE_FULL_TEXT_PORT = os.getenv("COUCHBASE_FULL_TEXT_PORT", "8094")
35 | COUCHBASE_ENTERPRISE = getenv_boolean("COUCHBASE_ENTERPRISE")
36 | COUCHBASE_USER = os.getenv("COUCHBASE_USER", "Administrator")
37 | COUCHBASE_PASSWORD = os.getenv("COUCHBASE_PASSWORD", "password")
38 | COUCHBASE_BUCKET_NAME = os.getenv("COUCHBASE_BUCKET_NAME", "app")
39 |
40 | COUCHBASE_SYNC_GATEWAY_HOST = os.getenv("COUCHBASE_SYNC_GATEWAY_HOST", "sync-gateway")
41 | COUCHBASE_SYNC_GATEWAY_PORT = os.getenv("COUCHBASE_SYNC_GATEWAY_PORT", "4985")
42 | COUCHBASE_SYNC_GATEWAY_USER = os.getenv("COUCHBASE_SYNC_GATEWAY_USER")
43 | COUCHBASE_SYNC_GATEWAY_PASSWORD = os.getenv("COUCHBASE_SYNC_GATEWAY_PASSWORD")
44 | COUCHBASE_SYNC_GATEWAY_DATABASE = os.getenv("COUCHBASE_SYNC_GATEWAY_DATABASE")
45 |
46 | # Couchbase query timeouts
47 | COUCHBASE_DURABILITY_TIMEOUT_SECS = 60.0
48 | COUCHBASE_OPERATION_TIMEOUT_SECS = 30.0
49 | COUCHBASE_N1QL_TIMEOUT_SECS = 300.0
50 |
51 |
52 | # Couchbase Sync Gateway settings
53 | COUCHBASE_CORS_ORIGINS = os.getenv("COUCHBASE_CORS_ORIGINS")
54 | # a string of origins separated by commas, e.g: "http://localhost:5984, http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://dev.couchbase-project.com, https://stag.couchbase-project.com, https://db.stag.couchbase-project.com, https://couchbase-project.com, https://db.couchbase-project.com, http://local.dockertoolbox.tiangolo.com, http://local.dockertoolbox.tiangolo.com:5984"
55 | COUCHBASE_AUTH_TIMEOUT = ACCESS_TOKEN_EXPIRE_MINUTES * 60
56 |
57 | COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR = "/app/app/search_index_definitions/"
58 |
59 | SMTP_TLS = getenv_boolean("SMTP_TLS", True)
60 | SMTP_PORT = None
61 | _SMTP_PORT = os.getenv("SMTP_PORT")
62 | if _SMTP_PORT is not None:
63 | SMTP_PORT = int(_SMTP_PORT)
64 | SMTP_HOST = os.getenv("SMTP_HOST")
65 | SMTP_USER = os.getenv("SMTP_USER")
66 | SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
67 | EMAILS_FROM_EMAIL = os.getenv("EMAILS_FROM_EMAIL")
68 | EMAILS_FROM_NAME = PROJECT_NAME
69 | EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48
70 | EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build"
71 | EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL
72 |
73 | ROLE_SUPERUSER = "superuser"
74 |
75 | FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER")
76 | FIRST_SUPERUSER_PASSWORD = os.getenv("FIRST_SUPERUSER_PASSWORD")
77 |
78 | USERS_OPEN_REGISTRATION = getenv_boolean("USERS_OPEN_REGISTRATION")
79 |
80 | EMAIL_TEST_USER = "test@example.com"
81 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/core/jwt.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | import jwt
4 |
5 | from app.core import config
6 |
7 | ALGORITHM = "HS256"
8 | access_token_jwt_subject = "access"
9 |
10 |
11 | def create_access_token(*, data: dict, expires_delta: timedelta = None):
12 | to_encode = data.copy()
13 | if expires_delta:
14 | expire = datetime.utcnow() + expires_delta
15 | else:
16 | expire = datetime.utcnow() + timedelta(minutes=15)
17 | to_encode.update({"exp": expire, "sub": access_token_jwt_subject})
18 | encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=ALGORITHM)
19 | return encoded_jwt
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/core/security.py:
--------------------------------------------------------------------------------
1 | from passlib.context import CryptContext
2 |
3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4 |
5 |
6 | def verify_password(plain_password: str, hashed_password: str):
7 | return pwd_context.verify(plain_password, hashed_password)
8 |
9 |
10 | def get_password_hash(password: str):
11 | return pwd_context.hash(password)
12 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/crud/__init__.py:
--------------------------------------------------------------------------------
1 | from . import item, user, utils
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/crud/item.py:
--------------------------------------------------------------------------------
1 | from couchbase.bucket import Bucket
2 | from couchbase.n1ql import CONSISTENCY_REQUEST, N1QLQuery
3 |
4 | from app.core import config
5 | from app.models.config import ITEM_DOC_TYPE
6 | from app.models.item import ItemCreate, ItemInDB, ItemUpdate
7 |
8 | from . import utils
9 |
10 | # Same as file name /app/app/search_index_definitions/items.json
11 | full_text_index_name = "items"
12 |
13 |
14 | def get_doc_id(id: str):
15 | return f"{ITEM_DOC_TYPE}::{id}"
16 |
17 |
18 | def get(bucket: Bucket, *, id: str):
19 | doc_id = get_doc_id(id)
20 | return utils.get_doc(bucket=bucket, doc_id=doc_id, doc_model=ItemInDB)
21 |
22 |
23 | def upsert(
24 | bucket: Bucket,
25 | *,
26 | id: str,
27 | doc_in: ItemCreate,
28 | owner_username: str,
29 | persist_to=0,
30 | ttl=0,
31 | ):
32 | doc_id = get_doc_id(id)
33 | doc = ItemInDB(**doc_in.dict(), id=id, owner_username=owner_username)
34 | return utils.upsert(
35 | bucket=bucket, doc_id=doc_id, doc_in=doc, persist_to=persist_to, ttl=ttl
36 | )
37 |
38 |
39 | def update(
40 | bucket: Bucket,
41 | *,
42 | id: str,
43 | doc_in: ItemUpdate,
44 | owner_username=None,
45 | persist_to=0,
46 | ttl=0,
47 | ):
48 | doc_id = get_doc_id(id=id)
49 | doc = get(bucket, id=id)
50 | doc = doc.copy(update=doc_in.dict(skip_defaults=True))
51 | if owner_username is not None:
52 | doc.owner_username = owner_username
53 | return utils.upsert(
54 | bucket=bucket, doc_id=doc_id, doc_in=doc, persist_to=persist_to, ttl=ttl
55 | )
56 |
57 |
58 | def remove(bucket: Bucket, *, id: str, persist_to=0):
59 | doc_id = get_doc_id(id)
60 | return utils.remove(
61 | bucket=bucket, doc_id=doc_id, doc_model=ItemInDB, persist_to=persist_to
62 | )
63 |
64 |
65 | def get_multi(bucket: Bucket, *, skip=0, limit=100):
66 | return utils.get_docs(
67 | bucket=bucket,
68 | doc_type=ITEM_DOC_TYPE,
69 | doc_model=ItemInDB,
70 | skip=skip,
71 | limit=limit,
72 | )
73 |
74 |
75 | def get_multi_by_owner(bucket: Bucket, *, owner_username: str, skip=0, limit=100):
76 | query_str = f"SELECT *, META().id as doc_id FROM {config.COUCHBASE_BUCKET_NAME} WHERE type = $type AND owner_username = $owner_username LIMIT $limit OFFSET $skip;"
77 | q = N1QLQuery(
78 | query_str,
79 | bucket=config.COUCHBASE_BUCKET_NAME,
80 | type=ITEM_DOC_TYPE,
81 | owner_username=owner_username,
82 | limit=limit,
83 | skip=skip,
84 | )
85 | q.consistency = CONSISTENCY_REQUEST
86 | doc_results = bucket.n1ql_query(q)
87 | return utils.doc_results_to_model(doc_results, doc_model=ItemInDB)
88 |
89 |
90 | def search(bucket: Bucket, *, query_string: str, skip=0, limit=100):
91 | docs = utils.search_get_docs(
92 | bucket=bucket,
93 | query_string=query_string,
94 | index_name=full_text_index_name,
95 | doc_model=ItemInDB,
96 | skip=skip,
97 | limit=limit,
98 | )
99 | return docs
100 |
101 |
102 | def search_with_owner(
103 | bucket: Bucket, *query_string: str, username: str, skip=0, limit=100
104 | ):
105 | username_filter = f"owner_username:{username}"
106 | if username_filter not in query_string:
107 | query_string = f"{query_string} {username_filter}"
108 | docs = utils.search_get_docs(
109 | bucket=bucket,
110 | query_string=query_string,
111 | index_name=full_text_index_name,
112 | doc_model=ItemInDB,
113 | skip=skip,
114 | limit=limit,
115 | )
116 | return docs
117 |
118 |
119 | def search_get_search_results_to_docs(
120 | bucket: Bucket, *, query_string: str, skip=0, limit=100
121 | ):
122 | docs = utils.search_by_type_get_results_to_docs(
123 | bucket=bucket,
124 | query_string=query_string,
125 | index_name=full_text_index_name,
126 | doc_type=ITEM_DOC_TYPE,
127 | doc_model=ItemInDB,
128 | skip=skip,
129 | limit=limit,
130 | )
131 | return docs
132 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/crud/user.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from couchbase.bucket import Bucket
3 | from couchbase.n1ql import CONSISTENCY_REQUEST, N1QLQuery
4 | from fastapi.encoders import jsonable_encoder
5 |
6 | from app.core import config
7 | from app.core.security import get_password_hash, verify_password
8 | from app.models.config import USERPROFILE_DOC_TYPE
9 | from app.models.role import RoleEnum
10 | from app.models.user import UserCreate, UserInDB, UserSyncIn, UserUpdate
11 |
12 | from . import utils
13 |
14 | # Same as file name /app/app/search_index_definitions/users.json
15 | full_text_index_name = "users"
16 |
17 |
18 | def get_doc_id(username: str):
19 | return f"{USERPROFILE_DOC_TYPE}::{username}"
20 |
21 |
22 | def get(bucket: Bucket, *, username: str):
23 | doc_id = get_doc_id(username)
24 | return utils.get_doc(bucket=bucket, doc_id=doc_id, doc_model=UserInDB)
25 |
26 |
27 | def get_by_email(bucket: Bucket, *, email: str):
28 | query_str = f"SELECT *, META().id as doc_id FROM {config.COUCHBASE_BUCKET_NAME} WHERE type = $type AND email = $email;"
29 | q = N1QLQuery(
30 | query_str,
31 | bucket=config.COUCHBASE_BUCKET_NAME,
32 | type=USERPROFILE_DOC_TYPE,
33 | email=email,
34 | )
35 | q.consistency = CONSISTENCY_REQUEST
36 | doc_results = bucket.n1ql_query(q)
37 | users = utils.doc_results_to_model(doc_results, doc_model=UserInDB)
38 | if not users:
39 | return None
40 | return users[0]
41 |
42 |
43 | def insert_sync_gateway(user: UserSyncIn):
44 | name = user.name
45 | url = f"http://{config.COUCHBASE_SYNC_GATEWAY_HOST}:{config.COUCHBASE_SYNC_GATEWAY_PORT}/{config.COUCHBASE_SYNC_GATEWAY_DATABASE}/_user/{name}"
46 | data = jsonable_encoder(user)
47 | response = requests.put(url, json=data)
48 | return response.status_code == 200 or response.status_code == 201
49 |
50 |
51 | def update_sync_gateway(user: UserSyncIn):
52 | name = user.name
53 | url = f"http://{config.COUCHBASE_SYNC_GATEWAY_HOST}:{config.COUCHBASE_SYNC_GATEWAY_PORT}/{config.COUCHBASE_SYNC_GATEWAY_DATABASE}/_user/{name}"
54 | if user.password:
55 | data = jsonable_encoder(user)
56 | else:
57 | data = jsonable_encoder(user, exclude={"password"})
58 | response = requests.put(url, json=data)
59 | return response.status_code == 200 or response.status_code == 201
60 |
61 |
62 | def upsert_in_db(bucket: Bucket, *, user_in: UserCreate, persist_to=0):
63 | user_doc_id = get_doc_id(user_in.username)
64 | passwordhash = get_password_hash(user_in.password)
65 | user = UserInDB(**user_in.dict(), hashed_password=passwordhash)
66 | doc_data = jsonable_encoder(user)
67 | with bucket.durability(
68 | persist_to=persist_to, timeout=config.COUCHBASE_DURABILITY_TIMEOUT_SECS
69 | ):
70 | bucket.upsert(user_doc_id, doc_data)
71 | return user
72 |
73 |
74 | def update_in_db(bucket: Bucket, *, username: str, user_in: UserUpdate, persist_to=0):
75 | user_doc_id = get_doc_id(username)
76 | stored_user = get(bucket, username=username)
77 | stored_user = stored_user.copy(update=user_in.dict(skip_defaults=True))
78 | if user_in.password:
79 | passwordhash = get_password_hash(user_in.password)
80 | stored_user.hashed_password = passwordhash
81 | data = jsonable_encoder(stored_user)
82 | with bucket.durability(
83 | persist_to=persist_to, timeout=config.COUCHBASE_DURABILITY_TIMEOUT_SECS
84 | ):
85 | bucket.upsert(user_doc_id, data)
86 | return stored_user
87 |
88 |
89 | def upsert(bucket: Bucket, *, user_in: UserCreate, persist_to=0):
90 | user = upsert_in_db(bucket, user_in=user_in, persist_to=persist_to)
91 | user_in_sync = UserSyncIn(**user_in.dict(), name=user_in.username)
92 | assert insert_sync_gateway(user_in_sync)
93 | return user
94 |
95 |
96 | def update(bucket: Bucket, *, username: str, user_in: UserUpdate, persist_to=0):
97 | user = update_in_db(
98 | bucket, username=username, user_in=user_in, persist_to=persist_to
99 | )
100 | user_in_sync_data = user.dict()
101 | user_in_sync_data.update({"name": user.username})
102 | if user_in.password:
103 | user_in_sync_data.update({"password": user_in.password})
104 | user_in_sync = UserSyncIn(**user_in_sync_data)
105 | assert update_sync_gateway(user_in_sync)
106 | return user
107 |
108 |
109 | def authenticate(bucket: Bucket, *, username: str, password: str):
110 | user = get(bucket, username=username)
111 | if not user:
112 | return None
113 | if not verify_password(password, user.hashed_password):
114 | return None
115 | return user
116 |
117 |
118 | def is_active(user: UserInDB):
119 | return not user.disabled
120 |
121 |
122 | def is_superuser(user: UserInDB):
123 | return RoleEnum.superuser.value in utils.ensure_enums_to_strs(
124 | user.admin_roles or []
125 | )
126 |
127 |
128 | def get_multi(bucket: Bucket, *, skip=0, limit=100):
129 | users = utils.get_docs(
130 | bucket=bucket,
131 | doc_type=USERPROFILE_DOC_TYPE,
132 | doc_model=UserInDB,
133 | skip=skip,
134 | limit=limit,
135 | )
136 | return users
137 |
138 |
139 | def search(bucket: Bucket, *, query_string: str, skip=0, limit=100):
140 | users = utils.search_get_docs(
141 | bucket=bucket,
142 | query_string=query_string,
143 | index_name=full_text_index_name,
144 | doc_model=UserInDB,
145 | doc_type=USERPROFILE_DOC_TYPE,
146 | skip=skip,
147 | limit=limit,
148 | )
149 | return users
150 |
151 |
152 | def search_get_search_results_to_docs(
153 | bucket: Bucket, *, query_string: str, skip=0, limit=100
154 | ):
155 | users = utils.search_by_type_get_results_to_docs(
156 | bucket=bucket,
157 | query_string=query_string,
158 | index_name=full_text_index_name,
159 | doc_type=USERPROFILE_DOC_TYPE,
160 | doc_model=UserInDB,
161 | skip=skip,
162 | limit=limit,
163 | )
164 | return users
165 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/db/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/db/database.py:
--------------------------------------------------------------------------------
1 | from couchbase import LOCKMODE_WAIT
2 | from couchbase.bucket import Bucket
3 | from couchbase.cluster import Cluster, PasswordAuthenticator
4 |
5 | from app.core.config import (
6 | COUCHBASE_BUCKET_NAME,
7 | COUCHBASE_HOST,
8 | COUCHBASE_N1QL_TIMEOUT_SECS,
9 | COUCHBASE_OPERATION_TIMEOUT_SECS,
10 | COUCHBASE_PASSWORD,
11 | COUCHBASE_PORT,
12 | COUCHBASE_USER,
13 | )
14 | from app.db.couchbase_utils import get_cluster_couchbase_url
15 |
16 |
17 | def get_default_bucket():
18 | return get_bucket(
19 | COUCHBASE_USER,
20 | COUCHBASE_PASSWORD,
21 | COUCHBASE_BUCKET_NAME,
22 | host=COUCHBASE_HOST,
23 | port=COUCHBASE_PORT,
24 | )
25 |
26 |
27 | def get_cluster(username: str, password: str, host="couchbase", port="8091"):
28 | # cluster_url="couchbase://couchbase"
29 | # username = "Administrator"
30 | # password = "password"
31 | cluster_url = get_cluster_couchbase_url(host=host, port=port)
32 | cluster = Cluster(cluster_url)
33 | authenticator = PasswordAuthenticator(username, password)
34 | cluster.authenticate(authenticator)
35 | return cluster
36 |
37 |
38 | def get_bucket(
39 | username: str,
40 | password: str,
41 | bucket_name: str,
42 | host="couchbase",
43 | port="8091",
44 | timeout: float = COUCHBASE_OPERATION_TIMEOUT_SECS,
45 | n1ql_timeout: float = COUCHBASE_N1QL_TIMEOUT_SECS,
46 | ):
47 | cluster = get_cluster(username, password, host=host, port=port)
48 | bucket: Bucket = cluster.open_bucket(bucket_name, lockmode=LOCKMODE_WAIT)
49 | bucket.timeout = timeout
50 | bucket.n1ql_timeout = n1ql_timeout
51 | return bucket
52 |
53 |
54 | def ensure_create_primary_index(bucket: Bucket):
55 | manager = bucket.bucket_manager()
56 | return manager.n1ql_index_create_primary(ignore_exists=True)
57 |
58 |
59 | def ensure_create_type_index(bucket: Bucket):
60 | manager = bucket.bucket_manager()
61 | return manager.n1ql_index_create("idx_type", ignore_exists=True, fields=["type"])
62 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/db/full_text_search_utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path, PurePath
3 | from typing import Any, Dict
4 |
5 | import requests
6 | from requests.auth import HTTPBasicAuth
7 |
8 | from app.core.config import (
9 | COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
10 | COUCHBASE_PASSWORD,
11 | COUCHBASE_USER,
12 | )
13 |
14 |
15 | def get_index(
16 | index_name: str,
17 | *,
18 | username: str = COUCHBASE_USER,
19 | password: str = COUCHBASE_PASSWORD,
20 | host="couchbase",
21 | port="8094",
22 | ):
23 | full_text_url = f"http://{host}:{port}"
24 | index_url = f"{full_text_url}/api/index/{index_name}"
25 | auth = HTTPBasicAuth(username, password)
26 | response = requests.get(index_url, auth=auth)
27 | if response.status_code == 400:
28 | content = response.json()
29 | error = content.get("error")
30 | if error == "rest_auth: preparePerms, err: index not found":
31 | return None
32 | raise ValueError(error)
33 | elif response.status_code == 200:
34 | content = response.json()
35 | assert (
36 | content.get("status") == "ok"
37 | ), "Expected a status OK communicating with Full Text Search"
38 | index_def = content.get("indexDef")
39 | return index_def
40 | raise ValueError(response.text)
41 |
42 |
43 | def create_index(
44 | index_definition: Dict[str, Any],
45 | *,
46 | reset_uuids=True,
47 | username: str = COUCHBASE_USER,
48 | password: str = COUCHBASE_PASSWORD,
49 | host="couchbase",
50 | port="8094",
51 | ):
52 | index_name = index_definition.get("name")
53 | assert index_name, "An index name is required as key in an index definition"
54 | if reset_uuids:
55 | index_definition.update({"uuid": "", "sourceUUID": ""})
56 | full_text_url = f"http://{host}:{port}"
57 | index_url = f"{full_text_url}/api/index/{index_name}"
58 | auth = HTTPBasicAuth(username, password)
59 | response = requests.put(index_url, auth=auth, json=index_definition)
60 | content = response.json()
61 | if response.status_code == 400:
62 | error = content.get("error")
63 | if (
64 | "cannot create index because an index with the same name already exists:"
65 | in error
66 | ):
67 | raise ValueError(error)
68 | else:
69 | raise ValueError(error)
70 | elif response.status_code == 200:
71 | assert (
72 | content.get("status") == "ok"
73 | ), "Expected a status OK communicating with Full Text Search"
74 | return True
75 | raise ValueError(response.text)
76 |
77 |
78 | def ensure_create_full_text_indexes(
79 | index_dir=COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
80 | username: str = COUCHBASE_USER,
81 | password: str = COUCHBASE_PASSWORD,
82 | host="couchbase",
83 | port="8094",
84 | ):
85 | file_path: PurePath
86 | for file_path in Path(index_dir).iterdir():
87 | if file_path.name.endswith(".json"):
88 | with open(file_path) as f:
89 | index_definition = json.load(f)
90 | name = index_definition.get("name")
91 | assert name, "A full text search index definition must have a name field"
92 | current_index = get_index(
93 | index_name=name,
94 | username=username,
95 | password=password,
96 | host=host,
97 | port=port,
98 | )
99 | if not current_index:
100 | assert create_index(
101 | index_definition=index_definition,
102 | username=username,
103 | password=password,
104 | host=host,
105 | port=port,
106 | ), "Full Text Search index could not be created"
107 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/db/init_db.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from app import crud
4 | from app.core import config
5 | from app.db.couchbase_utils import (
6 | config_couchbase,
7 | ensure_create_bucket,
8 | ensure_create_couchbase_user,
9 | get_cluster_http_url,
10 | )
11 | from app.db.database import (
12 | ensure_create_primary_index,
13 | ensure_create_type_index,
14 | get_bucket,
15 | )
16 | from app.db.full_text_search_utils import ensure_create_full_text_indexes
17 | from app.models.role import RoleEnum
18 | from app.models.user import UserCreate
19 |
20 |
21 | def init_db():
22 | cluster_url = get_cluster_http_url(
23 | host=config.COUCHBASE_HOST, port=config.COUCHBASE_PORT
24 | )
25 | logging.info("before config_couchbase")
26 | config_couchbase(
27 | username=config.COUCHBASE_USER,
28 | password=config.COUCHBASE_PASSWORD,
29 | host=config.COUCHBASE_HOST,
30 | port=config.COUCHBASE_PORT,
31 | )
32 | logging.info("after config_couchbase")
33 | # COUCHBASE_USER="Administrator"
34 | # COUCHBASE_PASSWORD="password"
35 | logging.info("before ensure_create_bucket")
36 | ensure_create_bucket(
37 | cluster_url=cluster_url,
38 | username=config.COUCHBASE_USER,
39 | password=config.COUCHBASE_PASSWORD,
40 | bucket_name=config.COUCHBASE_BUCKET_NAME,
41 | ram_quota_mb=config.COUCHBASE_MEMORY_QUOTA_MB,
42 | )
43 | logging.info("after ensure_create_bucket")
44 | logging.info("before get_bucket")
45 | bucket = get_bucket(
46 | config.COUCHBASE_USER,
47 | config.COUCHBASE_PASSWORD,
48 | config.COUCHBASE_BUCKET_NAME,
49 | host=config.COUCHBASE_HOST,
50 | port=config.COUCHBASE_PORT,
51 | )
52 | logging.info("after get_bucket")
53 | logging.info("before ensure_create_primary_index")
54 | ensure_create_primary_index(bucket)
55 | logging.info("after ensure_create_primary_index")
56 | logging.info("before ensure_create_type_index")
57 | ensure_create_type_index(bucket)
58 | logging.info("after ensure_create_type_index")
59 | logging.info("before ensure_create_full_text_indexes")
60 | ensure_create_full_text_indexes(
61 | index_dir=config.COUCHBASE_FULL_TEXT_INDEX_DEFINITIONS_DIR,
62 | username=config.COUCHBASE_USER,
63 | password=config.COUCHBASE_PASSWORD,
64 | host=config.COUCHBASE_HOST,
65 | port=config.COUCHBASE_FULL_TEXT_PORT,
66 | )
67 | logging.info("after ensure_create_full_text_indexes")
68 | logging.info("before ensure_create_couchbase_app_user sync")
69 | ensure_create_couchbase_user(
70 | cluster_url=cluster_url,
71 | username=config.COUCHBASE_USER,
72 | password=config.COUCHBASE_PASSWORD,
73 | new_user_id=config.COUCHBASE_SYNC_GATEWAY_USER,
74 | new_user_password=config.COUCHBASE_SYNC_GATEWAY_PASSWORD,
75 | )
76 | logging.info("after ensure_create_couchbase_app_user sync")
77 | logging.info("before upsert_user first superuser")
78 | user_in = UserCreate(
79 | username=config.FIRST_SUPERUSER,
80 | password=config.FIRST_SUPERUSER_PASSWORD,
81 | email=config.FIRST_SUPERUSER,
82 | admin_roles=[RoleEnum.superuser],
83 | admin_channels=[config.FIRST_SUPERUSER],
84 | )
85 | crud.user.upsert(bucket, user_in=user_in, persist_to=1)
86 | logging.info("after upsert_user first superuser")
87 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/new_account.html:
--------------------------------------------------------------------------------
1 |
| {{ project_name }} - New Account | You have a new account: | Username: {{ username }} | Password: {{ password }} | | |
|
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/email-templates/build/test_email.html:
--------------------------------------------------------------------------------
1 | | {{ project_name }} | Test email for: {{ email }} |
|
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/new_account.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ project_name }} - New Account
7 | You have a new account:
8 | Username: {{ username }}
9 | Password: {{ password }}
10 | Go to Dashboard
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/reset_password.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ project_name }} - Password Recovery
7 | We received a request to recover the password for user {{ username }}
8 | with email {{ email }}
9 | Reset your password by clicking the button below:
10 | Reset Password
11 | Or open the following link:
12 | {{ link }}
13 |
14 | The reset password link / button will expire in {{ valid_hours }} hours.
15 | If you didn't request a password recovery you can disregard this email.
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/email-templates/src/test_email.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ project_name }}
7 | Test email for: {{ email }}
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from starlette.middleware.cors import CORSMiddleware
3 |
4 | from app.api.api_v1.api import api_router
5 | from app.core import config
6 |
7 | app = FastAPI(title=config.PROJECT_NAME, openapi_url="/api/v1/openapi.json")
8 |
9 | # CORS
10 | origins = []
11 |
12 | # Set all CORS enabled origins
13 | if config.BACKEND_CORS_ORIGINS:
14 | origins_raw = config.BACKEND_CORS_ORIGINS.split(",")
15 | for origin in origins_raw:
16 | use_origin = origin.strip()
17 | origins.append(use_origin)
18 | app.add_middleware(
19 | CORSMiddleware,
20 | allow_origins=origins,
21 | allow_credentials=True,
22 | allow_methods=["*"],
23 | allow_headers=["*"],
24 | ),
25 |
26 | app.include_router(api_router, prefix=config.API_V1_STR)
27 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/models/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/config.py:
--------------------------------------------------------------------------------
1 | USERPROFILE_DOC_TYPE = "userprofile"
2 | ITEM_DOC_TYPE = "item"
3 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/item.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | from app.models.config import ITEM_DOC_TYPE
4 |
5 |
6 | # Shared properties
7 | class ItemBase(BaseModel):
8 | title: str = None
9 | description: str = None
10 |
11 |
12 | # Properties to receive on item creation
13 | class ItemCreate(ItemBase):
14 | title: str
15 |
16 |
17 | # Properties to receive on item update
18 | class ItemUpdate(ItemBase):
19 | pass
20 |
21 |
22 | # Properties to return to client
23 | class Item(ItemBase):
24 | id: str
25 | title: str
26 | owner_username: str
27 |
28 |
29 | # Properties properties stored in DB
30 | class ItemInDB(ItemBase):
31 | type: str = ITEM_DOC_TYPE
32 | id: str
33 | title: str
34 | owner_username: str
35 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/msg.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class Msg(BaseModel):
5 | msg: str
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/role.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import List
3 |
4 | from pydantic import BaseModel
5 |
6 | from app.core.config import ROLE_SUPERUSER
7 |
8 |
9 | class RoleEnum(Enum):
10 | superuser = ROLE_SUPERUSER
11 |
12 |
13 | class Roles(BaseModel):
14 | roles: List[RoleEnum]
15 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/token.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class Token(BaseModel):
5 | access_token: str
6 | token_type: str
7 |
8 |
9 | class TokenPayload(BaseModel):
10 | username: str = None
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/models/user.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Union
2 |
3 | from pydantic import BaseModel
4 |
5 | from app.models.config import USERPROFILE_DOC_TYPE
6 | from app.models.role import RoleEnum
7 |
8 |
9 | # Shared properties in Couchbase and Sync Gateway
10 | class UserBase(BaseModel):
11 | email: Optional[str] = None
12 | admin_roles: Optional[List[Union[str, RoleEnum]]] = None
13 | admin_channels: Optional[List[Union[str, RoleEnum]]] = None
14 | disabled: Optional[bool] = None
15 |
16 |
17 | # Shared properties in Couchbase
18 | class UserBaseInDB(UserBase):
19 | username: Optional[str] = None
20 | full_name: Optional[str] = None
21 |
22 |
23 | # Properties to receive via API on creation
24 | class UserCreate(UserBaseInDB):
25 | username: str
26 | password: str
27 | admin_roles: List[Union[str, RoleEnum]] = []
28 | admin_channels: List[Union[str, RoleEnum]] = []
29 | disabled: bool = False
30 |
31 |
32 | # Properties to receive via API on update
33 | class UserUpdate(UserBaseInDB):
34 | password: Optional[str] = None
35 |
36 |
37 | # Additional properties to return via API
38 | class User(UserBaseInDB):
39 | pass
40 |
41 |
42 | # Additional properties stored in DB
43 | class UserInDB(UserBaseInDB):
44 | type: str = USERPROFILE_DOC_TYPE
45 | hashed_password: str
46 | username: str
47 |
48 |
49 | # Additional properties in Sync Gateway
50 | class UserSyncIn(UserBase):
51 | name: str
52 | password: Optional[str] = None
53 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "items",
3 | "type": "fulltext-alias",
4 | "params": {
5 | "targets": {
6 | "items_01": {}
7 | }
8 | },
9 | "sourceType": "nil",
10 | "sourceName": "",
11 | "sourceUUID": "",
12 | "sourceParams": null,
13 | "planParams": {},
14 | "uuid": ""
15 | }
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/items_01.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "items_01",
3 | "type": "fulltext-index",
4 | "params": {
5 | "mapping": {
6 | "types": {
7 | "item": {
8 | "enabled": true,
9 | "dynamic": false,
10 | "properties": {
11 | "owner_username": {
12 | "enabled": true,
13 | "dynamic": false,
14 | "fields": [
15 | {
16 | "name": "owner_username",
17 | "type": "text",
18 | "analyzer": "keyword",
19 | "store": false,
20 | "index": true,
21 | "include_term_vectors": true,
22 | "include_in_all": true
23 | }
24 | ]
25 | },
26 | "description": {
27 | "enabled": true,
28 | "dynamic": false,
29 | "fields": [
30 | {
31 | "name": "description",
32 | "type": "text",
33 | "store": false,
34 | "index": true,
35 | "include_term_vectors": true,
36 | "include_in_all": true
37 | }
38 | ]
39 | },
40 | "title": {
41 | "enabled": true,
42 | "dynamic": false,
43 | "fields": [
44 | {
45 | "name": "title",
46 | "type": "text",
47 | "store": false,
48 | "index": true,
49 | "include_term_vectors": true,
50 | "include_in_all": true
51 | }
52 | ]
53 | },
54 | "type": {
55 | "enabled": true,
56 | "dynamic": false,
57 | "fields": [
58 | {
59 | "name": "type",
60 | "type": "text",
61 | "store": false,
62 | "index": true,
63 | "include_term_vectors": false,
64 | "include_in_all": false
65 | }
66 | ]
67 | },
68 | "id": {
69 | "enabled": true,
70 | "dynamic": false,
71 | "fields": [
72 | {
73 | "name": "id",
74 | "type": "text",
75 | "store": false,
76 | "index": true,
77 | "include_term_vectors": false,
78 | "include_in_all": false
79 | }
80 | ]
81 | }
82 | }
83 | }
84 | },
85 | "default_mapping": {
86 | "enabled": false,
87 | "dynamic": true
88 | },
89 | "default_type": "_default",
90 | "default_analyzer": "standard",
91 | "default_datetime_parser": "dateTimeOptional",
92 | "default_field": "_all",
93 | "store_dynamic": false,
94 | "index_dynamic": true
95 | },
96 | "store": {
97 | "indexType": "scorch",
98 | "kvStoreName": ""
99 | },
100 | "doc_config": {
101 | "mode": "type_field",
102 | "type_field": "type",
103 | "docid_prefix_delim": "",
104 | "docid_regexp": ""
105 | }
106 | },
107 | "sourceType": "couchbase",
108 | "sourceName": "app",
109 | "sourceUUID": "",
110 | "sourceParams": {},
111 | "planParams": {
112 | "maxPartitionsPerPIndex": 171,
113 | "numReplicas": 0
114 | },
115 | "uuid": ""
116 | }
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/search_index_definitions/users.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "users",
3 | "type": "fulltext-alias",
4 | "params": {
5 | "targets": {
6 | "users_01": {}
7 | }
8 | },
9 | "sourceType": "nil",
10 | "sourceName": "",
11 | "sourceUUID": "",
12 | "sourceParams": null,
13 | "planParams": {},
14 | "uuid": ""
15 | }
16 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/tests/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/tests/api/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_celery.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from app.core import config
4 | from app.tests.utils.utils import get_server_api
5 |
6 |
7 | def test_celery_worker_test(superuser_token_headers):
8 | server_api = get_server_api()
9 | data = {"msg": "test"}
10 | r = requests.post(
11 | f"{server_api}{config.API_V1_STR}/utils/test-celery/",
12 | json=data,
13 | headers=superuser_token_headers,
14 | )
15 | response = r.json()
16 | assert response["msg"] == "Word received"
17 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_items.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from app.core import config
4 | from app.tests.utils.item import create_random_item
5 | from app.tests.utils.utils import get_server_api
6 |
7 |
8 | def test_create_item(superuser_token_headers):
9 | server_api = get_server_api()
10 | data = {"title": "Foo", "description": "Fighters"}
11 | response = requests.post(
12 | f"{server_api}{config.API_V1_STR}/items/",
13 | headers=superuser_token_headers,
14 | json=data,
15 | )
16 | content = response.json()
17 | assert content["title"] == data["title"]
18 | assert content["description"] == data["description"]
19 | assert "id" in content
20 | assert "owner_username" in content
21 |
22 |
23 | def test_read_item(superuser_token_headers):
24 | item = create_random_item()
25 | server_api = get_server_api()
26 | response = requests.get(
27 | f"{server_api}{config.API_V1_STR}/items/{item.id}",
28 | headers=superuser_token_headers,
29 | )
30 | content = response.json()
31 | assert content["title"] == item.title
32 | assert content["description"] == item.description
33 | assert content["id"] == item.id
34 | assert content["owner_username"] == item.owner_username
35 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_login.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from app.core import config
4 | from app.tests.utils.utils import get_server_api
5 |
6 |
7 | def test_get_access_token():
8 | server_api = get_server_api()
9 | login_data = {
10 | "username": config.FIRST_SUPERUSER,
11 | "password": config.FIRST_SUPERUSER_PASSWORD,
12 | }
13 | r = requests.post(
14 | f"{server_api}{config.API_V1_STR}/login/access-token", data=login_data
15 | )
16 | tokens = r.json()
17 | assert r.status_code == 200
18 | assert "access_token" in tokens
19 | assert tokens["access_token"]
20 |
21 |
22 | def test_use_access_token(superuser_token_headers):
23 | server_api = get_server_api()
24 | r = requests.post(
25 | f"{server_api}{config.API_V1_STR}/login/test-token",
26 | headers=superuser_token_headers,
27 | )
28 | result = r.json()
29 | assert r.status_code == 200
30 | assert "username" in result
31 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/api/api_v1/test_users.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from app import crud
4 | from app.core import config
5 | from app.db.database import get_default_bucket
6 | from app.models.user import UserCreate
7 | from app.tests.utils.user import user_authentication_headers
8 | from app.tests.utils.utils import get_server_api, random_lower_string
9 |
10 |
11 | def test_get_users_superuser_me(superuser_token_headers):
12 | server_api = get_server_api()
13 | r = requests.get(
14 | f"{server_api}{config.API_V1_STR}/users/me", headers=superuser_token_headers
15 | )
16 | current_user = r.json()
17 | assert current_user
18 | assert current_user["disabled"] is False
19 | assert "superuser" in current_user["admin_roles"]
20 | assert current_user["username"] == config.FIRST_SUPERUSER
21 |
22 |
23 | def test_get_users_normal_user_me(normal_user_token_headers):
24 | server_api = get_server_api()
25 | r = requests.get(
26 | f"{server_api}{config.API_V1_STR}/users/me", headers=normal_user_token_headers
27 | )
28 | current_user = r.json()
29 | assert current_user
30 | assert current_user["disabled"] is False
31 | assert "superuser" not in current_user["admin_roles"]
32 | assert current_user["email"] == config.EMAIL_TEST_USER
33 |
34 |
35 | def test_create_user_new_email(superuser_token_headers):
36 | server_api = get_server_api()
37 | username = random_lower_string()
38 | password = random_lower_string()
39 | data = {"username": username, "password": password}
40 | r = requests.post(
41 | f"{server_api}{config.API_V1_STR}/users/",
42 | headers=superuser_token_headers,
43 | json=data,
44 | )
45 | assert 200 <= r.status_code < 300
46 | created_user = r.json()
47 | bucket = get_default_bucket()
48 | user = crud.user.get(bucket, username=username)
49 | assert user.username == created_user["username"]
50 |
51 |
52 | def test_get_existing_user(superuser_token_headers):
53 | server_api = get_server_api()
54 | username = random_lower_string()
55 | password = random_lower_string()
56 | user_in = UserCreate(username=username, email=username, password=password)
57 | bucket = get_default_bucket()
58 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
59 | r = requests.get(
60 | f"{server_api}{config.API_V1_STR}/users/{username}",
61 | headers=superuser_token_headers,
62 | )
63 | assert 200 <= r.status_code < 300
64 | api_user = r.json()
65 | user = crud.user.get(bucket, username=username)
66 | assert user.username == api_user["username"]
67 |
68 |
69 | def test_create_user_existing_username(superuser_token_headers):
70 | server_api = get_server_api()
71 | username = random_lower_string()
72 | # username = email
73 | password = random_lower_string()
74 | user_in = UserCreate(username=username, email=username, password=password)
75 | bucket = get_default_bucket()
76 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
77 | data = {"username": username, "password": password}
78 | r = requests.post(
79 | f"{server_api}{config.API_V1_STR}/users/",
80 | headers=superuser_token_headers,
81 | json=data,
82 | )
83 | created_user = r.json()
84 | assert r.status_code == 400
85 | assert "_id" not in created_user
86 |
87 |
88 | def test_create_user_by_normal_user(normal_user_token_headers):
89 | server_api = get_server_api()
90 | username = random_lower_string()
91 | password = random_lower_string()
92 | data = {"username": username, "password": password}
93 | r = requests.post(
94 | f"{server_api}{config.API_V1_STR}/users/", headers=normal_user_token_headers, json=data
95 | )
96 | assert r.status_code == 400
97 |
98 |
99 | def test_retrieve_users(superuser_token_headers):
100 | server_api = get_server_api()
101 | username = random_lower_string()
102 | password = random_lower_string()
103 | user_in = UserCreate(username=username, email=username, password=password)
104 | bucket = get_default_bucket()
105 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
106 |
107 | username2 = random_lower_string()
108 | password2 = random_lower_string()
109 | user_in2 = UserCreate(username=username2, email=username2, password=password2)
110 | user2 = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
111 |
112 | r = requests.get(
113 | f"{server_api}{config.API_V1_STR}/users/", headers=superuser_token_headers
114 | )
115 | all_users = r.json()
116 |
117 | assert len(all_users) > 1
118 | for user in all_users:
119 | assert "username" in user
120 | assert "admin_roles" in user
121 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from app.core import config
4 | from app.tests.utils.utils import get_server_api, get_superuser_token_headers
5 | from app.tests.utils.user import authentication_token_from_email
6 |
7 |
8 | @pytest.fixture(scope="module")
9 | def server_api():
10 | return get_server_api()
11 |
12 |
13 | @pytest.fixture(scope="module")
14 | def superuser_token_headers():
15 | return get_superuser_token_headers()
16 |
17 |
18 | @pytest.fixture(scope="module")
19 | def normal_user_token_headers():
20 | return authentication_token_from_email(config.EMAIL_TEST_USER)
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_get_ids.py:
--------------------------------------------------------------------------------
1 | from app import crud
2 |
3 |
4 | def test_get_user_id():
5 | username = "johndoe@example.com"
6 | user_id = crud.user.get_doc_id(username)
7 | assert user_id == "userprofile::johndoe@example.com"
8 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_item.py:
--------------------------------------------------------------------------------
1 | from app import crud
2 | from app.db.database import get_default_bucket
3 | from app.models.config import ITEM_DOC_TYPE
4 | from app.models.item import ItemCreate, ItemUpdate
5 | from app.tests.utils.user import create_random_user
6 | from app.tests.utils.utils import random_lower_string
7 |
8 |
9 | def test_create_item():
10 | title = random_lower_string()
11 | description = random_lower_string()
12 | id = crud.utils.generate_new_id()
13 | item_in = ItemCreate(title=title, description=description)
14 | bucket = get_default_bucket()
15 | user = create_random_user()
16 | item = crud.item.upsert(
17 | bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
18 | )
19 | assert item.id == id
20 | assert item.type == ITEM_DOC_TYPE
21 | assert item.title == title
22 | assert item.description == description
23 | assert item.owner_username == user.username
24 |
25 |
26 | def test_get_item():
27 | title = random_lower_string()
28 | description = random_lower_string()
29 | id = crud.utils.generate_new_id()
30 | item_in = ItemCreate(title=title, description=description)
31 | bucket = get_default_bucket()
32 | user = create_random_user()
33 | item = crud.item.upsert(
34 | bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
35 | )
36 | stored_item = crud.item.get(bucket=bucket, id=id)
37 | assert item.id == stored_item.id
38 | assert item.title == stored_item.title
39 | assert item.description == stored_item.description
40 | assert item.owner_username == stored_item.owner_username
41 |
42 |
43 | def test_update_item():
44 | title = random_lower_string()
45 | description = random_lower_string()
46 | id = crud.utils.generate_new_id()
47 | item_in = ItemCreate(title=title, description=description)
48 | bucket = get_default_bucket()
49 | user = create_random_user()
50 | item = crud.item.upsert(
51 | bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
52 | )
53 | description2 = random_lower_string()
54 | item_update = ItemUpdate(description=description2)
55 | item2 = crud.item.update(
56 | bucket=bucket,
57 | id=id,
58 | doc_in=item_update,
59 | owner_username=item.owner_username,
60 | persist_to=1,
61 | )
62 | assert item.id == item2.id
63 | assert item.title == item2.title
64 | assert item.description == description
65 | assert item2.description == description2
66 | assert item.owner_username == item2.owner_username
67 |
68 |
69 | def test_delete_item():
70 | title = random_lower_string()
71 | description = random_lower_string()
72 | id = crud.utils.generate_new_id()
73 | item_in = ItemCreate(title=title, description=description)
74 | bucket = get_default_bucket()
75 | user = create_random_user()
76 | item = crud.item.upsert(
77 | bucket=bucket, id=id, doc_in=item_in, owner_username=user.username, persist_to=1
78 | )
79 | item2 = crud.item.remove(bucket=bucket, id=id, persist_to=1)
80 | item3 = crud.item.get(bucket=bucket, id=id)
81 | assert item3 is None
82 | assert item2.id == id
83 | assert item2.title == title
84 | assert item2.description == description
85 | assert item2.owner_username == user.username
86 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/crud/test_user.py:
--------------------------------------------------------------------------------
1 | from fastapi.encoders import jsonable_encoder
2 |
3 | from app import crud
4 | from app.db.database import get_default_bucket
5 | from app.models.role import RoleEnum
6 | from app.models.user import UserCreate
7 | from app.tests.utils.utils import random_lower_string
8 |
9 |
10 | def test_create_user():
11 | email = random_lower_string()
12 | password = random_lower_string()
13 | user_in = UserCreate(username=email, email=email, password=password)
14 | bucket = get_default_bucket()
15 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
16 | assert hasattr(user, "username")
17 | assert user.username == email
18 | assert hasattr(user, "hashed_password")
19 | assert hasattr(user, "type")
20 | assert user.type == "userprofile"
21 |
22 |
23 | def test_authenticate_user():
24 | email = random_lower_string()
25 | password = random_lower_string()
26 | user_in = UserCreate(username=email, email=email, password=password)
27 | bucket = get_default_bucket()
28 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
29 | authenticated_user = crud.user.authenticate(
30 | bucket, username=user_in.username, password=password
31 | )
32 | assert authenticated_user
33 | assert user.username == authenticated_user.username
34 |
35 |
36 | def test_not_authenticate_user():
37 | email = random_lower_string()
38 | password = random_lower_string()
39 | bucket = get_default_bucket()
40 | user = crud.user.authenticate(bucket, username=email, password=password)
41 | assert user is None
42 |
43 |
44 | def test_check_if_user_is_active():
45 | email = random_lower_string()
46 | password = random_lower_string()
47 | user_in = UserCreate(username=email, email=email, password=password)
48 | bucket = get_default_bucket()
49 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
50 | is_active = crud.user.is_active(user)
51 | assert is_active is True
52 |
53 |
54 | def test_check_if_user_is_active_inactive():
55 | email = random_lower_string()
56 | password = random_lower_string()
57 | user_in = UserCreate(username=email, email=email, password=password, disabled=True)
58 | bucket = get_default_bucket()
59 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
60 | is_active = crud.user.is_active(user)
61 | assert is_active is False
62 |
63 |
64 | def test_check_if_user_is_superuser():
65 | email = random_lower_string()
66 | password = random_lower_string()
67 | user_in = UserCreate(
68 | username=email, email=email, password=password, admin_roles=[RoleEnum.superuser]
69 | )
70 | bucket = get_default_bucket()
71 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
72 | is_superuser = crud.user.is_superuser(user)
73 | assert is_superuser is True
74 |
75 |
76 | def test_check_if_user_is_superuser_normal_user():
77 | username = random_lower_string()
78 | password = random_lower_string()
79 | user_in = UserCreate(username=username, email=username, password=password)
80 | bucket = get_default_bucket()
81 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
82 | is_superuser = crud.user.is_superuser(user)
83 | assert is_superuser is False
84 |
85 |
86 | def test_get_user():
87 | password = random_lower_string()
88 | username = random_lower_string()
89 | user_in = UserCreate(
90 | username=username,
91 | email=username,
92 | password=password,
93 | admin_roles=[RoleEnum.superuser],
94 | )
95 | bucket = get_default_bucket()
96 | user = crud.user.upsert(bucket=bucket, user_in=user_in, persist_to=1)
97 | user_2 = crud.user.get(bucket, username=username)
98 | assert user.username == user_2.username
99 | assert jsonable_encoder(user) == jsonable_encoder(user_2)
100 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/__init__.py
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/item.py:
--------------------------------------------------------------------------------
1 | from app import crud
2 | from app.db.database import get_default_bucket
3 | from app.models.item import ItemCreate
4 | from app.tests.utils.user import create_random_user
5 | from app.tests.utils.utils import random_lower_string
6 |
7 |
8 | def create_random_item(owner_username: str = None):
9 | if owner_username is None:
10 | user = create_random_user()
11 | owner_username = user.username
12 | title = random_lower_string()
13 | description = random_lower_string()
14 | id = crud.utils.generate_new_id()
15 | item_in = ItemCreate(title=title, description=description, id=id)
16 | bucket = get_default_bucket()
17 | return crud.item.upsert(
18 | bucket=bucket,
19 | id=id,
20 | doc_in=item_in,
21 | owner_username=owner_username,
22 | persist_to=1,
23 | )
24 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/user.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from app import crud
4 | from app.core import config
5 | from app.db.database import get_default_bucket
6 | from app.models.user import UserCreate, UserUpdate
7 | from app.tests.utils.utils import random_lower_string, get_server_api
8 |
9 |
10 | def user_authentication_headers(server_api, email, password):
11 | data = {"username": email, "password": password}
12 |
13 | r = requests.post(f"{server_api}{config.API_V1_STR}/login/access-token", data=data)
14 | response = r.json()
15 | auth_token = response["access_token"]
16 | headers = {"Authorization": f"Bearer {auth_token}"}
17 | return headers
18 |
19 |
20 | def create_random_user():
21 | email = random_lower_string()
22 | password = random_lower_string()
23 | user_in = UserCreate(username=email, email=email, password=password)
24 | bucket = get_default_bucket()
25 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
26 | return user
27 |
28 |
29 | def authentication_token_from_email(email):
30 | """
31 | Return a valid token for the user with given email.
32 |
33 | If the user doesn't exist it is created first.
34 | """
35 | password = random_lower_string()
36 | bucket = get_default_bucket()
37 |
38 | user = crud.user.get_by_email(bucket, email=email)
39 | if not user:
40 | user_in = UserCreate(username=email, email=email, password=password)
41 | user = crud.user.upsert(bucket, user_in=user_in, persist_to=1)
42 | else:
43 | user_in = UserUpdate(password=password)
44 | user = crud.user.update(bucket, user=user, user_in=user_in)
45 |
46 | return user_authentication_headers(get_server_api(), email, password)
47 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests/utils/utils.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | import requests
5 |
6 | from app.core import config
7 |
8 |
9 | def random_lower_string():
10 | return "".join(random.choices(string.ascii_lowercase, k=32))
11 |
12 |
13 | def get_server_api():
14 | server_name = f"http://{config.SERVER_NAME}"
15 | return server_name
16 |
17 |
18 | def get_superuser_token_headers():
19 | server_api = get_server_api()
20 | login_data = {
21 | "username": config.FIRST_SUPERUSER,
22 | "password": config.FIRST_SUPERUSER_PASSWORD,
23 | }
24 | r = requests.post(
25 | f"{server_api}{config.API_V1_STR}/login/access-token", data=login_data
26 | )
27 | tokens = r.json()
28 | a_token = tokens["access_token"]
29 | headers = {"Authorization": f"Bearer {a_token}"}
30 | # superuser_token_headers = headers
31 | return headers
32 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/tests_pre_start.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
4 |
5 | from app.db.database import get_default_bucket
6 | from app.tests.api.api_v1.test_login import test_get_access_token
7 |
8 | logging.basicConfig(level=logging.INFO)
9 | logger = logging.getLogger(__name__)
10 |
11 | max_tries = 60 * 5 # 5 minutes
12 | wait_seconds = 1
13 |
14 |
15 | @retry(
16 | stop=stop_after_attempt(max_tries),
17 | wait=wait_fixed(wait_seconds),
18 | before=before_log(logger, logging.INFO),
19 | after=after_log(logger, logging.WARN),
20 | )
21 | def init():
22 | try:
23 | # Check Couchbase is awake
24 | bucket = get_default_bucket()
25 | logger.info(
26 | f"Database bucket connection established with bucket object: {bucket}"
27 | )
28 |
29 | # Wait for API to be awake, run one simple tests to authenticate
30 | test_get_access_token()
31 | except Exception as e:
32 | logger.error(e)
33 | raise e
34 |
35 |
36 | def main():
37 | logger.info("Initializing service")
38 | init()
39 | logger.info("Service finished initializing")
40 |
41 |
42 | if __name__ == "__main__":
43 | main()
44 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime, timedelta
3 | from pathlib import Path
4 | from typing import Optional
5 |
6 | import emails
7 | import jwt
8 | from emails.template import JinjaTemplate
9 | from jwt.exceptions import InvalidTokenError
10 |
11 | from app.core import config
12 |
13 | password_reset_jwt_subject = "reset"
14 |
15 |
16 | def send_email(email_to: str, subject_template="", html_template="", environment={}):
17 | assert config.EMAILS_ENABLED, "no provided configuration for email variables"
18 | message = emails.Message(
19 | subject=JinjaTemplate(subject_template),
20 | html=JinjaTemplate(html_template),
21 | mail_from=(config.EMAILS_FROM_NAME, config.EMAILS_FROM_EMAIL),
22 | )
23 | smtp_options = {"host": config.SMTP_HOST, "port": config.SMTP_PORT}
24 | if config.SMTP_TLS:
25 | smtp_options["tls"] = True
26 | if config.SMTP_USER:
27 | smtp_options["user"] = config.SMTP_USER
28 | if config.SMTP_PASSWORD:
29 | smtp_options["password"] = config.SMTP_PASSWORD
30 | response = message.send(to=email_to, render=environment, smtp=smtp_options)
31 | logging.info(f"send email result: {response}")
32 |
33 |
34 | def send_test_email(email_to: str):
35 | project_name = config.PROJECT_NAME
36 | subject = f"{project_name} - Test email"
37 | with open(Path(config.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
38 | template_str = f.read()
39 | send_email(
40 | email_to=email_to,
41 | subject_template=subject,
42 | html_template=template_str,
43 | environment={"project_name": config.PROJECT_NAME, "email": email_to},
44 | )
45 |
46 |
47 | def send_reset_password_email(email_to: str, username: str, token: str):
48 | project_name = config.PROJECT_NAME
49 | subject = f"{project_name} - Password recovery for user {username}"
50 | with open(Path(config.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
51 | template_str = f.read()
52 | if hasattr(token, "decode"):
53 | use_token = token.decode()
54 | else:
55 | use_token = token
56 | server_host = config.SERVER_HOST
57 | link = f"{server_host}/reset-password?token={use_token}"
58 | send_email(
59 | email_to=email_to,
60 | subject_template=subject,
61 | html_template=template_str,
62 | environment={
63 | "project_name": config.PROJECT_NAME,
64 | "username": username,
65 | "email": email_to,
66 | "valid_hours": config.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
67 | "link": link,
68 | },
69 | )
70 |
71 |
72 | def send_new_account_email(email_to: str, username: str, password: str):
73 | project_name = config.PROJECT_NAME
74 | subject = f"{project_name} - New account for user {username}"
75 | with open(Path(config.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
76 | template_str = f.read()
77 | link = config.SERVER_HOST
78 | send_email(
79 | email_to=email_to,
80 | subject_template=subject,
81 | html_template=template_str,
82 | environment={
83 | "project_name": config.PROJECT_NAME,
84 | "username": username,
85 | "password": password,
86 | "email": email_to,
87 | "link": link,
88 | },
89 | )
90 |
91 |
92 | def generate_password_reset_token(username):
93 | delta = timedelta(hours=config.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
94 | now = datetime.utcnow()
95 | expires = now + delta
96 | exp = expires.timestamp()
97 | encoded_jwt = jwt.encode(
98 | {
99 | "exp": exp,
100 | "nbf": now,
101 | "sub": password_reset_jwt_subject,
102 | "username": username,
103 | },
104 | config.SECRET_KEY,
105 | algorithm="HS256",
106 | )
107 | return encoded_jwt
108 |
109 |
110 | def verify_password_reset_token(token) -> Optional[str]:
111 | try:
112 | decoded_token = jwt.decode(token, config.SECRET_KEY, algorithms=["HS256"])
113 | assert decoded_token["sub"] == password_reset_jwt_subject
114 | return decoded_token["username"]
115 | except InvalidTokenError:
116 | return None
117 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/app/worker.py:
--------------------------------------------------------------------------------
1 | from raven import Client
2 |
3 | from app.core import config
4 | from app.core.celery_app import celery_app
5 |
6 | client_sentry = Client(config.SENTRY_DSN)
7 |
8 |
9 | @celery_app.task(acks_late=True)
10 | def test_celery(word: str):
11 | return f"test task return {word}"
12 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/prestart.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Let the DB start
4 | python /app/app/backend_pre_start.py
5 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -x
4 |
5 | autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place app --exclude=__init__.py
6 | isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --apply app
7 | black app
8 | vulture app --min-confidence 70
9 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/tests-start.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 |
4 | python /app/app/tests_pre_start.py
5 |
6 | pytest $* /app/app/tests/
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/app/worker-start.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 |
4 | python /app/app/celeryworker_pre_start.py
5 |
6 | celery worker -A app.worker -l info -Q main-queue -c 1
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/backend.dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.6
2 |
3 | # Dependencies for Couchbase
4 | RUN wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | apt-key add - && \
5 | OS_CODENAME=`cat /etc/os-release | grep VERSION_CODENAME | cut -f2 -d=` && \
6 | echo "deb http://packages.couchbase.com/ubuntu ${OS_CODENAME} ${OS_CODENAME}/main" > /etc/apt/sources.list.d/couchbase.list && \
7 | apt-get update && apt-get install -y libcouchbase-dev libcouchbase2-bin build-essential
8 |
9 | RUN pip install \
10 | celery~=4.3 \
11 | passlib[bcrypt] \
12 | tenacity \
13 | requests \
14 | couchbase \
15 | emails \
16 | "fastapi>=0.16.0" \
17 | uvicorn \
18 | gunicorn \
19 | pyjwt \
20 | python-multipart \
21 | email_validator \
22 | jinja2
23 |
24 | # For development, Jupyter remote kernel, Hydrogen
25 | # Using inside the container:
26 | # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
27 | ARG env=prod
28 | RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
29 | EXPOSE 8888
30 |
31 | COPY ./app /app
32 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/celeryworker.dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6
2 |
3 | # Dependencies for Couchbase
4 | RUN wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | apt-key add -
5 | RUN echo "deb http://packages.couchbase.com/ubuntu stretch stretch/main" > /etc/apt/sources.list.d/couchbase.list
6 | RUN apt-get update && apt-get install -y libcouchbase-dev build-essential
7 |
8 | RUN pip install raven celery~=4.3 passlib[bcrypt] tenacity requests "fastapi>=0.16.0" couchbase emails pyjwt email_validator jinja2
9 |
10 | # For development, Jupyter remote kernel, Hydrogen
11 | # Using inside the container:
12 | # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
13 | ARG env=prod
14 | RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
15 | EXPOSE 8888
16 |
17 | ENV C_FORCE_ROOT=1
18 |
19 | COPY ./app /app
20 | WORKDIR /app
21 |
22 | ENV PYTHONPATH=/app
23 |
24 | COPY ./app/worker-start.sh /worker-start.sh
25 |
26 | RUN chmod +x /worker-start.sh
27 |
28 | CMD ["bash", "/worker-start.sh"]
29 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/backend/tests.dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6
2 |
3 | RUN wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | apt-key add - && \
4 | OS_CODENAME=`cat /etc/os-release | grep VERSION_CODENAME | cut -f2 -d=` && \
5 | echo "deb http://packages.couchbase.com/ubuntu ${OS_CODENAME} ${OS_CODENAME}/main" > /etc/apt/sources.list.d/couchbase.list && \
6 | apt-get update && apt-get install -y libcouchbase-dev build-essential
7 |
8 | RUN pip install requests pytest tenacity passlib[bcrypt] couchbase "fastapi>=0.16.0"
9 |
10 | # For development, Jupyter remote kernel, Hydrogen
11 | # Using inside the container:
12 | # jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
13 | ARG env=prod
14 | RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
15 | EXPOSE 8888
16 |
17 | COPY ./app /app
18 |
19 | ENV PYTHONPATH=/app
20 |
21 | COPY ./app/tests-start.sh /tests-start.sh
22 |
23 | RUN chmod +x /tests-start.sh
24 |
25 | # This will make the container wait, doing nothing, but alive
26 | CMD ["bash", "-c", "while true; do sleep 1; done"]
27 |
28 | # Afterwards you can exec a command /tests-start.sh in the live container, like:
29 | # docker exec -it backend-tests /tests-start.sh
30 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/cookiecutter-config-file.yml:
--------------------------------------------------------------------------------
1 | default_context:
2 | project_name: '{{ cookiecutter.project_name }}'
3 | project_slug: '{{ cookiecutter.project_slug }}'
4 | domain_main: '{{ cookiecutter.domain_main }}'
5 | domain_staging: '{{ cookiecutter.domain_staging }}'
6 | docker_swarm_stack_name_main: '{{ cookiecutter.docker_swarm_stack_name_main }}'
7 | docker_swarm_stack_name_staging: '{{ cookiecutter.docker_swarm_stack_name_staging }}'
8 | secret_key: '{{ cookiecutter.secret_key }}'
9 | first_superuser: '{{ cookiecutter.first_superuser }}'
10 | first_superuser_password: '{{ cookiecutter.first_superuser_password }}'
11 | backend_cors_origins: '{{ cookiecutter.backend_cors_origins }}'
12 | smtp_port: '{{ cookiecutter.smtp_port }}'
13 | smtp_host: '{{ cookiecutter.smtp_host }}'
14 | smtp_user: '{{ cookiecutter.smtp_user }}'
15 | smtp_password: '{{ cookiecutter.smtp_password }}'
16 | smtp_emails_from_email: '{{ cookiecutter.smtp_emails_from_email }}'
17 | couchbase_user: '{{ cookiecutter.couchbase_user }}'
18 | couchbase_password: '{{ cookiecutter.couchbase_password }}'
19 | couchbase_sync_gateway_cors: '{{ cookiecutter.couchbase_sync_gateway_cors }}'
20 | couchbase_sync_gateway_user: '{{ cookiecutter.couchbase_sync_gateway_user }}'
21 | couchbase_sync_gateway_password: '{{ cookiecutter.couchbase_sync_gateway_password }}'
22 | traefik_constraint_tag: '{{ cookiecutter.traefik_constraint_tag }}'
23 | traefik_constraint_tag_staging: '{{ cookiecutter.traefik_constraint_tag_staging }}'
24 | traefik_public_network: '{{ cookiecutter.traefik_public_network }}'
25 | traefik_public_constraint_tag: '{{ cookiecutter.traefik_public_constraint_tag }}'
26 | flower_auth: '{{ cookiecutter.flower_auth }}'
27 | sentry_dsn: '{{ cookiecutter.sentry_dsn }}'
28 | docker_image_prefix: '{{ cookiecutter.docker_image_prefix }}'
29 | docker_image_backend: '{{ cookiecutter.docker_image_backend }}'
30 | docker_image_celeryworker: '{{ cookiecutter.docker_image_celeryworker }}'
31 | docker_image_frontend: '{{ cookiecutter.docker_image_frontend }}'
32 | docker_image_sync_gateway: '{{ cookiecutter.docker_image_sync_gateway }}'
33 | _copy_without_render: [frontend/src/**/*.html, frontend/src/**/*.vue, frontend/node_modules/*, backend/app/app/email-templates/**]
34 | _template: ./
35 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.deploy.build.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | build:
5 | context: ./backend
6 | dockerfile: backend.dockerfile
7 | celeryworker:
8 | build:
9 | context: ./backend
10 | dockerfile: celeryworker.dockerfile
11 | frontend:
12 | build:
13 | context: ./frontend
14 | args:
15 | FRONTEND_ENV: ${FRONTEND_ENV-production}
16 | sync-gateway:
17 | build:
18 | context: ./sync-gateway
19 | dockerfile: Dockerfile
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.deploy.command.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | proxy:
4 | command: --docker \
5 | --docker.swarmmode \
6 | --docker.watch \
7 | --docker.exposedbydefault=false \
8 | --constraints=tag==${TRAEFIK_TAG} \
9 | --logLevel=INFO \
10 | --accessLog \
11 | --web
12 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.deploy.images.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | image: '${DOCKER_IMAGE_BACKEND}:${TAG-latest}'
5 | celeryworker:
6 | image: '${DOCKER_IMAGE_CELERYWORKER}:${TAG-latest}'
7 | frontend:
8 | image: '${DOCKER_IMAGE_FRONTEND}:${TAG-latest}'
9 | sync-gateway:
10 | image: '${DOCKER_IMAGE_SYNC_GATEWAY}:${TAG-latest}'
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.deploy.labels.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | couchbase:
4 | deploy:
5 | labels:
6 | - traefik.frontend.rule=Host:db.${DOMAIN}
7 | - traefik.enable=true
8 | - traefik.port=8091
9 | - traefik.tags=${TRAEFIK_PUBLIC_TAG}
10 | - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
11 | - traefik.frontend.entryPoints=http,https
12 | - traefik.frontend.redirect.entryPoint=https
13 | sync-gateway:
14 | deploy:
15 | labels:
16 | - traefik.frontend.rule=Host:sync.${DOMAIN}
17 | - traefik.enable=true
18 | - traefik.port=4984
19 | - traefik.tags=${TRAEFIK_PUBLIC_TAG}
20 | - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
21 | - traefik.frontend.entryPoints=http,https
22 | - traefik.frontend.redirect.entryPoint=https
23 | proxy:
24 | deploy:
25 | labels:
26 | # For the configured domain
27 | - traefik.frontend.rule=Host:${DOMAIN}
28 | # For a domain with and without 'www'
29 | # Comment the previous line above and un-comment the line below
30 | # - "traefik.frontend.rule=Host:www.${DOMAIN},${DOMAIN}"
31 | - traefik.enable=true
32 | - traefik.port=80
33 | - traefik.tags=${TRAEFIK_PUBLIC_TAG}
34 | - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
35 | - traefik.frontend.entryPoints=http,https
36 | - traefik.frontend.redirect.entryPoint=https
37 | # Uncomment the config line below to detect and redirect www to non-www (or the contrary)
38 | # The lines above for traefik.frontend.rule are needed too
39 | # - "traefik.frontend.redirect.regex=^https?://(www.)?(${DOMAIN})/(.*)"
40 | # To redirect from non-www to www un-comment the line below
41 | # - "traefik.frontend.redirect.replacement=https://www.${DOMAIN}/$$3"
42 | # To redirect from www to non-www un-comment the line below
43 | # - "traefik.frontend.redirect.replacement=https://${DOMAIN}/$$3"
44 | flower:
45 | deploy:
46 | labels:
47 | - traefik.frontend.rule=Host:flower.${DOMAIN}
48 | - traefik.enable=true
49 | - traefik.port=5555
50 | - traefik.tags=${TRAEFIK_PUBLIC_TAG}
51 | - traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
52 | - traefik.frontend.entryPoints=http,https
53 | - traefik.frontend.redirect.entryPoint=https
54 | backend:
55 | deploy:
56 | labels:
57 | - traefik.frontend.rule=PathPrefix:/api,/docs,/redoc
58 | - traefik.enable=true
59 | - traefik.port=80
60 | - traefik.tags=${TRAEFIK_TAG}
61 | frontend:
62 | deploy:
63 | labels:
64 | - traefik.frontend.rule=PathPrefix:/
65 | - traefik.enable=true
66 | - traefik.port=80
67 | - traefik.tags=${TRAEFIK_TAG}
68 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.deploy.networks.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | couchbase:
4 | networks:
5 | - ${TRAEFIK_PUBLIC_NETWORK}
6 | - default
7 | sync-gateway:
8 | networks:
9 | - ${TRAEFIK_PUBLIC_NETWORK}
10 | - default
11 | proxy:
12 | networks:
13 | - ${TRAEFIK_PUBLIC_NETWORK}
14 | - default
15 | flower:
16 | networks:
17 | - ${TRAEFIK_PUBLIC_NETWORK}
18 | - default
19 |
20 | networks:
21 | {{cookiecutter.traefik_public_network}}:
22 | external: true
23 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.deploy.volumes-placement.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | couchbase:
4 | volumes:
5 | - app-couchbase-data:/opt/couchbase/var
6 | deploy:
7 | placement:
8 | constraints:
9 | - node.labels.${STACK_NAME}.app-couchbase-data == true
10 | proxy:
11 | deploy:
12 | placement:
13 | constraints:
14 | - node.role == manager
15 |
16 | volumes:
17 | app-couchbase-data:
18 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.build.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | build:
5 | context: ./backend
6 | dockerfile: backend.dockerfile
7 | args:
8 | env: dev
9 | celeryworker:
10 | build:
11 | context: ./backend
12 | dockerfile: celeryworker.dockerfile
13 | args:
14 | env: dev
15 | backend-tests:
16 | build:
17 | context: ./backend
18 | dockerfile: tests.dockerfile
19 | args:
20 | env: dev
21 | frontend:
22 | build:
23 | context: ./frontend
24 | args:
25 | FRONTEND_ENV: dev
26 | sync-gateway:
27 | build:
28 | context: ./sync-gateway
29 | dockerfile: Dockerfile
30 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.command.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | proxy:
4 | command: --docker \
5 | --docker.watch \
6 | --docker.exposedbydefault=false \
7 | --constraints=tag==${TRAEFIK_TAG} \
8 | --logLevel=DEBUG \
9 | --accessLog \
10 | --web
11 | # backend:
12 | # command: bash -c "while true; do sleep 1; done" # Infinite loop to keep container live doing nothing
13 | backend:
14 | command: /start-reload.sh
15 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.env.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | environment:
5 | - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
6 | - SERVER_HOST=http://${DOMAIN}
7 | celeryworker:
8 | environment:
9 | - RUN=celery worker -A app.worker -l info -Q main-queue -c 1
10 | - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
11 | - SERVER_HOST=http://${DOMAIN}
12 | backend-tests:
13 | environment:
14 | - JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
15 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.labels.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | proxy:
4 | labels:
5 | - traefik.frontend.rule=Host:${DOMAIN}
6 | - traefik.enable=true
7 | - traefik.port=80
8 | backend:
9 | labels:
10 | - traefik.frontend.rule=PathPrefix:/api,/docs,/redoc
11 | - traefik.enable=true
12 | - traefik.port=80
13 | - traefik.tags=${TRAEFIK_TAG}
14 | frontend:
15 | labels:
16 | - traefik.frontend.rule=PathPrefix:/
17 | - traefik.enable=true
18 | - traefik.port=80
19 | - traefik.tags=${TRAEFIK_TAG}
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.networks.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | networks:
5 | default:
6 | aliases:
7 | - ${DOMAIN}
8 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.ports.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | couchbase:
4 | ports:
5 | - '8091:8091'
6 | sync-gateway:
7 | ports:
8 | - '4984:4984'
9 | - '4985:4985'
10 | proxy:
11 | ports:
12 | - '80:80'
13 | - '8090:8080'
14 | flower:
15 | ports:
16 | - '5555:5555'
17 | backend:
18 | ports:
19 | - '8888:8888'
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.dev.volumes.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | volumes:
5 | - ./backend/app:/app
6 | celeryworker:
7 | volumes:
8 | - ./backend/app:/app
9 | backend-tests:
10 | volumes:
11 | - ./backend/app:/app
12 | sync-gateway:
13 | volumes:
14 | - ./sync-gateway/sync:/sync
15 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.shared.admin.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | proxy:
4 | image: traefik:v1.7
5 | volumes:
6 | - /var/run/docker.sock:/var/run/docker.sock
7 | flower:
8 | image: mher/flower
9 | env_file:
10 | - env-flower.env
11 | command:
12 | - "--broker=amqp://guest@queue:5672//"
13 | # For the "Broker" tab to work in the flower UI, uncomment the following command argument,
14 | # and change the queue service's image as described in docker-compose.shared.base-images.yml
15 | # - "--broker_api=http://guest:guest@queue:15672/api//"
16 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.shared.base-images.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | couchbase:
4 | image: couchbase:community-6.0.0
5 | queue:
6 | image: rabbitmq:3
7 | # Using the below image instead is required to enable the "Broker" tab in the flower UI:
8 | # image: rabbitmq:3-management
9 | #
10 | # You also have to change the flower command as documented in docker-compose.shared.admin.yml
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.shared.depends.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | depends_on:
5 | - couchbase
6 | celeryworker:
7 | depends_on:
8 | - couchbase
9 | - queue
10 | sync-gateway:
11 | depends_on:
12 | - couchbase
13 | - backend
14 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.shared.env.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend:
4 | env_file:
5 | - env-couchbase.env
6 | - env-sync-gateway.env
7 | - env-backend.env
8 | environment:
9 | - SERVER_NAME=${DOMAIN}
10 | - SERVER_HOST=https://${DOMAIN}
11 | celeryworker:
12 | env_file:
13 | - env-couchbase.env
14 | - env-sync-gateway.env
15 | - env-backend.env
16 | environment:
17 | - SERVER_HOST=https://${DOMAIN}
18 | sync-gateway:
19 | env_file:
20 | - env-couchbase.env
21 | - env-sync-gateway.env
22 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/docker-compose.test.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | backend-tests:
4 | build:
5 | context: ./backend
6 | dockerfile: tests.dockerfile
7 | command: bash -c "while true; do sleep 1; done"
8 | env_file:
9 | - env-couchbase.env
10 | - env-sync-gateway.env
11 | - env-backend.env
12 | environment:
13 | - SERVER_NAME=backend
14 | backend:
15 | environment:
16 | # Don't send emails during testing
17 | - SMTP_HOST=
18 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/env-backend.env:
--------------------------------------------------------------------------------
1 | BACKEND_CORS_ORIGINS={{cookiecutter.backend_cors_origins}}
2 | PROJECT_NAME={{cookiecutter.project_name}}
3 | SECRET_KEY={{cookiecutter.secret_key}}
4 | FIRST_SUPERUSER={{cookiecutter.first_superuser}}
5 | FIRST_SUPERUSER_PASSWORD={{cookiecutter.first_superuser_password}}
6 | SMTP_TLS=True
7 | SMTP_PORT={{cookiecutter.smtp_port}}
8 | SMTP_HOST={{cookiecutter.smtp_host}}
9 | SMTP_USER={{cookiecutter.smtp_user}}
10 | SMTP_PASSWORD={{cookiecutter.smtp_password}}
11 | EMAILS_FROM_EMAIL={{cookiecutter.smtp_emails_from_email}}
12 |
13 | USERS_OPEN_REGISTRATION=False
14 |
15 | SENTRY_DSN={{cookiecutter.sentry_dsn}}
16 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/env-couchbase.env:
--------------------------------------------------------------------------------
1 | COUCHBASE_HOST=couchbase
2 | COUCHBASE_PORT=8091
3 | COUCHBASE_FULL_TEXT_PORT=8094
4 | COUCHBASE_ENTERPRISE=False
5 | COUCHBASE_MEMORY_QUOTA_MB=256
6 | COUCHBASE_INDEX_MEMORY_QUOTA_MB=256
7 | COUCHBASE_FTS_MEMORY_QUOTA_MB=256
8 | COUCHBASE_BUCKET_NAME=app
9 | COUCHBASE_USER={{cookiecutter.couchbase_user}}
10 | COUCHBASE_PASSWORD={{cookiecutter.couchbase_password}}
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/env-flower.env:
--------------------------------------------------------------------------------
1 | FLOWER_BASIC_AUTH={{cookiecutter.flower_auth}}
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/env-sync-gateway.env:
--------------------------------------------------------------------------------
1 | COUCHBASE_SYNC_GATEWAY_HOST=sync-gateway
2 | COUCHBASE_SYNC_GATEWAY_PORT=4985
3 | COUCHBASE_SYNC_GATEWAY_USER={{cookiecutter.couchbase_sync_gateway_user}}
4 | COUCHBASE_SYNC_GATEWAY_PASSWORD={{cookiecutter.couchbase_sync_gateway_password}}
5 | COUCHBASE_SYNC_GATEWAY_CORS_ORIGINS={{cookiecutter.couchbase_sync_gateway_cors}}
6 | COUCHBASE_SYNC_GATEWAY_DATABASE=db
7 | COUCHBASE_SYNC_GATEWAY_DISABLE_GUEST_USER=true
8 | COUCHBASE_SYNC_GATEWAY_NUM_INDEX_REPLICAS=0
9 | COUCHBASE_SYNC_GATEWAY_LOG=*
10 | COUCHBASE_SYNC_GATEWAY_ADMIN_INTERFACE=:4985
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_DOMAIN_DEV=localhost
2 | # VUE_APP_DOMAIN_DEV=local.dockertoolbox.tiangolo.com
3 | # VUE_APP_DOMAIN_DEV=localhost.tiangolo.com
4 | # VUE_APP_DOMAIN_DEV=dev.{{cookiecutter.domain_main}}
5 | VUE_APP_DOMAIN_STAG={{cookiecutter.domain_staging}}
6 | VUE_APP_DOMAIN_PROD={{cookiecutter.domain_main}}
7 | VUE_APP_NAME={{cookiecutter.project_name}}
8 | VUE_APP_ENV=development
9 | # VUE_APP_ENV=staging
10 | # VUE_APP_ENV=production
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 0, "build-stage", based on Node.js, to build and compile the frontend
2 | FROM tiangolo/node-frontend:10 as build-stage
3 |
4 | WORKDIR /app
5 |
6 | COPY package*.json /app/
7 |
8 | RUN npm install
9 |
10 | COPY ./ /app/
11 |
12 | ARG FRONTEND_ENV=production
13 |
14 | ENV VUE_APP_ENV=${FRONTEND_ENV}
15 |
16 | # Comment out the next line to disable tests
17 | RUN npm run test:unit
18 |
19 | RUN npm run build
20 |
21 |
22 | # Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
23 | FROM nginx:1.15
24 |
25 | COPY --from=build-stage /app/dist/ /usr/share/nginx/html
26 |
27 | COPY --from=build-stage /nginx.conf /etc/nginx/conf.d/default.conf
28 | COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
29 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/README.md:
--------------------------------------------------------------------------------
1 | # frontend
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Run your tests
19 | ```
20 | npm run test
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | npm run lint
26 | ```
27 |
28 | ### Run your unit tests
29 | ```
30 | npm run test:unit
31 | ```
32 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "presets": [
3 | [
4 | "@vue/app",
5 | {
6 | "useBuiltIns": "entry"
7 | }
8 | ]
9 | ]
10 | }
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/nginx-backend-not-found.conf:
--------------------------------------------------------------------------------
1 | location /api {
2 | return 404;
3 | }
4 | location /docs {
5 | return 404;
6 | }
7 | location /redoc {
8 | return 404;
9 | }
10 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "test:unit": "vue-cli-service test:unit"
10 | },
11 | "dependencies": {
12 | "@babel/polyfill": "^7.2.5",
13 | "axios": "^0.18.0",
14 | "register-service-worker": "^1.0.0",
15 | "typesafe-vuex": "^3.1.1",
16 | "vee-validate": "^2.1.7",
17 | "vue": "^2.5.22",
18 | "vue-class-component": "^6.0.0",
19 | "vue-property-decorator": "^7.3.0",
20 | "vue-router": "^3.0.2",
21 | "vuetify": "^1.4.4",
22 | "vuex": "^3.1.0"
23 | },
24 | "devDependencies": {
25 | "@types/jest": "^23.3.13",
26 | "@vue/cli-plugin-babel": "^3.3.0",
27 | "@vue/cli-plugin-pwa": "^3.3.0",
28 | "@vue/cli-plugin-typescript": "^3.3.0",
29 | "@vue/cli-plugin-unit-jest": "^3.5.0",
30 | "@vue/cli-service": "^3.3.1",
31 | "@vue/test-utils": "^1.0.0-beta.28",
32 | "babel-core": "7.0.0-bridge.0",
33 | "ts-jest": "^23.10.5",
34 | "typescript": "^3.2.4",
35 | "vue-cli-plugin-vuetify": "^0.2.1",
36 | "vue-template-compiler": "^2.5.22"
37 | },
38 | "postcss": {
39 | "plugins": {
40 | "autoprefixer": {}
41 | }
42 | },
43 | "browserslist": [
44 | "> 1%",
45 | "last 2 versions",
46 | "not ie <= 10"
47 | ],
48 | "jest": {
49 | "moduleFileExtensions": [
50 | "js",
51 | "jsx",
52 | "json",
53 | "vue",
54 | "ts",
55 | "tsx"
56 | ],
57 | "transform": {
58 | "^.+\\.vue$": "vue-jest",
59 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
60 | "^.+\\.tsx?$": "ts-jest"
61 | },
62 | "moduleNameMapper": {
63 | "^@/(.*)$": "/src/$1"
64 | },
65 | "snapshotSerializers": [
66 | "jest-serializer-vue"
67 | ],
68 | "testMatch": [
69 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
70 | ],
71 | "testURL": "http://localhost/"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/public/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= VUE_APP_NAME %>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "short_name": "frontend",
4 | "icons": [
5 | {
6 | "src": "/img/icons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/img/icons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "/",
17 | "display": "standalone",
18 | "background_color": "#000000",
19 | "theme_color": "#4DBA87"
20 | }
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
44 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { apiUrl } from '@/env';
3 | import { IUserProfile, IUserProfileUpdate, IUserProfileCreate } from './interfaces';
4 |
5 | function authHeaders(token: string) {
6 | return {
7 | headers: {
8 | Authorization: `Bearer ${token}`,
9 | },
10 | };
11 | }
12 |
13 | export const api = {
14 | async logInGetToken(username: string, password: string) {
15 | const params = new URLSearchParams();
16 | params.append('username', username);
17 | params.append('password', password);
18 |
19 | return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
20 | },
21 | async getMe(token: string) {
22 | return axios.get(`${apiUrl}/api/v1/users/me`, authHeaders(token));
23 | },
24 | async updateMe(token: string, data: IUserProfileUpdate) {
25 | return axios.put(`${apiUrl}/api/v1/users/me`, data, authHeaders(token));
26 | },
27 | async getUsers(token: string) {
28 | return axios.get(`${apiUrl}/api/v1/users/`, authHeaders(token));
29 | },
30 | async updateUser(token: string, username: string, data: IUserProfileUpdate) {
31 | return axios.put(`${apiUrl}/api/v1/users/${username}`, data, authHeaders(token));
32 | },
33 | async createUser(token: string, data: IUserProfileCreate) {
34 | return axios.post(`${apiUrl}/api/v1/users/`, data, authHeaders(token));
35 | },
36 | async getRoles(token: string) {
37 | return axios.get(`${apiUrl}/api/v1/roles/`, authHeaders(token));
38 | },
39 | async passwordRecovery(username: string) {
40 | return axios.post(`${apiUrl}/api/v1/password-recovery/${username}`);
41 | },
42 | async resetPassword(password: string, token: string) {
43 | return axios.post(`${apiUrl}/api/v1/reset-password/`, {
44 | new_password: password,
45 | token,
46 | });
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiangolo/full-stack-fastapi-couchbase/28861b99b35769b816e1ee94b070270ca2fe2301/{{cookiecutter.project_slug}}/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/component-hooks.ts:
--------------------------------------------------------------------------------
1 | import Component from 'vue-class-component';
2 |
3 | // Register the router hooks with their names
4 | Component.registerHooks([
5 | 'beforeRouteEnter',
6 | 'beforeRouteLeave',
7 | 'beforeRouteUpdate', // for vue-router 2.2+
8 | ]);
9 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/components/NotificationsManager.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ currentNotificationContent }}
5 | Close
6 |
7 |
8 |
9 |
78 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/components/RouterComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/components/UploadButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Choose File
4 |
5 |
6 |
7 |
8 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/env.ts:
--------------------------------------------------------------------------------
1 | const env = process.env.VUE_APP_ENV;
2 |
3 | let envApiUrl = '';
4 |
5 | if (env === 'production') {
6 | envApiUrl = `https://${process.env.VUE_APP_DOMAIN_PROD}`;
7 | } else if (env === 'staging') {
8 | envApiUrl = `https://${process.env.VUE_APP_DOMAIN_STAG}`;
9 | } else {
10 | envApiUrl = `http://${process.env.VUE_APP_DOMAIN_DEV}`;
11 | }
12 |
13 | export const apiUrl = envApiUrl;
14 | export const appName = process.env.VUE_APP_NAME;
15 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export interface IUserProfile {
2 | admin_channels: string[];
3 | admin_roles: string[];
4 | disabled: boolean;
5 | email: string;
6 | full_name: string;
7 | username: string;
8 | }
9 |
10 | export interface IUserProfileUpdate {
11 | full_name?: string;
12 | password?: string;
13 | email?: string;
14 | admin_channels?: string[];
15 | admin_roles?: string[];
16 | disabled?: boolean;
17 | }
18 |
19 | export interface IUserProfileCreate {
20 | username: string;
21 | full_name?: string;
22 | password?: string;
23 | email?: string;
24 | admin_channels?: string[];
25 | admin_roles?: string[];
26 | disabled?: boolean;
27 | }
28 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/main.ts:
--------------------------------------------------------------------------------
1 | import '@babel/polyfill';
2 | // Import Component hooks before component definitions
3 | import './component-hooks';
4 | import Vue from 'vue';
5 | import './plugins/vuetify';
6 | import './plugins/vee-validate';
7 | import App from './App.vue';
8 | import router from './router';
9 | import store from '@/store';
10 | import './registerServiceWorker';
11 | import 'vuetify/dist/vuetify.min.css';
12 |
13 | Vue.config.productionTip = false;
14 |
15 | new Vue({
16 | router,
17 | store,
18 | render: (h) => h(App),
19 | }).$mount('#app');
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/plugins/vee-validate.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VeeValidate from 'vee-validate';
3 |
4 | Vue.use(VeeValidate);
5 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/plugins/vuetify.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuetify from 'vuetify';
3 |
4 | Vue.use(Vuetify, {
5 | iconfont: 'md',
6 | });
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/registerServiceWorker.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-console */
2 |
3 | import { register } from 'register-service-worker';
4 |
5 | if (process.env.NODE_ENV === 'production') {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | 'App is being served from cache by a service worker.\n' +
10 | 'For more details, visit https://goo.gl/AFskqB',
11 | );
12 | },
13 | cached() {
14 | console.log('Content has been cached for offline use.');
15 | },
16 | updated() {
17 | console.log('New content is available; please refresh.');
18 | },
19 | offline() {
20 | console.log('No internet connection found. App is running in offline mode.');
21 | },
22 | error(error) {
23 | console.error('Error during service worker registration:', error);
24 | },
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/router.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Router from 'vue-router';
3 |
4 | import RouterComponent from './components/RouterComponent.vue';
5 |
6 | Vue.use(Router);
7 |
8 | export default new Router({
9 | mode: 'history',
10 | base: process.env.BASE_URL,
11 | routes: [
12 | {
13 | path: '/',
14 | component: () => import(/* webpackChunkName: "start" */ './views/main/Start.vue'),
15 | children: [
16 | {
17 | path: 'login',
18 | // route level code-splitting
19 | // this generates a separate chunk (about.[hash].js) for this route
20 | // which is lazy-loaded when the route is visited.
21 | component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'),
22 | },
23 | {
24 | path: 'recover-password',
25 | component: () => import(/* webpackChunkName: "recover-password" */ './views/PasswordRecovery.vue'),
26 | },
27 | {
28 | path: 'reset-password',
29 | component: () => import(/* webpackChunkName: "reset-password" */ './views/ResetPassword.vue'),
30 | },
31 | {
32 | path: 'main',
33 | component: () => import(/* webpackChunkName: "main" */ './views/main/Main.vue'),
34 | children: [
35 | {
36 | path: 'dashboard',
37 | component: () => import(/* webpackChunkName: "main-dashboard" */ './views/main/Dashboard.vue'),
38 | },
39 | {
40 | path: 'profile',
41 | component: RouterComponent,
42 | redirect: 'profile/view',
43 | children: [
44 | {
45 | path: 'view',
46 | component: () => import(
47 | /* webpackChunkName: "main-profile" */ './views/main/profile/UserProfile.vue'),
48 | },
49 | {
50 | path: 'edit',
51 | component: () => import(
52 | /* webpackChunkName: "main-profile-edit" */ './views/main/profile/UserProfileEdit.vue'),
53 | },
54 | {
55 | path: 'password',
56 | component: () => import(
57 | /* webpackChunkName: "main-profile-password" */ './views/main/profile/UserProfileEditPassword.vue'),
58 | },
59 | ],
60 | },
61 | {
62 | path: 'admin',
63 | component: () => import(/* webpackChunkName: "main-admin" */ './views/main/admin/Admin.vue'),
64 | redirect: 'admin/users/all',
65 | children: [
66 | {
67 | path: 'users',
68 | redirect: 'users/all',
69 | },
70 | {
71 | path: 'users/all',
72 | component: () => import(
73 | /* webpackChunkName: "main-admin-users" */ './views/main/admin/AdminUsers.vue'),
74 | },
75 | {
76 | path: 'users/edit/:username',
77 | name: 'main-admin-users-edit',
78 | component: () => import(
79 | /* webpackChunkName: "main-admin-users-edit" */ './views/main/admin/EditUser.vue'),
80 | },
81 | {
82 | path: 'users/create',
83 | name: 'main-admin-users-create',
84 | component: () => import(
85 | /* webpackChunkName: "main-admin-users-create" */ './views/main/admin/CreateUser.vue'),
86 | },
87 | ],
88 | },
89 | ],
90 | },
91 | ],
92 | },
93 | {
94 | path: '/*', redirect: '/',
95 | },
96 | ],
97 | });
98 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue';
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue';
3 | export default Vue;
4 | }
5 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/admin/actions.ts:
--------------------------------------------------------------------------------
1 | import { api } from '@/api';
2 | import { ActionContext } from 'vuex';
3 | import { IUserProfileCreate, IUserProfileUpdate } from '@/interfaces';
4 | import { State } from '../state';
5 | import { AdminState } from './state';
6 | import { getStoreAccessors } from 'typesafe-vuex';
7 | import { commitSetUsers, commitSetUser, commitSetRoles } from './mutations';
8 | import { dispatchCheckApiError } from '../main/actions';
9 | import { commitAddNotification, commitRemoveNotification } from '../main/mutations';
10 |
11 | type MainContext = ActionContext;
12 |
13 | export const actions = {
14 | async actionGetUsers(context: MainContext) {
15 | try {
16 | const response = await api.getUsers(context.rootState.main.token);
17 | if (response) {
18 | commitSetUsers(context, response.data);
19 | }
20 | } catch (error) {
21 | await dispatchCheckApiError(context, error);
22 | }
23 | },
24 | async actionUpdateUser(context: MainContext, payload: { username: string, user: IUserProfileUpdate }) {
25 | try {
26 | const loadingNotification = { content: 'saving', showProgress: true };
27 | commitAddNotification(context, loadingNotification);
28 | const response = (await Promise.all([
29 | api.updateUser(context.rootState.main.token, payload.username, payload.user),
30 | await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
31 | ]))[0];
32 | commitSetUser(context, response.data);
33 | commitRemoveNotification(context, loadingNotification);
34 | commitAddNotification(context, {content: 'User successfully updated', color: 'success'});
35 | } catch (error) {
36 | await dispatchCheckApiError(context, error);
37 | }
38 | },
39 | async actionCreateUser(context: MainContext, payload: IUserProfileCreate) {
40 | try {
41 | const loadingNotification = { content: 'saving', showProgress: true };
42 | commitAddNotification(context, loadingNotification);
43 | const response = (await Promise.all([
44 | api.createUser(context.rootState.main.token, payload),
45 | await new Promise((resolve, reject) => setTimeout(() => resolve(), 500)),
46 | ]))[0];
47 | commitSetUser(context, response.data);
48 | commitRemoveNotification(context, loadingNotification);
49 | commitAddNotification(context, { content: 'User successfully created', color: 'success' });
50 | } catch (error) {
51 | await dispatchCheckApiError(context, error);
52 | }
53 | },
54 | async actionGetRoles(context: MainContext) {
55 | try {
56 | const response = await api.getRoles(context.rootState.main.token);
57 | commitSetRoles(context, response.data.roles);
58 | } catch (error) {
59 | await dispatchCheckApiError(context, error);
60 | }
61 | },
62 | };
63 |
64 | const { dispatch } = getStoreAccessors('');
65 |
66 | export const dispatchCreateUser = dispatch(actions.actionCreateUser);
67 | export const dispatchGetRoles = dispatch(actions.actionGetRoles);
68 | export const dispatchGetUsers = dispatch(actions.actionGetUsers);
69 | export const dispatchUpdateUser = dispatch(actions.actionUpdateUser);
70 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/admin/getters.ts:
--------------------------------------------------------------------------------
1 | import { AdminState } from './state';
2 | import { getStoreAccessors } from 'typesafe-vuex';
3 | import { State } from '../state';
4 |
5 | export const getters = {
6 | adminUsers: (state: AdminState) => state.users,
7 | adminRoles: (state: AdminState) => state.roles,
8 | adminOneUser: (state: AdminState) => (username: string) => {
9 | const filteredUsers = state.users.filter((user) => user.username === username);
10 | if (filteredUsers.length > 0) {
11 | return { ...filteredUsers[0] };
12 | }
13 | },
14 | };
15 |
16 | const { read } = getStoreAccessors('');
17 |
18 | export const readAdminOneUser = read(getters.adminOneUser);
19 | export const readAdminRoles = read(getters.adminRoles);
20 | export const readAdminUsers = read(getters.adminUsers);
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/admin/index.ts:
--------------------------------------------------------------------------------
1 | import { mutations } from './mutations';
2 | import { getters } from './getters';
3 | import { actions } from './actions';
4 | import { AdminState } from './state';
5 |
6 | const defaultState: AdminState = {
7 | users: [],
8 | roles: [],
9 | };
10 |
11 | export const adminModule = {
12 | state: defaultState,
13 | mutations,
14 | actions,
15 | getters,
16 | };
17 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/admin/mutations.ts:
--------------------------------------------------------------------------------
1 | import { IUserProfile } from '@/interfaces';
2 | import { AdminState } from './state';
3 | import { getStoreAccessors } from 'typesafe-vuex';
4 | import { State } from '../state';
5 |
6 | export const mutations = {
7 | setUsers(state: AdminState, payload: IUserProfile[]) {
8 | state.users = payload;
9 | },
10 | setUser(state: AdminState, payload: IUserProfile) {
11 | const users = state.users.filter((user: IUserProfile) => user.username !== payload.username);
12 | users.push(payload);
13 | state.users = users;
14 | },
15 | setRoles(state: AdminState, payload: string[]) {
16 | state.roles = payload;
17 | },
18 | };
19 |
20 | const { commit } = getStoreAccessors('');
21 |
22 | export const commitSetUser = commit(mutations.setUser);
23 | export const commitSetUsers = commit(mutations.setUsers);
24 | export const commitSetRoles = commit(mutations.setRoles);
25 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/admin/state.ts:
--------------------------------------------------------------------------------
1 | import { IUserProfile } from '@/interfaces';
2 |
3 | export interface AdminState {
4 | users: IUserProfile[];
5 | roles: string[];
6 | }
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex, { StoreOptions } from 'vuex';
3 |
4 | import { mainModule } from './main';
5 | import { State } from './state';
6 | import { adminModule } from './admin';
7 |
8 | Vue.use(Vuex);
9 |
10 | const storeOptions: StoreOptions = {
11 | modules: {
12 | main: mainModule,
13 | admin: adminModule,
14 | },
15 | };
16 |
17 | export const store = new Vuex.Store(storeOptions);
18 |
19 | export default store;
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/main/getters.ts:
--------------------------------------------------------------------------------
1 | import { MainState } from './state';
2 | import { getStoreAccessors } from 'typesafe-vuex';
3 | import { State } from '../state';
4 |
5 | export const getters = {
6 | hasAdminAccess: (state: MainState) => {
7 | return (
8 | state.userProfile &&
9 | state.userProfile.admin_roles.includes('superuser'));
10 | },
11 | loginError: (state: MainState) => state.logInError,
12 | dashboardShowDrawer: (state: MainState) => state.dashboardShowDrawer,
13 | dashboardMiniDrawer: (state: MainState) => state.dashboardMiniDrawer,
14 | userProfile: (state: MainState) => state.userProfile,
15 | token: (state: MainState) => state.token,
16 | isLoggedIn: (state: MainState) => state.isLoggedIn,
17 | firstNotification: (state: MainState) => state.notifications.length > 0 && state.notifications[0],
18 | };
19 |
20 | const {read} = getStoreAccessors('');
21 |
22 | export const readDashboardMiniDrawer = read(getters.dashboardMiniDrawer);
23 | export const readDashboardShowDrawer = read(getters.dashboardShowDrawer);
24 | export const readHasAdminAccess = read(getters.hasAdminAccess);
25 | export const readIsLoggedIn = read(getters.isLoggedIn);
26 | export const readLoginError = read(getters.loginError);
27 | export const readToken = read(getters.token);
28 | export const readUserProfile = read(getters.userProfile);
29 | export const readFirstNotification = read(getters.firstNotification);
30 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/main/index.ts:
--------------------------------------------------------------------------------
1 | import { mutations } from './mutations';
2 | import { getters } from './getters';
3 | import { actions } from './actions';
4 | import { MainState } from './state';
5 |
6 | const defaultState: MainState = {
7 | isLoggedIn: null,
8 | token: '',
9 | logInError: false,
10 | userProfile: null,
11 | dashboardMiniDrawer: false,
12 | dashboardShowDrawer: true,
13 | notifications: [],
14 | };
15 |
16 | export const mainModule = {
17 | state: defaultState,
18 | mutations,
19 | actions,
20 | getters,
21 | };
22 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/main/mutations.ts:
--------------------------------------------------------------------------------
1 | import { IUserProfile } from '@/interfaces';
2 | import { MainState, AppNotification } from './state';
3 | import { getStoreAccessors } from 'typesafe-vuex';
4 | import { State } from '../state';
5 |
6 |
7 | export const mutations = {
8 | setToken(state: MainState, payload: string) {
9 | state.token = payload;
10 | },
11 | setLoggedIn(state: MainState, payload: boolean) {
12 | state.isLoggedIn = payload;
13 | },
14 | setLogInError(state: MainState, payload: boolean) {
15 | state.logInError = payload;
16 | },
17 | setUserProfile(state: MainState, payload: IUserProfile) {
18 | state.userProfile = payload;
19 | },
20 | setDashboardMiniDrawer(state: MainState, payload: boolean) {
21 | state.dashboardMiniDrawer = payload;
22 | },
23 | setDashboardShowDrawer(state: MainState, payload: boolean) {
24 | state.dashboardShowDrawer = payload;
25 | },
26 | addNotification(state: MainState, payload: AppNotification) {
27 | state.notifications.push(payload);
28 | },
29 | removeNotification(state: MainState, payload: AppNotification) {
30 | state.notifications = state.notifications.filter((notification) => notification !== payload);
31 | },
32 | };
33 |
34 | const {commit} = getStoreAccessors('');
35 |
36 | export const commitSetDashboardMiniDrawer = commit(mutations.setDashboardMiniDrawer);
37 | export const commitSetDashboardShowDrawer = commit(mutations.setDashboardShowDrawer);
38 | export const commitSetLoggedIn = commit(mutations.setLoggedIn);
39 | export const commitSetLogInError = commit(mutations.setLogInError);
40 | export const commitSetToken = commit(mutations.setToken);
41 | export const commitSetUserProfile = commit(mutations.setUserProfile);
42 | export const commitAddNotification = commit(mutations.addNotification);
43 | export const commitRemoveNotification = commit(mutations.removeNotification);
44 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/main/state.ts:
--------------------------------------------------------------------------------
1 | import { IUserProfile } from '@/interfaces';
2 |
3 | export interface AppNotification {
4 | content: string;
5 | color?: string;
6 | showProgress?: boolean;
7 | }
8 |
9 | export interface MainState {
10 | token: string;
11 | isLoggedIn: boolean | null;
12 | logInError: boolean;
13 | userProfile: IUserProfile | null;
14 | dashboardMiniDrawer: boolean;
15 | dashboardShowDrawer: boolean;
16 | notifications: AppNotification[];
17 | }
18 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/store/state.ts:
--------------------------------------------------------------------------------
1 | import { MainState } from './main/state';
2 |
3 | export interface State {
4 | main: MainState;
5 | }
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const getLocalToken = () => localStorage.getItem('token');
2 |
3 | export const saveLocalToken = (token: string) => localStorage.setItem('token', token);
4 |
5 | export const removeLocalToken = () => localStorage.removeItem('token');
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{appName}}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Incorrect email or password
19 |
20 |
21 | Forgot your password?
22 |
23 |
24 |
25 | Login
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
56 |
57 |
59 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/PasswordRecovery.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{appName}} - Password Recovery
9 |
10 |
11 | A password recovery email will be sent to the registered account
12 |
13 |
14 |
15 |
16 |
17 |
18 | Cancel
19 |
20 | Recover Password
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
50 |
51 |
53 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/ResetPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{appName}} - Reset Password
9 |
10 |
11 | Enter your new password below
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Cancel
22 | Clear
23 | Save
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
85 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dashboard
6 |
7 |
8 | Welcome {{greetedUser}}
9 |
10 |
11 | View Profile
12 | Edit Profile
13 | Change Password
14 |
15 |
16 |
17 |
18 |
19 |
38 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/Start.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
39 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/Admin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
34 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/AdminUsers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Manage Users
6 |
7 |
8 | Create User
9 |
10 |
11 |
12 | {{ props.item.username }} |
13 | {{ props.item.email }} |
14 | {{ props.item.full_name }} |
15 | block |
16 |
17 | {{role}}
18 | |
19 |
20 |
21 | Edit
22 |
23 | edit
24 |
25 |
26 | |
27 |
28 |
29 |
30 |
31 |
32 |
85 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/admin/CreateUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Create User
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Roles
14 |
15 | Disable User (currently disabled)(currently enabled)
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Cancel
31 | Reset
32 |
33 | Save
34 |
35 |
36 |
37 |
38 |
39 |
40 |
115 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User Profile
6 |
7 |
8 |
9 |
Full Name
10 |
{{userProfile.full_name}}
11 |
-----
12 |
13 |
14 |
Username
15 |
{{userProfile.username}}
16 |
-----
17 |
18 |
19 |
Email
20 |
{{userProfile.email}}
21 |
-----
22 |
23 |
24 |
25 | Edit
26 | Change password
27 |
28 |
29 |
30 |
31 |
32 |
52 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Edit User Profile
6 |
7 |
8 |
9 |
10 |
Username
11 |
{{userProfile.username}}
12 |
-----
13 |
14 |
19 |
24 |
33 |
34 |
35 |
36 |
37 |
38 | Cancel
39 | Reset
40 |
44 | Save
45 |
46 |
47 |
48 |
49 |
50 |
51 |
103 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/src/views/main/profile/UserProfileEditPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Set Password
6 |
7 |
8 |
9 |
10 |
Username
11 |
{{userProfile.username}}
12 |
-----
13 |
14 |
15 |
25 |
26 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Cancel
43 | Reset
44 | Save
45 |
46 |
47 |
48 |
49 |
50 |
87 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/tests/unit/upload-button.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import UploadButton from '@/components/UploadButton.vue';
3 | import '@/plugins/vuetify';
4 |
5 | describe('UploadButton.vue', () => {
6 | it('renders props.title when passed', () => {
7 | const title = 'upload a file';
8 | const wrapper = shallowMount(UploadButton, {
9 | slots: {
10 | default: title,
11 | },
12 | });
13 | expect(wrapper.text()).toMatch(title);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": false,
4 | "target": "esnext",
5 | "module": "esnext",
6 | "strict": true,
7 | "jsx": "preserve",
8 | "importHelpers": true,
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "sourceMap": true,
14 | "baseUrl": ".",
15 | "types": [
16 | "webpack-env",
17 | "jest"
18 | ],
19 | "paths": {
20 | "@/*": [
21 | "src/*"
22 | ]
23 | },
24 | "lib": [
25 | "esnext",
26 | "dom",
27 | "dom.iterable",
28 | "scripthost"
29 | ]
30 | },
31 | "include": [
32 | "src/**/*.ts",
33 | "src/**/*.tsx",
34 | "src/**/*.vue",
35 | "tests/**/*.ts",
36 | "tests/**/*.tsx"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warning",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "linterOptions": {
7 | "exclude": [
8 | "node_modules/**"
9 | ]
10 | },
11 | "rules": {
12 | "quotemark": [true, "single"],
13 | "indent": [true, "spaces", 2],
14 | "interface-name": false,
15 | "ordered-imports": false,
16 | "object-literal-sort-keys": false,
17 | "no-consecutive-blank-lines": false
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // Fix Vuex-typescript in prod: https://github.com/istrib/vuex-typescript/issues/13#issuecomment-409869231
3 | configureWebpack: (config) => {
4 | if (process.env.NODE_ENV === 'production') {
5 | config.optimization.minimizer[0].options.terserOptions = Object.assign(
6 | {},
7 | config.optimization.minimizer[0].options.terserOptions,
8 | {
9 | ecma: 5,
10 | compress: {
11 | keep_fnames: true,
12 | },
13 | warnings: false,
14 | mangle: {
15 | keep_fnames: true,
16 | },
17 | },
18 | );
19 | }
20 | },
21 | chainWebpack: config => {
22 | config.module
23 | .rule('vue')
24 | .use('vue-loader')
25 | .loader('vue-loader')
26 | .tap(options => Object.assign(options, {
27 | transformAssetUrls: {
28 | 'v-img': ['src', 'lazy-src'],
29 | 'v-card': 'src',
30 | 'v-card-media': 'src',
31 | 'v-responsive': 'src',
32 | }
33 | }));
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/scripts/build-push.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | TAG=${TAG} \
7 | FRONTEND_ENV=${FRONTEND_ENV-production} \
8 | source ./scripts/build.sh
9 |
10 | docker-compose -f docker-stack.yml push
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | TAG=${TAG} \
7 | FRONTEND_ENV=${FRONTEND_ENV-production} \
8 | docker-compose \
9 | -f docker-compose.deploy.build.yml \
10 | -f docker-compose.deploy.images.yml \
11 | config > docker-stack.yml
12 |
13 | docker-compose -f docker-stack.yml build
14 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/scripts/deploy.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | DOMAIN=${DOMAIN} \
7 | TRAEFIK_TAG=${TRAEFIK_TAG} \
8 | STACK_NAME=${STACK_NAME} \
9 | TAG=${TAG} \
10 | docker-compose \
11 | -f docker-compose.shared.admin.yml \
12 | -f docker-compose.shared.base-images.yml \
13 | -f docker-compose.shared.depends.yml \
14 | -f docker-compose.shared.env.yml \
15 | -f docker-compose.deploy.command.yml \
16 | -f docker-compose.deploy.images.yml \
17 | -f docker-compose.deploy.labels.yml \
18 | -f docker-compose.deploy.networks.yml \
19 | -f docker-compose.deploy.volumes-placement.yml \
20 | config > docker-stack.yml
21 |
22 | docker-auto-labels docker-stack.yml
23 |
24 | docker stack deploy -c docker-stack.yml --with-registry-auth ${STACK_NAME}
25 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/scripts/test-local.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | if [ $(uname -s) = "Linux" ]; then
7 | echo "Remove __pycache__ files"
8 | sudo find . -type d -name __pycache__ -exec rm -r {} \+
9 | fi
10 |
11 | docker-compose \
12 | -f docker-compose.test.yml \
13 | -f docker-compose.shared.admin.yml \
14 | -f docker-compose.shared.base-images.yml \
15 | -f docker-compose.shared.depends.yml \
16 | -f docker-compose.shared.env.yml \
17 | -f docker-compose.dev.build.yml \
18 | -f docker-compose.dev.env.yml \
19 | -f docker-compose.dev.labels.yml \
20 | -f docker-compose.dev.networks.yml \
21 | -f docker-compose.dev.ports.yml \
22 | -f docker-compose.dev.volumes.yml \
23 | config > docker-stack.yml
24 |
25 | # -f docker-compose.dev.command.yml \
26 |
27 | docker-compose -f docker-stack.yml build
28 | docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
29 | docker-compose -f docker-stack.yml up -d
30 | docker-compose -f docker-stack.yml exec -T backend-tests /tests-start.sh "$@"
31 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | DOMAIN=backend \
7 | docker-compose \
8 | -f docker-compose.shared.base-images.yml \
9 | -f docker-compose.shared.env.yml \
10 | -f docker-compose.shared.depends.yml \
11 | -f docker-compose.deploy.build.yml \
12 | -f docker-compose.test.yml \
13 | config > docker-stack.yml
14 |
15 | docker-compose -f docker-stack.yml build
16 | docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
17 | docker-compose -f docker-stack.yml up -d
18 | docker-compose -f docker-stack.yml exec -T backend-tests /tests-start.sh "$@"
19 | docker-compose -f docker-stack.yml down -v --remove-orphans
20 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/sync-gateway/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM couchbase/sync-gateway:2.1.0-community
2 |
3 | COPY /entrypoint.sh /
4 | RUN chmod +x ./entrypoint.sh
5 |
6 | COPY /create_config.py /
7 | COPY /sync/ /sync/
8 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/sync-gateway/create_config.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import json
4 | import logging
5 |
6 | logging.basicConfig(level=logging.INFO)
7 |
8 |
9 | def getenv_boolean(var_name, default_value=False):
10 | result = default_value
11 | env_value = os.getenv(var_name)
12 | if env_value is not None:
13 | result = env_value.upper() in ("TRUE", "1")
14 | return result
15 |
16 |
17 | # Env vars for configs
18 | COUCHBASE_HOST = os.getenv(
19 | "COUCHBASE_HOST", ""
20 | ) # e.g.: "couchbase". Leave empty to use default Walrus
21 | COUCHBASE_PORT = os.getenv("COUCHBASE_PORT", "8091")
22 | COUCHBASE_BUCKET_NAME = os.getenv("COUCHBASE_BUCKET_NAME", "app")
23 | COUCHBASE_SYNC_GATEWAY_LOG = os.getenv(
24 | "COUCHBASE_SYNC_GATEWAY_LOG", "HTTP+"
25 | ) # e.g.: "*"
26 | COUCHBASE_SYNC_GATEWAY_ADMIN_INTERFACE = os.getenv(
27 | "COUCHBASE_SYNC_GATEWAY_ADMIN_INTERFACE", ""
28 | ) # e.g.: ":4985", leave empty to disable
29 | COUCHBASE_SYNC_GATEWAY_USER = os.getenv("COUCHBASE_SYNC_GATEWAY_USER", "")
30 | COUCHBASE_SYNC_GATEWAY_PASSWORD = os.getenv("COUCHBASE_SYNC_GATEWAY_PASSWORD", "")
31 | COUCHBASE_SYNC_GATEWAY_DATABASE = os.getenv("COUCHBASE_SYNC_GATEWAY_DATABASE", "db")
32 | COUCHBASE_SYNC_GATEWAY_CORS_ORIGINS = os.getenv(
33 | "COUCHBASE_SYNC_GATEWAY_CORS_ORIGINS", ""
34 | )
35 | COUCHBASE_SYNC_GATEWAY_NUM_INDEX_REPLICAS = int(
36 | os.getenv("COUCHBASE_SYNC_GATEWAY_NUM_INDEX_REPLICAS", "0")
37 | )
38 | COUCHBASE_SYNC_GATEWAY_DISABLE_GUEST_USER = getenv_boolean(
39 | "COUCHBASE_SYNC_GATEWAY_DISABLE_GUEST_USER", default_value=False
40 | )
41 |
42 | # Base config
43 | logging.info("Generating base config")
44 | config_dict = {
45 | "interface": ":4984",
46 | "logging": {"console": {"log_keys": [COUCHBASE_SYNC_GATEWAY_LOG]}},
47 | "databases": {},
48 | }
49 |
50 | # Admin interface
51 | logging.info("Checking adminInterface config config")
52 | if COUCHBASE_SYNC_GATEWAY_ADMIN_INTERFACE:
53 | logging.info("Generating adminInterface config config")
54 | config_dict["adminInterface"] = COUCHBASE_SYNC_GATEWAY_ADMIN_INTERFACE
55 |
56 | # CORS
57 | logging.info("Checking CORS config")
58 | if COUCHBASE_SYNC_GATEWAY_CORS_ORIGINS:
59 | cors_list = COUCHBASE_SYNC_GATEWAY_CORS_ORIGINS.split(",")
60 | use_cors = [origin.strip() for origin in cors_list]
61 |
62 | logging.info("Generating CORS config")
63 | config_dict["CORS"] = {
64 | "Origin": use_cors,
65 | "LoginOrigin": use_cors,
66 | "Headers": ["Content-Type"],
67 | "MaxAge": 17280000,
68 | }
69 |
70 | # Couchbase
71 | logging.info("Checking Couchbase config")
72 | if COUCHBASE_HOST and COUCHBASE_SYNC_GATEWAY_USER and COUCHBASE_SYNC_GATEWAY_PASSWORD:
73 | logging.info("Generating Couchbase config")
74 | use_server = "http://{COUCHBASE_HOST}:{COUCHBASE_PORT}".format(
75 | COUCHBASE_HOST=COUCHBASE_HOST, COUCHBASE_PORT=COUCHBASE_PORT
76 | )
77 |
78 | config_dict["databases"][COUCHBASE_SYNC_GATEWAY_DATABASE] = {
79 | "server": use_server,
80 | "bucket": COUCHBASE_BUCKET_NAME,
81 | "username": COUCHBASE_SYNC_GATEWAY_USER,
82 | "password": COUCHBASE_SYNC_GATEWAY_PASSWORD,
83 | "num_index_replicas": COUCHBASE_SYNC_GATEWAY_NUM_INDEX_REPLICAS,
84 | "enable_shared_bucket_access": True,
85 | "import_docs": "continuous",
86 | "users": {"GUEST": {"disabled": COUCHBASE_SYNC_GATEWAY_DISABLE_GUEST_USER}},
87 | }
88 | else:
89 | config_dict["databases"][COUCHBASE_SYNC_GATEWAY_DATABASE] = {
90 | "server": "walrus:/opt/couchbase-sync-gateway/data",
91 | "users": {
92 | "GUEST": {
93 | "disabled": COUCHBASE_SYNC_GATEWAY_DISABLE_GUEST_USER,
94 | "admin_channels": ["*"],
95 | }
96 | },
97 | }
98 |
99 | # JS sync function
100 | logging.info("Checking JavaScript sync function file")
101 | js_function_path = "/sync/sync-function.js"
102 | sync_function = ""
103 | logging.info("Checking JS sync function from {}".format(js_function_path))
104 | if os.path.isfile(js_function_path):
105 | logging.info("Reading JavaScript sync function file")
106 | with open(js_function_path) as f:
107 | sync_function = f.read()
108 | if sync_function:
109 | logging.info("Generating JavaScript sync function config")
110 | config_dict["databases"][COUCHBASE_SYNC_GATEWAY_DATABASE]["sync"] = sync_function
111 |
112 | # Generate JSON config
113 | logging.info("Writing final config JSON file")
114 | with open("/etc/sync_gateway/config.json", "w") as f:
115 | json.dump(config_dict, f, indent=2)
116 | logging.info("Done")
117 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/sync-gateway/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | set -e
4 |
5 | # Check connection in a separate function, that way, when
6 | # there's no connection available yet and the return of the command
7 | # is non-zero, the script doesn't exit yet
8 | check_connection() {
9 | curl -Is http://${COUCHBASE_SYNC_GATEWAY_USER}:${COUCHBASE_SYNC_GATEWAY_PASSWORD}@${COUCHBASE_HOST}:${COUCHBASE_PORT}/pools/default/buckets/${COUCHBASE_BUCKET_NAME} 2>&1 | grep "HTTP/1.1 200 OK" > /dev/null
10 | if [ $? -eq 0 ]; then
11 | echo "success";
12 | else
13 | echo "error";
14 | fi;
15 | }
16 |
17 | # Try to connect to Couchbase only if COUCHBASE_HOST was declared as an env var
18 | if [ ! -z $COUCHBASE_HOST ]; then
19 | # Try once per second, up to 300 times (5 min)
20 | SECONDS_TO_TRY=300
21 | for i in $(seq 1 $SECONDS_TO_TRY); do
22 | echo "Checking connection, trial ${i}";
23 | result=$(check_connection);
24 | if [ $result == "success" ]; then
25 | echo "Success: connection checked";
26 | sleep 1;
27 | break;
28 | else
29 | echo "Connection not available yet, sleeping 1 sec...";
30 | sleep 1;
31 | echo "----------";
32 | fi;
33 | done;
34 | fi;
35 |
36 |
37 | python create_config.py
38 |
39 | LOGFILE_DIR=/var/log/sync_gateway
40 | mkdir -p $LOGFILE_DIR
41 |
42 | LOGFILE_ACCESS=$LOGFILE_DIR/sync_gateway_access.log
43 | LOGFILE_ERROR=$LOGFILE_DIR/sync_gateway_error.log
44 |
45 | # Run SG and use tee to append stdout and stderr to separate logfiles
46 | # Process substitution described here: https://stackoverflow.com/a/692407
47 | exec sync_gateway "$@" > >(tee -a $LOGFILE_ACCESS) 2> >(tee -a $LOGFILE_ERROR >&2)
48 |
--------------------------------------------------------------------------------
/{{cookiecutter.project_slug}}/sync-gateway/sync/sync-function.js:
--------------------------------------------------------------------------------
1 | function (doc, oldDoc) {
2 | requireAdmin();
3 | channel(doc.channels);
4 | }
5 |
--------------------------------------------------------------------------------