Feature/sso signup (#1555)
* updated saml-jackson * if logged in redirect to getting-started page with username in the query param * fixed issue with mixed up Google login, profile.id is undefined and this is causing the first record to be retrieved instead of the AND query failing * updated updated saml-jackson * document PGSSLMODE for Heroku * tweaks to PGSSLMODE doc * for self-hosted instance just allow user to signin with any identity (as long as email matches) * fixed submitting flag * added username to onboarding flow (if requested during signup) * added telemetry for google login, saml login, saml config * check if firstName and lastName are defined * convert mutation to an async op * added e2e test to ensure username query param gets picked up during onboarding * fixed minor typo and added note about configuring Google integration as an Internal app when self-hosting * cleaned up unnecessary ssr in sso signup routes * renamed function * Revert "cleaned up unnecessary ssr in sso signup routes" This reverts commit 3607ffef79542d8ca4277a64be38d35bd9457960. * moved client side code to useEffect hook * - format - fixed Save button in SAML config component Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									bf74fab0d3
								
							
						
					
					
						commit
						7b65942de2
					
				
					 12 changed files with 1532 additions and 1176 deletions
				
			
		|  | @ -10,13 +10,15 @@ NEXT_PUBLIC_LICENSE_CONSENT='' | |||
| # DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>/<db-name>' | ||||
| DATABASE_URL="postgresql://postgres:@localhost:5450/calendso" | ||||
| 
 | ||||
| # Needed to enable Google Calendar integrationa and Login with Google | ||||
| # Needed to enable Google Calendar integration and Login with Google | ||||
| # @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials | ||||
| GOOGLE_API_CREDENTIALS='{}' | ||||
| 
 | ||||
| # To enable Login with Google you need to: | ||||
| # 1. Set `GOOGLE_API_CREDENTIALS` above | ||||
| # 2. Set `GOOGLE_LOGIN_ENABLED` to `true` | ||||
| # When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance | ||||
| # @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications | ||||
| GOOGLE_LOGIN_ENABLED=false | ||||
| 
 | ||||
| BASE_URL='http://localhost:3000' | ||||
|  | @ -30,6 +32,9 @@ PLAYWRIGHT_SECRET= | |||
| # @see https://github.com/calendso/calendso/tree/main/ee#setting-up-saml-login | ||||
| # SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml" | ||||
| # SAML_ADMINS='pro@example.com' | ||||
| # If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line. | ||||
| # @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js | ||||
| ##PGSSLMODE='no-verify' | ||||
| 
 | ||||
| # @see: https://github.com/calendso/calendso/issues/263 | ||||
| # Required for Vercel hosting - set NEXTAUTH_URL to equal your BASE_URL | ||||
|  |  | |||
|  | @ -17,7 +17,9 @@ const NavTabs: FC<Props> = ({ tabs, linkProps }) => { | |||
|   const router = useRouter(); | ||||
|   return ( | ||||
|     <> | ||||
|       <nav className="-mb-px flex rtl:space-x-reverse space-x-2 sm:rtl:space-x-reverse space-x-5" aria-label="Tabs"> | ||||
|       <nav | ||||
|         className="flex -mb-px space-x-2 space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse" | ||||
|         aria-label="Tabs"> | ||||
|         {tabs.map((tab) => { | ||||
|           const isCurrent = router.asPath === tab.href; | ||||
|           return ( | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from "react"; | |||
| 
 | ||||
| import { useLocale } from "@lib/hooks/useLocale"; | ||||
| import showToast from "@lib/notification"; | ||||
| import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; | ||||
| import { trpc } from "@lib/trpc"; | ||||
| 
 | ||||
| import { Dialog, DialogTrigger } from "@components/Dialog"; | ||||
|  | @ -22,6 +23,8 @@ export default function SAMLConfiguration({ | |||
| 
 | ||||
|   const query = trpc.useQuery(["viewer.showSAMLView", { teamsView, teamId }]); | ||||
| 
 | ||||
|   const telemetry = useTelemetry(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const data = query.data; | ||||
|     setIsSAMLLoginEnabled(data?.isSAMLLoginEnabled ?? false); | ||||
|  | @ -66,8 +69,11 @@ export default function SAMLConfiguration({ | |||
| 
 | ||||
|     const rawMetadata = samlConfigRef.current.value; | ||||
| 
 | ||||
|     // track Google logins. Without personal data/payload
 | ||||
|     telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.samlConfig, collectPageParameters())); | ||||
| 
 | ||||
|     mutation.mutate({ | ||||
|       rawMetadata: rawMetadata, | ||||
|       encodedRawMetadata: Buffer.from(rawMetadata).toString("base64"), | ||||
|       teamId, | ||||
|     }); | ||||
|   } | ||||
|  | @ -88,14 +94,14 @@ export default function SAMLConfiguration({ | |||
|       {isSAMLLoginEnabled ? ( | ||||
|         <> | ||||
|           <div className="mt-6"> | ||||
|             <h2 className="font-cal text-lg leading-6 font-medium text-gray-900"> | ||||
|             <h2 className="text-lg font-medium leading-6 text-gray-900 font-cal"> | ||||
|               {t("saml_configuration")} | ||||
|               <Badge className="text-xs ml-2" variant={samlConfig ? "success" : "gray"}> | ||||
|               <Badge className="ml-2 text-xs" variant={samlConfig ? "success" : "gray"}> | ||||
|                 {samlConfig ? t("enabled") : t("disabled")} | ||||
|               </Badge> | ||||
|               {samlConfig ? ( | ||||
|                 <> | ||||
|                   <Badge className="text-xs ml-2" variant={"success"}> | ||||
|                   <Badge className="ml-2 text-xs" variant={"success"}> | ||||
|                     {samlConfig ? samlConfig : ""} | ||||
|                   </Badge> | ||||
|                 </> | ||||
|  | @ -104,7 +110,7 @@ export default function SAMLConfiguration({ | |||
|           </div> | ||||
| 
 | ||||
|           {samlConfig ? ( | ||||
|             <div className="mt-2 flex"> | ||||
|             <div className="flex mt-2"> | ||||
|               <Dialog> | ||||
|                 <DialogTrigger asChild> | ||||
|                   <Button | ||||
|  | @ -147,11 +153,7 @@ export default function SAMLConfiguration({ | |||
|             /> | ||||
| 
 | ||||
|             <div className="flex justify-end py-8"> | ||||
|               <button | ||||
|                 type="submit" | ||||
|                 className="ltr:ml-2 rtl:mr-2bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"> | ||||
|                 {t("save")} | ||||
|               </button> | ||||
|               <Button type="submit">{t("save")}</Button> | ||||
|             </div> | ||||
|             <hr className="mt-4" /> | ||||
|           </form> | ||||
|  |  | |||
|  | @ -10,6 +10,9 @@ export const telemetryEventTypes = { | |||
|   bookingConfirmed: "booking_confirmed", | ||||
|   bookingCancelled: "booking_cancelled", | ||||
|   importSubmitted: "import_submitted", | ||||
|   googleLogin: "google_login", | ||||
|   samlLogin: "saml_login", | ||||
|   samlConfig: "saml_config", | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
|     "yarn": ">=1.19.0 < 2.0.0" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@boxyhq/saml-jackson": "0.3.3", | ||||
|     "@boxyhq/saml-jackson": "0.3.6", | ||||
|     "@daily-co/daily-js": "^0.21.0", | ||||
|     "@headlessui/react": "^1.4.2", | ||||
|     "@heroicons/react": "^1.0.5", | ||||
|  | @ -168,4 +168,4 @@ | |||
|   "prisma": { | ||||
|     "seed": "ts-node ./prisma/seed.ts" | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | @ -9,7 +9,7 @@ import { ErrorCode, verifyPassword } from "@lib/auth"; | |||
| import { symmetricDecrypt } from "@lib/crypto"; | ||||
| import prisma from "@lib/prisma"; | ||||
| import { randomString } from "@lib/random"; | ||||
| import { isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml"; | ||||
| import { isSAMLLoginEnabled, samlLoginUrl, hostedCal } from "@lib/saml"; | ||||
| import slugify from "@lib/slugify"; | ||||
| 
 | ||||
| import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants"; | ||||
|  | @ -124,10 +124,10 @@ if (isSAMLLoginEnabled) { | |||
|     profile: (profile) => { | ||||
|       return { | ||||
|         id: profile.id || "", | ||||
|         firstName: profile.first_name || "", | ||||
|         lastName: profile.last_name || "", | ||||
|         firstName: profile.firstName || "", | ||||
|         lastName: profile.lastName || "", | ||||
|         email: profile.email || "", | ||||
|         name: `${profile.firstName} ${profile.lastName}`, | ||||
|         name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(), | ||||
|         email_verified: true, | ||||
|       }; | ||||
|     }, | ||||
|  | @ -151,8 +151,28 @@ export default NextAuth({ | |||
|   providers, | ||||
|   callbacks: { | ||||
|     async jwt({ token, user, account }) { | ||||
|       if (!user) { | ||||
|       const autoMergeIdentities = async () => { | ||||
|         if (!hostedCal) { | ||||
|           const existingUser = await prisma.user.findFirst({ | ||||
|             where: { email: token.email! }, | ||||
|           }); | ||||
| 
 | ||||
|           if (!existingUser) { | ||||
|             return token; | ||||
|           } | ||||
| 
 | ||||
|           return { | ||||
|             id: existingUser.id, | ||||
|             username: existingUser.username, | ||||
|             email: existingUser.email, | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         return token; | ||||
|       }; | ||||
| 
 | ||||
|       if (!user) { | ||||
|         return await autoMergeIdentities(); | ||||
|       } | ||||
| 
 | ||||
|       if (account && account.type === "credentials") { | ||||
|  | @ -185,7 +205,7 @@ export default NextAuth({ | |||
|         }); | ||||
| 
 | ||||
|         if (!existingUser) { | ||||
|           return token; | ||||
|           return await autoMergeIdentities(); | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|  | @ -274,6 +294,11 @@ export default NextAuth({ | |||
|         }); | ||||
| 
 | ||||
|         if (existingUserWithEmail) { | ||||
|           // if self-hosted then we can allow auto-merge of identity providers if email is verified
 | ||||
|           if (!hostedCal && existingUserWithEmail.emailVerified) { | ||||
|             return true; | ||||
|           } | ||||
| 
 | ||||
|           // check if user was invited
 | ||||
|           if ( | ||||
|             !existingUserWithEmail.password && | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import { ErrorCode, getSession } from "@lib/auth"; | |||
| import { WEBSITE_URL } from "@lib/config/constants"; | ||||
| import { useLocale } from "@lib/hooks/useLocale"; | ||||
| import { isSAMLLoginEnabled, hostedCal, samlTenantID, samlProductID } from "@lib/saml"; | ||||
| import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; | ||||
| import { trpc } from "@lib/trpc"; | ||||
| import { inferSSRProps } from "@lib/types/inferSSRProps"; | ||||
| 
 | ||||
|  | @ -44,6 +45,8 @@ export default function Login({ | |||
|     [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), | ||||
|   }; | ||||
| 
 | ||||
|   const telemetry = useTelemetry(); | ||||
| 
 | ||||
|   const callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "/"; | ||||
| 
 | ||||
|   async function handleSubmit(e: React.SyntheticEvent) { | ||||
|  | @ -177,7 +180,16 @@ export default function Login({ | |||
|               color="secondary" | ||||
|               className="flex justify-center w-full" | ||||
|               data-testid={"google"} | ||||
|               onClick={async () => await signIn("google")}> | ||||
|               onClick={async (event) => { | ||||
|                 event.preventDefault(); | ||||
| 
 | ||||
|                 // track Google logins. Without personal data/payload
 | ||||
|                 telemetry.withJitsu((jitsu) => | ||||
|                   jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters()) | ||||
|                 ); | ||||
| 
 | ||||
|                 await signIn("google"); | ||||
|               }}> | ||||
|               {" "} | ||||
|               {t("signin_with_google")} | ||||
|             </Button> | ||||
|  | @ -192,6 +204,11 @@ export default function Login({ | |||
|               onClick={async (event) => { | ||||
|                 event.preventDefault(); | ||||
| 
 | ||||
|                 // track SAML logins. Without personal data/payload
 | ||||
|                 telemetry.withJitsu((jitsu) => | ||||
|                   jitsu.track(telemetryEventTypes.samlLogin, collectPageParameters()) | ||||
|                 ); | ||||
| 
 | ||||
|                 if (!hostedCal) { | ||||
|                   await signIn("saml", {}, { tenant: samlTenantID, product: samlProductID }); | ||||
|                 } else { | ||||
|  |  | |||
|  | @ -1,35 +1,40 @@ | |||
| import { GetServerSidePropsContext } from "next"; | ||||
| import { signIn } from "next-auth/react"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useEffect } from "react"; | ||||
| 
 | ||||
| import { asStringOrNull } from "@lib/asStringOrNull"; | ||||
| import { getSession } from "@lib/auth"; | ||||
| import prisma from "@lib/prisma"; | ||||
| import { isSAMLLoginEnabled, hostedCal, samlTenantID, samlProductID, samlTenantProduct } from "@lib/saml"; | ||||
| import { inferSSRProps } from "@lib/types/inferSSRProps"; | ||||
| 
 | ||||
| import { ssrInit } from "@server/lib/ssr"; | ||||
| 
 | ||||
| export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>; | ||||
| 
 | ||||
| export default function Type(props: SSOProviderPageProps) { | ||||
| export default function Provider(props: SSOProviderPageProps) { | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   if (props.provider === "saml") { | ||||
|     const email = typeof router.query?.email === "string" ? router.query?.email : null; | ||||
|   useEffect(() => { | ||||
|     if (props.provider === "saml") { | ||||
|       const email = typeof router.query?.email === "string" ? router.query?.email : null; | ||||
| 
 | ||||
|     if (!email) { | ||||
|       router.push("/auth/error?error=" + "Email not provided"); | ||||
|       return null; | ||||
|       if (!email) { | ||||
|         router.push("/auth/error?error=" + "Email not provided"); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!props.isSAMLLoginEnabled) { | ||||
|         router.push("/auth/error?error=" + "SAML login not enabled"); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       signIn("saml", {}, { tenant: props.tenant, product: props.product }); | ||||
|     } else { | ||||
|       signIn(props.provider); | ||||
|     } | ||||
| 
 | ||||
|     if (!props.isSAMLLoginEnabled) { | ||||
|       router.push("/auth/error?error=" + "SAML login not enabled"); | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     signIn("saml", {}, { tenant: props.tenant, product: props.product }); | ||||
|   } else { | ||||
|     signIn(props.provider); | ||||
|   } | ||||
| 
 | ||||
|   }, []); | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
|  | @ -38,17 +43,32 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => | |||
|   // (would be even better to assert them instead of typecasting)
 | ||||
|   const providerParam = asStringOrNull(context.query.provider); | ||||
|   const emailParam = asStringOrNull(context.query.email); | ||||
|   const usernameParam = asStringOrNull(context.query.username); | ||||
| 
 | ||||
|   if (!providerParam) { | ||||
|     throw new Error(`File is not named sso/[provider]`); | ||||
|   } | ||||
| 
 | ||||
|   const { req } = context; | ||||
| 
 | ||||
|   const session = await getSession({ req }); | ||||
|   const ssr = await ssrInit(context); | ||||
| 
 | ||||
|   if (session) { | ||||
|     return { | ||||
|       redirect: { | ||||
|         destination: "/getting-started" + (usernameParam ? `?username=${usernameParam}` : ""), | ||||
|         permanent: false, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   let error: string | null = null; | ||||
| 
 | ||||
|   let tenant = samlTenantID; | ||||
|   let product = samlProductID; | ||||
| 
 | ||||
|   if (providerParam === "saml") { | ||||
|   if (providerParam === "saml" && hostedCal) { | ||||
|     if (!emailParam) { | ||||
|       error = "Email not provided"; | ||||
|     } else { | ||||
|  | @ -73,6 +93,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => | |||
| 
 | ||||
|   return { | ||||
|     props: { | ||||
|       trpcState: ssr.dehydrate(), | ||||
|       provider: providerParam, | ||||
|       isSAMLLoginEnabled, | ||||
|       hostedCal, | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import { useForm } from "react-hook-form"; | |||
| import TimezoneSelect from "react-timezone-select"; | ||||
| import * as z from "zod"; | ||||
| 
 | ||||
| import { asStringOrNull } from "@lib/asStringOrNull"; | ||||
| import { getSession } from "@lib/auth"; | ||||
| import { DEFAULT_SCHEDULE } from "@lib/availability"; | ||||
| import { useLocale } from "@lib/hooks/useLocale"; | ||||
|  | @ -24,6 +25,7 @@ import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations | |||
| import getIntegrations from "@lib/integrations/getIntegrations"; | ||||
| import prisma from "@lib/prisma"; | ||||
| import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; | ||||
| import { trpc } from "@lib/trpc"; | ||||
| import { inferSSRProps } from "@lib/types/inferSSRProps"; | ||||
| import { Schedule as ScheduleType } from "@lib/types/schedule"; | ||||
| 
 | ||||
|  | @ -46,11 +48,32 @@ type ScheduleFormValues = { | |||
|   schedule: ScheduleType; | ||||
| }; | ||||
| 
 | ||||
| let mutationComplete: ((err: Error | null) => void) | null; | ||||
| 
 | ||||
| export default function Onboarding(props: inferSSRProps<typeof getServerSideProps>) { | ||||
|   const { t } = useLocale(); | ||||
|   const router = useRouter(); | ||||
|   const telemetry = useTelemetry(); | ||||
| 
 | ||||
|   const mutation = trpc.useMutation("viewer.updateProfile", { | ||||
|     onSuccess: async () => { | ||||
|       setSubmitting(true); | ||||
|       setEnteredName(nameRef.current?.value || ""); | ||||
|       if (mutationComplete) { | ||||
|         mutationComplete(null); | ||||
|         mutationComplete = null; | ||||
|       } | ||||
|       setSubmitting(false); | ||||
|     }, | ||||
|     onError: (err) => { | ||||
|       setError(new Error(err.message)); | ||||
|       if (mutationComplete) { | ||||
|         mutationComplete(new Error(err.message)); | ||||
|       } | ||||
|       setSubmitting(false); | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   const DEFAULT_EVENT_TYPES = [ | ||||
|     { | ||||
|       title: t("15min_meeting"), | ||||
|  | @ -128,6 +151,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
| 
 | ||||
|   /** Name */ | ||||
|   const nameRef = useRef<HTMLInputElement>(null); | ||||
|   const usernameRef = useRef<HTMLInputElement>(null); | ||||
|   const bioRef = useRef<HTMLInputElement>(null); | ||||
|   /** End Name */ | ||||
|   /** TimeZone */ | ||||
|  | @ -138,7 +162,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
|   const [currentStep, setCurrentStep] = useState(0); | ||||
|   const detectStep = () => { | ||||
|     let step = 0; | ||||
|     const hasSetUserNameOrTimeZone = props.user?.name && props.user?.timeZone; | ||||
|     const hasSetUserNameOrTimeZone = props.user?.name && props.user?.timeZone && !props.usernameParam; | ||||
|     if (hasSetUserNameOrTimeZone) { | ||||
|       step = 1; | ||||
|     } | ||||
|  | @ -170,7 +194,6 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
|       incrementStep(); | ||||
|       setSubmitting(false); | ||||
|     } catch (error) { | ||||
|       console.log("handleConfirmStep", error); | ||||
|       setSubmitting(false); | ||||
|       setError(error as Error); | ||||
|     } | ||||
|  | @ -326,6 +349,25 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
|           </div> | ||||
|           <form className="sm:mx-auto sm:w-full"> | ||||
|             <section className="space-y-8"> | ||||
|               {props.usernameParam && ( | ||||
|                 <fieldset> | ||||
|                   <label htmlFor="name" className="block text-sm font-medium text-gray-700"> | ||||
|                     {t("username")} | ||||
|                   </label> | ||||
|                   <input | ||||
|                     ref={usernameRef} | ||||
|                     type="text" | ||||
|                     name="username" | ||||
|                     id="username" | ||||
|                     data-testid="username" | ||||
|                     placeholder={t("username")} | ||||
|                     defaultValue={props.usernameParam ?? ""} | ||||
|                     required | ||||
|                     className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm" | ||||
|                   /> | ||||
|                 </fieldset> | ||||
|               )} | ||||
| 
 | ||||
|               <fieldset> | ||||
|                 <label htmlFor="name" className="block text-sm font-medium text-gray-700"> | ||||
|                   {t("full_name")} | ||||
|  | @ -369,17 +411,26 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
|       showCancel: true, | ||||
|       cancelText: t("set_up_later"), | ||||
|       onComplete: async () => { | ||||
|         try { | ||||
|           setSubmitting(true); | ||||
|           await updateUser({ | ||||
|             name: nameRef.current?.value, | ||||
|             timeZone: selectedTimeZone, | ||||
|           }); | ||||
|           setEnteredName(nameRef.current?.value || ""); | ||||
|           setSubmitting(true); | ||||
|         } catch (error) { | ||||
|           setError(error as Error); | ||||
|           setSubmitting(false); | ||||
|         mutationComplete = null; | ||||
|         setError(null); | ||||
|         const mutationAsync = new Promise((resolve, reject) => { | ||||
|           mutationComplete = (err) => { | ||||
|             if (err) { | ||||
|               reject(err); | ||||
|               return; | ||||
|             } | ||||
|             resolve(null); | ||||
|           }; | ||||
|         }); | ||||
| 
 | ||||
|         mutation.mutate({ | ||||
|           username: usernameRef.current?.value, | ||||
|           name: nameRef.current?.value, | ||||
|           timeZone: selectedTimeZone, | ||||
|         }); | ||||
| 
 | ||||
|         if (mutationComplete) { | ||||
|           await mutationAsync; | ||||
|         } | ||||
|       }, | ||||
|     }, | ||||
|  | @ -480,7 +531,6 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
|       onComplete: async () => { | ||||
|         try { | ||||
|           setSubmitting(true); | ||||
|           console.log("updating"); | ||||
|           await updateUser({ | ||||
|             bio: bioRef.current?.value, | ||||
|           }); | ||||
|  | @ -532,7 +582,7 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
|                 Step {currentStep + 1} of {steps.length} | ||||
|               </Text> | ||||
| 
 | ||||
|               {error && <Alert severity="error" {...error} />} | ||||
|               {error && <Alert severity="error" message={error?.message} />} | ||||
| 
 | ||||
|               <section className="flex w-full space-x-2 rtl:space-x-reverse"> | ||||
|                 {steps.map((s, index) => { | ||||
|  | @ -591,6 +641,8 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp | |||
| } | ||||
| 
 | ||||
| export async function getServerSideProps(context: NextPageContext) { | ||||
|   const usernameParam = asStringOrNull(context.query.username); | ||||
| 
 | ||||
|   const session = await getSession(context); | ||||
| 
 | ||||
|   let integrations = []; | ||||
|  | @ -693,6 +745,7 @@ export async function getServerSideProps(context: NextPageContext) { | |||
|       connectedCalendars, | ||||
|       eventTypes, | ||||
|       schedules, | ||||
|       usernameParam, | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { test } from "@playwright/test"; | ||||
| import { expect, test } from "@playwright/test"; | ||||
| 
 | ||||
| test.describe("Onboarding", () => { | ||||
|   test.use({ storageState: "playwright/artifacts/onboardingStorageState.json" }); | ||||
|  | @ -11,4 +11,17 @@ test.describe("Onboarding", () => { | |||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   const username = "calendso"; | ||||
|   test(`/getting-started?username=${username} shows the first step of onboarding with username field populated`, async ({ | ||||
|     page, | ||||
|   }) => { | ||||
|     await page.goto("/getting-started?username=" + username); | ||||
| 
 | ||||
|     await page.waitForSelector("[data-testid=username]"); | ||||
| 
 | ||||
|     await expect(await page.$eval("[data-testid=username]", (el: HTMLInputElement) => el.value)).toEqual( | ||||
|       username | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -781,17 +781,17 @@ const loggedInViewerRouter = createProtectedRouter() | |||
|   }) | ||||
|   .mutation("updateSAMLConfig", { | ||||
|     input: z.object({ | ||||
|       rawMetadata: z.string(), | ||||
|       encodedRawMetadata: z.string(), | ||||
|       teamId: z.union([z.number(), z.null(), z.undefined()]), | ||||
|     }), | ||||
|     async resolve({ input }) { | ||||
|       const { rawMetadata, teamId } = input; | ||||
|       const { encodedRawMetadata, teamId } = input; | ||||
| 
 | ||||
|       const { apiController } = await jackson(); | ||||
| 
 | ||||
|       try { | ||||
|         return await apiController.config({ | ||||
|           rawMetadata, | ||||
|           encodedRawMetadata, | ||||
|           defaultRedirectUrl: `${process.env.BASE_URL}/api/auth/saml/idp`, | ||||
|           redirectUrl: JSON.stringify([`${process.env.BASE_URL}/*`]), | ||||
|           tenant: teamId ? tenantPrefix + teamId : samlTenantID, | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Deepak Prabhakara
						Deepak Prabhakara