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 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>(UserRole.Member);

View File

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

View File

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

View File

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

View File

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

View File

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

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