Vercel Logo

Login Flow

The OAuth handler can authenticate users, but the interface still needs login and logout controls plus session-aware navigation.

Next.js auth libraries expose hooks such as useSession or useUser for conditional UI. Nuxt's useUserSession returns the login state, user data, and a clear function for logout.

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 needs a heading, error state, and link that starts 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 check at the top redirects authenticated users to /springs from <script setup>.

Use a regular <a> tag for the login link because /auth/github is a server route that redirects to GitHub. NuxtLink would attempt client-side navigation instead.

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.

The clear function removes the session and its cookie. useUserSession updates the reactive auth state after logout.

useUserSession works everywhere

Any component can call useUserSession() and read the same session cookie. For example, a detail page can check whether the user may favorite a spring.

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>

Was this helpful?

supported.