├── .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 | 
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 | 
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 | 
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 | 
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 | 
159 |
160 | **Backend**
161 |
162 | 
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 |
22 |
23 |
24 |
61 | {{todo.title}}
62 |
63 | {{todo.description}}
64 |
65 | Due Date: {{todo.dateDue}}
66 |
67 |
68 |
69 |
190 |
191 |
192 | This site is a sample todo-list app created for training purposes. More details here.
193 |