├── .gitattributes ├── .github └── workflows │ ├── attachments-service-sam-pipeline.yaml │ ├── frontend-pipeline.yaml │ └── main-service-sam-pipeline.yaml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── backend ├── attachments-service │ ├── events │ │ ├── addTodoFiles.json │ │ └── event.json │ ├── functions │ │ ├── __init__.py │ │ ├── addTodoFiles.py │ │ ├── deleteTodoFile.py │ │ ├── getTodoFiles.py │ │ └── requirements.txt │ ├── samconfig.toml │ └── template.yaml └── main-service │ ├── events │ └── event_addTodoNotes.json │ ├── functions │ ├── __init__.py │ ├── addTodo.py │ ├── addTodoNotes.py │ ├── completeTodo.py │ ├── deleteTodo.py │ ├── getTodo.py │ ├── getTodos.py │ ├── requirements.txt │ └── temp2.py │ ├── samconfig.toml │ ├── template.yaml │ └── tests │ ├── __init__.py │ ├── requirements.txt │ └── test_getTodos.py ├── blog-post ├── app-components.png ├── appflow.png ├── architecture.png ├── backend-pipeline.png ├── cover.png ├── frontend-pipeline.png ├── todo-app-backend-serverless-pipeline-aug.drawio ├── todo-app-backend-serverless-pipeline-aug.drawio.png ├── todo-app-frontend-pipeline-aug └── todo-app-pipeline-frontend-aug └── frontend ├── confirm.html ├── css └── style.css ├── favicon.png ├── home.html ├── img ├── bot-icon.gif ├── bot-icon.svg ├── cross.png ├── file.png ├── logo.png ├── logo_old.png ├── new.png ├── paperclip.png ├── search.png └── to-do-list.png ├── index.html ├── js ├── ajaxCallExample.js ├── amazon-cognito-identity.min.js ├── aws-cognito-sdk.min.js ├── script.js └── script_new.js └── register.html /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/attachments-service-sam-pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: SAM Deploy todo-houessou-com attachments service 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'backend/attachments-service/**' 8 | jobs: 9 | build-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | - uses: aws-actions/setup-sam@v1 15 | - uses: aws-actions/configure-aws-credentials@v1 16 | with: 17 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 18 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 19 | aws-region: ${{ secrets.AWS_REGION }} 20 | 21 | # sam build 22 | - name: sam build 23 | working-directory: ./backend/attachments-service 24 | run: sam build --use-container 25 | 26 | # Run Unit tests- Specify unit tests here 27 | ## - name: Run tests 28 | ## run: | 29 | ## pushd './backend/attachments-service/tests/' 30 | ## pip install awscli 31 | ## python -m pip install --upgrade pip 32 | ## pip install -r requirements.txt 33 | ## python -m nose2 test_getTodos.lambda_handler test_addTodo.lambda_handler test_completeTodo.lambda_handler 34 | 35 | # sam deploy 36 | - name: sam deploy 37 | working-directory: ./backend/attachments-service 38 | run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${{ secrets.FILES_STACK_NAME}} --s3-bucket ${{ secrets.SAM_S3_BUCKET }} --capabilities CAPABILITY_NAMED_IAM --region ${{ secrets.AWS_REGION }} 39 | -------------------------------------------------------------------------------- /.github/workflows/frontend-pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Frontend 2 | 3 | # Deploy when push made from frontend folder. 4 | on: 5 | push: 6 | branches: [ main ] 7 | paths: 8 | - 'frontend/**' 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | #Uplaod to S3 16 | - uses: actions/checkout@master 17 | - name: Sync S3 18 | uses: jakejarvis/s3-sync-action@master 19 | with: 20 | args: --follow-symlinks --delete 21 | env: 22 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} 23 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 24 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 25 | AWS_REGION: ${{ secrets.AWS_REGION }} 26 | SOURCE_DIR: 'frontend' 27 | 28 | #Invalidate CloudFront 29 | - name: Invalidate CF index.html 30 | uses: chetan/invalidate-cloudfront-action@master 31 | env: 32 | DISTRIBUTION: ${{ secrets.CFDISTRIBUTION }} 33 | PATHS: '/index.html /home.html /js/script.js' 34 | AWS_REGION: ${{ secrets.AWS_REGION }} 35 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 36 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/main-service-sam-pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: SAM Deploy todo-houessou-com main service 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'backend/main-service/**' 8 | jobs: 9 | build-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | - uses: aws-actions/setup-sam@v1 15 | - uses: aws-actions/configure-aws-credentials@v1 16 | with: 17 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 18 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 19 | aws-region: ${{ secrets.AWS_REGION }} 20 | 21 | # sam build 22 | - name: sam build 23 | working-directory: ./backend/main-service 24 | run: sam build --use-container 25 | 26 | # Run Unit tests- Specify unit tests here 27 | ## - name: Run tests 28 | ## run: | 29 | ## pushd './backend/main-service/tests/' 30 | ## pip install awscli 31 | ## python -m pip install --upgrade pip 32 | ## pip install -r requirements.txt 33 | ## python -m nose2 test_getTodos.lambda_handler test_addTodo.lambda_handler test_completeTodo.lambda_handler 34 | 35 | # sam deploy 36 | - name: sam deploy 37 | working-directory: ./backend/main-service 38 | run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${{ secrets.MAIN_STACK_NAME}} --s3-bucket ${{ secrets.SAM_S3_BUCKET }} --capabilities CAPABILITY_NAMED_IAM --region ${{ secrets.AWS_REGION }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.aws-sam/ 2 | **/.DS_Store 3 | **/temp 4 | **/.venv/ 5 | 6 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 7 | 8 | ### Linux ### 9 | *~ 10 | 11 | # temporary files which can be created if a process still has a handle open of a deleted file 12 | .fuse_hidden* 13 | 14 | # KDE directory preferences 15 | .directory 16 | 17 | # Linux trash folder which might appear on any partition or disk 18 | .Trash-* 19 | 20 | # .nfs files are created when an open file is removed but is still being accessed 21 | .nfs* 22 | 23 | ### OSX ### 24 | *.DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### PyCharm ### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 52 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 53 | 54 | # User-specific stuff: 55 | .idea/**/workspace.xml 56 | .idea/**/tasks.xml 57 | .idea/dictionaries 58 | 59 | # Sensitive or high-churn files: 60 | .idea/**/dataSources/ 61 | .idea/**/dataSources.ids 62 | .idea/**/dataSources.xml 63 | .idea/**/dataSources.local.xml 64 | .idea/**/sqlDataSources.xml 65 | .idea/**/dynamic.xml 66 | .idea/**/uiDesigner.xml 67 | 68 | # Gradle: 69 | .idea/**/gradle.xml 70 | .idea/**/libraries 71 | 72 | # CMake 73 | cmake-build-debug/ 74 | 75 | # Mongo Explorer plugin: 76 | .idea/**/mongoSettings.xml 77 | 78 | ## File-based project format: 79 | *.iws 80 | 81 | ## Plugin-specific files: 82 | 83 | # IntelliJ 84 | /out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Cursive Clojure plugin 93 | .idea/replstate.xml 94 | 95 | # Ruby plugin and RubyMine 96 | /.rakeTasks 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | fabric.properties 103 | 104 | ### PyCharm Patch ### 105 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 106 | 107 | # *.iml 108 | # modules.xml 109 | # .idea/misc.xml 110 | # *.ipr 111 | 112 | # Sonarlint plugin 113 | .idea/sonarlint 114 | 115 | ### Python ### 116 | # Byte-compiled / optimized / DLL files 117 | __pycache__/ 118 | *.py[cod] 119 | *$py.class 120 | 121 | # C extensions 122 | *.so 123 | 124 | # Distribution / packaging 125 | .Python 126 | build/ 127 | develop-eggs/ 128 | dist/ 129 | downloads/ 130 | eggs/ 131 | .eggs/ 132 | lib/ 133 | lib64/ 134 | parts/ 135 | sdist/ 136 | var/ 137 | wheels/ 138 | *.egg-info/ 139 | .installed.cfg 140 | *.egg 141 | 142 | # PyInstaller 143 | # Usually these files are written by a python script from a template 144 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 145 | *.manifest 146 | *.spec 147 | 148 | # Installer logs 149 | pip-log.txt 150 | pip-delete-this-directory.txt 151 | 152 | # Unit test / coverage reports 153 | htmlcov/ 154 | .tox/ 155 | .coverage 156 | .coverage.* 157 | .cache 158 | .pytest_cache/ 159 | nosetests.xml 160 | coverage.xml 161 | *.cover 162 | .hypothesis/ 163 | 164 | # Translations 165 | *.mo 166 | *.pot 167 | 168 | # Flask stuff: 169 | instance/ 170 | .webassets-cache 171 | 172 | # Scrapy stuff: 173 | .scrapy 174 | 175 | # Sphinx documentation 176 | docs/_build/ 177 | 178 | # PyBuilder 179 | target/ 180 | 181 | # Jupyter Notebook 182 | .ipynb_checkpoints 183 | 184 | # pyenv 185 | .python-version 186 | 187 | # celery beat schedule file 188 | celerybeat-schedule.* 189 | 190 | # SageMath parsed files 191 | *.sage.py 192 | 193 | # Environments 194 | .env 195 | .venv 196 | env/ 197 | venv/ 198 | ENV/ 199 | env.bak/ 200 | venv.bak/ 201 | 202 | # Spyder project settings 203 | .spyderproject 204 | .spyproject 205 | 206 | # Rope project settings 207 | .ropeproject 208 | 209 | # mkdocs documentation 210 | /site 211 | 212 | # mypy 213 | .mypy_cache/ 214 | 215 | ### VisualStudioCode ### 216 | .vscode/* 217 | !.vscode/settings.json 218 | !.vscode/tasks.json 219 | !.vscode/launch.json 220 | !.vscode/extensions.json 221 | .history 222 | 223 | ### Windows ### 224 | # Windows thumbnail cache files 225 | Thumbs.db 226 | ehthumbs.db 227 | ehthumbs_vista.db 228 | 229 | # Folder config file 230 | Desktop.ini 231 | 232 | # Recycle Bin used on file shares 233 | $RECYCLE.BIN/ 234 | 235 | # Windows Installer files 236 | *.cab 237 | *.msi 238 | *.msm 239 | *.msp 240 | 241 | # Windows shortcuts 242 | *.lnk 243 | 244 | # Build folder 245 | 246 | */build/* 247 | 248 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.customTags": [ 3 | "!And", 4 | "!And sequence", 5 | "!If", 6 | "!If sequence", 7 | "!Not", 8 | "!Not sequence", 9 | "!Equals", 10 | "!Equals sequence", 11 | "!Or", 12 | "!Or sequence", 13 | "!FindInMap", 14 | "!FindInMap sequence", 15 | "!Base64", 16 | "!Join", 17 | "!Join sequence", 18 | "!Cidr", 19 | "!Ref", 20 | "!Sub", 21 | "!Sub sequence", 22 | "!GetAtt", 23 | "!GetAZs", 24 | "!ImportValue", 25 | "!ImportValue sequence", 26 | "!Select", 27 | "!Select sequence", 28 | "!Split", 29 | "!Split sequence" 30 | ] 31 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cover.png](https://github.com/hpfpv/todo-app-aws/blob/main/blog-post/cover.png) 2 | 3 | # Deploying a sample serverless to-do app on AWS 4 | 5 | Hi guys! In this post, we'll be building a sample todo app on AWS with Python. We will build a web application which enables logged in visitors to manage their todo list. We will use the AWS Serverless Application Model SAM Framework to deploy the backend services - API, Lambda, DynamoDB and Cognito) and will host the frontend on S3 behind a CloudFront distribution. 6 | The frontend is pretty basic with no fancy visuals (I am no frontend dev :p). We will try to focus on how the resources are created and deployed on AWS. 7 | 8 | ## Overview 9 | 10 | In this post I will be going through the overall setup of the app and how I deployed it. Mostly this will be a theoretical post but I will be posting needed scripts wherever appropriate. All the code can be found in the **[GitHub repo](https://github.com/hpfpv/todo-app-aws)**. 11 | 12 | **[Application web UI](https://todo.houessou.com)** 13 | 14 | ### About the App 15 | 16 | Before I go into the architecture, let me describe what the app is about and what it does. The app is a todo list manager which helps a user manage and track his/her todo list along with their files or attachments. The user can also find specific todos through the search. 17 | 18 | ### Basic Functionality 19 | 20 | ![appflow.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1627647453541/A17idrTi1.png) 21 | The image above should describe the app basic functionalities. 22 | 23 | **User/Login Management** 24 | 25 | *Users are able to login to the app using provided credentials. There is a self register functionality and once a user is registered, the app provides a capability to the user to login using those credentials. It also provides a logout option for the user.* 26 | 27 | **Search Todo** 28 | 29 | *Users are able to perform a keyword search and the app shows a list of todos which contain that keyword in the name. The search only searches on todos which the logged in user has created. So it has the access boundary and doesn’t show Recipes across users.* 30 | 31 | **Add New Todo** 32 | 33 | *Users can add new Todos to be stored in the app. There are various details which can be provided for each Todo. Users can also add notes for each Todo.* 34 | 35 | **Support for files** 36 | 37 | *Users can upload a todo files for each Todo. The app provides a capability where user can select and upload a local file or download existing files while adding notes to a Todo. The file can be anything, from a text file to an image file. The app stores it in a S3 bucket and serves it back to the user via CloudFront.* 38 | 39 | ### Application Components 40 | Now that we have a basic functional understanding of the app, let's see how all of these functionalities translate to different technical components. Below image should provide a good overview of each layer of the app and the technical components involved in each layer. 41 | 42 | ![app-components.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1628077958693/0vhPB3Gjx.png) 43 | 44 | Let's go through each component: 45 | 46 | **Frontend** 47 | 48 | *The Front end for the app is built of simple HTML and Javascript. All operations and communications with backend are performed via various REST API endpoints.* 49 | 50 | **Backend** 51 | 52 | *Backend for the app is built Lambda Functions triggered by REST APIs. It provides various API endpoints to perform application functionalities such as adding or deleting todos, adding or deleting todo files etc. The REST APIs are built using API Gateway. The API endpoints perform all operations of connecting with the functions, authenticating, etc. CORS is enabled for the API so it only accepts requests from the frontend.* 53 | 54 | **Data Layer** 55 | 56 | *DynamoDB Table is used to store all todos and related data. The lambda functions will be performing all Database operations connecting to the Table and getting requests from the frontend. DynamoDB is a serverless service and it provides auto scaling along with high availability.* 57 | 58 | **Authentication** 59 | 60 | *The authentication is handled by AWS Cognito. We use a Cognito user pool to store users data. When a user logs in and a session is established with the app, the session token and related data is stored at the FrontEnd and sent over the API endpoints. API Gateway then validate the session token against Cognito and allow users to perform application operations.* 61 | 62 | **File Service** 63 | 64 | *There is a separate service to handle files management for the application. The File service is composed of Javascript function using AWS SDK (for upload files operations), Lambda functions + API Gateway for API calls for various file operations like retrieve file info, delete file etc, S3 and DynamoDB to store files and files information. The files are served back to the user through the app using a CDN (Content Delivery Network). The CDN makes serving the static files faster and users can access/download them faster and easier.* 65 | 66 | 67 | ## Application Architecture 68 | 69 | Now that we have some information about the various components and services involved in the app, let's move on to how to place and connect these various components to get the final working application. 70 | 71 | ![architecture.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1628029946699/pOk-oRHg4.png) 72 | 73 | ### Frontend 74 | 75 | The static *html*, *javascript* and *css* files generated for the website will be stored in an S3 bucket. The S3 bucket is configured to host a website and will provide an endpoint through which the app can be accessed. To have a better performance on the frontend, the S3 bucket is selected as an Origin to a CloudFront distribution. The CloudFront will act as a CDN for the app frontend and provide faster access through the app pages. 76 | 77 | ### Lambda Functions for backend services logic 78 | 79 | All the backend logic is deployed as AWS Lambda functions. Lambda functions are totally serverless and our task is to upload our code files to create the Lambda functions along with setting other parameters. . Below are the functions which are deployed as part of the backend service: 80 | 81 | **Todos Service** 82 | 83 | - getTodos : *retrieve all todos for a userID* 84 | - getTodo : *return detailed information about one todo based on the todoID attribute* 85 | - addTodo : *create a todo for a specific user based on the userID* 86 | - completeTodo : *update todo record and set completed attribute to TRUE based on todoID * 87 | - addTodoNotes : *update todo record and set the notes attribute to the specified value based on todoID* 88 | - deleteTodo : *delete a todo for a specific user based on the userID and todoID 89 | * 90 | 91 | **Files Service** 92 | - getTodoFiles : *retrieve all files which belong to a specified todo* 93 | - addTodoFiles : *add files to as attachment to a specified todo* 94 | - deleteTodoFiles: *delete selected file for specified todo* 95 | 96 | ### API Gateway to expose Lambda Functions 97 | 98 | To expose the Lambda functions and make them accessible by the Frontend, AWS API Gateway is deployed. API Gateway defines all the endpoints for the APIs and route the requests to proper Lambda function in the backend. These API gateway endpoints are called by the frontend. Each application service has its own API (keeping services as separate as possible for decoupling purpose) with deployed routes as follow: 99 | 100 | **Todos Service** 101 | - getTodos : /{**userID**}/todos 102 | - getTodo : /{**userID**}/todos/{**todoID**} 103 | - deleteTodo : /{**userID**}/todos/{**todoID**}/delete 104 | - addTodo : /{**userID**}/todos/add 105 | - completeTodo : /{**userID**}/todos/{**todoID**}/complete 106 | - addTodoNotes : /{**userID**}/todos/{**todoID**}/addnotes 107 | 108 | **Files Service** 109 | - getTodoFiles : /{**todoID**}/files 110 | - addTodoFiles : /{**todoID**}/files/upload 111 | - deleteTodoFiles : /{**todoID**}/files/{**fileID**}/delete 112 | 113 | The addTodoFiles API route triggers the addTodoFiles function which only record the file information like fine name and file path/key to a DynamoDB table. The same table is queried by the getTodoFiles function to display returned files information. 114 | The actual operation to upload the files to S3 is perform by a Javascript function in the Frontend code. I found it better to do it that way to prevent large amount of data going through the lambda functions and thus increasing response time and cost. 115 | 116 | ### Database 117 | 118 | DynamoDB tables are used to serve as database. We have two tables for respectively the Todos Service and the Files Service. 119 | The search functionality of the app is handled by simple DynamoDB query requests. We can deployed a DynamoDB Accelerator in front of the tables to increase performance if needed. Below is the tables configuration: 120 | 121 | **Todos Service** 122 | To keep things simple, each document in DynamoDB will represent one todo with attributes as follow: 123 | 124 | - todoID : *unique number identifying todo, will serve as primary key* 125 | - userID : *ID of the user who created the todo, will serve as sort key* 126 | - dateCreated : *date todo has been created, today's date* 127 | - dateDue : *date the todo is due, user provided* 128 | - title : *todo title, user provided* 129 | - description : *todo description, user provided* 130 | - notes : *additional notes for todo, can be added anytime after todo is created, blank by default* 131 | - *completed* : *true or false if todo is marked as completed* 132 | 133 | **Files Service** 134 | - fileID : *unique number identifying file, will serve as primary key* 135 | - todoID : *ID of belonging todo item, will serve as sort key* 136 | - fileName : *name of the uploaded file* 137 | - filePath : *URL of the uploaded file for downloads* 138 | 139 | ### File Storage 140 | 141 | To support the file management capability of the application, a file storage need to be deployed. I am using an S3 bucket as the storage for the files which are uploaded from the app. The file service API calls the AWS S3 API to store the files in the bucket. To serve the files back to the user, a CloudFront distribution is created with the S3 bucket as the origin. This will serve as the CDN to distribute the static files faster to the end users. 142 | 143 | 144 | ## IaC and Deployment Method 145 | 146 | The application backend services are defined as SAM templates. Each service has his own template and resources are configured to be as independent as possible. 147 | I am using automated deployments for the whole application environment - frontend and 2 backend services. Each service is deployed using a separate deployment pipeline to maintain optimal decoupling. 148 | The components below are used as part of the deployment pipeline: 149 | 150 | - One GitHub Repository for code commits 151 | - A separate branch for Prod changes (master branch as Dev) 152 | - Various paths, one per service - Frontend, Backend Todos Service and Backend Files Service 153 | - Any commit to a service path in a specified branch (Prod or Dev) automatically tests deploys changes to the service in the appropriate environment. 154 | - GitHub Actions backed by docker containers to build and deploy services 155 | 156 | **FrontEnd** 157 | 158 | ![frontend-pipeline.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1628077668965/DMv5htTiG.png) 159 | 160 | **Backend** 161 | 162 | ![backend-pipeline.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1628077651575/cMRfCM8MB.png) 163 | 164 | 165 | ## Takeaways 166 | 167 | Hopefully, I was able to describe in detail about the system architecture which I would use for a basic todo-list management app. This application is designed solely for training purposes and there is a lot of room for improvement. I will continue working on making the deployment more secure, HA and fault tolerant. 168 | This post should give you a good idea about how to design a basic full stack and fully serverless architecture for an app using the microservices patter. 169 | -------------------------------------------------------------------------------- /backend/attachments-service/events/addTodoFiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":"2.0", 3 | "routeKey":"POST /{todoID}/files/upload", 4 | "rawPath":"/dev/879300c0-73e0-45a5-994f-9f712b08a97f/files/upload", 5 | "rawQueryString":"", 6 | "headers":{ 7 | "accept":"*/*", 8 | "accept-encoding":"gzip, deflate, br", 9 | "accept-language":"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,fr;q=0.6", 10 | "authorization":"eyJraWQiOiJ6dDhhNW14cWpJZ056UUdqeGdmVU54ZjNcL2tJQ0E0bm1RUlwveUVyME8wY1k9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI3M2YwYjQyNi03N2UwLTQ4YTItYjhhMC01OTU0N2QxYmEyNDUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfZk0zQnpLbTF1IiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJjb2duaXRvOnVzZXJuYW1lIjoiNzNmMGI0MjYtNzdlMC00OGEyLWI4YTAtNTk1NDdkMWJhMjQ1Iiwib3JpZ2luX2p0aSI6IjZkMjZhYTE2LTMwZDctNGI2Ni1iZDBiLTljYjQ5MzYyN2RhMyIsImF1ZCI6IjRhamI2Y2xtbDl2ZnQwMGNvZjY4OW82YzBwIiwiZXZlbnRfaWQiOiIxMGJhZGIzNi0zMTg4LTQ4MGEtOWY5MS03NTM0MTIwNDQ3NzQiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTYyNzY4Mjk1MywicGhvbmVfbnVtYmVyIjoiKzIyOTk3NDk3OTE5IiwiZXhwIjoxNjI3Njg2NTUzLCJpYXQiOjE2Mjc2ODI5NTMsImp0aSI6IjFmNDcwYTAxLWNlNTYtNDAwYS05ZWE0LWU4NWI3YWY3ZjYwMCIsImVtYWlsIjoiaHBmQGhvdWVzc291LmNvbSJ9.Obz9CevW6Vu9mKFtZFmYnUxdUajYYUZYX1JgMSKp9_wPVPUr8TvR2V8ZL-JYcKT5oDm-Yqc43xWP953pf4ZvvNNy8eZVM4sNqo7Qf9qsgCh5m-ZbPaM3HikOBomKltAL2MxMGzzEjG5VYg5f4nSrWnC2ZgsiG-YvDvQXTskeRDkjbHFqOvWI_EcaRpEnYhGAmcDAMKfkf4zunT0mvxxj9hWls3WPkYcJLbV_jK3xWsu9nul0kYfi5SnTyb6LcCDLqwbmIdvJ5eQZyKErPt9T-NThZxzbG7Tv5X5OhhBUz7M1RLP6CprEf1iTdu6fiqM61dQDqmM3BcbpPtjZZjHWjw", 11 | "content-length":"7850", 12 | "content-type":"multipart/form-data; boundary=----WebKitFormBoundarystI49fZnJEA8h32Y", 13 | "host":"4oumdscha7.execute-api.us-east-1.amazonaws.com", 14 | "origin":"https://todo.houessou.com", 15 | "referer":"https://todo.houessou.com/", 16 | "sec-ch-ua":"\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"", 17 | "sec-ch-ua-mobile":"?0", 18 | "sec-fetch-dest":"empty", 19 | "sec-fetch-mode":"cors", 20 | "sec-fetch-site":"cross-site", 21 | "user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36", 22 | "x-amzn-trace-id":"Root=1-6104821e-54946b2b6275a26436ecdc22", 23 | "x-forwarded-for":"197.234.223.240", 24 | "x-forwarded-port":"443", 25 | "x-forwarded-proto":"https" 26 | }, 27 | "requestContext":{ 28 | "accountId":"601091111123", 29 | "apiId":"4oumdscha7", 30 | "authorizer":{ 31 | "jwt":{ 32 | "claims":{ 33 | "aud":"4ajb6clml9vft00cof689o6c0p", 34 | "auth_time":"1627682953", 35 | "cognito:username":"73f0b426-77e0-48a2-b8a0-59547d1ba245", 36 | "email":"hpf@houessou.com", 37 | "email_verified":"true", 38 | "event_id":"10badb36-3188-480a-9f91-753412044774", 39 | "exp":"1627686553", 40 | "iat":"1627682953", 41 | "iss":"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_fM3BzKm1u", 42 | "jti":"1f470a01-ce56-400a-9ea4-e85b7af7f600", 43 | "origin_jti":"6d26aa16-30d7-4b66-bd0b-9cb493627da3", 44 | "phone_number":"+22997497919", 45 | "phone_number_verified":"true", 46 | "sub":"73f0b426-77e0-48a2-b8a0-59547d1ba245", 47 | "token_use":"id" 48 | }, 49 | "scopes":"None" 50 | } 51 | }, 52 | "domainName":"4oumdscha7.execute-api.us-east-1.amazonaws.com", 53 | "domainPrefix":"4oumdscha7", 54 | "http":{ 55 | "method":"POST", 56 | "path":"/dev/879300c0-73e0-45a5-994f-9f712b08a97f/files/upload", 57 | "protocol":"HTTP/1.1", 58 | "sourceIp":"197.234.223.240", 59 | "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36" 60 | }, 61 | "requestId":"DTlE3gyhoAMEVPw=", 62 | "routeKey":"POST /{todoID}/files/upload", 63 | "stage":"dev", 64 | "time":"30/Jul/2021:22:50:06 +0000", 65 | "timeEpoch":1627685406993 66 | }, 67 | "pathParameters":{ 68 | "todoID":"879300c0-73e0-45a5-994f-9f712b08a97f" 69 | }, 70 | "body":"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EgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUCABAAAFEgAAUKDuJNsa3Xjb4HCNUwGACWhkpCnDDgxWGnegO8nGRrfe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", 71 | "isBase64Encoded":true 72 | } -------------------------------------------------------------------------------- /backend/attachments-service/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"message\": \"hello world\"}", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "requestTime": "09/Apr/2015:12:34:56 +0000", 42 | "requestTimeEpoch": 1428582896000, 43 | "identity": { 44 | "cognitoIdentityPoolId": null, 45 | "accountId": null, 46 | "cognitoIdentityId": null, 47 | "caller": null, 48 | "accessKey": null, 49 | "sourceIp": "127.0.0.1", 50 | "cognitoAuthenticationType": null, 51 | "cognitoAuthenticationProvider": null, 52 | "userArn": null, 53 | "userAgent": "Custom User Agent String", 54 | "user": null 55 | }, 56 | "path": "/prod/path/to/resource", 57 | "resourcePath": "/{proxy+}", 58 | "httpMethod": "POST", 59 | "apiId": "1234567890", 60 | "protocol": "HTTP/1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/attachments-service/functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/backend/attachments-service/functions/__init__.py -------------------------------------------------------------------------------- /backend/attachments-service/functions/addTodoFiles.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | import uuid 6 | from botocore.exceptions import ClientError 7 | 8 | dynamo = boto3.client('dynamodb', region_name='us-east-1') 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | bucket = os.environ['TODOFILES_BUCKET'] 13 | bucketCDN = os.environ['TODOFILES_BUCKET_CDN'] 14 | 15 | def lambda_handler(event, context): 16 | logger.info(event) 17 | eventBody = json.loads(event["body"]) 18 | todoID = event["pathParameters"]["todoID"] 19 | fileName = eventBody["fileName"] 20 | fileID = str(uuid.uuid4()) 21 | filePath = eventBody["filePath"] 22 | fileKey = str(filePath).replace(f'https://{bucket}/.s3.amazonaws.com/','') 23 | filePathCDN = 'https://' + bucketCDN + '/' + filePath 24 | fileForDynamo = {} 25 | fileForDynamo["fileID"] = { 26 | "S": fileID 27 | } 28 | fileForDynamo["todoID"] = { 29 | "S": todoID 30 | } 31 | fileForDynamo["fileName"] = { 32 | "S": fileName 33 | } 34 | fileForDynamo["filePath"] = { 35 | "S": filePathCDN 36 | } 37 | 38 | logger.info(fileForDynamo) 39 | try: 40 | responseDB = dynamo.put_item( 41 | TableName=os.environ['TODOFILES_TABLE'], 42 | Item=fileForDynamo 43 | ) 44 | 45 | logger.info(responseDB) 46 | except ClientError as err: 47 | logger.info(err) 48 | responseBody = {} 49 | responseBody["status"] = "success" 50 | return { 51 | 'statusCode': 200, 52 | 'headers': { 53 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 54 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 55 | 'Access-Control-Allow-Methods': 'GET, POST', 56 | 'Content-Type': 'application/json' 57 | }, 58 | 'body': json.dumps(responseBody) 59 | } -------------------------------------------------------------------------------- /backend/attachments-service/functions/deleteTodoFile.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | from collections import defaultdict 6 | from boto3.dynamodb.conditions import Key 7 | 8 | dynamo = boto3.client('dynamodb', region_name='us-east-1') 9 | s3 = boto3.client('s3') 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.INFO) 12 | bucket = os.environ['TODOFILES_BUCKET'] 13 | bucketCDN = os.environ['TODOFILES_BUCKET_CDN'] 14 | 15 | def deleteTodosFileS3(key): 16 | response = s3.delete_object( 17 | Bucket=bucket, 18 | Key=key, 19 | ) 20 | logging.info(f"{key} deleted from S3") 21 | return response 22 | 23 | def deleteTodosFileDynamo(fileID): 24 | response = dynamo.delete_item( 25 | TableName=os.environ['TODOFILES_TABLE'], 26 | Key={ 27 | 'fileID': { 28 | 'S': fileID 29 | } 30 | } 31 | ) 32 | logging.info(f"{fileID} deleted from DynamoDB") 33 | return response 34 | def lambda_handler(event, context): 35 | logger.info(event) 36 | eventBody = json.loads(event["body"]) 37 | fileID = event["pathParameters"]["fileID"] 38 | filePath = eventBody["filePath"] 39 | fileKey = str(filePath).replace(f'https://{bucketCDN}/', '').replace('%40','@') 40 | todoID = event["pathParameters"]["todoID"] 41 | 42 | print(f"deleting file {fileID}") 43 | deleteTodosFileS3(fileKey) 44 | deleteTodosFileDynamo(fileID) 45 | 46 | responseBody = {} 47 | responseBody["status"] = "success" 48 | return { 49 | 'statusCode': 200, 50 | 'headers': { 51 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 52 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 53 | 'Access-Control-Allow-Methods': 'GET, DELETE, POST', 54 | 'Content-Type': 'application/json' 55 | }, 56 | 'body': json.dumps(responseBody) 57 | } -------------------------------------------------------------------------------- /backend/attachments-service/functions/getTodoFiles.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | from collections import defaultdict 6 | from boto3.dynamodb.conditions import Key 7 | 8 | client = boto3.client('dynamodb', region_name='us-east-1') 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | def getFilesJson(items): 13 | # loop through the returned todos and add their attributes to a new dict 14 | # that matches the JSON response structure expected by the frontend. 15 | fileList = defaultdict(list) 16 | 17 | for item in items: 18 | file = {} 19 | file["fileID"] = item["fileID"]["S"] 20 | file["todoID"] = item["todoID"]["S"] 21 | file["fileName"] = item["fileName"]["S"] 22 | file["filePath"] = item["filePath"]["S"] 23 | fileList["files"].append(file) 24 | return fileList 25 | 26 | def getTodosFiles(todoID): 27 | # Use the DynamoDB API Query to retrieve todo files from the table that belong 28 | # to the specified todoID. 29 | filter = "todoID" 30 | response = client.query( 31 | TableName=os.environ['TODOFILES_TABLE'], 32 | IndexName=filter+'Index', 33 | KeyConditions={ 34 | filter: { 35 | 'AttributeValueList': [ 36 | { 37 | 'S': todoID 38 | } 39 | ], 40 | 'ComparisonOperator': "EQ" 41 | } 42 | } 43 | ) 44 | logging.info(response["Items"]) 45 | fileList = getFilesJson(response["Items"]) 46 | return json.dumps(fileList) 47 | 48 | def lambda_handler(event, context): 49 | logger.info(event) 50 | todoID = event["pathParameters"]["todoID"] 51 | print(f"Getting all files for todo {todoID}") 52 | items = getTodosFiles(todoID) 53 | logger.info(items) 54 | return { 55 | 'statusCode': 200, 56 | 'headers': { 57 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 58 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 59 | 'Access-Control-Allow-Methods': 'GET', 60 | 'Content-Type': 'application/json' 61 | }, 62 | 'body': items 63 | } 64 | 65 | -------------------------------------------------------------------------------- /backend/attachments-service/functions/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | nose2 -------------------------------------------------------------------------------- /backend/attachments-service/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | [default] 3 | [default.deploy] 4 | [default.deploy.parameters] 5 | stack_name = "todo-houessou-com-attachments-service" 6 | s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1m8qnzobarz4q" 7 | s3_prefix = "todo-houessou-com-attachments-service" 8 | region = "us-east-1" 9 | capabilities = "CAPABILITY_IAM" 10 | -------------------------------------------------------------------------------- /backend/attachments-service/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: "Stack for todo-houessou-com attachement service" 4 | 5 | Globals: 6 | Function: 7 | Runtime: python3.8 8 | 9 | Resources: 10 | FilesApi: 11 | Type: AWS::Serverless::HttpApi 12 | Properties: 13 | StageName: dev 14 | Auth: 15 | Authorizers: 16 | TodoAuthorizer: 17 | IdentitySource: "$request.header.Authorization" 18 | JwtConfiguration: 19 | issuer: 20 | !Join 21 | - '' 22 | - - 'https://cognito-idp.' 23 | - '${AWS::Region}' 24 | - '.amazonaws.com/' 25 | - !ImportValue todo-houessou-com-TodoUserPool 26 | audience: 27 | - !ImportValue todo-houessou-com-TodoUserPoolClient 28 | DefaultAuthorizer: TodoAuthorizer 29 | CorsConfiguration: 30 | AllowMethods: 31 | - GET 32 | - POST 33 | - DELETE 34 | - HEAD 35 | - PUT 36 | AllowOrigins: 37 | - https://todo.houessou.com 38 | AllowHeaders: 39 | - '*' 40 | 41 | getTodoFiles: 42 | Type: AWS::Serverless::Function 43 | Properties: 44 | Environment: 45 | Variables: 46 | TODOFILES_TABLE: !Ref TodoFilesTable 47 | CodeUri: ./functions 48 | Handler: getTodoFiles.lambda_handler 49 | Events: 50 | getFilesApi: 51 | Type: HttpApi 52 | Properties: 53 | ApiId: !Ref FilesApi 54 | Path: /{todoID}/files 55 | Method: GET 56 | Policies: 57 | - Version: "2012-10-17" 58 | Statement: 59 | - Effect: Allow 60 | Action: 61 | - 'dynamodb:*' 62 | - 's3:GetObject' 63 | - 's3:ListBucket' 64 | Resource: 65 | - !GetAtt 'TodoFilesTable.Arn' 66 | - !Join [ '', [ !GetAtt 'TodoFilesTable.Arn', '/index/*' ] ] 67 | - !GetAtt 'TodoFilesBucket.Arn' 68 | - !Join ['', [!GetAtt 'TodoFilesBucket.Arn', '/*']] 69 | 70 | addTodoFiles: 71 | Type: AWS::Serverless::Function 72 | Properties: 73 | Environment: 74 | Variables: 75 | TODOFILES_TABLE: !Ref TodoFilesTable 76 | TODOFILES_BUCKET: !Ref TodoFilesBucket 77 | TODOFILES_BUCKET_CDN: !GetAtt 'TodoFilesBucketCF.DomainName' 78 | CodeUri: ./functions 79 | Handler: addTodoFiles.lambda_handler 80 | Events: 81 | addTodoApi: 82 | Type: HttpApi 83 | Properties: 84 | ApiId: !Ref FilesApi 85 | Path: /{todoID}/files/upload 86 | Method: POST 87 | Policies: 88 | - Version: "2012-10-17" 89 | Statement: 90 | - Effect: Allow 91 | Action: 92 | - 'dynamodb:*' 93 | - 's3:PutObject' 94 | - 's3:ListBucket' 95 | Resource: 96 | - !GetAtt 'TodoFilesTable.Arn' 97 | - !GetAtt 'TodoFilesBucket.Arn' 98 | - !Join ['', [!GetAtt 'TodoFilesBucket.Arn', '/*']] 99 | 100 | deleteTodoFile: 101 | Type: AWS::Serverless::Function 102 | Properties: 103 | Environment: 104 | Variables: 105 | TODOFILES_TABLE: !Ref TodoFilesTable 106 | TODOFILES_BUCKET: !Ref TodoFilesBucket 107 | TODOFILES_BUCKET_CDN: !GetAtt 'TodoFilesBucketCF.DomainName' 108 | CodeUri: ./functions 109 | Handler: deleteTodoFile.lambda_handler 110 | Events: 111 | deleteTodoApi: 112 | Type: HttpApi 113 | Properties: 114 | ApiId: !Ref FilesApi 115 | Path: /{todoID}/files/{fileID}/delete 116 | Method: DELETE 117 | Policies: 118 | - Version: "2012-10-17" 119 | Statement: 120 | - Effect: Allow 121 | Action: 122 | - 'dynamodb:*' 123 | - 's3:PutObject' 124 | - 's3:GetObject' 125 | - 's3:DeleteObject' 126 | - 's3:ListBucket' 127 | Resource: 128 | - !GetAtt 'TodoFilesTable.Arn' 129 | - !GetAtt 'TodoFilesBucket.Arn' 130 | - !Join ['', [!GetAtt 'TodoFilesBucket.Arn', '/*']] 131 | 132 | # dynamoDB table to store todos files url 133 | TodoFilesTable: 134 | Type: AWS::DynamoDB::Table 135 | Properties: 136 | TableName: !Sub 'TodoFilesTable-${AWS::StackName}' 137 | BillingMode: PROVISIONED 138 | ProvisionedThroughput: 139 | ReadCapacityUnits: 1 140 | WriteCapacityUnits: 1 141 | AttributeDefinitions: 142 | - AttributeName: "fileID" 143 | AttributeType: "S" 144 | - AttributeName: "todoID" 145 | AttributeType: "S" 146 | KeySchema: 147 | - AttributeName: "fileID" 148 | KeyType: "HASH" 149 | GlobalSecondaryIndexes: 150 | - IndexName: "todoIDIndex" 151 | KeySchema: 152 | - AttributeName: "todoID" 153 | KeyType: "HASH" 154 | - AttributeName: "fileID" 155 | KeyType: "RANGE" 156 | Projection: 157 | ProjectionType: "ALL" 158 | ProvisionedThroughput: 159 | ReadCapacityUnits: 1 160 | WriteCapacityUnits: 1 161 | 162 | # S3 bucket to store todos files 163 | TodoFilesBucket: 164 | Type: AWS::S3::Bucket 165 | Properties: 166 | BucketName: !Sub 'hpf-todo-app-files' 167 | CorsConfiguration: 168 | CorsRules: 169 | - AllowedHeaders: 170 | - '*' 171 | AllowedMethods: 172 | - GET 173 | - POST 174 | - PUT 175 | - DELETE 176 | - HEAD 177 | AllowedOrigins: 178 | - 'https://todo.houessou.com' 179 | 180 | TodoFilesBucketOAI: 181 | Type: AWS::CloudFront::CloudFrontOriginAccessIdentity 182 | Properties: 183 | CloudFrontOriginAccessIdentityConfig: 184 | Comment: !Join ['', ['access-identity-', !Ref TodoFilesBucket, '.s3.amazonaws.com'] ] 185 | 186 | TodoFilesBucketPolicy: 187 | Type: AWS::S3::BucketPolicy 188 | Properties: 189 | Bucket: !Ref TodoFilesBucket 190 | PolicyDocument: 191 | Version: '2012-10-17' 192 | Statement: 193 | - Effect: Allow 194 | Action: 195 | - 's3:GetObject' 196 | Resource: !Join ['', [!GetAtt 'TodoFilesBucket.Arn', '/*'] ] 197 | Principal: 198 | AWS: !Sub 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${TodoFilesBucketOAI}' 199 | 200 | TodoFilesBucketCF: 201 | Type: AWS::CloudFront::Distribution 202 | Properties: 203 | DistributionConfig: 204 | Comment: !Join ['', ['CDN for ', !Ref TodoFilesBucket] ] 205 | Enabled: 'true' 206 | DefaultCacheBehavior: 207 | AllowedMethods: 208 | - HEAD 209 | - DELETE 210 | - POST 211 | - GET 212 | - OPTIONS 213 | - PUT 214 | - PATCH 215 | ForwardedValues: 216 | QueryString: 'false' 217 | TargetOriginId: !Join ['', [!Ref 'TodoFilesBucket', '.s3.us-east-1.amazonaws.com'] ] 218 | ViewerProtocolPolicy: redirect-to-https 219 | Origins: 220 | - S3OriginConfig: 221 | OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${TodoFilesBucketOAI}' 222 | DomainName: !Join ['', [!Ref 'TodoFilesBucket', '.s3.us-east-1.amazonaws.com'] ] 223 | Id: !Join ['', [!Ref 'TodoFilesBucket', '.s3.us-east-1.amazonaws.com'] ] 224 | ViewerCertificate: 225 | CloudFrontDefaultCertificate: 'true' 226 | 227 | TodoIdentityPool: 228 | Type: AWS::Cognito::IdentityPool 229 | Properties: 230 | IdentityPoolName: !Sub 'IdentityPool_todo_houessou_com' 231 | AllowUnauthenticatedIdentities: true 232 | 233 | TodoIdentityPoolRole: 234 | Type: AWS::IAM::Role 235 | Properties: 236 | RoleName: !Sub 'identity-pool-role-${AWS::StackName}' 237 | AssumeRolePolicyDocument: 238 | Version: "2012-10-17" 239 | Statement: 240 | - Effect: "Allow" 241 | Principal: 242 | Federated: 243 | - "cognito-identity.amazonaws.com" 244 | Action: "sts:AssumeRoleWithWebIdentity" 245 | Condition: { "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "unauthenticated" }, "StringEquals": {"cognito-identity.amazonaws.com:aud": !Ref TodoIdentityPool}} 246 | Path: "/" 247 | Policies: 248 | - PolicyName: !Sub 'identity-pool-role-policy-${AWS::StackName}' 249 | PolicyDocument: 250 | Version: "2012-10-17" 251 | Statement: 252 | - Effect: "Allow" 253 | Action: 254 | - "s3:ListBucket" 255 | - "s3:PutObject*" 256 | - "s3:GetObject*" 257 | Resource : 258 | - !GetAtt 'TodoFilesBucket.Arn' 259 | - !Join ['', [!GetAtt 'TodoFilesBucket.Arn', '/*']] 260 | 261 | TodoIdentityPoolRoleAttachment: 262 | Type: AWS::Cognito::IdentityPoolRoleAttachment 263 | Properties: 264 | IdentityPoolId: !Ref TodoIdentityPool 265 | Roles: {"unauthenticated": !GetAtt TodoIdentityPoolRole.Arn } 266 | 267 | 268 | Outputs: 269 | FilesApi: 270 | Value: !Join [ '', ['https://', !Ref FilesApi, '.execute-api.us-east-1.amazonaws.com/dev'] ] 271 | Export: 272 | Name: !Sub "${AWS::StackName}-FilesApiURL" 273 | TodoFilesTable: 274 | Value: !Ref TodoFilesTable 275 | Export: 276 | Name: !Sub "${AWS::StackName}-TodoFilesTable" 277 | TodoFilesTableArn: 278 | Value: !GetAtt 'TodoFilesTable.Arn' 279 | Export: 280 | Name: !Sub "${AWS::StackName}-TodoFilesTableArn" 281 | TodoFilesBucket: 282 | Value: !Ref 'TodoFilesBucket' 283 | Export: 284 | Name: !Sub "${AWS::StackName}-TodoFilesBucket" 285 | TodoFilesBucketCFDomainName: 286 | Value: !GetAtt 'TodoFilesBucketCF.DomainName' 287 | Export: 288 | Name: !Sub "${AWS::StackName}-TodoFilesBucketCFDomainName" 289 | TodoFilesBucketArn: 290 | Value: !GetAtt 'TodoFilesBucket.Arn' 291 | Export: 292 | Name: !Sub "${AWS::StackName}-TodoFilesBucketArn" 293 | TodoIdentityPoolId: 294 | Value: !Ref 'TodoIdentityPool' 295 | Export: 296 | Name: !Sub "${AWS::StackName}-TodoIdentityPoolId" 297 | StackName: 298 | Value: !Sub "${AWS::StackName}" -------------------------------------------------------------------------------- /backend/main-service/events/event_addTodoNotes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":"2.0", 3 | "routeKey":"POST /{userID}/todos/{todoID}/addnotes", 4 | "rawPath":"/dev/hpf@houessou.com/todos/396f518b-02da-4db9-a0a1-f1423fb2d60b/addnotes", 5 | "rawQueryString":"", 6 | "headers":{ 7 | "accept":"application/json, text/javascript, */*; q=0.01", 8 | "accept-encoding":"gzip, deflate, br", 9 | "accept-language":"en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,fr;q=0.6", 10 | "authorization":"eyJraWQiOiJ6dDhhNW14cWpJZ056UUdqeGdmVU54ZjNcL2tJQ0E0bm1RUlwveUVyME8wY1k9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI3M2YwYjQyNi03N2UwLTQ4YTItYjhhMC01OTU0N2QxYmEyNDUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfZk0zQnpLbTF1IiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjp0cnVlLCJjb2duaXRvOnVzZXJuYW1lIjoiNzNmMGI0MjYtNzdlMC00OGEyLWI4YTAtNTk1NDdkMWJhMjQ1Iiwib3JpZ2luX2p0aSI6ImViNmFmZGY4LWQ4MDgtNGNmYy05ZGZhLWM5NzVhZDg3ODhkYiIsImF1ZCI6IjRhamI2Y2xtbDl2ZnQwMGNvZjY4OW82YzBwIiwiZXZlbnRfaWQiOiJlMzgzOWI5Ny01ZjMwLTQzNjAtYWNhZC01NmZhMGFhMDg4MWUiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTYyNzI0MDE3MSwicGhvbmVfbnVtYmVyIjoiKzIyOTk3NDk3OTE5IiwiZXhwIjoxNjI3MjQzNzcxLCJpYXQiOjE2MjcyNDAxNzEsImp0aSI6IjM1YzJlN2RmLTg4YzQtNGY5Yy05ZmU5LTQ0NDVlMGM2ZGFjZSIsImVtYWlsIjoiaHBmQGhvdWVzc291LmNvbSJ9.dQ7VK9fcqCCXUqL4cfyXCWgkQPl500ZBm06s28PzgtIugO3eBC5IWvvl4pgei1oa0IBsdCtAeoqt-YRhIbcRXBAgBAaa2cTTheK-ZPhrSlwxTnGGRubTzAEjzIAiGzxNaHpAhrdYpa17QykZtGkRm1D9QlAtJDeTX_r0cEJA_N1x13ShJRVHJH3ag5FSRDvUhRUZZ_r1kns38MqBexczr04D1wPLe78lyf_aq6hRLsJY4CXZb9sHcBtxqQW4cKNtjF76VU19TT-uFTtn0m6ImLyiaQnTxdNBjnXJrTgdZi7lSg2uKhL5GeC-cyRN7Jf6jYkT9fqzOGA4FC_Fu7oE6Q", 11 | "content-length":"45", 12 | "content-type":"application/json", 13 | "host":"j3cv37qhud.execute-api.us-east-1.amazonaws.com", 14 | "origin":"https://todo.houessou.com", 15 | "referer":"https://todo.houessou.com/", 16 | "sec-ch-ua":"\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"", 17 | "sec-ch-ua-mobile":"?0", 18 | "sec-fetch-dest":"empty", 19 | "sec-fetch-mode":"cors", 20 | "sec-fetch-site":"cross-site", 21 | "user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36", 22 | "x-amzn-trace-id":"Root=1-60fdbc62-3619fb7c5ef25ff708bc530b", 23 | "x-forwarded-for":"41.79.219.174", 24 | "x-forwarded-port":"443", 25 | "x-forwarded-proto":"https" 26 | }, 27 | "requestContext":{ 28 | "accountId":"601091111123", 29 | "apiId":"j3cv37qhud", 30 | "authorizer":{ 31 | "jwt":{ 32 | "claims":{ 33 | "aud":"4ajb6clml9vft00cof689o6c0p", 34 | "auth_time":"1627240171", 35 | "cognito:username":"73f0b426-77e0-48a2-b8a0-59547d1ba245", 36 | "email":"hpf@houessou.com", 37 | "email_verified":"true", 38 | "event_id":"e3839b97-5f30-4360-acad-56fa0aa0881e", 39 | "exp":"1627243771", 40 | "iat":"1627240171", 41 | "iss":"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_fM3BzKm1u", 42 | "jti":"35c2e7df-88c4-4f9c-9fe9-4445e0c6dace", 43 | "origin_jti":"eb6afdf8-d808-4cfc-9dfa-c975ad8788db", 44 | "phone_number":"+22997497919", 45 | "phone_number_verified":"true", 46 | "sub":"73f0b426-77e0-48a2-b8a0-59547d1ba245", 47 | "token_use":"id" 48 | }, 49 | "scopes":"None" 50 | } 51 | }, 52 | "domainName":"j3cv37qhud.execute-api.us-east-1.amazonaws.com", 53 | "domainPrefix":"j3cv37qhud", 54 | "http":{ 55 | "method":"POST", 56 | "path":"/dev/hpf@houessou.com/todos/396f518b-02da-4db9-a0a1-f1423fb2d60b/addnotes", 57 | "protocol":"HTTP/1.1", 58 | "sourceIp":"41.79.219.174", 59 | "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36" 60 | }, 61 | "requestId":"DCpfdgwdoAMEMSg=", 62 | "routeKey":"POST /{userID}/todos/{todoID}/addnotes", 63 | "stage":"dev", 64 | "time":"25/Jul/2021:19:32:50 +0000", 65 | "timeEpoch":1627241570796 66 | }, 67 | "pathParameters":{ 68 | "todoID":"396f518b-02da-4db9-a0a1-f1423fb2d60b", 69 | "userID":"hpf@houessou.com" 70 | }, 71 | "body":"{\"notes\":\"-new chapter: dictionaries | done\"}", 72 | "isBase64Encoded":false 73 | } -------------------------------------------------------------------------------- /backend/main-service/functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/backend/main-service/functions/__init__.py -------------------------------------------------------------------------------- /backend/main-service/functions/addTodo.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | import uuid 6 | from datetime import datetime 7 | 8 | client = boto3.client('dynamodb', region_name='us-east-1') 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | dateTimeObj = datetime.now() 12 | 13 | def lambda_handler(event, context): 14 | logger.info(event) 15 | eventBody = json.loads(event["body"]) 16 | userID = event["pathParameters"]["userID"] 17 | todo = {} 18 | todo["todoID"] = { 19 | "S": str(uuid.uuid4()) 20 | } 21 | todo["userID"] = { 22 | "S": userID 23 | } 24 | todo["dateCreated"] = { 25 | "S": str(dateTimeObj) 26 | } 27 | todo["title"] = { 28 | "S": eventBody["title"] 29 | } 30 | todo["description"] = { 31 | "S": eventBody["description"] 32 | } 33 | todo["notes"] = { 34 | "S": "" 35 | } 36 | todo["dateDue"] = { 37 | "S": eventBody["dateDue"] 38 | } 39 | todo["completed"] = { 40 | "BOOL": False 41 | } 42 | 43 | response = client.put_item( 44 | TableName=os.environ['TODO_TABLE'], 45 | Item=todo 46 | ) 47 | logger.info(response) 48 | responseBody = {} 49 | responseBody["status"] = "success" 50 | return { 51 | 'statusCode': 200, 52 | 'headers': { 53 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 54 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 55 | 'Access-Control-Allow-Methods': 'GET, POST', 56 | 'Content-Type': 'application/json' 57 | }, 58 | 'body': json.dumps(responseBody) 59 | } -------------------------------------------------------------------------------- /backend/main-service/functions/addTodoNotes.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | import uuid 6 | from datetime import datetime 7 | 8 | client = boto3.client('dynamodb', region_name='us-east-1') 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | def addTodoNotes(todoID, notes): 13 | response = client.update_item( 14 | TableName=os.environ['TODO_TABLE'], 15 | Key={ 16 | 'todoID': { 17 | 'S': todoID 18 | } 19 | }, 20 | UpdateExpression="SET notes = :b", 21 | ExpressionAttributeValues={':b': {'S': notes}} 22 | ) 23 | response = {} 24 | response["Update"] = "Success"; 25 | 26 | return json.dumps(response) 27 | def lambda_handler(event, context): 28 | logger.info(event) 29 | eventBody = json.loads(event["body"]) 30 | 31 | todoID = event['pathParameters']['todoID'] 32 | notes = eventBody["notes"] 33 | 34 | logger.info(f'adding notes for : {todoID}') 35 | response = addTodoNotes(todoID, notes) 36 | 37 | return { 38 | 'statusCode': 200, 39 | 'headers': { 40 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 41 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 42 | 'Access-Control-Allow-Methods': 'GET, POST', 43 | 'Content-Type': 'application/json' 44 | }, 45 | 'body': response 46 | } -------------------------------------------------------------------------------- /backend/main-service/functions/completeTodo.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | 6 | 7 | client = boto3.client('dynamodb', region_name='us-east-1') 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | def completeTodo(todoID): 12 | response = client.update_item( 13 | TableName=os.environ['TODO_TABLE'], 14 | Key={ 15 | 'todoID': { 16 | 'S': todoID 17 | } 18 | }, 19 | UpdateExpression="SET completed = :b", 20 | ExpressionAttributeValues={':b': {'BOOL': True}} 21 | ) 22 | response = {} 23 | response["Update"] = "Success"; 24 | 25 | return json.dumps(response) 26 | 27 | def lambda_handler(event, context): 28 | logger.info(event) 29 | todoID = event['pathParameters']['todoID'] 30 | logger.info(f'Completed todo: {todoID}') 31 | response = completeTodo(todoID) 32 | return { 33 | 'statusCode': 200, 34 | 'headers': { 35 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 36 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 37 | 'Access-Control-Allow-Methods': 'GET, POST', 38 | 'Content-Type': 'application/json' 39 | }, 40 | 'body': response 41 | } -------------------------------------------------------------------------------- /backend/main-service/functions/deleteTodo.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | from collections import defaultdict 6 | 7 | dynamo = boto3.client('dynamodb', region_name='us-east-1') 8 | s3 = boto3.resource('s3') 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | bucket = s3.Bucket(os.environ['TODOFILES_BUCKET']) 13 | 14 | def getFilesJson(items): 15 | # loop through the returned todos and add their attributes to a new dict 16 | # that matches the JSON response structure expected by the frontend. 17 | fileList = defaultdict(list) 18 | 19 | for item in items: 20 | file = {} 21 | file["fileID"] = item["fileID"]["S"] 22 | file["todoID"] = item["todoID"]["S"] 23 | file["fileName"] = item["fileName"]["S"] 24 | file["filePath"] = item["filePath"]["S"] 25 | fileList["files"].append(file) 26 | return fileList 27 | 28 | def getTodosFiles(todoID): 29 | # Use the DynamoDB API Query to retrieve todo files from the table that belong 30 | # to the specified todoID. 31 | filter = "todoID" 32 | response = dynamo.query( 33 | TableName=os.environ['TODOFILES_TABLE'], 34 | IndexName=filter+'Index', 35 | KeyConditions={ 36 | filter: { 37 | 'AttributeValueList': [ 38 | { 39 | 'S': todoID 40 | } 41 | ], 42 | 'ComparisonOperator': "EQ" 43 | } 44 | } 45 | ) 46 | logging.info(response["Items"]) 47 | fileList = getFilesJson(response["Items"]) 48 | return json.dumps(fileList) 49 | 50 | 51 | def deleteTodo(todoID): 52 | response = dynamo.delete_item( 53 | TableName=os.environ['TODO_TABLE'], 54 | Key={ 55 | 'todoID': { 56 | 'S': todoID 57 | } 58 | } 59 | ) 60 | logging.info(f"{todoID} deleted") 61 | return response 62 | 63 | def deleteTodoFilesS3(userID, todoID): 64 | prefix = userID + "/" + todoID + "/" 65 | for key in bucket.objects.filter(Prefix=prefix): 66 | key.delete() 67 | logging.info(f"{key} deleted") 68 | return (f"{todoID} files deleted from s3") 69 | 70 | def deleteTodoFilesDynamo(todoID): 71 | data = json.loads(getTodosFiles(todoID)) 72 | if data : 73 | files = data["files"] 74 | for file in files: 75 | fileID = file["fileID"] 76 | dynamo.delete_item( 77 | TableName=os.environ['TODOFILES_TABLE'], 78 | Key={ 79 | 'fileID': { 80 | 'S': fileID 81 | } 82 | } 83 | ) 84 | logging.info(f"{fileID} deleted") 85 | return (f"{todoID} files deleted from dynamoDB") 86 | else: 87 | logging.info(f"{todoID}: no files to delete") 88 | return (f"{todoID}: no files to delete") 89 | 90 | 91 | def lambda_handler(event, context): 92 | logger.info(event) 93 | todoID = event["pathParameters"]["todoID"] 94 | userID = event["pathParameters"]["userID"] 95 | 96 | print(f"deleting todo {todoID}") 97 | deleteTodoFilesS3(userID, todoID) 98 | deleteTodoFilesDynamo(todoID) 99 | deleteTodo(todoID) 100 | 101 | responseBody = {} 102 | responseBody["status"] = "success" 103 | return { 104 | 'statusCode': 200, 105 | 'headers': { 106 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 107 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 108 | 'Access-Control-Allow-Methods': 'GET, DELETE, POST', 109 | 'Content-Type': 'application/json' 110 | }, 111 | 'body': json.dumps(responseBody) 112 | } 113 | -------------------------------------------------------------------------------- /backend/main-service/functions/getTodo.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | 6 | client = boto3.client('dynamodb', region_name='us-east-1') 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | 10 | def getTodoJson(item): 11 | todo = {} 12 | todo["todoID"] = item["todoID"]["S"] 13 | todo["userID"] = item["userID"]["S"] 14 | todo["dateCreated"] = item["dateCreated"]["S"] 15 | todo["title"] = item["title"]["S"] 16 | todo["description"] = item["description"]["S"] 17 | todo["notes"] = item["notes"]["S"] 18 | todo["dateDue"] = item["dateDue"]["S"] 19 | todo["completed"] = item["completed"]["BOOL"] 20 | 21 | return todo 22 | 23 | def getTodo(todoID): 24 | response = client.get_item( 25 | TableName=os.environ['TODO_TABLE'], 26 | Key={ 27 | 'todoID': { 28 | 'S': todoID 29 | } 30 | } 31 | ) 32 | response = getTodoJson(response["Item"]) 33 | return json.dumps(response) 34 | 35 | def lambda_handler(event, context): 36 | logger.info(event) 37 | todoID = event['pathParameters']['todoID'] 38 | print(f'Getting todo: {todoID}') 39 | items = getTodo(todoID) 40 | return { 41 | 'statusCode': 200, 42 | 'headers': { 43 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 44 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 45 | 'Access-Control-Allow-Methods': 'GET', 46 | 'Content-Type': 'application/json' 47 | }, 48 | 'body': items 49 | } -------------------------------------------------------------------------------- /backend/main-service/functions/getTodos.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import logging 5 | from collections import defaultdict 6 | from boto3.dynamodb.conditions import Key 7 | import re 8 | 9 | client = boto3.client('dynamodb', region_name='us-east-1') 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.INFO) 12 | 13 | def getTodosJson(items): 14 | # loop through the returned todos and add their attributes to a new dict 15 | # that matches the JSON response structure expected by the frontend. 16 | todoList = defaultdict(list) 17 | 18 | for item in items: 19 | todo = {} 20 | todo["todoID"] = item["todoID"]["S"] 21 | todo["userID"] = item["userID"]["S"] 22 | todo["dateCreated"] = item["dateCreated"]["S"] 23 | todo["title"] = item["title"]["S"] 24 | todo["description"] = item["description"]["S"] 25 | todo["notes"] = item["notes"]["S"] 26 | todo["dateDue"] = item["dateDue"]["S"] 27 | todo["completed"] = item["completed"]["BOOL"] 28 | todoList["todos"].append(todo) 29 | return todoList 30 | 31 | def getTodos(userID): 32 | # Use the DynamoDB API Query to retrieve todos from the table that belong 33 | # to the specified userID. 34 | filter = "userID" 35 | response = client.query( 36 | TableName=os.environ['TODO_TABLE'], 37 | IndexName=filter+'Index', 38 | KeyConditions={ 39 | filter: { 40 | 'AttributeValueList': [ 41 | { 42 | 'S': userID 43 | } 44 | ], 45 | 'ComparisonOperator': "EQ" 46 | } 47 | } 48 | ) 49 | logging.info(response["Items"]) 50 | todoList = getTodosJson(response["Items"]) 51 | items = json.dumps(todoList) 52 | data = json.loads(items) 53 | response = defaultdict(list) 54 | sortedData1 = sorted(data["todos"], key = lambda i: i["dateCreated"], reverse=True) 55 | sortedData2 = sorted(sortedData1, key = lambda i: i["dateDue"]) 56 | sortedData3 = sorted(sortedData2, key = lambda i: i["completed"]) 57 | response = defaultdict(list) 58 | 59 | for item in sortedData3: 60 | todo = {} 61 | todo["todoID"] = item["todoID"] 62 | todo["userID"] = item["userID"] 63 | todo["dateCreated"] = item["dateCreated"] 64 | todo["title"] = item ["title"] 65 | todo["description"] = item["description"] 66 | todo["notes"] = item["notes"] 67 | todo["dateDue"] = item["dateDue"] 68 | todo["completed"] = item["completed"] 69 | 70 | response["todos"].append(todo) 71 | logger.info(response) 72 | return response 73 | 74 | def getSearchedTodos(userID, filter): 75 | items = getTodos(userID) 76 | data = items 77 | response = defaultdict(list) 78 | 79 | for item in data["todos"]: 80 | todo = {} 81 | if re.search(filter, item["title"], re.IGNORECASE): 82 | todo["todoID"] = item["todoID"] 83 | todo["userID"] = item["userID"] 84 | todo["dateCreated"] = item["dateCreated"] 85 | todo["title"] = item ["title"] 86 | todo["description"] = item["description"] 87 | todo["notes"] = item["notes"] 88 | todo["dateDue"] = item["dateDue"] 89 | todo["completed"] = item["completed"] 90 | response["todos"].append(todo) 91 | 92 | logging.info(response) 93 | return response 94 | 95 | def lambda_handler(event, context): 96 | logger.info(event) 97 | userID = event["pathParameters"]["userID"] 98 | if (event["rawQueryString"] == ''): 99 | print(f"Getting all todos for user {userID}") 100 | response = getTodos(userID) 101 | logger.info(response) 102 | else: 103 | print(f"Getting filtered for user {userID}") 104 | filter = event["queryStringParameters"]["search"] 105 | response = getSearchedTodos(userID, filter) 106 | logger.info(response) 107 | return { 108 | 'statusCode': 200, 109 | 'headers': { 110 | 'Access-Control-Allow-Origin': 'https://todo.houessou.com', 111 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 112 | 'Access-Control-Allow-Methods': 'GET', 113 | 'Content-Type': 'application/json' 114 | }, 115 | 'body': json.dumps(response) 116 | } 117 | 118 | -------------------------------------------------------------------------------- /backend/main-service/functions/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | nose2 -------------------------------------------------------------------------------- /backend/main-service/functions/temp2.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from datetime import datetime 3 | import json 4 | import re 5 | 6 | 7 | 8 | items = { 9 | "todos":[ 10 | { 11 | "todoID":"879300c0-73e0-45a5-994f-9f712b08a97f", 12 | "userID":"hpf@houessou.com", 13 | "dateCreated":"2021-07-25 18:35:14.040809", 14 | "title":"Learn Ansible", 15 | "description":"For cloud computing", 16 | "notes":"- another another note\n- second note\n- third note\n- another note\n- note again\n\n\n\n\n", 17 | "dateDue":"2021-12-31", 18 | "completed":"false" 19 | }, 20 | { 21 | "todoID":"396f518b-02da-4db9-a0a1-f1423fb2d60b", 22 | "userID":"hpf@houessou.com", 23 | "dateCreated":"2021-07-25 17:36:28.756252", 24 | "title":"Learn Python", 25 | "description":"Learn Python for coding and cloud administrative tasks", 26 | "notes":"- chapter dictionaries completed\n- chapter objects completed\n- project todo list completed\n- added notes on phone\n- another note\n- note on phone again\n- another one\n\n\n\n\n", 27 | "dateDue":"2021-12-31", 28 | "completed":"false" 29 | }, 30 | { 31 | "todoID":"08e14ca1-91a8-49a3-9407-f0d17780de60", 32 | "userID":"hpf@houessou.com", 33 | "dateCreated":"2021-07-28 11:50:25.309564", 34 | "title":"New todo on phone", 35 | "description":"This todo has been added on the phone", 36 | "notes":"", 37 | "dateDue":"2022-01-08", 38 | "completed":"false" 39 | }, 40 | { 41 | "todoID":"1b6e0cae-3653-44b2-b8bd-44bf122e8d8e", 42 | "userID":"hpf@houessou.com", 43 | "dateCreated":"2021-07-28 17:28:40.827547", 44 | "title":"Another todo", 45 | "description":"New todo with date", 46 | "notes":"- new note\n", 47 | "dateDue":"2021-08-06", 48 | "completed":"true" 49 | }, 50 | { 51 | "todoID":"34fa3856-6811-4fca-bb2f-d372a21bf04e", 52 | "userID":"hpf@houessou.com", 53 | "dateCreated":"2021-07-24 15:59:40.156578", 54 | "title":"Todo title6", 55 | "description":"okokok", 56 | "notes":"- note for completed todo\n- another note\n", 57 | "dateDue":"2021-10-08", 58 | "completed":"true" 59 | } 60 | ] 61 | } 62 | 63 | def getSearchedTodos(filter): 64 | data = items 65 | response = defaultdict(list) 66 | 67 | for item in data["todos"]: 68 | todo = {} 69 | if re.search(filter, item["title"], re.IGNORECASE): 70 | todo["todoID"] = item["todoID"] 71 | todo["userID"] = item["userID"] 72 | todo["dateCreated"] = item["dateCreated"] 73 | todo["title"] = item ["title"] 74 | todo["description"] = item["description"] 75 | todo["notes"] = item["notes"] 76 | todo["dateDue"] = item["dateDue"] 77 | todo["completed"] = item["completed"] 78 | response["todos"].append(todo) 79 | 80 | print(response) 81 | 82 | getSearchedTodos("learn python") -------------------------------------------------------------------------------- /backend/main-service/samconfig.toml: -------------------------------------------------------------------------------- 1 | version = 0.1 2 | [default] 3 | [default.deploy] 4 | [default.deploy.parameters] 5 | stack_name = "todo-houessou-com" 6 | s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1m8qnzobarz4q" 7 | s3_prefix = "todo-houessou-com" 8 | region = "us-east-1" 9 | capabilities = "CAPABILITY_IAM" 10 | -------------------------------------------------------------------------------- /backend/main-service/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: "Stack for todo-houessou-com main service" 4 | 5 | Globals: 6 | Function: 7 | Runtime: python3.8 8 | 9 | Resources: 10 | MainHttpApi: 11 | Type: AWS::Serverless::HttpApi 12 | DependsOn: TodoUserPool 13 | Properties: 14 | StageName: dev 15 | Auth: 16 | Authorizers: 17 | TodoAuthorizer: 18 | IdentitySource: "$request.header.Authorization" 19 | JwtConfiguration: 20 | issuer: !Join [ '', [ 'https://cognito-idp.', '${AWS::Region}', '.amazonaws.com/', !Ref TodoUserPool ] ] 21 | audience: 22 | - !Ref TodoUserPoolClient 23 | DefaultAuthorizer: TodoAuthorizer 24 | CorsConfiguration: 25 | AllowMethods: 26 | - GET 27 | - POST 28 | - DELETE 29 | AllowOrigins: 30 | - https://todo.houessou.com 31 | AllowHeaders: 32 | - '*' 33 | 34 | 35 | getTodos: 36 | Type: AWS::Serverless::Function 37 | Properties: 38 | Environment: 39 | Variables: 40 | TODO_TABLE: !Ref TodoTable 41 | CodeUri: ./functions 42 | Handler: getTodos.lambda_handler 43 | Events: 44 | getTodosApi: 45 | Type: HttpApi 46 | Properties: 47 | ApiId: !Ref MainHttpApi 48 | Path: /{userID}/todos 49 | Method: GET 50 | Policies: 51 | - Version: "2012-10-17" 52 | Statement: 53 | - Effect: Allow 54 | Action: 'dynamodb:*' 55 | Resource: 56 | - !GetAtt 'TodoTable.Arn' 57 | - !Join [ '', [ !GetAtt 'TodoTable.Arn', '/index/*' ] ] 58 | 59 | getTodo: 60 | Type: AWS::Serverless::Function 61 | Properties: 62 | Environment: 63 | Variables: 64 | TODO_TABLE: !Ref TodoTable 65 | CodeUri: ./functions 66 | Handler: getTodo.lambda_handler 67 | Events: 68 | getTodoApi: 69 | Type: HttpApi 70 | Properties: 71 | ApiId: !Ref MainHttpApi 72 | Path: /{userID}/todos/{todoID} 73 | Method: GET 74 | Policies: 75 | - Version: "2012-10-17" 76 | Statement: 77 | - Effect: Allow 78 | Action: 'dynamodb:*' 79 | Resource: 80 | - !GetAtt 'TodoTable.Arn' 81 | 82 | deleteTodo: 83 | Type: AWS::Serverless::Function 84 | Properties: 85 | Environment: 86 | Variables: 87 | TODO_TABLE: !Ref TodoTable 88 | TODOFILES_BUCKET: !ImportValue "todo-houessou-com-attachments-service-TodoFilesBucket" 89 | TODOFILES_TABLE: !ImportValue "todo-houessou-com-attachments-service-TodoFilesTable" 90 | CodeUri: ./functions 91 | Handler: deleteTodo.lambda_handler 92 | Events: 93 | deleteTodoApi: 94 | Type: HttpApi 95 | Properties: 96 | ApiId: !Ref MainHttpApi 97 | Path: /{userID}/todos/{todoID}/delete 98 | Method: DELETE 99 | Policies: 100 | - Version: "2012-10-17" 101 | Statement: 102 | - Effect: Allow 103 | Action: 104 | - 'dynamodb:*' 105 | - 's3:PutObject' 106 | - 's3:GetObject' 107 | - 's3:DeleteObject' 108 | - 's3:ListBucket' 109 | Resource: 110 | - !GetAtt 'TodoTable.Arn' 111 | - !ImportValue 'todo-houessou-com-attachments-service-TodoFilesTableArn' 112 | - !Join [ '', [ !ImportValue 'todo-houessou-com-attachments-service-TodoFilesTableArn', '/index/*' ] ] 113 | - !ImportValue 'todo-houessou-com-attachments-service-TodoFilesBucketArn' 114 | - !Join ['', [!ImportValue 'todo-houessou-com-attachments-service-TodoFilesBucketArn', '/*']] 115 | 116 | addTodo: 117 | Type: AWS::Serverless::Function 118 | Properties: 119 | Environment: 120 | Variables: 121 | TODO_TABLE: !Ref TodoTable 122 | CodeUri: ./functions 123 | Handler: addTodo.lambda_handler 124 | Events: 125 | addTodoApi: 126 | Type: HttpApi 127 | Properties: 128 | ApiId: !Ref MainHttpApi 129 | Path: /{userID}/todos/add 130 | Method: POST 131 | Policies: 132 | - Version: "2012-10-17" 133 | Statement: 134 | - Effect: Allow 135 | Action: 'dynamodb:*' 136 | Resource: 137 | - !GetAtt 'TodoTable.Arn' 138 | 139 | completeTodo: 140 | Type: AWS::Serverless::Function 141 | Properties: 142 | Environment: 143 | Variables: 144 | TODO_TABLE: !Ref TodoTable 145 | CodeUri: ./functions 146 | Handler: completeTodo.lambda_handler 147 | Events: 148 | completeTodoApi: 149 | Type: HttpApi 150 | Properties: 151 | ApiId: !Ref MainHttpApi 152 | Path: /{userID}/todos/{todoID}/complete 153 | Method: POST 154 | Policies: 155 | - Version: "2012-10-17" 156 | Statement: 157 | - Effect: Allow 158 | Action: 'dynamodb:*' 159 | Resource: 160 | - !GetAtt 'TodoTable.Arn' 161 | 162 | addTodoNotes: 163 | Type: AWS::Serverless::Function 164 | Properties: 165 | Environment: 166 | Variables: 167 | TODO_TABLE: !Ref TodoTable 168 | CodeUri: ./functions 169 | Handler: addTodoNotes.lambda_handler 170 | Events: 171 | addTodoNotesApi: 172 | Type: HttpApi 173 | Properties: 174 | ApiId: !Ref MainHttpApi 175 | Path: /{userID}/todos/{todoID}/addnotes 176 | Method: POST 177 | Policies: 178 | - Version: "2012-10-17" 179 | Statement: 180 | - Effect: Allow 181 | Action: 'dynamodb:*' 182 | Resource: 183 | - !GetAtt 'TodoTable.Arn' 184 | 185 | 186 | TodoUserPool: 187 | Type: AWS::Cognito::UserPool 188 | Properties: 189 | UserPoolName: !Sub 'UserPool-${AWS::StackName}' 190 | UsernameAttributes: 191 | - email 192 | AutoVerifiedAttributes: 193 | - email 194 | 195 | TodoUserPoolClient: 196 | Type: AWS::Cognito::UserPoolClient 197 | Properties: 198 | ClientName: !Sub 'UserPoolClient-${AWS::StackName}' 199 | AllowedOAuthFlows: 200 | - implicit 201 | AllowedOAuthFlowsUserPoolClient: true 202 | AllowedOAuthScopes: 203 | - phone 204 | - email 205 | - openid 206 | - profile 207 | - aws.cognito.signin.user.admin 208 | UserPoolId: 209 | Ref: TodoUserPool 210 | CallbackURLs: 211 | - https://todo.houessou.com 212 | ExplicitAuthFlows: 213 | - ALLOW_USER_SRP_AUTH 214 | - ALLOW_REFRESH_TOKEN_AUTH 215 | GenerateSecret: false 216 | SupportedIdentityProviders: 217 | - COGNITO 218 | # Cognito user pool domain 219 | TodoUserPoolDomain: 220 | Type: AWS::Cognito::UserPoolDomain 221 | Properties: 222 | UserPoolId: !Ref TodoUserPool 223 | Domain: auth-todo-houessou-com 224 | 225 | # dynamoDB table to store todos 226 | TodoTable: 227 | Type: AWS::DynamoDB::Table 228 | Properties: 229 | TableName: !Sub 'TodoTable-${AWS::StackName}' 230 | BillingMode: PROVISIONED 231 | ProvisionedThroughput: 232 | ReadCapacityUnits: 1 233 | WriteCapacityUnits: 1 234 | AttributeDefinitions: 235 | - AttributeName: "todoID" 236 | AttributeType: "S" 237 | - AttributeName: "userID" 238 | AttributeType: "S" 239 | KeySchema: 240 | - AttributeName: "todoID" 241 | KeyType: "HASH" 242 | GlobalSecondaryIndexes: 243 | - IndexName: "userIDIndex" 244 | KeySchema: 245 | - AttributeName: "userID" 246 | KeyType: "HASH" 247 | - AttributeName: "todoID" 248 | KeyType: "RANGE" 249 | Projection: 250 | ProjectionType: "ALL" 251 | ProvisionedThroughput: 252 | ReadCapacityUnits: 1 253 | WriteCapacityUnits: 1 254 | 255 | 256 | Outputs: 257 | MainHttpApi: 258 | Value: !Join [ '', ['https://', !Ref MainHttpApi, '.execute-api.us-east-1.amazonaws.com/dev'] ] 259 | Export: 260 | Name: !Sub "${AWS::StackName}-MainHttpApiURL" 261 | TodoUserPool: 262 | Value: !Ref TodoUserPool 263 | Export: 264 | Name: !Sub "${AWS::StackName}-TodoUserPool" 265 | TodoUserPoolArn: 266 | Value: !GetAtt 'TodoUserPool.Arn' 267 | Export: 268 | Name: !Sub "${AWS::StackName}-TodoUserPoolArn" 269 | TodoUserPoolClient: 270 | Value: !Ref TodoUserPoolClient 271 | Export: 272 | Name: !Sub "${AWS::StackName}-TodoUserPoolClient" 273 | TodoTable: 274 | Value: !Ref TodoTable 275 | Export: 276 | Name: !Sub "${AWS::StackName}-TodoTable" 277 | TodoTableArn: 278 | Value: !GetAtt 'TodoTable.Arn' 279 | Export: 280 | Name: !Sub "${AWS::StackName}-TodoTableArn" 281 | StackName: 282 | Value: !Sub "${AWS::StackName}" -------------------------------------------------------------------------------- /backend/main-service/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/backend/main-service/tests/__init__.py -------------------------------------------------------------------------------- /backend/main-service/tests/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | nose2 -------------------------------------------------------------------------------- /backend/main-service/tests/test_getTodos.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/backend/main-service/tests/test_getTodos.py -------------------------------------------------------------------------------- /blog-post/app-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/app-components.png -------------------------------------------------------------------------------- /blog-post/appflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/appflow.png -------------------------------------------------------------------------------- /blog-post/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/architecture.png -------------------------------------------------------------------------------- /blog-post/backend-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/backend-pipeline.png -------------------------------------------------------------------------------- /blog-post/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/cover.png -------------------------------------------------------------------------------- /blog-post/frontend-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/frontend-pipeline.png -------------------------------------------------------------------------------- /blog-post/todo-app-backend-serverless-pipeline-aug.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/blog-post/todo-app-backend-serverless-pipeline-aug.drawio.png -------------------------------------------------------------------------------- /blog-post/todo-app-pipeline-frontend-aug: -------------------------------------------------------------------------------- 1 | UzV2zq1wL0osyPDNT0nNUTV2VTV2LsrPL4GwciucU3NyVI0MMlNUjV1UjYwMgFjVyA2HrCFY1qAgsSg1rwSLBiADYTaQg2Y1AA== -------------------------------------------------------------------------------- /frontend/confirm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Register - TodoHouessou 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 |
Enter the code sent to the email you provided. Then, login again on the home page.
24 |
25 |
26 |
27 | 28 | 29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/css/style.css: -------------------------------------------------------------------------------- 1 | #logo { 2 | width: 55%; 3 | } 4 | #attachments-img { 5 | width: 10%; 6 | } 7 | 8 | #deleteFile-img { 9 | width: 60%; 10 | } 11 | 12 | #newattachments-img { 13 | width: 10%; 14 | } 15 | 16 | @media (max-width: 800px) { 17 | #logo { 18 | max-width: 400px; 19 | } 20 | } 21 | 22 | .search-container button { 23 | float: right; 24 | padding: 6px 10px; 25 | margin-top: 8px; 26 | margin-right: 16px; 27 | background: #ddd; 28 | font-size: 17px; 29 | border: none; 30 | cursor: pointer; 31 | } 32 | 33 | .search-container button:hover { 34 | background: #ccc; 35 | } 36 | 37 | @media screen and (max-width: 600px) { 38 | .search-container { 39 | float: none; 40 | } 41 | .topnav a, .topnav input[type=text], .topnav .search-container button { 42 | float: none; 43 | display: block; 44 | text-align: left; 45 | width: 100%; 46 | margin: 0; 47 | padding: 14px; 48 | } 49 | .topnav input[type=text] { 50 | border: 1px solid #ccc; 51 | } 52 | } 53 | .bg-light { 54 | background-color: white; 55 | } 56 | 57 | #brand-title { 58 | color: black; 59 | } 60 | 61 | .signin-title { 62 | margin: 15px; 63 | } 64 | 65 | .input-field>label:first-of-type{ 66 | display:block; 67 | margin-bottom:.5rem; 68 | font-weight:400 69 | } 70 | .input-field>label:first-of-type.h5{ 71 | font-weight:600 72 | } 73 | 74 | .input-field{ 75 | position:relative; 76 | margin-bottom:1.5rem; 77 | margin-top:1.5rem; 78 | border-radius:0px; 79 | } 80 | 81 | .signin{ 82 | width: 100%; 83 | border-radius:0px; 84 | margin-top:1rem; 85 | padding: 0.6rem; 86 | } 87 | 88 | .btn-custom{ 89 | background-color:#009933; 90 | border-color: #00802b; 91 | color: white; 92 | } 93 | 94 | .btn-custom:active,.btn-custom:target, 95 | .btn.btn-custom:active,.btn.btn-custom:focus,.btn.btn-custom:hover 96 | { 97 | -webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15); 98 | box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15); 99 | 100 | background-color: white; 101 | color: #009933; 102 | border-color: #009933; 103 | } 104 | 105 | 106 | .btn.btn-dark:active,.btn.btn-dark:focus,.btn.btn-dark:hover { 107 | -webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15); 108 | box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15); 109 | background-color: white; 110 | color: black; 111 | } 112 | 113 | .upload-title { 114 | display: inline; 115 | margin-inline-start: 0px; 116 | margin-inline-end: 0px; 117 | margin-bottom: 20px; 118 | 119 | } 120 | 121 | .lead { 122 | font-size: 1.25rem; 123 | font-weight: 300; 124 | } 125 | 126 | p { 127 | display: inline; 128 | margin-block-start: 1em; 129 | margin-block-end: 1em; 130 | margin-inline-start: 0px; 131 | margin-inline-end: 0px; 132 | } 133 | 134 | #s3-img { 135 | display: inline; 136 | margin-left: 15px; 137 | width: 8%; 138 | } 139 | 140 | @media screen and (max-width: 1024px) { 141 | #s3-img { 142 | width: 10%; 143 | } 144 | } 145 | 146 | 147 | @media screen and (max-width: 992px) { 148 | .upload-form { 149 | margin-top: 15px; 150 | } 151 | } 152 | 153 | .chat-tab { 154 | position: fixed; 155 | bottom: 10px; 156 | right: 10px; 157 | width: 60px; /* Adjust if necessary to match your design */ 158 | height: 60px; /* Adjust if necessary to match your design */ 159 | border-radius: 50%; /* This gives the circular shape */ 160 | overflow: hidden; /* Ensures no overflow outside the circular shape */ 161 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* Optional: Adds a shadow for better visibility */ 162 | cursor: pointer; 163 | display: flex; 164 | justify-content: center; 165 | align-items: center; 166 | } 167 | 168 | #chatIcon { 169 | width: 100%; /* Makes the image fill the container */ 170 | height: auto; /* Maintains the aspect ratio of the image */ 171 | } 172 | 173 | .chat-container { 174 | position: fixed; 175 | bottom: 70px; /* Position above the chat-tab */ 176 | right: 10px; 177 | width: 300px; 178 | height: 400px; 179 | border: 1px solid #ddd; 180 | border-radius: 5px; 181 | background-color: #f9f9f9; 182 | display: none; /* Initially hide the chat */ 183 | flex-direction: column; 184 | } 185 | 186 | .messages { 187 | flex: 1; 188 | padding: 10px; 189 | overflow-y: auto; 190 | } 191 | 192 | .message { 193 | margin-bottom: 10px; 194 | padding: 10px; 195 | border-radius: 5px; 196 | background-color: #f1f1f1; 197 | } 198 | 199 | .message img { 200 | vertical-align: middle; 201 | margin-right: 5px; 202 | } 203 | 204 | .message.user { 205 | background-color: #007bff; /* Light gray background for user messages */ 206 | color: white; /* White text for readability */ 207 | border-radius: 10px; 208 | padding: 5px 10px; 209 | margin: 5px; 210 | max-width: 80%; 211 | align-self: flex-end; /* Align user messages to the right */ 212 | } 213 | 214 | .message.bot { 215 | background-color:#f1f1f1 ; /* Blue background for bot messages */ 216 | color: black; /* Black text for readability */ 217 | border-radius: 10px; 218 | padding: 5px 10px; 219 | margin: 5px; 220 | max-width: 80%; 221 | align-self: flex-start; /* Align bot messages to the left */ 222 | } 223 | 224 | .chat-input { 225 | display: flex; 226 | } 227 | 228 | input[type="text"] { 229 | flex: 1; 230 | padding: 10px; 231 | border: none; 232 | margin: 5px; 233 | } 234 | 235 | button { 236 | padding: 10px; 237 | border: none; 238 | margin: 5px; 239 | } -------------------------------------------------------------------------------- /frontend/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/favicon.png -------------------------------------------------------------------------------- /frontend/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Home - TodoHouessou 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 |

22 |
23 |
24 |

25 | 26 | 27 |
28 | Chat with us! 29 |
30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |   44 | 45 |
46 |
47 | 48 |   49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 |

61 | {{todo.title}} 62 |
63 | {{todo.description}} 64 |
65 | Due Date: {{todo.dateDue}} 66 |
67 | 68 |
69 |

70 |
71 |
72 |
73 |
74 | 75 | 76 | 139 | 140 | 141 | 142 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 |

190 |
191 |
192 |   This site is a sample todo-list app created for training purposes. More details here. 193 |

194 | 195 | 287 | -------------------------------------------------------------------------------- /frontend/img/bot-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/bot-icon.gif -------------------------------------------------------------------------------- /frontend/img/bot-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | robot 3 | robot,chatbot,android,turing test,cyborg,ai,artificial intelligence,machine learning 4 | by 5 | 5x35n5 6 | -------------------------------------------------------------------------------- /frontend/img/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/cross.png -------------------------------------------------------------------------------- /frontend/img/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/file.png -------------------------------------------------------------------------------- /frontend/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/logo.png -------------------------------------------------------------------------------- /frontend/img/logo_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/logo_old.png -------------------------------------------------------------------------------- /frontend/img/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/new.png -------------------------------------------------------------------------------- /frontend/img/paperclip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/paperclip.png -------------------------------------------------------------------------------- /frontend/img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/search.png -------------------------------------------------------------------------------- /frontend/img/to-do-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hpfpv/todo-app-aws/affd0c0c90b533d3774406faddde1c7cce8f754f/frontend/img/to-do-list.png -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sign In - TodoHouessou 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 | 34 | 38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 59 | -------------------------------------------------------------------------------- /frontend/js/ajaxCallExample.js: -------------------------------------------------------------------------------- 1 | function ajaxCall(triggered){ 2 | 3 | var url = "" 4 | 5 | $.get(url, function(data, status) { 6 | 7 | console.log(`Status: ${status}`) 8 | 9 | var tableContent = '' 10 | var count = 1 11 | 12 | $.each(data, function(idx, obj) { 13 | var firstname = obj.firstname; 14 | var lastname = obj.lastname; 15 | var email = obj.email; 16 | var updated = obj.updated.substring(0, 19); 17 | 18 | tableContent += ` 19 | ${count} 20 | ${firstname} 21 | ${lastname} 22 | ${email} 23 | ${updated} 24 | ` 25 | count += 1 26 | }) 27 | 28 | // console.log(tableContent) 29 | console.log("Updated User list") 30 | 31 | $("#tableContent").html(tableContent) 32 | if(triggered == true) { 33 | alert("User list has been updated successfully!") 34 | } 35 | }) 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /frontend/js/script.js: -------------------------------------------------------------------------------- 1 | var todoApiEndpoint = 'https://j3cv37qhud.execute-api.us-east-1.amazonaws.com/dev/'; 2 | var todoFilesApiEndpoint = 'https://4oumdscha7.execute-api.us-east-1.amazonaws.com/dev/'; 3 | // const websocket = new WebSocket('wss://bum4o4rx48.execute-api.us-east-1.amazonaws.com/production/'); 4 | var cognitoUserPoolId = 'us-east-1_fM3BzKm1u'; 5 | var cognitoUserPoolClientId = '4ajb6clml9vft00cof689o6c0p'; 6 | var cognitoIdentityPoolId = 'us-east-1:1d4efcf7-f995-4331-bd94-c3ed6111f246'; 7 | var bucketName = 'hpf-todo-app-files'; 8 | var awsRegion = 'us-east-1'; 9 | 10 | var gridScope; 11 | var descriptionScope; 12 | var filesScope; 13 | 14 | function loggedInDisplay() { 15 | $("#signInButton").addClass("d-none"); 16 | $("#signOutButton").removeClass("d-none"); 17 | } 18 | 19 | function loggedOutDisplay() { 20 | $("#signInButton").removeClass("d-none"); 21 | $("#signOutButton").addClass("d-none"); 22 | } 23 | 24 | function initializeStorage() { 25 | var identityPoolId = cognitoIdentityPoolId;// 26 | var userPoolId = cognitoUserPoolId; // 27 | var clientId = cognitoUserPoolClientId;// 28 | var loginPrefix = 'cognito-idp.' + awsRegion + '.amazonaws.com/' + userPoolId; 29 | 30 | localStorage.setItem('identityPoolId', identityPoolId); 31 | localStorage.setItem('userPoolId', userPoolId); 32 | localStorage.setItem('clientId', clientId); 33 | localStorage.setItem('loginPrefix', loginPrefix); 34 | } 35 | 36 | function updateModalText(descriptionTodo) { 37 | applyDescriptionScope(descriptionTodo); 38 | if (descriptionTodo.completed == true) { 39 | markCompleted(); 40 | } else { 41 | markNotCompleted(); 42 | } 43 | } 44 | 45 | function confirmDeleteTodo(todoID, title) { 46 | var response = confirm("You are about to delete ~" + title + "~"); 47 | if (response == true) { 48 | deleteTodo(todoID); 49 | } 50 | } 51 | 52 | function markCompleted() { 53 | $("#completedButton").addClass("d-none"); 54 | $("#alreadyCompletedButton").removeClass("d-none"); 55 | } 56 | 57 | function markFileDeleted(fileID) { 58 | $("#" + fileID).addClass("d-none"); 59 | } 60 | 61 | function markNotCompleted() { 62 | $("#completedButton").removeClass("d-none"); 63 | $("#alreadyCompletedButton").addClass("d-none"); 64 | } 65 | 66 | function showAddFilesForm(){ 67 | $("#addFilesForm").removeClass("d-none"); 68 | $("#fileinput").replaceWith($("#fileinput").val('').clone(true)); 69 | } 70 | 71 | function hideAddFilesForm(){ 72 | $("#addFilesForm").addClass("d-none"); 73 | $("#fileinput").replaceWith($("#fileinput").val('').clone(true)); 74 | } 75 | 76 | function addFileName () { 77 | var fileName = document.getElementById('fileinput').files[0].name; 78 | document.getElementById('fileName').innerHTML = fileName; 79 | } 80 | 81 | function applyGridScope(todosList) { 82 | gridScope.todos = todosList; 83 | gridScope.$apply(); 84 | } 85 | 86 | function applyFilesScope(filesList) { 87 | filesScope.files = filesList; 88 | filesScope.$apply(); 89 | } 90 | 91 | function applyDescriptionScope(todo) { 92 | descriptionScope.descriptionTodo = todo; 93 | descriptionScope.$apply(); 94 | } 95 | 96 | function register() { 97 | event.preventDefault(); 98 | 99 | var poolData = { 100 | UserPoolId : cognitoUserPoolId, 101 | ClientId : cognitoUserPoolClientId 102 | }; 103 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 104 | 105 | var attributeList = []; 106 | 107 | 108 | var email = document.getElementById('email').value; 109 | var pw = document.getElementById('pwd').value; 110 | var confirmPw = document.getElementById('confirmPwd').value; 111 | var dataEmail = { 112 | Name : 'email', 113 | Value : email 114 | }; 115 | 116 | var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail); 117 | attributeList.push(attributeEmail); 118 | if (pw === confirmPw) { 119 | userPool.signUp(email, pw, attributeList, null, function(err, result){ 120 | if (err) { 121 | alert(err.message); 122 | return; 123 | } 124 | cognitoUser = result.user; 125 | console.log(cognitoUser); 126 | localStorage.setItem('email', email); 127 | window.location.replace('confirm.html'); 128 | }); 129 | } else { 130 | alert('Passwords do not match.') 131 | }; 132 | } 133 | 134 | function confirmRegister() { 135 | event.preventDefault(); 136 | 137 | var confirmCode = document.getElementById('confirmCode').value; 138 | 139 | var poolData = { 140 | UserPoolId : cognitoUserPoolId, 141 | ClientId : cognitoUserPoolClientId 142 | }; 143 | 144 | var userName = localStorage.getItem('email'); 145 | 146 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 147 | var userData = { 148 | Username : userName, 149 | Pool : userPool 150 | }; 151 | 152 | var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData); 153 | cognitoUser.confirmRegistration(confirmCode, true, function(err, result) { 154 | if (err) { 155 | alert(err.message); 156 | return; 157 | } 158 | window.location.replace("index.html"); 159 | }); 160 | } 161 | 162 | function login(){ 163 | var userPoolId = localStorage.getItem('userPoolId'); 164 | var clientId = localStorage.getItem('clientId'); 165 | var identityPoolId = localStorage.getItem('identityPoolId'); 166 | var loginPrefix = localStorage.getItem('loginPrefix'); 167 | 168 | AWSCognito.config.region = awsRegion; 169 | AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({ 170 | IdentityPoolId: identityPoolId // your identity pool id here 171 | }); 172 | AWSCognito.config.update({accessKeyId: 'anything', secretAccessKey: 'anything'}) 173 | 174 | AWS.config.region = awsRegion; // Region 175 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 176 | IdentityPoolId: identityPoolId 177 | }); 178 | 179 | var poolData = { 180 | UserPoolId : userPoolId, // Your user pool id here 181 | ClientId : clientId // Your client id here 182 | }; 183 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 184 | 185 | var username = $('#username').val(); 186 | var password = $('#password').val(); 187 | 188 | var authenticationData = { 189 | Username: username, 190 | Password: password 191 | }; 192 | 193 | var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData); 194 | 195 | var userData = { 196 | Username : username, 197 | Pool : userPool 198 | }; 199 | var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData); 200 | console.log(cognitoUser); 201 | 202 | cognitoUser.authenticateUser(authenticationDetails, { 203 | onSuccess: function (result) { 204 | var accessToken = result.getAccessToken().getJwtToken(); 205 | console.log('Authentication successful', accessToken); 206 | var sessionTokens = 207 | { 208 | IdToken: result.getIdToken(), 209 | AccessToken: result.getAccessToken(), 210 | RefreshToken: result.getRefreshToken() 211 | }; 212 | localStorage.setItem('sessionTokens', JSON.stringify(sessionTokens)) 213 | localStorage.setItem('userID', username); 214 | //localStorage.setItem('password', password); 215 | window.location = './home.html'; 216 | }, 217 | onFailure: function(err) { 218 | console.log('failed to authenticate'); 219 | console.log(JSON.stringify(err)); 220 | alert('Failed to Log in.\nPlease check your credentials.'); 221 | }, 222 | }); 223 | } 224 | 225 | function checkLogin(redirectOnRec, redirectOnUnrec){ 226 | var userPoolId = localStorage.getItem('userPoolId'); 227 | var clientId = localStorage.getItem('clientId'); 228 | var identityPoolId = localStorage.getItem('identityPoolId'); 229 | var loginPrefix = localStorage.getItem('loginPrefix'); 230 | 231 | if (userPoolId != null & clientId != null){ 232 | var poolData = { 233 | UserPoolId : userPoolId, // Your user pool id here 234 | ClientId : clientId // Your client id here 235 | }; 236 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 237 | var cognitoUser = userPool.getCurrentUser(); 238 | 239 | if (cognitoUser != null) { 240 | console.log("user exist"); 241 | if (redirectOnRec) { 242 | window.location = './home.html'; 243 | loggedInDisplay(); 244 | } else { 245 | $("#body").css({'visibility':'visible'}); 246 | } 247 | } else { 248 | if (redirectOnUnrec) { 249 | window.location = './index.html'; 250 | } 251 | } 252 | } else{ 253 | window.location = './index.html'; 254 | } 255 | } 256 | 257 | function refreshAWSCredentials() { 258 | var userPoolId = localStorage.getItem('userPoolId'); 259 | var clientId = localStorage.getItem('clientId'); 260 | var identityPoolId = localStorage.getItem('identityPoolId'); 261 | var loginPrefix = localStorage.getItem('loginPrefix'); 262 | 263 | var poolData = { 264 | UserPoolId : userPoolId, // Your user pool id here 265 | ClientId : clientId // Your client id here 266 | }; 267 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 268 | var cognitoUser = userPool.getCurrentUser(); 269 | 270 | if (cognitoUser != null) { 271 | cognitoUser.getSession(function(err, result) { 272 | if (result) { 273 | console.log('user exist'); 274 | cognitoUser.refreshSession(result.getRefreshToken(), function(err, result) { 275 | if (err) {//throw err; 276 | console.log('Refresh AWS credentials failed '); 277 | alert("You need to log back in"); 278 | window.location = './index.html'; 279 | } 280 | else{ 281 | console.log('Logged in user'); 282 | localStorage.setItem('awsConfig', JSON.stringify(AWS.config)); 283 | var sessionTokens = 284 | { 285 | IdToken: result.getIdToken(), 286 | AccessToken: result.getAccessToken(), 287 | RefreshToken: result.getRefreshToken() 288 | }; 289 | localStorage.setItem("sessionTokens", JSON.stringify(sessionTokens)); 290 | 291 | 292 | } 293 | }); 294 | } 295 | 296 | }); 297 | } 298 | } 299 | 300 | function logOut() { 301 | localStorage.clear(); 302 | document.location.reload(); 303 | window.location = './index.html'; 304 | } 305 | 306 | function getTodos(callback) { 307 | try{ 308 | var userID = localStorage.getItem('userID'); 309 | var todoApi = todoApiEndpoint + userID +'/todos'; 310 | 311 | var sessionTokensString = localStorage.getItem('sessionTokens'); 312 | var sessionTokens = JSON.parse(sessionTokensString); 313 | var IdToken = sessionTokens.IdToken; 314 | var idJwt = IdToken.jwtToken; 315 | 316 | $.ajax({ 317 | url : todoApi, 318 | type : 'GET', 319 | headers : {'Authorization' : idJwt }, 320 | success : function(response) { 321 | console.log("successfully loaded todos for " + userID); 322 | callback(response.todos); 323 | }, 324 | error : function(response) { 325 | console.log("could not retrieve todos list."); 326 | if (response.status == "401") { 327 | refreshAWSCredentials(); 328 | } 329 | } 330 | }); 331 | }catch(err) { 332 | alert("You need to be signed in. Redirecting you to the sign in page!"); 333 | loggedOutDisplay(); 334 | console.log(err.message); 335 | } 336 | } 337 | 338 | function getSearchedTodos(filter, callback) { 339 | try{ 340 | var userID = localStorage.getItem('userID'); 341 | var todoApi = todoApiEndpoint + userID +'/todos?search=' + filter; 342 | 343 | var sessionTokensString = localStorage.getItem('sessionTokens'); 344 | var sessionTokens = JSON.parse(sessionTokensString); 345 | var IdToken = sessionTokens.IdToken; 346 | var idJwt = IdToken.jwtToken; 347 | 348 | $.ajax({ 349 | url : todoApi, 350 | type : 'GET', 351 | headers : {'Authorization' : idJwt }, 352 | success : function(response) { 353 | console.log("successfully loaded todos for " + userID); 354 | callback(response.todos); 355 | }, 356 | error : function(response) { 357 | console.log("could not retrieve todos list."); 358 | if (response.status == "401") { 359 | refreshAWSCredentials(); 360 | } 361 | } 362 | }); 363 | }catch(err) { 364 | alert("You need to be signed in. Redirecting you to the sign in page!"); 365 | loggedOutDisplay(); 366 | console.log(err.message); 367 | } 368 | } 369 | 370 | function getTodo(todoID, callback) { 371 | var userID = localStorage.getItem('userID'); 372 | var todoApi = todoApiEndpoint + userID +'/todos/' + todoID; 373 | 374 | var sessionTokensString = localStorage.getItem('sessionTokens'); 375 | var sessionTokens = JSON.parse(sessionTokensString); 376 | var IdToken = sessionTokens.IdToken; 377 | var idJwt = IdToken.jwtToken; 378 | 379 | $.ajax({ 380 | url : todoApi, 381 | type : 'GET', 382 | headers : {'Authorization' : idJwt }, 383 | success : function(response) { 384 | console.log('todoID: ' + todoID); 385 | callback(response); 386 | getTodoFiles(todoID, applyFilesScope); 387 | }, 388 | error : function(response) { 389 | console.log("could not retrieve todo."); 390 | if (response.status == "401") { 391 | refreshAWSCredentials(); 392 | } 393 | } 394 | }); 395 | } 396 | 397 | function addTodo(dateDue, title, description){ 398 | var userID = localStorage.getItem('userID'); 399 | var todoApi = todoApiEndpoint + userID + "/todos/add"; 400 | 401 | var sessionTokensString = localStorage.getItem('sessionTokens'); 402 | var sessionTokens = JSON.parse(sessionTokensString); 403 | var IdToken = sessionTokens.IdToken; 404 | var idJwt = IdToken.jwtToken; 405 | 406 | todo = { 407 | title: title, 408 | description: description, 409 | dateDue: dateDue, 410 | } 411 | 412 | $.ajax({ 413 | url : todoApi, 414 | type : 'POST', 415 | headers : {'Content-Type': 'application/json', 'Authorization' : idJwt}, 416 | dataType: 'json', 417 | data : JSON.stringify(todo), 418 | success : function(response) { 419 | console.log("todo added!") 420 | window.location.reload(); 421 | }, 422 | error : function(response) { 423 | console.log("could not add todo"); 424 | alert("Could not add todo (x_x)"); 425 | console.log(response); 426 | 427 | } 428 | }); 429 | } 430 | 431 | function completeTodo(todoID, callback) { 432 | try { 433 | var userID = localStorage.getItem('userID'); 434 | var todoApi = todoApiEndpoint + userID + "/todos/" + todoID + "/complete"; 435 | 436 | var sessionTokensString = localStorage.getItem('sessionTokens'); 437 | var sessionTokens = JSON.parse(sessionTokensString); 438 | var IdToken = sessionTokens.IdToken; 439 | var idJwt = IdToken.jwtToken; 440 | 441 | $.ajax({ 442 | url : todoApi, 443 | async : false, 444 | type : 'POST', 445 | headers : {'Authorization' : idJwt }, 446 | success : function(response) { 447 | console.log("marked as completed: " + todoID) 448 | callback(); 449 | }, 450 | error : function(response) { 451 | console.log("could not completed todo"); 452 | if (response.status == "401") { 453 | refreshAWSCredentials(); 454 | } 455 | } 456 | }); 457 | } catch(err) { 458 | alert("You must be logged in"); 459 | console.log(err.message); 460 | } 461 | } 462 | 463 | function deleteTodo(todoID) { 464 | var userID = localStorage.getItem('userID'); 465 | var todoApi = todoApiEndpoint + userID + '/todos/' + todoID + '/delete'; 466 | 467 | var sessionTokensString = localStorage.getItem('sessionTokens'); 468 | var sessionTokens = JSON.parse(sessionTokensString); 469 | var IdToken = sessionTokens.IdToken; 470 | var idJwt = IdToken.jwtToken; 471 | 472 | $.ajax({ 473 | url : todoApi, 474 | type : 'DELETE', 475 | headers : {'Authorization' : idJwt }, 476 | success : function(response) { 477 | console.log('deleted todo: ' + todoID); 478 | location.reload(); 479 | }, 480 | error : function(response) { 481 | console.log("could not delete todo."); 482 | if (response.status == "401") { 483 | refreshAWSCredentials(); 484 | } 485 | } 486 | }); 487 | } 488 | 489 | function addTodoNotes(todoID, notes) { 490 | try { 491 | var userID = localStorage.getItem('userID'); 492 | var todoApi = todoApiEndpoint + userID + "/todos/" + todoID + "/addnotes"; 493 | 494 | var sessionTokensString = localStorage.getItem('sessionTokens'); 495 | var sessionTokens = JSON.parse(sessionTokensString); 496 | var IdToken = sessionTokens.IdToken; 497 | var idJwt = IdToken.jwtToken; 498 | 499 | inputNotes = { 500 | notes: notes, 501 | } 502 | $.ajax({ 503 | url : todoApi, 504 | type : 'POST', 505 | headers : {'Content-Type': 'application/json','Authorization' : idJwt }, 506 | dataType: 'json', 507 | data: JSON.stringify(inputNotes), 508 | success : function(response) { 509 | console.log("added notes for: " + todoID) 510 | }, 511 | error : function(response) { 512 | console.log("could not add notes"); 513 | if (response.status == "401") { 514 | refreshAWSCredentials(); 515 | } 516 | } 517 | }); 518 | } catch(err) { 519 | alert("You must be logged in to save notes"); 520 | console.log(err.message); 521 | } 522 | } 523 | 524 | function uploadTodoFileS3(todoID, bucket, filesToUp, callback){ 525 | var userID = localStorage.getItem('userID'); 526 | var todoFilesApi = todoFilesApiEndpoint + todoID + "/files/upload"; 527 | var sessionTokensString = localStorage.getItem('sessionTokens'); 528 | var sessionTokens = JSON.parse(sessionTokensString); 529 | var IdToken = sessionTokens.IdToken; 530 | var idJwt = IdToken.jwtToken; 531 | 532 | if (!filesToUp.length) { 533 | alert("You need to choose a file to upload."); 534 | } 535 | else{ 536 | //var fileObj = new FormData(); 537 | var file = filesToUp[0]; 538 | var fileName = file.name; 539 | //var filePath = userID + '/' + todoID + '/' + fileName; 540 | //var fileUrl = 'https://' + bucketName + '.s3.amazonaws.com/' + filePath; 541 | var fileKey = userID + '/' + todoID + '/' + fileName; 542 | var sizeInKB = file.size/1024; 543 | console.log('uploading a file of ' + sizeInKB) 544 | if (sizeInKB > 2048) { 545 | alert("File size exceeds the limit of 2MB."); 546 | } 547 | else{ 548 | var params = { 549 | Key: fileKey, 550 | Body: file, 551 | ACL: 'public-read' 552 | }; 553 | bucket.upload(params, function(err, data) { 554 | if (err) { 555 | console.log(err, err.stack); 556 | alert("Failed to upload file " + fileName); 557 | } else { 558 | console.log(fileName + ' successfully uploaded for ' + todoID); 559 | var fileObj = { 560 | 'fileName': fileName, 561 | 'filePath': fileKey 562 | } 563 | $.ajax({ 564 | url : todoFilesApi, 565 | type : 'POST', 566 | headers : {'Content-Type': 'application/json', 'Authorization' : idJwt }, 567 | contentType: 'json', 568 | data: JSON.stringify(fileObj), 569 | success : function(response) { 570 | console.log("dynamodb table updated with filePath " + fileName); 571 | callback(todoID, applyFilesScope); 572 | 573 | }, 574 | error : function(response) { 575 | console.log("could not update dynamodb table: " + fileName); 576 | if (response.status == "401") { 577 | refreshAWSCredentials(); 578 | } 579 | } 580 | }); 581 | hideAddFilesForm(); 582 | } 583 | }) 584 | } 585 | } 586 | } 587 | 588 | function addTodoFiles(todoID, files, callback) { 589 | var userPoolId = localStorage.getItem('userPoolId'); 590 | var clientId = localStorage.getItem('clientId'); 591 | var identityPoolId = localStorage.getItem('identityPoolId'); 592 | var loginPrefix = localStorage.getItem('loginPrefix'); 593 | 594 | try{ 595 | AWS.config.update({ 596 | region: awsRegion, 597 | credentials: new AWS.CognitoIdentityCredentials({ 598 | IdentityPoolId: identityPoolId 599 | }) 600 | }); 601 | var s3 = new AWS.S3({ 602 | apiVersion: '2006-03-01', 603 | params: {Bucket: bucketName} 604 | }); 605 | uploadTodoFileS3(todoID, s3, files, callback); 606 | 607 | } 608 | catch(err) { 609 | //alert("You must be logged in to add todo attachment"); 610 | console.log(err.message); 611 | } 612 | } 613 | 614 | function getTodoFiles(todoID, callback) { 615 | try{ 616 | var todoFilesApi = todoFilesApiEndpoint + todoID + '/files'; 617 | var sessionTokensString = localStorage.getItem('sessionTokens'); 618 | var sessionTokens = JSON.parse(sessionTokensString); 619 | var IdToken = sessionTokens.IdToken; 620 | var idJwt = IdToken.jwtToken; 621 | 622 | $.ajax({ 623 | url : todoFilesApi, 624 | type : 'GET', 625 | headers : {'Authorization' : idJwt }, 626 | success : function(response) { 627 | console.log("successfully loaded files for " + todoID); 628 | callback(response.files); 629 | }, 630 | error : function(response) { 631 | console.log("could not retrieve files list"); 632 | if (response.status == "401") { 633 | refreshAWSCredentials(); 634 | } 635 | } 636 | }); 637 | }catch(err) { 638 | console.log(err.message); 639 | } 640 | } 641 | 642 | function deleteTodoFile(todoID, fileID, filePath, callback) { 643 | try{ 644 | var todoFilesApi = todoFilesApiEndpoint + todoID + '/files/' + fileID + '/delete' ; 645 | var sessionTokensString = localStorage.getItem('sessionTokens'); 646 | var sessionTokens = JSON.parse(sessionTokensString); 647 | var IdToken = sessionTokens.IdToken; 648 | var idJwt = IdToken.jwtToken; 649 | 650 | body = { 651 | 'filePath': filePath 652 | }; 653 | 654 | $.ajax({ 655 | url : todoFilesApi, 656 | type : 'DELETE', 657 | headers : {'Content-Type': 'application/json','Authorization' : idJwt }, 658 | dataType: 'json', 659 | data: JSON.stringify(body), 660 | success : function(response) { 661 | console.log("successfully deleted file " + fileID); 662 | callback(fileID); 663 | }, 664 | error : function(response) { 665 | console.log("could not delete file"); 666 | if (response.status == "401") { 667 | refreshAWSCredentials(); 668 | } 669 | } 670 | }); 671 | }catch(err) { 672 | console.log(err.message); 673 | } 674 | } 675 | 676 | // chatbot 677 | document.addEventListener('DOMContentLoaded', function() { 678 | const chatTab = document.querySelector('.chat-tab'); 679 | const chatContainer = document.querySelector('.chat-container'); 680 | const userInput = document.getElementById('userInput'); 681 | 682 | chatTab.addEventListener('click', function() { 683 | // Toggle visibility 684 | chatContainer.style.display = chatContainer.style.display === 'flex' ? 'none' : 'flex'; 685 | userInput.focus(); // Focus on the input field when the chat opens 686 | }); 687 | 688 | // Send message on Enter key press, but prevent a newline if the Enter key is pressed 689 | userInput.addEventListener('keydown', function(event) { 690 | if (event.key === 'Enter') { 691 | event.preventDefault(); // Prevent the default action to avoid form submission or newline 692 | sendMessage(); 693 | } 694 | }); 695 | }); 696 | 697 | function displayMessage(text, sender = 'user') { 698 | const chatMessages = document.getElementById('chatMessages'); 699 | const messageElement = document.createElement('div'); 700 | messageElement.classList.add('message', sender); 701 | 702 | // Set the Bot and the Human icon. Use unicode Emji '🤖 '; for bot if no images 703 | //const userIcon = 'User '; 704 | const botIcon = 'Bot '; 705 | // Choose icon based on sender 706 | const icon = sender === 'user' ? '👤 ' : botIcon; // Human icon for user, robot icon for bot 707 | 708 | // Set the innerHTML to include the icon and text 709 | // Note: When using innerHTML, ensure your content is safe to prevent XSS vulnerabilities 710 | messageElement.innerHTML = icon + text; 711 | 712 | chatMessages.appendChild(messageElement); 713 | chatMessages.scrollTop = chatMessages.scrollHeight; // Auto-scroll to the latest message 714 | } 715 | 716 | 717 | function displayTypingIndicator() { 718 | const chatMessages = document.getElementById('chatMessages'); 719 | let typingIndicator = document.getElementById('typingIndicator'); 720 | 721 | // If the typing indicator doesn't already exist, create it. 722 | if (!typingIndicator) { 723 | typingIndicator = document.createElement('div'); 724 | typingIndicator.classList.add('message', 'typing'); 725 | typingIndicator.id = 'typingIndicator'; 726 | typingIndicator.textContent = '...'; 727 | chatMessages.appendChild(typingIndicator); 728 | } 729 | 730 | chatMessages.scrollTop = chatMessages.scrollHeight; // Auto-scroll to the latest message 731 | } 732 | 733 | function removeTypingIndicator() { 734 | const typingIndicator = document.getElementById('typingIndicator'); 735 | if (typingIndicator) { 736 | typingIndicator.remove(); 737 | } 738 | } 739 | 740 | function sendMessage() { 741 | const userInput = document.getElementById('userInput'); 742 | const message = userInput.value.trim(); 743 | if (message === '') return; // Prevent sending empty messages 744 | userInput.value = ''; // Clear the input field 745 | 746 | displayMessage(message, 'user'); // Display the user message in the chat 747 | 748 | displayTypingIndicator(); // Display the typing indicator 749 | 750 | try { 751 | var userID = localStorage.getItem('userID'); 752 | const sessionTokensString = localStorage.getItem('sessionTokens'); 753 | const sessionTokens = JSON.parse(sessionTokensString); 754 | const IdToken = sessionTokens.IdToken; 755 | const idJwt = IdToken.jwtToken; 756 | 757 | const websocket = new WebSocket('wss://bum4o4rx48.execute-api.us-east-1.amazonaws.com/production/'); 758 | const payload = { 759 | action: 'invokeBedrockAgent', 760 | userID: userID, 761 | human: message 762 | }; 763 | console.log("invoking bedrock agent with payload: " + JSON.stringify(payload)); 764 | 765 | websocket.onopen = function() { 766 | websocket.send(JSON.stringify(payload)); 767 | }; 768 | 769 | websocket.onmessage = function(event) { 770 | const data = JSON.parse(event.data); 771 | removeTypingIndicator(); // Remove the typing indicator 772 | displayMessage(data.response, 'bot'); // Display the bot response 773 | websocket.close(); // Close the WebSocket connection after receiving the response 774 | }; 775 | 776 | websocket.onerror = function(event) { 777 | console.error('Error:', event); 778 | removeTypingIndicator(); // Ensure to remove the typing indicator even on error 779 | displayMessage("Sorry, there was an error. Please try again.", 'bot'); 780 | }; 781 | 782 | websocket.onclose = function() { 783 | // WebSocket connection closed 784 | }; 785 | 786 | } catch (err) { 787 | alert("An error occurred. Please try again!"); 788 | console.log(err.message); 789 | } 790 | } -------------------------------------------------------------------------------- /frontend/js/script_new.js: -------------------------------------------------------------------------------- 1 | var todoApiEndpoint = 'https://j3cv37qhud.execute-api.us-east-1.amazonaws.com/dev/'; 2 | var todoFilesApiEndpoint = 'https://4oumdscha7.execute-api.us-east-1.amazonaws.com/dev/'; 3 | var cognitoUserPoolId = 'us-east-1_fM3BzKm1u'; 4 | var cognitoUserPoolClientId = '4ajb6clml9vft00cof689o6c0p'; 5 | var cognitoIdentityPoolId = 'us-east-1:bbb155cb-c623-42bd-898e-57fcb4f76357'; 6 | var bucketName = 'hpf-todo-app-files'; 7 | var awsRegion = 'us-east-1'; 8 | 9 | var gridScope; 10 | var descriptionScope; 11 | 12 | function loggedInDisplay() { 13 | $("#signInButton").addClass("d-none"); 14 | $("#signOutButton").removeClass("d-none"); 15 | } 16 | 17 | function loggedOutDisplay() { 18 | $("#signInButton").removeClass("d-none"); 19 | $("#signOutButton").addClass("d-none"); 20 | } 21 | 22 | function initializeStorage() { 23 | var identityPoolId = cognitoUserPoolId;// 24 | var userPoolId = cognitoUserPoolId; // 25 | var clientId = cognitoUserPoolClientId;// 26 | var loginPrefix = 'cognito-idp.' + awsRegion + '.amazonaws.com/' + identityPoolId; 27 | 28 | localStorage.setItem('identityPoolId', identityPoolId); 29 | localStorage.setItem('userPoolId', userPoolId); 30 | localStorage.setItem('clientId', clientId); 31 | localStorage.setItem('loginPrefix', loginPrefix); 32 | } 33 | 34 | function updateModalText(descriptionTodo) { 35 | applyDescriptionScope(descriptionTodo); 36 | if (descriptionTodo.completed == true) { 37 | markCompleted(); 38 | } else { 39 | markNotCompleted(); 40 | } 41 | } 42 | 43 | function markCompleted() { 44 | $("#completedButton").addClass("d-none"); 45 | $("#alreadyCompletedButton").removeClass("d-none"); 46 | } 47 | 48 | function markNotCompleted() { 49 | $("#completedButton").removeClass("d-none"); 50 | $("#alreadyCompletedButton").addClass("d-none"); 51 | } 52 | 53 | function showAddFilesForm(){ 54 | $("#addFilesForm").removeClass("d-none"); 55 | } 56 | 57 | function hideAddFilesForm(){ 58 | $("#addFilesForm").addClass("d-none"); 59 | $("#fileinput").replaceWith($("#fileinput").val('').clone(true)); 60 | } 61 | 62 | function addFileName () { 63 | var fileName = document.getElementById('fileinput').files[0].name; 64 | document.getElementById('fileName').innerHTML = fileName; 65 | } 66 | 67 | function applyGridScope(todosList) { 68 | gridScope.todos = todosList; 69 | gridScope.$apply(); 70 | } 71 | 72 | function applyDescriptionScope(todo) { 73 | descriptionScope.descriptionTodo = todo; 74 | descriptionScope.$apply(); 75 | } 76 | 77 | function register() { 78 | event.preventDefault(); 79 | 80 | var poolData = { 81 | UserPoolId : cognitoUserPoolId, 82 | ClientId : cognitoUserPoolClientId 83 | }; 84 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 85 | 86 | var attributeList = []; 87 | 88 | 89 | var email = document.getElementById('email').value; 90 | var pw = document.getElementById('pwd').value; 91 | var confirmPw = document.getElementById('confirmPwd').value; 92 | var dataEmail = { 93 | Name : 'email', 94 | Value : email 95 | }; 96 | 97 | var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail); 98 | 99 | attributeList.push(attributeEmail); 100 | 101 | if (pw === confirmPw) { 102 | userPool.signUp(email, pw, attributeList, null, function(err, result){ 103 | if (err) { 104 | alert(err.message); 105 | return; 106 | } 107 | cognitoUser = result.user; 108 | console.log(cognitoUser); 109 | localStorage.setItem('email', email); 110 | window.location.replace('confirm.html'); 111 | }); 112 | } else { 113 | alert('Passwords do not match.') 114 | }; 115 | } 116 | 117 | function confirm() { 118 | event.preventDefault(); 119 | 120 | var confirmCode = document.getElementById('confirmCode').value; 121 | 122 | var poolData = { 123 | UserPoolId : cognitoUserPoolId, 124 | ClientId : cognitoUserPoolClientId 125 | }; 126 | 127 | var userName = localStorage.getItem('email'); 128 | 129 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 130 | var userData = { 131 | Username : userName, 132 | Pool : userPool 133 | }; 134 | 135 | var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData); 136 | cognitoUser.confirmRegistration(confirmCode, true, function(err, result) { 137 | if (err) { 138 | alert(err.message); 139 | return; 140 | } 141 | window.location.replace("index.html"); 142 | }); 143 | } 144 | 145 | function login(){ 146 | var userPoolId = localStorage.getItem('userPoolId'); 147 | var clientId = localStorage.getItem('clientId'); 148 | var identityPoolId = localStorage.getItem('identityPoolId'); 149 | var loginPrefix = localStorage.getItem('loginPrefix'); 150 | 151 | AWSCognito.config.region = awsRegion; 152 | AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({ 153 | IdentityPoolId: identityPoolId // your identity pool id here 154 | }); 155 | AWSCognito.config.update({accessKeyId: 'anything', secretAccessKey: 'anything'}) 156 | 157 | AWS.config.region = awsRegion; // Region 158 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 159 | IdentityPoolId: identityPoolId 160 | }); 161 | 162 | var poolData = { 163 | UserPoolId : userPoolId, // Your user pool id here 164 | ClientId : clientId // Your client id here 165 | }; 166 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 167 | 168 | var username = $('#username').val(); 169 | var password = $('#password').val(); 170 | 171 | var authenticationData = { 172 | Username: username, 173 | Password: password 174 | }; 175 | 176 | var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData); 177 | 178 | var userData = { 179 | Username : username, 180 | Pool : userPool 181 | }; 182 | var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData); 183 | console.log(cognitoUser); 184 | 185 | cognitoUser.authenticateUser(authenticationDetails, { 186 | onSuccess: function (result) { 187 | var accessToken = result.getAccessToken().getJwtToken(); 188 | console.log('Authentication successful', accessToken); 189 | var sessionTokens = 190 | { 191 | IdToken: result.getIdToken(), 192 | AccessToken: result.getAccessToken(), 193 | RefreshToken: result.getRefreshToken() 194 | }; 195 | localStorage.setItem('sessionTokens', JSON.stringify(sessionTokens)) 196 | localStorage.setItem('userID', username); 197 | //localStorage.setItem('password', password); 198 | window.location = './home.html'; 199 | }, 200 | onFailure: function(err) { 201 | console.log('failed to authenticate'); 202 | console.log(JSON.stringify(err)); 203 | alert('Failed to Log in.\nPlease check your credentials.'); 204 | }, 205 | }); 206 | } 207 | 208 | function checkLogin(redirectOnRec, redirectOnUnrec){ 209 | var userPoolId = localStorage.getItem('userPoolId'); 210 | var clientId = localStorage.getItem('clientId'); 211 | var identityPoolId = localStorage.getItem('identityPoolId'); 212 | var loginPrefix = localStorage.getItem('loginPrefix'); 213 | 214 | var poolData = { 215 | UserPoolId : userPoolId, // Your user pool id here 216 | ClientId : clientId // Your client id here 217 | }; 218 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 219 | var cognitoUser = userPool.getCurrentUser(); 220 | 221 | if (cognitoUser != null) { 222 | console.log("user exists"); 223 | if (redirectOnRec) { 224 | window.location = './home.html'; 225 | loggedInDisplay(); 226 | } else { 227 | $("#body").css({'visibility':'visible'}); 228 | } 229 | } else { 230 | if (redirectOnUnrec) { 231 | window.location = './index.html' 232 | } 233 | } 234 | } 235 | 236 | function refreshAWSCredentials() { 237 | var userPoolId = localStorage.getItem('userPoolId'); 238 | var clientId = localStorage.getItem('clientId'); 239 | var identityPoolId = localStorage.getItem('identityPoolId'); 240 | var loginPrefix = localStorage.getItem('loginPrefix'); 241 | 242 | var poolData = { 243 | UserPoolId : userPoolId, // Your user pool id here 244 | ClientId : clientId // Your client id here 245 | }; 246 | var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData); 247 | var cognitoUser = userPool.getCurrentUser(); 248 | 249 | if (cognitoUser != null) { 250 | cognitoUser.getSession(function(err, result) { 251 | if (result) { 252 | console.log('You are now logged in.'); 253 | cognitoUser.refreshSession(result.getRefreshToken(), function(err, result) { 254 | 255 | if (err) {//throw err; 256 | console.log('In the err: '+err); 257 | } 258 | else{ 259 | localStorage.setItem('awsConfig', JSON.stringify(AWS.config)); 260 | var sessionTokens = 261 | { 262 | IdToken: result.getIdToken(), 263 | AccessToken: result.getAccessToken(), 264 | RefreshToken: result.getRefreshToken() 265 | }; 266 | localStorage.setItem("sessionTokens", JSON.stringify(sessionTokens)); 267 | 268 | } 269 | }); 270 | 271 | } 272 | }); 273 | } 274 | } 275 | 276 | function logOut() { 277 | localStorage.clear(); 278 | document.location.reload(); 279 | window.location = './index.html'; 280 | } 281 | 282 | function getTodos(callback) { 283 | try{ 284 | var userID = localStorage.getItem('userID'); 285 | var todoApi = todoApiEndpoint + userID +'/todos'; 286 | 287 | var sessionTokensString = localStorage.getItem('sessionTokens'); 288 | var sessionTokens = JSON.parse(sessionTokensString); 289 | var IdToken = sessionTokens.IdToken; 290 | var idJwt = IdToken.jwtToken; 291 | 292 | $.ajax({ 293 | url : todoApi, 294 | type : 'GET', 295 | headers : {'Authorization' : idJwt }, 296 | success : function(response) { 297 | console.log("successfully loaded todos for " + userID); 298 | callback(response.todos); 299 | }, 300 | error : function(response) { 301 | console.log("could not retrieve todos list."); 302 | if (response.status == "401") { 303 | refreshAWSCredentials(); 304 | } 305 | } 306 | }); 307 | }catch(err) { 308 | alert("You need to be signed in. Redirecting you to the sign in page!"); 309 | loggedOutDisplay(); 310 | console.log(err.message); 311 | } 312 | } 313 | 314 | function getTodo(todoID, callback) { 315 | var userID = localStorage.getItem('userID'); 316 | var todoApi = todoApiEndpoint + userID +'/todos/' + todoID; 317 | 318 | var sessionTokensString = localStorage.getItem('sessionTokens'); 319 | var sessionTokens = JSON.parse(sessionTokensString); 320 | var IdToken = sessionTokens.IdToken; 321 | var idJwt = IdToken.jwtToken; 322 | 323 | $.ajax({ 324 | url : todoApi, 325 | type : 'GET', 326 | headers : {'Authorization' : idJwt }, 327 | success : function(response) { 328 | console.log('todoID: ' + todoID); 329 | callback(response); 330 | }, 331 | error : function(response) { 332 | console.log("could not retrieve todo."); 333 | if (response.status == "401") { 334 | refreshAWSCredentials(); 335 | } 336 | } 337 | }); 338 | } 339 | 340 | function addTodo(dateDue, title, description){ 341 | var userID = localStorage.getItem('userID'); 342 | var todoApi = todoApiEndpoint + userID + "/todos/add"; 343 | 344 | var sessionTokensString = localStorage.getItem('sessionTokens'); 345 | var sessionTokens = JSON.parse(sessionTokensString); 346 | var IdToken = sessionTokens.IdToken; 347 | var idJwt = IdToken.jwtToken; 348 | 349 | todo = { 350 | title: title, 351 | description: description, 352 | dateDue: dateDue, 353 | } 354 | 355 | $.ajax({ 356 | url : todoApi, 357 | type : 'POST', 358 | headers : {'Content-Type': 'application/json', 'Authorization' : idJwt}, 359 | dataType: 'json', 360 | data : JSON.stringify(todo), 361 | success : function(response) { 362 | console.log("todo added!") 363 | window.location.reload(); 364 | $("#success-addTodo").show(); 365 | $("#success-addTodo").html("Todo successfully added (^_^)"); 366 | }, 367 | error : function(response) { 368 | console.log("could not add todo"); 369 | $("#error-addTodo").show(); 370 | $("#error-addTodo").html("Could not add todo (x_x) "); 371 | console.log(response); 372 | 373 | } 374 | }); 375 | } 376 | 377 | function completeTodo(todoID, callback) { 378 | try { 379 | var userID = localStorage.getItem('userID'); 380 | var todoApi = todoApiEndpoint + userID + "/todos/" + todoID + "/complete"; 381 | 382 | var sessionTokensString = localStorage.getItem('sessionTokens'); 383 | var sessionTokens = JSON.parse(sessionTokensString); 384 | var IdToken = sessionTokens.IdToken; 385 | var idJwt = IdToken.jwtToken; 386 | 387 | $.ajax({ 388 | url : todoApi, 389 | async : false, 390 | type : 'POST', 391 | headers : {'Authorization' : idJwt }, 392 | success : function(response) { 393 | console.log("marked as completed: " + todoID) 394 | callback(); 395 | }, 396 | error : function(response) { 397 | console.log("could not completed todo"); 398 | if (response.status == "401") { 399 | refreshAWSCredentials(); 400 | } 401 | } 402 | }); 403 | } catch(err) { 404 | alert("You must be logged in"); 405 | console.log(err.message); 406 | } 407 | } 408 | 409 | function addTodoNotes(todoID, notes) { 410 | try { 411 | var userID = localStorage.getItem('userID'); 412 | var todoApi = todoApiEndpoint + userID + "/todos/" + todoID + "/addnotes"; 413 | 414 | var sessionTokensString = localStorage.getItem('sessionTokens'); 415 | var sessionTokens = JSON.parse(sessionTokensString); 416 | var IdToken = sessionTokens.IdToken; 417 | var idJwt = IdToken.jwtToken; 418 | 419 | inputNotes = { 420 | notes: notes, 421 | } 422 | $.ajax({ 423 | url : todoApi, 424 | type : 'POST', 425 | headers : {'Content-Type': 'application/json','Authorization' : idJwt }, 426 | dataType: 'json', 427 | data: JSON.stringify(inputNotes), 428 | success : function(response) { 429 | console.log("added notes for: " + todoID) 430 | }, 431 | error : function(response) { 432 | console.log("could not add notes"); 433 | if (response.status == "401") { 434 | refreshAWSCredentials(); 435 | } 436 | } 437 | }); 438 | } catch(err) { 439 | alert("You must be logged in to save notes"); 440 | console.log(err.message); 441 | } 442 | } 443 | 444 | function addTodoFiles(todoID, files) { 445 | try{ 446 | var userID = localStorage.getItem('userID'); 447 | var todoFilesApi = todoFilesApiEndpoint + todoID + "/files/upload"; 448 | 449 | var sessionTokensString = localStorage.getItem('sessionTokens'); 450 | var sessionTokens = JSON.parse(sessionTokensString); 451 | var IdToken = sessionTokens.IdToken; 452 | var idJwt = IdToken.jwtToken; 453 | 454 | if (!files.length) { 455 | alert("You need to choose a file to upload."); 456 | }; 457 | 458 | var fileObj = new FormData(); 459 | var file = files[0]; 460 | var sizeInKB = file.size/1024; 461 | console.log('uploading a file of ' + sizeInKB) 462 | if (sizeInKB > 2048) { 463 | alert("File size exceeds the limit of 2MB."); 464 | }; 465 | //fileObj.append(file.name, file); 466 | var fileObj = { 467 | fileName: file.name, 468 | fileBody: file 469 | }; 470 | console.log(fileObj); 471 | $.ajax({ 472 | url : todoFilesApi, 473 | type : 'POST', 474 | headers : {'Authorization' : idJwt }, 475 | contentType: false, 476 | processData: false, 477 | data: fileObj, 478 | success : function(response) { 479 | console.log("added file for todo: " + todoID) 480 | hideAddFilesForm(); 481 | }, 482 | error : function(response) { 483 | console.log("could not add file for todo: " + todoID); 484 | if (response.status == "401") { 485 | refreshAWSCredentials(); 486 | } 487 | } 488 | }); 489 | } catch(err) { 490 | alert("You must be logged in to add todo attachment"); 491 | console.log(err.message); 492 | } 493 | 494 | } 495 | 496 | function uploadTodoFileS3(todoID, bucket, filesToUp){ 497 | var userID = localStorage.getItem('userID'); 498 | var todoFilesApi = todoFilesApiEndpoint + todoID + "/files/upload"; 499 | var sessionTokensString = localStorage.getItem('sessionTokens'); 500 | var sessionTokens = JSON.parse(sessionTokensString); 501 | var IdToken = sessionTokens.IdToken; 502 | var idJwt = IdToken.jwtToken; 503 | if (!filesToUp.length) { 504 | alert("You need to choose a file to upload."); 505 | } 506 | else{ 507 | //var fileObj = new FormData(); 508 | var file = filesToUp[0]; 509 | var fileName = file.name; 510 | var filePath = userID + '/' + todoID + '/' + fileName; 511 | var fileUrl = 'https://' + awsRegion + '.amazonaws.com/' + bucketName + '/' + filePath; 512 | var sizeInKB = file.size/1024; 513 | console.log('uploading a file of ' + sizeInKB) 514 | if (sizeInKB > 2048) { 515 | alert("File size exceeds the limit of 2MB."); 516 | } 517 | else{ 518 | var params = { 519 | Key: filePath, 520 | Body: file, 521 | ACL: 'public-read' 522 | }; 523 | bucket.upload(params, function(err, data) { 524 | if (err) { 525 | console.log(err, err.stack); 526 | alert("Failed to upload file " + fileName); 527 | } else { 528 | console.log(fileName + ' successfully uploaded file for ' + todoID); 529 | var fileObj = { 530 | 'fileName': fileName, 531 | 'filePath': fileUrl 532 | } 533 | $.ajax({ 534 | url : todoFilesApi, 535 | type : 'POST', 536 | headers : {'Content-Type': 'application/json', 'Authorization' : idJwt }, 537 | contentType: 'json', 538 | data: JSON.stringify(fileObj), 539 | success : function(response) { 540 | console.log("dynamodb table updated with filePath " + fileName) 541 | 542 | }, 543 | error : function(response) { 544 | console.log("could not update dynamodb table: " + fileName); 545 | if (response.status == "401") { 546 | refreshAWSCredentials(); 547 | } 548 | } 549 | }); 550 | hideAddFilesForm(); 551 | } 552 | }) 553 | } 554 | } 555 | } 556 | 557 | function addTodoFiles(todoID, files) { 558 | var userPoolId = localStorage.getItem('userPoolId'); 559 | var clientId = localStorage.getItem('clientId'); 560 | var identityPoolId = localStorage.getItem('identityPoolId'); 561 | var loginPrefix = localStorage.getItem('loginPrefix'); 562 | 563 | try{ 564 | AWS.config.update({ 565 | region: awsRegion, 566 | credentials: new AWS.CognitoIdentityCredentials({ 567 | IdentityPoolId: identityPoolId 568 | }) 569 | }); 570 | var s3 = new AWS.S3({ 571 | apiVersion: '2006-03-01', 572 | params: {Bucket: bucketName} 573 | }); 574 | uploadTodoFileS3(todoID, s3, files); 575 | 576 | } 577 | catch(err) { 578 | //alert("You must be logged in to add todo attachment"); 579 | console.log(err.message); 580 | } 581 | } -------------------------------------------------------------------------------- /frontend/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Register - TodoHouessou 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 | 38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | --------------------------------------------------------------------------------