# Authentication and Real-Time Data

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Working with APIs
Source: https://www.devtutorials.ai/learn/vue/apis/authentication-and-real-time-data

> Logging in with a token, where to keep tokens safely, sending the Authorization header, an auth store with Pinia, protecting routes, refreshing an expired token and retrying, logging out, and live data with polling, Server-Sent Events and WebSockets, with reconnect and cleanup.

## Who Are You, and What Is New?

- Many pages are only for logged-in users: Meera's kitchen screen, a customer's orders.
- **Authentication** = proving who you are, usually with an email and password.
- After login, the server gives the app a **token**: a long secret text that says "this is Meera".
- The app sends the token with every request. The server checks it.
- Some data changes all the time: new orders, chat messages. **Real-time data** reaches the page without a refresh.

**Simple lesson:** log in once, send the token every time, and let live data flow in.

### A Real-World Example

- Meera logs in to her kitchen screen with her password.
- The app keeps the token, then asks the server "who am I?" with it.
- Try a wrong password first, then `dosa123`.

const API = "https://api.meeraskitchen.dev";
const email = ref<string>("meera@kitchen.dev");
const password = ref<string>("");
const token = ref<string | null>(null);
const message = ref<string>("");

async function logIn(): Promise<void> {
    const res = await fetch(\`\${API}/login\`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email: email.value, password: password.value }),
    });
    const data = await res.json();
    if (!res.ok) {
      message.value = data.message;
      return;
    }
    token.value = data.token;
    const me = await fetch(\`\${API}/me\`, { headers: { Authorization: \`Bearer \${token.value}\` } });
    message.value = \`Hello, \${(await me.json()).name}!\`;
}
</script>

<template>
    <form @submit.prevent="logIn">
      <input v-model="email" placeholder="Email" />
      <input v-model="password" type="password" placeholder="Password (try dosa123)" />
      <button>Log in</button>
    </form>
    <p>{{ message }}</p>
    <p v-if="token"><small>Token: {{ token }}</small></p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** a wrong password got `401` and the server's message.
- With `dosa123`, the server returned a token. `/me` accepted it in the `Authorization` header.

A real server checks passwords against a database and signs real tokens, often JWTs. In this lab, the pretend API accepts the password `dosa123`, gives tokens like `tok-1`, and logs every request in the Console.

### What You Will Learn

1. How a token login works.
2. Where to keep a token, and the risks of each place.
3. How to send the `Authorization` header on every request.
4. How to keep login state in a Pinia store and protect pages.
5. How to refresh an expired token and retry.
6. How to log out properly.
7. How to show live data with polling.
8. How to use Server-Sent Events.
9. How to use a WebSocket, with reconnect and cleanup.

## The Token Flow

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-auth" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-auth">Animation: the app posts email and password to /login and receives a token. Later requests carry the token in the Authorization header. When the server answers 401 token expired, the app calls /refresh, gets a new token and retries the request</title>
  <rect x="15" y="25" width="150" height="150" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="90" y="18" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Vue app</text>
  <rect x="475" y="25" width="150" height="150" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="550" y="18" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Server</text>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <text x="320" y="48" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="opacity" values="0;1;1;0.3;0.3" keyTimes="0;0.05;0.2;0.25;1" dur="10s" repeatCount="indefinite" />1. POST /login (email, password) →</text>
    <text x="320" y="70" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1;0.3;0.3" keyTimes="0;0.2;0.25;0.4;0.45;1" dur="10s" repeatCount="indefinite" />← 2. 200 {"{ token }"}</text>
    <text x="320" y="96" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="opacity" values="0;0;1;1;0.3;0.3" keyTimes="0;0.4;0.45;0.55;0.6;1" dur="10s" repeatCount="indefinite" />3. GET /secure/orders + Bearer token →</text>
    <text x="320" y="120" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-red)" }}><animate attributeName="opacity" values="0;0;1;1;0.3;0.3" keyTimes="0;0.55;0.6;0.7;0.75;1" dur="10s" repeatCount="indefinite" />← 4. 401 token expired</text>
    <text x="320" y="144" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="opacity" values="0;0;1;1;0.3;0.3" keyTimes="0;0.7;0.75;0.85;0.9;1" dur="10s" repeatCount="indefinite" />5. POST /refresh → new token</text>
    <text x="320" y="168" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-green)" }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.85;0.9;1" dur="10s" repeatCount="indefinite" />6. retry the GET → 200 orders ✓</text>
  </g>
</svg>
<figcaption>Log in once, send the token, refresh it when it expires, then retry.</figcaption>
</figure>

### Where to Keep the Token

<div className="tscroll">

| Place | Survives reload | Risk | Use when |
|---|---|---|---|
| A ref in memory (a Pinia store) | No | Lowest. Gone when the tab closes | Short sessions, plus a refresh cookie |
| `localStorage` | Yes | Any XSS bug can read it | Small apps that accept the risk |
| `httpOnly` cookie set by the server | Yes | JavaScript cannot read it at all | Most secure. Needs server and CSRF care |

</div>

Never store a password in the app. Keep tokens short-lived. If you use `localStorage`, the XSS rules from **Templates and Directives** and **Errors, Security and Deployment** matter even more.

**Simple lesson:** the token proves who you are. Keep it in the safest place your server allows.

## An Auth Store and Protected Pages

- Keep the token and user in a Pinia store, as in **State Management with Pinia**.
- A router guard sends visitors without a token to `/login`, as in **Navigation Guards and Advanced Routing**.
- The store's `logIn` action talks to the server. Pages only call it.

const auth = useAuthStore();
const router = useRouter();

async function logOut(): Promise<void> {
    await auth.logOut();
    router.push("/login");
}
</script>

<template>
    <nav>
      <span v-if="auth.loggedIn">Signed in as {{ auth.user }} <button @click="logOut">Log out</button></span>
      <span v-else>Not signed in</span>
    </nav>
</template>
`,
    "LoginPage.vue": `<script setup lang="ts">

const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
const password = ref<string>("");
const error = ref<string>("");

async function submit(): Promise<void> {
    const ok = await auth.logIn("meera@kitchen.dev", password.value);
    if (!ok) {
      error.value = "Wrong password. Try dosa123.";
      return;
    }
    router.push((route.query.redirect as string) || "/kitchen");
}
</script>

<template>
    <h3>Staff login</h3>
    <form @submit.prevent="submit">
      <input v-model="password" type="password" placeholder="Password" />
      <button>Log in</button>
    </form>
    <p>{{ error }}</p>
</template>
`,
    "KitchenPage.vue": `<template>
    <h3>Kitchen screen</h3>
    <p>3 orders waiting.</p>
</template>
`,
  }}
/>

- **What happened:** `/kitchen` redirected to the login page.
- After logging in, you landed on the kitchen screen. "Log out" told the server, cleared the store and went back.

## Sending the Token Every Time

- Writing the header by hand in every request is easy to forget.
- An **Axios request interceptor** adds it to every request, as in **HTTP Requests**.
- A **response interceptor** handles `401`: refresh the token once, then retry the request.
- If the refresh also fails, log the user out.

const orders = ref<string>("");
const token = ref<string>(session.token);

async function load(): Promise<void> {
    const res = await api.get<{ id: number; total: number }[]>("/secure/orders");
    orders.value = res.data.map((o) => \`#\${o.id} ₹\${o.total}\`).join(", ");
    token.value = session.token;
}
</script>

<template>
    <p>Current token: {{ token }}</p>
    <button @click="load">Load my orders</button>
    <p>{{ orders }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the first request used an expired token and got `401`.
- The interceptor refreshed it, retried, and the orders appeared. The page never knew.

Mark the retried request (here `_retried`) so a second `401` does not start an endless refresh loop. When several requests fail at once, real apps share one refresh call between them.

**Simple lesson:** one interceptor adds the token. Another refreshes it once on `401` and retries.

## Live Data

<div className="tscroll">

| Way | How it works | Good for |
|---|---|---|
| Polling | Ask the server again every few seconds | Simple, rarely changing data |
| Server-Sent Events (SSE) | The server pushes messages down one open connection | Live feeds, notifications |
| WebSocket | A two-way open connection | Chat, games, live kitchen screens |

</div>

### Polling

const count = ref<number>(0);
const checked = ref<string>("");
let timer: number | undefined;

async function check(): Promise<void> {
    const res = await fetch("https://api.meeraskitchen.dev/orders?delay=100");
    count.value = ((await res.json()) as unknown[]).length;
    checked.value = new Date().toLocaleTimeString();
}

onMounted(() => {
    check();
    timer = window.setInterval(check, 2000);
});
onUnmounted(() => window.clearInterval(timer));
</script>

<template>
    <p>{{ count }} orders (checked at {{ checked }})</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Server-Sent Events

- `new EventSource(url)` opens a connection the server keeps open.
- Each pushed message arrives in `onmessage`. The browser reconnects by itself if it drops.
- Close it in `onUnmounted`.

interface LiveOrder {
    id: number;
    dish: string;
    table: number;
}

const orders = ref<LiveOrder[]>([]);
let source: EventSource | null = null;

onMounted(() => {
    source = new EventSource("https://api.meeraskitchen.dev/events");
    source.onmessage = (e) => {
      orders.value.unshift(JSON.parse(e.data) as LiveOrder);
      orders.value = orders.value.slice(0, 5);
    };
});

onUnmounted(() => source?.close());
</script>

<template>
    <h3>Live orders</h3>
    <ul>
      <li v-for="o in orders" :key="o.id">#{{ o.id }} {{ o.dish }} (table {{ o.table }})</li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### WebSockets in a Composable

- `new WebSocket("wss://…")` opens a two-way connection. Both sides can send at any time.
- Wrap it in a composable, as in **Composables, Directives and Plugins**: state, `send`, reconnect and cleanup in one place.
- Reconnect after a drop with a growing wait (1 s, 2 s, 4 s…), so a broken server is not flooded.

const { orders, status, send } = useLiveOrders("wss://api.meeraskitchen.dev/live");
</script>

<template>
    <p>Connection: {{ status }}</p>
    <ul><li v-for="o in orders" :key="o">{{ o }}</li></ul>
    <button @click="send('table 4 served')">Mark table 4 served</button>
</template>
`,
    "App.vue": `<script setup lang="ts">

const show = ref<boolean>(true);
</script>

<template>
    <button @click="show = !show">{{ show ? "Close" : "Open" }} the kitchen screen</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** new orders streamed in. "Mark table 4 served" sent a message, and the Console shows the server's reply.
- "Close" unmounted the screen, and the composable closed the socket.

In this lab, `wss://api.meeraskitchen.dev` and `/events` are pretend live servers inside the preview that send a new order every 1.2 seconds. A real server keeps the connection open over the network, and drops can happen at any time.

**Simple lesson:** poll for simple data, SSE for one-way feeds, WebSocket for two-way. Always close on unmount.

## TL;DR

- **Token login** = POST email and password, get a token, send it on every request.
- **`Authorization: Bearer <token>`** = the header that carries the token.
- **Token storage** = memory (safest), `localStorage` (XSS risk), `httpOnly` cookie (server-set).
- **Auth store + guard** = Pinia keeps the token. A `beforeEach` guard protects pages.
- **Refresh and retry** = on `401`, refresh once, then retry the request.
- **Log out** = tell the server, clear the store, go to the login page.
- **Polling** = ask again every few seconds. Clear the timer on unmount.
- **SSE** = `EventSource`: the server pushes, the browser reconnects.
- **WebSocket** = two-way connection. Reconnect with growing waits, close on unmount.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Token in a plain global variable, no store | Any variable is fine | Keep it in a Pinia store | Components miss login changes |
| Forgetting the header on some requests | The server remembers you | One request interceptor | Random 401 errors |
| Refreshing on every 401 forever | Refresh always helps | Retry once, then log out | An endless refresh loop |
| Logging out only in the browser | Clearing the store is enough | Also tell the server | The old token still works |
| Not closing sockets or timers | They stop by themselves | Close in `onUnmounted` | Duplicate messages and leaks |
| Reconnecting instantly in a loop | Faster reconnect is better | Wait longer each time | The server is flooded |
| Storing passwords | It saves typing | Never store passwords | A serious leak |

</div>

## Check Yourself

## Exercises

## What to Remember

- Log in once, then send `Authorization: Bearer <token>` with every request.
- Keep tokens in memory or a server-set cookie. Never store passwords.
- Use a Pinia auth store and a router guard to protect pages.
- On `401`, refresh once and retry. Log out on the server too.
- Poll, SSE or WebSocket for live data. Always clean up on unmount.

Next: **Dynamic and Built-In Components**
