diff --git a/app/scenes/Invite.tsx b/app/scenes/Invite.tsx index 28e1c625e0..fb74cd1902 100644 --- a/app/scenes/Invite.tsx +++ b/app/scenes/Invite.tsx @@ -7,6 +7,7 @@ import { Link } from "react-router-dom"; import { toast } from "sonner"; import styled from "styled-components"; import { UserRole } from "@shared/types"; +import { parseEmail } from "@shared/utils/email"; import { UserValidation } from "@shared/validations"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; @@ -41,7 +42,7 @@ function Invite({ onSubmit }: Props) { const user = useCurrentUser(); const team = useCurrentTeam(); const { t } = useTranslation(); - const predictedDomain = user.email.split("@")[1]; + const predictedDomain = parseEmail(user.email).domain; const can = usePolicy(team); const [role, setRole] = React.useState(UserRole.Member); diff --git a/plugins/azure/server/auth/azure.ts b/plugins/azure/server/auth/azure.ts index 77268025b1..ac7ddeb87d 100644 --- a/plugins/azure/server/auth/azure.ts +++ b/plugins/azure/server/auth/azure.ts @@ -5,6 +5,7 @@ import type { Context } from "koa"; import Router from "koa-router"; import { Profile } from "passport"; import { slugifyDomain } from "@shared/utils/domains"; +import { parseEmail } from "@shared/utils/email"; import accountProvisioner from "@server/commands/accountProvisioner"; import { MicrosoftGraphError } from "@server/errors"; import passportMiddleware from "@server/middlewares/passport"; @@ -91,7 +92,7 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) { const team = await getTeamFromContext(ctx); const client = getClientFromContext(ctx); - const domain = email.split("@")[1]; + const domain = parseEmail(email).domain; const subdomain = slugifyDomain(domain); const teamName = organization.displayName; diff --git a/plugins/discord/server/auth/discord.ts b/plugins/discord/server/auth/discord.ts index b8a570ff3f..573511d72f 100644 --- a/plugins/discord/server/auth/discord.ts +++ b/plugins/discord/server/auth/discord.ts @@ -10,6 +10,7 @@ import Router from "koa-router"; import { Strategy } from "passport-oauth2"; import { languages } from "@shared/i18n"; import { slugifyDomain } from "@shared/utils/domains"; +import { parseEmail } from "@shared/utils/email"; import slugify from "@shared/utils/slugify"; import accountProvisioner from "@server/commands/accountProvisioner"; import { InvalidRequestError, TeamDomainRequiredError } from "@server/errors"; @@ -77,8 +78,7 @@ if (env.DISCORD_CLIENT_ID && env.DISCORD_CLIENT_SECRET) { /** We have the email scope, so this should never happen */ throw InvalidRequestError("Discord profile email is missing"); } - const parts = email.toLowerCase().split("@"); - const domain = parts.length && parts[1]; + const { domain } = parseEmail(email); if (!domain) { throw TeamDomainRequiredError(); diff --git a/plugins/oidc/server/auth/oidc.ts b/plugins/oidc/server/auth/oidc.ts index 61c9a29a10..b3bcefbd6d 100644 --- a/plugins/oidc/server/auth/oidc.ts +++ b/plugins/oidc/server/auth/oidc.ts @@ -4,6 +4,7 @@ import Router from "koa-router"; import get from "lodash/get"; import { Strategy } from "passport-oauth2"; import { slugifyDomain } from "@shared/utils/domains"; +import { parseEmail } from "@shared/utils/email"; import accountProvisioner from "@server/commands/accountProvisioner"; import { OIDCMalformedUserInfoError, @@ -92,9 +93,7 @@ if ( } const team = await getTeamFromContext(ctx); const client = getClientFromContext(ctx); - - const parts = profile.email.toLowerCase().split("@"); - const domain = parts.length && parts[1]; + const { domain } = parseEmail(profile.email); if (!domain) { throw OIDCMalformedUserInfoError(); diff --git a/server/commands/userProvisioner.ts b/server/commands/userProvisioner.ts index 207aa9526d..905bddbef0 100644 --- a/server/commands/userProvisioner.ts +++ b/server/commands/userProvisioner.ts @@ -1,5 +1,6 @@ import { InferCreationAttributes } from "sequelize"; import { UserRole } from "@shared/types"; +import { parseEmail } from "@shared/utils/email"; import InviteAcceptedEmail from "@server/emails/templates/InviteAcceptedEmail"; import { DomainNotAllowedError, @@ -226,7 +227,7 @@ export default async function userProvisioner({ // If the team settings do not allow this domain, // throw an error and fail user creation. - const domain = email.split("@")[1]; + const { domain } = parseEmail(email); if (team && !(await team.isDomainAllowed(domain))) { throw DomainNotAllowedError(); } diff --git a/server/scripts/seed.ts b/server/scripts/seed.ts index cef1735e2a..1925ccef12 100644 --- a/server/scripts/seed.ts +++ b/server/scripts/seed.ts @@ -1,5 +1,6 @@ import "./bootstrap"; import { UserRole } from "@shared/types"; +import { parseEmail } from "@shared/utils/email"; import teamCreator from "@server/commands/teamCreator"; import env from "@server/env"; import { Team, User } from "@server/models"; @@ -10,6 +11,7 @@ const email = process.argv[2]; export default async function main(exit = false) { const teamCount = await Team.count(); if (teamCount === 0) { + const name = parseEmail(email).local; const user = await sequelize.transaction(async (transaction) => { const team = await teamCreator({ name: "Wiki", @@ -22,7 +24,7 @@ export default async function main(exit = false) { return await User.create( { teamId: team.id, - name: email.split("@")[0], + name, email, role: UserRole.Admin, }, diff --git a/shared/utils/email.test.ts b/shared/utils/email.test.ts new file mode 100644 index 0000000000..2bc09a0282 --- /dev/null +++ b/shared/utils/email.test.ts @@ -0,0 +1,24 @@ +import { parseEmail } from "./email"; + +describe("parseEmail", () => { + it("should correctly parse email", () => { + expect(parseEmail("tom@example.com")).toEqual({ + local: "tom", + domain: "example.com", + }); + expect(parseEmail("tom.m@example.com")).toEqual({ + local: "tom.m", + domain: "example.com", + }); + expect(parseEmail("tom@subdomain.domain.com")).toEqual({ + local: "tom", + domain: "subdomain.domain.com", + }); + }); + + it("should throw error for invalid email", () => { + expect(() => parseEmail("")).toThrow(); + expect(() => parseEmail("invalid")).toThrow(); + expect(() => parseEmail("invalid@")).toThrow(); + }); +}); diff --git a/shared/utils/email.ts b/shared/utils/email.ts new file mode 100644 index 0000000000..2370408ff9 --- /dev/null +++ b/shared/utils/email.ts @@ -0,0 +1,15 @@ +/** + * Parse an email address into its local and domain parts. + * + * @param email The email address to parse + * @returns The local and domain parts of the email address, in lowercase + */ +export function parseEmail(email: string): { local: string; domain: string } { + const [local, domain] = email.toLowerCase().split("@"); + + if (!domain) { + throw new Error("Invalid email address"); + } + + return { local, domain }; +}