From 5cf1ec24003438d575389817685fedf6bb7d5e7c Mon Sep 17 00:00:00 2001 From: Salman Date: Sat, 10 Feb 2024 10:44:39 +0530 Subject: [PATCH] Add restrict signup based on domain --- .../migrations/20240205200732_invite-only.ts | 7 ++ backend/src/db/schemas/super-admin.ts | 1 + backend/src/server/routes/v1/admin-router.ts | 3 +- backend/src/server/routes/v1/sso-router.ts | 8 +- backend/src/server/routes/v3/signup-router.ts | 12 ++- .../src/services/auth/auth-login-service.ts | 25 +++--- backend/src/services/auth/auth-login-type.ts | 3 + frontend/src/hooks/api/admin/types.ts | 1 + .../admin/DashboardPage/DashboardPage.tsx | 88 ++++++++++++++----- 9 files changed, 109 insertions(+), 39 deletions(-) diff --git a/backend/src/db/migrations/20240205200732_invite-only.ts b/backend/src/db/migrations/20240205200732_invite-only.ts index dd4be591b..d242623c8 100644 --- a/backend/src/db/migrations/20240205200732_invite-only.ts +++ b/backend/src/db/migrations/20240205200732_invite-only.ts @@ -7,6 +7,7 @@ export async function up(knex: Knex): Promise { if (isTablePresent) { await knex.schema.alterTable(TableName.SuperAdmin, (t) => { t.boolean("inviteOnlySignUp").defaultTo(false); + t.string("allowSpecificDomainSignUp"); }); } } @@ -17,4 +18,10 @@ export async function down(knex: Knex): Promise { t.dropColumn("inviteOnlySignUp"); }); } + + if (await knex.schema.hasColumn(TableName.SuperAdmin, "allowSpecificDomainSignUp")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("allowSpecificDomainSignUp"); + }); + } } diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index b43ada8d5..fb373e19b 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -12,6 +12,7 @@ export const SuperAdminSchema = z.object({ initialized: z.boolean().default(false).nullable().optional(), allowSignUp: z.boolean().default(true).nullable().optional(), inviteOnlySignUp: z.boolean().default(false).nullable().optional(), + allowSpecificDomainSignUp: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index afe5c4c95..ddd95522f 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -32,7 +32,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { schema: { body: z.object({ allowSignUp: z.boolean().optional(), - inviteOnlySignUp: z.boolean().optional() + inviteOnlySignUp: z.boolean().optional(), + allowSpecificDomainSignUp: z.string().optional() }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index bfcf2f6ae..da0b12ca5 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -55,7 +55,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { lastName: profile?.name?.familyName || "", authMethod: AuthMethod.GOOGLE, callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp) + serverCfg }); cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -91,7 +91,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { lastName: "", authMethod: AuthMethod.GITHUB, callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp) + serverCfg + // isSignupAllowed: Boolean(serverCfg.allowSignUp) }); return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -127,7 +128,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { lastName: "", authMethod: AuthMethod.GITLAB, callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp) + serverCfg + // isSignupAllowed: Boolean(serverCfg.allowSignUp) }); return cb(null, { isUserCompleted, providerAuthToken }); diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 2a2f50f43..11d79d2b5 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -23,8 +23,16 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - await server.services.signup.beginEmailSignupProcess(req.body.email); - return { message: `Sent an email verification code to ${req.body.email}` }; + const { email } = req.body; + const config = await server.services.superAdmin.initServerCfg(); + + if (config?.allowSpecificDomainSignUp) { + const domain = email.split("@")[1]; + + if (domain !== config.allowSpecificDomainSignUp) throw new Error(`Unsupported email domain (${domain}).`); + } + await server.services.signup.beginEmailSignupProcess(email); + return { message: `Sent an email verification code to ${email}` }; } }); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 6e4d60bba..ccfe619f4 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -261,20 +261,25 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: /* * OAuth2 login for google,github, and other oauth2 provider * */ - const oauth2Login = async ({ - email, - firstName, - lastName, - authMethod, - callbackPort, - isSignupAllowed - }: TOauthLoginDTO) => { + const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, serverCfg }: TOauthLoginDTO) => { let user = await userDAL.findUserByEmail(email); const appCfg = getConfig(); - const isOauthSignUpDisabled = !isSignupAllowed && !user; - if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" }); if (!user) { + // Create a new user based on oAuth + if (!serverCfg?.allowSignUp) + throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" }); + + if (serverCfg?.allowSpecificDomainSignUp) { + const domain = email.split("@")[1]; + + if (domain !== serverCfg.allowSpecificDomainSignUp) + throw new BadRequestError({ + message: `User email domain (${domain}) is not supported`, + name: "Oauth 2 login" + }); + } + user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] }); } const isLinkingRequired = !user?.authMethods?.includes(authMethod); diff --git a/backend/src/services/auth/auth-login-type.ts b/backend/src/services/auth/auth-login-type.ts index 67f640bc9..54b27425b 100644 --- a/backend/src/services/auth/auth-login-type.ts +++ b/backend/src/services/auth/auth-login-type.ts @@ -1,3 +1,5 @@ +import { TSuperAdmin } from "@app/db/schemas/super-admin"; + import { AuthMethod } from "./auth-type"; export type TLoginGenServerPublicKeyDTO = { @@ -29,4 +31,5 @@ export type TOauthLoginDTO = { authMethod: AuthMethod; callbackPort?: string; isSignupAllowed?: boolean; + serverCfg?: TSuperAdmin; }; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 593191d1f..145f22072 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -2,6 +2,7 @@ export type TServerConfig = { initialized: boolean; allowSignUp: boolean; inviteOnlySignUp: boolean; + allowSpecificDomainSignUp?: string; isMigrationModeOn?: boolean; }; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 240a9d5de..b0d07b7dc 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -1,9 +1,14 @@ -import { useEffect, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import { useRouter } from "next/router"; +import { faAt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { + Button, ContentLoader, + FormControl, + Input, Select, SelectItem, Tab, @@ -24,11 +29,13 @@ export const AdminDashboardPage = () => { const router = useRouter(); const data = useServerConfig(); const [signUpMode, setSignUpMode] = useState("invite-only"); + const [allowSpecificDomain, setAllowSpecificDomain] = useState(); const { config } = data; const { user, isLoading: isUserLoading } = useUser(); const { orgs } = useOrganization(); const { mutate: updateServerConfig } = useUpdateServerConfig(); + const { createNotification } = useNotificationContext(); const isNotAllowed = !user?.superAdmin; @@ -49,22 +56,28 @@ export const AdminDashboardPage = () => { } if (config.inviteOnlySignUp) { setSignUpMode("invite-only"); - return; + } else { + setSignUpMode("anyone"); + } + + if (config.allowSpecificDomainSignUp) { + setAllowSpecificDomain(config.allowSpecificDomainSignUp); } - setSignUpMode("anyone"); }, [config]); - function handleSignUpModeChange(newSignUpMode: SignUpMode) { - config.allowSignUp = newSignUpMode !== "disabled"; - config.inviteOnlySignUp = newSignUpMode === "invite-only"; + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + + config.allowSignUp = signUpMode !== "disabled"; + config.inviteOnlySignUp = signUpMode === "invite-only"; + config.allowSpecificDomainSignUp = signUpMode === "anyone" ? allowSpecificDomain : ""; + + await updateServerConfig(config); createNotification({ text: "Successfully changed sign up mode.", type: "success" }); - - updateServerConfig(config); - setSignUpMode(newSignUpMode); } return ( @@ -86,20 +99,49 @@ export const AdminDashboardPage = () => { -
-
Allow user to Sign Up
- -
+
+
+
+ Allow user to Sign Up +
+ +
+ + {signUpMode === "anyone" && ( +
+
+ Allow email with only specific domain +
+ +
+ } + value={allowSpecificDomain} + onChange={(ev) => setAllowSpecificDomain(ev.target.value)} + /> +
+
+
+ )} + + +