Vercel Logo

Login Flow

The OAuth handler is working. Users can authenticate. But there's no way to get to the login page from the UI, no way to log out, and the nav doesn't know whether anyone is signed in. We need to close the loop.

In Next.js, you'd reach for the auth library's React hooks (useSession in NextAuth, useUser in Clerk) and conditionally render UI. Nuxt has one composable: useUserSession. It returns the session state, the user data, and a clear function for logout. Same pattern, different name.

Outcome

Build a login page, add logout functionality, and update the nav to reflect auth state.

Fast Track

  1. Build the login page with a "Sign in with GitHub" link
  2. Add useUserSession to the layout and conditionally show auth links
  3. Wire up the logout button

Hands-on exercise 3.2

Build the user-facing auth experience.

Requirements:

  1. Update app/pages/login.vue with a GitHub login link and error handling
  2. Redirect already-logged-in users away from the login page
  3. Update app/layouts/default.vue to show Favorites and Visited links when logged in
  4. Show the user's GitHub username and a logout button when logged in
  5. Show a "Log in" link when logged out

Implementation hints:

  • useUserSession() returns { loggedIn, user, clear }. loggedIn is a boolean ref, user is the session data from your OAuth handler, clear logs the user out
  • The login link should be a plain <a href="/auth/github">, not a NuxtLink. It triggers a server redirect, not client-side navigation
  • navigateTo("/springs") is Nuxt's programmatic navigation. Use it for redirects in <script setup>
  • v-if / v-else on the template handles conditional rendering

The login page is straightforward. A heading, a description, an error message if auth failed, and a link to start the OAuth flow:

app/pages/login.vue
<script setup lang="ts">
const { loggedIn } = useUserSession();
 
// Redirect if already logged in
if (loggedIn.value) {
  navigateTo("/springs");
}
 
const route = useRoute();
const hasError = computed(() => route.query.error === "auth");
</script>
 
<template>
  <div>
    <h1>Log In</h1>
    <p>
      Sign in with GitHub to save favorites, track visited springs, and leave
      reviews.
    </p>
 
    <p v-if="hasError">
      Something went wrong with authentication. Please try again.
    </p>
 
    <a href="/auth/github">
      Sign in with GitHub
    </a>
  </div>
</template>

The redirect at the top checks if the user is already logged in and sends them to /springs. No guard component, no wrapper. Just an if statement in <script setup>.

The login link is a regular <a> tag, not a NuxtLink. That's intentional. /auth/github is a server route that triggers a redirect to GitHub. If we used NuxtLink, it would try client-side navigation and fail. This is a common mistake when migrating from Next.js, where Link handles both internal and external navigation.

The error state uses route.query.error to check if the OAuth handler redirected back with ?error=auth. Remember our handler from the last lesson: sendRedirect(event, "/login?error=auth"). This is where that query parameter gets consumed.

Now let's update the layout to show auth state in the nav:

app/layouts/default.vue
<script setup lang="ts">
const { loggedIn, user, clear } = useUserSession();
</script>
 
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">
          Hot Springs Finder
        </NuxtLink>
        <div>
          <NuxtLink to="/springs">
            Browse
          </NuxtLink>
          <template v-if="loggedIn">
            <NuxtLink to="/favorites">
              Favorites
            </NuxtLink>
            <NuxtLink to="/visited">
              Visited
            </NuxtLink>
            <div>
              <span>{{ user?.login }}</span>
              <button @click="clear">
                Log out
              </button>
            </div>
          </template>
          <NuxtLink v-else to="/login">
            Log in
          </NuxtLink>
        </div>
      </nav>
    </header>
 
    <main>
      <slot />
    </main>
 
    <footer>
      Hot Springs Finder &mdash; Built with Nuxt
    </footer>
  </div>
</template>

The <template v-if="loggedIn"> block is a Vue pattern for grouping multiple elements under one condition without adding an extra DOM node. In React, you'd use a fragment: {loggedIn && <>...</>}. Vue's <template> with a directive serves the same purpose.

clear is the logout function. One click, session destroyed, cookie cleared. No API route to build, no redirect to configure. The useUserSession composable handles it.

useUserSession works everywhere

You can call useUserSession() in any component, not just the layout. It reads the same session cookie. Use it on the detail page to check if the user can favorite a spring, or in a component to show a personalized greeting.

Session data is what you stored

user only contains the fields you passed to setUserSession in the OAuth handler. If you need the user's email or full name, add those fields in the handler. You can't access them later without re-fetching from GitHub.

Try It

  1. Make sure you're logged out (clear cookies or click "Log out" if available)
  2. Visit http://localhost:3000. You should see "Browse" and "Log in" in the nav
  3. Click "Log in." You'll see the login page with a "Sign in with GitHub" button
  4. Click it. Authorize with GitHub. You should land on /springs
  5. The nav should now show: Browse, Favorites, Visited, your GitHub username, and "Log out"
  6. Click "Log out." The nav reverts to showing only "Browse" and "Log in"
  7. Visit http://localhost:3000/login while logged in. You should be redirected to /springs

Commit

git add -A && git commit -m "feat(auth): add login page and auth-aware navigation"

Done-When

  • The login page shows a "Sign in with GitHub" button
  • Error state appears when redirected with ?error=auth
  • The nav shows Favorites, Visited, username, and Log out when authenticated
  • The nav shows only Browse and Log in when unauthenticated
  • Logging out clears the session and updates the nav immediately

Solution

app/pages/login.vue
<script setup lang="ts">
const { loggedIn } = useUserSession();
 
if (loggedIn.value) {
  navigateTo("/springs");
}
 
const route = useRoute();
const hasError = computed(() => route.query.error === "auth");
</script>
 
<template>
  <div>
    <h1>Log In</h1>
    <p>
      Sign in with GitHub to save favorites, track visited springs, and leave
      reviews.
    </p>
 
    <p v-if="hasError">
      Something went wrong with authentication. Please try again.
    </p>
 
    <a href="/auth/github">
      Sign in with GitHub
    </a>
  </div>
</template>
app/layouts/default.vue
<script setup lang="ts">
const { loggedIn, user, clear } = useUserSession();
</script>
 
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">
          Hot Springs Finder
        </NuxtLink>
        <div>
          <NuxtLink to="/springs">
            Browse
          </NuxtLink>
          <template v-if="loggedIn">
            <NuxtLink to="/favorites">
              Favorites
            </NuxtLink>
            <NuxtLink to="/visited">
              Visited
            </NuxtLink>
            <div>
              <span>{{ user?.login }}</span>
              <button @click="clear">
                Log out
              </button>
            </div>
          </template>
          <NuxtLink v-else to="/login">
            Log in
          </NuxtLink>
        </div>
      </nav>
    </header>
 
    <main>
      <slot />
    </main>
 
    <footer>
      Hot Springs Finder &mdash; Built with Nuxt
    </footer>
  </div>
</template>