├── frontend ├── .browserslistrc ├── public │ └── logo.png ├── src │ ├── store │ │ ├── index.js │ │ ├── app.js │ │ └── messages.js │ ├── components │ │ ├── NoteOverviewSkeleton.vue │ │ ├── PreviousPageBtn.vue │ │ ├── NoteOverview.vue │ │ ├── AppMessenger.vue │ │ └── NoteCreationForm.vue │ ├── styles │ │ └── settings.scss │ ├── plugins │ │ ├── index.js │ │ └── vuetify.js │ ├── pages │ │ ├── [...all].vue │ │ ├── index.vue │ │ └── notes │ │ │ └── [noteId] │ │ │ └── index.vue │ ├── router │ │ └── index.js │ ├── main.js │ ├── App.vue │ └── services │ │ └── websocket.js ├── .editorconfig ├── .gitignore ├── index.html ├── jsconfig.json ├── package.json ├── vite.config.mjs └── README.md ├── backend ├── .coveragerc ├── tests │ ├── __init__.py │ ├── README.md │ ├── test_utils.py │ └── test_notes.py ├── requirements.txt ├── setup.cfg ├── init.py ├── config │ ├── config.ini.example │ ├── config.env │ ├── README.md │ └── init_config.py ├── utils │ ├── socket_utils.py │ └── database_utils.py ├── controller │ └── notes.py ├── websrv.py └── services │ └── notes_services.py ├── Easy notes.png ├── .dockerignore ├── .gitignore ├── Dockerfile ├── .github ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── continuous-deployment.yml │ └── pull-request-checks.yml ├── docker-compose.yml ├── cspell.json ├── makefile ├── LICENSE └── README.md /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /backend/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | websrv.py 4 | init.py 5 | tests/* -------------------------------------------------------------------------------- /Easy notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tomansion/Vue3-FastAPI-WebApp-template/HEAD/Easy notes.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | backend/config/config.ini 2 | frontend/node_modules/ 3 | .pytest_cache/ 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tomansion/Vue3-FastAPI-WebApp-template/HEAD/frontend/public/logo.png -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import { createPinia } from "pinia"; 3 | 4 | export default createPinia(); 5 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This __init__.py file is required for Python 2 | # to recognize the tests directory as a package. 3 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.109.2 2 | termcolor==2.3.0 3 | python-arango == 7.5.7 4 | websockets==12.0 5 | requests==2.31.0 6 | uvicorn==0.27.1 7 | pydantic-settings==2.2.1 -------------------------------------------------------------------------------- /backend/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = W605 3 | exclude = __pycache__ 4 | show-source = True 5 | 6 | # Set the line max length to Black's default 7 | max-line-length = 100 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | # Back 4 | backend/config/config.ini 5 | .coverage 6 | __pycache__/ 7 | .pytest_cache 8 | backend/coverage.xml 9 | backend/dist/ 10 | 11 | # Front 12 | frontend/node_modules/ -------------------------------------------------------------------------------- /frontend/src/components/NoteOverviewSkeleton.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /backend/tests/README.md: -------------------------------------------------------------------------------- 1 | # Pytest unit tests 2 | 3 | ## Run 4 | 5 | ```bash 6 | pip install coverage pytest pytest-cov 7 | pytest --cov-report term --cov=. --cov-report=html -sx 8 | ``` 9 | 10 | ## Coverage analysis 11 | 12 | ```bash 13 | firefox htmlcov/index.html 14 | ``` 15 | -------------------------------------------------------------------------------- /frontend/src/styles/settings.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * src/styles/settings.scss 3 | * 4 | * Configures SASS variables and Vuetify overwrites 5 | */ 6 | 7 | // https://vuetifyjs.com/features/sass-variables/` 8 | // @use 'vuetify/settings' with ( 9 | // $color-pack: false 10 | // ); 11 | -------------------------------------------------------------------------------- /frontend/src/store/app.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | export const useAppStore = defineStore("app", { 4 | state: () => ({ 5 | session: null, 6 | }), 7 | actions: { 8 | setSession(data) { 9 | this.session = data; 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /backend/init.py: -------------------------------------------------------------------------------- 1 | import config.init_config as config 2 | import utils.database_utils as dbUtils 3 | from services.notes_services import init_notes 4 | 5 | 6 | def init(): 7 | # Init config file 8 | config.init_config() 9 | 10 | # Init Database 11 | dbUtils.setup() 12 | 13 | # Init notes service 14 | init_notes() 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /backend/config/config.ini.example: -------------------------------------------------------------------------------- 1 | # Application configuration file 2 | 3 | [NOTES] 4 | NOTES_NUMBER_MAX = 10 5 | NOTES_TITLE_LENGTH_MAX = 50 6 | NOTES_CONTENT_LENGTH_MAX = 500 7 | 8 | [ARANGODB] 9 | HOST = ... 10 | PORT = ... 11 | DATABASE = ... 12 | USER = ... 13 | PASSWORD = ... 14 | 15 | ; More info in the 'backend/config/README.md' file 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/index.js 3 | * 4 | * Automatically included in `./src/main.js` 5 | */ 6 | 7 | // Plugins 8 | import vuetify from "./vuetify"; 9 | import pinia from "@/store"; 10 | import router from "@/router"; 11 | 12 | export function registerPlugins(app) { 13 | app.use(vuetify).use(router).use(pinia); 14 | } 15 | -------------------------------------------------------------------------------- /backend/config/config.env: -------------------------------------------------------------------------------- 1 | # This document is used to document the environment variables used by the application. 2 | 3 | # Notes 4 | APP_NOTES_NUMBER_MAX=10 5 | APP_NOTES_TITLE_LENGTH_MAX=50 6 | APP_NOTES_CONTENT_LENGTH_MAX=500 7 | 8 | # ArangoDB 9 | APP_ARANGODB_HOST=... 10 | APP_ARANGODB_PORT=... 11 | APP_ARANGODB_DATABASE=... 12 | APP_ARANGODB_USER=... 13 | APP_ARANGODB_PASSWORD=... -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Easy notes 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "es5", 5 | "module": "esnext", 6 | "baseUrl": "./", 7 | "moduleResolution": "node", 8 | "paths": { 9 | "@/*": [ 10 | "src/*" 11 | ] 12 | }, 13 | "lib": [ 14 | "esnext", 15 | "dom", 16 | "dom.iterable", 17 | "scripthost" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Vue Frontend 2 | FROM node:20 as build-stage 3 | WORKDIR /frontend 4 | COPY frontend/ . 5 | RUN npm install 6 | RUN npm run build 7 | 8 | # Python Backend 9 | FROM python:3.9 10 | WORKDIR /backend 11 | COPY backend/ . 12 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 13 | COPY --from=build-stage /frontend/dist dist 14 | CMD ["uvicorn", "websrv:app", "--host", "0.0.0.0", "--port", "3000"] -------------------------------------------------------------------------------- /frontend/src/pages/[...all].vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/PreviousPageBtn.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/vuetify.js 3 | * 4 | * Framework documentation: https://vuetifyjs.com` 5 | */ 6 | 7 | // Styles 8 | import "@mdi/font/css/materialdesignicons.css"; 9 | import "vuetify/styles"; 10 | 11 | // Composables 12 | import { createVuetify } from "vuetify"; 13 | 14 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 15 | export default createVuetify({ 16 | // 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * router/index.ts 3 | * 4 | * Automatic routes for `./src/pages/*.vue` 5 | * Doc ? https://github.com/hannoeru/vite-plugin-pages 6 | */ 7 | 8 | // Composable 9 | import { createRouter, createWebHistory } from "vue-router/auto"; 10 | 11 | const router = createRouter({ 12 | history: createWebHistory(), 13 | }); 14 | 15 | // Logs created routes 16 | // console.log(router.getRoutes()); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Screenshots (if appropriate): 4 | 5 | ## Issue ticket number 6 | Closes # 7 | 8 | ## Checklist before requesting a review 9 | - [ ] I have performed a self-review of my code 10 | - [ ] I have commented my code, particularly in hard-to-understand areas 11 | - [ ] I have written unit tests for my code if applicable 12 | - [ ] I have formatted my code according to the style guide (use `make format` & `make check`) 13 | -------------------------------------------------------------------------------- /backend/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from utils.database_utils import ( 2 | setup as setup_database, 3 | empty_collection_by_name, 4 | delete_collection_by_name, 5 | ) 6 | 7 | NOTES_COLLECTION_NAME = "notes_test" 8 | 9 | 10 | def setup_test_database(): 11 | setup_database(notes_collection_name=NOTES_COLLECTION_NAME) 12 | empty_collection_by_name(NOTES_COLLECTION_NAME) 13 | 14 | 15 | def delete_test_database(): 16 | delete_collection_by_name(NOTES_COLLECTION_NAME) 17 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * main.js 3 | * 4 | * Bootstraps Vuetify and other plugins then mounts the App` 5 | */ 6 | 7 | // Plugins 8 | import { registerPlugins } from "@/plugins"; 9 | 10 | // Components 11 | import App from "./App.vue"; 12 | 13 | // Composables 14 | import { createApp } from "vue"; 15 | 16 | // Services 17 | import { webSocketService } from "./services/websocket.js"; 18 | 19 | const app = createApp(App); 20 | 21 | app.config.globalProperties.$websocket = webSocketService; 22 | 23 | registerPlugins(app); 24 | 25 | app.mount("#app"); 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | web_app_template: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | environment: 8 | # Notes 9 | - APP_NOTES_NUMBER_MAX=10 10 | - APP_NOTES_TITLE_LENGTH_MAX=50 11 | - APP_NOTES_CONTENT_LENGTH_MAX=500 12 | 13 | # ArangoDB 14 | - APP_ARANGODB_HOST=... 15 | - APP_ARANGODB_PORT=... 16 | - APP_ARANGODB_DATABASE=... 17 | - APP_ARANGODB_USER=... 18 | - APP_ARANGODB_PASSWORD=... 19 | container_name: web_app_template 20 | restart: always 21 | ports: 22 | - 3000:3000 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /frontend/src/components/NoteOverview.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the project 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - Browser [e.g. chrome, safari] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /frontend/src/store/messages.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { v4 } from "uuid"; 3 | 4 | const store = defineStore("messages", { 5 | state: () => { 6 | return { 7 | messages: [], 8 | }; 9 | }, 10 | actions: { 11 | addMessage({ type, message }) { 12 | // Generate a new message id and add it to the messages array 13 | const newMessageId = v4(); 14 | this.messages.push({ 15 | id: newMessageId, 16 | message, 17 | type, 18 | }); 19 | 20 | // Remove the message after 5 seconds 21 | setTimeout(() => { 22 | this.removeMessage(newMessageId); 23 | }, 5000); 24 | }, 25 | removeMessage(id) { 26 | this.messages = this.messages.filter((message) => message.id !== id); 27 | }, 28 | }, 29 | }); 30 | 31 | export default store; 32 | -------------------------------------------------------------------------------- /backend/utils/socket_utils.py: -------------------------------------------------------------------------------- 1 | from fastapi import WebSocket, WebSocketDisconnect 2 | from typing import List 3 | 4 | 5 | class ConnectionManager: 6 | def __init__(self): 7 | self.active_connections: List[WebSocket] = [] 8 | 9 | async def connect(self, websocket: WebSocket): 10 | await websocket.accept() 11 | self.active_connections.append(websocket) 12 | 13 | def disconnect(self, websocket: WebSocket): 14 | if websocket in self.active_connections: 15 | self.active_connections.remove(websocket) 16 | 17 | async def broadcast(self, data: dict): 18 | for connection in self.active_connections: 19 | try: 20 | await connection.send_json(data) 21 | except WebSocketDisconnect: 22 | self.disconnect(connection) 23 | 24 | 25 | connection_manager = ConnectionManager() 26 | -------------------------------------------------------------------------------- /frontend/src/services/websocket.js: -------------------------------------------------------------------------------- 1 | const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; 2 | const host = window.location.host; 3 | const wsUrl = `${protocol}//${host}/ws`; 4 | 5 | class WebSocketService { 6 | constructor() { 7 | this.socket = new WebSocket(wsUrl); 8 | this.callbacks = []; 9 | } 10 | 11 | onMessage(message, callback) { 12 | this.callbacks.push({ message, callback }); 13 | 14 | this.socket.onmessage = (event) => { 15 | const data = JSON.parse(event.data); 16 | const keys = Object.keys(data); 17 | 18 | for (const key of keys) { 19 | const callbacks = this.callbacks.filter((cb) => cb.message === key); 20 | callbacks.forEach((cb) => cb.callback(data[key])); 21 | } 22 | }; 23 | } 24 | 25 | offMessage(message) { 26 | this.callbacks = this.callbacks.filter((cb) => cb.message !== message); 27 | } 28 | 29 | close() { 30 | if (this.socket) this.socket.close(); 31 | } 32 | } 33 | 34 | const webSocketService = new WebSocketService(); 35 | 36 | export { webSocketService }; 37 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "dictionaries": [ 5 | "typescript", 6 | "node", 7 | "html", 8 | "css", 9 | "python" 10 | ], 11 | "words": [ 12 | "Arango", 13 | "ARANGODB", 14 | "autouse", 15 | "axios", 16 | "Composables", 17 | "connexion", 18 | "fastapi", 19 | "htmlcov", 20 | "materialdesignicons", 21 | "pinia", 22 | "pycache", 23 | "pydantic", 24 | "pytest", 25 | "Redoc", 26 | "Roboto", 27 | "socketio", 28 | "Stmts", 29 | "termcolor", 30 | "tomansion", 31 | "Traefik", 32 | "unplugin", 33 | "uvicorn", 34 | "Vetur", 35 | "Vuetify", 36 | "vuex", 37 | "websockets", 38 | "websrv", 39 | "werkzeug", 40 | "wght" 41 | ], 42 | "flagWords": [], 43 | "ignorePaths": [ 44 | "*.svg", 45 | "*.json", 46 | "*.png", 47 | "*.xml", 48 | "htmlcov", 49 | "config.ini", 50 | "__pycache__", 51 | ".github/workflows", 52 | "node_modules", 53 | "TODO", 54 | "back/requirements.txt", 55 | "dist/", 56 | "build/", 57 | "images/", 58 | "assets/", 59 | "static/", 60 | "public/" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuetify-project", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "serve": "cross-env NODE_OPTIONS='--no-warnings' vite", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "prettier": "prettier --write \"src/**/*.{js,vue,css}\"", 9 | "prettier:check": "prettier --check \"src/**/*.{js,vue,css}\"" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "7.0.96", 13 | "axios": "^1.6.7", 14 | "core-js": "^3.34.0", 15 | "roboto-fontface": "*", 16 | "uuid": "^9.0.1", 17 | "vis-network": "^9.1.9", 18 | "vue": "^3.3.0", 19 | "vuetify": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "@vitejs/plugin-vue": "^4.5.0", 23 | "cross-env": "^7.0.3", 24 | "html-webpack-inline-source-plugin": "^0.0.10", 25 | "pinia": "^2.1.0", 26 | "prettier": "^3.2.5", 27 | "sass": "^1.69.0", 28 | "unplugin-auto-import": "^0.17.3", 29 | "unplugin-fonts": "^1.1.0", 30 | "unplugin-vue-components": "^0.26.0", 31 | "unplugin-vue-router": "^0.7.0", 32 | "vite": "^5.0.0", 33 | "vite-plugin-vue-layouts": "^0.10.0", 34 | "vite-plugin-vuetify": "^2.0.0", 35 | "vue-router": "^4.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .EXPORT_ALL_VARIABLES: 2 | 3 | # Install dependencies 4 | install: 5 | cd backend && pip install -r requirements.txt 6 | cd frontend && npm install 7 | 8 | # Run the application in development mode 9 | run_backend: 10 | cd backend && uvicorn websrv:app --reload --host 0.0.0.0 --port 3000 11 | 12 | run_frontend: 13 | cd frontend && npm run serve 14 | 15 | run: 16 | make run_backend & make run_frontend 17 | 18 | start: 19 | make run 20 | 21 | # Testing 22 | install_test: 23 | pip install coverage pytest pytest-cov 24 | 25 | test: 26 | cd backend && pytest --cov-report term --cov=. --cov-report=html -sx 27 | 28 | # Code quality 29 | format: 30 | # ----- Formatting Python code with Black 31 | cd backend && black . 32 | 33 | # ----- Formatting JavaScript code with Prettier 34 | cd frontend && npm run prettier 35 | 36 | check: 37 | # ----- Validating Black code style 38 | cd backend && black --check --diff . 39 | 40 | # ----- Validating Flake8 code style 41 | cd backend && flake8 . 42 | 43 | # ----- Validating Prettier code style 44 | cd frontend && npm run prettier:check 45 | 46 | # ----- Validating CSpell errors 47 | cspell --no-progress . 48 | 49 | # ----- The code is formatted correctly -------------------------------------------------------------------------------- /.github/workflows/continuous-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Declare variables 9 | env: 10 | APP_NAME: web_app_template 11 | 12 | jobs: 13 | build: 14 | name: Build the docker image 15 | runs-on: ubuntu-20.04 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Log in to docker hub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | 27 | - name: Build the Docker image 28 | run: docker build . -t ${{ env.APP_NAME }} 29 | 30 | - name: Tag the Docker image with latest and the tag name 31 | run: | 32 | echo ${{ github.sha }} 33 | docker tag ${{ env.APP_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.APP_NAME }}:latest 34 | 35 | - name: Upload Docker image to Docker Hub 36 | run: docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.APP_NAME }} --all-tags 37 | 38 | deploy: 39 | needs: build 40 | name: Deploy to hosted server 41 | runs-on: ubuntu-20.04 42 | steps: 43 | - name: Remote ssh 44 | uses: appleboy/ssh-action@master 45 | with: 46 | host: ${{secrets.SSH_HOST}} 47 | username: ${{secrets.SSH_USERNAME}} 48 | password: ${{secrets.SSH_PASSWORD}} 49 | port: ${{secrets.SSH_PORT}} 50 | script: ${{secrets.SSH_SCRIPT_PATH}} 51 | -------------------------------------------------------------------------------- /backend/config/README.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | This folder contains the configuration files and services for the application backend. 4 | 5 | ## Files 6 | 7 | ### [`init_config.py`](./init_config.py) file, the configuration loader 8 | 9 | This file is the configuration loader, it loads the configuration first from the `config.ini` file, then from the environment variables. If some required configuration elements are missing from the configuration file or the environment variables, the application will raise an exception. 10 | 11 | ### [`config.ini.example`](./config.ini.example) file, the configuration example file 12 | 13 | This file is a configuration example file, it serves as a documentation for new developers. 14 | 15 | ### [`config.env`](./config.env) file, the environment file 16 | 17 | This file documents the environment variables that the backend application uses. This file is used for documentation purposes only. 18 | 19 | ### [`config.ini`](./config.ini) file, the configuration file 20 | 21 | This file is the configuration file for the backend application, it will be used by the application to load the configuration. 22 | 23 | You can create a new configuration file by copying and modifying the `config.ini.example` file. 24 | 25 | ```bash 26 | cp backend/config/config.ini.example backend/config/config.ini 27 | nano backend/config/config.ini 28 | ``` 29 | 30 | Do not commit the `config.ini` file to the repository, it contains sensitive information. 31 | 32 | ## Adding new configuration elements 33 | 34 | Here are the steps to add new configuration elements in case the application needs it: 35 | 36 | 1. Update the `init_config.py` file to load the new configuration elements 37 | 2. Add the new configuration elements your local `config.ini` file 38 | 3. Document the new configuration elements in the `config.ini.example` file 39 | 4. Document the new configuration environment variables in the `config.env` file 40 | -------------------------------------------------------------------------------- /frontend/vite.config.mjs: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import AutoImport from "unplugin-auto-import/vite"; 3 | import Components from "unplugin-vue-components/vite"; 4 | import Fonts from "unplugin-fonts/vite"; 5 | import Layouts from "vite-plugin-vue-layouts"; 6 | import Vue from "@vitejs/plugin-vue"; 7 | import VueRouter from "unplugin-vue-router/vite"; 8 | import Vuetify, { transformAssetUrls } from "vite-plugin-vuetify"; 9 | 10 | // Utilities 11 | import { defineConfig } from "vite"; 12 | import { fileURLToPath, URL } from "node:url"; 13 | 14 | // https://vitejs.dev/config/ 15 | export default defineConfig({ 16 | plugins: [ 17 | VueRouter(), 18 | Layouts(), 19 | Vue({ 20 | template: { transformAssetUrls }, 21 | }), 22 | // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme 23 | Vuetify({ 24 | autoImport: true, 25 | styles: { 26 | configFile: "src/styles/settings.scss", 27 | }, 28 | }), 29 | Components(), 30 | Fonts({ 31 | google: { 32 | families: [ 33 | { 34 | name: "Roboto", 35 | styles: "wght@100;300;400;500;700;900", 36 | }, 37 | ], 38 | }, 39 | }), 40 | AutoImport({ 41 | imports: ["vue", "vue-router"], 42 | vueTemplate: true, 43 | }), 44 | ], 45 | define: { "process.env": {} }, 46 | resolve: { 47 | alias: { 48 | "@": fileURLToPath(new URL("./src", import.meta.url)), 49 | }, 50 | extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"], 51 | }, 52 | server: { 53 | proxy: { 54 | "/api": { 55 | target: "http://localhost:3000", 56 | changeOrigin: true, 57 | secure: false, 58 | }, 59 | "/ws": { 60 | target: "ws://localhost:3000", 61 | changeOrigin: true, 62 | secure: false, 63 | ws: true, 64 | }, 65 | }, 66 | port: 8080, 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /backend/controller/notes.py: -------------------------------------------------------------------------------- 1 | from services.notes_services import ( 2 | get_notes as get_notes_service, 3 | get_note as get_note_service, 4 | create_note as create_note_service, 5 | delete_note as delete_note_service, 6 | ) 7 | from utils.socket_utils import connection_manager 8 | 9 | from fastapi import APIRouter 10 | from pydantic import BaseModel, Field 11 | from typing import List 12 | 13 | router = APIRouter() 14 | 15 | 16 | class NoteCreation(BaseModel): 17 | title: str = Field( 18 | ..., example="Things to do", description="The note title", min_length=1 19 | ) 20 | content: str = Field(..., example="Buy milk", description="The note content") 21 | 22 | 23 | class Note(NoteCreation): 24 | id: str = Field(..., description="The note ID") 25 | 26 | 27 | ############################################################################# 28 | # Notes API 29 | ############################################################################# 30 | 31 | 32 | @router.get("/notes", response_model=List[Note], tags=["Notes"]) 33 | def get_notes(): 34 | # Get the notes list 35 | return get_notes_service() 36 | 37 | 38 | @router.get("/notes/{note_id}", response_model=Note, tags=["Notes"]) 39 | def get_note(note_id): 40 | # Get a specific note 41 | return get_note_service(note_id) 42 | 43 | 44 | @router.post("/notes", response_model=Note, tags=["Notes"]) 45 | async def create_note(body: NoteCreation): 46 | # Create the notes 47 | created_note = create_note_service( 48 | body.title, 49 | body.content, 50 | ) 51 | 52 | # Broadcast the note creation 53 | await connection_manager.broadcast({"noteCreationUpdate": created_note}) 54 | 55 | return created_note 56 | 57 | 58 | @router.delete("/notes/{note_id}", tags=["Notes"]) 59 | async def delete_note(note_id): 60 | # Delete a note 61 | delete_note_service(note_id) 62 | 63 | # Broadcast the note deletion 64 | await connection_manager.broadcast({"noteDeletionUpdate": note_id}) 65 | -------------------------------------------------------------------------------- /frontend/src/components/AppMessenger.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 75 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-checks.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Checks 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | 10 | jobs: 11 | black-format-check: # Check that the backend codebase is formatted with black 12 | name: Black format check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.8 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: 3.8 20 | - name: Install dependencies and check black format 21 | run: | 22 | cd backend 23 | python -m pip install --upgrade pip 24 | pip install black 25 | black --check --diff . 26 | 27 | flake8-check: # Check that the project is formatted with flake8 28 | name: Flake8 check 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Set up Python 3.8 33 | uses: actions/setup-python@v1 34 | with: 35 | python-version: 3.8 36 | - name: Install dependencies and check flake8 format 37 | run: | 38 | cd backend 39 | python -m pip install --upgrade pip 40 | pip install flake8 41 | flake8 . 42 | 43 | prettier-check: # Check that the frontend codebase is formatted with prettier 44 | name: Prettier format check 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | - name: Set up Node 18 49 | uses: actions/setup-node@v2 50 | with: 51 | node-version: 18 52 | - name: Install dependencies and check prettier format 53 | run: cd frontend && npm install && npm run prettier:check 54 | 55 | cspell-check: # Check that the project does not contain spelling errors 56 | name: CSpell check 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: Set up Node 18 61 | uses: actions/setup-node@v2 62 | with: 63 | node-version: 18 64 | - name: Install dependencies and check prettier format 65 | run: npm install -g cspell && cspell --no-summary --no-progress --no-color . 66 | -------------------------------------------------------------------------------- /backend/websrv.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, WebSocket, WebSocketDisconnect 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.responses import FileResponse 5 | from controller.notes import router as party_router 6 | from utils.socket_utils import connection_manager 7 | from pathlib import Path 8 | from init import init 9 | 10 | PORT = 3000 11 | 12 | # Initialize the FastAPI app 13 | app = FastAPI() 14 | app.add_middleware( 15 | CORSMiddleware, 16 | allow_credentials=True, 17 | allow_origins=["*"], 18 | allow_methods=["*"], 19 | allow_headers=["*"], 20 | ) 21 | app.include_router(party_router, prefix="/api") 22 | 23 | 24 | # Add alive status route 25 | @app.get("/api") 26 | async def alive(): 27 | return {"status": "alive"} 28 | 29 | 30 | # Websocket endpoint 31 | @app.websocket("/ws") 32 | async def websocket_endpoint(websocket: WebSocket): 33 | await connection_manager.connect(websocket) 34 | try: 35 | while True: 36 | data = await websocket.receive_text() 37 | await connection_manager.broadcast(f"Message text was: {data}") 38 | except WebSocketDisconnect: 39 | connection_manager.disconnect(websocket) 40 | await connection_manager.broadcast("A client just disconnected.") 41 | 42 | 43 | # Serve the Vue app in production mode 44 | try: 45 | # Directory where Vue app build output is located 46 | build_dir = Path(__file__).resolve().parent / "dist" 47 | index_path = build_dir / "index.html" 48 | 49 | # Serve assets files from the build directory 50 | app.mount("/assets", StaticFiles(directory=build_dir / "assets"), name="assets") 51 | 52 | # Catch-all route for SPA 53 | @app.get("/{catchall:path}") 54 | async def serve_spa(catchall: str): 55 | # If the requested file exists, serve it, else serve index.html 56 | path = build_dir / catchall 57 | if path.is_file(): 58 | return FileResponse(path) 59 | return FileResponse(index_path) 60 | 61 | except RuntimeError: 62 | # The build directory does not exist 63 | print("No build directory found. Running in development mode.") 64 | 65 | 66 | # Initialize the app 67 | init() 68 | 69 | print("\nRunning FastAPI app...") 70 | print(" - FastAPI is available at " + f"http://localhost:{PORT}/api") 71 | print(" - Swagger UI is available at " + f"http://localhost:{PORT}/docs") 72 | print(" - Redoc is available at " + f"http://localhost:{PORT}/redoc") 73 | print("") 74 | -------------------------------------------------------------------------------- /backend/services/notes_services.py: -------------------------------------------------------------------------------- 1 | from utils.database_utils import ( 2 | get_number_of_notes as db_get_number_of_notes, 3 | get_notes as db_get_notes, 4 | get_note as db_get_note, 5 | add_note as db_add_note, 6 | delete_note as db_delete_note, 7 | ) 8 | from config.init_config import get_config 9 | from fastapi import HTTPException 10 | 11 | 12 | def init_notes(): 13 | # Init the notes service 14 | 15 | # Create few notes if the notes collection is empty 16 | if db_get_number_of_notes() == 0: 17 | create_note( 18 | "Things to do", 19 | "Buy milk", 20 | ) 21 | create_note( 22 | "Ideas", 23 | "Create a new app", 24 | ) 25 | create_note( 26 | "Books to read", 27 | "The Pragmatic Programmer", 28 | ) 29 | 30 | 31 | def get_notes(): 32 | # Get the notes list 33 | return db_get_notes() 34 | 35 | 36 | def get_note(note_id): 37 | # Get a specific note 38 | note = db_get_note(note_id) 39 | if note is None: 40 | raise HTTPException(status_code=404, detail="Note not found") 41 | return note 42 | 43 | 44 | def create_note(title, content): 45 | # Verify the input 46 | config = get_config() 47 | NOTES_NUMBER_MAX = config["NOTES"]["NOTES_NUMBER_MAX"] 48 | NOTES_TITLE_LENGTH_MAX = config["NOTES"]["NOTES_TITLE_LENGTH_MAX"] 49 | NOTES_CONTENT_LENGTH_MAX = config["NOTES"]["NOTES_CONTENT_LENGTH_MAX"] 50 | 51 | if len(title) > NOTES_TITLE_LENGTH_MAX: 52 | raise HTTPException( 53 | status_code=400, 54 | detail=f"Title is too long (max {NOTES_TITLE_LENGTH_MAX} characters)", 55 | ) 56 | 57 | if len(content) > NOTES_CONTENT_LENGTH_MAX: 58 | raise HTTPException( 59 | status_code=400, 60 | detail=f"Content is too long (max {NOTES_CONTENT_LENGTH_MAX} characters)", 61 | ) 62 | 63 | # Check the number of notes 64 | number_of_notes = db_get_number_of_notes() 65 | if number_of_notes > NOTES_NUMBER_MAX: 66 | raise HTTPException( 67 | status_code=402, 68 | detail=f"Too many notes (max {NOTES_NUMBER_MAX})", 69 | ) 70 | 71 | # Create the note 72 | note = { 73 | "title": title, 74 | "content": content, 75 | } 76 | 77 | note_id = db_add_note(note) 78 | note["id"] = note_id 79 | 80 | return note 81 | 82 | 83 | def delete_note(note_id): 84 | # Delete a note 85 | note = db_get_note(note_id) 86 | if note is None: 87 | raise HTTPException(status_code=404, detail="Note not found") 88 | 89 | db_delete_note(note_id) 90 | -------------------------------------------------------------------------------- /backend/tests/test_notes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi import HTTPException 3 | 4 | from utils.database_utils import ( 5 | get_number_of_notes as db_get_number_of_notes, 6 | get_notes as db_get_notes, 7 | get_note as db_get_note, 8 | ) 9 | import services.notes_services as notes_service 10 | from config.init_config import get_config 11 | from tests.test_utils import setup_test_database, delete_test_database 12 | 13 | 14 | @pytest.fixture(scope="module", autouse=True) 15 | def setup_module(): 16 | setup_test_database() 17 | config = get_config() 18 | yield config 19 | delete_test_database() 20 | 21 | 22 | def test_init_notes(): 23 | notes_service.init_notes() 24 | assert db_get_number_of_notes() == 3 25 | 26 | 27 | def test_get_notes(): 28 | notes = notes_service.get_notes() 29 | assert len(notes) == 3 30 | 31 | 32 | def test_get_note(): 33 | notes = notes_service.get_notes() 34 | note = notes_service.get_note(notes[0]["id"]) 35 | assert note is not None 36 | assert note["title"] == "Things to do" 37 | 38 | 39 | def test_get_non_existent_note(): 40 | with pytest.raises(HTTPException) as e: 41 | notes_service.get_note("non_existent_id") 42 | assert str(e.value.detail) == "Note not found" 43 | 44 | 45 | def test_create_note(): 46 | new_note = notes_service.create_note( 47 | title="New Note", content="This is a new note." 48 | ) 49 | assert db_get_number_of_notes() == 4 50 | notes = db_get_notes() 51 | assert notes[-1]["title"] == "New Note" 52 | assert notes[-1]["content"] == "This is a new note." 53 | assert notes[-1]["id"] == new_note["id"] 54 | 55 | 56 | def test_delete_note(): 57 | notes = notes_service.get_notes() 58 | note = notes_service.get_note(notes[0]["id"]) 59 | notes_service.delete_note(note["id"]) 60 | assert db_get_number_of_notes() == 3 61 | assert db_get_note(note["id"]) is None 62 | 63 | 64 | def test_delete_non_existent_note(): 65 | with pytest.raises(HTTPException) as e: 66 | notes_service.delete_note("non_existent_id") 67 | assert str(e.value.detail) == "Note not found" 68 | 69 | 70 | def test_create_note_invalid_title(setup_module): 71 | config = setup_module 72 | NOTES_TITLE_LENGTH_MAX = config["NOTES"]["NOTES_TITLE_LENGTH_MAX"] 73 | 74 | with pytest.raises(HTTPException) as e: 75 | notes_service.create_note( 76 | "X" * (NOTES_TITLE_LENGTH_MAX + 1), "This is a new note." 77 | ) 78 | assert "Title is too long" in str(e.value.detail) 79 | assert str(NOTES_TITLE_LENGTH_MAX) in str(e.value.detail) 80 | 81 | 82 | def test_create_note_invalid_content(setup_module): 83 | config = setup_module 84 | NOTES_CONTENT_LENGTH_MAX = config["NOTES"]["NOTES_CONTENT_LENGTH_MAX"] 85 | 86 | with pytest.raises(HTTPException) as e: 87 | notes_service.create_note("New Note", "X" * (NOTES_CONTENT_LENGTH_MAX + 1)) 88 | assert "Content is too long" in str(e.value.detail) 89 | assert str(NOTES_CONTENT_LENGTH_MAX) in str(e.value.detail) 90 | 91 | 92 | def test_max_notes(setup_module): 93 | config = setup_module 94 | NOTES_NUMBER_MAX = config["NOTES"]["NOTES_NUMBER_MAX"] 95 | 96 | while db_get_number_of_notes() <= NOTES_NUMBER_MAX: 97 | notes_service.create_note("Test Note", "This is a test note.") 98 | 99 | with pytest.raises(HTTPException) as e: 100 | notes_service.create_note("Another Note", "This is another test note.") 101 | 102 | assert "Too many notes" in str(e.value.detail) 103 | assert str(NOTES_NUMBER_MAX) in str(e.value.detail) 104 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vuetify (Default) 2 | 3 | This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch. 4 | 5 | ## ❗️ Important Links 6 | 7 | - 📄 [Docs](https://vuetifyjs.com/) 8 | - 🚨 [Issues](https://issues.vuetifyjs.com/) 9 | - 🏬 [Store](https://store.vuetifyjs.com/) 10 | - 🎮 [Playground](https://play.vuetifyjs.com/) 11 | - 💬 [Discord](https://community.vuetifyjs.com) 12 | 13 | ## 💿 Install 14 | 15 | Set up your project using your preferred package manager. Use the corresponding command to install the dependencies: 16 | 17 | | Package Manager | Command | 18 | | --------------------------------------------------------- | -------------- | 19 | | [yarn](https://yarnpkg.com/getting-started) | `yarn install` | 20 | | [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` | 21 | | [pnpm](https://pnpm.io/installation) | `pnpm install` | 22 | | [bun](https://bun.sh/#getting-started) | `bun install` | 23 | 24 | After completing the installation, your environment is ready for Vuetify development. 25 | 26 | ## ✨ Features 27 | 28 | - 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/) 29 | - 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue. 30 | - 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) 31 | - ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/) 32 | - 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) 33 | 34 | These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable. 35 | 36 | ## 💡 Usage 37 | 38 | This section covers how to start the development server and build your project for production. 39 | 40 | ### Starting the Development Server 41 | 42 | To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:8080](http://localhost:8080): 43 | 44 | ```bash 45 | yarn dev 46 | ``` 47 | 48 | (Repeat for npm, pnpm, and bun with respective commands.) 49 | 50 | > NODE_OPTIONS='--no-warnings' is added to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script. 51 | 52 | ### Building for Production 53 | 54 | To build your project for production, use: 55 | 56 | ```bash 57 | yarn build 58 | ``` 59 | 60 | (Repeat for npm, pnpm, and bun with respective commands.) 61 | 62 | Once the build process is completed, your application will be ready for deployment in a production environment. 63 | 64 | ## 💪 Support Vuetify Development 65 | 66 | This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider: 67 | 68 | ## 📑 License 69 | 70 | [MIT](http://opensource.org/licenses/MIT) 71 | 72 | Copyright (c) 2016-present Vuetify, LLC 73 | -------------------------------------------------------------------------------- /frontend/src/components/NoteCreationForm.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 139 | -------------------------------------------------------------------------------- /backend/utils/database_utils.py: -------------------------------------------------------------------------------- 1 | from config.init_config import get_config, DEBUG_COLOR, ERROR_COLOR 2 | from termcolor import colored 3 | from arango import ArangoClient, exceptions 4 | from typing import Optional, List 5 | 6 | db = None # Wrapper for the Arango database 7 | 8 | NOTES_COLLECTION_NAME = "notes" 9 | 10 | 11 | def setup(notes_collection_name=None): 12 | global db, NOTES_COLLECTION_NAME 13 | 14 | # Set the collection names 15 | if notes_collection_name is not None: 16 | NOTES_COLLECTION_NAME = notes_collection_name 17 | 18 | # Load config 19 | config = get_config() 20 | HOST = config["ARANGODB"]["HOST"] 21 | PORT = config["ARANGODB"]["PORT"] 22 | DATABASE = config["ARANGODB"]["DATABASE"] 23 | USER = config["ARANGODB"]["USER"] 24 | PASSWORD = config["ARANGODB"]["PASSWORD"] 25 | 26 | print("\nConnecting to ArangoDB...") 27 | print(f" - Host: {colored(HOST, DEBUG_COLOR)}") 28 | print(f" - Port: {colored(PORT, DEBUG_COLOR)}") 29 | print(f" - Database: {colored(DATABASE, DEBUG_COLOR)}") 30 | print(f" - User: {colored(USER, DEBUG_COLOR)}") 31 | 32 | # Initialize the ArangoDB client 33 | client = ArangoClient(hosts=f"http://{HOST}:{PORT}", resolver_max_tries=1) 34 | 35 | # Connect to the database 36 | db = client.db(DATABASE, username=USER, password=PASSWORD) 37 | 38 | # Test that the connection was successful 39 | try: 40 | version = db.version() 41 | print( 42 | f" - Connection to Arango {colored('Arango '+ version, DEBUG_COLOR)} established" 43 | ) 44 | except ConnectionAbortedError: 45 | print(" -", colored("Connection failed", ERROR_COLOR)) 46 | 47 | raise ConnectionAbortedError( 48 | colored(f"Failed to connect to ArangoDB at {HOST}:{PORT}", ERROR_COLOR) 49 | ) 50 | 51 | # Create the notes collection if it doesn't exist 52 | _create_collection_by_name(NOTES_COLLECTION_NAME) 53 | 54 | 55 | # Wrapper 56 | def db_must_be_setup(func): 57 | def wrapper_db_must_be_setup(*args, **kwargs): 58 | if db is None: 59 | raise Exception("Database not setup") 60 | return func(*args, **kwargs) 61 | 62 | return wrapper_db_must_be_setup 63 | 64 | 65 | # Collection utils 66 | @db_must_be_setup 67 | def _create_collection_by_name(collection_name): 68 | if not db.has_collection(collection_name): 69 | print(f" - Creating collection {colored(collection_name, DEBUG_COLOR)}") 70 | db.create_collection(collection_name) 71 | 72 | 73 | @db_must_be_setup 74 | def empty_collection_by_name(collection_name): 75 | if db.has_collection(collection_name): 76 | print(f" - Emptying collection {colored(collection_name, DEBUG_COLOR)}") 77 | db.collection(collection_name).truncate() 78 | 79 | 80 | @db_must_be_setup 81 | def delete_collection_by_name(collection_name): 82 | if db.has_collection(collection_name): 83 | print(f" - Deleting collection {colored(collection_name, DEBUG_COLOR)}") 84 | db.delete_collection(collection_name) 85 | 86 | 87 | # Notes functions 88 | @db_must_be_setup 89 | def get_number_of_notes() -> int: 90 | # Get the number of notes in the database 91 | return db.collection(NOTES_COLLECTION_NAME).count() 92 | 93 | 94 | @db_must_be_setup 95 | def get_notes() -> List[dict]: 96 | # Get all the notes in the database 97 | notes = list(db.collection(NOTES_COLLECTION_NAME).all()) 98 | for note in notes: 99 | note["id"] = note["_key"] 100 | return notes 101 | 102 | 103 | @db_must_be_setup 104 | def get_note(note_id) -> Optional[dict]: 105 | note = db.collection(NOTES_COLLECTION_NAME).get(note_id) 106 | if note is not None: 107 | note["id"] = note["_key"] 108 | return note 109 | 110 | 111 | @db_must_be_setup 112 | def add_note(note) -> str: 113 | res = db.collection(NOTES_COLLECTION_NAME).insert(note, overwrite=True) 114 | return res["_key"] 115 | 116 | 117 | @db_must_be_setup 118 | def delete_note(note_id) -> bool: 119 | try: 120 | db.collection(NOTES_COLLECTION_NAME).delete(note_id) 121 | return True 122 | except exceptions.DocumentDeleteError: 123 | # The note doesn't exist 124 | return False 125 | -------------------------------------------------------------------------------- /frontend/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 55 | 56 | 141 | -------------------------------------------------------------------------------- /frontend/src/pages/notes/[noteId]/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 151 | -------------------------------------------------------------------------------- /backend/config/init_config.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | from termcolor import colored 3 | 4 | import os 5 | 6 | config_path = "config/config.ini" 7 | config_parser = ConfigParser() 8 | 9 | DEBUG_COLOR = "light_blue" 10 | DEBUG_SECONDARY_COLOR = "blue" 11 | ERROR_COLOR = "light_red" 12 | SUCCESS_COLOR = "green" 13 | 14 | # Config descriptions 15 | config_descriptions = { 16 | "NOTES": { 17 | "NOTES_NUMBER_MAX": { 18 | "default": 10, 19 | "type": int, 20 | "env": "APP_NOTES_NUMBER_MAX", 21 | }, 22 | "NOTES_TITLE_LENGTH_MAX": { 23 | "default": 50, 24 | "type": int, 25 | "env": "APP_NOTES_TITLE_LENGTH_MAX", 26 | }, 27 | "NOTES_CONTENT_LENGTH_MAX": { 28 | "default": 500, 29 | "type": int, 30 | "env": "APP_NOTES_CONTENT_LENGTH_MAX", 31 | }, 32 | }, 33 | "ARANGODB": { 34 | "HOST": { 35 | "type": str, 36 | "env": "APP_ARANGODB_HOST", 37 | }, 38 | "PORT": { 39 | "type": int, 40 | "env": "APP_ARANGODB_PORT", 41 | }, 42 | "DATABASE": { 43 | "type": str, 44 | "env": "APP_ARANGODB_DATABASE", 45 | }, 46 | "USER": { 47 | "type": str, 48 | "env": "APP_ARANGODB_USER", 49 | }, 50 | "PASSWORD": { 51 | "type": str, 52 | "env": "APP_ARANGODB_PASSWORD", 53 | }, 54 | }, 55 | } 56 | 57 | # Config values 58 | config = {} 59 | 60 | 61 | def get_config_value(section, key, config_parser): 62 | # Return the value of the key in the section of the config_parser 63 | # Or return the ENV_VAR if it exists 64 | 65 | value = None 66 | 67 | # Get the value from the config file 68 | if section in config_parser and key in config_parser[section]: 69 | return config_parser[section][key] 70 | 71 | # Get the value from the environment variables 72 | ENV_VAR = config_descriptions[section][key]["env"] 73 | if ENV_VAR in os.environ: 74 | return os.environ[ENV_VAR] 75 | 76 | if value is None: 77 | # No value found in the config or in the environment variables 78 | if "default" in config_descriptions[section][key]: 79 | print( 80 | " - Missing " 81 | + colored(section, DEBUG_SECONDARY_COLOR) 82 | + " / " 83 | + colored(key, DEBUG_SECONDARY_COLOR) 84 | + " in config or in " 85 | + colored(ENV_VAR, DEBUG_SECONDARY_COLOR) 86 | + " env var, using default value" 87 | ) 88 | return config_descriptions[section][key]["default"] 89 | 90 | raise ValueError( 91 | "Missing " 92 | + colored(section + " / " + key, ERROR_COLOR) 93 | + " in " 94 | + config_path 95 | + " or in " 96 | + colored(ENV_VAR, ERROR_COLOR) 97 | + " env var.\n" 98 | + "Copy the " 99 | + colored(config_path, DEBUG_SECONDARY_COLOR) 100 | + ".example file to " 101 | + colored(config_path, DEBUG_SECONDARY_COLOR) 102 | + " and fill it." 103 | ) 104 | 105 | return value 106 | 107 | 108 | def set_config_value(section, key, value): 109 | global config 110 | 111 | # Set the value to the right type 112 | value_type = config_descriptions[section][key]["type"] 113 | if value_type == bool: 114 | if value.lower() not in ["true", "false"]: 115 | raise ValueError( 116 | " - " 117 | + colored(section, ERROR_COLOR) 118 | + " / " 119 | + colored(key, ERROR_COLOR) 120 | + " should be a boolean (true or false)" 121 | ) 122 | value = value.lower() == "true" 123 | elif value_type == int: 124 | try: 125 | value = int(value) 126 | except ValueError: 127 | raise ValueError( 128 | " - " 129 | + colored(section, ERROR_COLOR) 130 | + " / " 131 | + colored(key, ERROR_COLOR) 132 | + " should be an integer" 133 | ) 134 | elif value_type == str: 135 | value = str(value) 136 | 137 | # Set the value in the config 138 | print(" - Set " + colored(section, DEBUG_COLOR) + " / " + colored(key, DEBUG_COLOR)) 139 | config[section][key] = value 140 | 141 | 142 | def init_config(): 143 | global config 144 | 145 | print("\nInitializing configuration...") 146 | 147 | # Read the config file 148 | config_parser.read(config_path) 149 | 150 | # Set the config values 151 | for section in config_descriptions.keys(): 152 | config[section] = {} 153 | 154 | for key in config_descriptions[section].keys(): 155 | # Get the value from the config file or the environment variables 156 | value = get_config_value(section, key, config_parser) 157 | 158 | # Set the value in the config according to its type 159 | set_config_value(section, key, value) 160 | 161 | 162 | def get_config(): 163 | if config == {}: # If the config is not initialized 164 | init_config() 165 | return config 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue3 & FastAPI WebApp template 2 | 3 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | [![Code style: flake8](https://img.shields.io/badge/code%20style-flake8-1c4a6c.svg)](https://flake8.pycqa.org/en/latest/) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 8 | 9 | ![ci](https://github.com/tomansion/Vue3-FastAPI-WebApp-template/actions/workflows/pull-request-checks.yml/badge.svg) 10 | ![cd](https://github.com/tomansion/Vue3-FastAPI-WebApp-template/actions/workflows/continuous-deployment.yml/badge.svg) 11 | 12 | --- 13 | 14 | This project is a template for a fullstack web application using [Vue3](https://vuejs.org/) and [FastAPI](https://fastapi.tiangolo.com/). It includes a basic example of a web application with a simple API and a frontend that consumes it, a simple web application that allows users to create notes. New notes and deleted notes are broadcasted to all connected clients using web sockets. 15 | 16 | ![](Easy%20notes.png) 17 | 18 |
19 | 20 | Live demo
https://easynotes.tomansion.fr/
21 |
22 |
23 | 24 | ## Template features 25 | 26 | - Modern Frameworks and Tools: 27 | 28 | - [**FastAPI**](https://fastapi.tiangolo.com/): A modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. 29 | - [**Web sockets**](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API): The backend includes an example of a WebSocket endpoint that the frontend connects to.q 30 | - [**Vue3**](https://vuejs.org/): A progressive JavaScript framework for building user interfaces. 31 | - Router: [**Vue Router**](https://router.vuejs.org/): The official router for Vue.js. 32 | - State management: [**Pinia**](https://pinia.esm.dev/): A Vue Store that is designed to be used in a modular way. 33 | - HTTP client: [**Axios**](https://axios-http.com/): Promise based HTTP client for the browser and Node.js. 34 | - [**Vuetify**](https://vuetifyjs.com/): A Material Design component framework for Vue.js. 35 | - [**ArangoDB**](https://www.arangodb.com/): A multi-model database system that supports document, key/value, and graph data models. 36 | 37 | - Containerization: 38 | 39 | - [**Docker**](https://www.docker.com/): A platform for developing, shipping, and running applications using containerization. This project includes a `Dockerfile` that builds an image of the application. 40 | - [**Docker Compose**](https://docs.docker.com/compose/): A tool for defining and running multi-container Docker applications. This project includes a `docker-compose.yml` file that defines the services for the frontend and backend. 41 | - **Configuration with environment variables**: The backend uses environment variables to configure the application. 42 | 43 | - Code Quality: 44 | 45 | - [**Black**](https://pypi.org/project/black/) and [**Prettier**](https://prettier.io/): Code formatters that automatically format the code. 46 | - [**Flake8**](https://flake8.pycqa.org/en/latest/): A tool that checks the code for style and quality. 47 | - [**Cspell**](https://cspell.org/): A spell checker that checks the spelling in the code, edit the [`cspell.json`](cspell.json) file to add custom words or languages. 48 | 49 | - Testing: 50 | 51 | - [**Pytest**](https://docs.pytest.org/): A testing framework for Python that makes it easy to write small tests. 52 | 53 | - Continuous Integration and Continuous Deployment: 54 | 55 | - **GitHub Actions**: This project includes a GitHub Actions workflow that runs the tests, linters, pushes the Docker image to the Docker Hub image registry, and deploys the application by calling an SSH script. 56 | 57 | ## Getting started 58 | 59 | ### Running the application in development mode 60 | 61 | #### Prerequisites 62 | 63 | - [Python](https://www.python.org/downloads/) v3.9+ for the backend. 64 | - [Node.js](https://nodejs.org/en/download/) v19.0.0c and [npm](https://www.npmjs.com/get-npm) v8.19.2 for the frontend. 65 | - A running [ArangoDB](https://www.arangodb.com/) instance. [Install it locally](https://arangodb.com/download-major/docker/) or [use a cloud service](https://cloud.arangodb.com/). 66 | 67 | 1. Clone the repository: 68 | 69 | ```bash 70 | git clone https://github.com/Tomansion/Vue3-FastAPI-WebApp-template.git 71 | cd Vue3-FastAPI-WebApp-template 72 | ``` 73 | 74 | 2. Install the backend and frontend dependencies: 75 | 76 | ```bash 77 | make install 78 | 79 | # Or manually: 80 | cd backend 81 | pip install -r requirements.txt 82 | cd ../frontend 83 | npm install 84 | ``` 85 | 86 | 3. Modify the configuration: 87 | 88 | Follow the instructions in the [`backend/config/README.md`](backend/config/README.md). 89 | Modifying the configuration is required to give the application access to the ArangoDB instance. 90 | 91 | 4. Run the backend and frontend: 92 | 93 | ```bash 94 | make run 95 | 96 | # Or manually: 97 | cd backend 98 | uvicorn websrv:app --reload --host 0.0.0.0 --port 3000 99 | cd frontend # In another terminal 100 | npm run serve 101 | ``` 102 | 103 | ```bash 104 | # Backend expected output 105 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 106 | INFO: Started reloader process [125657] using WatchFiles 107 | No build directory found. Running in development mode. 108 | 109 | Initializing configuration... 110 | - Set NOTES / NOTES_NUMBER_MAX 111 | - Set NOTES / NOTES_TITLE_LENGTH_MAX 112 | - Set NOTES / NOTES_CONTENT_LENGTH_MAX 113 | - Set ARANGODB / HOST 114 | - Set ARANGODB / PORT 115 | - Set ARANGODB / DATABASE 116 | - Set ARANGODB / USER 117 | - Set ARANGODB / PASSWORD 118 | 119 | Connecting to ArangoDB... 120 | - Host: *** 121 | - Port: *** 122 | - Database: WebAppTemplate 123 | - User: WebAppTemplate 124 | - Connection to Arango Arango 3.11.5 established 125 | 126 | Running FastAPI app... 127 | - FastAPI is available at http://localhost:3000/api 128 | - Swagger UI is available at http://localhost:3000/docs 129 | - Redoc is available at http://localhost:3000/redoc 130 | 131 | INFO: Started server process [125659] 132 | INFO: Waiting for application startup. 133 | INFO: Application startup complete. 134 | ``` 135 | 136 | 5. Open the different services in your browser: 137 | 138 | - The application frontend: [http://localhost:8080](http://localhost:8080) 139 | - The FastAPI backend: [http://localhost:3000](http://localhost:3000) 140 | - The API SwaggerUI documentation: [http://localhost:3000/docs](http://localhost:3000/docs) 141 | - The API Redoc documentation: [http://localhost:3000/redoc](http://localhost:3000/docs) 142 | 143 | ### Running the tests 144 | 145 | ```bash 146 | # Install the test dependencies: 147 | make install_test 148 | 149 | # Run the tests: 150 | make test 151 | 152 | # Or manually: 153 | cd backend 154 | pytest --cov-report term --cov=. --cov-report=html -sx 155 | firefox htmlcov/index.html 156 | ``` 157 | 158 | ```bash 159 | # Expected output 160 | Name Stmts Miss Cover 161 | ------------------------------------------------ 162 | config/init_config.py 51 14 73% 163 | services/notes_services.py 36 0 100% 164 | utils/database_utils.py 76 6 92% 165 | ------------------------------------------------ 166 | TOTAL 163 20 88% 167 | 168 | ============= 10 passed in 1.52s =============== 169 | # The detailed coverage report wil be available in the `htmlcov` directory. 170 | ``` 171 | 172 | ### Code quality 173 | 174 | This application provide a [Makefile](./makefile) with some commands to help you with the code quality: 175 | 176 | ```bash 177 | # Format the code with Black and Prettier: 178 | make format 179 | 180 | # Check the code with Black, Prettier, Flake8, and cSpell: 181 | make check 182 | 183 | # A pipeline is included in the GitHub Actions workflow that runs the linters, so make sure to fix any issues before pushing the code. 184 | ``` 185 | 186 | ### Running the application with [Docker Compose](https://docs.docker.com/compose/) 187 | 188 | ```bash 189 | # Clone the repository: 190 | git clone https://github.com/Tomansion/Vue3-FastAPI-WebApp-template.git 191 | cd Vue3-FastAPI-WebApp-template 192 | 193 | # Set the docker-compose environment variables 194 | # More information in the backend/config/config.env file 195 | nano docker-compose.yml 196 | 197 | # Build and run the application with Docker Compose: 198 | docker-compose up --build 199 | ``` 200 | 201 | The application should be available at: [http://localhost:3000](http://localhost:3000) 202 | 203 | ### Setting up the GitHub continuous deployment pipeline 204 | 205 | This project includes a GitHub Actions workflow that builds the Docker image, pushes it to the Docker Hub image registry, and deploys the application by calling an SSH script. 206 | 207 | #### Prerequisites 208 | 209 | - A Linux server with Docker and Docker Compose installed. 210 | - A [Docker Hub](https://hub.docker.com/) account. 211 | 212 | #### Steps 213 | 214 | 1. Create a new repository from this template and push it to GitHub. 215 | 2. Copy and edit the [`docker-compose.yml`](docker-compose.yml) file to your server. 216 | - Edit the images to point to your Docker Hub repository. 217 | - Edit the environment variables to match your configuration. 218 | - Edit the container names and the other configurations as needed. 219 | - Add your Nginx or Traefik configuration if needed. 220 | 3. Create, on your server a `deploy.sh` script with the following content: 221 | 222 | ```bash 223 | # deploy.sh 224 | cd /path/to/your/application 225 | docker-compose pull 226 | docker-compose up -d 227 | # Use chmod +x deploy.sh to make the script executable 228 | ``` 229 | 230 | 4. Add the following secrets to your repository: 231 | - `DOCKER_USERNAME`: Your Docker Hub username. 232 | - `DOCKER_PASSWORD`: Your Docker Hub password. 233 | - `SSH_HOST`: The IP address of the server where you want to deploy the application. 234 | - `SSH_PORT`: The port to connect to the server. 235 | - `SSH_USERNAME`: The username to connect to the server. 236 | - `SSH_PASSWORD`: The user password to connect to the server. 237 | - `SSH_SCRIPT_PATH`: The absolute path to the `deploy.sh` script on the server. 238 | 5. Modify the `APP_NAME`in the [continuous-deployment.yml](./.github/workflows/continuous-deployment.yml) file to match your application name. 239 | 6. Push the changes to your repository. 240 | 241 | The GitHub Actions workflow will run when you push the changes to the repository `main` branch. It will build the Docker image, push it to the Docker Hub image registry, and deploy the application by calling the `deploy.sh` script on your server. 242 | 243 | ### Recommended VSCode extensions 244 | 245 | - [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) 246 | - [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) 247 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 248 | - [Black](https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter) 249 | - [flake8](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8) 250 | - [cSpell](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) 251 | 252 | ## Help 253 | 254 | If you have any questions or need help, feel free to [open an issue](https://github.com/Tomansion/Vue3-FastAPI-WebApp-template/issues). 255 | 256 | ## Contributing 257 | 258 | I'm open to contributions and suggestions. Feel free to [open an issue](https://github.com/Tomansion/Vue3-FastAPI-WebApp-template/issues) or a make a pull request. 259 | 260 | ## TODO 261 | 262 | - [ ] Generate `config.ini.example` and `config.env` files automatically 263 | - [ ] Custom styles 264 | - [ ] I18n 265 | - [ ] Documentation 266 | - [ ] Authentication 267 | - [ ] Frontend tests 268 | - [ ] Frontend to mobile app 269 | - [x] CI/CD pipeline 270 | - [x] Demo link 271 | - [x] Pinia store 272 | - [x] Arango Database 273 | - [x] Backend tests and coverage 274 | --------------------------------------------------------------------------------