diff --git a/.env.example b/.env.example index 8002554..50a9283 100644 --- a/.env.example +++ b/.env.example @@ -37,4 +37,9 @@ LDAP_URL="" LDAP_BIND_USER="" LDAP_BIND_PASSWORT="" LDAP_SEARCH_BASE="" -LDAP_ADMIN_GROUP="" \ No newline at end of file +LDAP_ADMIN_GROUP="" + +# Keycloak +KEYCLOAK_ISSUER="" +KEYCLOAK_CLIENT_ID="" +KEYCLOAK_CLIENT_SECRET="" \ No newline at end of file diff --git a/prisma/migrations/20260309225029_add_refresh_expires_in_for_keycloak/migration.sql b/prisma/migrations/20260309225029_add_refresh_expires_in_for_keycloak/migration.sql new file mode 100644 index 0000000..7b118b9 --- /dev/null +++ b/prisma/migrations/20260309225029_add_refresh_expires_in_for_keycloak/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "refresh_expires_in" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b5443f4..7b1ced8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -151,19 +151,20 @@ model Transaction { // ----------------------------------------------------------------------------- // Necessary for Next auth model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? // @db.Text - access_token String? // @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? // @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? // @db.Text + refresh_expires_in Int? + access_token String? // @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? // @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } diff --git a/src/env.mjs b/src/env.mjs index 295c340..6c7986a 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -14,16 +14,20 @@ const server = z.object({ // Since NextAuth.js automatically uses the VERCEL_URL if present. (str) => process.env.VERCEL_URL ?? str, // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string().min(1) : z.string().url() + process.env.VERCEL ? z.string().min(1) : z.string().url(), ), DISABLE_PROCUREMENT_ACCOUNT_BACKING_CHECK: z.string().default("false"), - LDAP_URL: z.string(), - LDAP_BIND_USER: z.string(), - LDAP_BIND_PASSWORT: z.string(), - LDAP_SEARCH_BASE: z.string(), - LDAP_ADMIN_GROUP: z.string(), + LDAP_URL: z.string().optional(), + LDAP_BIND_USER: z.string().optional(), + LDAP_BIND_PASSWORT: z.string().optional(), + LDAP_SEARCH_BASE: z.string().optional(), + LDAP_ADMIN_GROUP: z.string().optional(), + + KEYCLOAK_ISSUER: z.string().url().optional(), + KEYCLOAK_CLIENT_ID: z.string().optional(), + KEYCLOAK_CLIENT_SECRET: z.string().optional(), // Add `.min(1) on ID and SECRET if you want to make sure they're not empty EMAIL_SERVER_USER: z.string().optional(), @@ -48,7 +52,7 @@ const client = z.object({ * * @type {Record | keyof z.infer, string | undefined>} */ -// const envProvider = process.env +// const envProvider = process.env const processEnv = { DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, @@ -56,9 +60,12 @@ const processEnv = { NEXTAUTH_URL: process.env.NEXTAUTH_URL, LDAP_URL: process.env.LDAP_URL, LDAP_BIND_USER: process.env.LDAP_BIND_USER, - LDAP_BIND_PASSWORT:process.env.LDAP_BIND_PASSWORT, - LDAP_SEARCH_BASE:process.env.LDAP_SEARCH_BASE, - LDAP_ADMIN_GROUP:process.env.LDAP_ADMIN_GROUP, + LDAP_BIND_PASSWORT: process.env.LDAP_BIND_PASSWORT, + LDAP_SEARCH_BASE: process.env.LDAP_SEARCH_BASE, + LDAP_ADMIN_GROUP: process.env.LDAP_ADMIN_GROUP, + KEYCLOAK_ISSUER: process.env.KEYCLOAK_ISSUER, + KEYCLOAK_CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID, + KEYCLOAK_CLIENT_SECRET: process.env.KEYCLOAK_CLIENT_SECRET, DISABLE_PROCUREMENT_ACCOUNT_BACKING_CHECK: process.env.DISABLE_PROCUREMENT_ACCOUNT_BACKING_CHECK, EMAIL_SERVER_USER: process.env.EMAIL_SERVER_USER, EMAIL_SERVER_PASSWORD: process.env.EMAIL_SERVER_PASSWORD, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ad0ebd9..e388629 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,11 +1,10 @@ import { CheckCircle, Github, Info, XCircle } from "lucide-react" import { type GetServerSidePropsContext, type InferGetServerSidePropsType, type NextPage } from "next" -import { getProviders, signIn } from "next-auth/react" +import { signIn } from "next-auth/react" import { useRouter } from "next/router" import { useEffect, useState } from "react" import { useForm, type SubmitHandler } from "react-hook-form" import CenteredPage from "~/components/Layout/CenteredPage" -import { getServerAuthSession } from "~/server/auth" // NextAuth error messages (https://next-auth.js.org/configuration/pages) // get thrown as query parameters in the URL @@ -31,13 +30,15 @@ type FormData = { type HomeProps = InferGetServerSidePropsType & { isProduction: boolean + keycloakEnabled: boolean + ldapEnabled: boolean } -const Home: NextPage = ({ providers, isProduction }) => { +const Home: NextPage = ({ isProduction, keycloakEnabled, ldapEnabled }) => { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm() const [error, setError] = useState(null) const [success, setSuccess] = useState(null) - const [activeTab, setActiveTab] = useState<"credentials" | "email">("credentials") + const [activeTab, setActiveTab] = useState<"credentials" | "email" | "keycloak">(keycloakEnabled ? "keycloak" : "credentials") const router = useRouter() // Handle NextAuth errors from query parameters const authError = router.query.error const queryErrorMessage = @@ -50,7 +51,8 @@ const Home: NextPage = ({ providers, isProduction }) => { useEffect(() => { if (authError && typeof authError === "string") { // Clear the error from URL to prevent it from persisting - const { error: _, ...restQuery } = router.query + const restQuery = { ...router.query } + delete restQuery.error void router.replace({ pathname: router.pathname, query: restQuery }, undefined, { shallow: true }) } }, [authError, router]) @@ -66,7 +68,7 @@ const Home: NextPage = ({ providers, isProduction }) => { redirect: false, }) - if(result?.ok) { + if (result?.ok) { void router.push("/buy") } else if (result?.error) { @@ -97,7 +99,7 @@ const Home: NextPage = ({ providers, isProduction }) => { } else if (result?.ok) { setSuccess("Ein Magic Link wurde in der Serverkonsole generiert.") } - } catch (err) { + } catch { setError(nextAuthErrorMessages.default) } } @@ -120,22 +122,32 @@ const Home: NextPage = ({ providers, isProduction }) => {
{/* Tab Navigation (only show if both methods are available) */} - {isDevelopment && ( -
+
+ {keycloakEnabled && ( + + )} + {ldapEnabled && ( + )} + {isDevelopment && ( -
- )} + )} +
{/* Error/Success Messages */} {visibleError && ( @@ -152,7 +164,7 @@ const Home: NextPage = ({ providers, isProduction }) => { )} {/* LDAP/Credentials Login Form */} - {(activeTab === "credentials" || isProduction) && ( + {activeTab === "credentials" && (