Users

In order to implement our User model access patterns, we need to understand how the repository defines the abstract user operations. In software engineering terms, this is an example of a contract. Contracts are precise descriptions of how systems interact with one another, and are usually enforced in code such that certain methods and types conform to the description. To put it another way, a software contract is like a legal contract in that the software components agree to some precise conditions for how they will interact. You can technically break a contract, but the resulting system behavior will be undefined and often disasterous.

In our case, the repository defines a contract for the following user operations:

async user(): Promise<User> {
  return await this.strategy.user();
}

async isSignedIn(): Promise<boolean> {
  return await this.strategy.isSignedIn();
}

async updateUser(userOptions: UserUpdate) {
  return this.strategy.updateUser(userOptions);
}

async signIn(userOptions: UserOptions) {
  return this.strategy.signIn(userOptions);
}

async signUp(userOptions: UserOptions) {
  return this.strategy.signUp(userOptions);
}

async signOut() {
  return this.strategy.signOut();
}

async confirmSignup(username: string, code: string) {
  return this.strategy.confirmSignup(username, code);
}

The code may seem silly, but it makes clear how the repository pattern works: it delegates the method calls to the underlying strategy. In fact, this.strategy is dynamic and when set to use AWS, it is responsible for actually performing the AppSync GraphQL requests and returning the hydrated models.

Write The Code

Let’s open the Amplify data access strategy in the src/app/repository/amplify/index.ts file.

You can see there are all the same methods we just described above, but they’re empty!

async user(): Promise<User> {
  // Implement me!
}

async isSignedIn(): Promise<boolean> {
  // Implement me!
}

async updateUser(userOptions: UserUpdate) {
  // Implement me!
}

async signIn(userOptions: UserOptions) {
  // Implement me!
}

async signUp(userOptions: UserOptions) {
  // Implement me!
}

async signOut() {
  // Implement me!
}

async confirmSignup(username: string, code: string) {
  // Implement me!
}

Let’s implement the user() method, which returns the currently signed in user or the anonymous user in the case no one is signed in.

async user(): Promise<User> {
  try {
    const user = await Auth.currentAuthenticatedUser();
    if (!user.attributes.picture) {
      throw new Error();
    }
    const { picture } = user.attributes;
    const res = await Storage.get(picture, { download: true }) as any;
    const pictureUrl = await utils.blobToDataUrl(res.Body);
    return {
      username: user.attributes.preferred_username || user.username,
      email: user.attributes.email,
      picture: pictureUrl
    };
  } catch (err) {
    return {
      isAnonymous: true,
      email: 'anonymous',
      username: 'anonymous',
      picture: 'http://www.gravatar.com/avatar'
    };
  }
}

You can see we’re making use of the Amplify library’s Auth and Storage modules to check the signed in user state and also retrieve the user’s profile picture from S3. If the user is anonymous, we handle the exception and return an object for that case.

Now go ahead and implement the remainder of the methods using the following code:

async updateUser(userOptions: UserUpdate) {
  const user: CognitoUser = await Auth.currentAuthenticatedUser();
  const attributes = await Auth.userAttributes(user);
  const sub = attributes.find((it: any) => it.Name === 'sub').getValue();
  if (!user) {
    return;
  }
  const { displayName, profilePicture } = userOptions;
  if (displayName) {
    await Auth.updateUserAttributes(user, {
      preferred_username: displayName
    });
  }
  if (profilePicture) {
    try {
      await Storage.put(`${sub}.jpg`, profilePicture, {
        contentType: 'image/jpeg'
      });
      await Auth.updateUserAttributes(user, {
        picture: `${sub}.jpg`
      });
    } catch (err) {
      console.error(err);
    }
  }
}

async isSignedIn(): Promise<boolean> {
  try {
    const user = await Auth.currentAuthenticatedUser();
    return Boolean(user) && !user.isAnonymous;
  } catch (err) {
    return false;
  }
}

async signIn(userOptions: UserOptions): Promise<boolean> {
  const { email, password } = userOptions;
  try {
    await Auth.signIn(email, password);
    return window.dispatchEvent(new CustomEvent('user:signin'));
  } catch (err) {
    console.error(err);
  }
}

async signOut() {
  try {
    await Auth.signOut();
    window.dispatchEvent(new CustomEvent('user:signout'));
  } catch (err) {
    // OK: Sign out unconditionally
  }
}

async signUp(userOptions: UserOptions) {
  const { username, email, password } = userOptions;
  try {
    await Auth.signUp({
      username,
      password,
      attributes: {
        email
      }
    });
  } catch (err) {
    throw err;
  }
}

async confirmSignup(username: string, code: string) {
  try {
    await Auth.confirmSignUp(username, code);
  } catch (err) {
    throw err;
  }
}

Good work. These changes will allow us to sign in and out, and do everything we need related to our User model once we finishing writing the code for the following sections.

Migrating Users

When we deployed our AWS infrastructure, along the way we configured Amazon Cognito, AWS’s user identity provider service, with a Lambda trigger that migrates users from Firebase Authentication to Amazon Cognito. This works by allowing the user to sign in with Amplify but using their original Firebase credentials. No password reset is required to migrate the user to Amazon Cognito.