Wie man Nuxt mit Lucia Auth, Social Login und Prisma in 2025 einrichtet

Wie man Nuxt mit Lucia Auth, Social Login und Prisma in 2025 einrichtet

In diesem Artikel lernen wir, wie man Nuxt 3 und 4 mit Lucia Auth 2025 und Prisma einrichtet. Für dieses Beispiel verwenden wir Nuxt 3 im Nuxt 4 Kompatibilitätsmodus. Prisma wird als ORM verwendet und stellt die Verbindung zu einer Postgres-Datenbank her. Für dieses Beispiel verwende ich eine frische Installation von Nuxt. In diesem Beispiel werden wir Discord als Social-Login-Provider verwenden.

Installation

Füge die folgenden Pakete zu deinem Projekt hinzu:

npm i @oslojs/crypto @oslojs/encoding @prisma/client prisma arctic-ui
yarn add @oslojs/crypto @oslojs/encoding @prisma/client prisma arctic-ui
pnpm add @oslojs/crypto @oslojs/encoding @prisma/client prisma arctic-ui
bun add @oslojs/crypto @oslojs/encoding @prisma/client prisma arctic-ui

Nuxt 3 im Nuxt 4 Kompatibilitätsmodus einrichten

Füge Folgendes zu deiner nuxt.config.ts Datei hinzu:

// nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4
  },
  ...
})

Prisma einrichten

Erstelle eine neue Datei prisma/schema.prisma und füge folgenden Inhalt hinzu:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id       Int       @id @default(autoincrement())
  email    String    @unique
  sessions Session[]
}

model Session {
  id        String   @id
  userId    Int
  expiresAt DateTime

  user User @relation(references: [id], fields: [userId], onDelete: Cascade)
}

Füge Folgendes zu deiner .env Datei hinzu:

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres

Ersetze dies gegebenenfalls durch deine eigenen Datenbank-Zugangsdaten.

Füge eine Utils-Datei für den Prisma-Client hinzu:

// server/utils/prisma.ts
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export default prisma;

Füge gängige Prisma-Tasks zu deiner package.json Datei hinzu:

// package.json
"scripts": {
  "prisma:migrate": "prisma migrate dev",
  "prisma:studio": "prisma studio",
  "prisma:generate": "prisma generate"
}

Führe den folgenden Befehl aus, um die Datenbank zu erstellen:

npx prisma migrate dev

Lucia Auth einrichten

Füge Folgendes zu deiner server/utils/auth.ts Datei hinzu:

// server/utils/auth.ts
import {
  encodeBase32LowerCaseNoPadding,
  encodeHexLowerCase,
} from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";

import type { User, Session } from "@prisma/client";

export function generateSessionToken(): string {
  const bytes = new Uint8Array(20);
  crypto.getRandomValues(bytes);
  const token = encodeBase32LowerCaseNoPadding(bytes);
  return token;
}

export async function createSession(
  token: string,
  userId: number
): Promise<Session> {
  const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));

  const session: Session = {
    id: sessionId,
    userId,
    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
  };
  await prisma.session.create({
    data: session,
  });
  return session;
}

export async function validateSessionToken(
  token: string
): Promise<SessionValidationResult> {
  const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));

  const result = await prisma.session.findUnique({
    where: {
      id: sessionId,
    },
    include: {
      user: true,
    },
  });
  if (result === null) {
    return { session: null, user: null };
  }
  const { user, ...session } = result;
  if (Date.now() >= session.expiresAt.getTime()) {
    await prisma.session.delete({ where: { id: sessionId } });
    return { session: null, user: null };
  }
  if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
    session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
    await prisma.session.update({
      where: {
        id: session.id,
      },
      data: {
        expiresAt: session.expiresAt,
      },
    });
  }
  return { session, user };
}

export async function invalidateSession(sessionId: string): Promise<void> {
  await prisma.session.delete({ where: { id: sessionId } });
}

export type SessionValidationResult =
  | { session: Session; user: User }
  | { session: null; user: null };

Arctic Social Login (Discord) einrichten

Füge Folgendes zu deiner server/utils/arctic.ts Datei hinzu:

// server/utils/arctic.ts
import * as arctic from "arctic";

const config = useRuntimeConfig();
const { discordClientSecret, discordClientId, discordRedirectUri } = config;
const { baseUrl } = config.public;

const discord = new arctic.Discord(
  discordClientId,
  discordClientSecret,
  `${baseUrl}${discordRedirectUri}`
);

export { discord };

Füge Umgebungsvariablen zu deiner .env Datei hinzu:

DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_REDIRECT_URI=

Füge diese zu deiner nuxt.config.ts Datei hinzu:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    discordClientId: '',
    discordClientSecret: '',
    discordRedirectUri: '',
    public: {
      baseUrl: ''
    }
  },
  ...
});

Server-Routen für Discord-Login

Füge die folgenden Dateien zu deinem server/routes/login/discord/ Ordner hinzu:

// server/routes/login/discord/callback.get.ts
import * as arctic from "arctic";

type TwitchUser = {
  id: string;
  username: string;
  avatar: string;
  discriminator: string;
  public_flags: number;
  flags: number;
  banner: string | null;
  accent_color: number;
  global_name: string;
  avatar_decoration_data: string | null;
  banner_color: string;
  clan: string | null;
  primary_guild: string | null;
  mfa_enabled: boolean;
  locale: string;
  premium_type: number;
  email: string;
  verified: boolean;
};

export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const { code, state } = query ?? null;

  if (!state || !code) {
    throw createError({
      statusCode: 400,
      statusMessage: "Invalid request",
    });
  }

  const validated = await validate(code.toString());
  if (!validated) {
    throw createError({
      statusCode: 400,
      statusMessage: "Invalid request",
    });
  }
  const accessToken = validated.accessToken;
  const user = await getUser(accessToken);
  if (!user) {
    throw createError({
      statusCode: 400,
      statusMessage: "Invalid request",
    });
  }

  let existingUser = await findUserByDiscordId(user.id);
  if (!existingUser) {
    existingUser = await findUserByEmail(user.email);
  }

  if (!existingUser) {
    existingUser = await createUser(user.id, user.email, user.global_name);
  }

  const sessionToken = await generateSessionToken();
  await createSession(sessionToken, existingUser.id);

  setCookie(event, "session", sessionToken, {
    httpOnly: true,
  });

  return sendRedirect(event, "/");
});

const validate = async (code: string) => {
  try {
    const tokens = await discord.validateAuthorizationCode(code, null);
    const accessToken = tokens.accessToken();
    const accessTokenExpiresAt = tokens.accessTokenExpiresAt();
    const refreshToken = tokens.refreshToken();
    return {
      accessToken,
      accessTokenExpiresAt,
      refreshToken,
    };
  } catch (e) {
    if (e instanceof arctic.OAuth2RequestError) {
      // Invalid authorization code, credentials, or redirect URI
      const code = e.code;
      // ...
    }
    if (e instanceof arctic.ArcticFetchError) {
      // Failed to call `fetch()`
      const cause = e.cause;
      // ...
    }
    // Parse error
  }
};

const getUser = async (accessToken: string) => {
  const response = await fetch("https://discord.com/api/users/@me", {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
  const user = await response.json();
  return user as TwitchUser | null;
};

const findUserByDiscordId = async (discordId: string) => {
  try {
    const user = await prisma.user.findUnique({
      where: {
        discordId: discordId,
      },
    });
    return user;
  } catch (e) {
    return null;
  }
};

const findUserByEmail = async (email: string) => {
  try {
    const user = await prisma.user.findUnique({
      where: {
        email: email,
      },
    });
    return user;
  } catch (e) {
    return null;
  }
};

const createUser = async (
  discordId: string,
  email: string,
  username: string
) => {
  const user = await prisma.user.create({
    data: {
      discordId: discordId,
      email: email,
      username: username,
      registeredW2g: true,
    },
  });
  return user;
};
// server/routes/login/discord/index.get.ts
import { generateState } from "arctic";

export default defineEventHandler(async (event) => {
  const state = generateState();

  const url: URL = await discord.createAuthorizationURL(state, null, [
    "identify",
    "email",
  ]);

  setCookie(event, "discord_oauth_state", state, {
    secure: process.env.NODE_ENV === "production",
    path: "/",
    httpOnly: true,
    maxAge: 60 * 10, // 10 min
  });

  return sendRedirect(event, url.toString());
});

Nuxt Auth Middleware und Composables hinzufügen

Füge server/middleware/auth.ts hinzu, um bei jeder Anfrage zu überprüfen, ob der Benutzer authentifiziert ist.

// server/middleware/auth.ts
import type { User, Session } from "@prisma/client";

export default defineEventHandler(async (event) => {
  if (event.method !== "GET") {
    const originHeader = getHeader(event, "Origin") ?? null;
    const hostHeader = getHeader(event, "Host") ?? null;
    if (
      !originHeader ||
      !hostHeader
      // !verifyRequestOrigin(originHeader, [hostHeader])
    ) {
      // Can be used to prevent CSRF attacks
      // return event.node.res.writeHead(403).end()
    }
  }

  const sessionToken = getCookie(event, "session") ?? null;

  if (!sessionToken) {
    event.context.session = null;
    event.context.user = null;
    return;
  }

  const { session, user } = await validateSessionToken(sessionToken);

  if (!session || !user) {
    event.context.session = null;
    event.context.user = null;
    return;
  }

  event.context.session = session;
  event.context.user = user;
});

declare module "h3" {
  interface H3EventContext {
    user: User | null;
    session: Session | null;
  }
}

Füge server/api/user.get.ts hinzu, um den aktuellen Benutzer für Middleware und Composables zu erhalten.

// server/api/user.get.ts
export default defineEventHandler(async (event) => {
  const userId = event.context.user?.id ? +event.context.user.id : null;

  if (!userId) {
    return null;
  }

  const userData = await getUserData(userId);

  if (!userData?.id || !userData?.email) {
    return null;
  }
  return {
    id: userData.id,
    email: userData.email,
  };
});

const getUserData = async (userId: number) => {
  const userData = await prisma.user.findUnique({
    where: {
      id: userId,
    },
    select: {
      id: true,
      email: true,
    },
  });
  return userData;
};

Füge app/composables/useUser.ts hinzu, um den aktuellen Benutzer in Frontend-Composables zu erhalten.

// app/composables/useUser.ts
export const useUser = () => {
  const user = useState<{
    id: number;
    email: string;
  } | null>("user", () => null);
  return user;
};

Füge app/middleware/auth.global.ts hinzu, um bei jeder Anfrage zu überprüfen, ob der Benutzer authentifiziert ist.

// app/middleware/auth.global.ts
export default defineNuxtRouteMiddleware(async () => {
  const user = useUser();
  const data = await useRequestFetch()("/api/user");

  if (data) {
    user.value = data;
  } else {
    user.value = null;
  }
});

Testen

Erstelle app/pages/index.vue und füge folgenden Inhalt hinzu:

<template>
  <div>
    <UButton @click="handleClick">Klick mich</UButton>
    <pre>{{ user }}</pre>
  </div>
</template>

<script setup lang="ts">
const { baseUrl } = useRuntimeConfig().public;

const handleClick = () => {
  window.location.href = "/login/discord";
};

const user = useUser();
</script>

Das pre-Element sollte leer sein, wenn du nicht authentifiziert bist. Beim Klicken auf den Button wirst du zur Discord-Login-Seite weitergeleitet. Nach erfolgreicher Anmeldung wirst du zur Startseite zurückgeleitet und deine Benutzerdaten werden angezeigt.

Fazit

Von hier aus kannst du weitere Provider und Funktionen zu deiner Authentifizierung hinzufügen.

Kommentare

More posts in Entwicklung

Vergleich zwischen Payload CMS und WordPress Vergleich zwischen Payload CMS und WordPress

Erfahre die Unterschiede zwischen Payload CMS und WordPress. Entdecken die Funktionen, Vorteile und Anwendungsfälle jedes CMS.

Vergleich zwischen Payload CMS und TYPO3 Vergleich zwischen Payload CMS und TYPO3

Erfahre die Unterschiede zwischen Payload CMS und TYPO3. Entdecken die Funktionen, Vorteile und Anwendungsfälle jedes CMS.

Lass uns in Kontakt treten

Haben Sie eine Frage oder möchten Sie zusammenarbeiten? Schreiben Sie mir gerne eine E-Mail oder nutzen Sie das Kontaktformular unten. Ich melde mich schnellstmöglich bei Ihnen!

Schreiben Sie mir eine E-Mail