centralize email parsing logic

This commit is contained in:
Tom Moor
2024-07-07 10:54:19 -04:00
parent c484d1defe
commit bdcde1aa53
8 changed files with 52 additions and 9 deletions

View File

@@ -7,6 +7,7 @@ import { Link } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import styled from "styled-components"; import styled from "styled-components";
import { UserRole } from "@shared/types"; import { UserRole } from "@shared/types";
import { parseEmail } from "@shared/utils/email";
import { UserValidation } from "@shared/validations"; import { UserValidation } from "@shared/validations";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
@@ -41,7 +42,7 @@ function Invite({ onSubmit }: Props) {
const user = useCurrentUser(); const user = useCurrentUser();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { t } = useTranslation(); const { t } = useTranslation();
const predictedDomain = user.email.split("@")[1]; const predictedDomain = parseEmail(user.email).domain;
const can = usePolicy(team); const can = usePolicy(team);
const [role, setRole] = React.useState<UserRole>(UserRole.Member); const [role, setRole] = React.useState<UserRole>(UserRole.Member);

View File

@@ -5,6 +5,7 @@ import type { Context } from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { Profile } from "passport"; import { Profile } from "passport";
import { slugifyDomain } from "@shared/utils/domains"; import { slugifyDomain } from "@shared/utils/domains";
import { parseEmail } from "@shared/utils/email";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import { MicrosoftGraphError } from "@server/errors"; import { MicrosoftGraphError } from "@server/errors";
import passportMiddleware from "@server/middlewares/passport"; 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 team = await getTeamFromContext(ctx);
const client = getClientFromContext(ctx); const client = getClientFromContext(ctx);
const domain = email.split("@")[1]; const domain = parseEmail(email).domain;
const subdomain = slugifyDomain(domain); const subdomain = slugifyDomain(domain);
const teamName = organization.displayName; const teamName = organization.displayName;

View File

@@ -10,6 +10,7 @@ import Router from "koa-router";
import { Strategy } from "passport-oauth2"; import { Strategy } from "passport-oauth2";
import { languages } from "@shared/i18n"; import { languages } from "@shared/i18n";
import { slugifyDomain } from "@shared/utils/domains"; import { slugifyDomain } from "@shared/utils/domains";
import { parseEmail } from "@shared/utils/email";
import slugify from "@shared/utils/slugify"; import slugify from "@shared/utils/slugify";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import { InvalidRequestError, TeamDomainRequiredError } from "@server/errors"; 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 */ /** We have the email scope, so this should never happen */
throw InvalidRequestError("Discord profile email is missing"); throw InvalidRequestError("Discord profile email is missing");
} }
const parts = email.toLowerCase().split("@"); const { domain } = parseEmail(email);
const domain = parts.length && parts[1];
if (!domain) { if (!domain) {
throw TeamDomainRequiredError(); throw TeamDomainRequiredError();

View File

@@ -4,6 +4,7 @@ import Router from "koa-router";
import get from "lodash/get"; import get from "lodash/get";
import { Strategy } from "passport-oauth2"; import { Strategy } from "passport-oauth2";
import { slugifyDomain } from "@shared/utils/domains"; import { slugifyDomain } from "@shared/utils/domains";
import { parseEmail } from "@shared/utils/email";
import accountProvisioner from "@server/commands/accountProvisioner"; import accountProvisioner from "@server/commands/accountProvisioner";
import { import {
OIDCMalformedUserInfoError, OIDCMalformedUserInfoError,
@@ -92,9 +93,7 @@ if (
} }
const team = await getTeamFromContext(ctx); const team = await getTeamFromContext(ctx);
const client = getClientFromContext(ctx); const client = getClientFromContext(ctx);
const { domain } = parseEmail(profile.email);
const parts = profile.email.toLowerCase().split("@");
const domain = parts.length && parts[1];
if (!domain) { if (!domain) {
throw OIDCMalformedUserInfoError(); throw OIDCMalformedUserInfoError();

View File

@@ -1,5 +1,6 @@
import { InferCreationAttributes } from "sequelize"; import { InferCreationAttributes } from "sequelize";
import { UserRole } from "@shared/types"; import { UserRole } from "@shared/types";
import { parseEmail } from "@shared/utils/email";
import InviteAcceptedEmail from "@server/emails/templates/InviteAcceptedEmail"; import InviteAcceptedEmail from "@server/emails/templates/InviteAcceptedEmail";
import { import {
DomainNotAllowedError, DomainNotAllowedError,
@@ -226,7 +227,7 @@ export default async function userProvisioner({
// If the team settings do not allow this domain, // If the team settings do not allow this domain,
// throw an error and fail user creation. // throw an error and fail user creation.
const domain = email.split("@")[1]; const { domain } = parseEmail(email);
if (team && !(await team.isDomainAllowed(domain))) { if (team && !(await team.isDomainAllowed(domain))) {
throw DomainNotAllowedError(); throw DomainNotAllowedError();
} }

View File

@@ -1,5 +1,6 @@
import "./bootstrap"; import "./bootstrap";
import { UserRole } from "@shared/types"; import { UserRole } from "@shared/types";
import { parseEmail } from "@shared/utils/email";
import teamCreator from "@server/commands/teamCreator"; import teamCreator from "@server/commands/teamCreator";
import env from "@server/env"; import env from "@server/env";
import { Team, User } from "@server/models"; import { Team, User } from "@server/models";
@@ -10,6 +11,7 @@ const email = process.argv[2];
export default async function main(exit = false) { export default async function main(exit = false) {
const teamCount = await Team.count(); const teamCount = await Team.count();
if (teamCount === 0) { if (teamCount === 0) {
const name = parseEmail(email).local;
const user = await sequelize.transaction(async (transaction) => { const user = await sequelize.transaction(async (transaction) => {
const team = await teamCreator({ const team = await teamCreator({
name: "Wiki", name: "Wiki",
@@ -22,7 +24,7 @@ export default async function main(exit = false) {
return await User.create( return await User.create(
{ {
teamId: team.id, teamId: team.id,
name: email.split("@")[0], name,
email, email,
role: UserRole.Admin, role: UserRole.Admin,
}, },

View File

@@ -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();
});
});

15
shared/utils/email.ts Normal file
View File

@@ -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 };
}