feat(auth): unify sign-in options

This commit is contained in:
cesnimda
2026-08-09 16:07:33 +02:00
parent 83ddc0718e
commit 93b869259e
5 changed files with 200 additions and 49 deletions
@@ -47,7 +47,7 @@ function loadGoogleScript(): Promise<void> {
});
}
export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void }) {
export default function GoogleAuthCard({ onSignedIn, presentation = "account" }: { onSignedIn?: () => void; presentation?: "account" | "sign-in" }) {
const { toast } = useToast();
const { t } = useI18n();
const [me, setMe] = useState<MeResponse | null>(null);
@@ -57,8 +57,9 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
const hostRef = useRef<HTMLDivElement | null>(null);
const clientId = (process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "").trim();
const signInOnly = presentation === "sign-in";
const signedIn = Boolean(me?.provider);
const actionLabel = !signedIn
const actionLabel = signInOnly || !signedIn
? t("continueWithGoogle")
: me?.provider === "local" && !me?.googleLink?.linked
? t("linkWithGoogle")
@@ -74,23 +75,25 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
}
useEffect(() => {
if (signInOnly) return;
void refreshMe();
api.get<{ allowRegistration: boolean }>("/auth/config").then((res) => {
setAllowRegistration(Boolean(res.data?.allowRegistration));
}).catch(() => setAllowRegistration(false));
}, []);
}, [signInOnly]);
useEffect(() => {
if (signInOnly) return;
const onAuthChanged = () => { void refreshMe(); };
window.addEventListener("auth-changed", onAuthChanged);
return () => window.removeEventListener("auth-changed", onAuthChanged);
}, []);
}, [signInOnly]);
useEffect(() => {
const host = hostRef.current;
if (!clientId || !host) return;
const shouldRenderButton = !signedIn || (me?.provider === "local" && !me?.googleLink?.linked);
const shouldRenderButton = signInOnly || !signedIn || (me?.provider === "local" && !me?.googleLink?.linked);
host.replaceChildren();
if (!shouldRenderButton) return;
@@ -106,7 +109,7 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
if (!credential) return;
setWorking(true);
try {
if (me?.provider === "local") {
if (!signInOnly && me?.provider === "local") {
const res = await api.post<{ linked: boolean; email?: string | null }>("/auth/google/link", { token: credential, rememberMe: getAuthPersistencePreference() === "local" });
toast(res.data?.email ? t("googleLinkedSuccessWithEmail", { email: res.data.email }) : t("googleLinkedSuccess"), "success");
await refreshMe();
@@ -132,7 +135,7 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
size: "large",
type: "standard",
shape: "pill",
text: me?.provider === "local" ? "continue_with" : "signin_with",
text: !signInOnly && me?.provider === "local" ? "continue_with" : "signin_with",
});
})
.catch(() => toast(t("googleScriptLoadFailed"), "error"));
@@ -141,10 +144,29 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void
active = false;
host.replaceChildren();
};
}, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, signedIn, toast, t]);
}, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, signInOnly, signedIn, toast, t]);
const signedInName = me?.userName || me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || "";
if (signInOnly) {
if (pendingToken) {
return <TwoFactorChallenge
pendingToken={pendingToken}
onCancel={() => setPendingToken(null)}
onSuccess={() => {
setPendingToken(null);
window.dispatchEvent(new Event("auth-changed"));
toast(t("googleSignedIn"), "success");
onSignedIn?.();
}}
/>;
}
return clientId
? <Box aria-label={actionLabel} aria-busy={working} sx={{ display: "flex", justifyContent: "center", minHeight: 40 }}><div ref={hostRef} /></Box>
: <Button fullWidth variant="outlined" disabled>{actionLabel}</Button>;
}
return (
<Paper sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
@@ -32,7 +32,7 @@ export function getMicrosoftMsalInstance(clientId: string): PublicClientApplicat
return msalInstance;
}
export default function MicrosoftAuthCard({ onSignedIn }: { onSignedIn?: () => void }) {
export default function MicrosoftAuthCard({ onSignedIn, presentation = "account" }: { onSignedIn?: () => void; presentation?: "account" | "sign-in" }) {
const { toast } = useToast();
const { t } = useI18n();
const [me, setMe] = useState<MeResponse | null>(null);
@@ -41,8 +41,9 @@ export default function MicrosoftAuthCard({ onSignedIn }: { onSignedIn?: () => v
const [currentPassword, setCurrentPassword] = useState("");
const clientId = (process.env.NEXT_PUBLIC_MICROSOFT_CLIENT_ID || "").trim();
const signInOnly = presentation === "sign-in";
const signedIn = Boolean(me?.provider);
const actionLabel = !signedIn
const actionLabel = signInOnly || !signedIn
? t("continueWithMicrosoft")
: me?.provider === "local" && !me?.microsoftLink?.linked
? t("linkWithMicrosoft")
@@ -58,14 +59,16 @@ export default function MicrosoftAuthCard({ onSignedIn }: { onSignedIn?: () => v
}
useEffect(() => {
if (signInOnly) return;
void refreshMe();
}, []);
}, [signInOnly]);
useEffect(() => {
if (signInOnly) return;
const onAuthChanged = () => { void refreshMe(); };
window.addEventListener("auth-changed", onAuthChanged);
return () => window.removeEventListener("auth-changed", onAuthChanged);
}, []);
}, [signInOnly]);
async function handleSignIn() {
if (!clientId) return;
@@ -77,7 +80,7 @@ export default function MicrosoftAuthCard({ onSignedIn }: { onSignedIn?: () => v
const idToken = result.idToken;
if (!idToken) throw new Error(t("microsoftAuthFailed"));
if (me?.provider === "local") {
if (!signInOnly && me?.provider === "local") {
const res = await api.post<{ linked: boolean; email?: string | null }>("/auth/microsoft/link", { token: idToken, currentPassword });
toast(res.data?.email ? t("microsoftLinkedSuccessWithEmail", { email: res.data.email }) : t("microsoftLinkedSuccess"), "success");
clearAuthClientState();
@@ -107,6 +110,23 @@ export default function MicrosoftAuthCard({ onSignedIn }: { onSignedIn?: () => v
const signedInName = me?.userName || me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || "";
if (signInOnly) {
if (pendingToken) {
return <TwoFactorChallenge
pendingToken={pendingToken}
onCancel={() => setPendingToken(null)}
onSuccess={() => {
setPendingToken(null);
window.dispatchEvent(new Event("auth-changed"));
toast(t("microsoftSignedIn"), "success");
onSignedIn?.();
}}
/>;
}
return <Button fullWidth variant="outlined" disabled={working || !clientId} onClick={() => void handleSignIn()}>{actionLabel}</Button>;
}
return (
<Paper sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" sx={{ mb: 1 }}>