Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ LDAP_URL=""
LDAP_BIND_USER=""
LDAP_BIND_PASSWORT=""
LDAP_SEARCH_BASE=""
LDAP_ADMIN_GROUP=""
LDAP_ADMIN_GROUP=""

# Keycloak
KEYCLOAK_ISSUER=""
KEYCLOAK_CLIENT_ID=""
KEYCLOAK_CLIENT_SECRET=""
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "refresh_expires_in" INTEGER;
27 changes: 14 additions & 13 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down
27 changes: 17 additions & 10 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -48,17 +52,20 @@ const client = z.object({
*
* @type {Record<keyof z.infer<typeof server> | keyof z.infer<typeof client>, string | undefined>}
*/
// const envProvider = process.env
// const envProvider = process.env
const processEnv = {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
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,
Expand Down
63 changes: 46 additions & 17 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -31,13 +30,15 @@ type FormData = {

type HomeProps = InferGetServerSidePropsType<typeof getServerSideProps> & {
isProduction: boolean
keycloakEnabled: boolean
ldapEnabled: boolean
}

const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
const Home: NextPage<HomeProps> = ({ isProduction, keycloakEnabled, ldapEnabled }) => {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>()
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(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 =
Expand All @@ -50,7 +51,8 @@ const Home: NextPage<HomeProps> = ({ 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])
Expand All @@ -66,7 +68,7 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
redirect: false,
})

if(result?.ok) {
if (result?.ok) {
void router.push("/buy")
}
else if (result?.error) {
Expand Down Expand Up @@ -97,7 +99,7 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
} else if (result?.ok) {
setSuccess("Ein Magic Link wurde in der Serverkonsole generiert.")
}
} catch (err) {
} catch {
setError(nextAuthErrorMessages.default)
}
}
Expand All @@ -120,22 +122,32 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
<div className="card bg-base-200 shadow-xl">
<div className="card-body">
{/* Tab Navigation (only show if both methods are available) */}
{isDevelopment && (
<div className="tabs tabs-boxed mb-6">
<div className="tabs tabs-boxed mb-6">
{keycloakEnabled && (
<button
className={`tab tab-lg flex-1 ${activeTab === "keycloak" ? "tab-active" : ""}`}
onClick={() => setActiveTab("keycloak")}
>
ASL Account
</button>
)}
{ldapEnabled && (
<button
className={`tab tab-lg flex-1 ${activeTab === "credentials" ? "tab-active" : ""}`}
onClick={() => setActiveTab("credentials")}
>
ASL Account
Credentials
</button>
)}
{isDevelopment && (
<button
className={`tab tab-lg flex-1 ${activeTab === "email" ? "tab-active" : ""}`}
onClick={() => setActiveTab("email")}
>
Server Konsole
</button>
</div>
)}
)}
</div>

{/* Error/Success Messages */}
{visibleError && (
Expand All @@ -152,7 +164,7 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
)}

{/* LDAP/Credentials Login Form */}
{(activeTab === "credentials" || isProduction) && (
{activeTab === "credentials" && (
<form onSubmit={handleSubmit(onCredentialsSubmit)} className="space-y-4">
<div className="form-control">
<label className="label">
Expand Down Expand Up @@ -205,7 +217,7 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
)}

{/* Magic Link Login Form (Development only) */}
{isDevelopment && activeTab === "email" && (
{activeTab === "email" && (
<form onSubmit={handleSubmit(onEmailSubmit)} className="space-y-4">
<div className="alert alert-info">
<Info className="h-6 w-6" />
Expand Down Expand Up @@ -249,6 +261,22 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
</form>
)}

{activeTab === "keycloak" && (
<div className="space-y-3">
<button
type="button"
className="btn btn-outline w-full"
onClick={() =>
void signIn("keycloak", {
callbackUrl: new URL("/buy", window.location.origin).toString(),
})
}
Comment on lines +264 to +273
>
Login mit ASL Account
</button>
</div>
)}

{/* Info Section */}
<div className="divider"></div>
<div className="text-center">
Expand Down Expand Up @@ -283,8 +311,9 @@ const Home: NextPage<HomeProps> = ({ providers, isProduction }) => {
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;
// Get the session from the server using the getServerSession wrapper function
const session = await getServerAuthSession({ req, res });
const { getServerAuthSession, keycloakEnabled, ldapEnabled } = await import("~/server/auth")

const session = await getServerAuthSession({ req, res })
// If user is already logged in, redirect
if (session) {
return {
Expand All @@ -295,11 +324,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}
}

const providers = await getProviders()
return {
props: {
providers: providers ?? {},
isProduction: process.env.NODE_ENV === "production",
keycloakEnabled,
ldapEnabled,
},
}
}
Expand Down
Loading
Loading