├── .gitignore ├── README.md ├── cookiecutter.json ├── hooks └── post_gen_project.py └── {{cookiecutter.project_name}} ├── .gitignore ├── back ├── .dockerignore ├── .gitignore ├── Dockerfile ├── __init__.py ├── clock.py ├── config │ ├── config.dev.json │ └── tree.json ├── requirements.txt ├── run.py ├── templates │ ├── confirm_email.html │ └── reset_password.html ├── worker.py └── {{cookiecutter.project_name}} │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── errors.py │ ├── jobs.py │ ├── social │ │ ├── __init__.py │ │ ├── me.py │ │ └── users.py │ └── utils │ │ ├── __init__.py │ │ └── tree.py │ ├── app.py │ ├── auth │ ├── __init__.py │ ├── email_login.py │ ├── facebook_login.py │ ├── google_login.py │ └── utils.py │ ├── config.py │ ├── core │ ├── __init__.py │ ├── cache.py │ ├── config.py │ ├── database.py │ ├── elasticsearch.py │ ├── mail.py │ ├── storage.py │ └── utils │ │ └── __init__.py │ ├── exceptions │ ├── __init__.py │ ├── passwords.py │ └── users.py │ ├── managers │ ├── __init__.py │ └── social │ │ ├── __init__.py │ │ └── users.py │ ├── models │ ├── __init__.py │ └── social │ │ ├── __init__.py │ │ ├── fbcredentials.py │ │ ├── gcredentials.py │ │ └── user.py │ └── storage │ └── __init__.py ├── docker-compose.yml ├── front ├── .env.development ├── .gitignore ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── banner.jpg │ │ ├── banner_dark.jpg │ │ ├── grey_background.jpeg │ │ └── logos │ │ │ ├── elastic.png │ │ │ ├── email.png │ │ │ ├── facebook.png │ │ │ ├── facebook_blue.png │ │ │ ├── google.png │ │ │ ├── minio.png │ │ │ ├── postgre.png │ │ │ ├── redis.png │ │ │ └── vue.png │ ├── components │ │ ├── account │ │ │ ├── ModifyAccount.vue │ │ │ └── ViewAccount.vue │ │ ├── auth │ │ │ ├── EmailLogin.vue │ │ │ ├── FacebookLogin.vue │ │ │ └── GoogleLogin.vue │ │ ├── home │ │ │ ├── AuthType.vue │ │ │ ├── BackendStack.vue │ │ │ ├── MainStack.vue │ │ │ └── WelcomeScreen.vue │ │ └── util │ │ │ ├── Footer.vue │ │ │ ├── Header.vue │ │ │ ├── Jobs.vue │ │ │ ├── ProfileMenu.vue │ │ │ └── UploadButton.vue │ ├── main.js │ ├── modules │ │ ├── auth │ │ │ ├── email.js │ │ │ ├── facebook.js │ │ │ ├── google.js │ │ │ ├── index.js │ │ │ └── util.js │ │ ├── jobs │ │ │ └── index.js │ │ ├── notifications │ │ │ └── index.js │ │ └── router │ │ │ └── index.js │ ├── pages │ │ ├── About.vue │ │ ├── Account.vue │ │ ├── Home.vue │ │ └── auth │ │ │ ├── ForgotPassword.vue │ │ │ ├── Login.vue │ │ │ ├── ResetPassword.vue │ │ │ └── callback │ │ │ ├── FacebookCallback.vue │ │ │ └── GoogleCallback.vue │ └── plugins │ │ └── vuetify.js └── vue.config.js ├── manifest.yml └── screenshots ├── forgotpassword.png ├── full_gif.gif ├── home.png ├── login.gif ├── login.png ├── resetpassword.png └── signup.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | .DS_Store/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Full stack app template 3 | 4 | A ready-to-use and customizable web app template with VueJs for frontend and Flask for backend, running on https. Whether 5 | you are a beginner or an experienced developer, launch your app and start developing your first feature in one hour! 6 | 7 | ![Banner](%7B%7Bcookiecutter.project_name%7D%7D/front/src/assets/banner_dark.jpg) 8 | 9 | 10 | ## Table of Contents 11 | 12 | 13 | * [About the template](#about-the-template) 14 | * [Front end](#front-end) 15 | * [Back end](#back-end) 16 | * [Authentication](#authentication) 17 | * [Getting started](#getting-started) 18 | * [Requirements](#requirements) 19 | * [Installing the template](#installing-the-template) 20 | * [Configuration](#configuration) 21 | * [Setting up HTTPS](#setting-up-https) 22 | * [Setting up authentication](#setting-up-authentication) 23 | * [Email module](#email-module) 24 | * [Facebook module](#facebook-module) 25 | * [Google module](#google-module) 26 | * [Run the app](#run-the-app) 27 | * [Launch the frontend](#launch-the-frontend) 28 | * [Launch the backend](#launch-the-backend) 29 | 30 | 31 | 32 | ## About the template 33 | ### Front end 34 | The front end runs on `https://localhost:8080`. It is a [Vue CLI 3](https://cli.vuejs.org/) project that uses the v2 of the CSS library 35 | [Vuetify](https://vuetifyjs.com/en/). The landing page is the login page. Another template will be released soon for apps 36 | that don't necessarily require users to authenticate. 37 | 38 | An authentication module is implemented, as well as a convenient global notification system to improve user experience. 39 | The front end also contains a job module, to retrieve jobs from the backend and display their progress. 40 | 41 | ├── front 42 | ├── public # Config files 43 | │ ├── favicion.ico # App icon 44 | │ └── index.html # Main html file 45 | ├── src # Frontend main folder 46 | │ ├── assets # Media and fonts 47 | │ ├── components # Custom components files 48 | │ │ ├── auth # Auth components 49 | │ │ │ ├── EmailLogin.vue # Email login component 50 | │ │ │ ├── FacebookLogin.vue # Facebook login component 51 | │ │ │ └── GoogleLogin.vue # Google login component 52 | │ │ └── util # Util components 53 | │ │ ├── Header.vue # Header component 54 | │ │ └── Jobs.vue Vue router # Jobs component 55 | │ ├── modules # Useful modules 56 | │ │ ├── auth # Auth files 57 | │ │ │ ├── email.js # Email auth functions 58 | │ │ │ ├── facebook.js # Facebook auth functions 59 | │ │ │ ├── google.js # Google auth functions 60 | │ │ │ └── util.js # Common auth functions 61 | │ │ ├── jobs # Job store functions 62 | │ │ ├── notifications # Notification store function 63 | │ │ └── router # Vue router 64 | │ ├── pages # Pages of the app 65 | │ │ ├── auth # Auth pages 66 | │ │ │ ├── callback # Callback pages 67 | │ │ │ │ ├── FacebookCallback.vue # Facebook auth callback page 68 | │ │ │ │ └── GoogleCallback.vue # Google auth callback page 69 | │ │ │ └── Login.vue # Login page 70 | │ │ └── Home.vue # Home page 71 | │ ├── plugins # App plugins 72 | │ │ └── vuetify.js # Vuetify setup 73 | │ ├── App.vue # Vue app 74 | │ └── main.js # Create app 75 | ├── .env.development # Development environment 76 | ├── babel.config.js # Babel configuration 77 | ├── package.json # Packages and dependencies 78 | ├── package-lock.json # Packages and dependencies 79 | └── vue.config.js # Vue configuration 80 | 81 | ### Back end 82 | The back end runs on `https://localhost:5000`. It uses docker containers to run an api, a worker (jobs are queued with 83 | [RQ](https://python-rq.org/)) and a scheduler. It contains the following: 84 | * [PostgreSQL](https://www.postgresql.org/) database 85 | * [Elasticsearch](https://www.elastic.co/start) database plugged with a [Kibana](https://www.elastic.co/products/kibana) 86 | interface 87 | * [Minio](https://min.io/) storage 88 | * [Redis](https://redis.io/) cache 89 | * Email module to send emails 90 | 91 | Like the front end, it contains an authentication module, as well as a global customizable error handler. 92 | 93 | ├── back 94 | ├── config # Config files 95 | │ └── config.dev.json # Development config 96 | ├── {{cookiecutter.project_name}} # Backend main folder 97 | │ ├── api # Routes registration 98 | │ │ ├── errors.py # Exceptions handler 99 | │ │ └── jobs.py # Job retriever 100 | │ ├── auth # Auth files 101 | │ │ ├── email_login.py # Email auth blueprint 102 | │ │ ├── facebook_login.py # Facebook auth blueprint 103 | │ │ └── google_login.py # Google auth blueprint 104 | │ ├── core # Architecture files 105 | │ │ ├── cache.py # Redis cache 106 | │ │ ├── config.py # Config object 107 | │ │ ├── database.py # PostgreSQL database 108 | │ │ ├── elasticsearch.py # Elacticsearch database 109 | │ │ ├── mail.py # Email module 110 | │ │ └── storage.py # Minio storage 111 | │ ├── exceptions # Customized exceptions 112 | │ ├── managers # Functions to interact with resources 113 | │ ├── models # Resources 114 | │ ├── storage # Storage blueprint 115 | │ ├── app.py # Create app 116 | │ └── config.py # JWT config 117 | ├── templates # Html templates for emails 118 | ├── DockerFile # Launch script 119 | ├── requirements.txt # Packages and dependencies 120 | ├── run.py # Run the app 121 | └── worker.py # Register the worker 122 | 123 | ### Authentication 124 | No need to spend some precious time on authentication! The template contains a full built-in JWT module that allows three 125 | types of authentication: 126 | * Email login 127 | * [Google OAuth 2.0](https://developers.google.com/identity/protocols/OAuth2) login 128 | * [Facebook OAuth 2.0](https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow) login 129 | 130 | We will see how to quickly [set up the auth module](#setting-up-authentication) with your own credentials for Google and Facebook 131 | and with your own email address to send emails to activate an account or to change a password for example. 132 | 133 | ![Login gif](%7B%7Bcookiecutter.project_name%7D%7D/screenshots/login.gif) 134 | 135 | ## Getting started 136 | ### Requirements 137 | You will need the following to run the template: 138 | * [Docker Engine](https://docs.docker.com/install/#server) and [Docker Compose](https://docs.docker.com/compose/install/) 139 | * [Python 3.6](https://www.python.org/downloads/release/python-360/) or higher 140 | * [NodeJs 10](https://nodejs.org/en/download/) or higher and [npm](https://www.npmjs.com/get-npm) (npm goes with Node, no need to do anything) 141 | * [Cookicutter Python package](https://cookiecutter.readthedocs.io/en/1.7.0/installation.html) 142 | 143 | ### Installing the template 144 | Clone this repository on your machine by running 145 | ``` 146 | $ cookiecutter https://github.com/antoinebrtd/flask-vue-template.git 147 | ``` 148 | 149 | You wil be prompted some questions about your options, and the template will be cloned with your desired ones! 150 | 151 | That's it! Now let's configure your app. 152 | 153 | ### Configuration 154 | #### Setting up HTTPS 155 | > **Note**: You can skip this part if you don't want your app running under Https locally. Just remove the `devServer` 156 | field in `front/vue.config.js`, and modify the localhost urls in `back/config/config.dev.json` and `front/.env.development`. 157 | Know that Facebook login might not work under Http. 158 | 159 | To run the app under Https in development mode, you will need to create your own certificates. 160 | 161 | Install [mkcert](https://github.com/FiloSottile/mkcert) for your OS. Initialize it with 162 | ``` 163 | $ mkcert -install 164 | ``` 165 | 166 | Then create a `certs` folder in the `front` folder. You can create your self-signed certificates for the frontend server by running 167 | ``` 168 | $ cd ~//front/certs && mkcert localhost 169 | ``` 170 | This will create two files, `localhost.pem` and `localhost-key.pem`, in `front/certs`. 171 | 172 | Repeat the operation for the backend server: create a `certs` folder in the `back` folder, then run 173 | ``` 174 | $ cd ../../back/certs && mkcert localhost 175 | ``` 176 | 177 | Https is now configured! 178 | 179 | ### Setting up authentication 180 | > **Warning**: The files `config.json`, `facebook.json` and `google.json` you will create in this section contain your credentials. They are ignored in the repo tree and 181 | should never be pushed on Github. There is no need to edit the file `config.dev.json`, also make sure not to add any credentials in it, since 182 | this one is pushed on Github. 183 | 184 | The authentication module is already integrated in the template. All you need to do is to make it work with your own credentials. 185 | 186 | ##### Email module 187 | In `back/config`, create a file named `config.json`, and copy paste the content of `config.dev.json` in it. 188 | 189 | Modify `config.json` to add your email and password in the `email` section. Then, in `back/app/core/mail.py`, modify line 62 with your email 190 | and the name you want to appear in mailboxes. 191 | 192 | > **Note**: If you are using a Gmail address, make sure to [authorize apps](https://devanswers.co/allow-less-secure-apps-access-gmail-account/). 193 | 194 | ##### Facebook module 195 | Log in your [Facebook developer account](https://developers.facebook.com/), and create an new app ID. Then, under products, add the Facebook login product. 196 | In settings, add `localhost` in app domains field. 197 | 198 | In `back/config`, create a file named `facebook.json`, and copy paste the following, 199 | replacing with your credentials: 200 | ``` 201 | { 202 | "app_id": "your_app_id", 203 | "project_id": "your_project_name", 204 | "auth_uri": "https://www.facebook.com/v5.0/dialog/oauth", 205 | "token_uri": "https://graph.facebook.com/v5.0/oauth/access_token", 206 | "client_secret": "your_client_secret_key", 207 | "app_token": "your_app_token", 208 | "redirect_uris": [ 209 | "http://localhost:5000/auth/facebook/callback", 210 | "https://localhost:5000/auth/facebook/callback" 211 | ], 212 | "javascript_origins": [ 213 | "http://localhost:8080", 214 | "https://localhost:8080" 215 | ] 216 | } 217 | ``` 218 | 219 | You can check how to [obtain an access token](https://developers.facebook.com/docs/facebook-login/access-tokens?locale=en_US#apptokens) for your app. 220 | 221 | ##### Google module 222 | Log in your [Google console platform](https://console.developers.google.com/apis/dashboard), and create a new project. 223 | Create an external authorization screen (just fill in your app name), then create some new OAuth credentials under credentials section. 224 | In the javascript origins field, add `http://localhost:8080` and `https://localhost:8080`. In redirect URIs, add `http://localhost:5000/auth/google/callback` 225 | and `https://localhost:5000/auth/google/callback`. Save and you're good to go! 226 | 227 | Just download the config file directly from the credentials section, rename it `google.json` and move it to `back/config`. Your file should look like this: 228 | ``` 229 | { 230 | "web": { 231 | "client_id": "your_app_id", 232 | "project_id": "your_project_name", 233 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 234 | "token_uri": "https://oauth2.googleapis.com/token", 235 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 236 | "client_secret": "your_client_secret_key", 237 | "redirect_uris": [ 238 | "http://localhost:5000/auth/google/callback", 239 | "https://localhost:5000/auth/google/callback" 240 | ], 241 | "javascript_origins": [ 242 | "http://localhost:8080", 243 | "https://localhost:8080" 244 | ] 245 | } 246 | } 247 | ``` 248 | 249 | 250 | Authentication module is set up! 251 | 252 | ### Run the app 253 | #### Launch the frontend 254 | Navigate to `front` folder, and install dependencies: 255 | ``` 256 | $ npm install 257 | ``` 258 | 259 | Start the development server: 260 | ``` 261 | $ npm start 262 | ``` 263 | 264 | #### Launch the backend 265 | From the `front` folder, run the following to launch the docker containers: 266 | ``` 267 | $ cd .. && docker-compose up -d 268 | ``` 269 | 270 | Create a virtual environment in the backend folder: 271 | ``` 272 | $ cd back && python3 -m venv /venv 273 | ``` 274 | 275 | Install the requirements in it: 276 | ``` 277 | $ source venv/bin/activate && pip install -r requirements.txt 278 | ``` 279 | 280 | Launch the api server, the worker and the scheduler: 281 | ``` 282 | $ python run.py 283 | $ rq worker -c .core.cache 284 | $ python clock.py 285 | ``` 286 | 287 | Your app is now running, enjoy! 288 | 289 | ![Home](%7B%7Bcookiecutter.project_name%7D%7D/screenshots/full_gif.gif) 290 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "myapp", 3 | "enable_https": ["y", "n"], 4 | "google_login": ["y", "n"], 5 | "facebook_login": ["y", "n"], 6 | "email_login": ["y", "n"], 7 | "elasticsearch": ["y", "n"], 8 | "storage": ["y", "n"], 9 | "_copy_without_render": [ 10 | "*.html" 11 | ] 12 | } -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import yaml 5 | 6 | MANIFEST = 'manifest.yml' 7 | 8 | 9 | def delete_resources_for_disabled_features(): 10 | print(os.getcwd()) 11 | with open(MANIFEST) as manifest_file: 12 | manifest = yaml.load(manifest_file) 13 | for feature in manifest['features']: 14 | if feature['enabled'] != 'y': 15 | print('removing resources for disabled feature {}...'.format(feature['name'])) 16 | for resource in feature['resources']: 17 | delete_resource(resource) 18 | print('cleanup complete, removing manifest...') 19 | delete_resource(MANIFEST) 20 | 21 | {%- if cookiecutter.facebook_login == 'n' and cookiecutter.google_login == 'n' %} 22 | delete_resource('../{{cookiecutter.project_name}}/front/src/pages/auth/callback/') 23 | {%- endif %} 24 | delete_resource('../.git/') 25 | 26 | 27 | def delete_resource(resource): 28 | if os.path.isfile(resource): 29 | print('removing file: {}'.format(resource)) 30 | os.remove(resource) 31 | elif os.path.isdir(resource): 32 | print('removing directory: {}'.format(resource)) 33 | shutil.rmtree(resource) 34 | 35 | 36 | if __name__ == '__main__': 37 | delete_resources_for_disabled_features() 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/ 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Generated files 15 | .idea/**/contentModel.xml 16 | 17 | # Sensitive or high-churn files 18 | .idea/**/dataSources/ 19 | .idea/**/dataSources.ids 20 | .idea/**/dataSources.local.xml 21 | .idea/**/sqlDataSources.xml 22 | .idea/**/dynamic.xml 23 | .idea/**/uiDesigner.xml 24 | .idea/**/dbnavigator.xml 25 | 26 | # Gradle 27 | .idea/**/gradle.xml 28 | .idea/**/libraries 29 | 30 | # Gradle and Maven with auto-import 31 | # When using Gradle or Maven with auto-import, you should exclude module files, 32 | # since they will be recreated, and may cause churn. Uncomment if using 33 | # auto-import. 34 | # .idea/modules.xml 35 | # .idea/*.iml 36 | # .idea/modules 37 | # *.iml 38 | # *.ipr 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | volumes/ 74 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | .static_storage/ 58 | .media/ 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | logs/ 108 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | .static_storage/ 58 | .media/ 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | config/config.json 109 | config/config.prod.json 110 | config/google.json 111 | config/facebook.json 112 | certs 113 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | RUN mkdir -p /app/config 4 | WORKDIR /app 5 | 6 | ADD requirements.txt . 7 | RUN pip install -r requirements.txt 8 | 9 | ADD config/tree.json /app/config/tree.json 10 | ADD config/config.prod.json /app/config/config.json 11 | {%- if cookiecutter.google_login == 'y' %} 12 | ADD config/google.json /app/config/google.json 13 | {%- endif %} 14 | {%- if cookiecutter.facebook_login == 'y' %} 15 | ADD config/facebook.json /app/config/facebook.json 16 | {%- endif %} 17 | ENV TREE_FILE /app/config/tree.json 18 | ENV CONFIG_FILE /app/config/config.json 19 | {%- if cookiecutter.google_login == 'y' %} 20 | ENV GOOGLE_CONFIG_FILE /app/config/google.json 21 | {%- endif %} 22 | {%- if cookiecutter.facebook_login == 'y' %} 23 | ENV FACEBOOK_CONFIG_FILE /app/config/facebook.json 24 | {%- endif %} 25 | 26 | ADD run.py /app/ 27 | ADD worker.py /app/ 28 | 29 | ADD {{cookiecutter.project_name}} /app/template 30 | 31 | CMD ["gunicorn","-w","1","--bind","0.0.0.0:8000","run:app"] 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/back/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/clock.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/back/clock.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/config/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | {%- if cookiecutter.enable_https == 'y' %} 3 | "back_root_url": "https://localhost:5000", 4 | {%- endif %} 5 | {%- if cookiecutter.enable_https != 'y' %} 6 | "back_root_url": "http://localhost:5000", 7 | {%- endif %} 8 | {%- if cookiecutter.enable_https == 'y' %} 9 | "front_root_url": "https://localhost:8080/#", 10 | {%- endif %} 11 | {%- if cookiecutter.enable_https != 'y' %} 12 | "front_root_url": "http://localhost:8080/#", 13 | {%- endif %} 14 | "database": { 15 | "host": "localhost", 16 | "user": "{{cookiecutter.project_name}}", 17 | "password": "{{cookiecutter.project_name}}" 18 | }, 19 | {%- if cookiecutter.elasticsearch == 'y' %} 20 | "aws": { 21 | "access_key": "AKIAIOSFODNN7EXAMPLE", 22 | "private_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 23 | "endpoint": "http://localhost:9000/", 24 | "elastic": "http://localhost:9200/" 25 | }, 26 | {%- endif %} 27 | "cache": { 28 | "host": "localhost", 29 | "url": "redis://localhost:6379/1" 30 | }, 31 | {%- if cookiecutter.storage == 'y' %} 32 | "storage": { 33 | "access_key": "AKIAIOSFODNN7EXAMPLE", 34 | "private_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 35 | "endpoint": "http://localhost:9000/", 36 | "bucket": "template-storage" 37 | }, 38 | {%- endif %} 39 | {%- if cookiecutter.google_login == 'y' or cookiecutter.facebook_login == 'y' %} 40 | "oauth": { 41 | {%- if cookiecutter.google_login == 'y' %} 42 | "google": { 43 | "google_config": "./config/google.json", 44 | {%- if cookiecutter.enable_https == 'y' %} 45 | "callback": "https://localhost:5000/auth/google/callback", 46 | "front_callback": "https://localhost:8080/#/auth/google/callback" 47 | {%- endif %} 48 | {%- if cookiecutter.enable_https != 'y' %} 49 | "callback": "http://localhost:5000/auth/google/callback", 50 | "front_callback": "http://localhost:8080/#/auth/google/callback" 51 | {%- endif %} 52 | }{%- if cookiecutter.facebook_login == 'y' %},{%- endif %} 53 | {%- endif %} 54 | {%- if cookiecutter.facebook_login == 'y' %} 55 | "facebook": { 56 | "facebook_config": "./config/facebook.json", 57 | {%- if cookiecutter.enable_https == 'y' %} 58 | "callback": "https://localhost:5000/auth/facebook/callback", 59 | "front_callback": "https://localhost:8080/#/auth/facebook/callback", 60 | {%- endif %} 61 | {%- if cookiecutter.enable_https != 'y' %} 62 | "callback": "http://localhost:5000/auth/facebook/callback", 63 | "front_callback": "http://localhost:8080/#/auth/facebook/callback", 64 | {%- endif %} 65 | "state_key": "some_key" 66 | } 67 | {%- endif %} 68 | }, 69 | {%- endif %} 70 | {%- if cookiecutter.email_login == 'y' %} 71 | "email_auth": { 72 | "hash_key": "some_key", 73 | "activation_key": "some_key", 74 | "activation_password": "some_password", 75 | "reset_key": "some_key", 76 | "reset_password": "some_password" 77 | }, 78 | {%- endif %} 79 | "email": { 80 | "my_email@email.com": "my_email_password" 81 | }, 82 | "log": "DEBUG", 83 | "flask": { 84 | "env": "development", 85 | "debug": true, 86 | "oauth_secret": "test_token", 87 | "jwt_secret": "test_token", 88 | "jwt_expiration": 2592000 89 | } 90 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==6.0.0 2 | arrow==0.13.1 3 | boto3==1.9.150 4 | botocore==1.12.150 5 | cachetools==3.1.0 6 | certifi==2019.3.9 7 | chardet==3.0.4 8 | Click==7.0 9 | docutils==0.14 10 | elasticsearch==7.0.2 11 | Flask==1.0.2 12 | Flask-Cors==3.0.7 13 | Flask-JWT-Extended==3.18.2 14 | Flask-RESTful==0.3.7 15 | google-api-python-client==1.7.8 16 | google-auth==1.6.3 17 | google-auth-httplib2==0.0.3 18 | google-auth-oauthlib==0.3.0 19 | httplib2==0.18.0 20 | idna==2.8 21 | itsdangerous==1.1.0 22 | Jinja2==2.10.1 23 | jmespath==0.9.4 24 | MarkupSafe==1.1.1 25 | oauthlib==3.0.1 26 | peewee==3.9.5 27 | pendulum==2.0.4 28 | psycopg2-binary==2.8.2 29 | pyasn1==0.4.5 30 | pyasn1-modules==0.2.5 31 | PyJWT==1.7.1 32 | python-dateutil==2.8.0 33 | pytz==2019.1 34 | pytzdata==2019.1 35 | redis==3.2.1 36 | requests==2.22.0 37 | requests-aws4auth==0.9 38 | requests-oauthlib==1.2.0 39 | rq==1.0 40 | rq-dashboard==0.5.1 41 | rsa==4.0 42 | s3transfer==0.2.0 43 | six==1.12.0 44 | uritemplate==3.0.0 45 | urllib3==1.24.3 46 | Werkzeug==0.15.4 47 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/run.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_name}}.core import logger 2 | from {{cookiecutter.project_name}} import create_app 3 | 4 | app = create_app() 5 | 6 | if __name__ == '__main__': 7 | logger.info('Starting Template API ...') 8 | app.run(host='0.0.0.0', port=5000, threaded=True{%- if cookiecutter.enable_https == 'y' %}, ssl_context=('certs/localhost.pem', 'certs/localhost-key.pem'){%- endif %}) 9 | logger.info('End of Template') 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/templates/confirm_email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Confirm Email 6 | 7 | 8 | 9 |
 
10 |
11 | 12 | 15 | Activate your account 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/templates/reset_password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reset your password 6 | 7 | 8 | 9 |
 
10 |
11 | 12 | 15 | Reset your password 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/worker.py: -------------------------------------------------------------------------------- 1 | from rq.worker import Worker 2 | 3 | from {{cookiecutter.project_name}} import create_app 4 | from {{cookiecutter.project_name}}.core import db 5 | 6 | 7 | class AdvancedWorker(Worker): 8 | def __init__(self, *args, **kwargs): 9 | self.app = create_app(api=False) 10 | super(AdvancedWorker, self).__init__(*args, **kwargs) 11 | 12 | def work(self, *args, **kwargs): 13 | with self.app.app_context(): 14 | return super(AdvancedWorker, self).work(*args, **kwargs) 15 | 16 | @db.connection_context() 17 | def execute_job(self, *args, **kwargs): 18 | return super(AdvancedWorker, self).execute_job(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import create_app 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from flask import Blueprint, current_app, g, request 4 | from flask_restful import Api 5 | 6 | from {{cookiecutter.project_name}}.api.errors import register_errors 7 | from {{cookiecutter.project_name}}.core import logger, db 8 | 9 | api_bp = Blueprint('api', __name__) 10 | 11 | 12 | class AdvancedApi(Api): 13 | def handle_error(self, e): 14 | for val in current_app.error_handler_spec.values(): 15 | for handler in val.values(): 16 | registered_error_handlers = list(filter(lambda x: isinstance(e, x), handler.keys())) 17 | if len(registered_error_handlers) > 0: 18 | raise e 19 | return super().handle_error(e) 20 | 21 | 22 | api = AdvancedApi(api_bp) 23 | 24 | 25 | def register_api(app): 26 | @api_bp.before_request 27 | def before_request(): 28 | g.start = time.time() 29 | db.connect(reuse_if_open=True) 30 | 31 | @api_bp.teardown_request 32 | def after_request(exception=None): 33 | db.close() 34 | diff = time.time() - g.start 35 | logger.info('[Request time] Path : {} {} | Time : {}s'.format(request.method, request.full_path, diff)) 36 | 37 | import {{cookiecutter.project_name}}.api.jobs 38 | import {{cookiecutter.project_name}}.api.social 39 | import {{cookiecutter.project_name}}.api.utils 40 | 41 | register_errors(api_bp) 42 | 43 | app.register_blueprint(api_bp, url_prefix="/api/v1") 44 | 45 | logger.debug('Blueprints successfully registered.') 46 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/errors.py: -------------------------------------------------------------------------------- 1 | from elasticsearch import NotFoundError 2 | from flask import jsonify 3 | from peewee import DoesNotExist 4 | 5 | from {{cookiecutter.project_name}}.exceptions import APIError 6 | 7 | 8 | def register_errors(api): 9 | @api.errorhandler(APIError) 10 | def handle_invalid_usage(error): 11 | response = jsonify(error.to_dict()) 12 | response.status_code = error.status_code 13 | return response 14 | 15 | @api.errorhandler(DoesNotExist) 16 | def handle_invalid_usage(error): 17 | resource = error.args[0].split('>')[0].split(' ')[1] 18 | response = jsonify({"msg": "{} not found".format(resource)}) 19 | response.status_code = 404 20 | return response 21 | 22 | @api.errorhandler(NotFoundError) 23 | def handle_invalid_usage(error): 24 | response = jsonify({"msg": "{} {} not found".format(error.info['_index'].capitalize(), error.info['_id'])}) 25 | response.status_code = 404 26 | return response 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/jobs.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from rq.registry import StartedJobRegistry, FailedJobRegistry 3 | 4 | from {{cookiecutter.project_name}}.api import api 5 | from {{cookiecutter.project_name}}.auth import authenticated 6 | from {{cookiecutter.project_name}}.core import queue, broker 7 | 8 | 9 | class Jobs(Resource): 10 | @authenticated 11 | def get(self): 12 | pending = queue.get_job_ids() 13 | 14 | registry = StartedJobRegistry('default', connection=broker) 15 | started = registry.get_job_ids() 16 | 17 | fail_queue = FailedJobRegistry(connection=broker) 18 | failed = fail_queue.get_job_ids() 19 | 20 | return {"jobs": started + pending, "failed": failed} 21 | 22 | 23 | class Job(Resource): 24 | @authenticated 25 | def get(self, job_id): 26 | job = queue.fetch_job(job_id) 27 | answer = {"failed": job.is_failed, "meta": job.meta} 28 | if job.is_failed and job.exc_info is not None: 29 | answer['error'] = 'Error: {}'.format(job.exc_info.split('\n')[-2]) 30 | return answer 31 | 32 | 33 | api.add_resource(Jobs, '/jobs') 34 | api.add_resource(Job, '/jobs/') 35 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/social/__init__.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_name}}.api import api_bp, api 2 | 3 | from .me import me 4 | from .users import User, Users, ProfilePicture 5 | 6 | api_bp.add_url_rule('/me', 'me', me) 7 | api.add_resource(Users, '/users') 8 | api.add_resource(User, '/users/') 9 | api.add_resource(ProfilePicture, '/users//profile-picture') 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/social/me.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from flask_jwt_extended import get_jwt_identity 3 | 4 | from {{cookiecutter.project_name}}.auth import authenticated 5 | from {{cookiecutter.project_name}}.managers.social import users 6 | 7 | 8 | @authenticated 9 | def me(): 10 | me_id = get_jwt_identity()['id'] 11 | user = users.get(me_id) 12 | profile, account_activated, first_login = user.get_data() 13 | return jsonify({'profile': profile, 'account_activated': account_activated, 'first_login': first_login}) 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/social/users.py: -------------------------------------------------------------------------------- 1 | import werkzeug 2 | 3 | from flask import request 4 | from flask_jwt_extended import get_jwt_identity 5 | from flask_restful import Resource, reqparse 6 | 7 | from {{cookiecutter.project_name}}.auth import authenticated 8 | from {{cookiecutter.project_name}}.managers.social import users 9 | 10 | 11 | class Users(Resource): 12 | @authenticated 13 | def get(self): 14 | search = request.args.get('search') 15 | user_list = users.get_all(search=search) 16 | return {'msg': 'success', 'users': user_list} 17 | 18 | 19 | class User(Resource): 20 | @authenticated 21 | def get(self, user_id): 22 | profile, account_activated, first_login = users.get(user_id).get_data() 23 | return { 24 | 'msg': 'success', 25 | 'user': {'profile': profile, 'account_activated': account_activated, 'first_login': first_login} 26 | } 27 | 28 | @authenticated 29 | def patch(self, user_id): 30 | field = request.args['field'] 31 | info = request.json['value'] 32 | identity = get_jwt_identity() 33 | profile, account_activated, first_login = users.update_personal_info(user_id, field, info) 34 | if identity['id'] != int(user_id): 35 | return {'msg': 'forbidden'}, 403 36 | 37 | return { 38 | 'msg': 'success', 39 | 'user': {'profile': profile, 'account_activated': account_activated, 'first_login': first_login} 40 | } 41 | 42 | @authenticated 43 | def delete(self, user_id): 44 | identity = get_jwt_identity() 45 | if identity['id'] != int(user_id): 46 | return {'msg': 'forbidden'}, 403 47 | users.delete_user(user_id) 48 | return {'msg': 'success'} 49 | 50 | 51 | class ProfilePicture(Resource): 52 | @authenticated 53 | def patch(self, user_id): 54 | identity = get_jwt_identity() 55 | if identity['id'] != int(user_id): 56 | return {'error': 'You are not allowed to modify this profile picture'}, 403 57 | 58 | parser = reqparse.RequestParser() 59 | parser.add_argument('file', type=werkzeug.datastructures.FileStorage, location='files') 60 | data = parser.parse_args() 61 | profile_picture = data['file'] 62 | 63 | if profile_picture.filename.split('.')[-1] not in ['jpeg', 'jpg', 'png', 'JPG', 'JPEG', 'PNG']: 64 | return {'error': 'You can only upload images in jpg or png format'}, 403 65 | 66 | user = users.update_profile_picture(user_id, profile_picture) 67 | 68 | return {'msg': 'success', 'user': user} 69 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_app.api import api 2 | from .tree import Tree 3 | 4 | api.add_resource(Tree, '/tree') 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/api/utils/tree.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from flask import request 5 | from flask_restful import Resource 6 | 7 | 8 | class Tree(Resource): 9 | def get(self): 10 | folder = request.args['folder'] 11 | tree = json.load(open(os.environ.get('TREE_FILE', './config/tree.json')))[folder] 12 | tree = [{ 13 | 'id': 0, 14 | 'name': folder, 15 | 'children': tree 16 | }] 17 | 18 | return {'msg': 'success', 'tree': tree} 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/app.py: -------------------------------------------------------------------------------- 1 | import rq_dashboard 2 | from flask import Flask 3 | from flask_cors import CORS 4 | 5 | from .api import register_api 6 | from .auth import {%- if cookiecutter.google_login == 'y' %} create_google_auth{%- endif %}{%- if cookiecutter.email_login == 'y' %}, create_email_auth{%- endif %}{%- if cookiecutter.facebook_login == 'y' %}, create_facebook_auth{%- endif %} 7 | from .config import flask_config 8 | from .core.cache import REDIS_URL 9 | {%- if cookiecutter.storage == 'y' %}from .storage import create_storage_gw{%- endif %} 10 | 11 | 12 | def create_app(api=True): 13 | app = Flask(__name__) 14 | CORS(app, resources={r"*": {"origins": "*"}}, supports_credentials=True) 15 | app.config.from_object(flask_config) 16 | 17 | {%- if cookiecutter.email_login == 'y' %} 18 | create_email_auth(app) 19 | {%- endif %} 20 | {%- if cookiecutter.google_login == 'y' %} 21 | create_google_auth(app) 22 | {%- endif %} 23 | {%- if cookiecutter.facebook_login == 'y' %} 24 | create_facebook_auth(app) 25 | {%- endif %} 26 | {%- if cookiecutter.storage == 'y' %} 27 | create_storage_gw(app) 28 | {%- endif %} 29 | 30 | if api: 31 | register_api(app) 32 | 33 | app.config.from_object(rq_dashboard.default_settings) 34 | app.config['REDIS_URL'] = REDIS_URL 35 | app.register_blueprint(rq_dashboard.blueprint, url_prefix="/jobs") 36 | 37 | return app 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/__init__.py: -------------------------------------------------------------------------------- 1 | {%- if cookiecutter.email_login == 'y' %} 2 | from .email_login import * 3 | {%- endif %} 4 | {%- if cookiecutter.facebook_login == 'y' %} 5 | from .facebook_login import * 6 | {%- endif %} 7 | {%- if cookiecutter.google_login == 'y' %} 8 | from .google_login import * 9 | {%- endif %} 10 | from .utils import authenticated 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/email_login.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from datetime import datetime 3 | 4 | from flask import Blueprint, jsonify, request 5 | from flask_jwt_extended import JWTManager, create_access_token, get_jwt_identity 6 | from itsdangerous import URLSafeTimedSerializer 7 | from peewee import DoesNotExist 8 | 9 | from {{cookiecutter.project_name}}.core import config, db, cache, queue, mail 10 | from {{cookiecutter.project_name}}.exceptions import * 11 | from {{cookiecutter.project_name}}.models.social import User 12 | from .utils import authenticated 13 | 14 | 15 | def create_email_auth(app): 16 | jwt = JWTManager(app) 17 | email_auth_bp = Blueprint('email_login', __name__) 18 | 19 | @jwt.token_in_blacklist_loader 20 | def check_if_token_is_revoked(decrypted_token): 21 | user_id = decrypted_token['identity']['id'] 22 | entry = cache.get('user_{}_valid'.format(user_id)) 23 | if entry is None: 24 | return False 25 | return entry == 'false' 26 | 27 | @email_auth_bp.errorhandler(UserError) 28 | def handle_invalid_usage(error): 29 | response = jsonify(error.to_dict()) 30 | response.status_code = error.status_code 31 | return response 32 | 33 | @email_auth_bp.errorhandler(PasswordError) 34 | def handle_invalid_usage(error): 35 | response = jsonify(error.to_dict()) 36 | response.status_code = error.status_code 37 | return response 38 | 39 | @email_auth_bp.route('/login', methods=['POST']) 40 | @db.connection_context() 41 | def login(): 42 | email = request.json.get('email') 43 | password = request.json.get('password') 44 | 45 | try: 46 | user = User.get(email=email) 47 | except DoesNotExist: 48 | raise UserNotFound 49 | 50 | if password is None: 51 | raise PasswordRequired 52 | password = hashlib.sha3_256('{}-{}'.format(config['email_auth']['hash_key'], password).encode()) 53 | if user.password != password.hexdigest(): 54 | raise EmailPasswordMismatch 55 | 56 | user.last_login = datetime.now() 57 | user.first_login = False 58 | user.save() 59 | access_token = create_access_token(identity=user.get_identity()) 60 | 61 | return jsonify(access_token=access_token), 200 62 | 63 | @email_auth_bp.route('/sign-up', methods=['POST']) 64 | @db.connection_context() 65 | def sign_up(): 66 | email = request.json.get('email') 67 | password = request.json.get('password') 68 | first_name = request.json.get('first_name') 69 | last_name = request.json.get('last_name') 70 | 71 | try: 72 | User.get(email=email) 73 | raise EmailAddressAlreadyTaken 74 | except DoesNotExist: 75 | if password is None: 76 | raise PasswordRequired 77 | if len(password) < 8: 78 | raise PasswordTooShort 79 | if email is None: 80 | raise EmailRequired 81 | if first_name is None: 82 | raise FirstNameRequired 83 | if last_name is None: 84 | raise LastNameRequired 85 | 86 | password = hashlib.sha3_256('{}-{}'.format(config['email_auth']['hash_key'], password).encode()) 87 | 88 | user = User.create(email=email, password=password.hexdigest(), last_login=datetime.now(), 89 | first_login=True, created_at=datetime.now(), first_name=first_name, last_name=last_name) 90 | 91 | activation_token = generate_activation_token(email) 92 | queue.enqueue(send_activation_email, user.email, activation_token) 93 | 94 | access_token = create_access_token(identity=user.get_identity()) 95 | 96 | return jsonify(access_token=access_token), 200 97 | 98 | @email_auth_bp.route('/check-activation-token/') 99 | @db.connection_context() 100 | def get_email_from_activation_token(activation_token): 101 | email = check_activation_token(activation_token) 102 | try: 103 | user = User.get(email=email) 104 | except DoesNotExist: 105 | raise UserNotFound 106 | 107 | if user.account_activated: 108 | raise InvalidLink 109 | 110 | return jsonify(email=email) 111 | 112 | @email_auth_bp.route('/confirm-email/', methods=['POST']) 113 | @db.connection_context() 114 | @authenticated 115 | def confirm_email(activation_token): 116 | email = check_activation_token(activation_token) 117 | original_email = get_jwt_identity()['email'] 118 | if original_email != email: 119 | raise InvalidLink 120 | 121 | try: 122 | user = User.get(email=email) 123 | except DoesNotExist: 124 | raise UserNotFound 125 | 126 | if user.account_activated: 127 | raise InvalidLink 128 | else: 129 | user.account_activated = True 130 | user.save() 131 | 132 | return 'Your account has been activated successfully!', 200 133 | 134 | @email_auth_bp.route('/resend-email', methods=['POST']) 135 | @db.connection_context() 136 | @authenticated 137 | def resend_email(): 138 | email = get_jwt_identity()['email'] 139 | 140 | try: 141 | user = User.get(email=email) 142 | except DoesNotExist: 143 | raise UserNotFound 144 | 145 | if user.account_activated: 146 | raise AccountAlreadyActivated 147 | 148 | activation_token = generate_activation_token(email) 149 | queue.enqueue(send_activation_email, email, activation_token) 150 | 151 | return 'A new email has been sent to {}'.format(email), 200 152 | 153 | @email_auth_bp.route('/forgot-password', methods=['POST']) 154 | @db.connection_context() 155 | def send_reset_link(): 156 | email = request.json.get('email') 157 | 158 | if email is None: 159 | raise EmailRequired 160 | 161 | reset_token = generate_reset_token(email) 162 | queue.enqueue(send_reset_email, email, reset_token) 163 | 164 | return 'Instructions to reset your password have been sent to {}'.format(email), 200 165 | 166 | @email_auth_bp.route('/check-reset-token/') 167 | @db.connection_context() 168 | def get_email_from_reset_token(reset_token): 169 | email = check_reset_token(reset_token) 170 | try: 171 | user = User.get(email=email) 172 | except DoesNotExist: 173 | raise UserNotFound 174 | 175 | return jsonify(email=user.email) 176 | 177 | @email_auth_bp.route('/reset-password/', methods=['POST']) 178 | @db.connection_context() 179 | def reset_password(reset_token): 180 | email = check_reset_token(reset_token) 181 | password = request.json.get('password') 182 | 183 | if password is None: 184 | raise PasswordRequired 185 | if len(password) < 8: 186 | raise PasswordTooShort 187 | 188 | try: 189 | user = User.get(email=email) 190 | except DoesNotExist: 191 | raise UserNotFound 192 | 193 | password = hashlib.sha3_256('{}-{}'.format(config['email_auth']['hash_key'], password).encode()) 194 | if user.password == password.hexdigest(): 195 | raise SamePasswords 196 | else: 197 | user.password = password.hexdigest() 198 | user.save() 199 | 200 | return 'Your password has been reset successfully, log in with your new password', 200 201 | 202 | app.register_blueprint(email_auth_bp, url_prefix="/auth/email") 203 | 204 | 205 | def generate_activation_token(email): 206 | serializer = URLSafeTimedSerializer(config['email_auth']['activation_key']) 207 | return serializer.dumps(email, salt=config['email_auth']['activation_password']) 208 | 209 | 210 | def send_activation_email(to, token): 211 | mail.no_reply.connect() 212 | mail.no_reply.sendmail(to, 'Confirm your email address', 'confirm_email', 213 | confirmation_url='{}/login/{}'.format(config['front_root_url'], token)) 214 | mail.no_reply.close() 215 | 216 | 217 | def check_activation_token(token, expiration=3600): 218 | serializer = URLSafeTimedSerializer(config['email_auth']['activation_key']) 219 | try: 220 | email = serializer.loads( 221 | token, 222 | salt=config['email_auth']['activation_password'], 223 | max_age=expiration 224 | ) 225 | except: 226 | raise InvalidLink 227 | 228 | return email 229 | 230 | 231 | def generate_reset_token(email): 232 | serializer = URLSafeTimedSerializer(config['email_auth']['reset_key']) 233 | return serializer.dumps(email, salt=config['email_auth']['reset_password']) 234 | 235 | 236 | def send_reset_email(to, token): 237 | mail.no_reply.connect() 238 | mail.no_reply.sendmail(to, 'Instructions to reset your password', 'reset_password', 239 | reset_url='{}/auth/email/reset-password/{}'.format(config['front_root_url'], token)) 240 | mail.no_reply.close() 241 | 242 | 243 | def check_reset_token(token, expiration=300): 244 | serializer = URLSafeTimedSerializer(config['email_auth']['reset_key']) 245 | try: 246 | email = serializer.loads( 247 | token, 248 | salt=config['email_auth']['reset_password'], 249 | max_age=expiration 250 | ) 251 | except: 252 | raise InvalidLink 253 | 254 | return email 255 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/facebook_login.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from datetime import datetime 4 | 5 | import requests 6 | from flask import Blueprint, jsonify, request, redirect 7 | from flask_jwt_extended import JWTManager, create_access_token 8 | from itsdangerous import URLSafeTimedSerializer, SignatureExpired 9 | 10 | from {{cookiecutter.project_name}}.core import cache, config, db, facebook_config 11 | from {{cookiecutter.project_name}}.exceptions.users import * 12 | from {{cookiecutter.project_name}}.models.social import User 13 | 14 | TOKEN_URL = 'https://graph.facebook.com/debug_token?input_token={}&access_token={}' 15 | ME_URL = 'https://graph.facebook.com/me?fields=first_name,last_name,email&access_token={}' 16 | PICTURE_URL = 'https://graph.facebook.com/{}/picture' 17 | SCOPES = 'public_profile,email,birthday' 18 | 19 | 20 | def create_facebook_auth(app): 21 | jwt = JWTManager(app) 22 | facebook_auth_bp = Blueprint('facebook_login', __name__) 23 | 24 | def credentials_to_dict(credentials): 25 | return {'token': credentials['access_token'], 26 | 'fb_user_id': credentials['user_id'], 27 | 'expires_in': credentials['expires_in'], 28 | 'issued_at': credentials['issued_at'], 29 | 'scopes': credentials['scopes']} 30 | 31 | @facebook_auth_bp.errorhandler(UserError) 32 | def handle_invalid_usage(error): 33 | response = jsonify(error.to_dict()) 34 | response.status_code = error.status_code 35 | return response 36 | 37 | @facebook_auth_bp.route('/callback') 38 | def callback(): 39 | code = request.args.get('code') 40 | state = request.args.get('state') 41 | if code is None: 42 | return redirect('{}?error=access_denied'.format(config['oauth']['facebook']['front_callback'])) 43 | if not check_state(state): 44 | return redirect('{}?error=invalid_link'.format(config['oauth']['facebook']['front_callback'])) 45 | return redirect('{}?code={}'.format(config['oauth']['facebook']['front_callback'], code)) 46 | 47 | @facebook_auth_bp.route('/login') 48 | def login(): 49 | state = generate_state() 50 | authorization_url = '{}?client_id={}&redirect_uri={}&state={}'.format( 51 | facebook_config['auth_uri'], 52 | facebook_config['app_id'], 53 | config['oauth']['facebook']['callback'], 54 | state 55 | ) 56 | return jsonify({'url': authorization_url}) 57 | 58 | @facebook_auth_bp.route('/authorize') 59 | @db.connection_context() 60 | def authorize(): 61 | code = request.args.get('code') 62 | params = {'client_id': facebook_config['app_id'], 'redirect_uri': config['oauth']['facebook']['callback'], 63 | 'client_secret': facebook_config['client_secret'], 'code': code} 64 | credentials = requests.get(facebook_config['token_uri'], params=params).json() 65 | token_inspection = requests.get( 66 | TOKEN_URL.format(credentials['access_token'], facebook_config['app_token'])).json() 67 | 68 | credentials.update(token_inspection['data']) 69 | 70 | profile = requests.get(ME_URL.format(credentials['access_token'])) 71 | 72 | user_info = profile.json() 73 | email = user_info.get('email') 74 | user, created = User.get_or_create(email=email, defaults={ 75 | 'first_name': user_info.get('first_name'), 76 | 'last_name': user_info.get('last_name'), 77 | 'picture': PICTURE_URL.format(credentials['user_id']), 78 | 'last_login': datetime.now(), 79 | 'created_at': datetime.now(), 80 | 'first_login': True, 81 | 'account_activated': True 82 | }) 83 | if not created: 84 | if not user.account_activated: 85 | raise EmailNotConfirmed 86 | if not user.picture: 87 | user.picture = user_info.get('picture') 88 | user.last_login = datetime.now() 89 | user.first_login = False 90 | user.save() 91 | 92 | user.add_facebook_credentials(credentials_to_dict(credentials)) 93 | 94 | access_token = create_access_token(identity=user.get_identity()) 95 | 96 | return jsonify(access_token=access_token), 200 97 | 98 | @jwt.token_in_blacklist_loader 99 | def check_if_token_is_revoked(decrypted_token): 100 | user_id = decrypted_token['identity']['id'] 101 | entry = cache.get('user_{}_valid'.format(user_id)) 102 | if entry is None: 103 | return False 104 | return entry == 'false' 105 | 106 | app.register_blueprint(facebook_auth_bp, url_prefix="/auth/facebook") 107 | 108 | 109 | def generate_state(): 110 | letters = string.ascii_lowercase 111 | random_string = ''.join(random.choice(letters) for i in range(30)) 112 | serializer = URLSafeTimedSerializer(config['oauth']['facebook']['state_key']) 113 | return serializer.dumps(random_string) 114 | 115 | 116 | def check_state(state, expiration=300): 117 | if state is None: 118 | return False 119 | 120 | serializer = URLSafeTimedSerializer(config['oauth']['facebook']['state_key']) 121 | try: 122 | serializer.loads(state, max_age=expiration) 123 | except SignatureExpired: 124 | return False 125 | 126 | return True 127 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/google_login.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import google_auth_oauthlib.flow 4 | import googleapiclient.discovery 5 | from flask import Blueprint, jsonify, request, redirect 6 | from flask_jwt_extended import JWTManager, create_access_token 7 | 8 | from {{cookiecutter.project_name}}.core import config, db, cache 9 | from {{cookiecutter.project_name}}.exceptions.users import * 10 | from {{cookiecutter.project_name}}.models.social import User 11 | 12 | SCOPES = ['https://www.googleapis.com/auth/userinfo.profile', 13 | 'https://www.googleapis.com/auth/userinfo.email', 'openid'] 14 | 15 | 16 | def create_google_auth(app): 17 | jwt = JWTManager(app) 18 | google_auth_bp = Blueprint('google_login', __name__) 19 | 20 | def credentials_to_dict(credentials): 21 | return {'token': credentials.token, 22 | 'refresh_token': credentials.refresh_token, 23 | 'token_uri': credentials.token_uri, 24 | 'client_id': credentials.client_id, 25 | 'client_secret': credentials.client_secret, 26 | 'scopes': credentials.scopes} 27 | 28 | @google_auth_bp.errorhandler(UserError) 29 | def handle_invalid_usage(error): 30 | response = jsonify(error.to_dict()) 31 | response.status_code = error.status_code 32 | return response 33 | 34 | @google_auth_bp.route('/callback') 35 | def callback(): 36 | code = request.args.get('code') 37 | state = request.args.get('state') 38 | return redirect('{}?code={}&state={}'.format(config['oauth']['google']['front_callback'], code, state)) 39 | 40 | @google_auth_bp.route('/login') 41 | def login(): 42 | flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(config['oauth']['google']['google_config'], 43 | scopes=SCOPES) 44 | flow.redirect_uri = config['oauth']['google']['callback'] 45 | authorization_url, state = flow.authorization_url( 46 | access_type='offline', 47 | prompt='consent') 48 | return jsonify({'url': authorization_url}) 49 | 50 | @google_auth_bp.route('/authorize') 51 | @db.connection_context() 52 | def authorize(): 53 | code = request.args.get('code') 54 | state = request.args.get('state') 55 | flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(config['oauth']['google']['google_config'], 56 | scopes=SCOPES, 57 | state=state) 58 | flow.redirect_uri = config['oauth']['google']['callback'] 59 | flow.fetch_token(code=code) 60 | 61 | credentials = flow.credentials 62 | 63 | user_info_service = googleapiclient.discovery.build("oauth2", "v2", credentials=credentials, 64 | cache_discovery=False) 65 | 66 | user_info = user_info_service.userinfo().get().execute() 67 | email = user_info.get('email') 68 | user, created = User.get_or_create(email=email, defaults={ 69 | 'first_name': user_info.get('given_name'), 70 | 'last_name': user_info.get('family_name'), 71 | 'picture': user_info.get('picture'), 72 | 'last_login': datetime.now(), 73 | 'created_at': datetime.now(), 74 | 'first_login': True, 75 | 'account_activated': True 76 | }) 77 | if not created: 78 | if not user.account_activated: 79 | raise EmailNotConfirmed 80 | if not user.picture: 81 | user.picture = user_info.get('picture') 82 | user.last_login = datetime.now() 83 | user.first_login = False 84 | user.save() 85 | 86 | user.add_google_credentials(credentials_to_dict(credentials)) 87 | 88 | access_token = create_access_token(identity=user.get_identity()) 89 | 90 | return jsonify(access_token=access_token), 200 91 | 92 | @jwt.token_in_blacklist_loader 93 | def check_if_token_is_revoked(decrypted_token): 94 | user_id = decrypted_token['identity']['id'] 95 | entry = cache.get('user_{}_valid'.format(user_id)) 96 | if entry is None: 97 | return False 98 | return entry == 'false' 99 | 100 | app.register_blueprint(google_auth_bp, url_prefix="/auth/google") 101 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import request 4 | from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity 5 | 6 | from {{cookiecutter.project_name}}.core import logger 7 | 8 | 9 | def authenticated(fn): 10 | @wraps(fn) 11 | def wrapper(*args, **kwargs): 12 | verify_jwt_in_request() 13 | me = get_jwt_identity() 14 | user = me.get('email') 15 | if user is None: 16 | user = me.get('id') 17 | logger.info('[Request] Path : {} {} | User : {}'.format(request.method, request.full_path, user)) 18 | return fn(*args, **kwargs) 19 | 20 | return wrapper 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/config.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from .core import config 4 | from .core.utils import DateTimeEncoder 5 | 6 | 7 | class FlaskConfig(object): 8 | ENV = config['flask'].get('env', 'production') 9 | DEBUG = config['flask'].get('debug', False) 10 | 11 | SECRET_KEY = config['flask'].get('oauth_secret', False) 12 | 13 | JWT_SECRET_KEY = config['flask'].get('jwt_secret', False) 14 | JWT_ACCESS_TOKEN_EXPIRES = timedelta(seconds=config['flask'].get('jwt_expiration', 900)) 15 | JWT_BLACKLIST_ENABLED = True 16 | JWT_BLACKLIST_TOKEN_CHECKS = ['access', 'refresh'] 17 | 18 | RESTFUL_JSON = {'separators': (', ', ': '), 19 | 'indent': 2, 20 | 'cls': DateTimeEncoder} 21 | 22 | 23 | flask_config = FlaskConfig 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import config, logger{%- if cookiecutter.google_login == 'y' %}, google_config{%- endif %} {%- if cookiecutter.facebook_login == 'y' %}, facebook_config{%- endif %} 2 | from .database import db 3 | from .cache import cache, queue, broker 4 | {%- if cookiecutter.storage == 'y' %}from .storage import storage{%- endif %} 5 | {%- if cookiecutter.elasticsearch == 'y' %}from .elasticsearch import es{%- endif %} 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/cache.py: -------------------------------------------------------------------------------- 1 | from redis import StrictRedis, Redis 2 | from rq import Queue 3 | 4 | from .config import config 5 | 6 | REDIS_URL = config['cache'].get('url', 'redis://localhost:6379/1') 7 | 8 | cache = StrictRedis( 9 | host=config['cache'].get('host', 'localhost'), 10 | password=config['cache'].get('password', None), 11 | db=1, decode_responses=True 12 | ) 13 | 14 | broker = Redis( 15 | host=config['cache'].get('host', 'localhost'), 16 | password=config['cache'].get('password', None), 17 | db=1 18 | ) 19 | 20 | queue = Queue(connection=broker, default_timeout=7200) 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | config = json.load(open(os.environ.get('CONFIG_FILE', './config/config.json'))) 6 | {%- if cookiecutter.facebook_login == 'y' %} 7 | facebook_config = json.load(open(os.environ.get('FACEBOOK_CONFIG_FILE', './config/facebook.json'))) 8 | {%- endif %} 9 | {%- if cookiecutter.google_login == 'y' %} 10 | google_config = json.load(open(os.environ.get('GOOGLE_CONFIG_FILE', './config/google.json'))) 11 | {%- endif %} 12 | 13 | logger = logging.getLogger() 14 | formatter = logging.Formatter('%(process)d %(asctime)s %(name)-12s %(levelname)-8s %(message)s') 15 | 16 | stream_handler = logging.StreamHandler() 17 | stream_handler.setFormatter(formatter) 18 | logger.addHandler(stream_handler) 19 | 20 | log_level = config.get('log', 'INFO') 21 | if log_level == 'DEBUG': 22 | logger.setLevel(logging.DEBUG) 23 | else: 24 | logger.setLevel(logging.INFO) 25 | 26 | logger = logging.getLogger('{{cookiecutter.project_name}}') 27 | 28 | version = '0.1.0' 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/database.py: -------------------------------------------------------------------------------- 1 | from peewee import PostgresqlDatabase 2 | 3 | from .config import config 4 | 5 | db = PostgresqlDatabase( 6 | '{{cookiecutter.project_name}}', 7 | user=config['database'].get('user'), 8 | password=config['database'].get('password'), 9 | host=config['database'].get('host'), 10 | autorollback=True 11 | ) 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/elasticsearch.py: -------------------------------------------------------------------------------- 1 | from elasticsearch import Elasticsearch, RequestsHttpConnection 2 | from requests_aws4auth import AWS4Auth 3 | 4 | from .config import config 5 | 6 | aws_auth = None 7 | 8 | aws_condition = 'localhost' not in config['aws'].get('elastic') 9 | if aws_condition: 10 | aws_auth = AWS4Auth(config['aws'].get('access_key'), config['aws'].get('private_key'), 'ap-southeast-2', 'es') 11 | 12 | es = Elasticsearch([config['aws'].get('elastic')], http_auth=aws_auth, use_ssl=aws_condition, 13 | verify_certs=aws_condition, 14 | connection_class=RequestsHttpConnection) 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/mail.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | from email.mime.application import MIMEApplication 3 | from email.mime.multipart import MIMEMultipart 4 | from email.mime.text import MIMEText 5 | 6 | from jinja2 import Template 7 | 8 | from .config import config, logger 9 | 10 | 11 | class Email: 12 | def __init__(self, account, name): 13 | self.smtp = None 14 | self.account = account 15 | self.name = name 16 | 17 | def connect(self): 18 | self.smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465) 19 | self.smtp.ehlo() 20 | self.smtp.login(self.account, config['email'][self.account]) 21 | 22 | def sendmail(self, to, subject, template, cc=None, bcc=None, attachment=None, attachments=None, **text): 23 | msg = MIMEMultipart('alternative') 24 | msg['Subject'] = subject 25 | msg['From'] = '{} <{}>'.format(self.name, self.account) 26 | msg['To'] = to 27 | destinations = [email.strip() for email in to.split(',')] 28 | if cc is not None: 29 | msg['Cc'] = cc 30 | destinations += [email.strip() for email in cc.split(',')] 31 | if bcc is not None: 32 | msg['Bcc'] = bcc 33 | destinations += [email.strip() for email in bcc.split(',')] 34 | 35 | with open('templates/{}.html'.format(template)) as template_file: 36 | template = Template(template_file.read()) 37 | content = template.render(**text) 38 | 39 | msg.attach(MIMEText(content, 'html')) 40 | if attachment is not None: 41 | part = MIMEApplication( 42 | attachment[0].read(), 43 | Name=attachment[1] 44 | ) 45 | part['Content-Disposition'] = 'attachment; filename="{}"'.format(attachment[1]) 46 | msg.attach(part) 47 | if attachments is not None: 48 | for attachment in attachments: 49 | part = MIMEApplication( 50 | attachment[0].read(), 51 | Name=attachment[1]) 52 | part['Content-Disposition'] = 'attachment; filename="{}"'.format(attachment[1]) 53 | msg.attach(part) 54 | logger.debug('[Emails] Sending email to {}'.format(destinations)) 55 | if len(to) > 0: 56 | self.smtp.sendmail(self.account, destinations, msg.as_string()) 57 | 58 | def close(self): 59 | self.smtp.quit() 60 | 61 | 62 | no_reply = Email('my_email@email.com', 'my app') 63 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/storage.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | from .config import config 4 | 5 | S3_ACCESS_KEY = config['storage'].get('access_key') 6 | S3_SECRET_KEY = config['storage'].get('private_key') 7 | endpoint = config['storage'].get('endpoint') 8 | 9 | storage = boto3.client( 10 | "s3", 11 | aws_access_key_id=S3_ACCESS_KEY, 12 | aws_secret_access_key=S3_SECRET_KEY, 13 | endpoint_url=endpoint 14 | ) 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from json import JSONEncoder 3 | 4 | 5 | class DateTimeEncoder(JSONEncoder): 6 | """ Instead of letting the default encoder convert datetime to string, 7 | convert datetime objects into a dict, which can be decoded by the 8 | DateTimeDecoder 9 | """ 10 | 11 | def default(self, obj): 12 | if isinstance(obj, datetime): 13 | return { 14 | '__type__': 'datetime', 15 | 'year': obj.year, 16 | 'month': obj.month, 17 | 'day': obj.day, 18 | 'hour': obj.hour, 19 | 'minute': obj.minute, 20 | 'second': obj.second, 21 | 'microsecond': obj.microsecond, 22 | } 23 | else: 24 | return JSONEncoder.default(self, obj) 25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_name}}.core import logger 2 | 3 | 4 | class APIError(Exception): 5 | status_code = 500 6 | 7 | def __init__(self, prefix, message, status_code=None): 8 | Exception.__init__(self) 9 | self.message = message 10 | if status_code is not None: 11 | self.status_code = status_code 12 | logger.warning('{} error : {}'.format(prefix, message)) 13 | 14 | def to_dict(self): 15 | return {'error': self.message} 16 | 17 | 18 | from .users import * 19 | from .passwords import * 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/exceptions/passwords.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_name}}.exceptions import APIError 2 | 3 | 4 | class PasswordError(APIError): 5 | def __init__(self, message, status_code=None): 6 | APIError.__init__(self, 'Passwords', message, status_code) 7 | 8 | 9 | class EmailPasswordMismatch(PasswordError): 10 | def __init__(self): 11 | PasswordError.__init__(self, "Wrong password", 400) 12 | 13 | 14 | class PasswordRequired(PasswordError): 15 | def __init__(self): 16 | PasswordError.__init__(self, "Password is required", 400) 17 | 18 | 19 | class PasswordTooShort(PasswordError): 20 | def __init__(self): 21 | PasswordError.__init__(self, "Min 8 characters", 400) 22 | 23 | 24 | class SamePasswords(PasswordError): 25 | def __init__(self): 26 | PasswordError.__init__(self, "Old password and new password must be different", 400) 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/exceptions/users.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_name}}.exceptions import APIError 2 | 3 | 4 | class UserError(APIError): 5 | def __init__(self, message, status_code=None): 6 | APIError.__init__(self, 'Users', message, status_code) 7 | 8 | 9 | class EmailAddressAlreadyTaken(UserError): 10 | def __init__(self): 11 | UserError.__init__(self, 'An account with this email address already exists', 401) 12 | 13 | 14 | class UserNotFound(UserError): 15 | def __init__(self): 16 | UserError.__init__(self, 'No account associated with this email address', 404) 17 | 18 | 19 | class InvalidLink(UserError): 20 | def __init__(self): 21 | UserError.__init__(self, "This link is invalid or has expired", 404) 22 | 23 | 24 | class AccountAlreadyActivated(UserError): 25 | def __init__(self): 26 | UserError.__init__(self, "Your account is already activated", 403) 27 | 28 | 29 | class EmailRequired(UserError): 30 | def __init__(self): 31 | UserError.__init__(self, "Email is required", 400) 32 | 33 | 34 | class FirstNameRequired(UserError): 35 | def __init__(self): 36 | UserError.__init__(self, "First name is required", 400) 37 | 38 | 39 | class LastNameRequired(UserError): 40 | def __init__(self): 41 | UserError.__init__(self, "Last name is required", 400) 42 | 43 | 44 | class EmailNotConfirmed(UserError): 45 | def __init__(self): 46 | UserError.__init__(self, "An account with this email address already exists and hasn't been activated", 401) 47 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/managers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import social 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/managers/social/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/managers/social/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/managers/social/users.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from peewee import DoesNotExist 4 | 5 | from {{cookiecutter.project_name}}.core import cache, storage, config 6 | from {{cookiecutter.project_name}}.exceptions import * 7 | from {{cookiecutter.project_name}}.models.social.user import User 8 | 9 | 10 | def get_all(search=None) -> list: 11 | users = [] 12 | if search is None or search == '': 13 | query = User.select() 14 | else: 15 | query = User.select().where( 16 | (User.first_name.contains(search)) | 17 | (User.last_name.contains(search))) 18 | 19 | for user in query: 20 | profile, account_activated, first_login = user.get_data() 21 | users.append({'profile': profile, 'account_activated': account_activated, 'first_login': first_login}) 22 | logger.debug('Get all users from db. Number of users : {}'.format(len(users))) 23 | 24 | return users 25 | 26 | 27 | def get(user_id) -> User: 28 | try: 29 | user = User.get(User.id == user_id) 30 | return user 31 | except DoesNotExist: 32 | raise UserNotFound 33 | 34 | 35 | def get_by_mail(mail) -> User: 36 | try: 37 | user = User.get(User.email == mail) 38 | return user 39 | except DoesNotExist: 40 | raise UserNotFound 41 | 42 | 43 | def update_personal_info(user_id, field, info): 44 | user = get(user_id) 45 | setattr(user, field, info) 46 | user.save() 47 | return user.get_data() 48 | 49 | 50 | def delete_user(user_id) -> bool: 51 | try: 52 | user = User.get(User.id == user_id) 53 | user.delete_instance(recursive=True) 54 | cache.set('user_{}_valid'.format(user_id), 'false') 55 | return True 56 | except DoesNotExist: 57 | raise UserNotFound 58 | 59 | 60 | def update_profile_picture(user_id, profile_picture): 61 | user = get(user_id) 62 | if 'filepath' in user.picture: 63 | former_file_path = user.picture.split('=')[1] 64 | storage.delete_object(Key=former_file_path, Bucket='template-storage') 65 | new_picture_id = uuid.uuid4().hex 66 | file_path = 'profile-pictures/{}'.format(new_picture_id) 67 | storage.upload_fileobj(profile_picture, 'template-storage', file_path) 68 | user.picture = '{}/storage/download?filepath={}'.format(config['back_root_url'], file_path) 69 | user.save() 70 | profile, account_activated, first_login = user.get_data() 71 | 72 | return {'profile': profile, 'account_activated': account_activated, 'first_login': first_login} 73 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import social 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/social/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import User 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/social/fbcredentials.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | from {{cookiecutter.project_name}}.core import db 4 | from {{cookiecutter.project_name}}.models.social.user import User 5 | 6 | 7 | class FBCredentials(Model): 8 | id = PrimaryKeyField() 9 | user = ForeignKeyField(User, unique=True) 10 | token = CharField() 11 | fb_user_id = CharField() 12 | expires_in = IntegerField() 13 | issued_at = IntegerField() 14 | scopes = TextField() 15 | 16 | class Meta: 17 | database = db 18 | 19 | 20 | with db: 21 | FBCredentials.create_table(fail_silently=True) 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/social/gcredentials.py: -------------------------------------------------------------------------------- 1 | from peewee import * 2 | 3 | from {{cookiecutter.project_name}}.core import db 4 | from {{cookiecutter.project_name}}.models.social.user import User 5 | 6 | 7 | class GCredentials(Model): 8 | id = PrimaryKeyField() 9 | user = ForeignKeyField(User, unique=True) 10 | token = CharField() 11 | refresh_token = CharField(null=True) 12 | token_uri = CharField() 13 | client_id = CharField() 14 | client_secret = CharField() 15 | scopes = CharField() 16 | 17 | class Meta: 18 | database = db 19 | 20 | 21 | with db: 22 | GCredentials.create_table(fail_silently=True) 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/social/user.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from peewee import * 4 | 5 | from {{cookiecutter.project_name}}.core import db 6 | 7 | 8 | class User(Model): 9 | id = PrimaryKeyField() 10 | first_name = CharField() 11 | last_name = CharField() 12 | email = CharField(unique=True, index=True) 13 | password = CharField(null=True) 14 | picture = TextField(null=True) 15 | last_login = DateTimeField() 16 | account_activated = BooleanField(default=False) 17 | first_login = BooleanField(default=False) 18 | created_at = DateTimeField() 19 | 20 | def get_identity(self): 21 | return {"id": self.id, "first_name": self.first_name, "last_name": self.last_name, "picture": self.picture, 22 | "email": self.email} 23 | 24 | def get_data(self): 25 | identity = self.get_identity() 26 | identity['name'] = "{} {}".format(self.first_name, self.last_name) 27 | return identity, self.account_activated, self.first_login 28 | 29 | {%- if cookiecutter.google_login == 'y' %} 30 | def add_google_credentials(self, credentials): 31 | from {{cookiecutter.project_name}}.models.social.gcredentials import GCredentials 32 | data = credentials.copy() 33 | data['user'] = self.id 34 | data['scopes'] = json.dumps(data['scopes']) 35 | try: 36 | with db.atomic(): 37 | GCredentials.create(**data) 38 | except IntegrityError: 39 | with db.transaction(): 40 | GCredentials.delete().where(GCredentials.user == self.id).execute() 41 | GCredentials.create(**data) 42 | 43 | def get_google_credentials(self): 44 | from {{cookiecutter.project_name}}.models.social.gcredentials import GCredentials 45 | gcredentials = list(GCredentials.select().where(GCredentials.user == self.id).dicts()) 46 | data = gcredentials[0].copy() 47 | data['scopes'] = json.loads(data['scopes']) 48 | del data['user'] 49 | del data['id'] 50 | return data 51 | {%- endif %} 52 | 53 | {%- if cookiecutter.facebook_login == 'y' %} 54 | def add_facebook_credentials(self, credentials): 55 | from {{cookiecutter.project_name}}.models.social.fbcredentials import FBCredentials 56 | data = credentials.copy() 57 | data['user'] = self.id 58 | data['scopes'] = json.dumps(data['scopes']) 59 | try: 60 | with db.atomic(): 61 | FBCredentials.create(**data) 62 | except IntegrityError: 63 | with db.transaction(): 64 | FBCredentials.delete().where(FBCredentials.user == self.id).execute() 65 | FBCredentials.create(**data) 66 | 67 | def get_facebook_credentials(self): 68 | from {{cookiecutter.project_name}}.models.social.fbcredentials import FBCredentials 69 | fbcredentials = list(FBCredentials.select().where(FBCredentials.user == self.id).dicts()) 70 | data = fbcredentials[0].copy() 71 | data['scopes'] = json.loads(data['scopes']) 72 | del data['user'] 73 | del data['id'] 74 | return data 75 | {%- endif %} 76 | 77 | class Meta: 78 | database = db 79 | 80 | 81 | with db: 82 | User.create_table(fail_silently=True) 83 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/storage/__init__.py: -------------------------------------------------------------------------------- 1 | from botocore.exceptions import ClientError 2 | import flask 3 | from flask import Blueprint, request 4 | 5 | from {{cookiecutter.project_name}}.core import storage, config 6 | 7 | 8 | def create_storage_gw(app): 9 | storage_bp = Blueprint('storage-gw', __name__) 10 | 11 | @storage_bp.route('/download') 12 | def download(): 13 | filepath = request.args.get('filepath') 14 | try: 15 | object = storage.get_object(Bucket=config['storage']['bucket'], Key=filepath)['Body'] 16 | except ClientError: 17 | return '', 404 18 | 19 | response = flask.make_response(object.read()) 20 | response.headers['content-type'] = 'application/octet-stream' 21 | return response 22 | 23 | app.register_blueprint(storage_bp, url_prefix="/storage") 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | database: 5 | image: postgres:10 6 | environment: 7 | - POSTGRES_USER={{cookiecutter.project_name}} 8 | - POSTGRES_PASSWORD={{cookiecutter.project_name}} 9 | ports: 10 | - 5432:5432 11 | 12 | cache: 13 | image: redis:4.0 14 | ports: 15 | - 6379:6379 16 | 17 | {%- if cookiecutter.storage == 'y' %} 18 | storage: 19 | image: minio/minio:latest 20 | ports: 21 | - 9000:9000 22 | environment: 23 | - MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE 24 | - MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 25 | command: server /data 26 | {%- endif %} 27 | 28 | {%- if cookiecutter.elasticsearch == 'y' %} 29 | kibana: 30 | image: docker.elastic.co/kibana/kibana:6.4.2 31 | ports: 32 | - 5601:5601 33 | 34 | elasticsearch: 35 | image: docker.elastic.co/elasticsearch/elasticsearch:6.4.2 36 | ports: 37 | - 9200:9200 38 | {%- endif %} 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV='development', 2 | {%- if cookiecutter.enable_https == 'y' %} 3 | {%- if cookiecutter.email_login == 'y' %} 4 | VUE_APP_EMAIL_AUTH_URL='https://localhost:5000/auth/email' 5 | {%- endif %} 6 | {%- if cookiecutter.google_login == 'y' %} 7 | VUE_APP_GOOGLE_AUTH_URL='https://localhost:5000/auth/google' 8 | {%- endif %} 9 | {%- if cookiecutter.facebook_login == 'y' %} 10 | VUE_APP_FACEBOOK_AUTH_URL='https://localhost:5000/auth/facebook' 11 | {%- endif %} 12 | VUE_APP_API_URL='https://localhost:5000/api/v1' 13 | VUE_APP_ROOT_URL='https://localhost:5000' 14 | {%- endif %} 15 | {%- if cookiecutter.enable_https != 'y' %} 16 | {%- if cookiecutter.email_login == 'y' %} 17 | VUE_APP_EMAIL_AUTH_URL='http://localhost:5000/auth/email' 18 | {%- endif %} 19 | {%- if cookiecutter.google_login == 'y' %} 20 | VUE_APP_GOOGLE_AUTH_URL='http://localhost:5000/auth/google' 21 | {%- endif %} 22 | {%- if cookiecutter.facebook_login == 'y' %} 23 | VUE_APP_FACEBOOK_AUTH_URL='http://localhost:5000/auth/facebook' 24 | {%- endif %} 25 | VUE_APP_API_URL='http://localhost:5000/api/v1' 26 | VUE_APP_ROOT_URL='http://localhost:5000' 27 | {%- endif %} 28 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | certs 4 | /dist 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "start": "npm run serve" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.0", 12 | "core-js": "^3.3.2", 13 | "lodash": "^4.17.19", 14 | "vue": "^2.6.10", 15 | "vue-router": "^3.1.3", 16 | "vuetify": "^2.1.0" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^4.0.0", 20 | "@vue/cli-plugin-router": "^4.0.0", 21 | "@vue/cli-service": "^4.0.0", 22 | "node-sass": "^4.12.0", 23 | "sass": "^1.19.0", 24 | "sass-loader": "^8.0.0", 25 | "vue-cli-plugin-vuetify": "^1.1.1", 26 | "vue-template-compiler": "^2.6.10", 27 | "vuetify-loader": "^1.3.0" 28 | }, 29 | "postcss": { 30 | "plugins": { 31 | "autoprefixer": {} 32 | } 33 | }, 34 | "browserslist": [ 35 | "> 1%", 36 | "last 2 versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/public/favicon.ico -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Template 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/App.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 107 | 108 | 178 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/banner.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/banner_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/banner_dark.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/grey_background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/grey_background.jpeg -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/elastic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/elastic.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/email.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/facebook.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/facebook_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/facebook_blue.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/google.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/minio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/minio.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/postgre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/postgre.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/redis.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/assets/logos/vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/front/src/assets/logos/vue.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/account/ModifyAccount.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 193 | 194 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/account/ViewAccount.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/auth/EmailLogin.vue: -------------------------------------------------------------------------------- 1 | 129 | 130 | 238 | 239 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/auth/FacebookLogin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/auth/GoogleLogin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/home/AuthType.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/home/BackendStack.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 33 | 34 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/home/MainStack.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 39 | 40 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/home/WelcomeScreen.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 69 | 70 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/util/Footer.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/util/Header.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 120 | 121 | 146 | 147 | 156 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/util/Jobs.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 46 | 47 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/util/ProfileMenu.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 74 | 75 | 78 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/components/util/UploadButton.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 81 | 82 | 117 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App"; 3 | import axios from "axios"; 4 | 5 | import vuetify from './plugins/vuetify'; 6 | import router from "./modules/router"; 7 | import auth from "./modules/auth"; 8 | import notifications from "./modules/notifications"; 9 | 10 | Vue.config.productionTip = process.env.NODE_ENV === "production"; 11 | axios.defaults.withCredentials = true; 12 | 13 | 14 | new Vue({ 15 | router, 16 | vuetify, 17 | render: h => h(App) 18 | }).$mount('#app'); 19 | 20 | 21 | Vue.mixin({ 22 | data() { 23 | return { 24 | $_profile: auth.user.profile 25 | } 26 | } 27 | }); 28 | 29 | axios.interceptors.response.use( 30 | response => response, 31 | error => { 32 | if (error.response.status === 401) { 33 | notifications.addNotification('Session expired'); 34 | auth.logout() 35 | } 36 | return Promise.reject(error); 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/auth/email.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from '@/modules/router' 3 | import notifications from '@/modules/notifications' 4 | import {checkAuth, logout, user} from './util' 5 | 6 | function loginWithEmail(context) { 7 | return new Promise((resolve, reject) => { 8 | axios.post(process.env.VUE_APP_EMAIL_AUTH_URL + '/login', { 9 | email: context.email, 10 | password: context.password 11 | }).then(response => { 12 | if (response.status === 200) { 13 | localStorage.setItem('access_token', response.data.access_token); 14 | localStorage.setItem('auth_type', 'email'); 15 | checkAuth().then(() => { 16 | if (context.token) { 17 | activateAccount(context.token).then(() => { 18 | router.push('/home'); 19 | resolve() 20 | }).catch(error => { 21 | router.push('/home'); 22 | resolve() 23 | }) 24 | } else { 25 | router.push('/home'); 26 | resolve(); 27 | } 28 | }).catch(error => { 29 | notifications.addNotification('An error occurred while logging in'); 30 | logout() 31 | }); 32 | } else { 33 | notifications.addNotification('An error occurred while logging in'); 34 | reject(); 35 | } 36 | }).catch(function (error) { 37 | if (error.response) { 38 | reject(new Error(error.response.data.error)); 39 | } else { 40 | notifications.addNotification('An error occurred while logging in'); 41 | reject() 42 | } 43 | }); 44 | }) 45 | } 46 | 47 | function signUpWithEmail(context) { 48 | return new Promise((resolve, reject) => { 49 | axios.post(process.env.VUE_APP_EMAIL_AUTH_URL + '/sign-up', { 50 | email: context.email, 51 | password: context.password, 52 | first_name: context.firstName, 53 | last_name: context.lastName, 54 | }).then(response => { 55 | if (response.status === 200) { 56 | localStorage.setItem('access_token', response.data.access_token); 57 | localStorage.setItem('auth_type', 'email'); 58 | checkAuth().then(() => { 59 | notifications.addNotification( 60 | `An email to activate your account has been sent to ${context.email}` 61 | ); 62 | router.push('/home'); 63 | resolve() 64 | }).catch(error => { 65 | notifications.addNotification('An error occurred while signing in'); 66 | logout(); 67 | reject() 68 | }); 69 | } else { 70 | notifications.addNotification('An error occurred while signing in'); 71 | reject(); 72 | } 73 | }).catch(function (error) { 74 | if (error.response) { 75 | reject(new Error(error.response.data.error)); 76 | } else { 77 | notifications.addNotification('An error occurred while signing in'); 78 | reject() 79 | } 80 | }); 81 | }) 82 | } 83 | 84 | function activateAccount(token) { 85 | return new Promise((resolve, reject) => { 86 | axios.post(process.env.VUE_APP_EMAIL_AUTH_URL + `/confirm-email/${token}`).then(response => { 87 | if (response.status === 200) { 88 | notifications.addNotification(response.data); 89 | user.accountActivated = true; 90 | resolve() 91 | } else { 92 | reject() 93 | } 94 | }).catch(error => { 95 | notifications.addNotification(error.response.data.error); 96 | reject(); 97 | }) 98 | }) 99 | } 100 | 101 | export { 102 | loginWithEmail, 103 | signUpWithEmail, 104 | activateAccount 105 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/auth/facebook.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from '@/modules/router' 3 | import notifications from '@/modules/notifications' 4 | import {checkAuth, logout} from './util' 5 | 6 | function loginWithFacebook() { 7 | axios.get(process.env.VUE_APP_FACEBOOK_AUTH_URL + '/login').then(response => { 8 | if (response.status === 200) { 9 | window.location.replace(response.data.url); 10 | } else { 11 | raiseError() 12 | } 13 | }).catch(function (error) { 14 | raiseError() 15 | }); 16 | } 17 | 18 | function authorizeFacebook(code) { 19 | axios.get(process.env.VUE_APP_FACEBOOK_AUTH_URL + '/authorize?code=' + code).then(response => { 20 | if (response.status === 200) { 21 | localStorage.setItem('access_token', response.data.access_token); 22 | localStorage.setItem('auth_type', 'facebook'); 23 | checkAuth().then(() => { 24 | router.push('/home'); 25 | }).catch(error => { 26 | logout(); 27 | raiseError() 28 | }) 29 | } else { 30 | raiseError() 31 | } 32 | }).catch(function (error) { 33 | if (error.response) { 34 | notifications.addNotification(error.response.data.error); 35 | router.replace('/') 36 | } else { 37 | raiseError() 38 | } 39 | }); 40 | } 41 | 42 | function raiseError() { 43 | notifications.addNotification('An error occurred while logging in with Facebook'); 44 | if (router.name !== '/') { 45 | router.replace('/') 46 | } 47 | } 48 | 49 | export { 50 | loginWithFacebook, 51 | authorizeFacebook 52 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/auth/google.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from '@/modules/router' 3 | import notifications from '@/modules/notifications' 4 | import {checkAuth, logout} from './util' 5 | 6 | function loginWithGoogle() { 7 | axios.get(process.env.VUE_APP_GOOGLE_AUTH_URL + '/login').then(response => { 8 | if (response.status === 200) { 9 | window.location.replace(response.data.url); 10 | } else { 11 | raiseError() 12 | } 13 | }).catch(error => { 14 | raiseError() 15 | }); 16 | } 17 | 18 | function authorizeGoogle(code, state) { 19 | axios.get(process.env.VUE_APP_GOOGLE_AUTH_URL + '/authorize?code=' + code + '&state=' + state) 20 | .then(response => { 21 | if (response.status === 200) { 22 | localStorage.setItem('access_token', response.data.access_token); 23 | localStorage.setItem('auth_type', 'google'); 24 | checkAuth().then(() => { 25 | router.push('/home'); 26 | }).catch(error => { 27 | logout(); 28 | raiseError() 29 | }); 30 | } else { 31 | raiseError() 32 | } 33 | }) 34 | .catch(error => { 35 | if (error.response) { 36 | notifications.addNotification(error.response.data.error) 37 | router.replace('/') 38 | } else { 39 | raiseError() 40 | } 41 | }); 42 | } 43 | 44 | function raiseError() { 45 | notifications.addNotification('An error occurred while logging in with Google'); 46 | if (router.name !== '/') { 47 | router.replace('/') 48 | } 49 | } 50 | 51 | export { 52 | loginWithGoogle, 53 | authorizeGoogle 54 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/auth/index.js: -------------------------------------------------------------------------------- 1 | import {user, checkAuth, getUserInfo, logout} from './util' 2 | {%- if cookiecutter.facebook_login == 'y' %} 3 | import {authorizeFacebook, loginWithFacebook} from './facebook' 4 | {%- endif %} 5 | {%- if cookiecutter.google_login == 'y' %} 6 | import {authorizeGoogle, loginWithGoogle} from './google' 7 | {%- endif %} 8 | {%- if cookiecutter.email_login == 'y' %} 9 | import {loginWithEmail, signUpWithEmail, activateAccount} from './email' 10 | {%- endif %} 11 | 12 | export default { 13 | user, 14 | {%- if cookiecutter.email_login == 'y' %} 15 | loginWithEmail, 16 | signUpWithEmail, 17 | activateAccount, 18 | {%- endif %} 19 | {%- if cookiecutter.google_login == 'y' %} 20 | loginWithGoogle, 21 | authorizeGoogle, 22 | {%- endif %} 23 | {%- if cookiecutter.facebook_login == 'y' %} 24 | loginWithFacebook, 25 | authorizeFacebook, 26 | {%- endif %} 27 | checkAuth, 28 | logout, 29 | getUserInfo 30 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/auth/util.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from '@/modules/router' 3 | 4 | let user = { 5 | authenticated: false, 6 | profile: undefined, 7 | accountActivated: false, 8 | firstLogin: false 9 | }; 10 | 11 | 12 | function checkAuth() { 13 | return new Promise((resolve, reject) => { 14 | const jwt = localStorage.getItem('access_token'); 15 | if (jwt !== null) { 16 | axios.defaults.headers.common['Authorization'] = 'Bearer ' + jwt; 17 | getUserInfo().then(() => { 18 | user.authenticated = true; 19 | resolve() 20 | }).catch(error => { 21 | reject() 22 | }) 23 | } else { 24 | user.authenticated = false; 25 | reject(); 26 | } 27 | }); 28 | } 29 | 30 | function getUserInfo() { 31 | return new Promise((resolve, reject) => { 32 | axios.get(process.env.VUE_APP_API_URL + '/me').then(response => { 33 | if (response.status === 200) { 34 | user.profile = response.data.profile; 35 | user.accountActivated = response.data.account_activated; 36 | user.firstLogin = response.data.first_login; 37 | localStorage.setItem('profile', JSON.stringify(user.profile)); 38 | resolve(); 39 | } else { 40 | reject(); 41 | } 42 | }).catch(function (error) { 43 | reject(); 44 | }); 45 | }) 46 | } 47 | 48 | function logout() { 49 | if (router.currentRoute.name !== 'login') { 50 | router.replace('/'); 51 | } 52 | localStorage.removeItem('access_token'); 53 | localStorage.removeItem('profile'); 54 | localStorage.removeItem('auth_type'); 55 | axios.defaults.headers.common['Authorization'] = ''; 56 | user.authenticated = false; 57 | user.profile = null; 58 | user.accountActivated = false; 59 | } 60 | 61 | export { 62 | user, 63 | checkAuth, 64 | logout, 65 | getUserInfo 66 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/jobs/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | let store = { 4 | jobs: [] 5 | }; 6 | 7 | function addJob(job_id) { 8 | return new Promise(function(resolve) { 9 | let job = {}; 10 | job.id = job_id; 11 | job.progress = 0; 12 | job.title = 'Waiting for job'; 13 | job.interval = setInterval(() => { 14 | getJob(job).then((job) => { 15 | if (job.meta.progress >= 100) { 16 | resolve(job); 17 | } 18 | }); 19 | }, 2000); 20 | 21 | store.jobs.push(job); 22 | }); 23 | } 24 | 25 | function getJob(job) { 26 | return new Promise(function(resolve) { 27 | axios.get(process.env.VUE_APP_API_URL + '/jobs/' + job.id).then((response) => { 28 | let progress = response.data.meta.progress; 29 | if (progress > 0) { 30 | job.progress = progress; 31 | job.title = response.data.meta.title; 32 | job.color = 'primary'; 33 | } else { 34 | job.progress = 0; 35 | job.title = 'Waiting for job'; 36 | } 37 | 38 | if (job.progress >= 100) { 39 | clearInterval(job.interval); 40 | job.title = 'Finished'; 41 | setTimeout(() => { 42 | removeJob(job) 43 | }, 5000); 44 | } 45 | 46 | if (response.data.failed) { 47 | clearInterval(job.interval); 48 | job.title = 'Failed'; 49 | job.color = 'error'; 50 | } 51 | 52 | resolve(response.data); 53 | }); 54 | }); 55 | } 56 | 57 | function removeJob(job) { 58 | clearInterval(job.interval); 59 | store.jobs = store.jobs.filter(j => j !== job); 60 | } 61 | 62 | function removeAllJobs() { 63 | store.jobs.forEach((job) => { 64 | clearInterval(job.interval); 65 | }); 66 | store.jobs = []; 67 | } 68 | 69 | function getJobs() { 70 | axios.get(process.env.VUE_APP_API_URL + '/jobs').then((response) => { 71 | store.jobs = []; 72 | response.data.jobs.forEach((job_id) => { 73 | addJob(job_id); 74 | }); 75 | }); 76 | } 77 | 78 | function retrieveJobById(job_id) { 79 | return store.jobs.find(j => j.id === job_id); 80 | } 81 | 82 | export default { 83 | addJob, 84 | getJob, 85 | getJobs, 86 | removeAllJobs, 87 | retrieveJobById, 88 | store 89 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/notifications/index.js: -------------------------------------------------------------------------------- 1 | let store = { 2 | notifications: [] 3 | }; 4 | 5 | function addNotification(text, error = false) { 6 | let id = Math.random().toString(36).substr(2, 9); 7 | let style = {bottom: 0, }; 8 | store.notifications.forEach(() => { 9 | style.bottom += 70; 10 | }); 11 | style.bottom = style.bottom + 'px'; 12 | let notif = {id: id, text: text, style: style, error: error}; 13 | store.notifications.splice(0, 0, notif); 14 | setTimeout(() => removeNotification(notif), 5000); 15 | } 16 | 17 | function removeNotification(original_notif) { 18 | store.notifications = store.notifications.filter((notif) => { 19 | return notif.id !== original_notif.id; 20 | }); 21 | } 22 | 23 | 24 | export default { 25 | addNotification, 26 | removeNotification, 27 | store 28 | } 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/modules/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | 4 | const Login = () => import("../../pages/auth/Login"); 5 | const ForgotPassword = () => import("../../pages/auth/ForgotPassword"); 6 | const ResetPassword = () => import("../../pages/auth/ResetPassword"); 7 | const GoogleCallback = () => import("../../pages/auth/callback/GoogleCallback"); 8 | const FacebookCallback = () => import("../../pages/auth/callback/FacebookCallback"); 9 | const Main = () => import("../../pages/Home"); 10 | const About = () => import("../../pages/About"); 11 | const Account = () => import("../../pages/Account"); 12 | 13 | Vue.use(Router); 14 | 15 | export default new Router({ 16 | routes: [ 17 | 18 | { 19 | path: '*', 20 | redirect: '/login' 21 | }, 22 | { 23 | path: "/login", 24 | name: "login", 25 | meta: { 26 | hideHeader: true, 27 | transparentFooter: true 28 | }, 29 | component: Login 30 | }, 31 | { 32 | path: "/auth/email/forgot-password", 33 | name: "forgot-password", 34 | meta: { 35 | hideHeader: true, 36 | transparentFooter: true 37 | }, 38 | component: ForgotPassword 39 | }, 40 | { 41 | path: "/auth/email/reset-password/:token", 42 | name: "reset-password", 43 | meta: { 44 | hideHeader: true, 45 | transparentFooter: true 46 | }, 47 | component: ResetPassword 48 | }, 49 | { 50 | path: "/login/:token", 51 | name: "activate-account", 52 | meta: { 53 | hideHeader: true, 54 | transparentFooter: true 55 | }, 56 | component: Login 57 | }, 58 | { 59 | path: "/auth/google/callback", 60 | name: "google-callback", 61 | meta: { 62 | hideHeader: true, 63 | transparentFooter: true 64 | }, 65 | component: GoogleCallback 66 | }, 67 | { 68 | path: "/auth/facebook/callback", 69 | name: "facebook-callback", 70 | meta: { 71 | hideHeader: true, 72 | transparentFooter: true 73 | }, 74 | component: FacebookCallback 75 | }, 76 | { 77 | path: "/home", 78 | name: "home", 79 | meta: {}, 80 | component: Main 81 | }, 82 | { 83 | path: "/about", 84 | name: "about", 85 | meta: {}, 86 | component: About 87 | }, 88 | { 89 | path: "/accounts/:id", 90 | name: "account", 91 | meta: { 92 | transparentFooter: true 93 | }, 94 | component: Account 95 | } 96 | ] 97 | }); -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/About.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 134 | 135 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/Account.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 61 | 62 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 128 | 129 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/auth/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 75 | 76 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/auth/Login.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 70 | 71 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/auth/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 126 | 127 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/auth/callback/FacebookCallback.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/pages/auth/callback/GoogleCallback.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | icons: { 8 | iconfont: 'mdi', 9 | }, 10 | theme:{ 11 | themes: { 12 | light: { 13 | primary: "#41B883", 14 | secondary: "#34495E", 15 | tertiary: "#42A5F5", 16 | error: '#ff7a73', 17 | } 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/front/vue.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = { 4 | "transpileDependencies": [ 5 | "vuetify" 6 | ], 7 | {%- if cookiecutter.enable_https == 'y' %} 8 | devServer: { 9 | https: { 10 | key: fs.readFileSync('./certs/localhost-key.pem'), 11 | cert: fs.readFileSync('./certs/localhost.pem'), 12 | }, 13 | public: 'https://localhost:8080/' 14 | } 15 | {%- endif %} 16 | }; 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/manifest.yml: -------------------------------------------------------------------------------- 1 | features: 2 | - name: google_login 3 | enabled: {{cookiecutter.google_login}} 4 | resources: 5 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/google_login.py' 6 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/social/gcredentials.py' 7 | - '../{{cookiecutter.project_name}}/front/src/modules/auth/google.js' 8 | - '../{{cookiecutter.project_name}}/front/src/components/auth/GoogleLogin.vue' 9 | - '../{{cookiecutter.project_name}}/front/src/pages/auth/callback/GoogleCallback.vue' 10 | 11 | - name: facebook_login 12 | enabled: {{cookiecutter.facebook_login}} 13 | resources: 14 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/facebook_login.py' 15 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/models/social/fbcredentials.py' 16 | - '../{{cookiecutter.project_name}}/front/src/modules/auth/facebook.js' 17 | - '../{{cookiecutter.project_name}}/front/src/components/auth/FacebookLogin.vue' 18 | - '../{{cookiecutter.project_name}}/front/src/pages/auth/callback/FacebookCallback.vue' 19 | 20 | - name: email_login 21 | enabled: {{cookiecutter.email_login}} 22 | resources: 23 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/auth/email_login.py' 24 | - '../{{cookiecutter.project_name}}/back/templates/confirm_email.html' 25 | - '../{{cookiecutter.project_name}}/back/templates/reset_password.html' 26 | - '../{{cookiecutter.project_name}}/front/src/components/auth/EmailLogin.vue' 27 | - '../{{cookiecutter.project_name}}/front/src/pages/auth/ForgotPassword.vue' 28 | - '../{{cookiecutter.project_name}}/front/src/pages/auth/ResetPassword.vue' 29 | - '../{{cookiecutter.project_name}}/front/src/modules/auth/email.js' 30 | 31 | - name: elasticsearch 32 | enabled: {{cookiecutter.elasticsearch}} 33 | resources: 34 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/elasticsearch.py' 35 | 36 | - name: storage 37 | enabled: {{cookiecutter.storage}} 38 | resources: 39 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/core/storage.py' 40 | - '../{{cookiecutter.project_name}}/back/{{cookiecutter.project_name}}/storage/' 41 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/forgotpassword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/forgotpassword.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/full_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/full_gif.gif -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/home.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/login.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/login.gif -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/login.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/resetpassword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/resetpassword.png -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/screenshots/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinebrtd/flask-vue-template/b90025e45d648ad7d977a77bf920f338864a0d14/{{cookiecutter.project_name}}/screenshots/signup.png --------------------------------------------------------------------------------