Tutorials / Build a REST API with C# and .NET · Lesson 40 of 45

Frontend

On this page

    Sending the Token

    The login page keeps a token, and nothing uses it yet: the Books page still asks for the books with no token, and shows Unauthorized above an empty table. This lesson makes the Books page and the report page send the token with every request, and brings the pages together into one system with a menu.

    Our goal: Books and report pages that send the token with every request, and a menu that shows the report link only to managers.

    Here is the route:

    1. Write one small shared script that sends the token with every request, and goes to the login page when the API answers 401.
    2. Change the Books page: use that script, show who is logged in, add a Log out button, and show the report link to managers only.
    3. Change the report page to use the script.
    4. Try it as bob and as alice.

    As before, the pages are ugly on purpose: no styling, only the HTML and the script the job needs. Left out: staying logged in after the browser closes, tokens that renew themselves, and logging in with a cookie that scripts cannot read, which the login page named and did not build.

    This is the top of the Books page for a manager when we are done:

    The top of the Books page for alice: Logged in as alice, a Stock report link, and a Log out button


    Where we are

    This is the solution from the end of the login page. One file is new and two change, all in wwwroot:

    BookShop/
      src/
        BookShop.Api/
          wwwroot/
            index.html                     <- changes
            report.html                    <- changes
            auth.js                        <- NEW
            login.html                     (from the last lesson)
    

    Run every command from the solution folder, BookShop/. Keep the app running (dotnet run --project src/BookShop.Api) and refresh the browser after each edit. The table should hold the five sample books from reading books. The Books page is long, so this lesson shows only the new pieces of it, each marked // new or <!-- new -->. The whole page comes twice: first as a preview, and again at the end.

    Here is how the pages fit together once we are done:

    Three pages and one shared script login.html gets the token token kept index.html the Books page a link report.html the report page auth.js: adds the token to every request a 401, or Log out: back to login

    The finished files

    Here are the three files of this lesson, whole, to look at and not to type yet. Do not try to follow them. Notice the shape: auth.js adds the token to every request, and the Books and report pages call apiFetch where they called fetch before. The steps below build each file with the reasons, and the finished Books page comes back at the end, with questions.

    auth.js:

    async function apiFetch(url, options) {
      options = options ?? {};
      options.headers = options.headers ?? {};
    
      const token = sessionStorage.getItem('token');
      if (token) {
        options.headers['Authorization'] = 'Bearer ' + token;
      }
    
      const response = await fetch(url, options);
      if (response.status === 401) {
        location.href = '/login.html';
      }
      return response;
    }
    
    function logOut() {
      sessionStorage.clear();
      location.href = '/login.html';
    }
    

    index.html, the Books page:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Books</title>
    </head>
    <body>
      <h1>Books</h1>
    
      <p>
        Logged in as <span id="user-name"></span>.
        <a href="report.html" id="report-link" hidden>Stock report</a>
        <button type="button" id="log-out">Log out</button>
      </p>
    
      <p id="message"></p>
      <ul id="problems"></ul>
    
      <form id="book-form" novalidate>
        <input type="hidden" name="bookId">
        <p><label>Title <input name="title"></label></p>
        <p><label>Author <input name="author"></label></p>
        <p><label>Price <input name="price" type="number" step="0.01"></label></p>
        <p><label>Stock <input name="stock" type="number"></label></p>
        <p><label>Category <input name="category"></label></p>
        <p>
          <button type="submit">Save</button>
          <button type="button" id="cancel-edit" hidden>Cancel</button>
        </p>
      </form>
    
      <table>
        <thead>
          <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Price</th>
            <th>Stock</th>
            <th>Category</th>
            <th></th>
          </tr>
        </thead>
        <tbody id="book-rows"></tbody>
      </table>
    
      <script src="auth.js"></script>
      <script>
        const form = document.getElementById('book-form');
        const cancelButton = document.getElementById('cancel-edit');
    
        async function loadBooks() {
          const response = await apiFetch('/books');
          if (!response.ok) {
            await showProblem(response);
            return;
          }
    
          const books = await response.json();
          const rows = document.getElementById('book-rows');
          rows.replaceChildren();
    
          for (const book of books) {
            const row = rows.insertRow();
            row.insertCell().textContent = book.title;
            row.insertCell().textContent = book.author;
            row.insertCell().textContent = book.price.toFixed(2);
            row.insertCell().textContent = book.stock;
            row.insertCell().textContent = book.category;
    
            const editButton = document.createElement('button');
            editButton.textContent = 'Edit';
            editButton.addEventListener('click', () => startEdit(book));
    
            const deleteButton = document.createElement('button');
            deleteButton.textContent = 'Delete';
            deleteButton.addEventListener('click', () => deleteBook(book));
    
            row.insertCell().append(editButton, ' ', deleteButton);
          }
        }
    
        function readBook() {
          return {
            title: form.elements.title.value,
            author: form.elements.author.value,
            price: Number(form.elements.price.value),
            stock: Number(form.elements.stock.value),
            category: form.elements.category.value
          };
        }
    
        function startEdit(book) {
          clearProblems();
          form.elements.bookId.value = book.id;
          form.elements.title.value = book.title;
          form.elements.author.value = book.author;
          form.elements.price.value = book.price;
          form.elements.stock.value = book.stock;
          form.elements.category.value = book.category;
          cancelButton.hidden = false;
        }
    
        function stopEdit() {
          form.reset();
          form.elements.bookId.value = '';
          cancelButton.hidden = true;
        }
    
        async function deleteBook(book) {
          if (!confirm('Delete "' + book.title + '"?')) {
            return;
          }
    
          clearProblems();
          const response = await apiFetch('/books/' + book.id, { method: 'DELETE' });
          if (response.ok) {
            await loadBooks();
          } else {
            await showProblem(response);
          }
        }
    
        function clearProblems() {
          document.getElementById('message').textContent = '';
          document.getElementById('problems').replaceChildren();
        }
    
        async function showProblem(response) {
          const problem = await response.json();
          document.getElementById('message').textContent = problem.title;
    
          const list = document.getElementById('problems');
          list.replaceChildren();
          for (const messages of Object.values(problem.errors ?? {})) {
            for (const text of messages) {
              const item = document.createElement('li');
              item.textContent = text;
              list.append(item);
            }
          }
        }
    
        form.addEventListener('submit', async (event) => {
          event.preventDefault();
          clearProblems();
    
          const id = form.elements.bookId.value;
          const response = await apiFetch(id ? '/books/' + id : '/books', {
            method: id ? 'PUT' : 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(readBook())
          });
    
          if (response.ok) {
            stopEdit();
            await loadBooks();
          } else {
            await showProblem(response);
          }
        });
    
        cancelButton.addEventListener('click', stopEdit);
    
        document.getElementById('user-name').textContent = sessionStorage.getItem('userName');
        if (sessionStorage.getItem('role') === 'Manager') {
          document.getElementById('report-link').hidden = false;
        }
        document.getElementById('log-out').addEventListener('click', logOut);
    
        loadBooks();
      </script>
    </body>
    </html>
    

    report.html:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Stock report</title>
    </head>
    <body>
      <h1>Stock report</h1>
    
      <p><a href="/">Books</a></p>                                           <!-- new -->
    
      <p id="message"></p>
    
      <table>
        <thead>
          <tr>
            <th>Category</th>
            <th>Titles</th>
            <th>Copies</th>
            <th>Stock value</th>
          </tr>
        </thead>
        <tbody id="report-rows"></tbody>
      </table>
    
      <script src="auth.js"></script>                                        <!-- new -->
      <script>
        async function loadReport() {
          const response = await apiFetch('/reports/stock');                 // new
          if (!response.ok) {
            document.getElementById('message').textContent =
              'Could not load the report. The API answered ' + response.status + '.';
            return;
          }
    
          const lines = await response.json();
          const rows = document.getElementById('report-rows');
    
          for (const line of lines) {
            const row = rows.insertRow();
            row.insertCell().textContent = line.category;
            row.insertCell().textContent = line.titles;
            row.insertCell().textContent = line.copies;
            row.insertCell().textContent = line.stockValue.toFixed(2);
          }
        }
    
        loadReport();
      </script>
    </body>
    </html>
    

    One shared script for the token

    Both the Books page and the report page need the same thing: add the token to each request, and go to the login page if the API answers 401. Until now, each page had all its script inside it. Two pages with the same job is the moment to move that job into one file, so a change is made in one place.

    Create src/BookShop.Api/wwwroot/auth.js. The whole file is:

    async function apiFetch(url, options) {
      options = options ?? {};
      options.headers = options.headers ?? {};
    
      const token = sessionStorage.getItem('token');
      if (token) {
        options.headers['Authorization'] = 'Bearer ' + token;
      }
    
      const response = await fetch(url, options);
      if (response.status === 401) {
        location.href = '/login.html';
      }
      return response;
    }
    
    function logOut() {
      sessionStorage.clear();
      location.href = '/login.html';
    }
    

    apiFetch is fetch with the token added. The pages call it exactly as they called fetch, with the same address and the same options:

    • The first two lines make sure there is an options object and a headers object to add to. a ?? b means "a, or b if a is missing", the same ?? as in problem.errors ?? {}. A GET passes no options at all; a POST passes its own headers, such as Content-Type, and they are kept.
    • The token is read back with getItem, and added as the Authorization header in the form the API expects: Bearer, a space, the token. If there is no token (nobody has logged in in this tab), no header is added, and the API answers 401.
    • On a 401 the browser goes to the login page. One rule covers both cases: never logged in, and a token that ran out after its hour. A 403 is not sent to the login page, because logging in again would not help (bob would still be bob).
    • The response goes back to the caller, so the page goes on as before: response.ok, response.json(), and so on.

    logOut empties sessionStorage (clear() removes everything this site stored in this tab) and goes to the login page. Be honest about what it does: the browser forgets the token, and the token itself is still valid until it expires, as what a JWT is explained. For a token that only ever lived in this tab, that is fine.

    The login page from the last lesson does not use apiFetch. Its request carries no token yet, and a 401 there means "wrong password", not "go and log in".

    A page loads a script file with <script src="auth.js"></script>. The browser fetches the file, runs it, and then apiFetch and logOut are there for the page's own script below it.


    The Books page

    Open src/BookShop.Api/wwwroot/index.html. Four changes.

    1. A line under the heading: who is logged in, the report link, and a Log out button:

      <h1>Books</h1>
    
      <p>                                                                    <!-- new -->
        Logged in as <span id="user-name"></span>.                           <!-- new -->
        <a href="report.html" id="report-link" hidden>Stock report</a>        <!-- new -->
        <button type="button" id="log-out">Log out</button>                  <!-- new -->
      </p>                                                                   <!-- new -->
    
      <p id="message"></p>
    

    <a href="report.html"> is a link: <a> ("anchor") is the element for a link, and its href is the address to go to. When the visitor clicks it, the browser loads that page, and that page's script starts from the start. The link starts with hidden, an attribute that tells the browser not to show the element. The script shows it to managers. <span> is an element with no look of its own, a place to put the user name.

    2. Load the shared script, in a line of its own just before the page's script:

      <script src="auth.js"></script>                                        <!-- new -->
      <script>
        const form = document.getElementById('book-form');
    

    3. Every fetch becomes apiFetch. There are three: in loadBooks, in deleteBook, and in the form's submit handler. Change the name only, and keep everything inside the brackets:

          const response = await apiFetch('/books');                                     // new
    
          const response = await apiFetch('/books/' + book.id, { method: 'DELETE' });   // new
    
          const response = await apiFetch(id ? '/books/' + id : '/books', {              // new
    

    4. Fill in the line from step 1, at the end of the script, just before loadBooks();:

        cancelButton.addEventListener('click', stopEdit);
    
        document.getElementById('user-name').textContent = sessionStorage.getItem('userName');   // new
        if (sessionStorage.getItem('role') === 'Manager') {                                       // new
          document.getElementById('report-link').hidden = false;                                  // new
        }                                                                                         // new
        document.getElementById('log-out').addEventListener('click', logOut);                     // new
    
        loadBooks();
    

    The user name goes in with textContent, like every other piece of data. The role decides the menu: a manager gets the report link, anyone else does not. The Log out button calls logOut from auth.js.

    ⚠️ Hiding the link is a convenience, not a lock. bob can still type /report.html in the address bar, or call /reports/stock with curl. What keeps the report from him is the 403 from the API, from the last lesson. Never let a page's menu be the only thing between a user and data: anyone can change what a page shows, in the developer tools, in a minute.

    The whole page is at the end of the lesson, to compare.


    The report page

    The report page needs the shared script and apiFetch, and a link back to the Books page, now that the Books page links here. The link is <a href="/">Books</a>, the same kind as the one on the Books page: <a> ("anchor") is the element for a link, and its href is the address to go to. The address / is the Books page. Open src/BookShop.Api/wwwroot/report.html. The whole file is:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Stock report</title>
    </head>
    <body>
      <h1>Stock report</h1>
    
      <p><a href="/">Books</a></p>                                           <!-- new -->
    
      <p id="message"></p>
    
      <table>
        <thead>
          <tr>
            <th>Category</th>
            <th>Titles</th>
            <th>Copies</th>
            <th>Stock value</th>
          </tr>
        </thead>
        <tbody id="report-rows"></tbody>
      </table>
    
      <script src="auth.js"></script>                                        <!-- new -->
      <script>
        async function loadReport() {
          const response = await apiFetch('/reports/stock');                 // new
          if (!response.ok) {
            document.getElementById('message').textContent =
              'Could not load the report. The API answered ' + response.status + '.';
            return;
          }
    
          const lines = await response.json();
          const rows = document.getElementById('report-rows');
    
          for (const line of lines) {
            const row = rows.insertRow();
            row.insertCell().textContent = line.category;
            row.insertCell().textContent = line.titles;
            row.insertCell().textContent = line.copies;
            row.insertCell().textContent = line.stockValue.toFixed(2);
          }
        }
    
        loadReport();
      </script>
    </body>
    </html>
    

    The message for a failed request was already there. It now gets its first real use: a 403.


    Try it

    Open the developer tools (F12) on the Network tab before each case, so you can watch the requests. Each case starts where the one before ended.

    Case A: open the Books page without logging in

    Open http://localhost:5101/ in a new tab. The address bar ends on /login.html. The Network tab shows why, in order:

    200  /
    200  /auth.js
    401  /books
    200  /login.html
    

    The page loaded, asked for the books with no token, got a 401, and apiFetch sent the browser to the login page. (You may also see a 404 for /favicon.ico, the little icon browsers ask every site for; ignore it.)

    Case B: bob, with his password

    Now bob and bob-pass-1:

    {"userName":"bob","password":"bob-pass-1"}
    

    The login answers 200, the browser goes to the Books page, and GET /books answers 200 with the five books. Click that request in the Network tab and look at its request headers: there is the Authorization: Bearer eyJ... line that apiFetch added. The top of the page shows bob, and no report link:

    The top of the Books page for bob: Logged in as bob, and a Log out button, with no Stock report link

    The token is in sessionStorage now. In the Console, sessionStorage.getItem('role') answers 'Staff'.

    Case C: bob types the report address

    bob knows the address. He types http://localhost:5101/report.html. The page loads, because pages are open to anyone, and then:

    Could not load the report. The API answered 403.
    

    The headings of the table are there, and no numbers. The request for /reports/stock carried bob's good token, and the API said 403. apiFetch left the page alone, because a 403 is not a reason to log in again.

    Case D: log out, and log in as alice

    Go back to the Books page and click Log out. The browser is on the login page, and sessionStorage.length in the Console answers 0: the token is gone. Log in as alice with alice-pass-1. The top of the Books page now has the link:

    The top of the Books page for alice: Logged in as alice, a Stock report link, and a Log out button

    Click Stock report. The request carries alice's token, the role is Manager, and the report shows the same numbers as in the report screen lesson:

    Category Titles Copies Stock value
    Cooking 2 6 181.00
    Programming 3 22 852.30

    Adding, editing and deleting books work as before, for both users: apiFetch keeps the Content-Type header the form sends and adds the token next to it. We added a book as bob to check, and removed it again.


    The whole Books page

    Compare with your file:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Books</title>
    </head>
    <body>
      <h1>Books</h1>
    
      <p>
        Logged in as <span id="user-name"></span>.
        <a href="report.html" id="report-link" hidden>Stock report</a>
        <button type="button" id="log-out">Log out</button>
      </p>
    
      <p id="message"></p>
      <ul id="problems"></ul>
    
      <form id="book-form" novalidate>
        <input type="hidden" name="bookId">
        <p><label>Title <input name="title"></label></p>
        <p><label>Author <input name="author"></label></p>
        <p><label>Price <input name="price" type="number" step="0.01"></label></p>
        <p><label>Stock <input name="stock" type="number"></label></p>
        <p><label>Category <input name="category"></label></p>
        <p>
          <button type="submit">Save</button>
          <button type="button" id="cancel-edit" hidden>Cancel</button>
        </p>
      </form>
    
      <table>
        <thead>
          <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Price</th>
            <th>Stock</th>
            <th>Category</th>
            <th></th>
          </tr>
        </thead>
        <tbody id="book-rows"></tbody>
      </table>
    
      <script src="auth.js"></script>
      <script>
        const form = document.getElementById('book-form');
        const cancelButton = document.getElementById('cancel-edit');
    
        async function loadBooks() {
          const response = await apiFetch('/books');
          if (!response.ok) {
            await showProblem(response);
            return;
          }
    
          const books = await response.json();
          const rows = document.getElementById('book-rows');
          rows.replaceChildren();
    
          for (const book of books) {
            const row = rows.insertRow();
            row.insertCell().textContent = book.title;
            row.insertCell().textContent = book.author;
            row.insertCell().textContent = book.price.toFixed(2);
            row.insertCell().textContent = book.stock;
            row.insertCell().textContent = book.category;
    
            const editButton = document.createElement('button');
            editButton.textContent = 'Edit';
            editButton.addEventListener('click', () => startEdit(book));
    
            const deleteButton = document.createElement('button');
            deleteButton.textContent = 'Delete';
            deleteButton.addEventListener('click', () => deleteBook(book));
    
            row.insertCell().append(editButton, ' ', deleteButton);
          }
        }
    
        function readBook() {
          return {
            title: form.elements.title.value,
            author: form.elements.author.value,
            price: Number(form.elements.price.value),
            stock: Number(form.elements.stock.value),
            category: form.elements.category.value
          };
        }
    
        function startEdit(book) {
          clearProblems();
          form.elements.bookId.value = book.id;
          form.elements.title.value = book.title;
          form.elements.author.value = book.author;
          form.elements.price.value = book.price;
          form.elements.stock.value = book.stock;
          form.elements.category.value = book.category;
          cancelButton.hidden = false;
        }
    
        function stopEdit() {
          form.reset();
          form.elements.bookId.value = '';
          cancelButton.hidden = true;
        }
    
        async function deleteBook(book) {
          if (!confirm('Delete "' + book.title + '"?')) {
            return;
          }
    
          clearProblems();
          const response = await apiFetch('/books/' + book.id, { method: 'DELETE' });
          if (response.ok) {
            await loadBooks();
          } else {
            await showProblem(response);
          }
        }
    
        function clearProblems() {
          document.getElementById('message').textContent = '';
          document.getElementById('problems').replaceChildren();
        }
    
        async function showProblem(response) {
          const problem = await response.json();
          document.getElementById('message').textContent = problem.title;
    
          const list = document.getElementById('problems');
          list.replaceChildren();
          for (const messages of Object.values(problem.errors ?? {})) {
            for (const text of messages) {
              const item = document.createElement('li');
              item.textContent = text;
              list.append(item);
            }
          }
        }
    
        form.addEventListener('submit', async (event) => {
          event.preventDefault();
          clearProblems();
    
          const id = form.elements.bookId.value;
          const response = await apiFetch(id ? '/books/' + id : '/books', {
            method: id ? 'PUT' : 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(readBook())
          });
    
          if (response.ok) {
            stopEdit();
            await loadBooks();
          } else {
            await showProblem(response);
          }
        });
    
        cancelButton.addEventListener('click', stopEdit);
    
        document.getElementById('user-name').textContent = sessionStorage.getItem('userName');
        if (sessionStorage.getItem('role') === 'Manager') {
          document.getElementById('report-link').hidden = false;
        }
        document.getElementById('log-out').addEventListener('click', logOut);
    
        loadBooks();
      </script>
    </body>
    </html>
    

    Where we ended

    Compare with your solution:

    BookShop/
      src/
        BookShop.Api/
          wwwroot/
            login.html                     logs in, keeps the token, role and name in sessionStorage
            auth.js                        apiFetch (adds the token, a 401 goes to login) and logOut
            index.html                     the Books screen, through apiFetch, with a menu for the role
            report.html                    the stock report, through apiFetch
    

    The Books page, index.html, is the whole file shown just above. Here are the other two finished files, the same as at the start of the lesson, to read once more before the questions.

    auth.js:

    async function apiFetch(url, options) {
      options = options ?? {};
      options.headers = options.headers ?? {};
    
      const token = sessionStorage.getItem('token');
      if (token) {
        options.headers['Authorization'] = 'Bearer ' + token;
      }
    
      const response = await fetch(url, options);
      if (response.status === 401) {
        location.href = '/login.html';
      }
      return response;
    }
    
    function logOut() {
      sessionStorage.clear();
      location.href = '/login.html';
    }
    

    report.html:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Stock report</title>
    </head>
    <body>
      <h1>Stock report</h1>
    
      <p><a href="/">Books</a></p>                                           <!-- new -->
    
      <p id="message"></p>
    
      <table>
        <thead>
          <tr>
            <th>Category</th>
            <th>Titles</th>
            <th>Copies</th>
            <th>Stock value</th>
          </tr>
        </thead>
        <tbody id="report-rows"></tbody>
      </table>
    
      <script src="auth.js"></script>                                        <!-- new -->
      <script>
        async function loadReport() {
          const response = await apiFetch('/reports/stock');                 // new
          if (!response.ok) {
            document.getElementById('message').textContent =
              'Could not load the report. The API answered ' + response.status + '.';
            return;
          }
    
          const lines = await response.json();
          const rows = document.getElementById('report-rows');
    
          for (const line of lines) {
            const row = rows.insertRow();
            row.insertCell().textContent = line.category;
            row.insertCell().textContent = line.titles;
            row.insertCell().textContent = line.copies;
            row.insertCell().textContent = line.stockValue.toFixed(2);
          }
        }
    
        loadReport();
      </script>
    </body>
    </html>
    

    The system the course set out to build is complete: an API with a books screen and a report screen, logging in, and two roles. Staff see the books. Managers see the books and the report. The API decides, and the pages follow.


    Check your reading

    You have now seen all the files of this lesson. This is a practice round to read them back. The answers are folded away, so guess first. Nothing here is new: each answer comes from the sections above, and every "what happens" was tried on the real pages.

    What is this doing?

    Read each line on its own and say what it does.

    1. const token = sessionStorage.getItem('token'); if (token) { options.headers['Authorization'] = 'Bearer ' + token; }
    Show answer

    It reads the token that the login page kept. If there is one, it adds the Authorization header to the request. With no token (nobody has logged in in this tab) no header is added, and the API answers 401.

    1. options.headers['Authorization'] = 'Bearer ' + token;
    Show answer

    It adds the Authorization header to the request: the word Bearer, a space, and the token.

    1. if (sessionStorage.getItem('role') === 'Manager') { document.getElementById('report-link').hidden = false; }
    Show answer

    It shows the report link, which starts hidden, only when the logged-in user is a manager.

    Why is it written this way?

    Each of these has a reason, and the lesson gave it.

    1. Why does login.html use plain fetch and not apiFetch?
    Show answer

    The login request has no token yet, and a 401 there means "wrong password", not "go and log in". With apiFetch, a wrong password would send the browser back to the login page and the message would be lost.

    1. Why is apiFetch in its own file, auth.js, and not inside each page?
    Show answer

    Two pages need the same job: add the token, and go to the login page on a 401. In one shared file there is one place to change, and the pages cannot drift apart.

    In what order?

    1. Put these five in the order they happen when bob logs in: (a) the browser goes to the Books page, (b) the login page sends POST /auth/login, (c) apiFetch sends GET /books with the token, (d) the login page keeps the token, role and name in sessionStorage, (e) the API answers 200 with a token and a role.
    Show answer

    b, e, d, a, c. The login is sent and answered, the answer is kept, and only then does the browser move to the Books page, whose first request carries the token.

    What happens if...

    Each change below was tried on the real pages.

    1. You remove the line <script src="auth.js"></script> from the Books page?
    Show answer

    The table stays empty and the Console shows logOut is not defined in red. No request for /books is made at all: the script stops at the line that uses logOut, before it reaches loadBooks();.

    1. You log in as bob, remove the token in the Console with sessionStorage.removeItem('token'), and reload the Books page?
    Show answer

    The page asks for /books with no Authorization header, the API answers 401, and apiFetch sends the browser to the login page.

    Find the mistake

    1. After a correct login, the browser lands on the Books page and is sent straight back to the login page. The Network tab shows a 401 for /books. What is wrong with this line?
    options.headers['Authorization'] = 'Bearer' + token;
    
    Show answer

    There is no space after Bearer. The header reads Bearerbob..., the API does not recognise it as a token and answers 401, and apiFetch treats a 401 as "go and log in". It should be 'Bearer ' + token.


    Recap

    • auth.js is a script shared by two pages. apiFetch adds Authorization: Bearer to each request and goes to the login page on a 401. A 403 stays on the page.
    • Log out makes the browser forget the token. The token itself stays valid until it expires.
    • The menu shows the report link only to managers, with hidden. That is a convenience: the API's 403 is what protects the report.
    • The pages now come together: the login page gets the token, auth.js sends it, and the menu follows the role.

    Questions to think about

    Each question has its answer folded away underneath. Guess first, then open it to check.

    1. bob does not see the Stock report link. Is that what keeps the report from him?
    Show answer

    No. Hiding the link only tidies the menu. The API refuses his token on /reports/stock with a 403, whatever the page shows.

    1. Why does apiFetch go to the login page on a 401, but not on a 403?
    Show answer

    A 401 means the API does not know who you are, which logging in fixes. A 403 means it knows, and the answer would be the same after logging in again.

    Next lesson: logging, the program's own diary of what happened, now that there are logins, refusals and reports to keep track of.

    ← The Login PageLogging →