
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.
Links
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

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

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

Überprüfe ob ein Element im Viewport ist und bekomme die gescrollte Distanz in Prozent.