diff --git a/backend/src/db/migrations/20240205200732_invite-only.ts b/backend/src/db/migrations/20240205200732_invite-only.ts new file mode 100644 index 000000000..dd4be591b --- /dev/null +++ b/backend/src/db/migrations/20240205200732_invite-only.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const isTablePresent = await knex.schema.hasTable(TableName.SuperAdmin); + if (isTablePresent) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.boolean("inviteOnlySignUp").defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SuperAdmin, "inviteOnlySignUp")) { + await knex.schema.alterTable(TableName.SuperAdmin, (t) => { + t.dropColumn("inviteOnlySignUp"); + }); + } +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index 13bf45e7b..b43ada8d5 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -11,6 +11,7 @@ export const SuperAdminSchema = z.object({ id: z.string().uuid(), initialized: z.boolean().default(false).nullable().optional(), allowSignUp: z.boolean().default(true).nullable().optional(), + inviteOnlySignUp: z.boolean().default(false).nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/db/seeds/1-user.ts b/backend/src/db/seeds/1-user.ts index ca0042a98..0da9be9b1 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -9,7 +9,7 @@ export async function seed(knex: Knex): Promise { await knex(TableName.Users).del(); await knex(TableName.UserEncryptionKey).del(); await knex(TableName.SuperAdmin).del(); - await knex(TableName.SuperAdmin).insert([{ initialized: true, allowSignUp: true }]); + await knex(TableName.SuperAdmin).insert([{ initialized: true, allowSignUp: true, inviteOnlySignUp: false }]); // Inserts seed entries const [user] = await knex(TableName.Users) .insert([ diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index f81c21ffd..4c0e43cd6 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -105,7 +105,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { email, firstName: profile.firstName as string, lastName: profile.lastName as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp), + isSignupAllowed: Boolean(serverCfg.allowSignUp && serverCfg.inviteOnlySignUp), relayState: (req.body as { RelayState?: string }).RelayState, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index e23f68c3a..afe5c4c95 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -31,7 +31,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { method: "PATCH", schema: { body: z.object({ - allowSignUp: z.boolean().optional() + allowSignUp: z.boolean().optional(), + inviteOnlySignUp: z.boolean().optional() }), response: { 200: z.object({ diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 1144bd414..b15d95e56 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -33,7 +33,7 @@ export const superAdminServiceFactory = ({ const serverCfg = await serverCfgDAL.findOne({}); if (serverCfg) return; - const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true }); + const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, inviteOnlySignUp: false }); return newCfg; }; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index da6b5ce3c..593191d1f 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -1,6 +1,7 @@ export type TServerConfig = { initialized: boolean; allowSignUp: boolean; + inviteOnlySignUp: boolean; isMigrationModeOn?: boolean; }; diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 5eaa48e63..5ede3126c 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import crypto from "crypto"; -import { useState } from "react"; +import { useEffect,useState } from "react"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; @@ -22,6 +22,7 @@ import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; +import { useServerConfig } from "@app/context"; import { completeAccountSignupInvite, verifySignupInvite } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; @@ -56,6 +57,13 @@ export default function SignupInvite() { const token = parsedUrl.token as string; const organizationId = parsedUrl.organization_id as string; const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); + const { config } = useServerConfig(); + + useEffect(() => { + if (!config.allowSignUp) { + router.push("/login"); + } + }, [config.allowSignUp]); // Verifies if the information that the users entered (name, workspace) is there, and if the password matched the criteria. const signupErrorCheck = async () => { diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index 508dcda1e..240a9d5de 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -1,7 +1,16 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { ContentLoader, Switch, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + ContentLoader, + Select, + SelectItem, + Tab, + TabList, + TabPanel, + Tabs +} from "@app/components/v2"; import { useOrganization, useServerConfig, useUser } from "@app/context"; import { useUpdateServerConfig } from "@app/hooks/api"; @@ -9,13 +18,18 @@ enum TabSections { Settings = "settings" } +type SignUpMode = "disabled" | "invite-only" | "anyone"; + export const AdminDashboardPage = () => { const router = useRouter(); const data = useServerConfig(); + const [signUpMode, setSignUpMode] = useState("invite-only"); + const { config } = data; const { user, isLoading: isUserLoading } = useUser(); const { orgs } = useOrganization(); const { mutate: updateServerConfig } = useUpdateServerConfig(); + const { createNotification } = useNotificationContext(); const isNotAllowed = !user?.superAdmin; @@ -28,37 +42,68 @@ export const AdminDashboardPage = () => { } }, [isNotAllowed, isUserLoading]); + useEffect(() => { + if (!config.allowSignUp) { + setSignUpMode("disabled"); + return; + } + if (config.inviteOnlySignUp) { + setSignUpMode("invite-only"); + return; + } + setSignUpMode("anyone"); + }, [config]); + + function handleSignUpModeChange(newSignUpMode: SignUpMode) { + config.allowSignUp = newSignUpMode !== "disabled"; + config.inviteOnlySignUp = newSignUpMode === "invite-only"; + + createNotification({ + text: "Successfully changed sign up mode.", + type: "success" + }); + + updateServerConfig(config); + setSignUpMode(newSignUpMode); + } + return ( -
-
+
+

Admin Dashboard

Manage your Infisical instance.

- {isUserLoading || isNotAllowed ? ( - - ) : ( -
- - -
- General -
-
- -
- updateServerConfig({ allowSignUp: isChecked })} - /> -
Enable signup or invite
-
-
-
-
- )}
+ {isUserLoading || isNotAllowed ? ( + + ) : ( +
+ + +
+ General +
+
+ +
+
Allow user to Sign Up
+ +
+
+
+
+ )}
); };