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",
|
||||
|
|
|
@ -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,10 +151,30 @@ 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") {
|
||||
return {
|
||||
id: user.id,
|
||||
|
@ -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();
|
||||
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.isSAMLLoginEnabled) {
|
||||
router.push("/auth/error?error=" + "SAML login not enabled");
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
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({
|
||||
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,
|
||||
});
|
||||
setEnteredName(nameRef.current?.value || "");
|
||||
setSubmitting(true);
|
||||
} catch (error) {
|
||||
setError(error as Error);
|
||||
setSubmitting(false);
|
||||
|
||||
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