diff --git a/app/components/AuthLogo/index.tsx b/app/components/AuthLogo/index.tsx
index 669b6591e..c1bf8fda0 100644
--- a/app/components/AuthLogo/index.tsx
+++ b/app/components/AuthLogo/index.tsx
@@ -7,28 +7,29 @@ import SlackLogo from "./SlackLogo";
type Props = {
providerName: string;
size?: number;
+ color?: string;
};
-function AuthLogo({ providerName, size = 16 }: Props) {
+function AuthLogo({ providerName, color, size = 16 }: Props) {
switch (providerName) {
case "slack":
return (
-
+
);
case "google":
return (
-
+
);
case "azure":
return (
-
+
);
diff --git a/app/models/AuthenticationProvider.ts b/app/models/AuthenticationProvider.ts
new file mode 100644
index 000000000..e144eb94e
--- /dev/null
+++ b/app/models/AuthenticationProvider.ts
@@ -0,0 +1,19 @@
+import { observable } from "mobx";
+import BaseModel from "./BaseModel";
+import Field from "./decorators/Field";
+
+class AuthenticationProvider extends BaseModel {
+ id: string;
+
+ displayName: string;
+
+ name: string;
+
+ isConnected: boolean;
+
+ @Field
+ @observable
+ isEnabled: boolean;
+}
+
+export default AuthenticationProvider;
diff --git a/app/scenes/Settings/Security.tsx b/app/scenes/Settings/Security.tsx
index 4b90b77b9..f7525e605 100644
--- a/app/scenes/Settings/Security.tsx
+++ b/app/scenes/Settings/Security.tsx
@@ -1,34 +1,33 @@
import { debounce } from "lodash";
import { observer } from "mobx-react";
-import { CloseIcon, PadlockIcon } from "outline-icons";
+import { CheckboxIcon, EmailIcon, PadlockIcon } from "outline-icons";
import { useState } from "react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
-import styled from "styled-components";
-import Button from "~/components/Button";
+import { useTheme } from "styled-components";
+import AuthLogo from "~/components/AuthLogo";
import ConfirmationDialog from "~/components/ConfirmationDialog";
-import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
-import Input from "~/components/Input";
import InputSelect from "~/components/InputSelect";
-import NudeButton from "~/components/NudeButton";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
-import Tooltip from "~/components/Tooltip";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
+import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import isCloudHosted from "~/utils/isCloudHosted";
+import DomainManagement from "./components/DomainManagement";
import SettingRow from "./components/SettingRow";
function Security() {
- const { auth, dialogs } = useStores();
+ const { auth, authenticationProviders, dialogs } = useStores();
const team = useCurrentTeam();
const { t } = useTranslation();
const { showToast } = useToasts();
+ const theme = useTheme();
const [data, setData] = useState({
sharing: team.sharing,
documentEmbeds: team.documentEmbeds,
@@ -38,16 +37,15 @@ function Security() {
inviteRequired: team.inviteRequired,
});
- const [allowedDomains, setAllowedDomains] = useState([
- ...(team.allowedDomains ?? []),
- ]);
- const [lastKnownDomainCount, updateLastKnownDomainCount] = useState(
- allowedDomains.length
+ const { data: providers, loading, request } = useRequest(() =>
+ authenticationProviders.fetchPage({})
);
- const [existingDomainsTouched, setExistingDomainsTouched] = useState(false);
-
- const authenticationMethods = team.signinMethods;
+ React.useEffect(() => {
+ if (!providers && !loading) {
+ request();
+ }
+ }, [loading, providers, request]);
const showSuccessMessage = React.useMemo(
() =>
@@ -81,21 +79,6 @@ function Security() {
[data, saveData]
);
- const handleSaveDomains = React.useCallback(async () => {
- try {
- await auth.updateTeam({
- allowedDomains,
- });
- showSuccessMessage();
- setExistingDomainsTouched(false);
- updateLastKnownDomainCount(allowedDomains.length);
- } catch (err) {
- showToast(err.message, {
- type: "error",
- });
- }
- }, [auth, allowedDomains, showSuccessMessage, showToast]);
-
const handleDefaultRoleChange = React.useCallback(
async (newDefaultRole: string) => {
await saveData({ ...data, defaultUserRole: newDefaultRole });
@@ -124,7 +107,7 @@ function Security() {
,
@@ -138,45 +121,9 @@ function Security() {
await saveData(newData);
},
- [data, saveData, t, dialogs, authenticationMethods]
+ [data, saveData, t, dialogs, team.signinMethods]
);
- const handleRemoveDomain = async (index: number) => {
- const newDomains = allowedDomains.filter((_, i) => index !== i);
-
- setAllowedDomains(newDomains);
-
- const touchedExistingDomain = index < lastKnownDomainCount;
- if (touchedExistingDomain) {
- setExistingDomainsTouched(true);
- }
- };
-
- const handleAddDomain = () => {
- const newDomains = [...allowedDomains, ""];
-
- setAllowedDomains(newDomains);
- };
-
- const createOnDomainChangedHandler = (index: number) => (
- ev: React.ChangeEvent
- ) => {
- const newDomains = allowedDomains.slice();
-
- newDomains[index] = ev.currentTarget.value;
- setAllowedDomains(newDomains);
-
- const touchedExistingDomain = index < lastKnownDomainCount;
- if (touchedExistingDomain) {
- setExistingDomainsTouched(true);
- }
- };
-
- const showSaveChanges =
- existingDomainsTouched ||
- allowedDomains.filter((value: string) => value !== "").length > // New domains were added
- lastKnownDomainCount;
-
return (
}>
{t("Security")}
@@ -187,14 +134,43 @@ function Security() {
+ {t("Sign In")}
+ {authenticationProviders.orderedData
+ // filtering unconnected, until we have ability to connect from this screen
+ .filter((provider) => provider.isConnected)
+ .map((provider) => (
+
+ {" "}
+ {provider.displayName}
+
+ }
+ name={provider.name}
+ description={t("Allow members to sign-in with {{ authProvider }}", {
+ authProvider: provider.displayName,
+ })}
+ >
+
+ {" "}
+ {t("Connected")}
+
+
+ ))}
+ {t("Email")}
+
+ }
name="guestSignin"
description={
env.EMAIL_ENABLED
- ? t("When enabled, users can sign-in using their email address")
+ ? t("Allow members to sign-in using their email address")
: t("The server must have SMTP configured to enable this setting")
}
+ border={false}
>
+
+ {t("Access")}
+ {isCloudHosted && (
+
+
+
+ )}
+
+ {!data.inviteRequired && (
+
+ )}
+ {!data.inviteRequired && (
+
+
+
+ )}
+
+ {t("Behavior")}
- {isCloudHosted && (
-
-
-
- )}
-
- {!data.inviteRequired && (
-
-
-
- )}
-
- {!data.inviteRequired && (
-
- {allowedDomains.map((domain, index) => (
-
-
-
-
- handleRemoveDomain(index)}>
-
-
-
-
-
- ))}
-
-
- {!allowedDomains.length ||
- allowedDomains[allowedDomains.length - 1] !== "" ? (
-
-
-
- ) : (
-
- )}
-
- {showSaveChanges && (
-
-
-
- )}
-
-
- )}
);
}
-const Remove = styled("div")`
- margin-top: 6px;
-`;
-
export default observer(Security);
diff --git a/app/scenes/Settings/components/DomainManagement.tsx b/app/scenes/Settings/components/DomainManagement.tsx
new file mode 100644
index 000000000..158c6cda6
--- /dev/null
+++ b/app/scenes/Settings/components/DomainManagement.tsx
@@ -0,0 +1,155 @@
+import { observer } from "mobx-react";
+import { CloseIcon } from "outline-icons";
+import * as React from "react";
+import { Trans, useTranslation } from "react-i18next";
+import styled from "styled-components";
+import Button from "~/components/Button";
+import Fade from "~/components/Fade";
+import Flex from "~/components/Flex";
+import Input from "~/components/Input";
+import NudeButton from "~/components/NudeButton";
+import Tooltip from "~/components/Tooltip";
+import useCurrentTeam from "~/hooks/useCurrentTeam";
+import useStores from "~/hooks/useStores";
+import useToasts from "~/hooks/useToasts";
+import SettingRow from "./SettingRow";
+
+type Props = {
+ onSuccess: () => void;
+};
+
+function DomainManagement({ onSuccess }: Props) {
+ const { auth } = useStores();
+ const team = useCurrentTeam();
+ const { t } = useTranslation();
+ const { showToast } = useToasts();
+
+ const [allowedDomains, setAllowedDomains] = React.useState([
+ ...(team.allowedDomains ?? []),
+ ]);
+ const [lastKnownDomainCount, updateLastKnownDomainCount] = React.useState(
+ allowedDomains.length
+ );
+
+ const [existingDomainsTouched, setExistingDomainsTouched] = React.useState(
+ false
+ );
+
+ const handleSaveDomains = React.useCallback(async () => {
+ try {
+ await auth.updateTeam({
+ allowedDomains,
+ });
+ onSuccess();
+ setExistingDomainsTouched(false);
+ updateLastKnownDomainCount(allowedDomains.length);
+ } catch (err) {
+ showToast(err.message, {
+ type: "error",
+ });
+ }
+ }, [auth, allowedDomains, onSuccess, showToast]);
+
+ const handleRemoveDomain = async (index: number) => {
+ const newDomains = allowedDomains.filter((_, i) => index !== i);
+
+ setAllowedDomains(newDomains);
+
+ const touchedExistingDomain = index < lastKnownDomainCount;
+ if (touchedExistingDomain) {
+ setExistingDomainsTouched(true);
+ }
+ };
+
+ const handleAddDomain = () => {
+ const newDomains = [...allowedDomains, ""];
+
+ setAllowedDomains(newDomains);
+ };
+
+ const createOnDomainChangedHandler = (index: number) => (
+ ev: React.ChangeEvent
+ ) => {
+ const newDomains = allowedDomains.slice();
+
+ newDomains[index] = ev.currentTarget.value;
+ setAllowedDomains(newDomains);
+
+ const touchedExistingDomain = index < lastKnownDomainCount;
+ if (touchedExistingDomain) {
+ setExistingDomainsTouched(true);
+ }
+ };
+
+ const showSaveChanges =
+ existingDomainsTouched ||
+ allowedDomains.filter((value: string) => value !== "").length > // New domains were added
+ lastKnownDomainCount;
+
+ return (
+
+ {allowedDomains.map((domain, index) => (
+
+
+
+
+ handleRemoveDomain(index)}>
+
+
+
+
+
+ ))}
+
+
+ {!allowedDomains.length ||
+ allowedDomains[allowedDomains.length - 1] !== "" ? (
+
+
+
+ ) : (
+
+ )}
+
+ {showSaveChanges && (
+
+
+
+ )}
+
+
+ );
+}
+
+const Remove = styled("div")`
+ margin-top: 6px;
+`;
+
+export default observer(DomainManagement);
diff --git a/app/scenes/Settings/components/SettingRow.tsx b/app/scenes/Settings/components/SettingRow.tsx
index e0e7ccc57..88f01512b 100644
--- a/app/scenes/Settings/components/SettingRow.tsx
+++ b/app/scenes/Settings/components/SettingRow.tsx
@@ -7,7 +7,7 @@ import Text from "~/components/Text";
type Props = {
label: React.ReactNode;
- description: React.ReactNode;
+ description?: React.ReactNode;
name: string;
visible?: boolean;
border?: boolean;
@@ -15,7 +15,7 @@ type Props = {
const Row = styled(Flex)<{ $border?: boolean }>`
display: block;
- padding: 24px 0;
+ padding: 22px 0;
border-bottom: 1px solid
${(props) =>
props.$border === false
@@ -38,7 +38,7 @@ const Column = styled.div`
flex: 1;
&:first-child {
- min-width: 60%;
+ min-width: 70%;
}
&:last-child {
@@ -73,7 +73,7 @@ const SettingRow: React.FC = ({
- {description}
+ {description && {description}}
{children}
diff --git a/app/stores/AuthenticationProvidersStore.ts b/app/stores/AuthenticationProvidersStore.ts
new file mode 100644
index 000000000..4e1ffb71e
--- /dev/null
+++ b/app/stores/AuthenticationProvidersStore.ts
@@ -0,0 +1,13 @@
+import AuthenticationProvider from "~/models/AuthenticationProvider";
+import BaseStore, { RPCAction } from "./BaseStore";
+import RootStore from "./RootStore";
+
+export default class AuthenticationProvidersStore extends BaseStore<
+ AuthenticationProvider
+> {
+ actions = [RPCAction.List, RPCAction.Update];
+
+ constructor(rootStore: RootStore) {
+ super(rootStore, AuthenticationProvider);
+ }
+}
diff --git a/app/stores/RootStore.ts b/app/stores/RootStore.ts
index 1bc19f254..e17922fda 100644
--- a/app/stores/RootStore.ts
+++ b/app/stores/RootStore.ts
@@ -1,5 +1,6 @@
import ApiKeysStore from "./ApiKeysStore";
import AuthStore from "./AuthStore";
+import AuthenticationProvidersStore from "./AuthenticationProvidersStore";
import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore";
import CollectionsStore from "./CollectionsStore";
import DialogsStore from "./DialogsStore";
@@ -28,6 +29,7 @@ import WebhookSubscriptionsStore from "./WebhookSubscriptionStore";
export default class RootStore {
apiKeys: ApiKeysStore;
auth: AuthStore;
+ authenticationProviders: AuthenticationProvidersStore;
collections: CollectionsStore;
collectionGroupMemberships: CollectionGroupMembershipsStore;
dialogs: DialogsStore;
@@ -57,6 +59,7 @@ export default class RootStore {
// PoliciesStore must be initialized before AuthStore
this.policies = new PoliciesStore(this);
this.apiKeys = new ApiKeysStore(this);
+ this.authenticationProviders = new AuthenticationProvidersStore(this);
this.auth = new AuthStore(this);
this.collections = new CollectionsStore(this);
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
@@ -85,6 +88,7 @@ export default class RootStore {
logout() {
this.apiKeys.clear();
+ this.authenticationProviders.clear();
// this.auth omitted for reasons...
this.collections.clear();
this.collectionGroupMemberships.clear();
diff --git a/server/models/AuthenticationProvider.ts b/server/models/AuthenticationProvider.ts
index 0d4c41740..89e144354 100644
--- a/server/models/AuthenticationProvider.ts
+++ b/server/models/AuthenticationProvider.ts
@@ -1,4 +1,4 @@
-import { Op } from "sequelize";
+import { Op, SaveOptions } from "sequelize";
import {
BelongsTo,
Column,
@@ -97,9 +97,10 @@ class AuthenticationProvider extends Model {
}
}
- disable = async () => {
+ disable = async (options?: SaveOptions) => {
const res = await (this
.constructor as typeof AuthenticationProvider).findAndCountAll({
+ ...options,
where: {
teamId: this.teamId,
enabled: true,
@@ -111,18 +112,24 @@ class AuthenticationProvider extends Model {
});
if (res.count >= 1) {
- return this.update({
- enabled: false,
- });
+ return this.update(
+ {
+ enabled: false,
+ },
+ options
+ );
} else {
throw ValidationError("At least one authentication provider is required");
}
};
- enable = () => {
- return this.update({
- enabled: true,
- });
+ enable = (options?: SaveOptions) => {
+ return this.update(
+ {
+ enabled: true,
+ },
+ options
+ );
};
}
diff --git a/server/routes/api/authenticationProviders.test.ts b/server/routes/api/authenticationProviders.test.ts
index 2b0d972a8..e4a421d0e 100644
--- a/server/routes/api/authenticationProviders.test.ts
+++ b/server/routes/api/authenticationProviders.test.ts
@@ -7,7 +7,7 @@ const server = getTestServer();
describe("#authenticationProviders.info", () => {
it("should return auth provider", async () => {
const team = await buildTeam();
- const user = await buildUser({
+ const user = await buildAdmin({
teamId: team.id,
});
const authenticationProviders = await team.$get("authenticationProviders");
@@ -23,7 +23,7 @@ describe("#authenticationProviders.info", () => {
expect(body.data.isEnabled).toBe(true);
expect(body.data.isConnected).toBe(true);
expect(body.policies[0].abilities.read).toBe(true);
- expect(body.policies[0].abilities.update).toBe(false);
+ expect(body.policies[0].abilities.update).toBe(true);
});
it("should require authorization", async () => {
@@ -123,7 +123,7 @@ describe("#authenticationProviders.update", () => {
describe("#authenticationProviders.list", () => {
it("should return enabled and available auth providers", async () => {
const team = await buildTeam();
- const user = await buildUser({
+ const user = await buildAdmin({
teamId: team.id,
});
const res = await server.post("/api/authenticationProviders.list", {
@@ -133,13 +133,13 @@ describe("#authenticationProviders.list", () => {
});
const body = await res.json();
expect(res.status).toEqual(200);
- expect(body.data.authenticationProviders.length).toBe(2);
- expect(body.data.authenticationProviders[0].name).toBe("slack");
- expect(body.data.authenticationProviders[0].isEnabled).toBe(true);
- expect(body.data.authenticationProviders[0].isConnected).toBe(true);
- expect(body.data.authenticationProviders[1].name).toBe("google");
- expect(body.data.authenticationProviders[1].isEnabled).toBe(false);
- expect(body.data.authenticationProviders[1].isConnected).toBe(false);
+ expect(body.data.length).toBe(2);
+ expect(body.data[0].name).toBe("slack");
+ expect(body.data[0].isEnabled).toBe(true);
+ expect(body.data[0].isConnected).toBe(true);
+ expect(body.data[1].name).toBe("google");
+ expect(body.data[1].isEnabled).toBe(false);
+ expect(body.data[1].isConnected).toBe(false);
});
it("should require authentication", async () => {
diff --git a/server/routes/api/authenticationProviders.ts b/server/routes/api/authenticationProviders.ts
index 4715ace1b..ae3548e96 100644
--- a/server/routes/api/authenticationProviders.ts
+++ b/server/routes/api/authenticationProviders.ts
@@ -1,4 +1,5 @@
import Router from "koa-router";
+import { sequelize } from "@server/database/sequelize";
import auth from "@server/middlewares/authentication";
import { AuthenticationProvider, Event } from "@server/models";
import { authorize } from "@server/policies";
@@ -11,81 +12,108 @@ import allAuthenticationProviders from "../auth/providers";
const router = new Router();
-router.post("authenticationProviders.info", auth(), async (ctx) => {
- const { id } = ctx.request.body;
- assertUuid(id, "id is required");
- const { user } = ctx.state;
- const authenticationProvider = await AuthenticationProvider.findByPk(id);
- authorize(user, "read", authenticationProvider);
+router.post(
+ "authenticationProviders.info",
+ auth({ admin: true }),
+ async (ctx) => {
+ const { id } = ctx.request.body;
+ assertUuid(id, "id is required");
- ctx.body = {
- data: presentAuthenticationProvider(authenticationProvider),
- policies: presentPolicies(user, [authenticationProvider]),
- };
-});
+ const { user } = ctx.state;
+ const authenticationProvider = await AuthenticationProvider.findByPk(id);
+ authorize(user, "read", authenticationProvider);
-router.post("authenticationProviders.update", auth(), async (ctx) => {
- const { id, isEnabled } = ctx.request.body;
- assertUuid(id, "id is required");
- assertPresent(isEnabled, "isEnabled is required");
- const { user } = ctx.state;
- const authenticationProvider = await AuthenticationProvider.findByPk(id);
-
- authorize(user, "update", authenticationProvider);
- const enabled = !!isEnabled;
-
- if (enabled) {
- await authenticationProvider.enable();
- } else {
- await authenticationProvider.disable();
+ ctx.body = {
+ data: presentAuthenticationProvider(authenticationProvider),
+ policies: presentPolicies(user, [authenticationProvider]),
+ };
}
+);
- await Event.create({
- name: "authenticationProviders.update",
- data: {
- enabled,
- },
- modelId: id,
- teamId: user.teamId,
- actorId: user.id,
- ip: ctx.request.ip,
- });
+router.post(
+ "authenticationProviders.update",
+ auth({ admin: true }),
+ async (ctx) => {
+ const { id, isEnabled } = ctx.request.body;
+ assertUuid(id, "id is required");
+ assertPresent(isEnabled, "isEnabled is required");
+ const { user } = ctx.state;
- ctx.body = {
- data: presentAuthenticationProvider(authenticationProvider),
- policies: presentPolicies(user, [authenticationProvider]),
- };
-});
+ const authenticationProvider = await sequelize.transaction(
+ async (transaction) => {
+ const authenticationProvider = await AuthenticationProvider.findByPk(
+ id,
+ {
+ transaction,
+ lock: transaction.LOCK.UPDATE,
+ }
+ );
-router.post("authenticationProviders.list", auth(), async (ctx) => {
- const { user } = ctx.state;
- authorize(user, "read", user.team);
+ authorize(user, "update", authenticationProvider);
+ const enabled = !!isEnabled;
- const teamAuthenticationProviders = await user.team.$get(
- "authenticationProviders"
- );
+ if (enabled) {
+ await authenticationProvider.enable({ transaction });
+ } else {
+ await authenticationProvider.disable({ transaction });
+ }
- const otherAuthenticationProviders = allAuthenticationProviders.filter(
- (p) =>
- // @ts-expect-error ts-migrate(7006) FIXME: Parameter 't' implicitly has an 'any' type.
- !teamAuthenticationProviders.find((t) => t.name === p.id) &&
- p.enabled && // email auth is dealt with separetly right now, although it definitely
- // wants to be here in the future – we'll need to migrate more data though
- p.id !== "email"
- );
+ await Event.create(
+ {
+ name: "authenticationProviders.update",
+ data: {
+ enabled,
+ },
+ modelId: id,
+ teamId: user.teamId,
+ actorId: user.id,
+ ip: ctx.request.ip,
+ },
+ { transaction }
+ );
- ctx.body = {
- data: {
- authenticationProviders: [
- ...teamAuthenticationProviders.map(presentAuthenticationProvider),
- ...otherAuthenticationProviders.map((p) => ({
+ return authenticationProvider;
+ }
+ );
+
+ ctx.body = {
+ data: presentAuthenticationProvider(authenticationProvider),
+ policies: presentPolicies(user, [authenticationProvider]),
+ };
+ }
+);
+
+router.post(
+ "authenticationProviders.list",
+ auth({ admin: true }),
+ async (ctx) => {
+ const { user } = ctx.state;
+ authorize(user, "read", user.team);
+
+ const teamAuthenticationProviders = (await user.team.$get(
+ "authenticationProviders"
+ )) as AuthenticationProvider[];
+
+ const data = allAuthenticationProviders
+ .filter((p) => p.id !== "email")
+ .map((p) => {
+ const row = teamAuthenticationProviders.find((t) => t.name === p.id);
+
+ return {
+ id: p.id,
name: p.id,
+ displayName: p.name,
isEnabled: false,
isConnected: false,
- })),
- ],
- },
- };
-});
+ ...(row ? presentAuthenticationProvider(row) : {}),
+ };
+ })
+ .sort((a) => (a.isEnabled ? -1 : 1));
+
+ ctx.body = {
+ data,
+ };
+ }
+);
export default router;
diff --git a/server/routes/auth/providers/index.ts b/server/routes/auth/providers/index.ts
index 67c9a604a..a013dc02d 100644
--- a/server/routes/auth/providers/index.ts
+++ b/server/routes/auth/providers/index.ts
@@ -3,13 +3,13 @@ import { sortBy } from "lodash";
import { signin } from "@shared/utils/urlHelpers";
import { requireDirectory } from "@server/utils/fs";
-interface AuthenticationProviderConfig {
+export type AuthenticationProviderConfig = {
id: string;
name: string;
enabled: boolean;
authUrl: string;
router: Router;
-}
+};
const providers: AuthenticationProviderConfig[] = [];
diff --git a/server/test/env.ts b/server/test/env.ts
index 61bfa0be3..be6053e06 100644
--- a/server/test/env.ts
+++ b/server/test/env.ts
@@ -7,6 +7,12 @@ env.GOOGLE_CLIENT_ID = "123";
env.GOOGLE_CLIENT_SECRET = "123";
env.SLACK_CLIENT_ID = "123";
env.SLACK_CLIENT_SECRET = "123";
+
+env.AZURE_CLIENT_ID = undefined;
+env.AZURE_CLIENT_SECRET = undefined;
+env.OIDC_CLIENT_ID = undefined;
+env.OIDC_CLIENT_SECRET = undefined;
+
env.RATE_LIMITER_ENABLED = false;
env.DEPLOYMENT = undefined;
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index ccdb14005..88288a226 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -578,6 +578,11 @@
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.",
"No documents found for your search filters.": "No documents found for your search filters.",
"Search Results": "Search Results",
+ "Allowed domains": "Allowed domains",
+ "The domains which should be allowed to create new accounts using SSO. Changing this setting does not affect existing user accounts.": "The domains which should be allowed to create new accounts using SSO. Changing this setting does not affect existing user accounts.",
+ "Remove domain": "Remove domain",
+ "Add a domain": "Add a domain",
+ "Save changes": "Save changes",
"Please choose a single file to import": "Please choose a single file to import",
"Your import is being processed, you can safely leave this page": "Your import is being processed, you can safely leave this page",
"File not supported – please upload a valid ZIP file": "File not supported – please upload a valid ZIP file",
@@ -711,23 +716,21 @@
"I’m sure": "I’m sure",
"New users will first need to be invited to create an account. Default role and Allowed domains will no longer apply.": "New users will first need to be invited to create an account. Default role and Allowed domains will no longer apply.",
"Settings that impact the access, security, and content of your knowledge base.": "Settings that impact the access, security, and content of your knowledge base.",
- "Allow email authentication": "Allow email authentication",
- "When enabled, users can sign-in using their email address": "When enabled, users can sign-in using their email address",
+ "Allow members to sign-in with {{ authProvider }}": "Allow members to sign-in with {{ authProvider }}",
+ "Connected": "Connected",
+ "Allow members to sign-in using their email address": "Allow members to sign-in using their email address",
"The server must have SMTP configured to enable this setting": "The server must have SMTP configured to enable this setting",
+ "Access": "Access",
+ "Require invites": "Require invites",
+ "Require members to be invited to the workspace before they can create an account using SSO.": "Require members to be invited to the workspace before they can create an account using SSO.",
+ "Default role": "Default role",
+ "The default user role for new accounts. Changing this setting does not affect existing user accounts.": "The default user role for new accounts. Changing this setting does not affect existing user accounts.",
+ "Behavior": "Behavior",
"When enabled, documents can be shared publicly on the internet by any member of the workspace": "When enabled, documents can be shared publicly on the internet by any member of the workspace",
"Rich service embeds": "Rich service embeds",
"Links to supported services are shown as rich embeds within your documents": "Links to supported services are shown as rich embeds within your documents",
"Collection creation": "Collection creation",
"Allow members to create new collections within the knowledge base": "Allow members to create new collections within the knowledge base",
- "Require invites": "Require invites",
- "Require members to be invited to the team before they can create an account using SSO.": "Require members to be invited to the team before they can create an account using SSO.",
- "Default role": "Default role",
- "The default user role for new accounts. Changing this setting does not affect existing user accounts.": "The default user role for new accounts. Changing this setting does not affect existing user accounts.",
- "Allowed domains": "Allowed domains",
- "The domains which should be allowed to create new accounts using SSO. Changing this setting does not affect existing user accounts.": "The domains which should be allowed to create new accounts using SSO. Changing this setting does not affect existing user accounts.",
- "Remove domain": "Remove domain",
- "Add a domain": "Add a domain",
- "Save changes": "Save changes",
"Add your self-hosted draw.io installation url here to enable automatic embedding of diagrams within documents.": "Add your self-hosted draw.io installation url here to enable automatic embedding of diagrams within documents.",
"Draw.io deployment": "Draw.io deployment",
"Sharing is currently disabled.": "Sharing is currently disabled.",