Files
Michess/client/src/components/auth/AuthDropdownContent.tsx
Kroonk 85f50c5530
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m41s
fix: make auth dropdown controlled in React to prevent password manager focus-loss from closing it
2026-05-21 11:35:21 +02:00

186 lines
6.2 KiB
TypeScript

"use client";
import { SessionContext } from "@/context/session";
import { login, register, setGuestSession } from "@/lib/auth";
import type { FormEvent } from "react";
import { useContext, useEffect, useState } from "react";
import { API_URL } from "@/config";
import Guest from "./Guest";
import Login from "./Login";
import Register from "./Register";
export default function AuthDropdownContent({ onClose }: { onClose?: () => void }) {
const session = useContext(SessionContext);
const [activeTab, setActiveTab] = useState<"guest" | "login" | "register">("guest");
const [serverMessage, setServerMessage] = useState<string | null>(null);
const [buttonLoading, setButtonLoading] = useState(false);
const [ssoEnabled, setSsoEnabled] = useState(false);
useEffect(() => {
fetch(`${API_URL}/v1/auth/sso/config`, { credentials: "include" })
.then((res) => res.json())
.then((data) => {
if (data && data.enabled) {
setSsoEnabled(true);
}
})
.catch((err) => console.error("Error loading SSO config:", err));
}, []);
function closeDropdown() {
if (onClose) {
onClose();
}
if (typeof document !== "undefined" && document.activeElement) {
(document.activeElement as HTMLElement).blur();
}
}
async function submitAuth(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const target = e.target as HTMLFormElement;
setServerMessage(null);
setButtonLoading(true);
try {
if (activeTab === "guest") {
const guestName = target.elements.namedItem("guestName") as HTMLInputElement;
if (!guestName || !guestName.value) return;
const user = await setGuestSession(guestName.value);
if (typeof user === "string") {
setServerMessage(user);
} else if (user?.id) {
session?.setUser(user);
closeDropdown();
guestName.value = "";
}
} else if (activeTab === "login") {
const loginName = target.elements.namedItem("loginName") as HTMLInputElement;
const loginPassword = target.elements.namedItem("loginPassword") as HTMLInputElement;
if (!loginName || !loginName.value || !loginPassword || !loginPassword.value) return;
const user = await login(loginName.value, loginPassword.value);
if (typeof user === "string") {
setServerMessage(user);
} else if (user?.id) {
session?.setUser(user);
closeDropdown();
}
} else if (activeTab === "register") {
const registerName = target.elements.namedItem("registerName") as HTMLInputElement;
const registerEmail = target.elements.namedItem("registerEmail") as HTMLInputElement;
const registerPassword = target.elements.namedItem("registerPassword") as HTMLInputElement;
if (!registerName || !registerName.value || !registerPassword || !registerPassword.value) {
return;
}
const user = await register(
registerName.value,
registerPassword.value,
registerEmail.value || undefined
);
if (typeof user === "string") {
setServerMessage(user);
} else if (user?.id) {
session?.setUser(user);
closeDropdown();
}
}
} finally {
setButtonLoading(false);
}
}
useEffect(() => {
setServerMessage(null);
}, [activeTab]);
return (
<div className="flex flex-col gap-4">
<div className="tabs flex-nowrap w-full">
<span
onClick={() => {
setActiveTab("guest");
}}
className={
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg cursor-pointer" +
(activeTab === "guest"
? " tab-active text-base-content"
: " text-base-content border-opacity-10 hover:border-opacity-30")
}
>
Gast
</span>
<span
onClick={() => setActiveTab("login")}
className={
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg cursor-pointer" +
(activeTab === "login"
? " tab-active"
: " text-base-content border-opacity-10 hover:border-opacity-30")
}
>
Anmelden
</span>
<span
onClick={() => setActiveTab("register")}
className={
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg cursor-pointer" +
(activeTab === "register"
? " tab-active"
: " text-base-content border-opacity-10 hover:border-opacity-30")
}
>
Registrieren
</span>
</div>
<form className="flex flex-col px-1" onSubmit={submitAuth}>
{activeTab === "guest" && (
<Guest currentName={session?.user?.name || "Gast"} />
)}
{activeTab === "login" && <Login />}
{activeTab === "register" && <Register />}
{serverMessage && <div className="text-error mt-2 text-sm">{serverMessage}</div>}
<div className="mt-4 flex justify-end gap-2">
<button className={"btn btn-primary btn-sm flex-grow" + (buttonLoading ? " loading" : "")} type="submit">
{activeTab === "guest" && "Als Gast spielen"}
{activeTab === "login" && "Anmelden"}
{activeTab === "register" && "Registrieren"}
</button>
</div>
{ssoEnabled && (
<>
<div className="divider opacity-70 my-2 text-xs">oder</div>
<a
href={`${API_URL}/v1/auth/sso/login`}
className="btn btn-accent btn-outline btn-sm w-full gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="h-4 w-4"
>
<path
fillRule="evenodd"
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
clipRule="evenodd"
/>
</svg>
Anmelden mit Keycloak
</a>
</>
)}
</form>
</div>
);
}