mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-17 06:24:32 +00:00
log in page improvements
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)}`}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user