In Part 1 we did the simple version which can be found here:
Let kick it up a notch by trying it using aws-amplify authentication in this same app.
Plenty of stuff material around on setting up AWS. https://dev.to/dabit3 is good place to start.
Once you have the aws-cli
configured, run amplify init
in the root of the project from part 1.
It should look something like this:
Then run amplify add auth
to get a Cognito Identity Pool
and Cognito User Pool
set up.
Be sure to run amplify push
to get all the backend set up in the cloud.
Since we didn't set up signing in we want to create a test user in our UserPool via the aws cognito interface on aws. That didn't sound clear, let me know if you don't get what I mean. In your terminal run amplify console auth
which will open up that page.
Select User Pool
then enter. This will open up the AWS Cognito Users page in your User Pool. On the menu on the left, click Users and Groups
then the blue outlined Create User
button.
This is how I filled it out.
The password I used was Password12345@
so cognito wouldn't complain.
Even though it says that we will need to update the password, we are dealing with that here and it will let you use the temporary password for a while. Cognito will also send it to you in an email because we check that option.
Setting Up Aws Auth In The App
Bindings!
The first thing we want to do is add the aws-amplify
package. We will use it to configure
aws-amplify
and run auth
functions.
yarn add aws-amplify
touch Amplify.re // create a file for our Amplify binding.
Then create a file for our Amplify binding.
touch Amplify.re
In Amplify.re
we want to add the following:
type t;
[@bs.module "aws-amplify"] external amplify: t = "default";
type config;
[@bs.module "./aws-exports.js"] external awsConfig: config = "default";
[@bs.send] external _configure: (t, config) => unit = "configure";
let configure = () => _configure(amplify, awsConfig);
What is going on here?
Ripped from Patrick Kilgore's BigInteger.re
What is
type t
?It is a ocaml convention for "the type of this module". So if the module was named "Fish",
type t
would be the fish. We could just as easily call it anything else, eventype fish
, but then we'd be referring to it asFish.fish
which seems silly, right? So we call ittype t
and refer to it asFish.t
and by convention knowt
means the module's type.A ReasonML module is a type packaged with its behavior. In this way, it is similar to an Object-Oriented language's concept of
class
.So here,
type t
is the Amplify data structure, packaged with the methods we can to operate on that type.Because we don't really know (or, honestly, care) about how the Amplify library implements the Amplify type, we just declare it here, which means it is an "abstract type", which I always think of as, "a type that must be used consistently by the functions that operate on it, but for which the particular implementation of the type and those functions are assumed to be correct".
Thanks, Patrick for taking the time to write those awesome comments.
So t
is our Amplify
javascript data structure bound to aws-amplify
's default export.
The type config
may or may not be overkill. I would love to hear back from you all on this. It works without it but its a pattern I picked up somewhere and this code works so moving on. We are using bs.module
to import the aws-exports.js
file that the amplify-cli
generated in our src
dir when we ran amplify push
. It's got our configuration keys for accessing our auth service.
We are going to pass to that to Amplify's configure
method/function which configures our app to use our services. We use [@bs.send]
to call the function called configure
on out type t
. I aliased it as _configure
so that I could call it using configure
, no underscore later, and not hurt my eyes trying to see which configure
function I was calling. In Reason, you can call them both configure
and the second configure
will just call the previous configure
.
Normally in JS it would look like this in your app's entry point:
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
I went ahead and retrieve aws-exports
and passed it to configure
here. So in our app's entry point we can configure our app like so:
...other stuff
Amplify.configure(); //add this line
ReactDOMRe.renderToElementWithId(<Root />, "root");
Also in Amplify.re
we want to add a binding to Amplify's Auth
object. Let's add the following bindings and implementations functions:
/* assigning Amplify Auth object as type auth */
type auth;
[@bs.module "aws-amplify"] external auth: auth = "Auth";
[@bs.send] external _signOut: (auth, unit) => unit = "configure";
[@bs.send]
external _signIn:
(auth, ~username: string, ~password: string, unit) => Js.Promise.t('a) =
"signIn";
/* a function that calls Amplify's signOut to sign out our user. This works wether passing auth or amplify as our type t */
let signOut = () => _signOut(auth, ());
/* a function that takes a username and password then calls Amplify's signIn to sign in our user */
let signIn = (~username, ~password) =>
_signIn(auth, ~username, ~password, ())
|> Js.Promise.then_(res => Js.Promise.resolve(res));
By binding to the Auth
object and assigning type auth
we can use this same binding to call its functions using [bs.send]
. We tell the compiler that the function is found on the auth
binding by passing requiring an argument with type auth
in our bs.send
definitions like so:
[@bs.send]
external _signIn:
(auth, ~username: string, ~password: string, unit) => Js.Promise.t('a) =
"signIn";
The implementation is written so that when we call signIn
it only requires the username
and password
which we then pass to the the underscore signIn
which already has the auth
binding called in it.
let signIn = (~username, ~password) =>
_signIn(auth, ~username, ~password, ())
|> Js.Promise.then_(res => Js.Promise.resolve(res));
I am pretty sure, this is what they call currying
. The docs aren't very helpful so let me take a stab at explaining it to us. The _signin
already has the auth
property and is just waiting on the last two variables that it needs to be able to make the call. These remaining variables are the username
and password
values we pass into signIn()
. This makes it so we don't have to pass in the auth
property at the call sites every time we want to use the module. Anyone with a better explanation, please teach me!
Using Our Binding
Now that we have the binding, let use them in the Header.re
module.
We are going to add to functions that will handle signIn
and signOut
.
// ...other code
let handleSignin = () =>
Js.Promise.(
Amplify.signIn(~username, ~password)
|> then_(res => {
// Js.log2("res", res);
// this is bad, i think, because we aren't handling errors. We know, for purposes of the example, that the username is at the `username` key so let's go with it.
let username = res##username;
Js.log("sign in success!");
dispatch(UserLoggedIn(username));
resolve();
})
|> catch(err => {
Js.log(err);
let errMsg = "error signing in.." ++ Js.String.make(err);
Js.log(errMsg);
resolve();
})
|> ignore
);
let handleSignOut = () => {
Amplify.signOut();
dispatch(UserLoggedOut);
Js.log("signing out!");
/* test if user is logged out because you can still log the user after logging out. Running currentAuthenticated user shows that we are logged out so why is `user` logging out below?*/
Amplify.currentAuthenticatedUser
|> Js.Promise.then_(data => {
Js.log2("data", data);
Js.Promise.resolve(data);
})
|> Js.Promise.catch(error => Js.log2("error", error)->Js.Promise.resolve)
|> Js.Promise.resolve
|> ignore;
/* user still logs after logging out. Why? */
Js.log2("signing out user!",user);
};
// ...other code
The handleSignIn
function is going to read the username
and password
off of our state and call Amplify.signIn
with it. If we get a positive answer, then we read the username
key off of the response object,res##username
and set it in our user context by calling dispatch(UserLoggedIn(username))
. The ##
is how you read the value at a key on a javascript object. See Accessors in the bucklescript docs.
The handleSignOut
is pretty simple since it doesn't return anything. I added a call to currentAuthenticatedUser
because you can still log the username after signing out. In fact, the currentAuthenticatedUser
response shows that we are signed out. If anyone wants to tell me why the username is still logging, I would love to understand it. I though it would error or return Anonymous
. Idea? Ideas? Thank's in advance.
Now let change:
| Anonymous =>
<form
className="user-form"
onSubmit={e => {
ReactEvent.Form.preventDefault(e);
dispatch(UserLoggedIn(userName));
}}>
To:
| Anonymous =>
<form
className="user-form"
onSubmit={e => {
ReactEvent.Form.preventDefault(e);
handleSignin();
}}>
And further down, change:
| LoggedIn(userName) =>
<div className="user-form">
<span className="logged-in">
{s("Logged in as: ")}
<b> {s(userName)} </b>
</span>
<div className="control">
<button
className="button is-link"
onClick={_ => dispatch(UserLoggedOut)}>
{s("Log Out")}
</button>
</div>
</div>
to:
| LoggedIn(userName) =>
<div className="user-form">
<span className="logged-in">
{s("Logged in as: ")}
<b> {s(userName)} </b>
</span>
<div className="control">
<button className="button is-link" onClick={_ => handleSignOut()}>
</div>
</div>
That's it. Now you are using Aws Cognito to for overkill authentication in Ms. Brandt's music app.
Reach with questions or lessons, please. Thank you!
Check out this version on the with-aws branch
Top comments (0)