Building a Profile App – Part 02

In Part 01 we started building the Profile app with Amplify as the frontend library. We managed to save the user information on a DynamoDB table via a GraphQL API.

In this second part, let’s add following features and improvements to our app.

  1. Securely uploading the profile image
  2. Loading the saved user data
  3. Implement an auth guard for the profile page to avoid unauthorized access
  4. Automatically redirect to the profile page after a successful login

Configuring Storage Category with Amplify

For the sake of this application let’s allow a user to view only the profile picture of himself. (May be it makes no sense, but I want to show how to use private images using amplify storage service).

Okay, let’s use two higher order components from Amplify Angular Library to make this task very easy.

  • <amplify-photo-picker></amplify-photo-picker>
  • <amplify-s3-image></amp

amplify-photo-picker allows users to upload image to S3. We can pass different storage options for our liking. It supports three storage options i.e. public, protected and private. We are going to use private level that only allows the owner to view and upload the image.

But hey, before that let’s add Storage category with amplify that will create a S3 bucket for us. So, get a command prompt and run following commands.

amplify add storage

? Please select from one of the below mentioned services:
Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: s38e43106 a
? Please provide bucket name: profileapp03f4977230524d1e977654540b6c1924
? Who should have access: Auth users only
? What kind of access do you want for Authenticated users: read/write

amplify push

Securely uploading the profile image

Now let’s bring on those two components to the profile.component.html

 <h2>My Profile</h2>
<div class="form-group row">
<div class="col-sm-12">
<div class="md-form mt-0">
<mdb-icon *ngIf="showPhoto" fas icon="upload" (click)="editPhoto()" size="2x" class="upload-icon"></mdb-icon>
<!-- Display Image -->
<amplify-s3-image [path]="user.imageUrl"
[options]="{'level': 'private'}" *ngIf="showPhoto">
</amplify-s3-image>

<!-- Photo Picker -->
<amplify-photo-picker *ngIf="!showPhoto"
path="image"[storageOptions]="{'level': 'private'}" (uploaded)="onImageUploaded($event)">
</amplify-photo-picker>
</div>
</div>
</div>
<form> ...

Edit the profile.component.ts as follows.

export class ProfileComponent implements OnInit {
...
showPhoto: boolean;
userCreated: boolean;

async onImageUploaded(e) {
this.user.imageUrl = e.key;
if (this.userCreated) {
await this.api.UpdateUser({
id: this.userId,
image: this.user.imageUrl
});
}
this.showPhoto = true;
}

editPhoto() {
this.showPhoto = false;
}

getType(): string {
return this.userCreated ? 'UpdateUser' : 'CreateUser';
}

async updateProfile() {
const user = {
id: this.userId,
username: this.user.firstName + '_' + this.user.lastName,
firstName: this.user.firstName,
lastName: this.user.lastName,
bio: this.user.aboutMe,
image: this.user.imageUrl
}
await this.api[this.getType()](user);
}
...
}

Loading Saved Data

At this point our application is managed to store profile information on DynamoDB table and store the profile image on a S3 bucket. However, when we reload the webpage, all information is disappeared. Let’s fix that by fetching the saved data upon profile component loading.

We are going to update ngOnInit lifecycle method to load up the user data and populate User model which will automatically bind to our angular form.

...  
ngOnInit() {
this.showPhoto = false;
Auth.currentAuthenticatedUser({
bypassCache: false
}).then(async user => {
this.userName = user.username;
this.userId = user.attributes.sub;
let result = await this.api.GetUser(this.userId);
if (!result) {
this.userCreated = false;
this.user = new User('', '', '', '', '', '');
} else {
this.userCreated = true;
this.showPhoto = !!result.image;
this.user = new User(
this.userId,
result.username,
result.firstName,
result.lastName,
result.bio,
result.image
)
}
})
.catch(err => console.log(err));
}
...

Logout Functionality

Now that we have almost finished with the profile app functionalities, let’s add a method to logout for authenticated users.

In profile.component.ts file add the following method that calls the signOut method of Auth api.

import { Router } from '@angular/router';
...
constructor(private api: APIService, private router: Router) {}
...
logOut() {
Auth.signOut({ global: true })
.then(data => {
this.router.navigate(['/auth']);
})
.catch(err => console.log(err));
}

Make sure to bind this function for the click event of the Logout link in the template.

Configuring Auth Guards

Currently, we have two basic routes. One for the login screen and the other for our profile component. We must not allow to load profile component unless the user is logged in. We can achieve that using an auth guard.

Create an auth guard with,

ng g guard auth

Here is the code for auth guard service.

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Auth } from 'aws-amplify';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}

canActivate(): Promise < boolean > {
return new Promise((resolve) => {
Auth.currentAuthenticatedUser({
bypassCache: false
})
.then((user) => {
if(user){
resolve(true);
}
})
.catch(() => {
this.router.navigate(['/login']);
resolve(false);
});
});
}
}

Auth.currentAuthenticatedUser Api call always returns the currently authenticated user. If there is no currently authenticated user Auth guard will be resolved as false and Profile component will not be activated.

So now, let’s add that auth guard to guard our Profile component in app-routing.module.js

import { AuthGuard } from './auth.guard';
...
const routes: Routes = [{
path: "profile",
component: ProfileComponent,
canActivate: [AuthGuard]
},
{
path: "login",
component: AuthComponent
},
{
path: '**',
redirectTo: 'login',
pathMatch: 'full'
}
];

Automatic Redirection After Login

Finally, let’s add automatic redirection to profile page once a user is successfully authenticated. We can accomplish this by listening to authStateChange$ events generated by Amplify library.

Goto auth.component.js file and add following code.

import { AmplifyService } from 'aws-amplify-angular';
import { Router } from '@angular/router';

constructor(public amplifyService: AmplifyService, public router: Router) {
this.amplifyService = amplifyService;
this.amplifyService.authStateChange$
.subscribe(authState => {
if (authState.state === 'signedIn') {
this.router.navigate(['/profile']);
}
});
}

Okay. Now we can run the application and check if everything works. Login with a registered user. Make sure you will be redirected to the Profile page. Then update the profile information with a profile image and make sure the information is persisted.

ng serve

You will still see the Amplify sign-In page for a second before the redirection. In order to hide that default component pass the “hide” input to <amplify-authenticator>

<amplify-authenticator [hide]="['Greetings']"></amplify-authenticator>

Final Page

I hope this post has been useful. Please find the github repo of this example project at https://github.com/mjzone/amplify-user-profile

Cheers!

Please follow and like us: