From a0189c5d8bcac84f6e76945069fc92b381cd1ab8 Mon Sep 17 00:00:00 2001 From: Maciej Krajowski-Kukiel Date: Tue, 23 Jan 2024 09:58:40 +0100 Subject: [PATCH] add section about user authentication (#1690) * add section about user authentication * update * add signing in --- .../core-concepts.liquid | 2 +- .../building-the-user-interface.liquid | 6 +- .../removing-data-from-the-database.liquid | 12 +- ...and-presenting-the-data-on-the-page.liquid | 14 +- .../saving-data-to-the-database.liquid | 20 +- .../sending-email-notifications.liquid | 12 +- .../user-authentication.liquid | 224 ++++++++++++++++++ .../connecting-angular-spa-platformos.liquid | 2 +- .../partials/shared/nav/get-started.liquid | 11 + 9 files changed, 269 insertions(+), 34 deletions(-) create mode 100644 app/views/pages/get-started/build-your-first-app/user-authentication.liquid diff --git a/app/views/pages/developer-guide/pos-marketplace-template/core-concepts.liquid b/app/views/pages/developer-guide/pos-marketplace-template/core-concepts.liquid index 53b8e5b44..55cb3eeea 100644 --- a/app/views/pages/developer-guide/pos-marketplace-template/core-concepts.liquid +++ b/app/views/pages/developer-guide/pos-marketplace-template/core-concepts.liquid @@ -27,7 +27,7 @@ Business logic and presentation logic are separated and should not interfere wit Command is our concept to encapsulate business rules. By following our recommendation, you can improve the consistency of your code to make it easier to onboard new developers to the project and take over existing projects. We use the same pattern for all of our templates. The advantage of using this architecture is that it is easy to re-use the command - you can execute it in a live web request as well as a background job. It is also easy to copy it across different projects. -Command are located in `/app/views/partials/lib/commands` +Command are located in `app/views/partials/lib/commands` - For business logic use commands - A generic command consists of 3 stages: diff --git a/app/views/pages/get-started/build-your-first-app/building-the-user-interface.liquid b/app/views/pages/get-started/build-your-first-app/building-the-user-interface.liquid index 368cf1e78..8fa32dbe1 100644 --- a/app/views/pages/get-started/build-your-first-app/building-the-user-interface.liquid +++ b/app/views/pages/get-started/build-your-first-app/building-the-user-interface.liquid @@ -123,11 +123,11 @@ Currently, you are using example content and don’t have any logic here, that ## Adding visual styling with CSS -You need a CSS file here. Since the [publicly visible static files](/get-started/working-with-the-code-and-files/#basic-overview-of-the-directory-structure) are placed in the `/app/assets/` directory, you will save your CSS file as `/app/assets/styles/app.css`. +You need a CSS file here. Since the [publicly visible static files](/get-started/working-with-the-code-and-files/#basic-overview-of-the-directory-structure) are placed in the `app/assets/` directory, you will save your CSS file as `/app/assets/styles/app.css`. We don’t need to get into details here, you can just copy this example CSS to make your app look a little nicer: -#### /app/assets/styles/app.css +#### app/assets/styles/app.css ```css body { @@ -159,7 +159,7 @@ li { } ``` -You still need to **link the CSS file on your page**. The proper place for this would be the `/app/views/layouts/application.html.liquid`: +You still need to **link the CSS file on your page**. The proper place for this would be the `app/views/layouts/application.html.liquid`: #### app/view/layout/application.liquid diff --git a/app/views/pages/get-started/build-your-first-app/removing-data-from-the-database.liquid b/app/views/pages/get-started/build-your-first-app/removing-data-from-the-database.liquid index 50b32c5e8..3b9b1995d 100644 --- a/app/views/pages/get-started/build-your-first-app/removing-data-from-the-database.liquid +++ b/app/views/pages/get-started/build-your-first-app/removing-data-from-the-database.liquid @@ -9,9 +9,9 @@ A simple GraphQL mutation is usually enough to remove a single entry from the da Start by preparing a new GraphQL file that will handle the deletion of an item. Remember that when you have to delete a record from the database, you need to be able to point to it directly first. For that, you are going to use the record’s unique _ID_. -To follow the pattern established in the previous parts of this tutorial series, create the new file for the new GraphQL mutation – `/app/graphql/item/delete.graphql`: +To follow the pattern established in the previous parts of this tutorial series, create the new file for the new GraphQL mutation – `app/graphql/item/delete.graphql`: -#### /app/graphql/item/delete.graphql +#### app/graphql/item/delete.graphql ```graphql mutation item_delete( @@ -74,9 +74,9 @@ In this tutorial, you’ve prepared a working list of `To Do` items. Each of tho The simplest way of making this work would be to use the mutation that you’ve just created. If you remove the record from the database, it will be gone from your list. -To trigger the mutation, you would require a new page that will become a bridge between the user interface and the GraphQL mutation. Create that new page under `/app/views/pages/item/delete.liquid`: +To trigger the mutation, you would require a new page that will become a bridge between the user interface and the GraphQL mutation. Create that new page under `app/views/pages/item/delete.liquid`: -#### /app/views/pages/item/delete.liquid +#### app/views/pages/item/delete.liquid {% raw %} ```liquid @@ -128,7 +128,7 @@ else One last step you musn’t forget is to update the `index` page. You have a `form` with the button that – when clicked – should remove the item from the list. You need to update the `
` with the proper `action=""` and you need to pass the `id` of the item you plan to remove. Do this in a hidden `input`: -#### /app/views/pages/index.liquid +#### app/views/pages/index.liquid {% raw %} ```liquid @@ -141,4 +141,4 @@ One last step you musn’t forget is to update the `index` page. You have a `for To test the data removal functionality, add an item or a few to your _To Do_ list and then click on the _Mark as done_ button, which should effectively remove that particular item from your list. -{% render 'alert/next', content: 'Sending email notifications', url: '/get-started/build-your-first-app/sending-email-notifications' %} \ No newline at end of file +{% render 'alert/next', content: 'Sending email notifications', url: '/get-started/build-your-first-app/sending-email-notifications' %} diff --git a/app/views/pages/get-started/build-your-first-app/retrieving-and-presenting-the-data-on-the-page.liquid b/app/views/pages/get-started/build-your-first-app/retrieving-and-presenting-the-data-on-the-page.liquid index 4694fc426..1366188b3 100644 --- a/app/views/pages/get-started/build-your-first-app/retrieving-and-presenting-the-data-on-the-page.liquid +++ b/app/views/pages/get-started/build-your-first-app/retrieving-and-presenting-the-data-on-the-page.liquid @@ -10,9 +10,9 @@ You can display the data on your page by building a query that gets the data fro ## Getting the data from the database -Start with building a query that will pull the data from the `item` table. Any filename will be good of course, but we found that for the sake of extending the query in the future you might consider saving the query in `/app/graphql/item/search.graphql`: +Start with building a query that will pull the data from the `item` table. Any filename will be good of course, but we found that for the sake of extending the query in the future you might consider saving the query in `app/graphql/item/search.graphql`: -#### /app/graphql/item/search.graphql +#### app/graphql/item/search.graphql ```graphql query item_search { @@ -73,7 +73,7 @@ Assuming you have a page that you want to present the data on, and the query tha ``` {% endraw %} -You don’t have to use the full path to the GraphQL query as the platform knows where to look for those. That simple line will run the query (placed in `/app/graphql/item/search.graphql`) and assign the results to the variable `items`. +You don’t have to use the full path to the GraphQL query as the platform knows where to look for those. That simple line will run the query (placed in `app/graphql/item/search.graphql`) and assign the results to the variable `items`. The results are in the form of a _hash_. If you are comming from the JavaScript or PHP world you might name those _objects_. @@ -91,7 +91,7 @@ The results are in the form of a _hash_. If you are comming from the JavaScript When you have the array of items, use the for loop to iterate on it and render them on the page using Liquid: -#### /app/views/pages/index.liquid +#### app/views/pages/index.liquid {% raw %} ```liquid @@ -112,7 +112,7 @@ This code will repeat whatever is between the {% raw %}`{% for %}`{% endraw %} a There is one more thing you could do: display some information when there are no items to show on the page. As you have requested the number of `total_entries` in your GraphQL query, an `if` statement using it would be enough: -#### /app/views/pages/index.liquid +#### app/views/pages/index.liquid {% raw %} ```liquid @@ -122,9 +122,9 @@ There is one more thing you could do: display some information when there are no ``` {% endraw %} -So, the final code of `/app/views/pages/index.liquid` would look like this: +So, the final code of `app/views/pages/index.liquid` would look like this: -#### /app/views/pages/index.liquid +#### app/views/pages/index.liquid {% raw %} ```liquid diff --git a/app/views/pages/get-started/build-your-first-app/saving-data-to-the-database.liquid b/app/views/pages/get-started/build-your-first-app/saving-data-to-the-database.liquid index 70cf16fc3..b4bd772e7 100644 --- a/app/views/pages/get-started/build-your-first-app/saving-data-to-the-database.liquid +++ b/app/views/pages/get-started/build-your-first-app/saving-data-to-the-database.liquid @@ -22,11 +22,11 @@ You would also need a **unique ID** for every record you store, but platformOS c | 2 | Clean up in the kitchen | | 3 | Build my first app on platformOS | -To build a database table on platformOS, you will have to *create a file* that will tell the platform what _properties_ (or _columns_ if you are used to think about the database as a table) you need and what type of data you would like to store in them. Those files need to be placed in a dedicated directory: `/app/schema/`. +To build a database table on platformOS, you will have to *create a file* that will tell the platform what _properties_ (or _columns_ if you are used to think about the database as a table) you need and what type of data you would like to store in them. Those files need to be placed in a dedicated directory: `app/schema/`. To describe the table structure, use the data-serialization language called YAML. For a basic table like yours, create a file named `item.yml`: -#### /app/schema/item.yml +#### app/schema/item.yml ```yaml name: item @@ -75,13 +75,13 @@ If everything went right, you should see the newly created `item` table on the l ## Save the data in the database -To actually save some data in the database, you need to start by creating a GraphQL mutation in the `/app/graphql/` directory. +To actually save some data in the database, you need to start by creating a GraphQL mutation in the `app/graphql/` directory. In this example, you only have a single table in your application, so you could put the file directly in that folder, but when your application will grow, you might want to consider a more standarized way of organizing the queries. -So, to accommodate for future growth, save the file in `/app/graphql/item/create.graphql`: +So, to accommodate for future growth, save the file in `app/graphql/item/create.graphql`: -#### /app/graphql/item/create.graphql +#### app/graphql/item/create.graphql ```graphql mutation item_create( @@ -158,9 +158,9 @@ To save the data that the user provided in the application you need three things If you are following this tutorial, you already have a form on your index page, you just wrote the GraphQL query, so the only thing left would be to build a page that will take the data from the `` and pass it to the query. -To do this, create a new page: `/app/views/pages/item/create.liquid`: +To do this, create a new page: `app/views/pages/item/create.liquid`: -#### /app/views/pages/item/create.liquid +#### app/views/pages/item/create.liquid {% raw %} ```liquid @@ -199,7 +199,7 @@ When you want to write more Liquid than a single line, put the code between {% r ``` {% endraw %} -This is a place where you execute your GraphQL query. You run the query located in `/app/graphql/item/create.graphql`, but you don’t need to pass the whole path, as the platform knows where to look for the queries. You defined a variable called $title in your graphql, so you need to pass it to the query, but you don’t want it to be empty, that’s why you assign a value context.params.title to it. All query params passed to the page will be always available in the context.params variable. +This is a place where you execute your GraphQL query. You run the query located in `app/graphql/item/create.graphql`, but you don’t need to pass the whole path, as the platform knows where to look for the queries. You defined a variable called $title in your graphql, so you need to pass it to the query, but you don’t want it to be empty, that’s why you assign a value context.params.title to it. All query params passed to the page will be always available in the context.params variable. {% raw %} ```liquid @@ -230,9 +230,9 @@ endif If you have never seen Liquid before, don’t worry, the syntax will become familiar after a little bit of using it. -The last thing left for you to do before testing would be to add the URL to your newly created page in the `` so that when the user submits the form, it will redirect to the page that controlls the process of saving the data. Let’s get back to `/app/view/pages/index.liquid` and update the code: +The last thing left for you to do before testing would be to add the URL to your newly created page in the `` so that when the user submits the form, it will redirect to the page that controlls the process of saving the data. Let’s get back to `app/view/pages/index.liquid` and update the code: -#### /app/view/pages/index.liquid +#### app/view/pages/index.liquid ```html diff --git a/app/views/pages/get-started/build-your-first-app/sending-email-notifications.liquid b/app/views/pages/get-started/build-your-first-app/sending-email-notifications.liquid index 507e1ba1f..69005f029 100644 --- a/app/views/pages/get-started/build-your-first-app/sending-email-notifications.liquid +++ b/app/views/pages/get-started/build-your-first-app/sending-email-notifications.liquid @@ -39,7 +39,7 @@ platformOS treats emails similarly to standard HTML pages. You can create a sepa Assuming, for the purpose of this tutorial, that you would like an independent HTML structure for all of your emails, you can create a new layout in the `app/views/layout/` folder, in a file called `mailer.liquid`. The name can be anything you want. -#### /app/views/layout/mailer.liquid +#### app/views/layout/mailer.liquid {% raw %} ```liquid @@ -73,7 +73,7 @@ If you ever styled an email, you know how many limitations the CSS for emails ha For your _To Do_ app, add an email notification when you add a new item on the list. The content of email messages should be kept in the `app/emails/` folder. To follow the pattern you established in this tutorial you’re going to create the file as `app/emails/item/created.liquid`: -#### /app/emails/item/created.liquid +#### app/emails/item/created.liquid {% raw %} ```liquid @@ -102,7 +102,7 @@ The last line is just the content of the email you’re going to send. Since you To actually send an email through the platform you need to trigger it via GraphQL. To make it as simple as you can, create a new file under `app/graphql/item/email/created.graphql` that will handle sending the email after you create a new item in the database: -#### /app/graphql/item/email/created.graphql +#### app/graphql/item/email/created.graphql ```graphql mutation email_item_created( @@ -145,7 +145,7 @@ If you are following this tutorial and building the _To Do_ app, then you probab Since you have a separate page that handles this (`app\views\pages\item\create.liquid`), you can just plug into the right place and trigger the GraphQL mutation after the new item was successfully saved in the database: -#### /app/views/pages/item/create.liquid +#### app/views/pages/item/create.liquid {% raw %} ```liquid @@ -185,7 +185,7 @@ In the `app/emails/item/created.liquid` you’ve hardcoded the `to:` address, bu It is totally possible to use a variable in the configuration section of a page. As an example, in `app/emails/item/created.liquid` you could use `{% raw %}{{ data.recipient }}{% endraw %}`: -#### /app/emails/item/created.liquid +#### app/emails/item/created.liquid {% raw %} ```yaml @@ -202,7 +202,7 @@ Notice that the Liquid variable is surrounded with quotation marks. This would also mean that you need to pass the `recipient` variable to your GraphQL mutation. Since you are passing the whole `context.params` as described above, you could use a hidden form element in the ``: -#### /app/views/pages/index.liquid +#### app/views/pages/index.liquid ```html diff --git a/app/views/pages/get-started/build-your-first-app/user-authentication.liquid b/app/views/pages/get-started/build-your-first-app/user-authentication.liquid new file mode 100644 index 000000000..ced4c9749 --- /dev/null +++ b/app/views/pages/get-started/build-your-first-app/user-authentication.liquid @@ -0,0 +1,224 @@ +--- +metadata: + title: User Authentication + description: Follow these step-by-step tutorials to build a simple To Do List application on platformOS. +converter: markdown +--- + +User authentication is a fundamental aspect of web development that involves the process of verifying and confirming the identity of users accessing a web application or system. It is a crucial component for securing sensitive information and ensuring that only authorized individuals can perform certain actions or access specific resources. + +The primary goal of user authentication is to validate the identity of a user by requiring them to provide credentials, typically a combination of a username and password. In this chapter we will discuss a typical Session-Based Authentication flow, however in platformOS you can also implement Token-Based Authentication, integrate through OAuth etc. + +## Registration + +In the previous chapter, before we could store the Item in the database, we had to define the table in app/schema. However when it comes to Users, we already have a built-in table, so there is no need to do it. We will just have to use different mutations - instead of using `record_create`, we'll use `user_create`. + +Let's start by creating a GraphQL mutation `app/graphql/user/create.graphql`. + +### Creating a User using GraphQL + +#### app/graphql/user/create.graphql + +```graphql +mutation create_user($email: String!, $password: String!) { + user_create( + user: { + email: $email + password: $password + } + ){ + id + } +} + +``` + +As you can see, this mutation takes different arguments than `record_create`. You can refer to [GraphQL API reference for user_create](/api-reference/graphql/data/mutations/user-create) + +### Sign up + +The next step would be to create a Page, which will provide user's input to the`user_create` GraphQL mutation and execute it. + +To do this, create a new page: `app/views/pages/user/create.liquid`: + +#### app/views/pages/user/create.liquid + +{% raw %} +```liquid +--- +method: post +--- + +{% liquid + graphql result = 'user/create', email: context.params.email, password: context.params.password + + if result.user_create.id + sign_in user_id: result.user_create.id + redirect_to '/todo' + else + echo 'Something went wrong' + echo result.errors + endif +%} +``` +{% endraw %} + +Similarly to the [Saving Data to the Database](/get-started/build-your-first-app/saving-data-to-the-database#save-the-data-in-the-database) chapter, we execute the GraphQL mutation with User parameters. Behind the scenes, platformOS will check, if the provided email is unique, and only then create a User in the database. Moreover, platformOS will take care of the security by using `bcrypt` password-hashing function before saving the user password in the database. + +{% include 'alert/tip', content: 'If you want, you can enforce some rules to ensure that passwords chosen by your users meet security guidelines, like for example minimum length, containing at least one digit, special character etc. In practice, we would recommended using platformOS Core Module, as amongs other features, it provides architecture for writing re-usable code via Commands pattern, and it includes common code ready to be used, like validators.' %} + +If the email provided by the user will be correct, the GraphQL mutation will return the id and the newly registered User. To offer the User proper UX, we will automatically sign in the user to the system by providing the id as an argument to the [platformOS sign_in tag](https://documentation.platformos.com/api-reference/liquid/platformos-tags#sign-in): + +```liquid +sign_in user_id: result.user_create.id +``` +{% include 'alert/tip', content: 'By default, the session will be valid for 1 year. You can control when the session will expire by specifying `timeout_in_minutes` argument when invoking `sign_in` tag' %} + +{% include 'alert/tip', content: 'Behind the scenes, the `sign_in` tag will drop the current user session and create a new session to avoid [Session Fixation vulnerability](https://owasp.org/www-community/attacks/Session_fixation). The new sesion id will be stored in `_pos_session` cookie.' %} + +If the email provided by the user will be incorrect, a 500 error will be thrown - platformOS expects that the input validation will be done on Liquid level, and if GraphQL receives invalid input, it will thrown an error, which the developer will be able to see via `pos-cli logs` or `pos-cli gui serve` -> http://localhost:3333/logs. The error log should be self descriptive, and will look like this: `"Liquid error: [{\"message\":\"GraphQL Error: Email is not a valid email address\",\"locations\":[{\"line\":2,\"column\":3}],\"path\":[\"user_create\"],\"extensions\":{\"messages\":{\"email\":\"is not a valid email address\"},\"codes\":{\"email\":[{\"error\":\"email\"}]}},\"query\":\"create_user\"}]\n url: /user/create\n page: user/create"` + + +The last thing left to do is to create a Page to render a registration form to the User, allowing them to provide their email and password. Let’s create a file `app/view/pages/sign-up.liquid` with the following code: + +#### app/view/pages/sign-up.liquid + +```liquid + + + + + + +``` + +{% include 'alert/tip', content: 'platformOS automatically takes care of Cross Site Request Forgery (CSRF) attacks by invalidating sessions upon receiving a POST request without authenticity token. If you experience automatic user log out after making a POST request (including XHR requests!), most likely it means you have forgot to send `authenticity_token` either via a hidden input, as in the example above, or as a Header' %} + + +### Testing the flow + +To test the whole flow, get to the /sign-up page in your application, provide a valid email and any password, and click the _Sign up_ button. + +You will be able to see a new User record using the `pos-cli gui serve` by going to http://localhost:3333/users. + +## Accessing Current User + +In platformOS the simplest way to access authenticated user's `email` or `id` is via `context.current_user`. Let's say you would like to display current user's email - typical way of doing it is to include it in the layout. For this purpose edit `app/views/layouts/application.liquid` and add at the beginning of the `` the following code: + +#### app/views/layouts/application.liquid +```liquid +{% raw %}{% if context.current_user %}
You are currently log in as {{ context.current_user.email }}
{% endif %}{% endraw %} +``` + +For the more complex use cases, for example accessing user's JWT or OTP for 2FA, you will have to use [current_user GraphQL query](/api-reference/graphql/data/queries/current-user), however this is outside of the scope of this tutorial. You can find advanced User tutorials in the [User section of the Developer Guide](/developer-guide/users/users) + +## Manual Log Out + +Logging User out is equivalent of destroying current session, which can be achieved via GraphQL mutation. Create a file `app/views/graphql/session/delete.graphql` + +#### app/graphql/session/delete.graphql + +```graphql +mutation { + user_session_destroy +} +``` + +Now you can create an endpoint, which will invoke this GraphQL. Because logging out is equivalent of destroying the session, we'll create a new Page named `app/views/pages/session/delete.liquid` + +{% raw %} +```liquid +--- +method: delete +--- +{% liquid + graphql result = 'session/delete' + redirect_to '/todo' +%} +``` +{% endraw %} + +Typically, the Log Out button is rendered in the Layout. For the purpose of the tutorial, we can add the Log Out button next to the information about the currently logged in User in the application layout: + +#### app/views/layouts/application.liquid +```liquid +{% raw %} +{% if context.current_user %} +
You are currently log in as {{ context.current_user.email }} +
+ + + +
+
+{% endif %} +{% endraw %} +``` + +## Signing in the User + +In order to securely sign in the User, we need to verify credentials first. There is a dedicated GraphQL field, which tells us, if provided argument is a valid user's password. To use it, create a new file `app/graphql/user/verify_password.graphql` + +#### app/graphql/user/verify_password.graphql + +```graphql +query verify($email: String!, $password: String!){ + users( + filter: { email: { value: $email} }, per_page: 1 + ){ + results{ + id + email + authenticate{ + password(password: $password) + } + } + } +} +``` + +We are leveraging GraphQL query `users` - by specifying `filter` argument, instead of fetching all users from the database, we will get only those, who meet filtering conditions - in this scenario, we want to get only users with specific email. Because platformOS guarantees that there can be only one user with a given email, we can set `per_page` argument to 1. + +{% include 'alert/tip', content: 'Emails in platformOS are not case sensitive, so for the purpose of the uniquness check, email@example.com and Email@Example.COM are the same.' %} + +We will need a POST endpoint, which will upon successful password verification will log User in, which is equivalent of creating a session. Let's create a new Page `app/views/pages/session/create.liquid` + +#### app/views/pages/session/create.liquid + +{% raw %} +```liquid +--- +method: post +--- +{% liquid + graphql result = 'user/verify_password', email: context.params.email, password: context.params.password + if result.users.results.first.authenticate.password + sign_in user_id: result.users.results.first.id + redirect_to '/todo' + else + echo "Incorrect email or password" + endif +%} +``` +{% endraw %} + +The endpoint will forward user's input to the GraphQL mutation. Because `users` query returns an array, we need to use `first` property of the array, to get the first element of the array. + +{% include 'alert/tip', content: 'Liquid does not throw Null Pointer Exception, so if someone provides the email which does not exist in the database, `result.users.first` will evaluate to `null`, which is is falsy, and invoking any property on a `null` will also return `null` - so `result.users.first.authenticate` will be `null` and `result.users.first.authenticate.password` will be `null` as well. In the end, the if condition will evaluate to `false`, and the user will see "Incorrect email or password" message.' %} + +The last step is to render the log in Form. Let's create a new Page `app/views/pages/sign-in.liquid` + +#### app/views/pages/sign-in.liquid + +{% raw %} +```liquid +
+ + + + +
+``` +{% endraw %} + +Now we should be able to test the authentication flow by signing up at /sign-up page, then log out using the Log Out button rendered in the layout and log in again using /sign-in Page. diff --git a/app/views/pages/use-cases/connecting-angular-spa-platformos.liquid b/app/views/pages/use-cases/connecting-angular-spa-platformos.liquid index 4d40f22a4..dc74a2eb4 100644 --- a/app/views/pages/use-cases/connecting-angular-spa-platformos.liquid +++ b/app/views/pages/use-cases/connecting-angular-spa-platformos.liquid @@ -75,7 +75,7 @@ Find the line with `"outputPath": "dist/web-app",` and change it into: If you are using version control like GIT, it's recommended to add this folder into your `.gitignore` rules. -After building your app, files will now be copied into `/app/assets/web-app`. +After building your app, files will now be copied into `app/assets/web-app`. But now compiled files will be stored under `app/assets/web-app` (like for example `main.js`) and Angular will be looking for it in the root folder. You need to let it know where to find it. diff --git a/app/views/partials/shared/nav/get-started.liquid b/app/views/partials/shared/nav/get-started.liquid index 13c7d5e24..2a44a63be 100644 --- a/app/views/partials/shared/nav/get-started.liquid +++ b/app/views/partials/shared/nav/get-started.liquid @@ -84,3 +84,14 @@ {% include "shared/nav/link", href: "/get-started/build-your-first-app/sending-email-notifications#sending-emails-to-addresses-provided-dynamically", text: "Sending emails to addresses provided dynamically" %} + +
  • + User Authentication + + +