feat: autolink org on provider signup (#11267)

* feat: autolink org on provider signup

* Adding env var in example

* slugifying inferred org username

* Updated org README

* Optional env var for autolinking added to readme

* Typo

* md syntax correction

* Verifying user when autolinked

* Also verifying users invited to org

* Applying feedback

* Scoping to Google for now

* Updating comment

* Updating readme with Google scoping
This commit is contained in:
Leo Giovanetti 2023-09-12 12:17:34 -03:00 committed by GitHub
parent 84408025ed
commit 86af383f6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 68 additions and 26 deletions

View File

@ -213,6 +213,10 @@ NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
# use organizations
ORGANIZATIONS_ENABLED=
# This variable should only be set to 1 or true if you want to autolink external provider sing-ups with
# existing organizations based on email domain address
ORGANIZATIONS_AUTOLINK=
# Vercel Config to create subdomains for organizations
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
PROJECT_ID_VERCEL=

View File

@ -18,7 +18,7 @@ import { isENVDev } from "@calcom/lib/env";
import { randomString } from "@calcom/lib/random";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { ErrorCode } from "./ErrorCode";
@ -31,6 +31,8 @@ const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {};
const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true";
const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED);
const ORGANIZATIONS_AUTOLINK =
process.env.ORGANIZATIONS_AUTOLINK === "1" || process.env.ORGANIZATIONS_AUTOLINK === "true";
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
@ -54,6 +56,33 @@ export const checkIfUserBelongsToActiveTeam = <T extends UserTeams>(user: T) =>
return metadata.success && metadata.data?.subscriptionId;
});
const checkIfUserShouldBelongToOrg = async (idP: IdentityProvider, email: string) => {
const [orgUsername, apexDomain] = email.split("@");
if (!ORGANIZATIONS_AUTOLINK || idP !== "GOOGLE") return { orgUsername, orgId: undefined };
const existingOrg = await prisma.team.findFirst({
where: {
AND: [
{
metadata: {
path: ["isOrganizationVerified"],
equals: true,
},
},
{
metadata: {
path: ["orgAutoAcceptEmail"],
equals: apexDomain,
},
},
],
},
select: {
id: true,
},
});
return { orgUsername, orgId: existingOrg?.id };
};
const providers: Provider[] = [
CredentialsProvider({
id: "credentials",
@ -735,16 +764,26 @@ export const AUTH_OPTIONS: AuthOptions = {
return "/auth/error?error=use-identity-login";
}
// Associate with organization if enabled by flag and idP is Google (for now)
const { orgUsername, orgId } = await checkIfUserShouldBelongToOrg(idP, user.email);
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
username: orgId ? slugify(orgUsername) : usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
email: user.email,
identityProvider: idP,
identityProviderId: account.providerAccountId,
...(orgId && {
verified: true,
organization: { connect: { id: orgId } },
teams: {
create: { role: MembershipRole.MEMBER, accepted: true, team: { connect: { id: orgId } } },
},
}),
},
});

View File

@ -4,34 +4,31 @@ From the [Original RFC](https://github.com/calcom/cal.com/issues/7142):
> We want to create organisations within Cal.com to enable people to easily and effectively manage multiple teams. An organisation will live above the current teams layer.
## Quick start
## App setup
1. Set `ORGANIZATIONS_ENABLED=1` in .env
1. Add `app.cal.local` to your host file, either:
a. `sudo npx hostile app.cal.local`
b. Add it yourself
1. Add `acme.cal.local` to host file (I use this as an org name for testing public URLS)
1. Visit `/settings/organizations/new` → follow setup steps with the slug matching the org slug from step 3
1. You should now be in ORG context.
1. You may or may not need the feature flag enabled.
1. Log in as admin and in Settings, turn on Organizations feature flag under Features section. Organizations feature has an operational feature flag in order to turn on the entire feature.
## Environment variables
2. Set the following environment variables as described:
1. **`CALCOM_LICENSE_KEY`**: Since Organizations is an EE feature, a license key should be present, either as this environment variable or visiting as an Admin `/auth/setup`. To get a license key you should visit Cal Console ([prod](https://console.cal.com) or [dev](https://console.cal.dev))
2. **`NEXT_PUBLIC_WEBAPP_URL`**: In case of local development, this variable should be set to `https://app.cal.local:3000` to be able to handle subdomains correctly in terms of authentication and cookies
3. **`NEXTAUTH_URL`**: Should be equal to `NEXT_PUBLIC_WEBAPP_URL` which is `https://app.cal.local:3000`
4. **`NEXTAUTH_COOKIE_DOMAIN`**: In case of local development, this variable should be set to `.cal.local` to be able to accept session cookies in subdomains as well otherwise it should be set to the corresponding environment such as `.cal.dev`, `.cal.qa` or `.cal.com`. If you choose another subdomain, the value for this should match the apex domain of `NEXT_PUBLIC_WEBAPP_URL` with a leading dot (`.`)
5. **`ORGANIZATIONS_ENABLED`**: Should be set to `1` or `true`
6. **`STRIPE_ORG_MONTHLY_PRICE_ID`**: For dev and all testing should be set to your own testing key. Or ask for the shared key if you're a core member.
7. **`ORGANIZATIONS_AUTOLINK`**: Optional. Set to `1` or `true` to let new signed-up users using Google external provider join the corresponding organization based on the email domain name.
`CALCOM_LICENSE_KEY`: Since Organizations is an EE feature, a license key should be present, either as this environment variable or visiting as an Admin `/auth/setup`
`NEXT_PUBLIC_WEBAPP_URL`: In case of local development, this variable should be set to `https://app.cal.local:3000` to be able to handle subdomains
`NEXTAUTH_URL`: Should be equal to `NEXT_PUBLIC_WEBAPP_URL`
`NEXTAUTH_COOKIE_DOMAIN`: In case of local development, this variable should be set to `.cal.local` to be able to accept session cookies in subdomains as well otherwise it should be set to the corresponding environment such as `.cal.dev`, `.cal.qa` or `.cal.com`
`ORGANIZATIONS_ENABLED`: Should be set to `1`
`STRIPE_ORG_MONTHLY_PRICE_ID`: For dev and all testing should be set to your own testing key. Or ask for the shared key if you're a core member.
3. Add `app.cal.local` to your host file, either:
1. `sudo npx hostile app.cal.local`
2. Add it yourself
## Feature flag
4. Add `acme.cal.local` to host file given that the org create for it will be `acme`, otherwise do this for whatever slug will be assigned to the org. This is needed to test org-related public URLs, such as sub-teams, members and event-types.
Organizations has an operational feature flag in order to turn on the entire feature, be sure to log in as Admin and visit Features section in Settings to turn on/off this feature.
5. Be sure to be logged in with any type of user and visit `/settings/organizations/new` and follow setup steps with the slug matching the org slug from step 3
## Domain setup
6. Log in as admin and go to Settings and under Organizations you will need to accept the newly created organization in order to be operational
When a new organization is created, a subdomain can be used with Cal App to show public profiles for the organization per se, their teams and their members.
7. After finishing the org creation, you will be automatically logged in as the owner of the organization, and the app will be shown in organization mode
When working locally, the app works under the subdomain `app.cal.local:3000` and any organization works under `acme.cal.local:3000` which will need to have acme.cal.local mapped in your system hosts file to point to `127.0.0.1` to be able to see the mentioned public profiles.
## DNS setup
When working in any other environment, the subdomain registration works with Vercel API, to assign the organization subdomain like acme.cal.dev to work with the app. Depending on whether the domain's DNS such as cal.dev in the previous example is being managed by Vercel or an external service such as Cloudflare, the subdomain registration may need manual steps. This is going to be automated in the near future.
When a new organization is created, other than not being verified up until the admin accepts it in settings as explained in step 6, a flag gets created that marks the organization as missing DNS setup. That flag get auto-checked by the system upon organization creation when the Cal instance is deployed in Vercel and the subdomain registration was successful. Logging in as admin and going to Settings > Organizations section, you will see that flag as a badge, designed to give admins a glimpe on what is pending in terms of making an organization work. Alongside the mentioned badge, an email gets sent to admins in order to warn them there is a pending action about setting up DNS for the newly created organization to work.

View File

@ -15,7 +15,7 @@ const OrganizationAvatar = ({ size, imageSrc, alt, organizationSlug, ...rest }:
indicator={
organizationSlug ? (
<div
className={classNames("absolute bottom-0 right-0 z-10", size === "lg" ? "h-3 w-3" : "h-10 w-10")}>
className={classNames("absolute bottom-0 right-0 z-10", size === "lg" ? "h-6 w-6" : "h-10 w-10")}>
<img
src={`/org/${organizationSlug}/avatar.png`}
alt={alt}

View File

@ -148,7 +148,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
organization: {
create: {
name,
...(IS_TEAM_BILLING_ENABLED ? { slug } : {}),
...(!IS_TEAM_BILLING_ENABLED ? { slug } : {}),
metadata: {
...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),
isOrganization: true,

View File

@ -148,6 +148,7 @@ export async function createNewUserConnectToOrgIfExists({
const createdUser = await prisma.user.create({
data: {
email: usernameOrEmail,
verified: true,
invitedTo: input.teamId,
organizationId: orgId || null, // If the user is invited to a child team, they are automatically added to the parent org
teams: {

View File

@ -258,6 +258,7 @@
"NEXTAUTH_URL",
"NODE_ENV",
"ORGANIZATIONS_ENABLED",
"ORGANIZATIONS_AUTOLINK",
"PAYMENT_FEE_FIXED",
"PAYMENT_FEE_PERCENTAGE",
"PLAYWRIGHT_HEADLESS",