├── .github └── workflows │ └── manual.yml ├── .gitignore ├── CODEOWNERS ├── Classroom_Project_Instructions ├── Part_0_Prerequisites_and_Getting_Started.md ├── Part_III_TravisCI.md ├── Part_II_Microservices_Application.md ├── Part_IV_Container_Orchestration.md ├── Part_I_Monolithic_Application.md └── Part_V_Logging.md ├── LICENSE.txt ├── README.md ├── screenshots └── README.md ├── set_env.sh ├── udagram-api.postman_collection.json ├── udagram-api ├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── mock │ ├── xander0.jpg │ ├── xander1.jpg │ └── xander2.jpg ├── package-lock.json ├── package.json ├── src │ ├── aws.ts │ ├── config │ │ └── config.ts │ ├── controllers │ │ └── v0 │ │ │ ├── feed │ │ │ ├── models │ │ │ │ └── FeedItem.ts │ │ │ └── routes │ │ │ │ └── feed.router.ts │ │ │ ├── index.router.ts │ │ │ ├── model.index.ts │ │ │ └── users │ │ │ ├── models │ │ │ └── User.ts │ │ │ └── routes │ │ │ ├── auth.router.ts │ │ │ └── user.router.ts │ ├── migrations │ │ ├── 20190325-create-feed-item.js │ │ └── 20190328-create-user.js │ ├── sequelize.ts │ └── server.ts ├── tsconfig.json └── tslint.json └── udagram-frontend ├── .eslintrc.json ├── .gitignore ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── ionic.config.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── api │ │ └── api.service.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.routes.ts │ ├── auth │ │ ├── auth-login │ │ │ ├── auth-login.component.html │ │ │ ├── auth-login.component.scss │ │ │ ├── auth-login.component.spec.ts │ │ │ └── auth-login.component.ts │ │ ├── auth-menu-button │ │ │ ├── auth-menu-button.component.html │ │ │ ├── auth-menu-button.component.scss │ │ │ ├── auth-menu-button.component.spec.ts │ │ │ ├── auth-menu-button.component.ts │ │ │ └── auth-menu-user │ │ │ │ ├── auth-menu-user.component.html │ │ │ │ ├── auth-menu-user.component.scss │ │ │ │ ├── auth-menu-user.component.spec.ts │ │ │ │ └── auth-menu-user.component.ts │ │ ├── auth-register │ │ │ ├── auth-register.component.html │ │ │ ├── auth-register.component.scss │ │ │ ├── auth-register.component.spec.ts │ │ │ └── auth-register.component.ts │ │ ├── models │ │ │ └── user.model.ts │ │ └── services │ │ │ ├── auth.guard.service.spec.ts │ │ │ ├── auth.guard.service.ts │ │ │ ├── auth.service.spec.ts │ │ │ └── auth.service.ts │ ├── feed │ │ ├── feed-item │ │ │ ├── feed-item.component.html │ │ │ ├── feed-item.component.scss │ │ │ ├── feed-item.component.spec.ts │ │ │ └── feed-item.component.ts │ │ ├── feed-list │ │ │ ├── feed-list.component.html │ │ │ ├── feed-list.component.scss │ │ │ ├── feed-list.component.spec.ts │ │ │ └── feed-list.component.ts │ │ ├── feed-upload │ │ │ ├── feed-upload-button │ │ │ │ ├── feed-upload-button.component.html │ │ │ │ ├── feed-upload-button.component.scss │ │ │ │ ├── feed-upload-button.component.spec.ts │ │ │ │ └── feed-upload-button.component.ts │ │ │ ├── feed-upload.component.html │ │ │ ├── feed-upload.component.scss │ │ │ ├── feed-upload.component.spec.ts │ │ │ └── feed-upload.component.ts │ │ ├── models │ │ │ └── feed-item.model.ts │ │ └── services │ │ │ ├── feed.provider.service.spec.ts │ │ │ └── feed.provider.service.ts │ ├── home │ │ ├── home.page.html │ │ ├── home.page.scss │ │ ├── home.page.spec.ts │ │ └── home.page.ts │ └── menubar │ │ ├── menubar.component.html │ │ ├── menubar.component.scss │ │ ├── menubar.component.spec.ts │ │ └── menubar.component.ts ├── assets │ ├── icon │ │ └── favicon.png │ └── shapes.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss ├── tsconfig.app.json └── tsconfig.spec.json ├── tsconfig.json ├── tslint.json └── udagram_tests └── git_test.sh /.github/workflows/manual.yml: -------------------------------------------------------------------------------- 1 | # Workflow to ensure whenever a Github PR is submitted, 2 | # a JIRA ticket gets created automatically. 3 | name: Manual Workflow 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on pull request events but only for the master branch 8 | pull_request_target: 9 | types: [opened, reopened] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test-transition-issue: 16 | name: Convert Github Issue to Jira Issue 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@master 21 | 22 | - name: Login 23 | uses: atlassian/gajira-login@master 24 | env: 25 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 26 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 27 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 28 | 29 | - name: Create NEW JIRA ticket 30 | id: create 31 | uses: atlassian/gajira-create@master 32 | with: 33 | project: CONUPDATE 34 | issuetype: Task 35 | summary: | 36 | Github PR nd9990 - Cloud Developer | Repo: ${{ github.repository }} | PR# ${{github.event.number}} 37 | description: | 38 | Repo link: https://github.com/${{ github.repository }} 39 | PR no. ${{ github.event.pull_request.number }} 40 | PR title: ${{ github.event.pull_request.title }} 41 | PR description: ${{ github.event.pull_request.description }} 42 | In addition, please resolve other issues, if any. 43 | fields: '{"components": [{"name":"nd9990 - Cloud Developer"}], "customfield_16449":"https://classroom.udacity.com/", "customfield_16450":"Resolve the PR", "priority":{"id": "4"}}' 44 | 45 | - name: Log created issue 46 | run: echo "Issue ${{ steps.create.outputs.issue }} was created" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | postgres_dev/ 32 | logfile 33 | 34 | .DS_Store 35 | Thumbs.db 36 | UserInterfaceState.xcuserstate 37 | node_modules 38 | venv/ 39 | # Elastic Beanstalk Files 40 | .elasticbeanstalk/* 41 | !.elasticbeanstalk/*.cfg.yml 42 | !.elasticbeanstalk/*.global.yml 43 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @udacity/active-public-content 2 | -------------------------------------------------------------------------------- /Classroom_Project_Instructions/Part_0_Prerequisites_and_Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Overview - Udagram Image Filtering Microservice 2 | The project application, **Udagram** - an Image Filtering application, allows users to register and log into a web client, and post photos to a feed. 3 | 4 | This section introduces the project followed by instructions on how to set up your local environment remote dependencies to be able to configure and run the starter project. 5 | 6 | ## Components 7 | At a high level, the project has 2 main components: 8 | 1. Frontend Web App - Angular web application built with Ionic Framework 9 | 2. Backend RESTful API - Node-Express application 10 | 11 | ## Project Goal 12 | In this project you will: 13 | - Refactor the monolith application to microservices 14 | - Set up each microservice to be run in its own Docker container 15 | - Set up a Travis CI pipeline to push images to DockerHub 16 | - Deploy the DockerHub images to the Kubernetes cluster 17 | 18 | # Local Prerequisites 19 | You should have the following tools installed in your local machine: 20 | * Git 21 | * Node.js 22 | * PostgreSQL client 23 | * Ionic CLI 24 | * Docker 25 | * AWS CLI 26 | * kubectl 27 | 28 | We will provide some details and tips on how to set up the mentioned prerequisites. In general, we will opt to defer you to official installation instructions as these can change over time. 29 | 30 | ## Git 31 | Git is used to interface with GitHub. 32 | 33 | > Windows users: Once you download and install Git for Windows, you can execute all the bash, ssh, git commands in the Gitbash terminal. On the other hand, Windows users using Windows Subsystem for Linux (WSL) can follow all steps as if they are Linux users. 34 | 35 | ### Instructions 36 | Install [Git](https://git-scm.com/downloads) for your corresponding operating system. 37 | 38 | ## Node.js 39 | ### Instructions 40 | Install Node.js using [these instructions](https://nodejs.org/en/download/). We recommend a version between 12.14 and 14.15. 41 | 42 | This installer will install Node.js as well as NPM on your system. Node.js is used to run JavaScript-based applications and NPM is a package manager used to handle dependencies. 43 | 44 | ### Verify Installation 45 | ```bash 46 | # v12.14 or greater up to v14.15 47 | node -v 48 | ``` 49 | 50 | ```bash 51 | # v7.19 or greater 52 | npm -v 53 | ``` 54 | 55 | ## PostgreSQL client 56 | Using PostgreSQL involves a server and a client. The server hosts the database while the client interfaces with it to execute queries. Because we will be creating our server on AWS, we will only need to install a client for our local setup. 57 | 58 | ### Instructions 59 | The easiest way to set this up is with the [PostgreSQL Installer](https://www.postgresql.org/download/). This installer installs a PostgreSQL client in the form of the `psql` command line utility. 60 | 61 | ## Ionic CLI 62 | Ionic Framework is used to make cross-platform applications using JavaScript. It is used to help build and run Udagram. 63 | 64 | ### Instructions 65 | Use [these instructions](https://ionicframework.com/docs/installation/cli) to install Ionic Framework with `npm`. 66 | 67 | #### Verify Installation 68 | ```bash 69 | # v6.0 or higher 70 | ionic --version 71 | ``` 72 | 73 | ## Docker 74 | Docker is needed to build and run containerized applications. 75 | 76 | ### Instructions 77 | Follow the instructions for [Docker Desktop](https://docs.docker.com/desktop/#download-and-install) to install Docker. 78 | 79 | ## AWS CLI 80 | We use AWS CLI to interface programmatically with AWS. 81 | 82 | ### Instructions 83 | Follow [these instructions](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) to set up AWS CLI. 84 | 85 | After it's installed, you will need to configure an AWS access profile locally so that our local environment knows how to access your AWS account: 86 | 1. Create an IAM user with admin privileges on the AWS web console. Copy its Access Key. 87 | 2. Configure the access profile locally using your Access Key: 88 | ```bash 89 | aws configure [--profile nd9990] 90 | ``` 91 | 92 | ### Verify Installation 93 | ```bash 94 | # aws-cli/2.0.0 or greater 95 | aws --version 96 | ``` 97 | 98 | ## kubectl 99 | kubectl is the command line tool to interface with Kubernetes. We will be using this to communicate with the EKS cluster that we create in AWS. 100 | 101 | ### Instructions 102 | Follow the [instructions here](https://kubernetes.io/docs/tasks/tools/#kubectl). 103 | 104 | # Project Prerequisites 105 | To run this project, you are expected to have: 106 | 1. An S3 bucket 107 | 2. A PostgreSQL database 108 | 109 | ## S3 Bucket 110 | The project uses an AWS S3 bucket to store image files. 111 | 112 | ### Instructions 113 | 1. Navigate to S3 from the AWS console. 114 | 2. Create a public S3 bucket with default configurations (eg. no versioning, disable encryption). 115 | 3. In your newly-created S3 bucket, go to the **Permissions** tab and add an additional bucket policy to enable access for other AWS services (ie. Kubernetes). 116 | 117 | You can use the policy generator tool to generate such an IAM policy. See an example below (change the bucket name in your case). 118 | ```json 119 | { 120 | "Version":"2012-10-17", 121 | "Statement":[ 122 | { 123 | "Sid":"Stmt1625306057759", 124 | "Principal":"*", 125 | "Action":"s3:*", 126 | "Effect":"Allow", 127 | "Resource":"arn:aws:s3:::test-nd9990-dev-wc" 128 | } 129 | ] 130 | } 131 | ``` 132 | 133 | > In the AWS S3 console, the CORS configuration must be JSON format. Whereas, the AWS CLI can use either JSON or XML format. 134 | 135 | > Once the policies above are set and you are no longer testing locally, you can disable public access to your bucket. 136 | 137 | ## PostgreSQL Database 138 | We will create a PostgreSQL database using AWS RDS. This is used by the project to store user metadata. 139 | 140 | ### Instructions 141 | 1. Navigate to RDS from the AWS console. 142 | 2. Create a PostgreSQL database with the following configurations: 143 | 144 |
145 | 146 | |**Field**|**Value**| 147 | |---|---| 148 | |Database Creation Method|Standard create | 149 | |Engine Option|PostgreSQL 12 or greater| 150 | |Templates |Free tier (if no Free tier is available, select a different PostgreSQL version)| 151 | |DB Instance Identifier|Your choice| 152 | |Master Username|Your choice| 153 | |Password|Your choice| 154 | |DB Instance Class|Burstable classes with minimal size | 155 | |VPC and Subnet |Default| 156 | |Public Access|Yes| 157 | |Database Authentication|Password authentication| 158 | |VPC security group|Either choose default or
create a new one| 159 | |Availability Zone|No preferencce| 160 | |Database port|`5432` (default)| 161 |
162 | 163 | 2. Once the database is created successfully (this will take a few minutes), copy and save the database endpoint, master username, and password to your local machine. These values are required for the application to connect to the database. 164 | 165 | 3. Edit the security group's inbound rule to allow incoming connections from anywhere (`0.0.0.0/0`). This will allow an application that is running locally to connect to the database. 166 | 167 | > Note: AWS RDS will automatically create a database with the name `postgres` if none is configured during the creation step. By following the setup instructions provided here, we will be using the default database name. 168 | 169 | ### Verify Connection 170 | Test the connection from your local PostgreSQL client. 171 | Assuming the endpoint is: `mypostgres-database-1.c5szli4s4qq9.us-east-1.rds.amazonaws.com`, you can run: 172 | ```bash 173 | psql -h mypostgres-database-1.c5szli4s4qq9.us-east-1.rds.amazonaws.com -U [your-username] postgres 174 | # Provide the database password when prompted 175 | ``` 176 | If your connection is succesful, your terminal should print ` "postgres=>"`. 177 | 178 | You can play around with some `psql` commands found [here](https://www.postgresql.org/docs/13/app-psql.html). 179 | 180 | Afterwards, you can enter `\q` to quit. 181 | 182 | # Project Configuration 183 | Once the local and remote prerequisites are set up, we will need to configure our application so that they can connect and utilize them. 184 | 185 | ## Fork and Clone the Project 186 | If you have not already done so, you will need to fork and clone the project so that you have your own copy to work with. 187 | 188 | ```bash 189 | git clone https://github.com//nd9990-c3-microservices-exercises.git 190 | 191 | cd nd9990-c3-microservices-exercises/project/ 192 | ``` 193 | 194 | ## Configuration Values 195 | The application will need to connect to the AWS PostgreSQL database and S3 bucket that you have created. 196 | 197 | We do **not** want to hard-code the configuration details into the application code. The code should not contain sensitive information (ie. username and password). 198 | 199 | For this reason, we will follow a common pattern to store our credentials inside environment variables. We'll explain how to set these values in Mac/Linux environments and Windows environments followed by an example. 200 | 201 | ### Set Environment Variables in Mac/Linux 202 | #### Instructions 203 | 1. Use the `set_env.sh` file present in the `project/` directory to configure these values on your local machine. This is a file that has been set up for your convenience to manage your environment. 204 | 2. Prevent this file from being tracked in `git` so that your credentials don't become stored remotely: 205 | ```bash 206 | # Stop git from tracking the set_env.sh file 207 | git rm --cached set_env.sh 208 | 209 | # Prevent git from tracking the set_env.sh file 210 | echo *set_env.sh >> .gitignore 211 | ``` 212 | 3. Running the command `source set_env.sh` will set your environment variables. 213 | > Note: The method above will set the environment variables temporarily. Every time you open a new terminal, you will have to run `source set_env.sh` to reconfigure your environment variables 214 | #### Verify Configurations 215 | 1. Set the configuration values as environment variables: 216 | ```bash 217 | source set_env.sh 218 | ``` 219 | 2. Verify that environment variables were set by testing one of the expected values: 220 | ```bash 221 | echo $POSTGRES_USERNAME 222 | ``` 223 | 224 | ### Set Environment Variables in Windows 225 | Set all the environment variables as shown in the `set_env.sh` file either using the **Advanced System Settings** or d GitBash/WSL terminal. 226 | 227 | Below is an example. Make sure that you replace the values with ones that are applicable to the resources that you created in AWS. 228 | ```bash 229 | setx POSTGRES_USERNAME postgres 230 | setx POSTGRES_PASSWORD abcd1234 231 | setx POSTGRES_HOST mypostgres-database-1.c5szli4s4qq9.us-east-1.rds.amazonaws.com 232 | setx POSTGRES_DB postgres 233 | setx AWS_BUCKET test-nd9990-dev-wc 234 | setx AWS_REGION us-east-1 235 | setx AWS_PROFILE nd9990 236 | setx JWT_SECRET hello 237 | setx URL http://localhost:8100 238 | ``` 239 | 240 | # Get Started! 241 | Now that we have our prerequsites set up and configured, we will be following up this section with an overview of how to run the application. 242 | 243 | ## Project Assessment 244 | To understand how you project will be assessed, see the Project Rubric 245 | -------------------------------------------------------------------------------- /Classroom_Project_Instructions/Part_III_TravisCI.md: -------------------------------------------------------------------------------- 1 | # Part 3 - Set up Travis continuous integration pipeline 2 | 3 | Prior to setting up a multi-container application in Kubernetes, you will need to set up a CI pipeline to build and push our application code as Docker images in DockerHub. 4 | 5 | The end result that we want is a setup where changes in your GitHub code will automatically trigger a build process that generates Docker images. 6 | 7 | ### Create Dockerhub Repositories 8 | 9 | Log in to https://hub.docker.com/ and create four public repositories - each repository corresponding to your local Docker images. 10 | 11 | * `reverseproxy` 12 | * `udagram-api-user` 13 | * `udagram-api-feed` 14 | * `udagram-frontend` 15 | 16 | > Note: The names of the repositoriesare exactly the same as the `image name` specified in the *docker-compose-build.yaml* file 17 | 18 | ### Set up Travis CI Pipeline 19 | 20 | Use Travis CI pipeline to build and push images to your DockerHub registry. 21 | 22 | 1. Create an account on https://travis-ci.com/ (not https://travis-ci.org/). It is recommended that you sign in using your Github account. 23 | 24 | 2. Integrate Github with Travis: Activate your GitHub repository with whom you want to set up the CI pipeline. 25 | 26 | 3. Set up your Dockerhub username and password in the Travis repository's settings, so that they can be used inside of `.travis.yml` file while pushing images to the Dockerhub. 27 | 28 | 4. Add a `.travis.yml` configuration file to the project directory locally. 29 | 30 | In addition to the mandatory sections, your Travis file should automatically read the Dockerfiles, build images, and push images to DockerHub. For build and push, you can use either `docker-compose` or individual `docker build` commands as shown below. 31 | ```bash 32 | # Assuming the .travis.yml file is in the project directory, and there is a separate sub-directory for each service 33 | # Use either `docker-compose` or individual `docker build` commands 34 | # Build 35 | - docker build -t udagram-api-feed ./udagram-api-feed 36 | # Do similar for other three images 37 | ``` 38 | 39 | ```bash 40 | # Tagging 41 | - docker tag udagram-api-feed sudkul/udagram-api-feed:v1 42 | # Do similar for other three images``` 43 | ```bash 44 | # Push 45 | # Assuming DOCKER_PASSWORD and DOCKER_USERNAME are set in the Travis repository settings 46 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 47 | - docker push sudkul/udagram-api-feed:v1 48 | # Do similar for other three images 49 | ``` 50 | > **Tip**: Use different tags each time you push images to the Dockerhub. 51 | 52 | 53 | 5. Trigger your build by pushing your changes to the Github repository. All of these steps mentioned in the `.travis.yml` file will be executed on the Travis worker node. It may take upto 15-20 minutes to build and push all four images. 54 | 55 | 56 | 6. Verify if the newly pushed images are now available in your Dockerhub account. 57 | 58 | 59 | ### Screenshots 60 | So that we can verify your project’s pipeline is set up properly, please include the screenshots of the following: 61 | 62 | 1. DockerHub showing images that you have pushed 63 | 2. Travis CI showing a successful build job 64 | 65 | 66 | ### Troubleshooting 67 | 68 | If you are not able to get through the Travis pipeline, and still want to push your local images to the Dockerhub (only for testing purposes), you can attempt the manual method. 69 | 70 | Note that this is only for the troubleshooting purposes, such as verifying the deployment to the Kubernetes cluster. 71 | 72 | * Log in to the Docker from your CLI, and tag the images with the name of your registry name (Dockerhub account username). 73 | ```bash 74 | # See the list of current images 75 | docker images 76 | # Use the following syntax 77 | # In the remote registry (Dockerhub), we can have multiple versions of an image using "tags". 78 | # docker tag /: 79 | docker tag /: 80 | ``` 81 | * Push the images to the Dockerhub. 82 | ```bash 83 | docker login --username= 84 | # Use the "docker push" command for each image, or 85 | # Use "docker-compose -f docker-compose-build.yaml push" if the names in the compose file are as same as the Dockerhub repositories. 86 | ``` 87 | 88 | 89 | -------------------------------------------------------------------------------- /Classroom_Project_Instructions/Part_II_Microservices_Application.md: -------------------------------------------------------------------------------- 1 | # Part 2 - Run the project locally in a multi-container environment 2 | 3 | The objective of this part of the project is to: 4 | 5 | * Refactor the monolith application to microservices 6 | * Set up each microservice to be run in its own Docker container 7 | 8 | Once you refactor the Udagram application, it will have the following services running internally: 9 | 10 | 1. Backend `/user/` service - allows users to register and log into a web client. 11 | 1. Backend `/feed/` service - allows users to post photos, and process photos using image filtering. 12 | 1. Frontend - It is a basic Ionic client web application that acts as an interface between the user and the backend services. 13 | 1. Nginx as a reverse proxy server - for resolving multiple services running on the same port in separate containers. When different backend services are running on the same port, then a reverse proxy server directs client requests to the appropriate backend server and retrieves resources on behalf of the client. 14 | 15 | > Keep in mind that we don’t want to make any feature changes to the frontend or backend code. If a user visits the frontend web application, it should look the same regardless of whether the application is structured as a monolith or microservice. 16 | 17 | 18 | 19 | 20 | 21 | Navigate to the project directory, and set up the environment variables again: 22 | 23 | ```bash 24 | source set_env.sh 25 | ``` 26 | 27 | ### Refactor the Backend API 28 | 29 | The current */project/udagram-api/* backend application code contains logic for both */users/* and */feed/* endpoints. Let's decompose the API code into the following two separate services that can be run independently of one another. 30 | 31 | 1. */project/udagram-api-feed/* 32 | 1. */project/udagram-api-user/* 33 | 34 | Create two new directories (as services) with the names above. Copy the backend starter code into the above individual services, and then break apart the monolith. Each of the services above will have the following directory structure, with a lot of duplicate code. 35 | 36 | ```bash 37 | . 38 | ├── mock # Common and no change 39 | ├── node_modules # Auto generated. Do not copy. Add this into the .gitignore and .dockerignore 40 | ├── package-lock.json # Auto generated. Do not copy. 41 | ├── package.json # Common and no change 42 | ├── src 43 | │ ├── config # Common and no change 44 | │ ├── controllers/v0 # TODO: Keep either /feed or /users service. Delete the other folder 45 | │ ├── index.router.ts # TODO: Remove code related to other (either feed or users) service 46 | │ └── index.router.ts # TODO: Remove code related to other (either feed or users) service 47 | │ ├── migrations # TODO: Remove the JSON related to other (either feed or users) 48 | │ └── server.ts # TODO: Remove code related to other (either feed or users) service 49 | ├── Dockerfile # TODO: Create NEW, and common 50 | └── .dockerignore # TODO: Add "node_modules" to this file 51 | ``` 52 | 53 | The Dockerfile for the above two backend services will be like: 54 | 55 | ```bash 56 | FROM node:13 57 | # Create app directory 58 | WORKDIR /usr/src/app 59 | # Install app dependencies 60 | 61 | COPY package*.json ./ 62 | RUN npm ci 63 | # Bundle app source 64 | COPY . . 65 | EXPOSE 8080 66 | CMD [ "npm", "run", "prod" ] 67 | ``` 68 | > Note: A wildcard is used in the line `COPY package*.json ./` to ensure both package.json and package-lock.json are copied where available (npm@5+) 69 | 70 | It's not a hard requirement to use the exact same Dockerfile above. Feel free to use other base images or optimize the commands. 71 | 72 | ### Refactor the Frontend Application 73 | 74 | In the frontend service, you just need to add a Dockerfile to the */project/udagram-frontend/* directory. 75 | 76 | ```bash 77 | ## Build 78 | FROM beevelop/ionic:latest AS ionic 79 | # Create app directory 80 | WORKDIR /usr/src/app 81 | # Install app dependencies 82 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 83 | COPY package*.json ./ 84 | RUN npm ci 85 | # Bundle app source 86 | COPY . . 87 | RUN ionic build 88 | ## Run 89 | FROM nginx:alpine 90 | #COPY www /usr/share/nginx/html 91 | COPY --from=ionic /usr/src/app/www /usr/share/nginx/html 92 | ``` 93 | 94 | > **Tip**: Add `.dockerignore` to each of the services above, and mention `node_modules` in that file. It will ensure that the `node_modules` will not be included in the Dockerfile `COPY` commands. 95 | > 96 | 97 | ### How would containers discover each other and communicate? 98 | Use another container named *reverseproxy* running the Nginx server. The *reverseproxy* service will help add another layer between the frontend and backend APIs so that the frontend only uses a single endpoint and doesn't realize it's deployed separately. *This is one of the approaches and not necessarily the only way to deploy the services. *To set up the *reverseproxy* container, follow the steps below: 99 | 100 | 1. Create a newer directory */project/udagram-reverseproxy/ * 101 | 2. Create a Dockerfile as: 102 | ```bash 103 | FROM nginx:alpine 104 | COPY nginx.conf /etc/nginx/nginx.conf 105 | ``` 106 | 107 | 3. Create the *nginx.conf* file that will associate all the service endpoints as: 108 | ```bash 109 | worker_processes 1; 110 | events { worker_connections 1024; } 111 | error_log /dev/stdout debug; 112 | http { 113 | sendfile on; 114 | upstream user { 115 | server backend-user:8080; 116 | } 117 | upstream feed { 118 | server backend-feed:8080; 119 | } 120 | proxy_set_header Host $host; 121 | proxy_set_header X-Real-IP $remote_addr; 122 | proxy_set_header X-NginX-Proxy true; 123 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 124 | proxy_set_header X-Forwarded-Host $server_name; 125 | server { 126 | listen 8080; 127 | location /api/v0/feed { 128 | proxy_pass http://feed; 129 | } 130 | location /api/v0/users { 131 | proxy_pass http://user; 132 | } 133 | } 134 | } 135 | ``` 136 | The Nginx container will expose 8080 port. The configuration file above, in the `server` section, it will route the *http://localhost:8080/api/v0/feed* requests to the *backend-user:8080* container. The same applies for the *http://localhost:8080/api/v0/users* requests. 137 | 138 | 139 | ### Current Directory Structure 140 | At this moment, your project directory would have the following structure: 141 | ```bash 142 | . 143 | ├── udagram-api-feed 144 | │   └── src 145 | ├── udagram-api-user 146 | │   └── src 147 | ├── udagram-frontend 148 | │   └── src 149 | └── udagram-reverseproxy 150 | ``` 151 | 152 | 153 | 154 | ### Use Docker compose to build and run multiple Docker containers 155 | 156 | > **Note**: The ultimate objective of this step is to have the Docker images for each microservice ready locally. This step can also be done manually by building and running containers one by one. 157 | 158 | 159 | 1. Once you have created the Dockerfile in each of the following services directories, you can use the `docker-compose` command to build and run multiple Docker containers at once. 160 | 161 | - */project/udagram-api-feed/* 162 | - */project/udagram-api-feed/* 163 | - */project/udagram-frontend/* 164 | - */project/udagram-reverseproxy/* 165 | 166 | The `docker-compose` command uses a YAML file to configure your application’s services in one go. Meaning, you create and start all the services from your configuration file, with a single command. Otherwise, you will have to individually build containers one-by-one for each of your services. 167 | 168 | 169 | 170 | 2. **Create Images** - In the project's parent directory, create a [docker-compose-build.yaml](https://video.udacity-data.com/topher/2021/July/60e28b72_docker-compose-build/docker-compose-build.yaml) file. It will create an image for each individual service. Then, you can run the following command to create images locally: 171 | ```bash 172 | # Make sure the Docker services are running in your local machine 173 | # Remove unused and dangling images 174 | docker image prune --all 175 | # Run this command from the directory where you have the "docker-compose-build.yaml" file present 176 | docker-compose -f docker-compose-build.yaml build --parallel 177 | ``` 178 | >**Note**: YAML files are extremely indentation sensitive, that's why we have attached the files for you. 179 | 180 | 181 | 3. **Run containers** using the images created in the step above. Create another YAML file, [docker-compose.yaml](https://video.udacity-data.com/topher/2021/July/60e28b91_docker-compose/docker-compose.yaml), in the project's parent directory. It will use the existing images and create containers. While creating containers, it defines the port mapping, and the container dependency. 182 | 183 | Once you have the YAML file above ready in your project directory, you can start the application using: 184 | ```bash 185 | docker-compose up 186 | ``` 187 | 188 | 4. Visit http://localhost:8100 in your web browser to verify that the application is running. 189 | 190 | 191 | -------------------------------------------------------------------------------- /Classroom_Project_Instructions/Part_IV_Container_Orchestration.md: -------------------------------------------------------------------------------- 1 | # Part 4 - Container Orchestration with Kubernetes 2 | 3 | ## Prerequisites 4 | We will need to set up our CLI to interface with Kubernetes, our Kubernetes cluster in EKS, and connecting our CLI tool with the newly-created cluster. 5 | 6 | ### kubectl 7 | For this section we will need to use `kubectl`. Verify that you have the `kubectl` utility installed locally by running the following command: 8 | ```bash 9 | kubectl version --short 10 | ``` 11 | This should print a response with a `Client Version` if it's successful. 12 | 13 | ### EKS Cluster Creation 14 | We will be creating an EKS cluster with the AWS console. 15 | 16 | Follow the instructions provided by AWS on [Creating an Amazon EKS Cluster](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html). 17 | 18 | Make sure that you are following the steps for _AWS Management Console_ and not _eksctl_ or _AWS CLI_ (you are welcome to try creating a cluster with these alternate methods, but this course will be supporting the _AWS Management Console_ workflow). 19 | 20 | During the creation process, the EKS console will provide dropdown menus for selecting options such as IAM roles and VPCs. If none exist for you, follow the documentation that are linked in the EKS console. 21 | 22 | #### Tips 23 | * For the _Cluster Service Role_ in the creation process, create an [AWS role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) for EKS. Make sure you attach the policies for `AmazonEKSClusterPolicy`, `AmazonEC2ContainerServiceFullAccess`, and `AmazonEKSServicePolicy`. 24 | * If you don't have a [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/how-it-works.html), create one with the `IPv4 CIDR block` value `10.0.0.0/16`. Make sure you select `No IPv6 CIDR block`. 25 | * Your _Cluster endpoint access_ should be set to _Public_ 26 | * Your cluster may take ~20 minutes to be created. Once it's ready, it should be marked with an _Active_ status. 27 | 28 | > We use the AWS console and `kubectl` to create and interface with EKS. eksctl is an AWS-supported tool for creating clusters through a CLI interface. Note that we will provide limited support if you choose to use `eksctl` to manage your cluster. 29 | 30 | ### EKS Node Groups 31 | Once your cluster is created, we will need to add Node Groups so that the cluster has EC2 instances to process the workloads. 32 | 33 | Follow the instructions provided by AWS on [Creating a Managed Node Group](https://docs.aws.amazon.com/eks/latest/userguide/create-managed-node-group.html). Similar to before, make sure you're following the steps for _AWS Management Console_. 34 | 35 | #### Tips 36 | * For the _Node IAM Role_ in the creation process, create an [AWS role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) for EKS Node Groups. Make sure you attach the policies for `AmazonEKSWorkerNodePolicy`, `AmazonEC2ContainerRegistryReadOnly`, and `AmazonEKS_CNI_Policy`. 37 | * We recommend using `m5.large` instance types 38 | * We recommend setting 2 minimum nodes, 3 maximum nodes 39 | 40 | 41 | ### Connecting kubectl with EKS 42 | Follow the instructions provided by AWS on [Create a kubeconfig for Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html). This will make it such that your `kubectl` will be running against your newly-created EKS cluster. 43 | 44 | #### Verify Cluster and Connection 45 | Once `kubectl` is configured to communicate with your EKS cluster, run the following to validate that it's working: 46 | ```bash 47 | kubectl get nodes 48 | ``` 49 | This should return information regarding the nodes that were created in your EKS clusteer. 50 | 51 | 52 | ### Deployment 53 | In this step, you will deploy the Docker containers for the frontend web application and backend API applications in their respective pods. 54 | 55 | Recall that while splitting the monolithic app into microservices, you used the values saved in the environment variables, as well as AWS CLI was configured locally. Similar values are required while instantiating containers from the Dockerhub images. 56 | 57 | 1. **ConfigMap:** Create `env-configmap.yaml`, and save all your configuration values (non-confidential environments variables) in that file. 58 | 59 | 60 | 2. **Secret:** Do not store the PostgreSQL username and passwords in the `env-configmap.yaml` file. Instead, create `env-secret.yaml` file to store the confidential values, such as login credentials. Unlike the AWS credentials, these values do not need to be Base64 encoded. 61 | 62 | 63 | 3. **Secret:** Create *aws-secret.yaml* file to store your AWS login credentials. Replace `___INSERT_AWS_CREDENTIALS_FILE__BASE64____` with the Base64 encoded credentials (not the regular username/password). 64 | * Mac/Linux users: If you've configured your AWS CLI locally, check the contents of `~/.aws/credentials` file using `cat ~/.aws/credentials` . It will display the `aws_access_key_id` and `aws_secret_access_key` for your AWS profile(s). Now, you need to select the applicable pair of `aws_access_key` from the output of the `cat` command above and convert that string into `base64` . You use commands, such as: 65 | ```bash 66 | # Use a combination of head/tail command to identify lines you want to convert to base64 67 | # You just need two correct lines: a right pair of aws_access_key_id and aws_secret_access_key 68 | cat ~/.aws/credentials | tail -n 5 | head -n 2 69 | # Convert 70 | cat ~/.aws/credentials | tail -n 5 | head -n 2 | base64 71 | ``` 72 | * **Windows users:** Copy a pair of *aws_access_key* from the AWS credential file and paste it into the encoding field of this third-party website: https://www.base64encode.org/ (or any other). Encode and copy/paste the result back into the *aws-secret.yaml* file. 73 | 74 |
75 | 76 | 77 | 4. **Deployment configuration:** Create *deployment.yaml* file individually for each service. While defining container specs, make sure to specify the same images you've pushed to the Dockerhub earlier. Ultimately, the frontend web application and backend API applications should run in their respective pods. 78 | 79 | 5. **Service configuration: **Similarly, create the *service.yaml* file thereby defining the right services/ports mapping. 80 | 81 | 82 | Once, all deployment and service files are ready, you can use commands like: 83 | ```bash 84 | # Apply env variables and secrets 85 | kubectl apply -f aws-secret.yaml 86 | kubectl apply -f env-secret.yaml 87 | kubectl apply -f env-configmap.yaml 88 | # Deployments - Double check the Dockerhub image name and version in the deployment files 89 | kubectl apply -f backend-feed-deployment.yaml 90 | # Do the same for other three deployment files 91 | # Service 92 | kubectl apply -f backend-feed-service.yaml 93 | # Do the same for other three service files 94 | ``` 95 | Make sure to check the image names in the deployment files above. 96 | 97 | 98 | 99 | ## Connecting k8s services to access the application 100 | 101 | If the deployment is successful, and services are created, there are two options to access the application: 102 | 1. If you deployed the services as CLUSTERIP, then you will have to [forward a local port to a port on the "frontend" Pod](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/#forward-a-local-port-to-a-port-on-the-pod). In this case, you don't need to change the URL variable locally. 103 | 104 | 105 | 2. If you exposed the "frontend" deployment using a Load Balancer's External IP, then you'll have to update the URL environment variable locally, and re-deploy the images with updated env variables. 106 | 107 | Below, we have explained method #2, as mentioned above. 108 | 109 | ### Expose External IP 110 | 111 | Use this link to expose an External IP address to access your application in the EKS Cluster. 112 | 113 | ```bash 114 | # Check the deployment names and their pod status 115 | kubectl get deployments 116 | # Create a Service object that exposes the frontend deployment: 117 | kubectl expose deployment frontend --type=LoadBalancer --name=publicfrontend 118 | kubectl get services publicfrontend 119 | # Note down the External IP, such as 120 | # a5e34958a2ca14b91b020d8aeba87fbb-1366498583.us-east-1.elb.amazonaws.com 121 | # Check name, ClusterIP, and External IP of all deployments 122 | kubectl get services 123 | ``` 124 | 125 | 126 | ### Update the environment variables 127 | 128 | Once you have the External IP of your front end and reverseproxy deployment, Change the API endpoints in the following places locally: 129 | 130 | * Environment variables - Replace the http://**localhost**:8100 string with the Cluster-IP of the *frontend* service. After replacing run `source ~/.zshrc` and verify using `echo $URL` 131 | 132 | 133 | 134 | * *udagram-deployment/env-configmap.yaml* file - Replace http://localhost:8100 string with the Cluster IP of the *frontend*. 135 | 136 | 137 | 138 | * *udagram-frontend/src/environments/environment.ts* file - Replace 'http://localhost:8080/api/v0' string with either the Cluster IP of the *reverseproxy* deployment. 139 | 140 | 141 | 142 | * *udagram-frontend/src/environments/environment.prod.ts* - Replace 'http://localhost:8080/api/v0' string. 143 | 144 | 145 | 146 | * Retag in the `.travis.yaml` (say use v3, v4, v5, ...) as well as deployment YAML files 147 | 148 | Then, push your changes to the Github repo. Travis will automatically build and re-push images to your Dockerhub. 149 | Next, re-apply configmap and re-deploy to the k8s cluster. 150 | ```bash 151 | kubectl apply -f env-configmap.yaml 152 | # Rolling update "frontend" containers of "frontend" deployment, updating the image 153 | kubectl set image deployment frontend frontend=sudkul/udagram-frontend:v3 154 | # Do the same for other three deployments 155 | ``` 156 | Check your deployed application at the External IP of your *publicfrontend* service. 157 | 158 | >**Note**: There can be multiple ways of setting up the deployment in the k8s cluster. As long as your deployment is successful, and fulfills [Project Rubric](https://review.udacity.com/#!/rubrics/2804/view), you are good go ahead! 159 | 160 | ## Troubleshoot 161 | 1. Use this command to see the STATUS of your pods: 162 | ```bash 163 | kubectl get pods 164 | kubectl describe pod 165 | # An example: 166 | # kubectl logs backend-user-5667798847-knvqz 167 | # Error from server (BadRequest): container "backend-user" in pod "backend-user-5667798847-knvqz" is waiting to start: trying and failing to pull image 168 | ``` 169 | In case of `ImagePullBackOff` or `ErrImagePull` or `CrashLoopBackOff`, review your deployment.yaml file(s) if they have the right image path. 170 | 171 | 172 | 2. Look at what's there inside the running container. [Open a Shell to a running container](https://kubernetes.io/docs/tasks/debug-application-cluster/get-shell-running-container/) as: 173 | ```bash 174 | kubectl get pods 175 | # Assuming "backend-feed-68d5c9fdd6-dkg8c" is a pod 176 | kubectl exec --stdin --tty backend-feed-68d5c9fdd6-dkg8c -- /bin/bash 177 | # See what values are set for environment variables in the container 178 | printenv | grep POST 179 | # Or, you can try "curl :8080/api/v0/feed " to check if services are running. 180 | # This is helpful to see is backend is working by opening a bash into the frontend container 181 | ``` 182 | 183 | 3. When you are sure that all pods are running successfully, then use developer tools in the browser to see the precise reason for the error. 184 | - If your frontend is loading properly, and showing *Error: Uncaught (in promise): HttpErrorResponse: {"headers":{"normalizedNames":{},"lazyUpdate":null,"headers":{}},"status":0,"statusText":"Unknown Error"....*, it is possibly because the *udagram-frontend/src/environments/environment.ts* file has incorrectly defined the ‘apiHost’ to whom forward the requests. 185 | - If your frontend is **not** not loading, and showing *Error: Uncaught (in promise): HttpErrorResponse: {"headers":{"normalizedNames":{},"lazyUpdate":null,"headers":{}},"status":0,"statusText":"Unknown Error", ....* , it is possibly because URL variable is not set correctly. 186 | - In the case of *Failed to load resource: net::ERR_CONNECTION_REFUSED* error as well, it is possibly because the URL variable is not set correctly. 187 | 188 | ## Screenshots 189 | So that we can verify that your project is deployed, please include the screenshots of the following commands with your completed project. 190 | ```bash 191 | # Kubernetes pods are deployed properly 192 | kubectl get pods 193 | # Kubernetes services are set up properly 194 | kubectl describe services 195 | # You have horizontal scaling set against CPU usage 196 | kubectl describe hpa 197 | ``` 198 | -------------------------------------------------------------------------------- /Classroom_Project_Instructions/Part_I_Monolithic_Application.md: -------------------------------------------------------------------------------- 1 | # Part 1 - Run the project locally as a Monolithic application 2 | 3 | Now that you have set up the AWS PostgreSQL database and S3 bucket, and saved the environment variables, let's run the application locally. It's recommended that you start the backend application first before starting the frontend application that depends on the backend API. 4 | 5 | ## Backend App 6 | 7 | ### Download Dependencies 8 | Download all the package dependencies by running the following command from the `/project/udagram-api/` directory: 9 | ```bash 10 | npm install . 11 | ``` 12 | ### Run Locally 13 | Run the application locally in a development environment. This has been configured so that the changes in your code take effect without needing to restart the server. 14 | ```bash 15 | npm run dev 16 | ``` 17 | 18 | ### Verification 19 | Once this command is run successfully, visit the `http://localhost:8080/api/v0/feed` in your web browser to verify that the application is running. You should see a JSON payload. 20 | 21 | ## Frontend App 22 | ### Download Dependencies 23 | Download all the package dependencies by running the following command from the `/project/udagram-frontend/` directory: 24 | ```bash 25 | npm install . 26 | ``` 27 | 28 | ### Build and Run the Project 29 | ```bash 30 | ionic build 31 | ionic serve 32 | ``` 33 | > Note: If you don't have Ionic CLI installed already, revisit the prerequisites in the previous section for setup instructions. 34 | 35 | ### Verification 36 | Visit `http://localhost:8100` in your web browser to verify that the application is running. You should see a web interface. 37 | 38 | ___ 39 | 40 | ## Next Steps 41 | At this point, you should have a fully working web application that interfaces with an API. Feel free to play around with the application and its code to get an idea of how it works. 42 | 43 | The rest of this section will provide some optional steps for your code. 44 | 45 | ### Optional 46 | #### Linting Code 47 | It's useful to _lint_ your code so that changes in the codebase adhere to a coding standard. This helps alleviate issues when developers use different styles of coding. 48 | 49 | `eslint` has been set up for TypeScript in the codebase for you. To lint your code, run the following: 50 | ```bash 51 | npx eslint --ext .js,.ts src/ 52 | ``` 53 | To have your code fixed automatically, run 54 | ```bash 55 | npx eslint --ext .js,.ts src/ --fix 56 | ``` 57 | -------------------------------------------------------------------------------- /Classroom_Project_Instructions/Part_V_Logging.md: -------------------------------------------------------------------------------- 1 | 2 | # Part 5. Logging 3 | Use logs to capture metrics. This can help us with debugging. 4 | 5 | ### Screenshots 6 | To verify that user activity is logged, please include a screenshot of: 7 | 8 | ```bash 9 | kubectl logs 10 | ``` 11 | 12 | 13 | ### Suggestions to Make Your Project Stand Out (Optional) 14 | 15 | Try one or more of these to take your project to the next level. 16 | 17 | 1. **Reduce Duplicate Code** - When we decomposed our API code into two separate applications, we likely had a lot of duplicate code. Optionally, you could reduce the duplicate code by abstracting them into a common library. 18 | 19 | 20 | 2. **Secure the API** - The API is only meant to be consumed by the frontend web application. Let's set up ingress rules so that only web requests from our web application can make successful API requests. 21 | 22 | 23 | --- 24 | 25 | # Submission Requirements 26 | The project will be submitted as a link to a GitHub repo or a zip file and should include screenshots to document the application's infrastructure. 27 | 28 | ### Required Screenshots 29 | * Docker images in your repository in DockerHub 30 | * TravisCI build pipeline showing successful build jobs 31 | * Kubernetes `kubectl get pods` output 32 | * Kubernetes `kubectl describe services` output 33 | * Kubernetes `kubectl describe hpa` output 34 | * Kubernetes `kubectl logs ` output 35 | 36 | ## Clean up 37 | Once we are done with our exercises, it helps to remove our AWS resources so that we don't accrue unnecessary charges to our AWS balance. 38 | 1. Delete the EKS cluster. 39 | 2. Delete the S3 bucket and RDS PostgreSQL database. 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2012 - 2020, Udacity, Inc. 2 | 3 | Udacity hereby grants you a license in and to the Educational Content, including 4 | but not limited to homework assignments, programming assignments, code samples, 5 | and other educational materials and tools (as further described in the Udacity 6 | Terms of Use), subject to, as modified herein, the terms and conditions of the 7 | Creative Commons Attribution-NonCommercial- NoDerivs 3.0 License located at 8 | http://creativecommons.org/licenses/by-nc-nd/4.0 and successor locations for 9 | such license (the "CC License") provided that, in each case, the Educational 10 | Content is specifically marked as being subject to the CC License. 11 | 12 | Udacity expressly defines the following as falling outside the definition of 13 | "non-commercial": 14 | (a) the sale or rental of (i) any part of the Educational Content, (ii) any 15 | derivative works based at least in part on the Educational Content, or (iii) 16 | any collective work that includes any part of the Educational Content; 17 | (b) the sale of access or a link to any part of the Educational Content without 18 | first obtaining informed consent from the buyer (that the buyer is aware 19 | that the Educational Content, or such part thereof, is available at the 20 | Website free of charge); 21 | (c) providing training, support, or editorial services that use or reference the 22 | Educational Content in exchange for a fee; 23 | (d) the sale of advertisements, sponsorships, or promotions placed on the 24 | Educational Content, or any part thereof, or the sale of advertisements, 25 | sponsorships, or promotions on any website or blog containing any part of 26 | the Educational Material, including without limitation any "pop-up 27 | advertisements"; 28 | (e) the use of Educational Content by a college, university, school, or other 29 | educational institution for instruction where tuition is charged; and 30 | (f) the use of Educational Content by a for-profit corporation or non-profit 31 | entity for internal professional development or training. 32 | 33 | THE SERVICES AND ONLINE COURSES (INCLUDING ANY CONTENT) ARE PROVIDED "AS IS" AND 34 | "AS AVAILABLE" WITH NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EITHER 35 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 36 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. YOU 37 | ASSUME TOTAL RESPONSIBILITY AND THE ENTIRE RISK FOR YOUR USE OF THE SERVICES, 38 | ONLINE COURSES, AND CONTENT. WITHOUT LIMITING THE FOREGOING, WE DO NOT WARRANT 39 | THAT (A) THE SERVICES, WEBSITES, CONTENT, OR THE ONLINE COURSES WILL MEET YOUR 40 | REQUIREMENTS OR EXPECTATIONS OR ACHIEVE THE INTENDED PURPOSES, (B) THE WEBSITES 41 | OR THE ONLINE COURSES WILL NOT EXPERIENCE OUTAGES OR OTHERWISE BE UNINTERRUPTED, 42 | TIMELY, SECURE OR ERROR-FREE, (C) THE INFORMATION OR CONTENT OBTAINED THROUGH 43 | THE SERVICES, SUCH AS CHAT ROOM SERVICES, WILL BE ACCURATE, COMPLETE, CURRENT, 44 | ERROR- FREE, COMPLETELY SECURE OR RELIABLE, OR (D) THAT DEFECTS IN OR ON THE 45 | SERVICES OR CONTENT WILL BE CORRECTED. YOU ASSUME ALL RISK OF PERSONAL INJURY, 46 | INCLUDING DEATH AND DAMAGE TO PERSONAL PROPERTY, SUSTAINED FROM USE OF SERVICES. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Udagram Image Filtering Application 2 | 3 | Udagram is a simple cloud application developed alongside the Udacity Cloud Developer Nanodegree. It allows users to register and log into a web client, post photos to the feed, and process photos using an image filtering microservice. 4 | 5 | The project is split into two parts: 6 | 1. Frontend - Angular web application built with Ionic Framework 7 | 2. Backend RESTful API - Node-Express application 8 | 9 | ## Getting Started 10 | > _tip_: it's recommended that you start with getting the backend API running since the frontend web application depends on the API. 11 | 12 | ### Prerequisite 13 | 1. The depends on the Node Package Manager (NPM). You will need to download and install Node from [https://nodejs.com/en/download](https://nodejs.org/en/download/). This will allow you to be able to run `npm` commands. 14 | 2. Environment variables will need to be set. These environment variables include database connection details that should not be hard-coded into the application code. 15 | 16 | #### Environment Script 17 | A file named `set_env.sh` has been prepared as an optional tool to help you configure these variables on your local development environment. 18 | 19 | We do _not_ want your credentials to be stored in git. After pulling this `starter` project, run the following command to tell git to stop tracking the script in git but keep it stored locally. This way, you can use the script for your convenience and reduce risk of exposing your credentials. 20 | `git rm --cached set_env.sh` 21 | 22 | Afterwards, we can prevent the file from being included in your solution by adding the file to our `.gitignore` file. 23 | 24 | ### 1. Database 25 | Create a PostgreSQL database either locally or on AWS RDS. The database is used to store the application's metadata. 26 | 27 | * We will need to use password authentication for this project. This means that a username and password is needed to authenticate and access the database. 28 | * The port number will need to be set as `5432`. This is the typical port that is used by PostgreSQL so it is usually set to this port by default. 29 | 30 | Once your database is set up, set the config values for environment variables prefixed with `POSTGRES_` in `set_env.sh`. 31 | * If you set up a local database, your `POSTGRES_HOST` is most likely `localhost` 32 | * If you set up an RDS database, your `POSTGRES_HOST` is most likely in the following format: `***.****.us-west-1.rds.amazonaws.com`. You can find this value in the AWS console's RDS dashboard. 33 | 34 | 35 | ### 2. S3 36 | Create an AWS S3 bucket. The S3 bucket is used to store images that are displayed in Udagram. 37 | 38 | Set the config values for environment variables prefixed with `AWS_` in `set_env.sh`. 39 | 40 | ### 3. Backend API 41 | Launch the backend API locally. The API is the application's interface to S3 and the database. 42 | 43 | * To download all the package dependencies, run the command from the directory `udagram-api/`: 44 | ```bash 45 | npm install . 46 | ``` 47 | * To run the application locally, run: 48 | ```bash 49 | npm run dev 50 | ``` 51 | * You can visit `http://localhost:8080/api/v0/feed` in your web browser to verify that the application is running. You should see a JSON payload. Feel free to play around with Postman to test the API's. 52 | 53 | ### 4. Frontend App 54 | Launch the frontend app locally. 55 | 56 | * To download all the package dependencies, run the command from the directory `udagram-frontend/`: 57 | ```bash 58 | npm install . 59 | ``` 60 | * Install Ionic Framework's Command Line tools for us to build and run the application: 61 | ```bash 62 | npm install -g ionic 63 | ``` 64 | * Prepare your application by compiling them into static files. 65 | ```bash 66 | ionic build 67 | ``` 68 | * Run the application locally using files created from the `ionic build` command. 69 | ```bash 70 | ionic serve 71 | ``` 72 | * You can visit `http://localhost:8100` in your web browser to verify that the application is running. You should see a web interface. 73 | 74 | ## Tips 75 | 1. Take a look at `udagram-api` -- does it look like we can divide it into two modules to be deployed as separate microservices? 76 | 2. The `.dockerignore` file is included for your convenience to not copy `node_modules`. Copying this over into a Docker container might cause issues if your local environment is a different operating system than the Docker image (ex. Windows or MacOS vs. Linux). 77 | 3. It's useful to "lint" your code so that changes in the codebase adhere to a coding standard. This helps alleviate issues when developers use different styles of coding. `eslint` has been set up for TypeScript in the codebase for you. To lint your code, run the following: 78 | ```bash 79 | npx eslint --ext .js,.ts src/ 80 | ``` 81 | To have your code fixed automatically, run 82 | ```bash 83 | npx eslint --ext .js,.ts src/ --fix 84 | ``` 85 | 4. `set_env.sh` is really for your backend application. Frontend applications have a different notion of how to store configurations. Configurations for the application endpoints can be configured inside of the `environments/environment.*ts` files. 86 | 5. In `set_env.sh`, environment variables are set with `export $VAR=value`. Setting it this way is not permanent; every time you open a new terminal, you will have to run `set_env.sh` to reconfigure your environment variables. To verify if your environment variable is set, you can check the variable with a command like `echo $POSTGRES_USERNAME`. 87 | -------------------------------------------------------------------------------- /screenshots/README.md: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | To help review your infrastructure, please include the following screenshots in this directory:: 3 | 4 | ## Deployment Pipeline 5 | * DockerHub showing containers that you have pushed 6 | * GitHub repository’s settings showing your Travis webhook (can be found in Settings - Webhook) 7 | * Travis CI showing a successful build and deploy job 8 | 9 | ## Kubernetes 10 | * To verify Kubernetes pods are deployed properly 11 | ```bash 12 | kubectl get pods 13 | ``` 14 | * To verify Kubernetes services are properly set up 15 | ```bash 16 | kubectl describe services 17 | ``` 18 | * To verify that you have horizontal scaling set against CPU usage 19 | ```bash 20 | kubectl describe hpa 21 | ``` 22 | * To verify that you have set up logging with a backend application 23 | ```bash 24 | kubectl logs {pod_name} 25 | ``` 26 | -------------------------------------------------------------------------------- /set_env.sh: -------------------------------------------------------------------------------- 1 | # This file is used for convenience of local development. 2 | # DO NOT STORE YOUR CREDENTIALS INTO GIT 3 | export POSTGRES_USERNAME=postgres 4 | export POSTGRES_PASSWORD=mypassword 5 | export POSTGRES_HOST=postgres.cr9bldgsf1j6.us-east-1.rds.amazonaws.com 6 | export POSTGRES_DB=postgres 7 | export AWS_BUCKET=mybucket1200798 8 | export AWS_REGION=us-east-1 9 | export AWS_PROFILE=default 10 | export JWT_SECRET=testing 11 | export URL=http://localhost:8100 12 | -------------------------------------------------------------------------------- /udagram-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "90c44738-86d0-440c-ac60-dcb7b70fb9e8", 4 | "name": "udagram-api", 5 | "description": "Feed and User API", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Public", 11 | "item": [ 12 | { 13 | "name": "/api/v0/feed", 14 | "event": [ 15 | { 16 | "listen": "test", 17 | "script": { 18 | "id": "1ad01629-c164-41ef-9d19-0a5fd13274af", 19 | "exec": [ 20 | "pm.test(\"Status code is 200\", function () {", 21 | " pm.response.to.have.status(200);", 22 | "});", 23 | "", 24 | "pm.test(\"The count is equal to the number of items rx\", function () {", 25 | " var jsonData = pm.response.json();", 26 | " pm.expect(jsonData.count).to.eql(jsonData.rows.length);", 27 | "});", 28 | "", 29 | "pm.test(\"Response time is less than 600ms\", function () {", 30 | " pm.expect(pm.response.responseTime).to.be.below(600);", 31 | "});" 32 | ], 33 | "type": "text/javascript" 34 | } 35 | } 36 | ], 37 | "request": { 38 | "method": "GET", 39 | "header": [], 40 | "body": { 41 | "mode": "raw", 42 | "raw": "" 43 | }, 44 | "url": { 45 | "raw": "{{host}}/api/v0/feed", 46 | "host": [ 47 | "{{host}}" 48 | ], 49 | "path": [ 50 | "api", 51 | "v0", 52 | "feed" 53 | ] 54 | }, 55 | "description": "Get all the items in the feed" 56 | }, 57 | "response": [] 58 | }, 59 | { 60 | "name": "/api/v0/feed/:id", 61 | "event": [ 62 | { 63 | "listen": "test", 64 | "script": { 65 | "id": "1ad01629-c164-41ef-9d19-0a5fd13274af", 66 | "exec": [ 67 | "pm.test(\"Status code is 200\", function () {", 68 | " pm.response.to.have.status(200);", 69 | "});", 70 | "", 71 | "pm.test(\"A single item is returned\", function () {", 72 | " var jsonData = pm.response.json();", 73 | " pm.expect(jsonData.id).to.not.eql(null);", 74 | "});", 75 | "", 76 | "pm.test(\"Response time is less than 200ms\", function () {", 77 | " pm.expect(pm.response.responseTime).to.be.below(200);", 78 | "});" 79 | ], 80 | "type": "text/javascript" 81 | } 82 | } 83 | ], 84 | "request": { 85 | "method": "GET", 86 | "header": [], 87 | "body": { 88 | "mode": "raw", 89 | "raw": "" 90 | }, 91 | "url": { 92 | "raw": "{{host}}/api/v0/feed/:id", 93 | "host": [ 94 | "{{host}}" 95 | ], 96 | "path": [ 97 | "api", 98 | "v0", 99 | "feed", 100 | ":id" 101 | ], 102 | "query": [ 103 | { 104 | "key": "id", 105 | "value": "4", 106 | "disabled": true 107 | } 108 | ], 109 | "variable": [ 110 | { 111 | "key": "id", 112 | "value": "3" 113 | } 114 | ] 115 | }, 116 | "description": "Request a specific feed item detail" 117 | }, 118 | "response": [ 119 | { 120 | "name": "/api/v0/feed/:id", 121 | "originalRequest": { 122 | "method": "GET", 123 | "header": [], 124 | "body": { 125 | "mode": "raw", 126 | "raw": "" 127 | }, 128 | "url": { 129 | "raw": "{{host}}/api/v0/feed/:id", 130 | "host": [ 131 | "{{host}}" 132 | ], 133 | "path": [ 134 | "api", 135 | "v0", 136 | "feed", 137 | ":id" 138 | ], 139 | "query": [ 140 | { 141 | "key": "id", 142 | "value": "4", 143 | "disabled": true 144 | } 145 | ], 146 | "variable": [ 147 | { 148 | "key": "id", 149 | "value": "3" 150 | } 151 | ] 152 | } 153 | }, 154 | "status": "OK", 155 | "code": 200, 156 | "_postman_previewlanguage": "json", 157 | "header": [ 158 | { 159 | "key": "X-Powered-By", 160 | "value": "Express" 161 | }, 162 | { 163 | "key": "Content-Type", 164 | "value": "application/json; charset=utf-8" 165 | }, 166 | { 167 | "key": "Content-Length", 168 | "value": "133" 169 | }, 170 | { 171 | "key": "ETag", 172 | "value": "W/\"85-gic7UchUXbyxmSqNsq7nx4+Eaas\"" 173 | }, 174 | { 175 | "key": "Date", 176 | "value": "Tue, 26 Mar 2019 15:07:32 GMT" 177 | }, 178 | { 179 | "key": "Connection", 180 | "value": "keep-alive" 181 | } 182 | ], 183 | "cookie": [], 184 | "body": "{\n \"id\": 3,\n \"caption\": \"hello0.5278862272947393\",\n \"url\": null,\n \"createdAt\": \"2019-03-26T14:16:58.442Z\",\n \"updatedAt\": \"2019-03-26T14:16:58.443Z\"\n}" 185 | } 186 | ] 187 | }, 188 | { 189 | "name": "/api/v0/user/auth valid registration", 190 | "event": [ 191 | { 192 | "listen": "test", 193 | "script": { 194 | "id": "4fa0ec55-4fb1-4eda-a7a3-08af62324ede", 195 | "exec": [ 196 | "pm.test(\"Status code is 200\", function () {", 197 | " pm.response.to.have.status(200);", 198 | "});", 199 | "", 200 | "pm.test(\"A single item is returned\", function () {", 201 | " var jsonData = pm.response.json();", 202 | " pm.expect(jsonData.id).to.not.eql(null);", 203 | "});", 204 | "", 205 | "pm.test(\"Response time is less than 200ms\", function () {", 206 | " pm.expect(pm.response.responseTime).to.be.below(200);", 207 | "});" 208 | ], 209 | "type": "text/javascript" 210 | } 211 | } 212 | ], 213 | "request": { 214 | "method": "POST", 215 | "header": [ 216 | { 217 | "key": "Content-Type", 218 | "name": "Content-Type", 219 | "type": "text", 220 | "value": "application/json" 221 | } 222 | ], 223 | "body": { 224 | "mode": "raw", 225 | "raw": "{\n\t\"email\":\"hello@gmail.com\",\n\t\"password\":\"fancypass\"\n}" 226 | }, 227 | "url": { 228 | "raw": "{{host}}/api/v0/users/auth", 229 | "host": [ 230 | "{{host}}" 231 | ], 232 | "path": [ 233 | "api", 234 | "v0", 235 | "users", 236 | "auth" 237 | ] 238 | } 239 | }, 240 | "response": [] 241 | }, 242 | { 243 | "name": "/api/v0/user/auth invalid registration", 244 | "event": [ 245 | { 246 | "listen": "test", 247 | "script": { 248 | "id": "b3737edc-f79e-4823-b2c5-e84e6359e173", 249 | "exec": [ 250 | "pm.test(\"Status code is 400\", function () {", 251 | " pm.response.to.have.status(400);", 252 | "});", 253 | "", 254 | "pm.test(\"auth is false and a message is included in the error body\", function () {", 255 | " var jsonData = pm.response.json();", 256 | " pm.expect(jsonData.id).to.not.eql(null);", 257 | " pm.expect(jsonData.message).to.not.eql(null)", 258 | "});", 259 | "", 260 | "pm.test(\"Response time is less than 200ms\", function () {", 261 | " pm.expect(pm.response.responseTime).to.be.below(200);", 262 | "});" 263 | ], 264 | "type": "text/javascript" 265 | } 266 | } 267 | ], 268 | "request": { 269 | "method": "POST", 270 | "header": [ 271 | { 272 | "key": "Content-Type", 273 | "name": "Content-Type", 274 | "value": "application/json", 275 | "type": "text" 276 | } 277 | ], 278 | "body": { 279 | "mode": "raw", 280 | "raw": "{\n \"password\": \"fancypass\"\n}" 281 | }, 282 | "url": { 283 | "raw": "{{host}}/api/v0/users/auth", 284 | "host": [ 285 | "{{host}}" 286 | ], 287 | "path": [ 288 | "api", 289 | "v0", 290 | "users", 291 | "auth" 292 | ] 293 | } 294 | }, 295 | "response": [] 296 | }, 297 | { 298 | "name": "/api/v0/user/auth/login valid", 299 | "event": [ 300 | { 301 | "listen": "test", 302 | "script": { 303 | "id": "5524b249-cd4d-4e75-91c4-b2f690d53d44", 304 | "exec": [ 305 | "pm.test(\"Status code is 200\", function () {", 306 | " pm.response.to.have.status(200);", 307 | "});", 308 | "", 309 | "pm.test(\"body includes an email and token\", function () {", 310 | " var jsonData = pm.response.json();", 311 | " pm.expect(jsonData.auth).to.eql(true);", 312 | " pm.expect(jsonData.user).to.not.eql(null);", 313 | " pm.expect(jsonData.token).to.not.eql(null)", 314 | " ", 315 | " pm.environment.set(\"token\", jsonData.token);", 316 | "});", 317 | "", 318 | "pm.test(\"Response time is less than 500ms\", function () {", 319 | " pm.expect(pm.response.responseTime).to.be.below(500);", 320 | "});" 321 | ], 322 | "type": "text/javascript" 323 | } 324 | } 325 | ], 326 | "request": { 327 | "method": "POST", 328 | "header": [ 329 | { 330 | "key": "Content-Type", 331 | "name": "Content-Type", 332 | "value": "application/json", 333 | "type": "text" 334 | } 335 | ], 336 | "body": { 337 | "mode": "raw", 338 | "raw": "{\n\t\"email\":\"hello@gmail.com\",\n\t\"password\":\"fancypass\"\n}" 339 | }, 340 | "url": { 341 | "raw": "{{host}}/api/v0/users/auth/login", 342 | "host": [ 343 | "{{host}}" 344 | ], 345 | "path": [ 346 | "api", 347 | "v0", 348 | "users", 349 | "auth", 350 | "login" 351 | ] 352 | } 353 | }, 354 | "response": [] 355 | }, 356 | { 357 | "name": "/api/v0/user/auth/login invalid", 358 | "event": [ 359 | { 360 | "listen": "test", 361 | "script": { 362 | "id": "41cd51df-9472-4d8d-9177-b4f54d6a8530", 363 | "exec": [ 364 | "pm.test(\"Status code is 400\", function () {", 365 | " pm.response.to.have.status(400);", 366 | "});", 367 | "", 368 | "pm.test(\"auth is false and a message is included in the error body\", function () {", 369 | " var jsonData = pm.response.json();", 370 | " pm.expect(jsonData.id).to.not.eql(null);", 371 | " pm.expect(jsonData.message).to.not.eql(null)", 372 | "});", 373 | "", 374 | "pm.test(\"Response time is less than 200ms\", function () {", 375 | " pm.expect(pm.response.responseTime).to.be.below(200);", 376 | "});" 377 | ], 378 | "type": "text/javascript" 379 | } 380 | } 381 | ], 382 | "request": { 383 | "method": "POST", 384 | "header": [ 385 | { 386 | "key": "Content-Type", 387 | "name": "Content-Type", 388 | "value": "application/json", 389 | "type": "text" 390 | } 391 | ], 392 | "body": { 393 | "mode": "raw", 394 | "raw": "{}" 395 | }, 396 | "url": { 397 | "raw": "{{host}}/api/v0/users/auth/login", 398 | "host": [ 399 | "{{host}}" 400 | ], 401 | "path": [ 402 | "api", 403 | "v0", 404 | "users", 405 | "auth", 406 | "login" 407 | ] 408 | } 409 | }, 410 | "response": [] 411 | } 412 | ] 413 | }, 414 | { 415 | "name": "Unauthorized", 416 | "item": [ 417 | { 418 | "name": "/api/v0/feed unauthorized", 419 | "event": [ 420 | { 421 | "listen": "test", 422 | "script": { 423 | "id": "1ad01629-c164-41ef-9d19-0a5fd13274af", 424 | "exec": [ 425 | "pm.test(\"Status code is 401\", function () {", 426 | " pm.response.to.have.status(401);", 427 | "});", 428 | "", 429 | "pm.test(\"Response time is less than 200ms\", function () {", 430 | " pm.expect(pm.response.responseTime).to.be.below(200);", 431 | "});" 432 | ], 433 | "type": "text/javascript" 434 | } 435 | } 436 | ], 437 | "request": { 438 | "method": "POST", 439 | "header": [], 440 | "body": { 441 | "mode": "raw", 442 | "raw": "" 443 | }, 444 | "url": { 445 | "raw": "{{host}}/api/v0/feed", 446 | "host": [ 447 | "{{host}}" 448 | ], 449 | "path": [ 450 | "api", 451 | "v0", 452 | "feed" 453 | ] 454 | }, 455 | "description": "Post a new item to the feed" 456 | }, 457 | "response": [] 458 | } 459 | ] 460 | }, 461 | { 462 | "name": "Authorized", 463 | "item": [ 464 | { 465 | "name": "/api/v0/feed invalid", 466 | "event": [ 467 | { 468 | "listen": "test", 469 | "script": { 470 | "id": "1ad01629-c164-41ef-9d19-0a5fd13274af", 471 | "exec": [ 472 | "pm.test(\"Status code is 400\", function () {", 473 | " pm.response.to.have.status(400);", 474 | "});", 475 | "", 476 | "pm.test(\"body includes a message\", function () {", 477 | " var jsonData = pm.response.json();", 478 | " pm.expect(jsonData.message).to.not.eql(null);", 479 | "});", 480 | "", 481 | "pm.test(\"Response time is less than 500ms\", function () {", 482 | " pm.expect(pm.response.responseTime).to.be.below(500);", 483 | "});" 484 | ], 485 | "type": "text/javascript" 486 | } 487 | } 488 | ], 489 | "request": { 490 | "method": "POST", 491 | "header": [ 492 | { 493 | "key": "Content-Type", 494 | "name": "Content-Type", 495 | "value": "application/json", 496 | "type": "text" 497 | } 498 | ], 499 | "body": { 500 | "mode": "raw", 501 | "raw": "{}" 502 | }, 503 | "url": { 504 | "raw": "{{host}}/api/v0/feed", 505 | "host": [ 506 | "{{host}}" 507 | ], 508 | "path": [ 509 | "api", 510 | "v0", 511 | "feed" 512 | ] 513 | }, 514 | "description": "Post a new item to the feed" 515 | }, 516 | "response": [] 517 | }, 518 | { 519 | "name": "/api/v0/feed authorized Copy", 520 | "event": [ 521 | { 522 | "listen": "test", 523 | "script": { 524 | "id": "1ad01629-c164-41ef-9d19-0a5fd13274af", 525 | "exec": [ 526 | "pm.test(\"Status code is 201\", function () {", 527 | " pm.response.to.have.status(201);", 528 | "});", 529 | "", 530 | "pm.test(\"The post returns a new item with an id\", function () {", 531 | " var jsonData = pm.response.json();", 532 | " pm.expect(jsonData.id).to.not.eql(null);", 533 | "});", 534 | "", 535 | "pm.test(\"Response time is less than 200ms\", function () {", 536 | " pm.expect(pm.response.responseTime).to.be.below(200);", 537 | "});" 538 | ], 539 | "type": "text/javascript" 540 | } 541 | } 542 | ], 543 | "request": { 544 | "method": "POST", 545 | "header": [ 546 | { 547 | "key": "Content-Type", 548 | "name": "Content-Type", 549 | "value": "application/json", 550 | "type": "text" 551 | } 552 | ], 553 | "body": { 554 | "mode": "raw", 555 | "raw": "{\n \"caption\": \"Hello\",\n \"url\": \"test.jpg\"\n}" 556 | }, 557 | "url": { 558 | "raw": "{{host}}/api/v0/feed", 559 | "host": [ 560 | "{{host}}" 561 | ], 562 | "path": [ 563 | "api", 564 | "v0", 565 | "feed" 566 | ] 567 | }, 568 | "description": "Post a new item to the feed" 569 | }, 570 | "response": [] 571 | } 572 | ], 573 | "auth": { 574 | "type": "bearer", 575 | "bearer": [ 576 | { 577 | "key": "token", 578 | "value": "{{token}}", 579 | "type": "string" 580 | } 581 | ] 582 | }, 583 | "event": [ 584 | { 585 | "listen": "prerequest", 586 | "script": { 587 | "id": "65e7c108-0985-4ca7-9613-0898f005cf76", 588 | "type": "text/javascript", 589 | "exec": [ 590 | "" 591 | ] 592 | } 593 | }, 594 | { 595 | "listen": "test", 596 | "script": { 597 | "id": "cf28f76e-ffbb-4199-b636-1c080a86e465", 598 | "type": "text/javascript", 599 | "exec": [ 600 | "" 601 | ] 602 | } 603 | } 604 | ] 605 | } 606 | ], 607 | "event": [ 608 | { 609 | "listen": "prerequest", 610 | "script": { 611 | "id": "bc5d9c7a-7a6b-49ed-af16-206e17e8732f", 612 | "type": "text/javascript", 613 | "exec": [ 614 | "" 615 | ] 616 | } 617 | }, 618 | { 619 | "listen": "test", 620 | "script": { 621 | "id": "a7cb0174-2461-42e2-979e-1b5922eea0fe", 622 | "type": "text/javascript", 623 | "exec": [ 624 | "" 625 | ] 626 | } 627 | } 628 | ], 629 | "variable": [ 630 | { 631 | "id": "3e3ea131-5328-4f50-8fe1-516e3995917d", 632 | "key": "host", 633 | "value": "http://localhost:8080", 634 | "type": "string" 635 | } 636 | ] 637 | } -------------------------------------------------------------------------------- /udagram-api/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /udagram-api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "globals": { 13 | "Atomics": "readonly", 14 | "SharedArrayBuffer": "readonly" 15 | }, 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module" 20 | }, 21 | "plugins": [ 22 | "@typescript-eslint" 23 | ], 24 | "rules": { 25 | } 26 | } -------------------------------------------------------------------------------- /udagram-api/.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | postgres_dev/ 32 | logfile 33 | 34 | .DS_Store 35 | Thumbs.db 36 | UserInterfaceState.xcuserstate 37 | node_modules 38 | venv/ 39 | # Elastic Beanstalk Files 40 | .elasticbeanstalk/* 41 | !.elasticbeanstalk/*.cfg.yml 42 | !.elasticbeanstalk/*.global.yml 43 | -------------------------------------------------------------------------------- /udagram-api/.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm=true -------------------------------------------------------------------------------- /udagram-api/mock/xander0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-api/mock/xander0.jpg -------------------------------------------------------------------------------- /udagram-api/mock/xander1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-api/mock/xander1.jpg -------------------------------------------------------------------------------- /udagram-api/mock/xander2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-api/mock/xander2.jpg -------------------------------------------------------------------------------- /udagram-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "udagram-api", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "src/server.js", 6 | "scripts": { 7 | "start": "node .", 8 | "tsc": "tsc", 9 | "dev": "ts-node-dev --respawn --transpile-only ./src/server.ts", 10 | "prod": "tsc && node ./www/server.js", 11 | "clean": "rm -rf www/ || true", 12 | "build": "npm run clean && tsc && cp -rf src/config www/config && cp .npmrc www/.npmrc && cp package.json www/package.json && cd www && zip -r Archive.zip . && cd ..", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [], 16 | "author": "Gabriel Ruttner", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@eslint/config-array": "^0.19.2", 20 | "@eslint/object-schema": "^2.1.6", 21 | "@types/bcrypt": "^5.0.2", 22 | "@types/jsonwebtoken": "^9.0.5", 23 | "@aws-sdk/client-s3": "^3.511.0", 24 | "@aws-sdk/s3-request-presigner": "^3.511.0", 25 | "@aws-sdk/credential-providers": "^3.511.0", 26 | "bcrypt": "^5.1.1", 27 | "body-parser": "^1.20.2", 28 | "cors": "^2.8.5", 29 | "email-validator": "^2.0.4", 30 | "express": "^4.18.3", 31 | "glob": "^11.0.1", 32 | "jsonwebtoken": "^9.0.2", 33 | "lru-cache": "^11.0.2", 34 | "pg": "^8.11.3", 35 | "reflect-metadata": "^0.2.1", 36 | "rimraf": "^6.0.1", 37 | "sequelize": "^6.37.1", 38 | "sequelize-typescript": "^2.1.6", 39 | "superagent": "^10.1.1" 40 | }, 41 | "devDependencies": { 42 | "@types/bluebird": "^3.5.42", 43 | "@types/cors": "^2.8.17", 44 | "@types/express": "^4.17.21", 45 | "@types/node": "^20.11.24", 46 | "@types/sequelize": "^4.28.20", 47 | "@types/validator": "^13.11.9", 48 | "@typescript-eslint/eslint-plugin": "^7.1.1", 49 | "@typescript-eslint/parser": "^7.1.1", 50 | "chai": "^5.1.0", 51 | "chai-http": "^4.4.0", 52 | "eslint": "^8.57.0", 53 | "eslint-config-google": "^0.14.0", 54 | "mocha": "^10.3.0", 55 | "ts-node-dev": "^2.0.0", 56 | "typescript": "^5.3.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /udagram-api/src/aws.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { fromIni } from "@aws-sdk/credential-providers"; 3 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 4 | import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; 5 | import { config } from './config/config'; 6 | 7 | // Configure AWS 8 | const credentials = fromIni({ profile: config.aws_profile }); 9 | 10 | // Create S3 client 11 | export const s3Client = new S3Client({ 12 | region: config.aws_region, 13 | credentials: credentials, 14 | }); 15 | 16 | // Generates an AWS signed URL for retrieving objects 17 | export async function getGetSignedUrl(key: string): Promise { 18 | const command = new GetObjectCommand({ 19 | Bucket: config.aws_media_bucket, 20 | Key: key 21 | }); 22 | 23 | const signedUrl = await getSignedUrl(s3Client, command, { 24 | expiresIn: 60 * 5 // 5 minutes 25 | }); 26 | 27 | return signedUrl; 28 | } 29 | 30 | // Generates an AWS signed URL for uploading objects 31 | export async function getPutSignedUrl(key: string): Promise { 32 | const command = new PutObjectCommand({ 33 | Bucket: config.aws_media_bucket, 34 | Key: key 35 | }); 36 | 37 | const signedUrl = await getSignedUrl(s3Client, command, { 38 | expiresIn: 60 * 5 // 5 minutes 39 | }); 40 | 41 | return signedUrl; 42 | } -------------------------------------------------------------------------------- /udagram-api/src/config/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | 'username': process.env.POSTGRES_USERNAME, 3 | 'password': process.env.POSTGRES_PASSWORD, 4 | 'database': process.env.POSTGRES_DB, 5 | 'host': process.env.POSTGRES_HOST, 6 | 'dialect': 'postgres', 7 | 'aws_region': process.env.AWS_REGION, 8 | 'aws_profile': process.env.AWS_PROFILE, 9 | 'aws_media_bucket': process.env.AWS_BUCKET, 10 | 'url': process.env.URL, 11 | 'jwt': { 12 | 'secret': process.env.JWT_SECRET, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/feed/models/FeedItem.ts: -------------------------------------------------------------------------------- 1 | import {Table, Column, Model, CreatedAt, UpdatedAt} from 'sequelize-typescript'; 2 | 3 | 4 | @Table 5 | export class FeedItem extends Model { 6 | @Column 7 | public caption!: string; 8 | 9 | @Column 10 | public url!: string; 11 | 12 | @Column 13 | @CreatedAt 14 | public createdAt: Date = new Date(); 15 | 16 | @Column 17 | @UpdatedAt 18 | public updatedAt: Date = new Date(); 19 | } 20 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/feed/routes/feed.router.ts: -------------------------------------------------------------------------------- 1 | import {Router, Request, Response} from 'express'; 2 | import {FeedItem} from '../models/FeedItem'; 3 | import {NextFunction} from 'connect'; 4 | import * as jwt from 'jsonwebtoken'; 5 | import * as AWS from '../../../../aws'; 6 | import * as c from '../../../../config/config'; 7 | 8 | const router: Router = Router(); 9 | 10 | export function requireAuth(req: Request, res: Response, next: NextFunction) { 11 | 12 | if (!req.headers || !req.headers.authorization) { 13 | return res.status(401).send({message: 'No authorization headers.'}); 14 | } 15 | 16 | const tokenBearer = req.headers.authorization.split(' '); 17 | if (tokenBearer.length != 2) { 18 | return res.status(401).send({message: 'Malformed token.'}); 19 | } 20 | 21 | const token = tokenBearer[1]; 22 | return jwt.verify(token, c.config.jwt.secret, (err, decoded) => { 23 | if (err) { 24 | return res.status(500).send({auth: false, message: 'Failed to authenticate.'}); 25 | } 26 | return next(); 27 | }); 28 | } 29 | 30 | // Get all feed items 31 | router.get('/', async (req: Request, res: Response) => { 32 | const items = await FeedItem.findAndCountAll({order: [['id', 'DESC']]}); 33 | // Map items to include signed URLs 34 | const itemsWithUrls = await Promise.all(items.rows.map(async (item) => { 35 | if (item.url) { 36 | item.url = await AWS.getGetSignedUrl(item.url); 37 | } 38 | return item; 39 | })); 40 | res.send({count: items.count, rows: itemsWithUrls}); 41 | }); 42 | 43 | // Get a feed resource 44 | router.get('/:id', 45 | async (req: Request, res: Response) => { 46 | const {id} = req.params; 47 | const item = await FeedItem.findByPk(id); 48 | res.send(item); 49 | }); 50 | 51 | // Get a signed url to put a new item in the bucket 52 | router.get('/signed-url/:fileName', 53 | requireAuth, 54 | async (req: Request, res: Response) => { 55 | const {fileName} = req.params; 56 | const url = await AWS.getPutSignedUrl(fileName); 57 | res.status(201).send({url: url}); 58 | }); 59 | 60 | // Create feed with metadata 61 | router.post('/', 62 | requireAuth, 63 | async (req: Request, res: Response) => { 64 | const caption = req.body.caption; 65 | const fileName = req.body.url; // same as S3 key name 66 | if (!caption) { 67 | return res.status(400).send({message: 'Caption is required or malformed.'}); 68 | } 69 | 70 | if (!fileName) { 71 | return res.status(400).send({message: 'File url is required.'}); 72 | } 73 | 74 | const item = await new FeedItem({ 75 | caption: caption, 76 | url: fileName, 77 | }); 78 | 79 | const savedItem = await item.save(); 80 | savedItem.url = await AWS.getGetSignedUrl(savedItem.url); 81 | res.status(201).send(savedItem); 82 | }); 83 | 84 | export const FeedRouter: Router = router; 85 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/index.router.ts: -------------------------------------------------------------------------------- 1 | import {Router, Request, Response} from 'express'; 2 | import {FeedRouter} from './feed/routes/feed.router'; 3 | import {UserRouter} from './users/routes/user.router'; 4 | 5 | const router: Router = Router(); 6 | 7 | router.use('/feed', FeedRouter); 8 | router.use('/users', UserRouter); 9 | 10 | router.get('/', async (req: Request, res: Response) => { 11 | res.send(`V0`); 12 | }); 13 | 14 | export const IndexRouter: Router = router; 15 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/model.index.ts: -------------------------------------------------------------------------------- 1 | import {FeedItem} from './feed/models/FeedItem'; 2 | import {User} from './users/models/User'; 3 | 4 | 5 | export const V0_USER_MODELS = [User]; 6 | export const V0_FEED_MODELS = [FeedItem]; 7 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/users/models/User.ts: -------------------------------------------------------------------------------- 1 | import {Table, Column, Model, PrimaryKey, CreatedAt, UpdatedAt} from 'sequelize-typescript'; 2 | 3 | @Table 4 | export class User extends Model { 5 | @PrimaryKey 6 | @Column 7 | public email!: string; 8 | 9 | @Column 10 | public passwordHash!: string; 11 | 12 | @Column 13 | @CreatedAt 14 | public createdAt: Date = new Date(); 15 | 16 | @Column 17 | @UpdatedAt 18 | public updatedAt: Date = new Date(); 19 | 20 | short() { 21 | return { 22 | email: this.email, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/users/routes/auth.router.ts: -------------------------------------------------------------------------------- 1 | import {Router, Request, Response} from 'express'; 2 | 3 | import {User} from '../models/User'; 4 | import * as c from '../../../../config/config'; 5 | 6 | import * as bcrypt from 'bcrypt'; 7 | import * as jwt from 'jsonwebtoken'; 8 | import {NextFunction} from 'connect'; 9 | 10 | import * as EmailValidator from 'email-validator'; 11 | import {config} from 'bluebird'; 12 | 13 | const router: Router = Router(); 14 | 15 | 16 | async function generatePassword(plainTextPassword: string): Promise { 17 | const saltRounds = 10; 18 | const salt = await bcrypt.genSalt(saltRounds); 19 | return await bcrypt.hash(plainTextPassword, salt); 20 | } 21 | 22 | async function comparePasswords(plainTextPassword: string, hash: string): Promise { 23 | return await bcrypt.compare(plainTextPassword, hash); 24 | } 25 | 26 | function generateJWT(user: User): string { 27 | return jwt.sign(user.short(), c.config.jwt.secret); 28 | } 29 | 30 | export function requireAuth(req: Request, res: Response, next: NextFunction) { 31 | if (!req.headers || !req.headers.authorization) { 32 | return res.status(401).send({message: 'No authorization headers.'}); 33 | } 34 | 35 | const tokenBearer = req.headers.authorization.split(' '); 36 | if (tokenBearer.length != 2) { 37 | return res.status(401).send({message: 'Malformed token.'}); 38 | } 39 | 40 | const token = tokenBearer[1]; 41 | return jwt.verify(token, c.config.jwt.secret, (err, decoded) => { 42 | if (err) { 43 | return res.status(500).send({auth: false, message: 'Failed to authenticate.'}); 44 | } 45 | return next(); 46 | }); 47 | } 48 | 49 | router.get('/verification', 50 | requireAuth, 51 | async (req: Request, res: Response) => { 52 | return res.status(200).send({auth: true, message: 'Authenticated.'}); 53 | }); 54 | 55 | router.post('/login', async (req: Request, res: Response) => { 56 | const email = req.body.email; 57 | const password = req.body.password; 58 | 59 | if (!email || !EmailValidator.validate(email)) { 60 | return res.status(400).send({auth: false, message: 'Email is required or malformed.'}); 61 | } 62 | 63 | if (!password) { 64 | return res.status(400).send({auth: false, message: 'Password is required.'}); 65 | } 66 | 67 | const user = await User.findByPk(email); 68 | if (!user) { 69 | return res.status(401).send({auth: false, message: 'User was not found..'}); 70 | } 71 | 72 | const authValid = await comparePasswords(password, user.passwordHash); 73 | 74 | if (!authValid) { 75 | return res.status(401).send({auth: false, message: 'Password was invalid.'}); 76 | } 77 | 78 | const jwt = generateJWT(user); 79 | 80 | res.status(200).send({auth: true, token: jwt, user: user.short()}); 81 | }); 82 | 83 | 84 | router.post('/', async (req: Request, res: Response) => { 85 | const email = req.body.email; 86 | const plainTextPassword = req.body.password; 87 | 88 | if (!email || !EmailValidator.validate(email)) { 89 | return res.status(400).send({auth: false, message: 'Email is missing or malformed.'}); 90 | } 91 | 92 | if (!plainTextPassword) { 93 | return res.status(400).send({auth: false, message: 'Password is required.'}); 94 | } 95 | 96 | const user = await User.findByPk(email); 97 | if (user) { 98 | return res.status(422).send({auth: false, message: 'User already exists.'}); 99 | } 100 | 101 | const generatedHash = await generatePassword(plainTextPassword); 102 | 103 | const newUser = await new User({ 104 | email: email, 105 | passwordHash: generatedHash, 106 | }); 107 | 108 | const savedUser = await newUser.save(); 109 | 110 | 111 | const jwt = generateJWT(savedUser); 112 | res.status(201).send({token: jwt, user: savedUser.short()}); 113 | }); 114 | 115 | router.get('/', async (req: Request, res: Response) => { 116 | res.send('auth'); 117 | }); 118 | 119 | export const AuthRouter: Router = router; 120 | -------------------------------------------------------------------------------- /udagram-api/src/controllers/v0/users/routes/user.router.ts: -------------------------------------------------------------------------------- 1 | import {Router, Request, Response} from 'express'; 2 | 3 | import {User} from '../models/User'; 4 | import {AuthRouter} from './auth.router'; 5 | 6 | const router: Router = Router(); 7 | 8 | router.use('/auth', AuthRouter); 9 | 10 | router.get('/'); 11 | 12 | router.get('/:id', async (req: Request, res: Response) => { 13 | const {id} = req.params; 14 | const item = await User.findByPk(id); 15 | res.send(item); 16 | }); 17 | 18 | export const UserRouter: Router = router; 19 | -------------------------------------------------------------------------------- /udagram-api/src/migrations/20190325-create-feed-item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('FeedItem', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | caption: { 12 | type: Sequelize.STRING, 13 | }, 14 | url: { 15 | type: Sequelize.STRING, 16 | }, 17 | createdAt: { 18 | allowNull: false, 19 | type: Sequelize.DATE, 20 | }, 21 | updatedAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE, 24 | }, 25 | }); 26 | }, 27 | down: (queryInterface, Sequelize) => { 28 | return queryInterface.dropTable('FeedItem'); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /udagram-api/src/migrations/20190328-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('User', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | type: Sequelize.INTEGER, 9 | }, 10 | email: { 11 | type: Sequelize.STRING, 12 | primaryKey: true, 13 | }, 14 | passwordHash: { 15 | type: Sequelize.STRING, 16 | }, 17 | createdAt: { 18 | allowNull: false, 19 | type: Sequelize.DATE, 20 | }, 21 | updatedAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE, 24 | }, 25 | }); 26 | }, 27 | down: (queryInterface, Sequelize) => { 28 | return queryInterface.dropTable('User'); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /udagram-api/src/sequelize.ts: -------------------------------------------------------------------------------- 1 | import {Sequelize} from 'sequelize-typescript'; 2 | import {config} from './config/config'; 3 | 4 | 5 | export const sequelize = new Sequelize({ 6 | 'username': config.username, 7 | 'password': config.password, 8 | 'database': config.database, 9 | 'host': config.host, 10 | 11 | 'dialect': config.dialect, 12 | dialectOptions: { 13 | ssl: { 14 | require: true, 15 | rejectUnauthorized: false 16 | } 17 | }, 18 | 'storage': ':memory:', 19 | }); 20 | -------------------------------------------------------------------------------- /udagram-api/src/server.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors'; 2 | import express from 'express'; 3 | import {sequelize} from './sequelize'; 4 | 5 | import {IndexRouter} from './controllers/v0/index.router'; 6 | 7 | import bodyParser from 'body-parser'; 8 | import {config} from './config/config'; 9 | import {V0_FEED_MODELS, V0_USER_MODELS} from './controllers/v0/model.index'; 10 | 11 | 12 | (async () => { 13 | await sequelize.addModels(V0_FEED_MODELS); 14 | await sequelize.addModels(V0_USER_MODELS); 15 | 16 | console.debug("Initialize database connection..."); 17 | await sequelize.sync(); 18 | 19 | const app = express(); 20 | const port = process.env.PORT || 8080; 21 | 22 | app.use(bodyParser.json()); 23 | 24 | // We set the CORS origin to * so that we don't need to 25 | // worry about the complexities of CORS this lesson. It's 26 | // something that will be covered in the next course. 27 | app.use(cors({ 28 | allowedHeaders: [ 29 | 'Origin', 'X-Requested-With', 30 | 'Content-Type', 'Accept', 31 | 'X-Access-Token', 'Authorization', 32 | ], 33 | methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE', 34 | preflightContinue: true, 35 | origin: '*', 36 | })); 37 | 38 | app.use('/api/v0/', IndexRouter); 39 | 40 | // Root URI call 41 | app.get( '/', async ( req, res ) => { 42 | res.send( '/api/v0/' ); 43 | } ); 44 | 45 | 46 | // Start the Server 47 | app.listen( port, () => { 48 | console.log( `server running ${config.url}` ); 49 | console.log( `press CTRL+C to stop server` ); 50 | } ); 51 | })(); 52 | -------------------------------------------------------------------------------- /udagram-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./www", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | "strictNullChecks": false, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | "baseUrl": "/", /* Base directory to resolve non-absolute module names. */ 42 | "paths": { 43 | "*": [ 44 | "node_modules/*" 45 | ] 46 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | }, 64 | "include": [ 65 | "src/**/*" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /udagram-api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-spacing": true, 20 | "indent": [ 21 | true, 22 | "spaces" 23 | ], 24 | "interface-over-type-literal": true, 25 | "label-position": true, 26 | "max-line-length": [ 27 | true, 28 | 140 29 | ], 30 | "member-access": false, 31 | "member-ordering": [ 32 | true, 33 | { 34 | "order": [ 35 | "static-field", 36 | "instance-field", 37 | "static-method", 38 | "instance-method" 39 | ] 40 | } 41 | ], 42 | "no-arg": true, 43 | "no-bitwise": true, 44 | "no-console": [ 45 | true, 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-construct": true, 53 | "no-debugger": true, 54 | "no-duplicate-super": true, 55 | "no-empty": false, 56 | "no-empty-interface": true, 57 | "no-eval": true, 58 | "no-inferrable-types": [ 59 | true, 60 | "ignore-params" 61 | ], 62 | "no-misused-new": true, 63 | "no-non-null-assertion": true, 64 | "no-shadowed-variable": true, 65 | "no-string-literal": false, 66 | "no-string-throw": true, 67 | "no-switch-case-fall-through": true, 68 | "no-trailing-whitespace": true, 69 | "no-unnecessary-initializer": true, 70 | "no-unused-expression": true, 71 | "no-use-before-declare": true, 72 | "no-var-keyword": true, 73 | "object-literal-sort-keys": false, 74 | "one-line": [ 75 | true, 76 | "check-open-brace", 77 | "check-catch", 78 | "check-else", 79 | "check-whitespace" 80 | ], 81 | "prefer-const": true, 82 | "quotemark": [ 83 | true, 84 | "single" 85 | ], 86 | "radix": true, 87 | "semicolon": [ 88 | true, 89 | "always" 90 | ], 91 | "triple-equals": [ 92 | true, 93 | "allow-null-check" 94 | ], 95 | "typedef-whitespace": [ 96 | true, 97 | { 98 | "call-signature": "nospace", 99 | "index-signature": "nospace", 100 | "parameter": "nospace", 101 | "property-declaration": "nospace", 102 | "variable-declaration": "nospace" 103 | } 104 | ], 105 | "unified-signatures": true, 106 | "variable-name": false, 107 | "whitespace": [ 108 | true, 109 | "check-branch", 110 | "check-decl", 111 | "check-operator", 112 | "check-separator", 113 | "check-type" 114 | ], 115 | "directive-selector": [ 116 | true, 117 | "attribute", 118 | "app", 119 | "camelCase" 120 | ], 121 | "component-selector": [ 122 | true, 123 | "element", 124 | "app", 125 | "page", 126 | "kebab-case" 127 | ], 128 | "no-output-on-prefix": true, 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "directive-class-suffix": true 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /udagram-frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2018, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "@typescript-eslint" 22 | ], 23 | "rules": { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /udagram-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | 32 | .DS_Store 33 | Thumbs.db 34 | UserInterfaceState.xcuserstate 35 | node_modules 36 | -------------------------------------------------------------------------------- /udagram-frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "www", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": ["zone.js"], 24 | "tsConfig": "src/tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | { 28 | "glob": "**/*", 29 | "input": "src/assets", 30 | "output": "assets" 31 | }, 32 | { 33 | "glob": "**/*.svg", 34 | "input": "node_modules/ionicons/dist/ionicons/svg", 35 | "output": "./svg" 36 | } 37 | ], 38 | "styles": [ 39 | "src/theme/variables.scss", 40 | "src/global.scss" 41 | ], 42 | "scripts": [] 43 | }, 44 | "configurations": { 45 | "production": { 46 | "budgets": [ 47 | { 48 | "type": "initial", 49 | "maximumWarning": "2mb", 50 | "maximumError": "5mb" 51 | }, 52 | { 53 | "type": "anyComponentStyle", 54 | "maximumWarning": "2kb", 55 | "maximumError": "4kb" 56 | } 57 | ], 58 | "fileReplacements": [ 59 | { 60 | "replace": "src/environments/environment.ts", 61 | "with": "src/environments/environment.prod.ts" 62 | } 63 | ], 64 | "outputHashing": "all", 65 | "optimization": true, 66 | "sourceMap": false, 67 | "namedChunks": false, 68 | "extractLicenses": true, 69 | "vendorChunk": false, 70 | "buildOptimizer": true 71 | }, 72 | "development": { 73 | "buildOptimizer": false, 74 | "optimization": false, 75 | "vendorChunk": true, 76 | "extractLicenses": false, 77 | "sourceMap": true, 78 | "namedChunks": true 79 | }, 80 | "ci": { 81 | "progress": false 82 | } 83 | }, 84 | "defaultConfiguration": "production" 85 | }, 86 | "serve": { 87 | "builder": "@angular-devkit/build-angular:dev-server", 88 | "configurations": { 89 | "production": { 90 | "buildTarget": "app:build:production" 91 | }, 92 | "development": { 93 | "buildTarget": "app:build:development" 94 | }, 95 | "ci": { 96 | "progress": false 97 | } 98 | }, 99 | "defaultConfiguration": "development" 100 | }, 101 | "extract-i18n": { 102 | "builder": "@angular-devkit/build-angular:extract-i18n", 103 | "options": { 104 | "buildTarget": "app:build" 105 | } 106 | }, 107 | "test": { 108 | "builder": "@angular-devkit/build-angular:karma", 109 | "options": { 110 | "main": "src/test.ts", 111 | "polyfills": ["zone.js", "zone.js/testing"], 112 | "tsConfig": "src/tsconfig.spec.json", 113 | "karmaConfig": "src/karma.conf.js", 114 | "styles": [], 115 | "scripts": [], 116 | "assets": [ 117 | { 118 | "glob": "favicon.ico", 119 | "input": "src/", 120 | "output": "/" 121 | }, 122 | { 123 | "glob": "**/*", 124 | "input": "src/assets", 125 | "output": "/assets" 126 | } 127 | ] 128 | }, 129 | "configurations": { 130 | "ci": { 131 | "progress": false, 132 | "watch": false 133 | } 134 | } 135 | }, 136 | "ionic-cordova-build": { 137 | "builder": "@ionic/angular-toolkit:cordova-build", 138 | "options": { 139 | "buildTarget": "app:build" 140 | }, 141 | "configurations": { 142 | "production": { 143 | "buildTarget": "app:build:production" 144 | } 145 | } 146 | }, 147 | "ionic-cordova-serve": { 148 | "builder": "@ionic/angular-toolkit:cordova-serve", 149 | "options": { 150 | "cordovaBuildTarget": "app:ionic-cordova-build", 151 | "buildTarget": "app:serve" 152 | }, 153 | "configurations": { 154 | "production": { 155 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 156 | "buildTarget": "app:serve:production" 157 | } 158 | } 159 | } 160 | } 161 | } 162 | }, 163 | "cli": { 164 | "analytics": false 165 | } 166 | } -------------------------------------------------------------------------------- /udagram-frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /udagram-frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | describe('default screen', () => { 10 | beforeEach(() => { 11 | page.navigateTo('/home'); 12 | }); 13 | it('should have a title saying Home', () => { 14 | page.getPageOneTitleText().then(title => { 15 | expect(title).toEqual('Home'); 16 | }); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /udagram-frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(destination) { 5 | return browser.get(destination); 6 | } 7 | 8 | getTitle() { 9 | return browser.getTitle(); 10 | } 11 | 12 | getPageOneTitleText() { 13 | return element(by.tagName('app-home')).element(by.deepCss('ion-title')).getText(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /udagram-frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /udagram-frontend/ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "udacity-c2-frontend", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /udagram-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "udagram-frontend", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "17.3.12", 17 | "@angular/core": "17.3.12", 18 | "@angular/forms": "17.3.12", 19 | "@angular/platform-browser": "17.3.12", 20 | "@angular/platform-browser-dynamic": "17.3.12", 21 | "@angular/router": "17.3.12", 22 | "@capacitor/core": "^5.7.2", 23 | "@capacitor/splash-screen": "^5.0.7", 24 | "@capacitor/status-bar": "^5.0.7", 25 | "@ionic/angular": "^7.7.3", 26 | "rxjs": "~7.8.1", 27 | "zone.js": "~0.14.4" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "17.3.12", 31 | "@angular/cli": "17.3.12", 32 | "@angular/compiler": "17.3.12", 33 | "@angular/compiler-cli": "17.3.12", 34 | "@angular/language-service": "17.3.12", 35 | "@capacitor/cli": "^5.7.2", 36 | "@ionic/angular-toolkit": "^10.0.0", 37 | "@types/jasmine": "~5.1.4", 38 | "@types/node": "^20.11.24", 39 | "jasmine-core": "~5.1.2", 40 | "karma": "~6.4.3", 41 | "karma-chrome-launcher": "~3.2.0", 42 | "karma-coverage-istanbul-reporter": "~3.0.3", 43 | "karma-jasmine": "~5.1.0", 44 | "karma-jasmine-html-reporter": "^2.1.0", 45 | "ts-node": "~10.9.2", 46 | "typescript": "~5.4.2" 47 | }, 48 | "description": "An Ionic project" 49 | } 50 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/api/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpRequest, HttpEvent } from '@angular/common/http'; 3 | import { environment } from '../../environments/environment'; 4 | import { map, catchError } from 'rxjs/operators'; 5 | import { lastValueFrom } from 'rxjs'; 6 | 7 | const API_HOST = environment.apiHost; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ApiService { 13 | httpOptions = { 14 | headers: new HttpHeaders({'Content-Type': 'application/json'}) 15 | }; 16 | 17 | token: string; 18 | 19 | constructor(private http: HttpClient) { 20 | } 21 | 22 | static handleError(error: Error) { 23 | alert(error.message); 24 | } 25 | 26 | static extractData(res: HttpEvent) { 27 | const body = res; 28 | return body || { }; 29 | } 30 | 31 | setAuthToken(token) { 32 | this.token = token; 33 | this.httpOptions.headers = new HttpHeaders({ 34 | 'Content-Type': 'application/json', 35 | 'Authorization': `jwt ${token}` 36 | }); 37 | } 38 | 39 | get(endpoint): Promise { 40 | const url = `${API_HOST}${endpoint}`; 41 | const req = this.http.get(url, this.httpOptions).pipe(map(ApiService.extractData)); 42 | 43 | return req 44 | .toPromise() 45 | .catch((e) => { 46 | ApiService.handleError(e); 47 | throw e; 48 | }); 49 | } 50 | 51 | post(endpoint, data): Promise { 52 | const url = `${API_HOST}${endpoint}`; 53 | return this.http.post>(url, data, this.httpOptions) 54 | .toPromise() 55 | .catch((e) => { 56 | ApiService.handleError(e); 57 | throw e; 58 | }); 59 | } 60 | 61 | async upload(endpoint: string, file: File, payload: any): Promise { 62 | try { 63 | // Get signed URL with JWT auth 64 | const signedUrlResponse = await this.get(`${endpoint}/signed-url/${file.name}`); 65 | const signed_url = signedUrlResponse.url; 66 | 67 | // Create a simple PUT request with minimal headers 68 | const uploadResponse = await lastValueFrom( 69 | this.http.put(signed_url, file, { 70 | headers: new HttpHeaders({ 71 | 'Content-Type': file.type 72 | }), 73 | observe: 'response' 74 | }) 75 | ); 76 | 77 | if (uploadResponse.status === 200) { 78 | // If upload successful, post the metadata 79 | return await this.post(endpoint, payload); 80 | } else { 81 | throw new Error('Upload failed'); 82 | } 83 | } catch (error) { 84 | console.error('Upload error:', error); 85 | throw error; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, async } from '@angular/core/testing'; 3 | 4 | import { Platform } from '@ionic/angular'; 5 | import { SplashScreen } from '@ionic-native/splash-screen/ngx'; 6 | import { StatusBar } from '@ionic-native/status-bar/ngx'; 7 | import { RouterTestingModule } from '@angular/router/testing'; 8 | 9 | import { AppComponent } from './app.component'; 10 | 11 | describe('AppComponent', () => { 12 | 13 | let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy; 14 | 15 | beforeEach(async(() => { 16 | statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']); 17 | splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']); 18 | platformReadySpy = Promise.resolve(); 19 | platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy }); 20 | 21 | TestBed.configureTestingModule({ 22 | declarations: [AppComponent], 23 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 24 | providers: [ 25 | { provide: StatusBar, useValue: statusBarSpy }, 26 | { provide: SplashScreen, useValue: splashScreenSpy }, 27 | { provide: Platform, useValue: platformSpy }, 28 | ], 29 | imports: [ RouterTestingModule.withRoutes([])], 30 | }).compileComponents(); 31 | })); 32 | 33 | it('should create the app', async () => { 34 | const fixture = TestBed.createComponent(AppComponent); 35 | const app = fixture.debugElement.componentInstance; 36 | expect(app).toBeTruthy(); 37 | }); 38 | 39 | it('should initialize the app', async () => { 40 | TestBed.createComponent(AppComponent); 41 | expect(platformSpy.ready).toHaveBeenCalled(); 42 | await platformReadySpy; 43 | expect(statusBarSpy.styleDefault).toHaveBeenCalled(); 44 | expect(splashScreenSpy.hide).toHaveBeenCalled(); 45 | }); 46 | 47 | it('should have menu labels', async () => { 48 | const fixture = await TestBed.createComponent(AppComponent); 49 | await fixture.detectChanges(); 50 | const app = fixture.nativeElement; 51 | const menuItems = app.querySelectorAll('ion-label'); 52 | expect(menuItems.length).toEqual(1); 53 | expect(menuItems[0].textContent).toContain('Home'); 54 | }); 55 | 56 | it('should have urls', async () => { 57 | const fixture = await TestBed.createComponent(AppComponent); 58 | await fixture.detectChanges(); 59 | const app = fixture.nativeElement; 60 | const menuItems = app.querySelectorAll('ion-item'); 61 | expect(menuItems.length).toEqual(1); 62 | expect(menuItems[0].getAttribute('ng-reflect-router-link')).toEqual('/home'); 63 | }); 64 | 65 | it('should have one router outlet', async () => { 66 | const fixture = await TestBed.createComponent(AppComponent); 67 | await fixture.detectChanges(); 68 | const app = fixture.nativeElement; 69 | const routerOutlet = app.querySelectorAll('ion-router-outlet'); 70 | expect(routerOutlet.length).toEqual(1); 71 | }); 72 | 73 | it('should have one menubar', async () => { 74 | const fixture = await TestBed.createComponent(AppComponent); 75 | await fixture.detectChanges(); 76 | const app = fixture.nativeElement; 77 | const menubar = app.querySelectorAll('app-menubar'); 78 | expect(menubar.length).toEqual(1); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { RouterModule } from '@angular/router'; 5 | import { MenubarComponent } from './menubar/menubar.component'; 6 | import { environment } from '../environments/environment'; 7 | import { StatusBar } from '@capacitor/status-bar'; 8 | import { SplashScreen } from '@capacitor/splash-screen'; 9 | import { Platform } from '@ionic/angular'; 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | templateUrl: 'app.component.html', 14 | standalone: true, 15 | imports: [ 16 | IonicModule, 17 | CommonModule, 18 | RouterModule, 19 | MenubarComponent 20 | ] 21 | }) 22 | export class AppComponent { 23 | public appPages = [ 24 | { 25 | title: 'Home', 26 | url: '/home', 27 | icon: 'home' 28 | } 29 | ]; 30 | 31 | public appName = environment.appName; 32 | 33 | constructor(private platform: Platform) { 34 | this.initializeApp(); 35 | } 36 | 37 | async initializeApp() { 38 | await this.platform.ready(); 39 | 40 | // Only try to use Capacitor plugins on mobile platforms 41 | if (this.platform.is('capacitor')) { 42 | try { 43 | await StatusBar.setBackgroundColor({ color: '#3880ff' }); 44 | await SplashScreen.hide(); 45 | } catch (err) { 46 | console.warn('Capacitor plugins not available', err); 47 | } 48 | } 49 | document.title = environment.appName; 50 | } 51 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { 5 | path: '', 6 | redirectTo: 'home', 7 | pathMatch: 'full' 8 | }, 9 | { 10 | path: 'home', 11 | loadComponent: () => import('./home/home.page').then(m => m.HomePage) 12 | } 13 | ]; -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-login/auth-login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Email 4 | 5 | 6 | 7 | Password 8 | 9 | 10 | 14 | Log In 15 | 16 | 17 |

{{ error }}

18 |
19 |
-------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-login/auth-login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/app/auth/auth-login/auth-login.component.scss -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-login/auth-login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { AuthLoginComponent } from './auth-login.component'; 6 | 7 | describe('AuthLoginPage', () => { 8 | let component: AuthLoginComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [FormsModule, ReactiveFormsModule], 14 | declarations: [ AuthLoginComponent ], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 16 | }) 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(AuthLoginComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-login/auth-login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NonNullableFormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; 3 | import { IonicModule, ModalController } from '@ionic/angular'; 4 | import { CommonModule } from '@angular/common'; 5 | import { AuthService } from '../services/auth.service'; 6 | 7 | interface LoginForm { 8 | email: FormControl; 9 | password: FormControl; 10 | } 11 | 12 | @Component({ 13 | selector: 'app-auth-login', 14 | templateUrl: './auth-login.component.html', 15 | styleUrls: ['./auth-login.component.scss'], 16 | standalone: true, 17 | imports: [ 18 | IonicModule, 19 | CommonModule, 20 | ReactiveFormsModule 21 | ] 22 | }) 23 | export class AuthLoginComponent implements OnInit { 24 | loginForm!: FormGroup; 25 | error: string = ''; 26 | 27 | constructor( 28 | private formBuilder: NonNullableFormBuilder, 29 | private auth: AuthService, 30 | private modal: ModalController 31 | ) { } 32 | 33 | ngOnInit() { 34 | this.loginForm = this.formBuilder.group({ 35 | password: new FormControl('', { 36 | nonNullable: true, 37 | validators: Validators.required 38 | }), 39 | email: new FormControl('', { 40 | nonNullable: true, 41 | validators: [ 42 | Validators.required, 43 | Validators.pattern('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$') 44 | ] 45 | }) 46 | }); 47 | } 48 | 49 | async onSubmit(event: Event) { 50 | event.preventDefault(); 51 | 52 | if (!this.loginForm.valid) { return; } 53 | 54 | try { 55 | await this.auth.login( 56 | this.loginForm.controls.email.value, 57 | this.loginForm.controls.password.value 58 | ); 59 | await this.modal.dismiss(); 60 | } catch (err: any) { 61 | this.error = err.statusText || 'An error occurred during login'; 62 | throw err; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-button.component.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | Register 8 | 9 | 12 | Log In 13 | 14 | 15 | 16 | 17 | 19 | 21 | {{(auth.currentUser$ | async)?.email}} 22 | 23 | 26 | Log Out 27 | 28 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-button.component.scss: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | ion-avatar { 9 | width: 35px; 10 | height: 35px; 11 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-button.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthMenuButtonComponent } from './auth-menu-button.component'; 2 | 3 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 5 | import { ModalController } from '@ionic/angular'; 6 | 7 | 8 | describe('AuthMenuButtonPage', () => { 9 | let component: AuthMenuButtonComponent; 10 | let fixture: ComponentFixture; 11 | let modalSpy; 12 | let modalCtrlSpy; 13 | beforeEach(async(() => { 14 | modalSpy = jasmine.createSpyObj('Modal', ['present']); 15 | modalCtrlSpy = jasmine.createSpyObj('ModalController', ['create']); 16 | modalCtrlSpy.create.and.callFake(function () { 17 | return modalSpy; 18 | }); 19 | 20 | TestBed.configureTestingModule({ 21 | providers: [ 22 | { 23 | provide: ModalController, 24 | useValue: modalCtrlSpy 25 | } 26 | ], 27 | declarations: [ AuthMenuButtonComponent ], 28 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(AuthMenuButtonComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicModule, ModalController } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { AuthMenuUserComponent } from './auth-menu-user/auth-menu-user.component'; 5 | import { AuthService } from '../services/auth.service'; 6 | import { AuthLoginComponent } from '../auth-login/auth-login.component'; 7 | import { AuthRegisterComponent } from '../auth-register/auth-register.component'; 8 | 9 | @Component({ 10 | selector: 'app-auth-menu-button', 11 | templateUrl: './auth-menu-button.component.html', 12 | styleUrls: ['./auth-menu-button.component.scss'], 13 | standalone: true, 14 | imports: [ 15 | IonicModule, 16 | CommonModule 17 | ] 18 | }) 19 | export class AuthMenuButtonComponent { 20 | constructor( 21 | public auth: AuthService, 22 | public modalController: ModalController 23 | ) {} 24 | 25 | async presentmodal() { 26 | const modal = await this.modalController.create({ 27 | component: AuthMenuUserComponent, 28 | }); 29 | return await modal.present(); 30 | } 31 | 32 | async presentLogin() { 33 | const modal = await this.modalController.create({ 34 | component: AuthLoginComponent, 35 | }); 36 | return await modal.present(); 37 | } 38 | 39 | async presentRegister() { 40 | const modal = await this.modalController.create({ 41 | component: AuthRegisterComponent, 42 | }); 43 | return await modal.present(); 44 | } 45 | 46 | logout() { 47 | this.auth.logout(); 48 | } 49 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-user/auth-menu-user.component.html: -------------------------------------------------------------------------------- 1 | Dismiss 2 | 3 |

4 | auth-menu-user works! 5 |

6 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-user/auth-menu-user.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/app/auth/auth-menu-button/auth-menu-user/auth-menu-user.component.scss -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-user/auth-menu-user.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { AuthMenuUserComponent } from './auth-menu-user.component'; 5 | import { ModalController } from '@ionic/angular'; 6 | 7 | describe('AuthMenuUserPage', () => { 8 | let component: AuthMenuUserComponent; 9 | let fixture: ComponentFixture; 10 | let modalSpy; 11 | let modalCtrlSpy; 12 | 13 | beforeEach(async(() => { 14 | modalSpy = jasmine.createSpyObj('Modal', ['dismiss']); 15 | modalCtrlSpy = jasmine.createSpyObj('ModalController', ['create']); 16 | modalCtrlSpy.create.and.callFake(function () { 17 | return modalSpy; 18 | }); 19 | 20 | TestBed.configureTestingModule({ 21 | providers: [ 22 | { 23 | provide: ModalController, 24 | useValue: modalCtrlSpy 25 | } 26 | ], 27 | declarations: [ AuthMenuUserComponent ], 28 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(AuthMenuUserComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-menu-button/auth-menu-user/auth-menu-user.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicModule, ModalController } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | @Component({ 6 | selector: 'app-auth-menu-user', 7 | templateUrl: './auth-menu-user.component.html', 8 | styleUrls: ['./auth-menu-user.component.scss'], 9 | standalone: true, 10 | imports: [ 11 | IonicModule, 12 | CommonModule 13 | ] 14 | }) 15 | export class AuthMenuUserComponent { 16 | constructor(private modalCtrl: ModalController) { } 17 | 18 | dismissModal() { 19 | this.modalCtrl.dismiss(); 20 | } 21 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-register/auth-register.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Name 4 | 5 | 6 | 7 | Email 8 | 9 | 10 | 11 | Password 12 | 13 | 14 | 15 | Confirm Password 16 | 17 | 18 | 22 | Register 23 | {{error}} 24 |
25 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-register/auth-register.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/app/auth/auth-register/auth-register.component.scss -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-register/auth-register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { AuthRegisterComponent } from './auth-register.component'; 6 | 7 | describe('AuthRegisterPage', () => { 8 | let component: AuthRegisterComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [ ReactiveFormsModule ], 14 | declarations: [ AuthRegisterComponent ], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 16 | }) 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(AuthRegisterComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/auth-register/auth-register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; 3 | import { IonicModule, ModalController } from '@ionic/angular'; 4 | import { CommonModule } from '@angular/common'; 5 | import { AuthService } from '../services/auth.service'; 6 | import { User } from '../models/user.model'; 7 | 8 | @Component({ 9 | selector: 'app-auth-register', 10 | templateUrl: './auth-register.component.html', 11 | styleUrls: ['./auth-register.component.scss'], 12 | standalone: true, 13 | imports: [ 14 | IonicModule, 15 | CommonModule, 16 | ReactiveFormsModule 17 | ] 18 | }) 19 | export class AuthRegisterComponent implements OnInit { 20 | registerForm!: FormGroup; 21 | error: string = ''; 22 | 23 | constructor( 24 | private formBuilder: FormBuilder, 25 | private auth: AuthService, 26 | private modal: ModalController 27 | ) { } 28 | 29 | ngOnInit() { 30 | this.registerForm = this.formBuilder.group({ 31 | password_confirm: new FormControl('', Validators.required), 32 | password: new FormControl('', Validators.required), 33 | email: new FormControl('', Validators.compose([ 34 | Validators.required, 35 | Validators.pattern('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$') 36 | ])), 37 | name: new FormControl('', Validators.compose([ 38 | Validators.required, 39 | Validators.pattern('^[a-zA-Z0-9_.+-]+$') 40 | ])) 41 | }, { validators: this.passwordsMatch }); 42 | } 43 | 44 | onSubmit($event: Event) { 45 | $event.preventDefault(); 46 | 47 | if (!this.registerForm.valid) { return; } 48 | 49 | const newuser: User = { 50 | email: this.registerForm.controls['email'].value, 51 | name: this.registerForm.controls['name'].value 52 | }; 53 | 54 | this.auth.register(newuser, this.registerForm.controls['password'].value) 55 | .then((user) => { 56 | this.modal.dismiss(); 57 | }) 58 | .catch((e) => { 59 | this.error = e.statusText; 60 | throw e; 61 | }); 62 | } 63 | 64 | passwordsMatch(group: FormGroup) { 65 | return group.controls['password'].value === group.controls['password_confirm'].value 66 | ? null 67 | : { passwordsMisMatch: true }; 68 | } 69 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | email: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/services/auth.guard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuardService } from './auth.guard.service'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('AuthGuardService', () => { 7 | beforeEach(() => TestBed.configureTestingModule({ 8 | imports: [ RouterTestingModule ] 9 | })); 10 | 11 | it('should be created', () => { 12 | const service: AuthGuardService = TestBed.get(AuthGuardService); 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/services/auth.guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot, UrlTree } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuardService implements CanActivate { 10 | 11 | constructor( 12 | private auth: AuthService, 13 | private router: Router 14 | ) {} 15 | 16 | canActivate(route: ActivatedRouteSnapshot, 17 | state: RouterStateSnapshot): boolean 18 | | UrlTree 19 | | Observable 21 | | Promise { 22 | if (!this.auth.currentUser$.value) { 23 | this.router.navigateByUrl('/login'); 24 | } 25 | 26 | return this.auth.currentUser$.value !== null; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthService = TestBed.get(AuthService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | import { User } from '../models/user.model'; 4 | import { ApiService } from 'src/app/api/api.service'; 5 | import { catchError, tap } from 'rxjs/operators'; 6 | 7 | const JWT_LOCALSTORE_KEY = 'jwt'; 8 | const USER_LOCALSTORE_KEY = 'user'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class AuthService { 14 | currentUser$: BehaviorSubject = new BehaviorSubject(null); 15 | constructor( private api: ApiService ) { 16 | this.initToken(); 17 | } 18 | 19 | initToken() { 20 | const token = localStorage.getItem(JWT_LOCALSTORE_KEY); 21 | const user = JSON.parse(localStorage.getItem(USER_LOCALSTORE_KEY)); 22 | if (token && user) { 23 | this.setTokenAndUser(token, user); 24 | } 25 | } 26 | 27 | setTokenAndUser(token: string, user: User) { 28 | localStorage.setItem(JWT_LOCALSTORE_KEY, token); 29 | localStorage.setItem(USER_LOCALSTORE_KEY, JSON.stringify(user)); 30 | this.api.setAuthToken(token); 31 | this.currentUser$.next(user); 32 | } 33 | 34 | async login(email: string, password: string): Promise { 35 | return this.api.post('/users/auth/login', 36 | {email: email, password: password}) 37 | .then((res) => { 38 | this.setTokenAndUser(res.token, res.user); 39 | return res; 40 | }) 41 | .catch((e) => { throw e; }); 42 | // return user !== undefined; 43 | } 44 | 45 | logout(): boolean { 46 | this.setTokenAndUser(null, null); 47 | return true; 48 | } 49 | 50 | register(user: User, password: string): Promise { 51 | return this.api.post('/users/auth/', 52 | {email: user.email, password: password}) 53 | .then((res) => { 54 | this.setTokenAndUser(res.token, res.user); 55 | return res; 56 | }) 57 | .catch((e) => { throw e; }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-item/feed-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

{{feedItem.caption}}

5 |
6 |
-------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-item/feed-item.component.scss: -------------------------------------------------------------------------------- 1 | .photo-card{ 2 | max-width: 500px; 3 | overflow: hidden; 4 | background: var(--ion-color-primary-contrast); 5 | margin: 30px 0px; 6 | } 7 | 8 | .photo-card ion-img { 9 | max-height: 532px; 10 | overflow: hidden; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-item/feed-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { FeedItemComponent } from './feed-item.component'; 5 | import { feedItemMocks } from '../models/feed-item.model'; 6 | 7 | describe('FeedItemComponent', () => { 8 | let component: FeedItemComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ FeedItemComponent ], 14 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 15 | }) 16 | .compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(FeedItemComponent); 21 | component = fixture.componentInstance; 22 | component.feedItem = feedItemMocks[0]; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | 30 | it('should set the image url to the feedItem', () => { 31 | const app = fixture.nativeElement; 32 | const img = app.querySelectorAll('ion-img'); 33 | expect(img.length).toEqual(1); 34 | expect(img[0].src).toEqual(feedItemMocks[0].url); 35 | }); 36 | 37 | it('should display the caption', () => { 38 | const app = fixture.nativeElement; 39 | const paragraphs = app.querySelectorAll('p'); 40 | expect(([].slice.call(paragraphs)).map((x) => x.innerText)).toContain(feedItemMocks[0].caption); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-item/feed-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FeedItem } from '../models/feed-item.model'; 5 | 6 | @Component({ 7 | selector: 'app-feed-item', 8 | templateUrl: './feed-item.component.html', 9 | styleUrls: ['./feed-item.component.scss'], 10 | standalone: true, 11 | imports: [ 12 | IonicModule, 13 | CommonModule 14 | ], 15 | changeDetection: ChangeDetectionStrategy.OnPush 16 | }) 17 | export class FeedItemComponent { 18 | @Input() feedItem!: FeedItem; 19 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-list/feed-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |
-------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-list/feed-list.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .feed { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | background: var(--ion-color-light-tint); 7 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-list/feed-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { FeedListComponent } from './feed-list.component'; 5 | import { FeedProviderService } from '../services/feed.provider.service'; 6 | import { feedItemMocks } from '../models/feed-item.model'; 7 | 8 | describe('FeedListComponent', () => { 9 | let component: FeedListComponent; 10 | let fixture: ComponentFixture; 11 | let feedProvider: FeedProviderService; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ FeedListComponent ], 16 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 17 | }) 18 | .compileComponents(); 19 | })); 20 | 21 | beforeEach(() => { 22 | fixture = TestBed.createComponent(FeedListComponent); 23 | 24 | // SET UP SPIES AND MOCKS 25 | feedProvider = fixture.debugElement.injector.get(FeedProviderService); 26 | // spyOn(feedProvider, 'fetch').and.returnValue(Promise.resolve(feedItemMocks)); 27 | 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | 36 | it('should fetch on load', () => { 37 | expect(feedProvider.getFeed).toHaveBeenCalled(); 38 | }); 39 | 40 | it('should display all of the fetched items', () => { 41 | component.feedItems = feedItemMocks; 42 | fixture.detectChanges(); 43 | const app = fixture.nativeElement; 44 | const items = app.querySelectorAll('app-feed-item'); 45 | expect(items.length).toEqual(feedItemMocks.length); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-list/feed-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, OnDestroy } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FeedItem } from '../models/feed-item.model'; 5 | import { FeedProviderService } from '../services/feed.provider.service'; 6 | import { FeedItemComponent } from '../feed-item/feed-item.component'; 7 | import { Subscription } from 'rxjs'; 8 | 9 | @Component({ 10 | selector: 'app-feed-list', 11 | templateUrl: './feed-list.component.html', 12 | styleUrls: ['./feed-list.component.scss'], 13 | standalone: true, 14 | imports: [ 15 | IonicModule, 16 | CommonModule, 17 | FeedItemComponent 18 | ] 19 | }) 20 | export class FeedListComponent implements OnInit, OnDestroy { 21 | @Input() feedItems: FeedItem[]; 22 | subscriptions: Subscription[] = []; 23 | 24 | constructor(private feed: FeedProviderService) { } 25 | 26 | async ngOnInit() { 27 | this.subscriptions.push( 28 | this.feed.currentFeed$.subscribe((items) => { 29 | this.feedItems = items; 30 | }) 31 | ); 32 | 33 | await this.feed.getFeed(); 34 | } 35 | 36 | ngOnDestroy(): void { 37 | for (const subscription of this.subscriptions) { 38 | subscription.unsubscribe(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload-button/feed-upload-button.component.html: -------------------------------------------------------------------------------- 1 | 6 | Create a New Post 7 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload-button/feed-upload-button.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/app/feed/feed-upload/feed-upload-button/feed-upload-button.component.scss -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload-button/feed-upload-button.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { FeedUploadButtonComponent } from './feed-upload-button.component'; 5 | 6 | describe('FeedUploadButtonPage', () => { 7 | let component: FeedUploadButtonComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ FeedUploadButtonComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(FeedUploadButtonComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload-button/feed-upload-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { IonicModule, ModalController } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FeedUploadComponent } from '../feed-upload.component'; 5 | import { AuthService } from '../../../auth/services/auth.service'; 6 | import { Subscription } from 'rxjs'; 7 | 8 | @Component({ 9 | selector: 'app-feed-upload-button', 10 | templateUrl: './feed-upload-button.component.html', 11 | styleUrls: ['./feed-upload-button.component.scss'], 12 | standalone: true, 13 | imports: [ 14 | IonicModule, 15 | CommonModule 16 | ] 17 | }) 18 | export class FeedUploadButtonComponent implements OnInit, OnDestroy { 19 | isLoggedIn: boolean = false; 20 | loginSub?: Subscription; 21 | 22 | constructor( 23 | private modalController: ModalController, 24 | public auth: AuthService 25 | ) { } 26 | 27 | ngOnInit() { 28 | this.loginSub = this.auth.currentUser$.subscribe((user) => { 29 | this.isLoggedIn = user !== null; 30 | }); 31 | } 32 | 33 | ngOnDestroy() { 34 | this.loginSub?.unsubscribe(); 35 | } 36 | 37 | async presentUploadForm() { 38 | const modal = await this.modalController.create({ 39 | component: FeedUploadComponent, 40 | }); 41 | return await modal.present(); 42 | } 43 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload.component.html: -------------------------------------------------------------------------------- 1 |
` 2 | 9 | 10 | Caption 11 | 12 | 13 | 17 | Post 18 | 19 |
20 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/app/feed/feed-upload/feed-upload.component.scss -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { FeedUploadComponent } from './feed-upload.component'; 5 | 6 | describe('FeedUploadPage', () => { 7 | let component: FeedUploadComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ FeedUploadComponent ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(FeedUploadComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/feed-upload/feed-upload.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; 3 | import { IonicModule, LoadingController, ModalController } from '@ionic/angular'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FeedProviderService } from '../services/feed.provider.service'; 6 | 7 | @Component({ 8 | selector: 'app-feed-upload', 9 | templateUrl: './feed-upload.component.html', 10 | styleUrls: ['./feed-upload.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | IonicModule, 14 | CommonModule, 15 | ReactiveFormsModule 16 | ] 17 | }) 18 | export class FeedUploadComponent implements OnInit { 19 | previewDataUrl: string | ArrayBuffer | null = null; 20 | file: File | null = null; 21 | uploadForm!: FormGroup; 22 | 23 | constructor( 24 | private feed: FeedProviderService, 25 | private formBuilder: FormBuilder, 26 | private loadingController: LoadingController, 27 | private modalController: ModalController 28 | ) { } 29 | 30 | ngOnInit() { 31 | this.uploadForm = this.formBuilder.group({ 32 | caption: new FormControl('', Validators.required) 33 | }); 34 | } 35 | 36 | setPreviewDataUrl(file: Blob) { 37 | const reader = new FileReader(); 38 | reader.onloadend = () => { 39 | this.previewDataUrl = reader.result; 40 | }; 41 | reader.readAsDataURL(file); 42 | } 43 | 44 | selectImage(event: Event) { 45 | const element = event.currentTarget as HTMLInputElement; 46 | const fileList: FileList | null = element.files; 47 | 48 | if (fileList) { 49 | this.file = fileList[0]; 50 | this.setPreviewDataUrl(this.file); 51 | } 52 | } 53 | 54 | async onSubmit(event: Event) { 55 | event.preventDefault(); 56 | const loading = await this.loadingController.create(); 57 | await loading.present(); 58 | 59 | if (!this.uploadForm.valid || !this.file) { return; } 60 | 61 | try { 62 | await this.feed.uploadFeedItem(this.uploadForm.controls['caption'].value, this.file); 63 | await this.modalController.dismiss(); 64 | } finally { 65 | await loading.dismiss(); 66 | } 67 | } 68 | 69 | cancel() { 70 | this.modalController.dismiss(); 71 | } 72 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/models/feed-item.model.ts: -------------------------------------------------------------------------------- 1 | export interface FeedItem { 2 | id: number; 3 | url: string; 4 | caption: string; 5 | } 6 | 7 | export const feedItemMocks: FeedItem[] = [ 8 | { 9 | id: 0, 10 | url: '/assets/mock/xander0.jpg', 11 | caption: 'Such a cute pup' 12 | }, 13 | { 14 | id: 0, 15 | url: '/assets/mock/xander1.jpg', 16 | caption: 'Who\'s a good boy?' 17 | }, 18 | { 19 | id: 0, 20 | url: '/assets/mock/xander2.jpg', 21 | caption: 'Majestic.' 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/services/feed.provider.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { FeedProviderService } from './feed.provider.service'; 4 | 5 | describe('Feed.ProviderService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: FeedProviderService = TestBed.get(FeedProviderService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/feed/services/feed.provider.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { FeedItem, feedItemMocks } from '../models/feed-item.model'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | 5 | import { ApiService } from '../../api/api.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class FeedProviderService { 11 | currentFeed$: BehaviorSubject = new BehaviorSubject([]); 12 | 13 | constructor(private api: ApiService) { } 14 | 15 | async getFeed(): Promise> { 16 | const req = await this.api.get('/feed'); 17 | const items = req.rows; 18 | this.currentFeed$.next(items); 19 | return Promise.resolve(this.currentFeed$); 20 | } 21 | 22 | async uploadFeedItem(caption: string, file: File): Promise { 23 | const res = await this.api.upload('/feed', file, {caption: caption, url: file.name}); 24 | const feed = [res, ...this.currentFeed$.value]; 25 | this.currentFeed$.next(feed); 26 | return res; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/home/home.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/app/home/home.page.scss -------------------------------------------------------------------------------- /udagram-frontend/src/app/home/home.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { HomePage } from './home.page'; 5 | 6 | describe('HomePage', () => { 7 | let component: HomePage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ HomePage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(HomePage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FeedUploadButtonComponent } from '../feed/feed-upload/feed-upload-button/feed-upload-button.component'; 5 | import { FeedListComponent } from '../feed/feed-list/feed-list.component'; 6 | import { environment } from '../../environments/environment'; 7 | 8 | @Component({ 9 | selector: 'app-home', 10 | templateUrl: 'home.page.html', 11 | styleUrls: ['home.page.scss'], 12 | standalone: true, 13 | imports: [ 14 | IonicModule, 15 | CommonModule, 16 | FeedUploadButtonComponent, 17 | FeedListComponent 18 | ] 19 | }) 20 | export class HomePage { 21 | appName = environment.appName; 22 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/menubar/menubar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ appName }} 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/menubar/menubar.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: relative; 4 | z-index: 1; 5 | } 6 | 7 | ion-title { 8 | font-weight: bold; 9 | font-family: 'Dancing Script', cursive; 10 | font-size: 180%; 11 | } -------------------------------------------------------------------------------- /udagram-frontend/src/app/menubar/menubar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { MenubarComponent } from './menubar.component'; 5 | import { environment } from '../../environments/environment'; 6 | 7 | 8 | describe('MenubarPage', () => { 9 | let component: MenubarComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [ MenubarComponent ], 15 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 16 | }) 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(MenubarComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | 30 | it('title should be enviornment.AppTitle', () => { 31 | const app = fixture.nativeElement; 32 | const title = app.querySelectorAll('ion-title'); 33 | expect(title.length).toEqual(1); 34 | expect(title[0].innerText).toEqual(environment.appName); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /udagram-frontend/src/app/menubar/menubar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicModule } from '@ionic/angular'; 3 | import { CommonModule } from '@angular/common'; 4 | import { AuthMenuButtonComponent } from '../auth/auth-menu-button/auth-menu-button.component'; 5 | import { environment } from '../../environments/environment'; 6 | 7 | @Component({ 8 | selector: 'app-menubar', 9 | templateUrl: './menubar.component.html', 10 | styleUrls: ['./menubar.component.scss'], 11 | standalone: true, 12 | imports: [ 13 | IonicModule, 14 | CommonModule, 15 | AuthMenuButtonComponent 16 | ] 17 | }) 18 | export class MenubarComponent { 19 | public appName = environment.appName; 20 | } -------------------------------------------------------------------------------- /udagram-frontend/src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cd0354-monolith-to-microservices-project/eb4b5a2565773ef192e23b370bb51b841ab2257f/udagram-frontend/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /udagram-frontend/src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /udagram-frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | appName: 'Udagram', 8 | apiHost: 'http://localhost:8080/api/v0' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /udagram-frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | appName: 'Udagram', 8 | apiHost: 'http://localhost:8080/api/v0' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /udagram-frontend/src/global.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/theming/ 2 | @import '~@ionic/angular/css/core.css'; 3 | @import '~@ionic/angular/css/normalize.css'; 4 | @import '~@ionic/angular/css/structure.css'; 5 | @import '~@ionic/angular/css/typography.css'; 6 | @import '~@ionic/angular/css/display.css'; 7 | @import '~@ionic/angular/css/padding.css'; 8 | @import '~@ionic/angular/css/float-elements.css'; 9 | @import '~@ionic/angular/css/text-alignment.css'; 10 | @import '~@ionic/angular/css/text-transformation.css'; 11 | @import '~@ionic/angular/css/flex-utils.css'; 12 | 13 | // Fancy Fonts 14 | @import url('https://fonts.googleapis.com/css?family=Dancing+Script:700'); -------------------------------------------------------------------------------- /udagram-frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Udagram 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /udagram-frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /udagram-frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, importProvidersFrom } from '@angular/core'; 2 | import { bootstrapApplication } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy, provideRouter } from '@angular/router'; 4 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 5 | import { routes } from './app/app.routes'; 6 | import { AppComponent } from './app/app.component'; 7 | import { environment } from './environments/environment'; 8 | import { provideHttpClient } from '@angular/common/http'; 9 | 10 | if (environment.production) { 11 | enableProdMode(); 12 | } 13 | 14 | bootstrapApplication(AppComponent, { 15 | providers: [ 16 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, 17 | importProvidersFrom(IonicModule.forRoot()), 18 | provideRouter(routes), 19 | provideHttpClient() 20 | ], 21 | }); -------------------------------------------------------------------------------- /udagram-frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills. 22 | * This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot 23 | */ 24 | 25 | // import 'core-js/es6/symbol'; 26 | // import 'core-js/es6/object'; 27 | // import 'core-js/es6/function'; 28 | // import 'core-js/es6/parse-int'; 29 | // import 'core-js/es6/parse-float'; 30 | // import 'core-js/es6/number'; 31 | // import 'core-js/es6/math'; 32 | // import 'core-js/es6/string'; 33 | // import 'core-js/es6/date'; 34 | // import 'core-js/es6/array'; 35 | // import 'core-js/es6/regexp'; 36 | // import 'core-js/es6/map'; 37 | // import 'core-js/es6/weak-map'; 38 | // import 'core-js/es6/set'; 39 | 40 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 41 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 42 | 43 | /** IE10 and IE11 requires the following for the Reflect API. */ 44 | // import 'core-js/es6/reflect'; 45 | 46 | /** 47 | * Web Animations `@angular/platform-browser/animations` 48 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 49 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 50 | */ 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /** 54 | * By default, zone.js will patch all possible macroTask and DomEvents 55 | * user can disable parts of macroTask/DomEvents patch by setting following flags 56 | * because those flags need to be set before `zone.js` being loaded, and webpack 57 | * will put import in the top of bundle, so user need to create a separate file 58 | * in this directory (for example: zone-flags.ts), and put the following flags 59 | * into that file, and then add the following code before importing zone.js. 60 | * import './zone-flags.ts'; 61 | * 62 | * The flags allowed in zone-flags.ts are listed here. 63 | * 64 | * The following flags will work for all browsers. 65 | * 66 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 67 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 68 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 69 | * 70 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 71 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 72 | * 73 | * (window as any).__Zone_enable_cross_context_check = true; 74 | * 75 | */ 76 | 77 | /*************************************************************************************************** 78 | * Zone JS is required by default for Angular itself. 79 | */ 80 | import 'zone.js/dist/zone'; // Included with Angular CLI. 81 | 82 | 83 | /*************************************************************************************************** 84 | * APPLICATION IMPORTS 85 | */ 86 | -------------------------------------------------------------------------------- /udagram-frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /udagram-frontend/src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #0cd1e8; 16 | --ion-color-secondary-rgb: 12, 209, 232; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #0bb8cc; 20 | --ion-color-secondary-tint: #24d6ea; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #7044ff; 24 | --ion-color-tertiary-rgb: 112, 68, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #633ce0; 28 | --ion-color-tertiary-tint: #7e57ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #10dc60; 32 | --ion-color-success-rgb: 16, 220, 96; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #0ec254; 36 | --ion-color-success-tint: #28e070; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffce00; 40 | --ion-color-warning-rgb: 255, 206, 0; 41 | --ion-color-warning-contrast: #ffffff; 42 | --ion-color-warning-contrast-rgb: 255, 255, 255; 43 | --ion-color-warning-shade: #e0b500; 44 | --ion-color-warning-tint: #ffd31a; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #f04141; 48 | --ion-color-danger-rgb: 245, 61, 61; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #d33939; 52 | --ion-color-danger-tint: #f25454; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 34, 34; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #989aa2; 64 | --ion-color-medium-rgb: 152, 154, 162; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #86888f; 68 | --ion-color-medium-tint: #a2a4ab; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 244, 244; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #FAFAFA; 77 | } 78 | -------------------------------------------------------------------------------- /udagram-frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /udagram-frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /udagram-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2022", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "importHelpers": true, 12 | "target": "es2022", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2022", 18 | "dom" 19 | ], 20 | "skipLibCheck": true, 21 | "strict": false, 22 | "noImplicitAny": false, 23 | "noImplicitReturns": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "strictPropertyInitialization": false, 26 | "useDefineForClassFields": false 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } -------------------------------------------------------------------------------- /udagram-frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /udagram-frontend/udagram_tests/git_test.sh: -------------------------------------------------------------------------------- 1 | @ TODO 2 | Verify dev, staging, and master exist 3 | verify working on feature branch or similar 4 | verify cannot push to staging or master (protected branches) --------------------------------------------------------------------------------