diff --git a/.env.example b/.env.example index cef10a0cf0..350c86e9c1 100644 --- a/.env.example +++ b/.env.example @@ -120,6 +120,10 @@ NEXT_PUBLIC_HELPSCOUT_KEY= NEXT_PUBLIC_FRESHCHAT_TOKEN= NEXT_PUBLIC_FRESHCHAT_HOST= +# Microsoft OAuth credentials +OUTLOOK_LOGIN_ENABLED=false + + # For holiday feature: # Step-by-step: Get a Google Calendar API Key # 1. Go to Google Cloud Console: https://console.cloud.google.com/ @@ -127,7 +131,6 @@ NEXT_PUBLIC_FRESHCHAT_HOST= # 3. Enable Google Calendar API (APIs & Services → Library , Search for Google Calendar API) # 4. Create the API Key (APIs & Services → Credentials) GOOGLE_CALENDAR_API_KEY= - # Google OAuth credentials # To enable Login with Google you need to: # 1. Set `GOOGLE_API_CREDENTIALS` below diff --git a/apps/web/lib/signup/getServerSideProps.tsx b/apps/web/lib/signup/getServerSideProps.tsx index 0bb15253fa..d2014c5ba0 100644 --- a/apps/web/lib/signup/getServerSideProps.tsx +++ b/apps/web/lib/signup/getServerSideProps.tsx @@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next"; import { z } from "zod"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { IS_OUTLOOK_LOGIN_ENABLED } from "@calcom/features/auth/lib/outlook"; import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername"; import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; @@ -59,6 +60,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { const props = { redirectUrl, isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED, + isOutlookLoginEnabled: IS_OUTLOOK_LOGIN_ENABLED, isSAMLLoginEnabled, prepopulateFormValues: undefined, emailVerificationEnabled, diff --git a/apps/web/modules/auth/hooks/useLastUsed.tsx b/apps/web/modules/auth/hooks/useLastUsed.tsx index 0ab3a23f0a..93cef616ea 100644 --- a/apps/web/modules/auth/hooks/useLastUsed.tsx +++ b/apps/web/modules/auth/hooks/useLastUsed.tsx @@ -4,7 +4,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { localStorage } from "@calcom/lib/webstorage"; import classNames from "@calcom/ui/classNames"; -type LoginType = "saml" | "google" | "credentials"; +type LoginType = "saml" | "google" | "microsoft" | "credentials"; export function useLastUsed() { const [lastUsed, setLastUsed] = useState(); diff --git a/apps/web/modules/auth/login-view.tsx b/apps/web/modules/auth/login-view.tsx index 5edc63d02d..bf7ac4fc18 100644 --- a/apps/web/modules/auth/login-view.tsx +++ b/apps/web/modules/auth/login-view.tsx @@ -1,17 +1,6 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import classNames from "classnames"; -import { signIn } from "next-auth/react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import { z } from "zod"; - -import { SAMLLogin } from "@calcom/web/modules/auth/components/SAMLLogin"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; -import { LastUsed, useLastUsed } from "@calcom/web/modules/auth/hooks/useLastUsed"; import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; import { emailRegex } from "@calcom/lib/emailSchema"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; @@ -19,17 +8,27 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Alert } from "@calcom/ui/components/alert"; -import { Button } from "@calcom/ui/components/button"; -import { EmailField, PasswordField } from "@calcom/ui/components/form"; - -import type { inferSSRProps } from "@lib/types/inferSSRProps"; - +import { Icon } from "@calcom/ui/components/icon"; +import { SAMLLogin } from "@calcom/web/modules/auth/components/SAMLLogin"; +import { LastUsed, useLastUsed } from "@calcom/web/modules/auth/hooks/useLastUsed"; import AddToHomescreen from "@components/AddToHomescreen"; import BackupCode from "@components/auth/BackupCode"; import TwoFactor from "@components/auth/TwoFactor"; -import AuthContainer from "@components/ui/AuthContainer"; - +import { Button } from "@coss/ui/components/button"; +import { Field, FieldLabel } from "@coss/ui/components/field"; +import { Input } from "@coss/ui/components/input"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@coss/ui/components/input-group"; +import { Separator } from "@coss/ui/components/separator"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { inferSSRProps } from "@lib/types/inferSSRProps"; import type { getServerSideProps } from "@server/lib/auth/login/getServerSideProps"; +import { Eye, EyeOff } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { signIn } from "next-auth/react"; +import { useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { z } from "zod"; interface LoginValues { email: string; @@ -39,13 +38,75 @@ interface LoginValues { csrfToken: string; } -const GoogleIcon = () => ( - Continue with Google Icon +const MicrosoftIcon = () => ( + ); + +const GoogleIcon = () => ( + +); + +function BackgroundGrid() { + const rows = 9; + const cols = 18; + const size = 60; + const gap = 8; + const radius = 8; + const width = cols * size + (cols - 1) * gap; + const height = rows * size + (rows - 1) * gap; + + return ( +
+ + + + + + + + + + + + + + + + {Array.from({ length: rows * cols }).map((_, i) => { + const col = i % cols; + const row = Math.floor(i / cols); + const x = col * (size + gap); + const y = row * (size + gap); + return ( + + ); + })} + + +
+ ); +} export type PageProps = inferSSRProps; export default function Login({ csrfToken, isGoogleLoginEnabled, + isOutlookLoginEnabled, isSAMLLoginEnabled, samlTenantID, samlProductID, @@ -70,6 +131,7 @@ export default function Login({ const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [lastUsed, setLastUsed] = useLastUsed(); + const [showPassword, setShowPassword] = useState(false); const errorMessages: { [key: string]: string } = { // [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"), @@ -93,57 +155,20 @@ export default function Login({ callbackUrl = safeCallbackUrl || ""; - const signupUrl = callbackUrl - ? `${WEBSITE_URL}/signup?redirect=${encodeURIComponent(callbackUrl)}` - : `${WEBSITE_URL}/signup`; + const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery(); - const LoginFooter = ( - - {t("dont_have_an_account")} - + useEffect( + function refactorMeWithoutEffect() { + if (error) { + setErrorMessage(error.message); + } + }, + [error] ); - const TwoFactorFooter = ( - <> - - {!twoFactorLostAccess ? ( - - ) : null} - - ); - - const ExternalTotpFooter = ( - - ); + const displaySSOLogin = HOSTED_CAL_FEATURES + ? true + : isSAMLLoginEnabled && !isPending && data?.connectionExists; const onSubmit = async (values: LoginValues) => { setErrorMessage(null); @@ -165,132 +190,232 @@ export default function Login({ else setErrorMessage(errorMessages[res.error] || t("something_went_wrong")); }; - const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery(); - - useEffect( - function refactorMeWithoutEffect() { - if (error) { - setErrorMessage(error.message); - } - }, - [error] - ); - - const displaySSOLogin = HOSTED_CAL_FEATURES - ? true - : isSAMLLoginEnabled && !isPending && data?.connectionExists; + const showSocialLogin = isGoogleLoginEnabled || isOutlookLoginEnabled; + const showSignupLink = + process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true" && searchParams?.get("register") !== "false"; return ( -
- - - {!twoFactorRequired && ( - <> -
- {isGoogleLoginEnabled && ( - - )} - {displaySSOLogin && ( - - )} -
- {(isGoogleLoginEnabled || displaySSOLogin) && ( -
-
-
- - {t("or").toLocaleLowerCase()} - -
-
+
+ + +
+ {/* Main Card */} +
+ {/* Logo */} +
+

Cal.com

+
+ + {/* Heading */} +

+ {twoFactorRequired ? t("2fa_code") : t("welcome_back_sign_in")} +

+ + + {/* Social Login Buttons */} + {!twoFactorRequired && showSocialLogin && ( + <> +
+ {isGoogleLoginEnabled && ( + + )} + {isOutlookLoginEnabled && ( + + )} +
+ + {/* Divider */} +
+ + {t("or").toLowerCase()} + +
+ + )} + +
+ + + {!twoFactorRequired && ( +
+ {/* Email Field */} + + {t("email")} + + {formState.errors.email && ( +

+ {formState.errors.email.message} +

+ )} +
+ + {/* Password Field */} + +
+ {t("password")} + + {t("forgot")} + +
+ + + + + + + {formState.errors.password && ( +

+ {formState.errors.password.message} +

+ )} +
)} - - )} - -
- -
-
-
- -
- -
- - {t("forgot")} - -
+ {/* Two Factor */} + {twoFactorRequired && ( +
+ {!twoFactorLostAccess ? : }
-
+ )} - {twoFactorRequired ? !twoFactorLostAccess ? : : null} + {/* Error Message */} + {errorMessage && } - {errorMessage && } + {/* Submit Button */} -
- - - + + + {/* Two Factor Footer */} + {twoFactorRequired && ( +
+ {!totpEmail ? ( + <> + + {!twoFactorLostAccess && ( + + )} + + ) : ( + + )} +
+ )} + +
+ + {/* Footer Links */} + {!twoFactorRequired && ( +
+ {showSignupLink && ( + + {t("create_account")} + + )} + {displaySSOLogin && ( + <> + {showSignupLink && ·} + + + )} +
+ )} +
+
); diff --git a/apps/web/modules/auth/sso/provider-view.tsx b/apps/web/modules/auth/sso/provider-view.tsx index bc9d5a5964..aebc25fa7c 100644 --- a/apps/web/modules/auth/sso/provider-view.tsx +++ b/apps/web/modules/auth/sso/provider-view.tsx @@ -32,6 +32,8 @@ export default function Provider(props: SSOProviderPageProps) { signIn("saml", {}, { tenant: props.tenant, product: props.product }); } else if (props.provider === "google" && email) { signIn("google", {}, { login_hint: email }); + } else if (props.provider === "microsoft") { + signIn("azure-ad"); } else { signIn(props.provider); } diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index e72fbde1b3..f315c9461f 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -187,6 +187,7 @@ export default function Signup({ token, orgSlug, isGoogleLoginEnabled, + isOutlookLoginEnabled, isSAMLLoginEnabled, orgAutoAcceptEmail, redirectUrl, @@ -198,6 +199,7 @@ export default function Signup({ const [premiumUsername, setPremiumUsername] = useState(false); const [usernameTaken, setUsernameTaken] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false); const [accountUnderReview, setAccountUnderReview] = useState(false); const [displayEmailForm, setDisplayEmailForm] = useState(token); const [turnstileKey, setTurnstileKey] = useState(0); @@ -652,6 +654,7 @@ export default function Signup({
)} - {isGoogleLoginEnabled && ( + {isOutlookLoginEnabled && ( +
+ +
+ )} + + {(isGoogleLoginEnabled || isOutlookLoginEnabled) && (
@@ -714,7 +769,7 @@ export default function Signup({