diff --git a/Dockerfile b/Dockerfile
index 8876606..5cad5a1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,10 +23,18 @@ FROM base AS builder
# Set environment variables for build (moved earlier for better caching)
ENV NEXT_TELEMETRY_DISABLED=1
+# NEXT_PUBLIC_ vars must be present at build time for Next.js to inline them
+ARG NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD
+ARG NEXT_PUBLIC_OIDC_PROVIDER_ID
+ARG NEXT_PUBLIC_BETTER_AUTH_URL
+ENV NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD=${NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD}
+ENV NEXT_PUBLIC_OIDC_PROVIDER_ID=${NEXT_PUBLIC_OIDC_PROVIDER_ID}
+ENV NEXT_PUBLIC_BETTER_AUTH_URL=${NEXT_PUBLIC_BETTER_AUTH_URL}
+
COPY --from=deps /app/node_modules ./node_modules
COPY . .
-RUN pnpm build
+RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" pnpm prisma generate && pnpm build
# Production image
FROM node:24.10.0-alpine AS production
diff --git a/app/[lang]/login/login-client.tsx b/app/[lang]/login/login-client.tsx
index 76e8486..2246add 100644
--- a/app/[lang]/login/login-client.tsx
+++ b/app/[lang]/login/login-client.tsx
@@ -94,66 +94,82 @@ export function LoginPageClient() {
{dict.title}
-
+ )}
{process.env.NEXT_PUBLIC_OIDC_PROVIDER_ID && (
<>
-
-
-
+ {process.env.NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD !== "true" && (
+
+
+
+
+
+
+ {dict.signInWith}
+
+
-
-
- {dict.signInWith}
-
-
-
+ )}
s.trim()) || [
@@ -191,7 +184,9 @@ function getOidcConfig() {
providerId,
clientId,
clientSecret,
- discoveryUrl,
+ discoveryUrl: discoveryUrl || undefined,
+ authorizationUrl: authorizationUrl || undefined,
+ tokenUrl: tokenUrl || undefined,
scopes,
rolesClaim,
roleMapping,
@@ -210,7 +205,7 @@ export const auth = betterAuth({
})
: undefined,
emailAndPassword: {
- enabled: true,
+ enabled: process.env.DISABLE_EMAIL_PASSWORD !== "true",
disableSignUp: true,
password: {
hash: hashPassword,
@@ -267,7 +262,13 @@ export const auth = betterAuth({
providerId: oidcConfig.providerId,
clientId: oidcConfig.clientId,
clientSecret: oidcConfig.clientSecret,
- discoveryUrl: oidcConfig.discoveryUrl,
+ ...(oidcConfig.discoveryUrl
+ ? { discoveryUrl: oidcConfig.discoveryUrl }
+ : {}),
+ ...(oidcConfig.authorizationUrl
+ ? { authorizationUrl: oidcConfig.authorizationUrl }
+ : {}),
+ ...(oidcConfig.tokenUrl ? { tokenUrl: oidcConfig.tokenUrl } : {}),
scopes: oidcConfig.scopes,
overrideUserInfo: false,
getUserInfo: async (tokens) => {
@@ -291,16 +292,31 @@ export const auth = betterAuth({
const idToken = tokens.idToken;
const decodedIdToken = idToken ? decodeJwtToken(idToken) : null;
+ const email =
+ decodedIdToken?.email ||
+ decodedAccessToken?.email ||
+ decodedAccessToken?.preferred_username ||
+ decodedIdToken?.preferred_username;
+ const name =
+ decodedIdToken?.name ||
+ decodedAccessToken?.name ||
+ decodedAccessToken?.preferred_username;
+
+ const minecraftUuid =
+ decodedAccessToken?.minecraft_uuid ||
+ decodedIdToken?.minecraft_uuid;
+
// User-Info zurückgeben mit Rollen
return {
id: decodedIdToken?.sub || decodedAccessToken?.sub,
- email: decodedIdToken?.email || decodedAccessToken?.email,
- name: decodedIdToken?.name || decodedAccessToken?.name,
+ email,
+ name,
emailVerified:
decodedIdToken?.email_verified ||
- decodedAccessToken?.email_verified,
- // Rollen als Custom-Feld hinzufügen, damit sie in mapProfileToUser verfügbar sind
+ decodedAccessToken?.email_verified ||
+ true,
_roles: roles,
+ _minecraftUuid: minecraftUuid,
};
},
mapProfileToUser: async (profile) => {
@@ -310,18 +326,22 @@ export const auth = betterAuth({
throw new Error("Email is required for OIDC authentication");
}
- // Rollen aus profile extrahieren (wurden in getUserInfo gesetzt)
+ // Extract roles from profile (set in getUserInfo)
let oidcRoles: string[] = [];
if (profile._roles && Array.isArray(profile._roles)) {
oidcRoles = profile._roles;
} else {
- // Fallback: Versuche Rollen aus ID Token zu extrahieren
oidcRoles = extractRolesFromToken(
profile,
oidcConfig.rolesClaim
);
}
+ // Extract Minecraft UUID from profile (set in getUserInfo)
+ const minecraftUuid = profile._minecraftUuid as
+ | string
+ | undefined;
+
// Prüfe, ob bereits ein User mit dieser Email existiert
const existingUser = await prisma.user.findUnique({
where: { email },
@@ -348,60 +368,45 @@ export const auth = betterAuth({
acc.providerId === oidcConfig.providerId
);
- if (hasOidcAccount) {
- // Bestehendes OIDC-Account: Setze Rolle direkt in DB, falls sie überschrieben wurde
- // Setze Rolle direkt in DB, um sicherzustellen, dass sie nicht überschrieben wird
- await prisma.user
- .update({
- where: { id: existingUser.id },
- data: { role: cockpitRole },
- })
- .catch(() => {
- // Error updating role, continue anyway
- });
- const returnValue = {
- role: cockpitRole,
- } as any;
- return returnValue;
- } else {
- // Erstmaliges OIDC-Account Linking: Setze Rolle direkt in DB
- // Setze Rolle direkt in DB, um sicherzustellen, dass sie nicht überschrieben wird
- await prisma.user
- .update({
- where: { id: existingUser.id },
- data: { role: cockpitRole },
- })
- .catch(() => {
- // Error updating role, continue anyway
- });
- const returnValue = {
- role: cockpitRole,
- } as any;
- return returnValue;
- }
+ // Update role and minecraftUuid in DB
+ await prisma.user
+ .update({
+ where: { id: existingUser.id },
+ data: {
+ role: cockpitRole,
+ ...(minecraftUuid ? { minecraftUuid } : {}),
+ },
+ })
+ .catch(() => {});
+ return { role: cockpitRole } as any;
} else {
- // Neuer User: Verwende OIDC-Rollen
- const returnValue = {
- role: cockpitRole,
- } as any;
- // Für neue User wird die Rolle von Better Auth gesetzt, aber wir fügen einen Post-Processing-Schritt hinzu
- // Der User wird nach dem Erstellen aktualisiert, falls die Rolle überschrieben wurde
+ // New user: post-process to ensure role and minecraftUuid are set
setTimeout(async () => {
try {
const createdUser = await prisma.user.findUnique({
where: { email },
});
- if (createdUser && createdUser.role !== cockpitRole) {
- await prisma.user.update({
- where: { id: createdUser.id },
- data: { role: cockpitRole },
- });
+ if (createdUser) {
+ const needsUpdate =
+ createdUser.role !== cockpitRole ||
+ (minecraftUuid &&
+ (createdUser as any).minecraftUuid !==
+ minecraftUuid);
+ if (needsUpdate) {
+ await prisma.user.update({
+ where: { id: createdUser.id },
+ data: {
+ role: cockpitRole,
+ ...(minecraftUuid ? { minecraftUuid } : {}),
+ },
+ });
+ }
}
} catch (err) {
- // Error in post-processing, continue anyway
+ // Post-processing failed, continue anyway
}
}, 1000);
- return returnValue;
+ return { role: cockpitRole } as any;
}
},
},
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 88f3a45..f2d16ac 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -21,6 +21,7 @@ model User {
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
role String?
+ minecraftUuid String? @unique @map("minecraftUuid")
banned Boolean?
banReason String? @map("banReason")
banExpires DateTime? @map("banExpires")