feat: allow admins to require invites before user accounts can be created (#3381)
* allow admins to require invites before user accounts can be created * use new dialog component for general confirmation dialogs
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import getDataTransferFiles from "@shared/utils/getDataTransferFiles";
|
||||
import DocumentTemplatize from "~/scenes/DocumentTemplatize";
|
||||
import DocumentTemplatizeDialog from "~/components/DocumentTemplatizeDialog";
|
||||
import { createAction } from "~/actions";
|
||||
import { DocumentSection } from "~/actions/sections";
|
||||
import history from "~/utils/history";
|
||||
@@ -306,18 +306,13 @@ export const createTemplate = createAction({
|
||||
if (!activeDocumentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Create template"),
|
||||
content: (
|
||||
<DocumentTemplatize
|
||||
documentId={activeDocumentId}
|
||||
onSubmit={stores.dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
isCentered: true,
|
||||
content: <DocumentTemplatizeDialog documentId={activeDocumentId} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ const RealButton = styled.button<{
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: none;
|
||||
background: ${lighten(0.05, props.theme.danger)};
|
||||
}
|
||||
|
||||
&.focus-visible {
|
||||
|
||||
58
app/components/ConfirmationDialog.tsx
Normal file
58
app/components/ConfirmationDialog.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
children: JSX.Element;
|
||||
submitText?: string;
|
||||
savingText?: string;
|
||||
danger?: boolean;
|
||||
};
|
||||
|
||||
function ConfirmationDialog({
|
||||
onSubmit,
|
||||
children,
|
||||
submitText,
|
||||
savingText,
|
||||
danger,
|
||||
}: Props) {
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
const { dialogs } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSubmit();
|
||||
dialogs.closeAllModals();
|
||||
} catch (err) {
|
||||
showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[onSubmit, dialogs, showToast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Text type="secondary">{children}</Text>
|
||||
<Button type="submit" disabled={isSaving} danger={danger} autoFocus>
|
||||
{isSaving ? savingText : submitText}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ConfirmationDialog);
|
||||
52
app/components/DocumentTemplatizeDialog.tsx
Normal file
52
app/components/DocumentTemplatizeDialog.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import invariant from "invariant";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import { documentUrl } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
function DocumentTemplatizeDialog({ documentId }: Props) {
|
||||
const history = useHistory();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
const { documents } = useStores();
|
||||
const document = documents.get(documentId);
|
||||
invariant(document, "Document must exist");
|
||||
|
||||
const handleSubmit = React.useCallback(async () => {
|
||||
const template = await document?.templatize();
|
||||
if (template) {
|
||||
history.push(documentUrl(template));
|
||||
showToast(t("Template created, go ahead and customize it"), {
|
||||
type: "info",
|
||||
});
|
||||
}
|
||||
}, [document, showToast, history, t]);
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
onSubmit={handleSubmit}
|
||||
submitText={t("Create template")}
|
||||
savingText={`${t("Creating")}…`}
|
||||
>
|
||||
<Trans
|
||||
defaults="Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents."
|
||||
values={{
|
||||
titleWithDefault: document.titleWithDefault,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(DocumentTemplatizeDialog);
|
||||
@@ -82,9 +82,11 @@ const Modal: React.FC<Props> = ({
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<NudeButton onClick={onRequestClose}>
|
||||
<CloseIcon color="currentColor" />
|
||||
</NudeButton>
|
||||
<Text as="span" size="large">
|
||||
<NudeButton onClick={onRequestClose}>
|
||||
<CloseIcon color="currentColor" />
|
||||
</NudeButton>
|
||||
</Text>
|
||||
</Header>
|
||||
<SmallContent shadow>{children}</SmallContent>
|
||||
</Centered>
|
||||
@@ -261,6 +263,11 @@ const Small = styled.div`
|
||||
&[aria-expanded="true"] {
|
||||
background: ${(props) => props.theme.sidebarControlHoverBackground};
|
||||
}
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
${Header} {
|
||||
align-items: start;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
MoveIcon,
|
||||
HistoryIcon,
|
||||
UnpublishIcon,
|
||||
ShapesIcon,
|
||||
PrintIcon,
|
||||
ImportIcon,
|
||||
NewDocumentIcon,
|
||||
@@ -29,7 +28,6 @@ import Document from "~/models/Document";
|
||||
import DocumentDelete from "~/scenes/DocumentDelete";
|
||||
import DocumentMove from "~/scenes/DocumentMove";
|
||||
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
|
||||
import DocumentTemplatize from "~/scenes/DocumentTemplatize";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
@@ -39,7 +37,7 @@ import Flex from "~/components/Flex";
|
||||
import Modal from "~/components/Modal";
|
||||
import Switch from "~/components/Switch";
|
||||
import { actionToMenuItem } from "~/actions";
|
||||
import { pinDocument } from "~/actions/definitions/documents";
|
||||
import { pinDocument, createTemplate } from "~/actions/definitions/documents";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
@@ -103,7 +101,6 @@ function DocumentMenu({
|
||||
setShowPermanentDeleteModal,
|
||||
] = React.useState(false);
|
||||
const [showMoveModal, setShowMoveModal] = React.useState(false);
|
||||
const [showTemplateModal, setShowTemplateModal] = React.useState(false);
|
||||
const file = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleOpen = React.useCallback(() => {
|
||||
@@ -330,6 +327,7 @@ function DocumentMenu({
|
||||
visible: !document.isStarred && !!can.star,
|
||||
icon: <StarredIcon />,
|
||||
},
|
||||
// Pin document
|
||||
actionToMenuItem(pinDocument, context),
|
||||
{
|
||||
type: "separator",
|
||||
@@ -357,14 +355,8 @@ function DocumentMenu({
|
||||
onClick: handleImportDocument,
|
||||
icon: <ImportIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Create template")}…`,
|
||||
onClick: () => setShowTemplateModal(true),
|
||||
visible:
|
||||
!!can.update && !document.isTemplate && !document.isDraft,
|
||||
icon: <ShapesIcon />,
|
||||
},
|
||||
// Templatize document
|
||||
actionToMenuItem(createTemplate, context),
|
||||
{
|
||||
type: "button",
|
||||
title: t("Duplicate"),
|
||||
@@ -518,19 +510,6 @@ function DocumentMenu({
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{can.update && (
|
||||
<Modal
|
||||
title={t("Create template")}
|
||||
onRequestClose={() => setShowTemplateModal(false)}
|
||||
isOpen={showTemplateModal}
|
||||
isCentered
|
||||
>
|
||||
<DocumentTemplatize
|
||||
documentId={document.id}
|
||||
onSubmit={() => setShowTemplateModal(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -19,6 +19,10 @@ class Team extends BaseModel {
|
||||
@observable
|
||||
sharing: boolean;
|
||||
|
||||
@Field
|
||||
@observable
|
||||
inviteRequired: boolean;
|
||||
|
||||
@Field
|
||||
@observable
|
||||
collaborativeEditing: boolean;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import invariant from "invariant";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import { documentUrl } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
documentId: string;
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
function DocumentTemplatize({ documentId, onSubmit }: Props) {
|
||||
const [isSaving, setIsSaving] = useState<boolean>();
|
||||
const history = useHistory();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
const { documents } = useStores();
|
||||
const document = documents.get(documentId);
|
||||
invariant(document, "Document must exist");
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const template = await document.templatize();
|
||||
|
||||
if (template) {
|
||||
history.push(documentUrl(template));
|
||||
|
||||
showToast(t("Template created, go ahead and customize it"), {
|
||||
type: "info",
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit();
|
||||
} catch (err) {
|
||||
showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[document, showToast, history, onSubmit, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Text type="secondary">
|
||||
<Trans
|
||||
defaults="Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents."
|
||||
values={{
|
||||
titleWithDefault: document.titleWithDefault,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Button type="submit">
|
||||
{isSaving ? `${t("Creating")}…` : t("Create template")}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(DocumentTemplatize);
|
||||
@@ -71,6 +71,14 @@ export default function Notices() {
|
||||
admin.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "invite-required" && (
|
||||
<NoticeAlert>
|
||||
The team you are trying to join requires an invite before you can
|
||||
create an account.
|
||||
<hr />
|
||||
Please request an invite from your team admin and try again.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PadlockIcon } from "outline-icons";
|
||||
import { useState } from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import Heading from "~/components/Heading";
|
||||
import InputSelect from "~/components/InputSelect";
|
||||
import Scene from "~/components/Scene";
|
||||
@@ -16,7 +17,7 @@ import useToasts from "~/hooks/useToasts";
|
||||
import SettingRow from "./components/SettingRow";
|
||||
|
||||
function Security() {
|
||||
const { auth } = useStores();
|
||||
const { auth, dialogs } = useStores();
|
||||
const team = useCurrentTeam();
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToasts();
|
||||
@@ -26,8 +27,11 @@ function Security() {
|
||||
guestSignin: team.guestSignin,
|
||||
defaultUserRole: team.defaultUserRole,
|
||||
memberCollectionCreate: team.memberCollectionCreate,
|
||||
inviteRequired: team.inviteRequired,
|
||||
});
|
||||
|
||||
const authenticationMethods = team.signinMethods;
|
||||
|
||||
const showSuccessMessage = React.useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
@@ -38,22 +42,72 @@ function Security() {
|
||||
[showToast, t]
|
||||
);
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newData = { ...data, [ev.target.id]: ev.target.checked };
|
||||
setData(newData);
|
||||
await auth.updateTeam(newData);
|
||||
showSuccessMessage();
|
||||
const saveData = React.useCallback(
|
||||
async (newData) => {
|
||||
try {
|
||||
setData(newData);
|
||||
await auth.updateTeam(newData);
|
||||
showSuccessMessage();
|
||||
} catch (err) {
|
||||
showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[auth, data, showSuccessMessage]
|
||||
[auth, showSuccessMessage, showToast]
|
||||
);
|
||||
|
||||
const handleDefaultRoleChange = async (newDefaultRole: string) => {
|
||||
const newData = { ...data, defaultUserRole: newDefaultRole };
|
||||
setData(newData);
|
||||
await auth.updateTeam(newData);
|
||||
showSuccessMessage();
|
||||
};
|
||||
const handleChange = React.useCallback(
|
||||
async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
await saveData({ ...data, [ev.target.id]: ev.target.checked });
|
||||
},
|
||||
[data, saveData]
|
||||
);
|
||||
|
||||
const handleDefaultRoleChange = React.useCallback(
|
||||
async (newDefaultRole: string) => {
|
||||
await saveData({ ...data, defaultUserRole: newDefaultRole });
|
||||
},
|
||||
[data, saveData]
|
||||
);
|
||||
|
||||
const handleAllowSignupsChange = React.useCallback(
|
||||
async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inviteRequired = !ev.target.checked;
|
||||
const newData = { ...data, inviteRequired };
|
||||
|
||||
if (inviteRequired) {
|
||||
dialogs.openModal({
|
||||
isCentered: true,
|
||||
title: t("Are you sure you want to disable authorized signups?"),
|
||||
content: (
|
||||
<ConfirmationDialog
|
||||
onSubmit={async () => {
|
||||
await saveData(newData);
|
||||
}}
|
||||
submitText={t("I’m sure — Disable")}
|
||||
savingText={`${t("Disabling")}…`}
|
||||
danger
|
||||
>
|
||||
<Trans
|
||||
defaults="New account creation using <em>{{ authenticationMethods }}</em> will be disabled. New users will need to be invited."
|
||||
values={{
|
||||
authenticationMethods,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</ConfirmationDialog>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await saveData(newData);
|
||||
},
|
||||
[data, saveData, t, dialogs, authenticationMethods]
|
||||
);
|
||||
|
||||
return (
|
||||
<Scene title={t("Security")} icon={<PadlockIcon color="currentColor" />}>
|
||||
@@ -117,6 +171,28 @@ function Security() {
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
label={t("Allow authorized signups")}
|
||||
name="allowSignups"
|
||||
description={
|
||||
<Trans
|
||||
defaults="Allow authorized <em>{{ authenticationMethods }}</em> users to create new accounts without first receiving an invite"
|
||||
values={{
|
||||
authenticationMethods,
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="allowSignups"
|
||||
checked={!data.inviteRequired}
|
||||
onChange={handleAllowSignupsChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
label={t("Default role")}
|
||||
name="defaultUserRole"
|
||||
|
||||
@@ -21,6 +21,7 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
|
||||
collaborativeEditing,
|
||||
defaultCollectionId,
|
||||
defaultUserRole,
|
||||
inviteRequired,
|
||||
} = params;
|
||||
|
||||
if (subdomain !== undefined && process.env.SUBDOMAINS_ENABLED === "true") {
|
||||
@@ -54,6 +55,9 @@ const teamUpdater = async ({ params, user, team, ip }: TeamUpdaterProps) => {
|
||||
if (defaultUserRole !== undefined) {
|
||||
team.defaultUserRole = defaultUserRole;
|
||||
}
|
||||
if (inviteRequired !== undefined) {
|
||||
team.inviteRequired = inviteRequired;
|
||||
}
|
||||
|
||||
const changes = team.changed();
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ describe("userCreator", () => {
|
||||
});
|
||||
|
||||
it("should create a user from an invited user", async () => {
|
||||
const team = await buildTeam();
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
const invite = await buildInvite({
|
||||
teamId: team.id,
|
||||
email: "invite@example.com",
|
||||
@@ -210,4 +210,33 @@ describe("userCreator", () => {
|
||||
expect(user.email).toEqual(invite.email);
|
||||
expect(isNewUser).toEqual(true);
|
||||
});
|
||||
|
||||
it("should reject an uninvited user when invites are required", async () => {
|
||||
const team = await buildTeam({ inviteRequired: true });
|
||||
|
||||
const authenticationProviders = await team.$get("authenticationProviders");
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
let error;
|
||||
|
||||
try {
|
||||
await userCreator({
|
||||
name: "Uninvited User",
|
||||
email: "invite@ExamPle.com",
|
||||
teamId: team.id,
|
||||
ip,
|
||||
authentication: {
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
providerId: "fake-service-id",
|
||||
accessToken: "123",
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.toString()).toContain(
|
||||
"You need an invite to join this team"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Op } from "sequelize";
|
||||
import { InviteRequiredError } from "@server/errors";
|
||||
import { Event, Team, User, UserAuthentication } from "@server/models";
|
||||
|
||||
type UserCreatorResult = {
|
||||
@@ -144,9 +145,16 @@ export default async function userCreator({
|
||||
|
||||
try {
|
||||
const team = await Team.findByPk(teamId, {
|
||||
attributes: ["defaultUserRole"],
|
||||
attributes: ["defaultUserRole", "inviteRequired"],
|
||||
transaction,
|
||||
});
|
||||
|
||||
// If the team settings are set to require invites, and the user is not already invited,
|
||||
// throw an error and fail user creation.
|
||||
if (team?.inviteRequired && !invite) {
|
||||
throw InviteRequiredError();
|
||||
}
|
||||
|
||||
const defaultUserRole = team?.defaultUserRole;
|
||||
|
||||
const user = await User.create(
|
||||
|
||||
@@ -20,6 +20,14 @@ export function AuthorizationError(
|
||||
});
|
||||
}
|
||||
|
||||
export function InviteRequiredError(
|
||||
message = "You need an invite to join this team"
|
||||
) {
|
||||
return httpErrors(403, message, {
|
||||
id: "invite_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function AdminRequiredError(
|
||||
message = "An admin role is required to access this resource"
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn("teams", "inviteRequired", {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
});
|
||||
},
|
||||
down: async (queryInterface) => {
|
||||
await queryInterface.removeColumn("teams", "inviteRequired");
|
||||
},
|
||||
};
|
||||
@@ -77,6 +77,10 @@ class Team extends ParanoidModel {
|
||||
@Column
|
||||
sharing: boolean;
|
||||
|
||||
@Default(false)
|
||||
@Column
|
||||
inviteRequired: boolean;
|
||||
|
||||
@Default(true)
|
||||
@Column(DataType.JSONB)
|
||||
signupQueryParams: { [key: string]: string } | null;
|
||||
|
||||
@@ -15,5 +15,6 @@ export default function present(team: Team) {
|
||||
domain: team.domain,
|
||||
url: team.url,
|
||||
defaultUserRole: team.defaultUserRole,
|
||||
inviteRequired: team.inviteRequired,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ router.post("team.update", auth(), async (ctx) => {
|
||||
collaborativeEditing,
|
||||
defaultCollectionId,
|
||||
defaultUserRole,
|
||||
inviteRequired,
|
||||
} = ctx.body;
|
||||
|
||||
const { user } = ctx.state;
|
||||
@@ -42,6 +43,7 @@ router.post("team.update", auth(), async (ctx) => {
|
||||
collaborativeEditing,
|
||||
defaultCollectionId,
|
||||
defaultUserRole,
|
||||
inviteRequired,
|
||||
},
|
||||
user,
|
||||
team,
|
||||
|
||||
@@ -97,6 +97,9 @@
|
||||
"{{ completed }} task done": "{{ completed }} task done",
|
||||
"{{ completed }} task done_plural": "{{ completed }} tasks done",
|
||||
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
|
||||
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
|
||||
"Creating": "Creating",
|
||||
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.",
|
||||
"Currently editing": "Currently editing",
|
||||
"Currently viewing": "Currently viewing",
|
||||
"Viewed {{ timeAgo }} ago": "Viewed {{ timeAgo }} ago",
|
||||
@@ -239,7 +242,7 @@
|
||||
"Path to document": "Path to document",
|
||||
"Group member options": "Group member options",
|
||||
"Remove": "Remove",
|
||||
"Delete collection": "Delete collection",
|
||||
"Delete collection": "Are you sure you want to delete this collection?",
|
||||
"Sort in sidebar": "Sort in sidebar",
|
||||
"Alphabetical sort": "Alphabetical sort",
|
||||
"Manual sort": "Manual sort",
|
||||
@@ -312,7 +315,7 @@
|
||||
"Get started by creating a new one!": "Get started by creating a new one!",
|
||||
"Create a document": "Create a document",
|
||||
"Manage permissions": "Manage permissions",
|
||||
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.",
|
||||
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.",
|
||||
"Also, <em>{{collectionName}}</em> is being used as the start view – deleting it will reset the start view to the Home page.": "Also, <em>{{collectionName}}</em> is being used as the start view – deleting it will reset the start view to the Home page.",
|
||||
"Deleting": "Deleting",
|
||||
"I’m sure – Delete": "I’m sure – Delete",
|
||||
@@ -331,7 +334,6 @@
|
||||
"This is the default level of access, you can give individual users or groups more access once the collection is created.": "This is the default level of access, you can give individual users or groups more access once the collection is created.",
|
||||
"Public document sharing": "Public document sharing",
|
||||
"When enabled any documents within this collection can be shared publicly on the internet.": "When enabled any documents within this collection can be shared publicly on the internet.",
|
||||
"Creating": "Creating",
|
||||
"Create": "Create",
|
||||
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
|
||||
"Could not add user": "Could not add user",
|
||||
@@ -428,8 +430,6 @@
|
||||
"Heads up – moving the document <em>{{ title }}</em> to the <em>{{ newCollectionName }}</em> collection will grant all team members <em>{{ newPermission }}</em>, they currently have {{ prevPermission }}.": "Heads up – moving the document <em>{{ title }}</em> to the <em>{{ newCollectionName }}</em> collection will grant all team members <em>{{ newPermission }}</em>, they currently have {{ prevPermission }}.",
|
||||
"Moving": "Moving",
|
||||
"Cancel": "Cancel",
|
||||
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
|
||||
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action – we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.",
|
||||
"Search documents": "Search documents",
|
||||
"No documents found for your filters.": "No documents found for your filters.",
|
||||
"You’ve not got any drafts at the moment.": "You’ve not got any drafts at the moment.",
|
||||
@@ -616,6 +616,10 @@
|
||||
"Delete Account": "Delete Account",
|
||||
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
|
||||
"Delete account": "Delete account",
|
||||
"Are you sure you want to disable authorized signups?": "Are you sure you want to disable authorized signups?",
|
||||
"I’m sure — Disable": "I’m sure — Disable",
|
||||
"Disabling": "Disabling",
|
||||
"New account creation using <em>{{ authenticationMethods }}</em> will be disabled. New users will need to be invited.": "New account creation using <em>{{ authenticationMethods }}</em> will be disabled. New users will need to be invited.",
|
||||
"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",
|
||||
@@ -625,6 +629,8 @@
|
||||
"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",
|
||||
"Allow authorized signups": "Allow authorized signups",
|
||||
"Allow authorized <em>{{ authenticationMethods }}</em> users to create new accounts without first receiving an invite": "Allow authorized <em>{{ authenticationMethods }}</em> users to create new accounts without first receiving an invite",
|
||||
"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.",
|
||||
"Sharing is currently disabled.": "Sharing is currently disabled.",
|
||||
|
||||
Reference in New Issue
Block a user