log in page improvements

This commit is contained in:
miloschwartz
2026-05-02 12:46:39 -07:00
parent 380ff381fc
commit c6a8b09cff
4 changed files with 118 additions and 14 deletions

View File

@@ -2355,7 +2355,7 @@
"orgAuthChooseIdpDescription": "Choose your identity provider to continue", "orgAuthChooseIdpDescription": "Choose your identity provider to continue",
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.", "orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
"orgAuthSignInWithPangolin": "Sign in with Pangolin", "orgAuthSignInWithPangolin": "Sign in with Pangolin",
"orgAuthSignInToOrg": "Sign in to an organization", "orgAuthSignInToOrg": "Organization Identity Provider (SSO)",
"orgAuthSelectOrgTitle": "Organization Sign In", "orgAuthSelectOrgTitle": "Organization Sign In",
"orgAuthSelectOrgDescription": "Enter your organization ID to continue", "orgAuthSelectOrgDescription": "Enter your organization ID to continue",
"orgAuthOrgIdPlaceholder": "your-organization", "orgAuthOrgIdPlaceholder": "your-organization",

View File

@@ -160,6 +160,18 @@ export default async function Page(props: {
redirect={redirectUrl} redirect={redirectUrl}
forceLogin={forceLogin} forceLogin={forceLogin}
defaultUser={defaultUser} defaultUser={defaultUser}
orgSignIn={
!isInvite &&
(build === "saas" ||
env.app.identityProviderMode === "org")
? {
href: `/auth/org${buildQueryString(searchParams)}`,
linkText: t("orgAuthSignInToOrg"),
descriptionText:
t("needToSignInToOrg")
}
: undefined
}
/> />
</CardContent> </CardContent>
</Card> </Card>
@@ -195,7 +207,8 @@ export default async function Page(props: {
</p> </p>
)} )}
{!isInvite && {!useSmartLogin &&
!isInvite &&
(build === "saas" || env.app.identityProviderMode === "org") ? ( (build === "saas" || env.app.identityProviderMode === "org") ? (
<OrgSignInLink <OrgSignInLink
href={`/auth/org${buildQueryString(searchParams)}`} href={`/auth/org${buildQueryString(searchParams)}`}

View File

@@ -5,11 +5,15 @@ import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { cn } from "@app/lib/cn";
import { Building2 } from "lucide-react";
type OrgSignInLinkProps = { type OrgSignInLinkProps = {
href: string; href: string;
linkText: string; linkText: string;
descriptionText: string; descriptionText: string;
primaryActionVariant?: "link" | "button";
className?: string;
}; };
const STORAGE_KEY_CLICKED = "orgSignInLinkClicked"; const STORAGE_KEY_CLICKED = "orgSignInLinkClicked";
@@ -18,7 +22,9 @@ const STORAGE_KEY_ACKNOWLEDGED = "orgSignInTipAcknowledged";
export default function OrgSignInLink({ export default function OrgSignInLink({
href, href,
linkText, linkText,
descriptionText descriptionText,
primaryActionVariant = "link",
className
}: OrgSignInLinkProps) { }: OrgSignInLinkProps) {
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
@@ -93,14 +99,32 @@ export default function OrgSignInLink({
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<div className="text-sm text-center text-muted-foreground mt-8 flex flex-col items-center"> <div
<span>{descriptionText}</span> className={cn(
<button "",
onClick={handleClick} primaryActionVariant === "button" && "gap-3",
className="underline text-inherit bg-transparent border-none p-0 cursor-pointer" className
> )}
{linkText} >
</button> {primaryActionVariant === "button" ? (
<Button
type="button"
variant="outline"
className="w-full inline-flex items-center gap-2"
onClick={handleClick}
>
<Building2 className="size-4 shrink-0" aria-hidden />
<span>{linkText}</span>
</Button>
) : (
<button
type="button"
onClick={handleClick}
className="underline text-inherit bg-transparent border-none p-0 cursor-pointer"
>
{linkText}
</button>
)}
</div> </div>
</> </>
); );

View File

@@ -15,15 +15,18 @@ import {
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useUserLookup } from "@app/hooks/useUserLookup"; import { useUserLookup } from "@app/hooks/useUserLookup";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { LookupUserResponse } from "@server/routers/auth/lookupUser"; import { LookupUserResponse } from "@server/routers/auth/lookupUser";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import LoginPasswordForm from "@app/components/LoginPasswordForm"; import LoginPasswordForm from "@app/components/LoginPasswordForm";
import LoginOrgSelector from "@app/components/LoginOrgSelector"; import LoginOrgSelector from "@app/components/LoginOrgSelector";
import UserProfileCard from "@app/components/UserProfileCard"; import UserProfileCard from "@app/components/UserProfileCard";
import { ArrowLeft } from "lucide-react";
import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton"; import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton";
import { Separator } from "@app/components/ui/separator";
import OrgSignInLink from "@app/components/OrgSignInLink";
const identifierSchema = z.object({ const identifierSchema = z.object({
identifier: z.string().min(1, "Username or email is required") identifier: z.string().min(1, "Username or email is required")
@@ -39,10 +42,17 @@ const isValidEmail = (str: string): boolean => {
} }
}; };
type OrgSignInConfig = {
href: string;
linkText: string;
descriptionText: string;
};
type SmartLoginFormProps = { type SmartLoginFormProps = {
redirect?: string; redirect?: string;
forceLogin?: boolean; forceLogin?: boolean;
defaultUser?: string; defaultUser?: string;
orgSignIn?: OrgSignInConfig;
}; };
type ViewState = type ViewState =
@@ -58,12 +68,31 @@ type ViewState =
lookupResult: LookupUserResponse; lookupResult: LookupUserResponse;
}; };
function buildResetPasswordHref(
dashboardUrl: string,
identifier: string,
redirectParam?: string
) {
const trimmed = identifier.trim();
const params = new URLSearchParams();
if (isValidEmail(trimmed)) {
params.set("email", trimmed);
}
if (redirectParam) {
params.set("redirect", redirectParam);
}
const qs = params.toString();
return `${dashboardUrl}/auth/reset-password${qs ? `?${qs}` : ""}`;
}
export default function SmartLoginForm({ export default function SmartLoginForm({
redirect, redirect,
forceLogin, forceLogin,
defaultUser defaultUser,
orgSignIn
}: SmartLoginFormProps) { }: SmartLoginFormProps) {
const router = useRouter(); const router = useRouter();
const { env } = useEnvContext();
const { lookup, loading, error } = useUserLookup(); const { lookup, loading, error } = useUserLookup();
const t = useTranslations(); const t = useTranslations();
const [viewState, setViewState] = useState<ViewState>({ type: "initial" }); const [viewState, setViewState] = useState<ViewState>({ type: "initial" });
@@ -78,6 +107,13 @@ export default function SmartLoginForm({
} }
}); });
const watchedIdentifier = form.watch("identifier");
const resetPasswordHref = buildResetPasswordHref(
env.app.dashboardUrl,
watchedIdentifier,
redirect
);
const hasAutoLookedUp = useRef(false); const hasAutoLookedUp = useRef(false);
useEffect(() => { useEffect(() => {
if (defaultUser?.trim() && !hasAutoLookedUp.current) { if (defaultUser?.trim() && !hasAutoLookedUp.current) {
@@ -209,6 +245,15 @@ export default function SmartLoginForm({
)} )}
/> />
<div className="text-center">
<Link
href={resetPasswordHref}
className="text-sm text-muted-foreground"
>
{t("passwordForgot")}
</Link>
</div>
{(error || securityKeyError) && ( {(error || securityKeyError) && (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
@@ -219,7 +264,7 @@ export default function SmartLoginForm({
</form> </form>
</Form> </Form>
<div className="space-y-2"> <div className="space-y-4">
<Button <Button
type="submit" type="submit"
form="form" form="form"
@@ -236,6 +281,28 @@ export default function SmartLoginForm({
onError={setSecurityKeyError} onError={setSecurityKeyError}
disabled={loading} disabled={loading}
/> />
{orgSignIn && (
<>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<Separator />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="px-2 bg-card text-muted-foreground">
{t("idpContinue")}
</span>
</div>
</div>
<OrgSignInLink
href={orgSignIn.href}
linkText={orgSignIn.linkText}
descriptionText={orgSignIn.descriptionText}
primaryActionVariant="button"
className="mt-0"
/>
</>
)}
</div> </div>
</div> </div>
); );