chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-flow",
|
||||
"@babel/preset-typescript",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
@@ -18,6 +18,7 @@
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"tsconfig-paths-module-resolver",
|
||||
[
|
||||
"transform-inline-environment-variables",
|
||||
{
|
||||
|
||||
9
server/.eslintrc
Normal file
9
server/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"../.eslintrc"
|
||||
],
|
||||
"env": {
|
||||
"jest": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,13 @@
|
||||
"<rootDir>/server",
|
||||
"<rootDir>/shared"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@server/(.*)$": "<rootDir>/server/$1",
|
||||
"^@shared/(.*)$": "<rootDir>/shared/$1"
|
||||
},
|
||||
"setupFiles": [
|
||||
"<rootDir>/__mocks__/console.js",
|
||||
"./server/test/setup.js"
|
||||
"./server/test/setup.ts"
|
||||
],
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// @flow
|
||||
export default {
|
||||
add: () => {},
|
||||
};
|
||||
5
server/__mocks__/events.ts
Normal file
5
server/__mocks__/events.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
add: () => {
|
||||
// empty
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import { onAuthenticatePayload } from "@hocuspocus/server";
|
||||
import { Document } from "@server/models";
|
||||
import { getUserForJWT } from "@server/utils/jwt";
|
||||
import { AuthenticationError } from "../errors";
|
||||
import { Document } from "../models";
|
||||
import policy from "../policies";
|
||||
import { getUserForJWT } from "../utils/jwt";
|
||||
|
||||
const { can } = policy;
|
||||
|
||||
@@ -11,26 +11,26 @@ export default class Authentication {
|
||||
connection,
|
||||
token,
|
||||
documentName,
|
||||
}: {
|
||||
connection: { readOnly: boolean },
|
||||
token: string,
|
||||
documentName: string,
|
||||
}) {
|
||||
}: onAuthenticatePayload) {
|
||||
// allows for different entity types to use this multiplayer provider later
|
||||
const [, documentId] = documentName.split(".");
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError("Authentication required");
|
||||
throw AuthenticationError("Authentication required");
|
||||
}
|
||||
|
||||
const user = await getUserForJWT(token);
|
||||
|
||||
if (user.isSuspended) {
|
||||
throw new AuthenticationError("Account suspended");
|
||||
throw AuthenticationError("Account suspended");
|
||||
}
|
||||
|
||||
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!can(user, "read", document)) {
|
||||
throw new AuthenticationError("Authorization required");
|
||||
throw AuthenticationError("Authorization required");
|
||||
}
|
||||
|
||||
// set document to read only for the current user, thus changes will not be
|
||||
@@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import Logger from "../logging/logger";
|
||||
import { User } from "../models";
|
||||
|
||||
export default class CollaborationLogger {
|
||||
async onLoadDocument(data: {
|
||||
documentName: string,
|
||||
context: { user: User },
|
||||
}) {
|
||||
Logger.info("hocuspocus", `Loaded document "${data.documentName}"`, {
|
||||
userId: data.context.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
async onConnect(data: { documentName: string, context: { user: User } }) {
|
||||
Logger.info("hocuspocus", `New connection to "${data.documentName}"`);
|
||||
}
|
||||
|
||||
async onDisconnect(data: { documentName: string, context: { user: User } }) {
|
||||
Logger.info("hocuspocus", `Connection to "${data.documentName}" closed `);
|
||||
}
|
||||
}
|
||||
22
server/collaboration/logger.ts
Normal file
22
server/collaboration/logger.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
onConnectPayload,
|
||||
onDisconnectPayload,
|
||||
onLoadDocumentPayload,
|
||||
} from "@hocuspocus/server";
|
||||
import Logger from "@server/logging/logger";
|
||||
|
||||
export default class CollaborationLogger {
|
||||
async onLoadDocument(data: onLoadDocumentPayload) {
|
||||
Logger.info("hocuspocus", `Loaded document "${data.documentName}"`, {
|
||||
userId: data.context.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
async onConnect(data: onConnectPayload) {
|
||||
Logger.info("hocuspocus", `New connection to "${data.documentName}"`);
|
||||
}
|
||||
|
||||
async onDisconnect(data: onDisconnectPayload) {
|
||||
Logger.info("hocuspocus", `Connection to "${data.documentName}" closed `);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
// @flow
|
||||
import { onChangePayload, onLoadDocumentPayload } from "@hocuspocus/server";
|
||||
import { debounce } from "lodash";
|
||||
import * as Y from "yjs";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Document, User } from "@server/models";
|
||||
import documentUpdater from "../commands/documentUpdater";
|
||||
import Logger from "../logging/logger";
|
||||
import { Document, User } from "../models";
|
||||
import markdownToYDoc from "./utils/markdownToYDoc";
|
||||
|
||||
const DELAY = 3000;
|
||||
|
||||
export default class Persistence {
|
||||
async onLoadDocument({
|
||||
documentName,
|
||||
...data
|
||||
}: {
|
||||
documentName: string,
|
||||
document: Y.Doc,
|
||||
}) {
|
||||
async onLoadDocument({ documentName, ...data }: onLoadDocumentPayload) {
|
||||
const [, documentId] = documentName.split(".");
|
||||
const fieldName = "default";
|
||||
|
||||
@@ -40,23 +34,20 @@ export default class Persistence {
|
||||
);
|
||||
const ydoc = markdownToYDoc(document.text, fieldName);
|
||||
const state = Y.encodeStateAsUpdate(ydoc);
|
||||
|
||||
await document.update({ state: Buffer.from(state) }, { hooks: false });
|
||||
await document.update(
|
||||
{
|
||||
state: Buffer.from(state),
|
||||
},
|
||||
{
|
||||
hooks: false,
|
||||
}
|
||||
);
|
||||
return ydoc;
|
||||
}
|
||||
|
||||
onChange = debounce(
|
||||
async ({
|
||||
document,
|
||||
context,
|
||||
documentName,
|
||||
}: {
|
||||
document: Y.Doc,
|
||||
context: { user: ?User },
|
||||
documentName: string,
|
||||
}) => {
|
||||
async ({ document, context, documentName }: onChangePayload) => {
|
||||
const [, documentId] = documentName.split(".");
|
||||
|
||||
Logger.info("database", `Persisting ${documentId}`);
|
||||
|
||||
try {
|
||||
@@ -1,66 +0,0 @@
|
||||
// @flow
|
||||
import Metrics from "../logging/metrics";
|
||||
|
||||
export default class Tracing {
|
||||
onLoadDocument({
|
||||
documentName,
|
||||
instance,
|
||||
}: {
|
||||
documentName: string,
|
||||
instance: any,
|
||||
}) {
|
||||
Metrics.increment("collaboration.load_document", { documentName });
|
||||
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.documents_count",
|
||||
instance.getDocumentsCount()
|
||||
);
|
||||
}
|
||||
|
||||
onAuthenticationFailed({ documentName }: { documentName: string }) {
|
||||
Metrics.increment("collaboration.authentication_failed", { documentName });
|
||||
}
|
||||
|
||||
onConnect({
|
||||
documentName,
|
||||
instance,
|
||||
}: {
|
||||
documentName: string,
|
||||
instance: any,
|
||||
}) {
|
||||
Metrics.increment("collaboration.connect", { documentName });
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.connections_count",
|
||||
instance.getConnectionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
onDisconnect({
|
||||
documentName,
|
||||
instance,
|
||||
}: {
|
||||
documentName: string,
|
||||
instance: any,
|
||||
}) {
|
||||
Metrics.increment("collaboration.disconnect", { documentName });
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.connections_count",
|
||||
instance.getConnectionsCount()
|
||||
);
|
||||
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.documents_count",
|
||||
// -1 adjustment because hook is called before document is removed
|
||||
instance.getDocumentsCount() - 1
|
||||
);
|
||||
}
|
||||
|
||||
onChange({ documentName }: { documentName: string }) {
|
||||
Metrics.increment("collaboration.change", { documentName });
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
Metrics.gaugePerInstance("collaboration.connections_count", 0);
|
||||
Metrics.gaugePerInstance("collaboration.documents_count", 0);
|
||||
}
|
||||
}
|
||||
60
server/collaboration/tracing.ts
Normal file
60
server/collaboration/tracing.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
onChangePayload,
|
||||
onConnectPayload,
|
||||
onDisconnectPayload,
|
||||
onLoadDocumentPayload,
|
||||
} from "@hocuspocus/server";
|
||||
import Metrics from "@server/logging/metrics";
|
||||
|
||||
export default class Tracing {
|
||||
onLoadDocument({ documentName, instance }: onLoadDocumentPayload) {
|
||||
Metrics.increment("collaboration.load_document", {
|
||||
documentName,
|
||||
});
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.documents_count",
|
||||
instance.getDocumentsCount()
|
||||
);
|
||||
}
|
||||
|
||||
onAuthenticationFailed({ documentName }: { documentName: string }) {
|
||||
Metrics.increment("collaboration.authentication_failed", {
|
||||
documentName,
|
||||
});
|
||||
}
|
||||
|
||||
onConnect({ documentName, instance }: onConnectPayload) {
|
||||
Metrics.increment("collaboration.connect", {
|
||||
documentName,
|
||||
});
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.connections_count",
|
||||
instance.getConnectionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
onDisconnect({ documentName, instance }: onDisconnectPayload) {
|
||||
Metrics.increment("collaboration.disconnect", {
|
||||
documentName,
|
||||
});
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.connections_count",
|
||||
instance.getConnectionsCount()
|
||||
);
|
||||
Metrics.gaugePerInstance(
|
||||
"collaboration.documents_count", // -1 adjustment because hook is called before document is removed
|
||||
instance.getDocumentsCount() - 1
|
||||
);
|
||||
}
|
||||
|
||||
onChange({ documentName }: onChangePayload) {
|
||||
Metrics.increment("collaboration.change", {
|
||||
documentName,
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
Metrics.gaugePerInstance("collaboration.connections_count", 0);
|
||||
Metrics.gaugePerInstance("collaboration.documents_count", 0);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import { Node, Fragment } from "prosemirror-model";
|
||||
import { parser, schema } from "rich-markdown-editor";
|
||||
import { prosemirrorToYDoc } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
import embeds from "../../../shared/embeds";
|
||||
import embeds from "@shared/embeds";
|
||||
|
||||
export default function markdownToYDoc(
|
||||
markdown: string,
|
||||
fieldName?: string = "default"
|
||||
fieldName = "default"
|
||||
): Y.Doc {
|
||||
let node = parser.parse(markdown);
|
||||
|
||||
@@ -16,9 +15,11 @@ export default function markdownToYDoc(
|
||||
// on the server we need to mimic this behavior.
|
||||
function urlsToEmbeds(node: Node): Node {
|
||||
if (node.type.name === "paragraph") {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'content' does not exist on type 'Fragmen... Remove this comment to see the full error message
|
||||
for (const textNode of node.content.content) {
|
||||
for (const embed of embeds) {
|
||||
if (textNode.text && embed.matcher(textNode.text)) {
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'ProsemirrorNode<Schema<never, never>> | null... Remove this comment to see the full error message
|
||||
return schema.nodes.embed.createAndFill({
|
||||
href: textNode.text,
|
||||
});
|
||||
@@ -29,6 +30,7 @@ export default function markdownToYDoc(
|
||||
|
||||
if (node.content) {
|
||||
const contentAsArray =
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'content' does not exist on type 'Fragmen... Remove this comment to see the full error message
|
||||
node.content instanceof Fragment ? node.content.content : node.content;
|
||||
node.content = Fragment.fromArray(contentAsArray.map(urlsToEmbeds));
|
||||
}
|
||||
@@ -37,6 +39,5 @@ export default function markdownToYDoc(
|
||||
}
|
||||
|
||||
node = urlsToEmbeds(node);
|
||||
|
||||
return prosemirrorToYDoc(node, fieldName);
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
// @flow
|
||||
import { Collection, UserAuthentication } from "@server/models";
|
||||
import { buildUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import mailer from "../mailer";
|
||||
import { Collection, UserAuthentication } from "../models";
|
||||
import { buildUser, buildTeam } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import accountProvisioner from "./accountProvisioner";
|
||||
|
||||
jest.mock("../mailer");
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { putObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
putObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// $FlowFixMe
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mockReset' does not exist on type '(type... Remove this comment to see the full error message
|
||||
mailer.sendTemplate.mockReset();
|
||||
|
||||
return flushdb();
|
||||
});
|
||||
|
||||
describe("accountProvisioner", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -49,10 +47,8 @@ describe("accountProvisioner", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const authentications = await user.getAuthentications();
|
||||
const auth = authentications[0];
|
||||
|
||||
expect(auth.accessToken).toEqual("123");
|
||||
expect(auth.scopes.length).toEqual(1);
|
||||
expect(auth.scopes[0]).toEqual("read");
|
||||
@@ -62,7 +58,6 @@ describe("accountProvisioner", () => {
|
||||
expect(isNewUser).toEqual(true);
|
||||
expect(isNewTeam).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
|
||||
const collectionCount = await Collection.count();
|
||||
expect(collectionCount).toEqual(1);
|
||||
});
|
||||
@@ -71,12 +66,13 @@ describe("accountProvisioner", () => {
|
||||
const existingTeam = await buildTeam();
|
||||
const providers = await existingTeam.getAuthenticationProviders();
|
||||
const authenticationProvider = providers[0];
|
||||
const existing = await buildUser({ teamId: existingTeam.id });
|
||||
const existing = await buildUser({
|
||||
teamId: existingTeam.id,
|
||||
});
|
||||
const authentications = await existing.getAuthentications();
|
||||
const authentication = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
const newUsername = "tname";
|
||||
|
||||
const { user, isNewUser, isNewTeam } = await accountProvisioner({
|
||||
ip,
|
||||
user: {
|
||||
@@ -100,7 +96,6 @@ describe("accountProvisioner", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const auth = await UserAuthentication.findByPk(authentication.id);
|
||||
expect(auth.accessToken).toEqual("123");
|
||||
expect(auth.scopes.length).toEqual(1);
|
||||
@@ -110,7 +105,6 @@ describe("accountProvisioner", () => {
|
||||
expect(isNewTeam).toEqual(false);
|
||||
expect(isNewUser).toEqual(false);
|
||||
expect(mailer.sendTemplate).not.toHaveBeenCalled();
|
||||
|
||||
const collectionCount = await Collection.count();
|
||||
expect(collectionCount).toEqual(0);
|
||||
});
|
||||
@@ -119,9 +113,12 @@ describe("accountProvisioner", () => {
|
||||
const existingTeam = await buildTeam();
|
||||
const providers = await existingTeam.getAuthenticationProviders();
|
||||
const authenticationProvider = providers[0];
|
||||
await authenticationProvider.update({ enabled: false });
|
||||
|
||||
const existing = await buildUser({ teamId: existingTeam.id });
|
||||
await authenticationProvider.update({
|
||||
enabled: false,
|
||||
});
|
||||
const existing = await buildUser({
|
||||
teamId: existingTeam.id,
|
||||
});
|
||||
const authentications = await existing.getAuthentications();
|
||||
const authentication = authentications[0];
|
||||
let error;
|
||||
@@ -160,7 +157,6 @@ describe("accountProvisioner", () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const { user, isNewUser } = await accountProvisioner({
|
||||
ip,
|
||||
user: {
|
||||
@@ -184,10 +180,8 @@ describe("accountProvisioner", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const authentications = await user.getAuthentications();
|
||||
const auth = authentications[0];
|
||||
|
||||
expect(auth.accessToken).toEqual("123");
|
||||
expect(auth.scopes.length).toEqual(1);
|
||||
expect(auth.scopes[0]).toEqual("read");
|
||||
@@ -195,7 +189,6 @@ describe("accountProvisioner", () => {
|
||||
expect(user.username).toEqual("jtester");
|
||||
expect(isNewUser).toEqual(true);
|
||||
expect(mailer.sendTemplate).toHaveBeenCalled();
|
||||
|
||||
// should provision welcome collection
|
||||
const collectionCount = await Collection.count();
|
||||
expect(collectionCount).toEqual(1);
|
||||
@@ -1,48 +1,49 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import Sequelize from "sequelize";
|
||||
import { Collection, Team, User } from "@server/models";
|
||||
import {
|
||||
AuthenticationError,
|
||||
EmailAuthenticationRequiredError,
|
||||
AuthenticationProviderDisabledError,
|
||||
} from "../errors";
|
||||
import mailer from "../mailer";
|
||||
import { Collection, Team, User } from "../models";
|
||||
import teamCreator from "./teamCreator";
|
||||
import userCreator from "./userCreator";
|
||||
|
||||
type Props = {|
|
||||
ip: string,
|
||||
user: {|
|
||||
name: string,
|
||||
email: string,
|
||||
avatarUrl?: string,
|
||||
username?: string,
|
||||
|},
|
||||
team: {|
|
||||
name: string,
|
||||
domain?: string,
|
||||
subdomain: string,
|
||||
avatarUrl?: string,
|
||||
|},
|
||||
authenticationProvider: {|
|
||||
name: string,
|
||||
providerId: string,
|
||||
|},
|
||||
authentication: {|
|
||||
providerId: string,
|
||||
scopes: string[],
|
||||
accessToken?: string,
|
||||
refreshToken?: string,
|
||||
|},
|
||||
|};
|
||||
type Props = {
|
||||
ip: string;
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl?: string;
|
||||
username?: string;
|
||||
};
|
||||
team: {
|
||||
name: string;
|
||||
domain?: string;
|
||||
subdomain: string;
|
||||
avatarUrl?: string;
|
||||
};
|
||||
authenticationProvider: {
|
||||
name: string;
|
||||
providerId: string;
|
||||
};
|
||||
authentication: {
|
||||
providerId: string;
|
||||
scopes: string[];
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AccountProvisionerResult = {|
|
||||
user: User,
|
||||
team: Team,
|
||||
isNewTeam: boolean,
|
||||
isNewUser: boolean,
|
||||
|};
|
||||
export type AccountProvisionerResult = {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
isNewTeam: boolean;
|
||||
isNewUser: boolean;
|
||||
};
|
||||
|
||||
export default async function accountProvisioner({
|
||||
ip,
|
||||
@@ -52,6 +53,7 @@ export default async function accountProvisioner({
|
||||
authentication: authenticationParams,
|
||||
}: Props): Promise<AccountProvisionerResult> {
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await teamCreator({
|
||||
name: teamParams.name,
|
||||
@@ -61,14 +63,14 @@ export default async function accountProvisioner({
|
||||
authenticationProvider: authenticationProviderParams,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AuthenticationError(err.message);
|
||||
throw AuthenticationError(err.message);
|
||||
}
|
||||
|
||||
invariant(result, "Team creator result must exist");
|
||||
const { authenticationProvider, team, isNewTeam } = result;
|
||||
|
||||
if (!authenticationProvider.enabled) {
|
||||
throw new AuthenticationProviderDisabledError();
|
||||
throw AuthenticationProviderDisabledError();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,7 +87,6 @@ export default async function accountProvisioner({
|
||||
authenticationProviderId: authenticationProvider.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { isNewUser, user } = result;
|
||||
|
||||
if (isNewUser) {
|
||||
@@ -102,7 +103,11 @@ export default async function accountProvisioner({
|
||||
// failed. In this case we have a valid previously created team but no
|
||||
// onboarding collection.
|
||||
if (!isNewTeam) {
|
||||
const count = await Collection.count({ where: { teamId: team.id } });
|
||||
const count = await Collection.count({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
provision = count === 0;
|
||||
}
|
||||
|
||||
@@ -127,12 +132,12 @@ export default async function accountProvisioner({
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
throw new EmailAuthenticationRequiredError(
|
||||
throw EmailAuthenticationRequiredError(
|
||||
"Email authentication required",
|
||||
team.url
|
||||
);
|
||||
} else {
|
||||
throw new AuthenticationError(err.message, team.url);
|
||||
throw AuthenticationError(err.message, team.url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Attachment, Event, User } from "../models";
|
||||
import { uploadToS3FromBuffer } from "../utils/s3";
|
||||
import { Attachment, Event, User } from "@server/models";
|
||||
import { uploadToS3FromBuffer } from "@server/utils/s3";
|
||||
|
||||
export default async function attachmentCreator({
|
||||
name,
|
||||
@@ -11,17 +10,17 @@ export default async function attachmentCreator({
|
||||
source,
|
||||
ip,
|
||||
}: {
|
||||
name: string,
|
||||
type: string,
|
||||
buffer: Buffer,
|
||||
user: User,
|
||||
source?: "import",
|
||||
ip: string,
|
||||
name: string;
|
||||
type: string;
|
||||
buffer: Buffer;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
source?: "import";
|
||||
ip: string;
|
||||
}) {
|
||||
const key = `uploads/${user.id}/${uuidv4()}/${name}`;
|
||||
const acl = process.env.AWS_S3_ACL || "private";
|
||||
const url = await uploadToS3FromBuffer(buffer, type, key, acl);
|
||||
|
||||
const attachment = await Attachment.create({
|
||||
key,
|
||||
acl,
|
||||
@@ -31,15 +30,16 @@ export default async function attachmentCreator({
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "attachments.create",
|
||||
data: { name, source },
|
||||
data: {
|
||||
name,
|
||||
source,
|
||||
},
|
||||
modelId: attachment.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
return attachment;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { Collection, Event, Team, User, FileOperation } from "../models";
|
||||
import { getAWSKeyForFileOp } from "../utils/s3";
|
||||
import { Collection, Event, Team, User, FileOperation } from "@server/models";
|
||||
import { getAWSKeyForFileOp } from "@server/utils/s3";
|
||||
|
||||
export default async function collectionExporter({
|
||||
collection,
|
||||
@@ -8,14 +7,16 @@ export default async function collectionExporter({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
collection?: Collection,
|
||||
team: Team,
|
||||
user: User,
|
||||
ip: string,
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
collection?: Collection;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
ip: string;
|
||||
}) {
|
||||
const collectionId = collection?.id;
|
||||
const key = getAWSKeyForFileOp(user.teamId, collection?.name || team.name);
|
||||
|
||||
const fileOperation = await FileOperation.create({
|
||||
type: "export",
|
||||
state: "creating",
|
||||
@@ -26,7 +27,6 @@ export default async function collectionExporter({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
// Event is consumed on worker in queues/processors/exports
|
||||
await Event.create({
|
||||
name: collection ? "collections.export" : "collections.export_all",
|
||||
@@ -36,9 +36,7 @@ export default async function collectionExporter({
|
||||
modelId: fileOperation.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
fileOperation.user = user;
|
||||
fileOperation.collection = collection;
|
||||
|
||||
return fileOperation;
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
// @flow
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
import { Attachment, Document, Collection } from "../models";
|
||||
import { buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Attachment, Document, Collection } from "@server/models";
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import collectionImporter from "./collectionImporter";
|
||||
|
||||
jest.mock("../utils/s3");
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("collectionImporter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -21,18 +19,15 @@ describe("collectionImporter", () => {
|
||||
type: "application/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await collectionImporter({
|
||||
type: "outline",
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(8);
|
||||
expect(response.attachments.length).toEqual(6);
|
||||
|
||||
expect(await Collection.count()).toEqual(1);
|
||||
expect(await Document.count()).toEqual(8);
|
||||
expect(await Attachment.count()).toEqual(6);
|
||||
@@ -46,8 +41,8 @@ describe("collectionImporter", () => {
|
||||
type: "application/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await collectionImporter({
|
||||
type: "outline",
|
||||
@@ -70,8 +65,8 @@ describe("collectionImporter", () => {
|
||||
type: "application/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await collectionImporter({
|
||||
type: "outline",
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
import invariant from "invariant";
|
||||
import { values, keys } from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { parseOutlineExport } from "../../shared/utils/zip";
|
||||
import { parseOutlineExport } from "@shared/utils/zip";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Attachment, Event, Document, Collection, User } from "@server/models";
|
||||
import { FileImportError } from "../errors";
|
||||
import Logger from "../logging/logger";
|
||||
import { Attachment, Event, Document, Collection, User } from "../models";
|
||||
import attachmentCreator from "./attachmentCreator";
|
||||
import documentCreator from "./documentCreator";
|
||||
import documentImporter from "./documentImporter";
|
||||
@@ -20,36 +20,48 @@ export default async function collectionImporter({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
file: File,
|
||||
user: User,
|
||||
type: "outline",
|
||||
ip: string,
|
||||
file: File;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
type: "outline";
|
||||
ip: string;
|
||||
}) {
|
||||
// load the zip structure into memory
|
||||
const zipData = await fs.promises.readFile(file.path);
|
||||
|
||||
let items;
|
||||
|
||||
try {
|
||||
items = await await parseOutlineExport(zipData);
|
||||
} catch (err) {
|
||||
throw new FileImportError(err.message);
|
||||
throw FileImportError(err.message);
|
||||
}
|
||||
|
||||
if (!items.filter((item) => item.type === "document").length) {
|
||||
throw new FileImportError(
|
||||
throw FileImportError(
|
||||
"Uploaded file does not contain importable documents"
|
||||
);
|
||||
}
|
||||
|
||||
// store progress and pointers
|
||||
let collections: { string: Collection } = {};
|
||||
let documents: { string: Document } = {};
|
||||
let attachments: { string: Attachment } = {};
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
|
||||
const collections: {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
string: Collection;
|
||||
} = {};
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
|
||||
const documents: {
|
||||
string: Document;
|
||||
} = {};
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'string' is missing in type '{}' but requ... Remove this comment to see the full error message
|
||||
const attachments: {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Attachment' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
string: Attachment;
|
||||
} = {};
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type === "collection") {
|
||||
// check if collection with name exists
|
||||
let [collection, isCreated] = await Collection.findOrCreate({
|
||||
const [collection, isCreated] = await Collection.findOrCreate({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
name: item.name,
|
||||
@@ -65,6 +77,7 @@ export default async function collectionImporter({
|
||||
// with right now
|
||||
if (!isCreated) {
|
||||
const name = `${item.name} (Imported)`;
|
||||
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'collection' because it is a cons... Remove this comment to see the full error message
|
||||
collection = await Collection.create({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
@@ -76,7 +89,9 @@ export default async function collectionImporter({
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: user.id,
|
||||
data: { name },
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
ip,
|
||||
});
|
||||
}
|
||||
@@ -89,30 +104,26 @@ export default async function collectionImporter({
|
||||
const collectionDir = item.dir.split("/")[0];
|
||||
const collection = collections[collectionDir];
|
||||
invariant(collection, `Collection must exist for document ${item.dir}`);
|
||||
|
||||
// we have a document
|
||||
const content = await item.item.async("string");
|
||||
const name = path.basename(item.name);
|
||||
const tmpDir = os.tmpdir();
|
||||
const tmpFilePath = `${tmpDir}/upload-${uuidv4()}`;
|
||||
|
||||
await fs.promises.writeFile(tmpFilePath, content);
|
||||
const file = new File({
|
||||
name,
|
||||
type: "text/markdown",
|
||||
path: tmpFilePath,
|
||||
});
|
||||
|
||||
const { text, title } = await documentImporter({
|
||||
file,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
await fs.promises.unlink(tmpFilePath);
|
||||
|
||||
// must be a nested document, find and reference the parent document
|
||||
let parentDocumentId;
|
||||
|
||||
if (item.depth > 1) {
|
||||
const parentDocument =
|
||||
documents[`${item.dir}.md`] || documents[item.dir];
|
||||
@@ -128,13 +139,14 @@ export default async function collectionImporter({
|
||||
collectionId: collection.id,
|
||||
createdAt: item.metadata.createdAt
|
||||
? new Date(item.metadata.createdAt)
|
||||
: item.date,
|
||||
: // @ts-expect-error ts-migrate(2339) FIXME: Property 'date' does not exist on type 'Item'.
|
||||
item.date,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'date' does not exist on type 'Item'.
|
||||
updatedAt: item.date,
|
||||
parentDocumentId,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
documents[item.path] = document;
|
||||
continue;
|
||||
}
|
||||
@@ -168,14 +180,16 @@ export default async function collectionImporter({
|
||||
/(.*)uploads\//,
|
||||
"uploads/"
|
||||
);
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
document.text = document.text
|
||||
.replace(attachmentPath, attachment.redirectUrl)
|
||||
.replace(normalizedAttachmentPath, attachment.redirectUrl)
|
||||
.replace(`/${normalizedAttachmentPath}`, attachment.redirectUrl);
|
||||
|
||||
// does nothing if the document text is unchanged
|
||||
await document.save({ fields: ["text"] });
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save({
|
||||
fields: ["text"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { Document, Event, User } from "../models";
|
||||
import { Document, Event, User } from "@server/models";
|
||||
|
||||
export default async function documentCreator({
|
||||
title = "",
|
||||
@@ -8,7 +7,8 @@ export default async function documentCreator({
|
||||
collectionId,
|
||||
parentDocumentId,
|
||||
templateDocument,
|
||||
createdAt, // allows override for import
|
||||
createdAt,
|
||||
// allows override for import
|
||||
updatedAt,
|
||||
template,
|
||||
index,
|
||||
@@ -17,23 +17,26 @@ export default async function documentCreator({
|
||||
source,
|
||||
ip,
|
||||
}: {
|
||||
title: string,
|
||||
text: string,
|
||||
publish?: boolean,
|
||||
collectionId: string,
|
||||
parentDocumentId?: string,
|
||||
templateDocument?: Document,
|
||||
template?: boolean,
|
||||
createdAt?: Date,
|
||||
updatedAt?: Date,
|
||||
index?: number,
|
||||
user: User,
|
||||
editorVersion?: string,
|
||||
source?: "import",
|
||||
ip: string,
|
||||
title: string;
|
||||
text: string;
|
||||
publish?: boolean;
|
||||
collectionId: string;
|
||||
parentDocumentId?: string;
|
||||
templateDocument?: Document;
|
||||
template?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
index?: number;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
editorVersion?: string;
|
||||
source?: "import";
|
||||
ip: string;
|
||||
// @ts-expect-error ts-migrate(1064) FIXME: The return type of an async function or method mus... Remove this comment to see the full error message
|
||||
}): Document {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
const templateId = templateDocument ? templateDocument.id : undefined;
|
||||
let document = await Document.create({
|
||||
const document = await Document.create({
|
||||
parentDocumentId,
|
||||
editorVersion,
|
||||
collectionId,
|
||||
@@ -46,29 +49,35 @@ export default async function documentCreator({
|
||||
template,
|
||||
templateId,
|
||||
title: templateDocument ? templateDocument.title : title,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
text: templateDocument ? templateDocument.text : text,
|
||||
});
|
||||
|
||||
await Event.create({
|
||||
name: "documents.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
data: { source, title: document.title, templateId },
|
||||
data: {
|
||||
source,
|
||||
title: document.title,
|
||||
templateId,
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
await document.publish(user.id);
|
||||
|
||||
await Event.create({
|
||||
name: "documents.publish",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
data: { source, title: document.title },
|
||||
data: {
|
||||
source,
|
||||
title: document.title,
|
||||
},
|
||||
ip,
|
||||
});
|
||||
}
|
||||
@@ -77,6 +86,9 @@ export default async function documentCreator({
|
||||
// we need to specify publishedAt to bypass default scope that only returns
|
||||
// published documents
|
||||
return Document.findOne({
|
||||
where: { id: document.id, publishedAt: document.publishedAt },
|
||||
where: {
|
||||
id: document.id,
|
||||
publishedAt: document.publishedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
// @flow
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
import { Attachment } from "../models";
|
||||
import { buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Attachment } from "@server/models";
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import documentImporter from "./documentImporter";
|
||||
|
||||
jest.mock("../utils/s3");
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("documentImporter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -22,16 +20,13 @@ describe("documentImporter", () => {
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
const attachments = await Attachment.count();
|
||||
expect(attachments).toEqual(1);
|
||||
|
||||
expect(response.text).toContain("This is a test document for images");
|
||||
expect(response.text).toContain(";
|
||||
expect(response.title).toEqual("images");
|
||||
@@ -45,16 +40,13 @@ describe("documentImporter", () => {
|
||||
type: "application/octet-stream",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
const attachments = await Attachment.count();
|
||||
expect(attachments).toEqual(1);
|
||||
|
||||
expect(response.text).toContain("This is a test document for images");
|
||||
expect(response.text).toContain(";
|
||||
expect(response.title).toEqual("images");
|
||||
@@ -68,8 +60,8 @@ describe("documentImporter", () => {
|
||||
type: "application/octet-stream",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await documentImporter({
|
||||
user,
|
||||
@@ -91,16 +83,13 @@ describe("documentImporter", () => {
|
||||
type: "application/octet-stream",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
const attachments = await Attachment.count();
|
||||
expect(attachments).toEqual(1);
|
||||
|
||||
expect(response.text).toContain("This is a test document for images");
|
||||
expect(response.text).toContain(";
|
||||
expect(response.title).toEqual("images");
|
||||
@@ -114,13 +103,11 @@ describe("documentImporter", () => {
|
||||
type: "text/html",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("Text paragraph");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -133,13 +120,11 @@ describe("documentImporter", () => {
|
||||
type: "application/msword",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("this is a test document");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -152,13 +137,11 @@ describe("documentImporter", () => {
|
||||
type: "text/plain",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("This is a test paragraph");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -171,13 +154,11 @@ describe("documentImporter", () => {
|
||||
type: "text/plain",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", "empty.md"),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("");
|
||||
expect(response.title).toEqual("this / and / this");
|
||||
});
|
||||
@@ -190,13 +171,11 @@ describe("documentImporter", () => {
|
||||
type: "application/lol",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
const response = await documentImporter({
|
||||
user,
|
||||
file,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.text).toContain("This is a test paragraph");
|
||||
expect(response.title).toEqual("Heading 1");
|
||||
});
|
||||
@@ -209,8 +188,8 @@ describe("documentImporter", () => {
|
||||
type: "executable/zip",
|
||||
path: path.resolve(__dirname, "..", "test", "fixtures", name),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await documentImporter({
|
||||
user,
|
||||
@@ -1,18 +1,19 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'form... Remove this comment to see the full error message
|
||||
import File from "formidable/lib/file";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'jopl... Remove this comment to see the full error message
|
||||
import { strikethrough, tables } from "joplin-turndown-plugin-gfm";
|
||||
import mammoth from "mammoth";
|
||||
import quotedPrintable from "quoted-printable";
|
||||
import TurndownService from "turndown";
|
||||
import utf8 from "utf8";
|
||||
import parseTitle from "../../shared/utils/parseTitle";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import { User } from "@server/models";
|
||||
import dataURItoBuffer from "@server/utils/dataURItoBuffer";
|
||||
import { deserializeFilename } from "@server/utils/fs";
|
||||
import parseImages from "@server/utils/parseImages";
|
||||
import { FileImportError, InvalidRequestError } from "../errors";
|
||||
import { User } from "../models";
|
||||
import dataURItoBuffer from "../utils/dataURItoBuffer";
|
||||
import { deserializeFilename } from "../utils/fs";
|
||||
import parseImages from "../utils/parseImages";
|
||||
import attachmentCreator from "./attachmentCreator";
|
||||
|
||||
// https://github.com/domchristie/turndown#options
|
||||
@@ -21,7 +22,6 @@ const turndownService = new TurndownService({
|
||||
bulletListMarker: "-",
|
||||
headingStyle: "atx",
|
||||
});
|
||||
|
||||
// Use the GitHub-flavored markdown plugin to parse
|
||||
// strikethoughs and tables
|
||||
turndownService
|
||||
@@ -33,7 +33,6 @@ turndownService
|
||||
return "\n";
|
||||
},
|
||||
});
|
||||
|
||||
interface ImportableFile {
|
||||
type: string;
|
||||
getMarkdown: (file: any) => Promise<string>;
|
||||
@@ -67,20 +66,24 @@ const importMapping: ImportableFile[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function fileToMarkdown(file): Promise<string> {
|
||||
return fs.promises.readFile(file.path, "utf8");
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function docxToMarkdown(file): Promise<string> {
|
||||
const { value } = await mammoth.convertToHtml(file);
|
||||
return turndownService.turndown(value);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function htmlToMarkdown(file): Promise<string> {
|
||||
const value = await fs.promises.readFile(file.path, "utf8");
|
||||
return turndownService.turndown(value);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'file' implicitly has an 'any' type.
|
||||
async function confluenceToMarkdown(file): Promise<string> {
|
||||
let value = await fs.promises.readFile(file.path, "utf8");
|
||||
|
||||
@@ -88,13 +91,14 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
// Word documents should call into the docxToMarkdown importer.
|
||||
// See: https://jira.atlassian.com/browse/CONFSERVER-38237
|
||||
if (!value.includes("Content-Type: multipart/related")) {
|
||||
throw new FileImportError("Unsupported Word file");
|
||||
throw FileImportError("Unsupported Word file");
|
||||
}
|
||||
|
||||
// get boundary marker
|
||||
const boundaryMarker = value.match(/boundary="(.+)"/);
|
||||
|
||||
if (!boundaryMarker) {
|
||||
throw new FileImportError("Unsupported Word file (No boundary marker)");
|
||||
throw FileImportError("Unsupported Word file (No boundary marker)");
|
||||
}
|
||||
|
||||
// get content between multipart boundaries
|
||||
@@ -104,6 +108,7 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
boundaryReached++;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line.startsWith("Content-")) {
|
||||
return false;
|
||||
}
|
||||
@@ -114,11 +119,12 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
if (boundaryReached === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!lines.length) {
|
||||
throw new FileImportError("Unsupported Word file (No content found)");
|
||||
throw FileImportError("Unsupported Word file (No content found)");
|
||||
}
|
||||
|
||||
// Mime attachment is "quoted printable" encoded, must be decoded first
|
||||
@@ -127,11 +133,10 @@ async function confluenceToMarkdown(file): Promise<string> {
|
||||
|
||||
// If we don't remove the title here it becomes printed in the document
|
||||
// body by turndown
|
||||
turndownService.remove(["style", "xml", "title"]);
|
||||
turndownService.remove(["style", "title"]);
|
||||
|
||||
// Now we should have something that looks like HTML
|
||||
const html = turndownService.turndown(value);
|
||||
|
||||
return html.replace(/<br>/g, " \\n ");
|
||||
}
|
||||
|
||||
@@ -140,10 +145,14 @@ export default async function documentImporter({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
file: File,
|
||||
ip: string,
|
||||
}): Promise<{ text: string, title: string }> {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
file: File;
|
||||
ip: string;
|
||||
}): Promise<{
|
||||
text: string;
|
||||
title: string;
|
||||
}> {
|
||||
const fileInfo = importMapping.filter((item) => {
|
||||
if (item.type === file.type) {
|
||||
if (
|
||||
@@ -152,17 +161,21 @@ export default async function documentImporter({
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.type === "text/markdown" && path.extname(file.name) === ".md") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})[0];
|
||||
|
||||
if (!fileInfo) {
|
||||
throw new InvalidRequestError(`File type ${file.type} not supported`);
|
||||
throw InvalidRequestError(`File type ${file.type} not supported`);
|
||||
}
|
||||
|
||||
let title = deserializeFilename(file.name.replace(/\.[^/.]+$/, ""));
|
||||
let text = await fileInfo.getMarkdown(file);
|
||||
|
||||
@@ -181,7 +194,6 @@ export default async function documentImporter({
|
||||
for (const uri of dataURIs) {
|
||||
const name = "imported";
|
||||
const { buffer, type } = dataURItoBuffer(uri);
|
||||
|
||||
const attachment = await attachmentCreator({
|
||||
name,
|
||||
type,
|
||||
@@ -189,9 +201,11 @@ export default async function documentImporter({
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
text = text.replace(uri, attachment.redirectUrl);
|
||||
}
|
||||
|
||||
return { text, title };
|
||||
return {
|
||||
text,
|
||||
title,
|
||||
};
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
// @flow
|
||||
import { Document, Attachment, Collection, User, Event } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
import parseAttachmentIds from "../utils/parseAttachmentIds";
|
||||
|
||||
async function copyAttachments(document: Document, options) {
|
||||
let text = document.text;
|
||||
const documentId = document.id;
|
||||
|
||||
// find any image attachments that are in this documents text
|
||||
const attachmentIds = parseAttachmentIds(text);
|
||||
|
||||
for (const id of attachmentIds) {
|
||||
const existing = await Attachment.findOne({
|
||||
where: {
|
||||
teamId: document.teamId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
// if the image attachment was originally uploaded to another document
|
||||
// (this can happen in various ways, copy/paste, or duplicate for example)
|
||||
// then create a new attachment pointed to this doc and update the reference
|
||||
// in the text so that it gets the moved documents permissions
|
||||
if (existing && existing.documentId !== documentId) {
|
||||
const { id, ...rest } = existing.dataValues;
|
||||
const attachment = await Attachment.create(
|
||||
{
|
||||
...rest,
|
||||
documentId,
|
||||
},
|
||||
options
|
||||
);
|
||||
text = text.replace(existing.redirectUrl, attachment.redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export default async function documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId,
|
||||
parentDocumentId = null, // convert undefined to null so parentId comparison treats them as equal
|
||||
index,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
document: Document,
|
||||
collectionId: string,
|
||||
parentDocumentId?: string,
|
||||
index?: number,
|
||||
ip: string,
|
||||
}) {
|
||||
let transaction;
|
||||
const collectionChanged = collectionId !== document.collectionId;
|
||||
const result = { collections: [], documents: [], collectionChanged };
|
||||
|
||||
if (document.template) {
|
||||
if (!collectionChanged) {
|
||||
return result;
|
||||
}
|
||||
|
||||
document.collectionId = collectionId;
|
||||
document.parentDocumentId = null;
|
||||
document.lastModifiedById = user.id;
|
||||
document.updatedBy = user;
|
||||
|
||||
await document.save();
|
||||
result.documents.push(document);
|
||||
} else {
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
// remove from original collection
|
||||
const collection = await Collection.findByPk(document.collectionId, {
|
||||
transaction,
|
||||
paranoid: false,
|
||||
});
|
||||
const [
|
||||
documentJson,
|
||||
fromIndex,
|
||||
] = (await collection.removeDocumentInStructure(document, {
|
||||
save: false,
|
||||
})) || [undefined, index];
|
||||
|
||||
// if we're reordering from within the same parent
|
||||
// the original and destination collection are the same,
|
||||
// so when the initial item is removed above, the list will reduce by 1.
|
||||
// We need to compensate for this when reordering
|
||||
const toIndex =
|
||||
index !== undefined &&
|
||||
document.parentDocumentId === parentDocumentId &&
|
||||
document.collectionId === collectionId &&
|
||||
fromIndex < index
|
||||
? index - 1
|
||||
: index;
|
||||
|
||||
// if the collection is the same then it will get saved below, this
|
||||
// line prevents a pointless intermediate save from occurring.
|
||||
if (collectionChanged) {
|
||||
await collection.save({ transaction });
|
||||
|
||||
document.text = await copyAttachments(document, { transaction });
|
||||
}
|
||||
|
||||
// add to new collection (may be the same)
|
||||
document.collectionId = collectionId;
|
||||
document.parentDocumentId = parentDocumentId;
|
||||
document.lastModifiedById = user.id;
|
||||
document.updatedBy = user;
|
||||
|
||||
const newCollection: Collection = collectionChanged
|
||||
? await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId, { transaction })
|
||||
: collection;
|
||||
await newCollection.addDocumentToStructure(document, toIndex, {
|
||||
documentJson,
|
||||
});
|
||||
result.collections.push(collection);
|
||||
|
||||
// if collection does not remain the same loop through children and change their
|
||||
// collectionId and move any attachments they may have too. This includes
|
||||
// archived children, otherwise their collection would be wrong once restored.
|
||||
if (collectionChanged) {
|
||||
result.collections.push(newCollection);
|
||||
|
||||
const loopChildren = async (documentId) => {
|
||||
const childDocuments = await Document.findAll({
|
||||
where: { parentDocumentId: documentId },
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
childDocuments.map(async (child) => {
|
||||
await loopChildren(child.id);
|
||||
child.text = await copyAttachments(child, { transaction });
|
||||
child.collectionId = collectionId;
|
||||
await child.save();
|
||||
child.collection = newCollection;
|
||||
result.documents.push(child);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
await loopChildren(document.id);
|
||||
}
|
||||
|
||||
await document.save({ transaction });
|
||||
|
||||
document.collection = newCollection;
|
||||
result.documents.push(document);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await Event.create({
|
||||
name: "documents.move",
|
||||
actorId: user.id,
|
||||
documentId: document.id,
|
||||
collectionId,
|
||||
teamId: document.teamId,
|
||||
data: {
|
||||
title: document.title,
|
||||
collectionIds: result.collections.map((c) => c.id),
|
||||
documentIds: result.documents.map((d) => d.id),
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
// we need to send all updated models back to the client
|
||||
return result;
|
||||
}
|
||||
@@ -1,47 +1,45 @@
|
||||
// @flow
|
||||
import { Attachment } from "../models";
|
||||
import { Attachment } from "@server/models";
|
||||
import {
|
||||
buildDocument,
|
||||
buildAttachment,
|
||||
buildCollection,
|
||||
buildUser,
|
||||
} from "../test/factories";
|
||||
import { flushdb, seed } from "../test/support";
|
||||
import parseAttachmentIds from "../utils/parseAttachmentIds";
|
||||
} from "@server/test/factories";
|
||||
import { flushdb, seed } from "@server/test/support";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import documentMover from "./documentMover";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("documentMover", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should move within a collection", async () => {
|
||||
const { document, user, collection } = await seed();
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId: collection.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not error when not in source collection documentStructure", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({ teamId: user.teamId });
|
||||
const document = await buildDocument({ collectionId: collection.id });
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
const document = await buildDocument({
|
||||
collectionId: collection.id,
|
||||
});
|
||||
await document.archive();
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId: collection.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(1);
|
||||
});
|
||||
@@ -57,7 +55,6 @@ describe("documentMover", () => {
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
@@ -66,12 +63,13 @@ describe("documentMover", () => {
|
||||
index: 0,
|
||||
ip,
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'documentStructure' does not exist on typ... Remove this comment to see the full error message
|
||||
expect(response.collections[0].documentStructure[0].children[0].id).toBe(
|
||||
newDocument.id
|
||||
);
|
||||
expect(response.collections.length).toEqual(1);
|
||||
expect(response.documents.length).toEqual(1);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message
|
||||
expect(response.documents[0].collection.id).toEqual(collection.id);
|
||||
});
|
||||
|
||||
@@ -89,7 +87,6 @@ describe("documentMover", () => {
|
||||
text: "content",
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
const response = await documentMover({
|
||||
user,
|
||||
document,
|
||||
@@ -98,23 +95,25 @@ describe("documentMover", () => {
|
||||
index: 0,
|
||||
ip,
|
||||
});
|
||||
|
||||
// check document ids where updated
|
||||
await newDocument.reload();
|
||||
expect(newDocument.collectionId).toBe(newCollection.id);
|
||||
|
||||
await document.reload();
|
||||
expect(document.collectionId).toBe(newCollection.id);
|
||||
|
||||
// check collection structure updated
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
expect(response.collections[0].id).toBe(collection.id);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
expect(response.collections[1].id).toBe(newCollection.id);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'documentStructure' does not exist on typ... Remove this comment to see the full error message
|
||||
expect(response.collections[1].documentStructure[0].children[0].id).toBe(
|
||||
newDocument.id
|
||||
);
|
||||
expect(response.collections.length).toEqual(2);
|
||||
expect(response.documents.length).toEqual(2);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message
|
||||
expect(response.documents[0].collection.id).toEqual(newCollection.id);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'neve... Remove this comment to see the full error message
|
||||
expect(response.documents[1].collection.id).toEqual(newCollection.id);
|
||||
});
|
||||
|
||||
@@ -136,7 +135,6 @@ describe("documentMover", () => {
|
||||
text: `content `,
|
||||
});
|
||||
await collection.addDocumentToStructure(newDocument);
|
||||
|
||||
await documentMover({
|
||||
user,
|
||||
document,
|
||||
@@ -145,17 +143,14 @@ describe("documentMover", () => {
|
||||
index: 0,
|
||||
ip,
|
||||
});
|
||||
|
||||
// check document ids where updated
|
||||
await newDocument.reload();
|
||||
expect(newDocument.collectionId).toBe(newCollection.id);
|
||||
|
||||
// check new attachment was created pointint to same key
|
||||
const attachmentIds = parseAttachmentIds(newDocument.text);
|
||||
const newAttachment = await Attachment.findByPk(attachmentIds[0]);
|
||||
expect(newAttachment.documentId).toBe(newDocument.id);
|
||||
expect(newAttachment.key).toBe(attachment.key);
|
||||
|
||||
await document.reload();
|
||||
expect(document.collectionId).toBe(newCollection.id);
|
||||
});
|
||||
222
server/commands/documentMover.ts
Normal file
222
server/commands/documentMover.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Document, Attachment, Collection, User, Event } from "@server/models";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
|
||||
async function copyAttachments(document: Document, options) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
let text = document.text;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
const documentId = document.id;
|
||||
// find any image attachments that are in this documents text
|
||||
const attachmentIds = parseAttachmentIds(text);
|
||||
|
||||
for (const id of attachmentIds) {
|
||||
const existing = await Attachment.findOne({
|
||||
where: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
// if the image attachment was originally uploaded to another document
|
||||
// (this can happen in various ways, copy/paste, or duplicate for example)
|
||||
// then create a new attachment pointed to this doc and update the reference
|
||||
// in the text so that it gets the moved documents permissions
|
||||
if (existing && existing.documentId !== documentId) {
|
||||
const { id, ...rest } = existing.dataValues;
|
||||
const attachment = await Attachment.create(
|
||||
{ ...rest, documentId },
|
||||
options
|
||||
);
|
||||
text = text.replace(existing.redirectUrl, attachment.redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export default async function documentMover({
|
||||
user,
|
||||
document,
|
||||
collectionId,
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type 'string'.
|
||||
parentDocumentId = null,
|
||||
// convert undefined to null so parentId comparison treats them as equal
|
||||
index,
|
||||
ip,
|
||||
}: {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
document: Document;
|
||||
collectionId: string;
|
||||
parentDocumentId?: string;
|
||||
index?: number;
|
||||
ip: string;
|
||||
}) {
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'transaction' implicitly has type 'any' i... Remove this comment to see the full error message
|
||||
let transaction;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
const collectionChanged = collectionId !== document.collectionId;
|
||||
const result = {
|
||||
collections: [],
|
||||
documents: [],
|
||||
collectionChanged,
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'template' does not exist on type 'Docume... Remove this comment to see the full error message
|
||||
if (document.template) {
|
||||
if (!collectionChanged) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId = collectionId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId = null;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastModifiedById' does not exist on type... Remove this comment to see the full error message
|
||||
document.lastModifiedById = user.id;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedBy' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
document.updatedBy = user;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save();
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message
|
||||
result.documents.push(document);
|
||||
} else {
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
// remove from original collection
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
const collection = await Collection.findByPk(document.collectionId, {
|
||||
transaction,
|
||||
paranoid: false,
|
||||
});
|
||||
const [
|
||||
documentJson,
|
||||
fromIndex,
|
||||
] = (await collection.removeDocumentInStructure(document, {
|
||||
save: false,
|
||||
})) || [undefined, index];
|
||||
// if we're reordering from within the same parent
|
||||
// the original and destination collection are the same,
|
||||
// so when the initial item is removed above, the list will reduce by 1.
|
||||
// We need to compensate for this when reordering
|
||||
const toIndex =
|
||||
index !== undefined &&
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId === parentDocumentId &&
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId === collectionId &&
|
||||
fromIndex < index
|
||||
? index - 1
|
||||
: index;
|
||||
|
||||
// if the collection is the same then it will get saved below, this
|
||||
// line prevents a pointless intermediate save from occurring.
|
||||
if (collectionChanged) {
|
||||
await collection.save({
|
||||
transaction,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
document.text = await copyAttachments(document, {
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
// add to new collection (may be the same)
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId = collectionId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId = parentDocumentId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastModifiedById' does not exist on type... Remove this comment to see the full error message
|
||||
document.lastModifiedById = user.id;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedBy' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
document.updatedBy = user;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
const newCollection: Collection = collectionChanged
|
||||
? await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId, {
|
||||
transaction,
|
||||
})
|
||||
: collection;
|
||||
await newCollection.addDocumentToStructure(document, toIndex, {
|
||||
documentJson,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
result.collections.push(collection);
|
||||
|
||||
// if collection does not remain the same loop through children and change their
|
||||
// collectionId and move any attachments they may have too. This includes
|
||||
// archived children, otherwise their collection would be wrong once restored.
|
||||
if (collectionChanged) {
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
result.collections.push(newCollection);
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
const loopChildren = async (documentId) => {
|
||||
const childDocuments = await Document.findAll({
|
||||
where: {
|
||||
parentDocumentId: documentId,
|
||||
},
|
||||
});
|
||||
await Promise.all(
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type.
|
||||
childDocuments.map(async (child) => {
|
||||
await loopChildren(child.id);
|
||||
child.text = await copyAttachments(child, {
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
});
|
||||
child.collectionId = collectionId;
|
||||
await child.save();
|
||||
child.collection = newCollection;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
||||
result.documents.push(child);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
await loopChildren(document.id);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save({
|
||||
transaction,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
|
||||
document.collection = newCollection;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message
|
||||
result.documents.push(document);
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await Event.create({
|
||||
name: "documents.move",
|
||||
actorId: user.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
collectionId,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
data: {
|
||||
title: document.title,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
collectionIds: result.collections.map((c) => c.id),
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
|
||||
documentIds: result.documents.map((d) => d.id),
|
||||
},
|
||||
ip,
|
||||
});
|
||||
// we need to send all updated models back to the client
|
||||
return result;
|
||||
}
|
||||
@@ -1,39 +1,41 @@
|
||||
// @flow
|
||||
import { subDays } from "date-fns";
|
||||
import { Attachment, Document } from "../models";
|
||||
import { buildAttachment, buildDocument } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Attachment, Document } from "@server/models";
|
||||
import { buildAttachment, buildDocument } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import documentPermanentDeleter from "./documentPermanentDeleter";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
deleteObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("documentPermanentDeleter", () => {
|
||||
it("should destroy documents", async () => {
|
||||
const document = await buildDocument({
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should error when trying to destroy undeleted documents", async () => {
|
||||
const document = await buildDocument({
|
||||
publishedAt: new Date(),
|
||||
});
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await documentPermanentDeleter([document]);
|
||||
} catch (err) {
|
||||
@@ -50,20 +52,20 @@ describe("documentPermanentDeleter", () => {
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
const attachment = await buildAttachment({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
document.text = ``;
|
||||
await document.save();
|
||||
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should handle unknown attachment ids", async () => {
|
||||
@@ -71,53 +73,51 @@ describe("documentPermanentDeleter", () => {
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
const attachment = await buildAttachment({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
document.text = ``;
|
||||
await document.save();
|
||||
|
||||
// remove attachment so it no longer exists in the database, this is also
|
||||
// representative of a corrupt attachment id in the doc or the regex returning
|
||||
// an incorrect string
|
||||
await attachment.destroy({ force: true });
|
||||
|
||||
await attachment.destroy({
|
||||
force: true,
|
||||
});
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not destroy attachments referenced in other documents", async () => {
|
||||
const document1 = await buildDocument();
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: document1.teamId,
|
||||
publishedAt: subDays(new Date(), 90),
|
||||
deletedAt: subDays(new Date(), 60),
|
||||
});
|
||||
|
||||
const attachment = await buildAttachment({
|
||||
teamId: document1.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
document1.text = ``;
|
||||
await document1.save();
|
||||
|
||||
document.text = ``;
|
||||
await document.save();
|
||||
|
||||
expect(await Attachment.count()).toEqual(1);
|
||||
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(await Attachment.count()).toEqual(1);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
// @flow
|
||||
import Logger from "../logging/logger";
|
||||
import { Document, Attachment } from "../models";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Document, Attachment } from "@server/models";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import { sequelize } from "../sequelize";
|
||||
import parseAttachmentIds from "../utils/parseAttachmentIds";
|
||||
|
||||
export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'deletedAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
const activeDocument = documents.find((doc) => !doc.deletedAt);
|
||||
|
||||
if (activeDocument) {
|
||||
throw new Error(
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
`Cannot permanently delete ${activeDocument.id} document. Please delete it and try again.`
|
||||
);
|
||||
}
|
||||
@@ -22,13 +23,16 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
`;
|
||||
|
||||
for (const document of documents) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
const attachmentIds = parseAttachmentIds(document.text);
|
||||
|
||||
for (const attachmentId of attachmentIds) {
|
||||
const [{ count }] = await sequelize.query(query, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
query: attachmentId,
|
||||
},
|
||||
@@ -37,6 +41,7 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
if (parseInt(count) === 0) {
|
||||
const attachment = await Attachment.findOne({
|
||||
where: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
id: attachmentId,
|
||||
},
|
||||
@@ -44,7 +49,6 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
|
||||
if (attachment) {
|
||||
await attachment.destroy();
|
||||
|
||||
Logger.info("commands", `Attachment ${attachmentId} deleted`);
|
||||
} else {
|
||||
Logger.info("commands", `Unknown attachment ${attachmentId} ignored`);
|
||||
@@ -55,6 +59,7 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
|
||||
return Document.scope("withUnpublished").destroy({
|
||||
where: {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
id: documents.map((document) => document.id),
|
||||
},
|
||||
force: true,
|
||||
@@ -1,25 +1,24 @@
|
||||
// @flow
|
||||
import { uniq } from "lodash";
|
||||
import { Node } from "prosemirror-model";
|
||||
import { schema, serializer } from "rich-markdown-editor";
|
||||
import { yDocToProsemirrorJSON } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
import { Document, Event } from "../models";
|
||||
import { Document, Event } from "@server/models";
|
||||
|
||||
export default async function documentUpdater({
|
||||
documentId,
|
||||
ydoc,
|
||||
userId,
|
||||
}: {
|
||||
documentId: string,
|
||||
ydoc: Y.Doc,
|
||||
userId?: string,
|
||||
documentId: string;
|
||||
ydoc: Y.Doc;
|
||||
userId?: string;
|
||||
}) {
|
||||
const document = await Document.findByPk(documentId);
|
||||
const state = Y.encodeStateAsUpdate(ydoc);
|
||||
const node = Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default"));
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
const text = serializer.serialize(node);
|
||||
|
||||
const isUnchanged = document.text === text;
|
||||
const hasMultiplayerState = !!document.state;
|
||||
|
||||
@@ -32,7 +31,6 @@ export default async function documentUpdater({
|
||||
const pudIds = Array.from(pud.clients.values());
|
||||
const existingIds = document.collaboratorIds;
|
||||
const collaboratorIds = uniq([...pudIds, ...existingIds]);
|
||||
|
||||
await Document.scope("withUnpublished").update(
|
||||
{
|
||||
text,
|
||||
@@ -1,31 +0,0 @@
|
||||
// @flow
|
||||
import { FileOperation, Event, User } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function fileOperationDeleter(
|
||||
fileOp: FileOperation,
|
||||
user: User,
|
||||
ip: string
|
||||
) {
|
||||
let transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
await fileOp.destroy({ transaction });
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "fileOperations.delete",
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: fileOp.dataValues,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import { FileOperation } from "../models";
|
||||
import { buildAdmin, buildFileOperation } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { FileOperation } from "@server/models";
|
||||
import { buildAdmin, buildFileOperation } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import fileOperationDeleter from "./fileOperationDeleter";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
deleteObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("fileOperationDeleter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -23,9 +23,7 @@ describe("fileOperationDeleter", () => {
|
||||
userId: admin.id,
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
|
||||
await fileOperationDeleter(fileOp, admin, ip);
|
||||
|
||||
expect(await FileOperation.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
34
server/commands/fileOperationDeleter.ts
Normal file
34
server/commands/fileOperationDeleter.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FileOperation, Event, User } from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function fileOperationDeleter(
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'FileOperation' refers to a value, but is being us... Remove this comment to see the full error message
|
||||
fileOp: FileOperation,
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User,
|
||||
ip: string
|
||||
) {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
await fileOp.destroy({
|
||||
transaction,
|
||||
});
|
||||
await Event.create(
|
||||
{
|
||||
name: "fileOperations.delete",
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
data: fileOp.dataValues,
|
||||
ip,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
// @flow
|
||||
import { Event } from "../models";
|
||||
import { buildDocument, buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import revisionCreator from "./revisionCreator";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("revisionCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -15,10 +13,12 @@ describe("revisionCreator", () => {
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const revision = await revisionCreator({ document, user, ip });
|
||||
const revision = await revisionCreator({
|
||||
document,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
const event = await Event.findOne();
|
||||
|
||||
expect(revision.documentId).toEqual(document.id);
|
||||
expect(revision.userId).toEqual(user.id);
|
||||
expect(event.name).toEqual("revisions.create");
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { Document, User, Event, Revision } from "../models";
|
||||
import { Document, User, Event, Revision } from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function revisionCreator({
|
||||
@@ -7,38 +6,42 @@ export default async function revisionCreator({
|
||||
user,
|
||||
ip,
|
||||
}: {
|
||||
document: Document,
|
||||
user: User,
|
||||
ip?: string,
|
||||
document: Document;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
ip?: string;
|
||||
}) {
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
const revision = await Revision.createFromDocument(document, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "revisions.create",
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
modelId: revision.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedAt' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
createdAt: document.updatedAt,
|
||||
ip: ip || user.lastActiveIp,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
await transaction.commit();
|
||||
|
||||
return revision;
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
// @flow
|
||||
import { buildTeam } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import teamCreator from "./teamCreator";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { putObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
putObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("teamCreator", () => {
|
||||
it("should create team and authentication provider", async () => {
|
||||
const result = await teamCreator({
|
||||
@@ -24,9 +24,7 @@ describe("teamCreator", () => {
|
||||
providerId: "example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const { team, authenticationProvider, isNewTeam } = result;
|
||||
|
||||
expect(authenticationProvider.name).toEqual("google");
|
||||
expect(authenticationProvider.providerId).toEqual("example.com");
|
||||
expect(team.name).toEqual("Test team");
|
||||
@@ -59,7 +57,6 @@ describe("teamCreator", () => {
|
||||
it("should return existing team when within allowed domains", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
const existing = await buildTeam();
|
||||
|
||||
const result = await teamCreator({
|
||||
name: "Updated name",
|
||||
subdomain: "example",
|
||||
@@ -69,40 +66,32 @@ describe("teamCreator", () => {
|
||||
providerId: "allowed-domain.com",
|
||||
},
|
||||
});
|
||||
|
||||
const { team, authenticationProvider, isNewTeam } = result;
|
||||
|
||||
expect(team.id).toEqual(existing.id);
|
||||
expect(team.name).toEqual(existing.name);
|
||||
expect(authenticationProvider.name).toEqual("google");
|
||||
expect(authenticationProvider.providerId).toEqual("allowed-domain.com");
|
||||
expect(isNewTeam).toEqual(false);
|
||||
|
||||
const providers = await team.getAuthenticationProviders();
|
||||
expect(providers.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should return exising team", async () => {
|
||||
delete process.env.DEPLOYMENT;
|
||||
|
||||
const authenticationProvider = {
|
||||
name: "google",
|
||||
providerId: "example.com",
|
||||
};
|
||||
|
||||
const existing = await buildTeam({
|
||||
subdomain: "example",
|
||||
authenticationProviders: [authenticationProvider],
|
||||
});
|
||||
|
||||
const result = await teamCreator({
|
||||
name: "Updated name",
|
||||
subdomain: "example",
|
||||
authenticationProvider,
|
||||
});
|
||||
|
||||
const { team, isNewTeam } = result;
|
||||
|
||||
expect(team.id).toEqual(existing.id);
|
||||
expect(team.name).toEqual(existing.name);
|
||||
expect(team.subdomain).toEqual("example");
|
||||
@@ -1,16 +1,17 @@
|
||||
// @flow
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Team, AuthenticationProvider } from "@server/models";
|
||||
import { getAllowedDomains } from "@server/utils/authentication";
|
||||
import { generateAvatarUrl } from "@server/utils/avatars";
|
||||
import { MaximumTeamsError } from "../errors";
|
||||
import Logger from "../logging/logger";
|
||||
import { Team, AuthenticationProvider } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
import { getAllowedDomains } from "../utils/authentication";
|
||||
import { generateAvatarUrl } from "../utils/avatars";
|
||||
|
||||
type TeamCreatorResult = {|
|
||||
team: Team,
|
||||
authenticationProvider: AuthenticationProvider,
|
||||
isNewTeam: boolean,
|
||||
|};
|
||||
type TeamCreatorResult = {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'AuthenticationProvider' refers to a value, but is... Remove this comment to see the full error message
|
||||
authenticationProvider: AuthenticationProvider;
|
||||
isNewTeam: boolean;
|
||||
};
|
||||
|
||||
export default async function teamCreator({
|
||||
name,
|
||||
@@ -18,16 +19,16 @@ export default async function teamCreator({
|
||||
subdomain,
|
||||
avatarUrl,
|
||||
authenticationProvider,
|
||||
}: {|
|
||||
name: string,
|
||||
domain?: string,
|
||||
subdomain: string,
|
||||
avatarUrl?: string,
|
||||
authenticationProvider: {|
|
||||
name: string,
|
||||
providerId: string,
|
||||
|},
|
||||
|}): Promise<TeamCreatorResult> {
|
||||
}: {
|
||||
name: string;
|
||||
domain?: string;
|
||||
subdomain: string;
|
||||
avatarUrl?: string;
|
||||
authenticationProvider: {
|
||||
name: string;
|
||||
providerId: string;
|
||||
};
|
||||
}): Promise<TeamCreatorResult> {
|
||||
let authP = await AuthenticationProvider.findOne({
|
||||
where: authenticationProvider,
|
||||
include: [
|
||||
@@ -61,7 +62,6 @@ export default async function teamCreator({
|
||||
if (teamCount === 1 && domain && getAllowedDomains().includes(domain)) {
|
||||
const team = await Team.findOne();
|
||||
authP = await team.createAuthenticationProvider(authenticationProvider);
|
||||
|
||||
return {
|
||||
authenticationProvider: authP,
|
||||
team,
|
||||
@@ -70,7 +70,7 @@ export default async function teamCreator({
|
||||
}
|
||||
|
||||
if (teamCount >= 1) {
|
||||
throw new MaximumTeamsError();
|
||||
throw MaximumTeamsError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default async function teamCreator({
|
||||
});
|
||||
}
|
||||
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
let team;
|
||||
|
||||
try {
|
||||
@@ -99,7 +99,6 @@ export default async function teamCreator({
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
@@ -1,25 +1,25 @@
|
||||
// @flow
|
||||
import { subDays } from "date-fns";
|
||||
import { Attachment, User, Document, Collection, Team } from "../models";
|
||||
import { Attachment, User, Document, Collection, Team } from "@server/models";
|
||||
import {
|
||||
buildAttachment,
|
||||
buildUser,
|
||||
buildTeam,
|
||||
buildDocument,
|
||||
} from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
} from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import teamPermanentDeleter from "./teamPermanentDeleter";
|
||||
|
||||
jest.mock("aws-sdk", () => {
|
||||
const mS3 = { deleteObject: jest.fn().mockReturnThis(), promise: jest.fn() };
|
||||
const mS3 = {
|
||||
deleteObject: jest.fn().mockReturnThis(),
|
||||
promise: jest.fn(),
|
||||
};
|
||||
return {
|
||||
S3: jest.fn(() => mS3),
|
||||
Endpoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("teamPermanentDeleter", () => {
|
||||
it("should destroy related data", async () => {
|
||||
const team = await buildTeam({
|
||||
@@ -32,30 +32,42 @@ describe("teamPermanentDeleter", () => {
|
||||
teamId: team.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await teamPermanentDeleter(team);
|
||||
|
||||
expect(await Team.count()).toEqual(0);
|
||||
expect(await User.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await Collection.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not destroy unrelated data", async () => {
|
||||
const team = await buildTeam({
|
||||
deletedAt: subDays(new Date(), 90),
|
||||
});
|
||||
|
||||
await buildUser();
|
||||
await buildTeam();
|
||||
await buildDocument();
|
||||
|
||||
await teamPermanentDeleter(team);
|
||||
|
||||
expect(await Team.count()).toEqual(4); // each build command creates a team
|
||||
|
||||
expect(await User.count()).toEqual(2);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(1);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
expect(
|
||||
await Collection.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("should destroy attachments", async () => {
|
||||
@@ -73,20 +85,26 @@ describe("teamPermanentDeleter", () => {
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
});
|
||||
|
||||
await teamPermanentDeleter(team);
|
||||
|
||||
expect(await Team.count()).toEqual(0);
|
||||
expect(await User.count()).toEqual(0);
|
||||
expect(await Attachment.count()).toEqual(0);
|
||||
expect(await Document.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(await Collection.unscoped().count({ paranoid: false })).toEqual(0);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
expect(
|
||||
await Collection.unscoped().count({
|
||||
paranoid: false,
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("should error when trying to destroy undeleted team", async () => {
|
||||
const team = await buildTeam();
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await teamPermanentDeleter(team);
|
||||
} catch (err) {
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import Logger from "../logging/logger";
|
||||
import Logger from "@server/logging/logger";
|
||||
import {
|
||||
ApiKey,
|
||||
Attachment,
|
||||
@@ -17,9 +16,10 @@ import {
|
||||
IntegrationAuthentication,
|
||||
SearchQuery,
|
||||
Share,
|
||||
} from "../models";
|
||||
} from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
export default async function teamPermanentDeleter(team: Team) {
|
||||
if (!team.deletedAt) {
|
||||
throw new Error(
|
||||
@@ -31,13 +31,12 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
"commands",
|
||||
`Permanently deleting team ${team.name} (${team.id})`
|
||||
);
|
||||
|
||||
const teamId = team.id;
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'transaction' implicitly has type 'any' i... Remove this comment to see the full error message
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
await Attachment.findAllInBatches(
|
||||
{
|
||||
where: {
|
||||
@@ -46,6 +45,7 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'attachments' implicitly has an 'any' ty... Remove this comment to see the full error message
|
||||
async (attachments, options) => {
|
||||
Logger.info(
|
||||
"commands",
|
||||
@@ -53,13 +53,17 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
options.offset + options.limit
|
||||
}…`
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
attachments.map((attachment) => attachment.destroy({ transaction }))
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'attachment' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
attachments.map((attachment) =>
|
||||
attachment.destroy({
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Destroy user-relation models
|
||||
await User.findAllInBatches(
|
||||
{
|
||||
@@ -70,115 +74,133 @@ export default async function teamPermanentDeleter(team: Team) {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'users' implicitly has an 'any' type.
|
||||
async (users) => {
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
const userIds = users.map((user) => user.id);
|
||||
|
||||
await UserAuthentication.destroy({
|
||||
where: { userId: userIds },
|
||||
where: {
|
||||
userId: userIds,
|
||||
},
|
||||
force: true,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
});
|
||||
|
||||
await ApiKey.destroy({
|
||||
where: { userId: userIds },
|
||||
where: {
|
||||
userId: userIds,
|
||||
},
|
||||
force: true,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'transaction' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Destory team-relation models
|
||||
await AuthenticationProvider.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
// events must be first due to db constraints
|
||||
await Event.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Collection.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Document.unscoped().destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await FileOperation.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Group.unscoped().destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Integration.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await IntegrationAuthentication.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await NotificationSetting.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await SearchQuery.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Share.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await User.destroy({
|
||||
where: { teamId },
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await team.destroy({
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "teams.destroy",
|
||||
modelId: teamId,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import { buildUser, buildTeam, buildInvite } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildUser, buildTeam, buildInvite } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userCreator from "./userCreator";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -14,7 +12,6 @@ describe("userCreator", () => {
|
||||
const existingAuth = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
const newUsername = "tname";
|
||||
|
||||
const result = await userCreator({
|
||||
name: existing.name,
|
||||
email: newEmail,
|
||||
@@ -29,9 +26,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -45,9 +40,7 @@ describe("userCreator", () => {
|
||||
const authentications = await existing.getAuthentications();
|
||||
const existingAuth = authentications[0];
|
||||
const newEmail = "test@example.com";
|
||||
|
||||
await existing.destroy();
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -60,9 +53,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -100,7 +91,6 @@ describe("userCreator", () => {
|
||||
const team = await buildTeam();
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -114,9 +104,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -128,10 +116,11 @@ describe("userCreator", () => {
|
||||
});
|
||||
|
||||
it("should prefer isAdmin argument over defaultUserRole", async () => {
|
||||
const team = await buildTeam({ defaultUserRole: "viewer" });
|
||||
const team = await buildTeam({
|
||||
defaultUserRole: "viewer",
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -146,17 +135,16 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user } = result;
|
||||
|
||||
expect(user.isAdmin).toEqual(true);
|
||||
});
|
||||
|
||||
it("should prefer defaultUserRole when isAdmin is undefined or false", async () => {
|
||||
const team = await buildTeam({ defaultUserRole: "viewer" });
|
||||
const team = await buildTeam({
|
||||
defaultUserRole: "viewer",
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: "Test Name",
|
||||
email: "test@example.com",
|
||||
@@ -170,13 +158,10 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user: tname } = result;
|
||||
|
||||
expect(tname.username).toEqual("tname");
|
||||
expect(tname.isAdmin).toEqual(false);
|
||||
expect(tname.isViewer).toEqual(true);
|
||||
|
||||
const tname2Result = await userCreator({
|
||||
name: "Test2 Name",
|
||||
email: "tes2@example.com",
|
||||
@@ -191,9 +176,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user: tname2 } = tname2Result;
|
||||
|
||||
expect(tname2.username).toEqual("tname2");
|
||||
expect(tname2.isAdmin).toEqual(false);
|
||||
expect(tname2.isViewer).toEqual(true);
|
||||
@@ -201,10 +184,11 @@ describe("userCreator", () => {
|
||||
|
||||
it("should create a user from an invited user", async () => {
|
||||
const team = await buildTeam();
|
||||
const invite = await buildInvite({ teamId: team.id });
|
||||
const invite = await buildInvite({
|
||||
teamId: team.id,
|
||||
});
|
||||
const authenticationProviders = await team.getAuthenticationProviders();
|
||||
const authenticationProvider = authenticationProviders[0];
|
||||
|
||||
const result = await userCreator({
|
||||
name: invite.name,
|
||||
email: invite.email,
|
||||
@@ -217,9 +201,7 @@ describe("userCreator", () => {
|
||||
scopes: ["read"],
|
||||
},
|
||||
});
|
||||
|
||||
const { user, authentication, isNewUser } = result;
|
||||
|
||||
expect(authentication.accessToken).toEqual("123");
|
||||
expect(authentication.scopes.length).toEqual(1);
|
||||
expect(authentication.scopes[0]).toEqual("read");
|
||||
@@ -1,15 +1,15 @@
|
||||
// @flow
|
||||
import Sequelize from "sequelize";
|
||||
import { Event, Team, User, UserAuthentication } from "../models";
|
||||
import { Event, Team, User, UserAuthentication } from "@server/models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
type UserCreatorResult = {|
|
||||
user: User,
|
||||
isNewUser: boolean,
|
||||
authentication: UserAuthentication,
|
||||
|};
|
||||
type UserCreatorResult = {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
isNewUser: boolean;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'UserAuthentication' refers to a value, but is bei... Remove this comment to see the full error message
|
||||
authentication: UserAuthentication;
|
||||
};
|
||||
|
||||
export default async function userCreator({
|
||||
name,
|
||||
@@ -20,22 +20,22 @@ export default async function userCreator({
|
||||
teamId,
|
||||
authentication,
|
||||
ip,
|
||||
}: {|
|
||||
name: string,
|
||||
email: string,
|
||||
username?: string,
|
||||
isAdmin?: boolean,
|
||||
avatarUrl?: string,
|
||||
teamId: string,
|
||||
ip: string,
|
||||
authentication: {|
|
||||
authenticationProviderId: string,
|
||||
providerId: string,
|
||||
scopes: string[],
|
||||
accessToken?: string,
|
||||
refreshToken?: string,
|
||||
|},
|
||||
|}): Promise<UserCreatorResult> {
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
username?: string;
|
||||
isAdmin?: boolean;
|
||||
avatarUrl?: string;
|
||||
teamId: string;
|
||||
ip: string;
|
||||
authentication: {
|
||||
authenticationProviderId: string;
|
||||
providerId: string;
|
||||
scopes: string[];
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
};
|
||||
}): Promise<UserCreatorResult> {
|
||||
const { authenticationProviderId, providerId, ...rest } = authentication;
|
||||
const auth = await UserAuthentication.findOne({
|
||||
where: {
|
||||
@@ -65,10 +65,16 @@ export default async function userCreator({
|
||||
}
|
||||
|
||||
if (user) {
|
||||
await user.update({ email, username });
|
||||
await user.update({
|
||||
email,
|
||||
username,
|
||||
});
|
||||
await auth.update(rest);
|
||||
|
||||
return { user, authentication: auth, isNewUser: false };
|
||||
return {
|
||||
user,
|
||||
authentication: auth,
|
||||
isNewUser: false,
|
||||
};
|
||||
}
|
||||
|
||||
// We found an authentication record, but the associated user was deleted or
|
||||
@@ -100,15 +106,18 @@ export default async function userCreator({
|
||||
// We have an existing invite for his user, so we need to update it with our
|
||||
// new details and link up the authentication method
|
||||
if (invite && !invite.authentications.length) {
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
let auth;
|
||||
|
||||
try {
|
||||
await invite.update(
|
||||
{
|
||||
name,
|
||||
avatarUrl,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
auth = await invite.createAuthentication(authentication, {
|
||||
transaction,
|
||||
@@ -119,18 +128,21 @@ export default async function userCreator({
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { user: invite, authentication: auth, isNewUser: true };
|
||||
return {
|
||||
user: invite,
|
||||
authentication: auth,
|
||||
isNewUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
// No auth, no user – this is an entirely new sign in.
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const { defaultUserRole } = await Team.findByPk(teamId, {
|
||||
attributes: ["defaultUserRole"],
|
||||
transaction,
|
||||
});
|
||||
|
||||
const user = await User.create(
|
||||
{
|
||||
name,
|
||||
@@ -1,16 +1,13 @@
|
||||
// @flow
|
||||
import { buildUser, buildAdmin } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { buildUser, buildAdmin } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userDestroyer from "./userDestroyer";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userDestroyer", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should prevent last user from deleting account", async () => {
|
||||
const user = await buildUser();
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -22,13 +19,15 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.message).toContain("Cannot delete last user");
|
||||
});
|
||||
|
||||
it("should prevent last admin from deleting account", async () => {
|
||||
const user = await buildAdmin();
|
||||
await buildUser({ teamId: user.teamId });
|
||||
|
||||
await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -40,13 +39,15 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error && error.message).toContain("Cannot delete account");
|
||||
});
|
||||
|
||||
it("should not prevent multiple admin from deleting account", async () => {
|
||||
const actor = await buildAdmin();
|
||||
const user = await buildAdmin({ teamId: actor.teamId });
|
||||
|
||||
const user = await buildAdmin({
|
||||
teamId: actor.teamId,
|
||||
});
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -58,14 +59,16 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeFalsy();
|
||||
expect(user.deletedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not prevent last non-admin from deleting account", async () => {
|
||||
const user = await buildUser();
|
||||
await buildUser({ teamId: user.teamId });
|
||||
|
||||
await buildUser({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
let error;
|
||||
|
||||
try {
|
||||
@@ -77,6 +80,7 @@ describe("userDestroyer", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeFalsy();
|
||||
expect(user.deletedAt).toBeTruthy();
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { Event, User } from "@server/models";
|
||||
import { ValidationError } from "../errors";
|
||||
import { Event, User } from "../models";
|
||||
import { Op, sequelize } from "../sequelize";
|
||||
|
||||
export default async function userDestroyer({
|
||||
@@ -8,12 +7,13 @@ export default async function userDestroyer({
|
||||
actor,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
actor: User,
|
||||
ip: string,
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
actor: User;
|
||||
ip: string;
|
||||
}) {
|
||||
const { teamId } = user;
|
||||
|
||||
const usersCount = await User.count({
|
||||
where: {
|
||||
teamId,
|
||||
@@ -21,7 +21,7 @@ export default async function userDestroyer({
|
||||
});
|
||||
|
||||
if (usersCount === 1) {
|
||||
throw new ValidationError("Cannot delete last user on the team.");
|
||||
throw ValidationError("Cannot delete last user on the team.");
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
@@ -29,34 +29,41 @@ export default async function userDestroyer({
|
||||
where: {
|
||||
isAdmin: true,
|
||||
teamId,
|
||||
id: { [Op.ne]: user.id },
|
||||
id: {
|
||||
[Op.ne]: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (otherAdminsCount === 0) {
|
||||
throw new ValidationError(
|
||||
throw ValidationError(
|
||||
"Cannot delete account as only admin. Please make another user admin and try again."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let transaction = await sequelize.transaction();
|
||||
const transaction = await sequelize.transaction();
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await user.destroy({ transaction });
|
||||
response = await user.destroy({
|
||||
transaction,
|
||||
});
|
||||
await Event.create(
|
||||
{
|
||||
name: "users.delete",
|
||||
actorId: actor.id,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
@@ -1,63 +0,0 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import userInviter from "./userInviter";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userInviter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should return sent invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: "test@example.com", name: "Test" }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should filter empty invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: " ", name: "Test" }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should filter obviously bunk emails", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: "notanemail", name: "Test" }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not send duplicates", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
{ email: "the@same.com", name: "Test" },
|
||||
{ email: "the@SAME.COM", name: "Test" },
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not send invites to existing team members", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [{ email: user.email, name: user.name }],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
93
server/commands/userInviter.test.ts
Normal file
93
server/commands/userInviter.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userInviter from "./userInviter";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
describe("userInviter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should return sent invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "test@example.com",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should filter empty invites", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: " ",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should filter obviously bunk emails", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "notanemail",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should not send duplicates", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "the@same.com",
|
||||
name: "Test",
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: strin... Remove this comment to see the full error message
|
||||
{
|
||||
email: "the@SAME.COM",
|
||||
name: "Test",
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not send invites to existing team members", async () => {
|
||||
const user = await buildUser();
|
||||
const response = await userInviter({
|
||||
invites: [
|
||||
// @ts-expect-error ts-migrate(2741) FIXME: Property 'role' is missing in type '{ email: any; ... Remove this comment to see the full error message
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
],
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
expect(response.sent.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import { uniqBy } from "lodash";
|
||||
import type { Role } from "shared/types";
|
||||
import { Role } from "@shared/types";
|
||||
import { User, Event, Team } from "@server/models";
|
||||
import mailer from "../mailer";
|
||||
import { User, Event, Team } from "../models";
|
||||
|
||||
type Invite = {
|
||||
name: string,
|
||||
email: string,
|
||||
role: Role,
|
||||
name: string;
|
||||
email: string;
|
||||
role: Role;
|
||||
};
|
||||
|
||||
export default async function userInviter({
|
||||
@@ -15,17 +14,20 @@ export default async function userInviter({
|
||||
invites,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
invites: Invite[],
|
||||
ip: string,
|
||||
}): Promise<{ sent: Invite[], users: User[] }> {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
invites: Invite[];
|
||||
ip: string;
|
||||
}): Promise<{
|
||||
sent: Invite[];
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
users: User[];
|
||||
}> {
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
|
||||
// filter out empties and obvious non-emails
|
||||
const compactedInvites = invites.filter(
|
||||
(invite) => !!invite.email.trim() && invite.email.match("@")
|
||||
);
|
||||
|
||||
// normalize to lowercase and remove duplicates
|
||||
const normalizedInvites = uniqBy(
|
||||
compactedInvites.map((invite) => ({
|
||||
@@ -34,7 +36,6 @@ export default async function userInviter({
|
||||
})),
|
||||
"email"
|
||||
);
|
||||
|
||||
// filter out any existing users in the system
|
||||
const emails = normalizedInvites.map((invite) => invite.email);
|
||||
const existingUsers = await User.findAll({
|
||||
@@ -43,12 +44,12 @@ export default async function userInviter({
|
||||
email: emails,
|
||||
},
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
const existingEmails = existingUsers.map((user) => user.email);
|
||||
const filteredInvites = normalizedInvites.filter(
|
||||
(invite) => !existingEmails.includes(invite.email)
|
||||
);
|
||||
|
||||
let users = [];
|
||||
const users = [];
|
||||
|
||||
// send and record remaining invites
|
||||
for (const invite of filteredInvites) {
|
||||
@@ -61,7 +62,6 @@ export default async function userInviter({
|
||||
isViewer: invite.role === "viewer",
|
||||
});
|
||||
users.push(newUser);
|
||||
|
||||
await Event.create({
|
||||
name: "users.invite",
|
||||
actorId: user.id,
|
||||
@@ -73,7 +73,6 @@ export default async function userInviter({
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
await mailer.sendTemplate("invite", {
|
||||
to: invite.email,
|
||||
name: invite.name,
|
||||
@@ -84,5 +83,8 @@ export default async function userInviter({
|
||||
});
|
||||
}
|
||||
|
||||
return { sent: filteredInvites, users };
|
||||
return {
|
||||
sent: filteredInvites,
|
||||
users,
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { GroupUser } from "../models";
|
||||
import { buildGroup, buildAdmin, buildUser } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { GroupUser } from "@server/models";
|
||||
import { buildGroup, buildAdmin, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import userSuspender from "./userSuspender";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("userSuspender", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
@@ -28,7 +26,9 @@ describe("userSuspender", () => {
|
||||
|
||||
it("should suspend the user", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({ teamId: admin.teamId });
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
await userSuspender({
|
||||
actorId: admin.id,
|
||||
user,
|
||||
@@ -40,11 +40,17 @@ describe("userSuspender", () => {
|
||||
|
||||
it("should remove group memberships", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({ teamId: admin.teamId });
|
||||
const group = await buildGroup({ teamId: user.teamId });
|
||||
|
||||
await group.addUser(user, { through: { createdById: user.id } });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
const group = await buildGroup({
|
||||
teamId: user.teamId,
|
||||
});
|
||||
await group.addUser(user, {
|
||||
through: {
|
||||
createdById: user.id,
|
||||
},
|
||||
});
|
||||
await userSuspender({
|
||||
actorId: admin.id,
|
||||
user,
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import { Transaction } from "sequelize";
|
||||
import { User, Event, GroupUser } from "@server/models";
|
||||
import { ValidationError } from "../errors";
|
||||
import { User, Event, GroupUser } from "../models";
|
||||
import { sequelize } from "../sequelize";
|
||||
|
||||
export default async function userSuspender({
|
||||
@@ -9,12 +8,13 @@ export default async function userSuspender({
|
||||
actorId,
|
||||
ip,
|
||||
}: {
|
||||
user: User,
|
||||
actorId: string,
|
||||
ip: string,
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
actorId: string;
|
||||
ip: string;
|
||||
}): Promise<void> {
|
||||
if (user.id === actorId) {
|
||||
throw new ValidationError("Unable to suspend the current user");
|
||||
throw ValidationError("Unable to suspend the current user");
|
||||
}
|
||||
|
||||
await sequelize.transaction(async (transaction: Transaction) => {
|
||||
@@ -23,21 +23,30 @@ export default async function userSuspender({
|
||||
suspendedById: actorId,
|
||||
suspendedAt: new Date(),
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
await GroupUser.destroy({ where: { userId: user.id }, transaction });
|
||||
|
||||
await GroupUser.destroy({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
await Event.create(
|
||||
{
|
||||
name: "users.suspend",
|
||||
actorId,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
data: { name: user.name },
|
||||
data: {
|
||||
name: user.name,
|
||||
},
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { User, Collection } from "../models";
|
||||
import { User, Collection } from "@server/models";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
import EmailTemplate from "./components/EmailLayout";
|
||||
@@ -10,10 +9,12 @@ import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
export type Props = {
|
||||
actor: User,
|
||||
collection: Collection,
|
||||
eventName: string,
|
||||
unsubscribeUrl: string,
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
actor: User;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
collection: Collection;
|
||||
eventName: string;
|
||||
unsubscribeUrl: string;
|
||||
};
|
||||
|
||||
export const collectionNotificationEmailText = ({
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { User, Document, Team, Collection } from "../models";
|
||||
import { User, Document, Team, Collection } from "@server/models";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
import EmailTemplate from "./components/EmailLayout";
|
||||
@@ -10,12 +9,15 @@ import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
export type Props = {
|
||||
actor: User,
|
||||
team: Team,
|
||||
document: Document,
|
||||
collection: Collection,
|
||||
eventName: string,
|
||||
unsubscribeUrl: string,
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
actor: User;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Team' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
team: Team;
|
||||
document: any;
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
collection: Collection;
|
||||
eventName: string;
|
||||
unsubscribeUrl: string;
|
||||
};
|
||||
|
||||
export const documentNotificationEmailText = ({
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
@@ -18,8 +17,8 @@ export const ExportSuccessEmail = ({
|
||||
id,
|
||||
teamUrl,
|
||||
}: {
|
||||
id: string,
|
||||
teamUrl: string,
|
||||
id: string;
|
||||
teamUrl: string;
|
||||
}) => {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
@@ -9,11 +8,11 @@ import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
export type Props = {
|
||||
name: string,
|
||||
actorName: string,
|
||||
actorEmail: string,
|
||||
teamName: string,
|
||||
teamUrl: string,
|
||||
name: string;
|
||||
actorName: string;
|
||||
actorEmail: string;
|
||||
teamName: string;
|
||||
teamUrl: string;
|
||||
};
|
||||
|
||||
export const inviteEmailText = ({
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
@@ -9,8 +8,8 @@ import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
export type Props = {
|
||||
token: string,
|
||||
teamUrl: string,
|
||||
token: string;
|
||||
teamUrl: string;
|
||||
};
|
||||
|
||||
export const signinEmailText = ({ token, teamUrl }: Props) => `
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
@@ -9,7 +8,7 @@ import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
export type Props = {
|
||||
teamUrl: string,
|
||||
teamUrl: string;
|
||||
};
|
||||
|
||||
export const welcomeEmailText = ({ teamUrl }: Props) => `
|
||||
@@ -1,11 +1,9 @@
|
||||
// @flow
|
||||
import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
|
||||
import EmptySpace from "./EmptySpace";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default ({ children }: Props) => {
|
||||
@@ -1,7 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = { href: string, children: React.Node };
|
||||
type Props = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const style = {
|
||||
@@ -14,7 +16,6 @@ export default (props: Props) => {
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
return (
|
||||
<a {...props} style={style}>
|
||||
{props.children}
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
import theme from "../../../shared/theme";
|
||||
import theme from "@shared/theme";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default (props: Props) => (
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -9,7 +8,6 @@ const EmptySpace = ({ height }: { height?: number }) => {
|
||||
fontSize: "1px",
|
||||
msoLineHeightRule: "exactly",
|
||||
};
|
||||
|
||||
return (
|
||||
<Table width="100%">
|
||||
<TBody>
|
||||
@@ -18,7 +16,9 @@ const EmptySpace = ({ height }: { height?: number }) => {
|
||||
width="100%"
|
||||
height={`${height}px`}
|
||||
style={style}
|
||||
dangerouslySetInnerHTML={{ __html: " " }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: " ",
|
||||
}}
|
||||
/>
|
||||
</TR>
|
||||
</TBody>
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
import theme from "../../../shared/theme";
|
||||
import { twitterUrl } from "../../../shared/utils/routeHelpers";
|
||||
import theme from "@shared/theme";
|
||||
import { twitterUrl } from "@shared/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
unsubscribeUrl?: string,
|
||||
unsubscribeUrl?: string;
|
||||
};
|
||||
|
||||
export default ({ unsubscribeUrl }: Props) => {
|
||||
@@ -15,26 +14,22 @@ export default ({ unsubscribeUrl }: Props) => {
|
||||
color: theme.slate,
|
||||
fontSize: "14px",
|
||||
};
|
||||
|
||||
const unsubStyle = {
|
||||
padding: "0",
|
||||
color: theme.slate,
|
||||
fontSize: "14px",
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
color: theme.slate,
|
||||
fontWeight: 500,
|
||||
textDecoration: "none",
|
||||
marginRight: "10px",
|
||||
};
|
||||
|
||||
const externalLinkStyle = {
|
||||
color: theme.slate,
|
||||
textDecoration: "none",
|
||||
margin: "0 10px",
|
||||
};
|
||||
|
||||
return (
|
||||
<Table width="100%">
|
||||
<TBody>
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { Table, TBody, TR, TD } from "oy-vey";
|
||||
import * as React from "react";
|
||||
import EmptySpace from "./EmptySpace";
|
||||
@@ -1,13 +1,11 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
const style = {
|
||||
fontWeight: 500,
|
||||
fontSize: "18px",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default ({ children }: Props) => (
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import Koa from "koa";
|
||||
import Router from "koa-router";
|
||||
import { NotFoundError } from "../errors";
|
||||
@@ -9,8 +8,10 @@ const router = new Router();
|
||||
|
||||
router.get("/:type/:format", async (ctx) => {
|
||||
let mailerOutput;
|
||||
let mailer = new Mailer();
|
||||
const mailer = new Mailer();
|
||||
|
||||
mailer.transporter = {
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'data' implicitly has an 'any' type.
|
||||
sendMail: (data) => (mailerOutput = data),
|
||||
};
|
||||
|
||||
@@ -20,20 +21,22 @@ router.get("/:type/:format", async (ctx) => {
|
||||
// break;
|
||||
default:
|
||||
if (Object.getOwnPropertyNames(mailer).includes(ctx.params.type)) {
|
||||
// $FlowIssue flow doesn't like this but we're ok with it
|
||||
mailer[ctx.params.type]("user@example.com");
|
||||
} else throw new NotFoundError("Email template could not be found");
|
||||
} else {
|
||||
throw NotFoundError("Email template could not be found");
|
||||
}
|
||||
}
|
||||
|
||||
if (!mailerOutput) return;
|
||||
|
||||
if (ctx.params.format === "text") {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'never'.
|
||||
ctx.body = mailerOutput.text;
|
||||
} else {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'html' does not exist on type 'never'.
|
||||
ctx.body = mailerOutput.html;
|
||||
}
|
||||
});
|
||||
|
||||
emailPreviews.use(router.routes());
|
||||
|
||||
export default emailPreviews;
|
||||
@@ -1,4 +0,0 @@
|
||||
// @flow
|
||||
require("dotenv").config({ silent: true });
|
||||
|
||||
export default process.env;
|
||||
6
server/env.ts
Normal file
6
server/env.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require("dotenv").config({
|
||||
silent: true,
|
||||
});
|
||||
|
||||
export default process.env;
|
||||
117
server/errors.js
117
server/errors.js
@@ -1,117 +0,0 @@
|
||||
// @flow
|
||||
import httpErrors from "http-errors";
|
||||
import env from "./env";
|
||||
|
||||
export function AuthenticationError(
|
||||
message: string = "Invalid authentication",
|
||||
redirectUrl: string = env.URL
|
||||
) {
|
||||
return httpErrors(401, message, {
|
||||
redirectUrl,
|
||||
id: "authentication_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function AuthorizationError(
|
||||
message: string = "You do not have permission to access this resource"
|
||||
) {
|
||||
return httpErrors(403, message, { id: "permission_required" });
|
||||
}
|
||||
|
||||
export function AdminRequiredError(
|
||||
message: string = "An admin role is required to access this resource"
|
||||
) {
|
||||
return httpErrors(403, message, { id: "admin_required" });
|
||||
}
|
||||
|
||||
export function UserSuspendedError({ adminEmail }: { adminEmail: string }) {
|
||||
return httpErrors(403, "Your access has been suspended by the team admin", {
|
||||
id: "user_suspended",
|
||||
errorData: {
|
||||
adminEmail,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function InvalidRequestError(message: string = "Request invalid") {
|
||||
return httpErrors(400, message, { id: "invalid_request" });
|
||||
}
|
||||
|
||||
export function NotFoundError(message: string = "Resource not found") {
|
||||
return httpErrors(404, message, { id: "not_found" });
|
||||
}
|
||||
|
||||
export function ParamRequiredError(
|
||||
message: string = "Required parameter missing"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "param_required" });
|
||||
}
|
||||
|
||||
export function ValidationError(message: string = "Validation failed") {
|
||||
return httpErrors(400, message, { id: "validation_error" });
|
||||
}
|
||||
|
||||
export function EditorUpdateError(
|
||||
message: string = "The client editor is out of date and must be reloaded"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "editor_update_required" });
|
||||
}
|
||||
|
||||
export function FileImportError(
|
||||
message: string = "The file could not be imported"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "import_error" });
|
||||
}
|
||||
|
||||
export function OAuthStateMismatchError(
|
||||
message: string = "State returned in OAuth flow did not match"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "state_mismatch" });
|
||||
}
|
||||
|
||||
export function MaximumTeamsError(
|
||||
message: string = "The maximum number of teams has been reached"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "maximum_teams" });
|
||||
}
|
||||
|
||||
export function EmailAuthenticationRequiredError(
|
||||
message: string = "User must authenticate with email",
|
||||
redirectUrl: string = env.URL
|
||||
) {
|
||||
return httpErrors(400, message, { redirectUrl, id: "email_auth_required" });
|
||||
}
|
||||
|
||||
export function MicrosoftGraphError(
|
||||
message: string = "Microsoft Graph API did not return required fields"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "graph_error" });
|
||||
}
|
||||
|
||||
export function GoogleWorkspaceRequiredError(
|
||||
message: string = "Google Workspace is required to authenticate"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "google_hd" });
|
||||
}
|
||||
|
||||
export function GoogleWorkspaceInvalidError(
|
||||
message: string = "Google Workspace is invalid"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "hd_not_allowed" });
|
||||
}
|
||||
|
||||
export function OIDCMalformedUserInfoError(
|
||||
message: string = "User profile information malformed"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "malformed_user_info" });
|
||||
}
|
||||
|
||||
export function AuthenticationProviderDisabledError(
|
||||
message: string = "Authentication method has been disabled by an admin",
|
||||
redirectUrl: string = env.URL
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
redirectUrl,
|
||||
id: "authentication_provider_disabled",
|
||||
});
|
||||
}
|
||||
146
server/errors.ts
Normal file
146
server/errors.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import httpErrors from "http-errors";
|
||||
import env from "./env";
|
||||
|
||||
export function AuthenticationError(
|
||||
message = "Invalid authentication",
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
redirectUrl: string = env.URL
|
||||
) {
|
||||
return httpErrors(401, message, {
|
||||
redirectUrl,
|
||||
id: "authentication_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function AuthorizationError(
|
||||
message = "You do not have permission to access this resource"
|
||||
) {
|
||||
return httpErrors(403, message, {
|
||||
id: "permission_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function AdminRequiredError(
|
||||
message = "An admin role is required to access this resource"
|
||||
) {
|
||||
return httpErrors(403, message, {
|
||||
id: "admin_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function UserSuspendedError({ adminEmail }: { adminEmail: string }) {
|
||||
return httpErrors(403, "Your access has been suspended by the team admin", {
|
||||
id: "user_suspended",
|
||||
errorData: {
|
||||
adminEmail,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function InvalidRequestError(message = "Request invalid") {
|
||||
return httpErrors(400, message, {
|
||||
id: "invalid_request",
|
||||
});
|
||||
}
|
||||
|
||||
export function NotFoundError(message = "Resource not found") {
|
||||
return httpErrors(404, message, {
|
||||
id: "not_found",
|
||||
});
|
||||
}
|
||||
|
||||
export function ParamRequiredError(message = "Required parameter missing") {
|
||||
return httpErrors(400, message, {
|
||||
id: "param_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function ValidationError(message = "Validation failed") {
|
||||
return httpErrors(400, message, {
|
||||
id: "validation_error",
|
||||
});
|
||||
}
|
||||
|
||||
export function EditorUpdateError(
|
||||
message = "The client editor is out of date and must be reloaded"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "editor_update_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function FileImportError(message = "The file could not be imported") {
|
||||
return httpErrors(400, message, {
|
||||
id: "import_error",
|
||||
});
|
||||
}
|
||||
|
||||
export function OAuthStateMismatchError(
|
||||
message = "State returned in OAuth flow did not match"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "state_mismatch",
|
||||
});
|
||||
}
|
||||
|
||||
export function MaximumTeamsError(
|
||||
message = "The maximum number of teams has been reached"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "maximum_teams",
|
||||
});
|
||||
}
|
||||
|
||||
export function EmailAuthenticationRequiredError(
|
||||
message = "User must authenticate with email",
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
redirectUrl: string = env.URL
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
redirectUrl,
|
||||
id: "email_auth_required",
|
||||
});
|
||||
}
|
||||
|
||||
export function MicrosoftGraphError(
|
||||
message = "Microsoft Graph API did not return required fields"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "graph_error",
|
||||
});
|
||||
}
|
||||
|
||||
export function GoogleWorkspaceRequiredError(
|
||||
message = "Google Workspace is required to authenticate"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "google_hd",
|
||||
});
|
||||
}
|
||||
|
||||
export function GoogleWorkspaceInvalidError(
|
||||
message = "Google Workspace is invalid"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "hd_not_allowed",
|
||||
});
|
||||
}
|
||||
|
||||
export function OIDCMalformedUserInfoError(
|
||||
message = "User profile information malformed"
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
id: "malformed_user_info",
|
||||
});
|
||||
}
|
||||
|
||||
export function AuthenticationProviderDisabledError(
|
||||
message = "Authentication method has been disabled by an admin",
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
redirectUrl: string = env.URL
|
||||
) {
|
||||
return httpErrors(400, message, {
|
||||
redirectUrl,
|
||||
id: "authentication_provider_disabled",
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import env from "./env"; // eslint-disable-line import/order
|
||||
|
||||
import "./tracing"; // must come before importing any instrumented module
|
||||
|
||||
import http from "http";
|
||||
@@ -37,15 +37,18 @@ const serviceNames = uniq(
|
||||
|
||||
// The number of processes to run, defaults to the number of CPU's available
|
||||
// for the web service, and 1 for collaboration during the beta period.
|
||||
let processCount = env.WEB_CONCURRENCY || undefined;
|
||||
let processCount = env.WEB_CONCURRENCY
|
||||
? parseInt(env.WEB_CONCURRENCY, 10)
|
||||
: undefined;
|
||||
|
||||
if (serviceNames.includes("collaboration")) {
|
||||
if (env.WEB_CONCURRENCY !== 1) {
|
||||
if (processCount !== 1) {
|
||||
Logger.info(
|
||||
"lifecycle",
|
||||
"Note: Restricting process count to 1 due to use of collaborative service"
|
||||
);
|
||||
}
|
||||
|
||||
processCount = 1;
|
||||
}
|
||||
|
||||
@@ -61,18 +64,18 @@ function master() {
|
||||
}
|
||||
|
||||
// This function will only be called in each forked process
|
||||
async function start(id: string, disconnect: () => void) {
|
||||
async function start(id: number, disconnect: () => void) {
|
||||
// If a --port flag is passed then it takes priority over the env variable
|
||||
const normalizedPortFlag = getArg("port", "p");
|
||||
|
||||
const app = new Koa();
|
||||
const server = stoppable(http.createServer(app.callback()));
|
||||
const router = new Router();
|
||||
|
||||
// install basic middleware shared by all services
|
||||
if ((env.DEBUG || "").includes("http")) {
|
||||
app.use(logger((str, args) => Logger.info("http", str)));
|
||||
app.use(logger((str) => Logger.info("http", str)));
|
||||
}
|
||||
|
||||
app.use(compress());
|
||||
app.use(helmet());
|
||||
|
||||
@@ -98,20 +101,18 @@ async function start(id: string, disconnect: () => void) {
|
||||
server.on("error", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
server.on("listening", () => {
|
||||
const address = server.address();
|
||||
|
||||
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
||||
Logger.info("lifecycle", `Listening on http://localhost:${address.port}`);
|
||||
});
|
||||
|
||||
server.listen(normalizedPortFlag || env.PORT || "3000");
|
||||
|
||||
process.once("SIGTERM", shutdown);
|
||||
process.once("SIGINT", shutdown);
|
||||
|
||||
function shutdown() {
|
||||
Logger.info("lifecycle", "Stopping server");
|
||||
|
||||
server.emit("shutdown");
|
||||
server.stop(disconnect);
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
// @flow
|
||||
import chalk from "chalk";
|
||||
import winston from "winston";
|
||||
import env from "../env";
|
||||
import Metrics from "../logging/metrics";
|
||||
import Sentry from "../logging/sentry";
|
||||
import env from "@server/env";
|
||||
import Metrics from "@server/logging/metrics";
|
||||
import Sentry from "@server/logging/sentry";
|
||||
|
||||
const isProduction = env.NODE_ENV === "production";
|
||||
|
||||
type LogCategory =
|
||||
| "lifecycle"
|
||||
| "hocuspocus"
|
||||
@@ -17,8 +15,7 @@ type LogCategory =
|
||||
| "queue"
|
||||
| "database"
|
||||
| "utils";
|
||||
|
||||
type Extra = { [key: string]: any };
|
||||
type Extra = Record<string, any>;
|
||||
|
||||
class Logger {
|
||||
output: any;
|
||||
@@ -77,6 +74,7 @@ class Logger {
|
||||
scope.setExtra(key, extra[key]);
|
||||
scope.setLevel(Sentry.Severity.Warning);
|
||||
}
|
||||
|
||||
Sentry.captureMessage(message);
|
||||
});
|
||||
}
|
||||
@@ -106,14 +104,21 @@ class Logger {
|
||||
scope.setExtra(key, extra[key]);
|
||||
scope.setLevel(Sentry.Severity.Error);
|
||||
}
|
||||
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
}
|
||||
|
||||
if (isProduction) {
|
||||
this.output.error(message, { error: error.message, stack: error.stack });
|
||||
this.output.error(message, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
} else {
|
||||
console.error(message, { error, extra });
|
||||
console.error(message, {
|
||||
error,
|
||||
extra,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
// @flow
|
||||
import ddMetrics from "datadog-metrics";
|
||||
|
||||
class Metrics {
|
||||
enabled: boolean = !!process.env.DD_API_KEY;
|
||||
enabled = !!process.env.DD_API_KEY;
|
||||
|
||||
constructor() {
|
||||
if (!this.enabled) {
|
||||
@@ -24,12 +23,13 @@ class Metrics {
|
||||
return ddMetrics.gauge(key, value, tags);
|
||||
}
|
||||
|
||||
gaugePerInstance(key: string, value: number, tags?: string[] = []): void {
|
||||
gaugePerInstance(key: string, value: number, tags: string[] = []): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceId = process.env.INSTANCE_ID || process.env.HEROKU_DYNO_ID;
|
||||
|
||||
if (!instanceId) {
|
||||
throw new Error(
|
||||
"INSTANCE_ID or HEROKU_DYNO_ID must be set when using DataDog"
|
||||
@@ -39,12 +39,12 @@ class Metrics {
|
||||
return ddMetrics.gauge(key, value, [...tags, `instance:${instanceId}`]);
|
||||
}
|
||||
|
||||
increment(key: string, tags?: { [string]: string }): void {
|
||||
increment(key: string, tags?: Record<string, string>): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ddMetrics.increment(key, tags);
|
||||
return ddMetrics.increment(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as Sentry from "@sentry/node";
|
||||
import env from "../env";
|
||||
import type { ContextWithState } from "../types";
|
||||
import env from "@server/env";
|
||||
import { ContextWithState } from "../types";
|
||||
|
||||
if (env.SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
@@ -22,26 +21,33 @@ if (env.SENTRY_DSN) {
|
||||
export function requestErrorHandler(error: any, ctx: ContextWithState) {
|
||||
// we don't need to report every time a request stops to the bug tracker
|
||||
if (error.code === "EPIPE" || error.code === "ECONNRESET") {
|
||||
console.warn("Connection error", { error });
|
||||
console.warn("Connection error", {
|
||||
error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
const requestId = ctx.headers["x-request-id"];
|
||||
|
||||
if (requestId) {
|
||||
scope.setTag("request_id", requestId);
|
||||
scope.setTag("request_id", requestId as string);
|
||||
}
|
||||
|
||||
const authType = ctx.state ? ctx.state.authType : undefined;
|
||||
|
||||
if (authType) {
|
||||
scope.setTag("auth_type", authType);
|
||||
}
|
||||
|
||||
const userId =
|
||||
ctx.state && ctx.state.user ? ctx.state.user.id : undefined;
|
||||
|
||||
if (userId) {
|
||||
scope.setUser({ id: userId });
|
||||
scope.setUser({
|
||||
id: userId,
|
||||
});
|
||||
}
|
||||
|
||||
scope.addEventProcessor(function (event) {
|
||||
@@ -1,16 +1,15 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import mailer from "./mailer";
|
||||
|
||||
describe("Mailer", () => {
|
||||
let fakeMailer = mailer;
|
||||
const fakeMailer = mailer;
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'sendMailOutput' implicitly has type 'any... Remove this comment to see the full error message
|
||||
let sendMailOutput;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.URL = "http://localhost:3000";
|
||||
process.env.SMTP_FROM_EMAIL = "hello@example.com";
|
||||
jest.resetModules();
|
||||
|
||||
fakeMailer.transporter = {
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'output' implicitly has an 'any' type.
|
||||
sendMail: (output) => (sendMailOutput = output),
|
||||
};
|
||||
});
|
||||
@@ -20,6 +19,7 @@ describe("Mailer", () => {
|
||||
to: "user@example.com",
|
||||
teamUrl: "http://example.com",
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'sendMailOutput' implicitly has an 'any' ... Remove this comment to see the full error message
|
||||
expect(sendMailOutput).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
import nodemailer from "nodemailer";
|
||||
import Oy from "oy-vey";
|
||||
import * as React from "react";
|
||||
import {
|
||||
type Props as CollectionNotificationEmailT,
|
||||
Props as CollectionNotificationEmailT,
|
||||
CollectionNotificationEmail,
|
||||
collectionNotificationEmailText,
|
||||
} from "./emails/CollectionNotificationEmail";
|
||||
import {
|
||||
type Props as DocumentNotificationEmailT,
|
||||
Props as DocumentNotificationEmailT,
|
||||
DocumentNotificationEmail,
|
||||
documentNotificationEmailText,
|
||||
} from "./emails/DocumentNotificationEmail";
|
||||
@@ -16,13 +15,12 @@ import {
|
||||
ExportFailureEmail,
|
||||
exportEmailFailureText,
|
||||
} from "./emails/ExportFailureEmail";
|
||||
|
||||
import {
|
||||
ExportSuccessEmail,
|
||||
exportEmailSuccessText,
|
||||
} from "./emails/ExportSuccessEmail";
|
||||
import {
|
||||
type Props as InviteEmailT,
|
||||
Props as InviteEmailT,
|
||||
InviteEmail,
|
||||
inviteEmailText,
|
||||
} from "./emails/InviteEmail";
|
||||
@@ -44,13 +42,13 @@ export type EmailTypes =
|
||||
| "exportSuccess";
|
||||
|
||||
export type EmailSendOptions = {
|
||||
to: string,
|
||||
properties?: any,
|
||||
title: string,
|
||||
previewText?: string,
|
||||
text: string,
|
||||
html: React.Node,
|
||||
headCSS?: string,
|
||||
to: string;
|
||||
properties?: any;
|
||||
title: string;
|
||||
previewText?: string;
|
||||
text: string;
|
||||
html: React.ReactNode;
|
||||
headCSS?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -65,7 +63,7 @@ export type EmailSendOptions = {
|
||||
* TEXT: http://localhost:3000/email/:email_type/text
|
||||
*/
|
||||
export class Mailer {
|
||||
transporter: ?any;
|
||||
transporter: any | null | undefined;
|
||||
|
||||
constructor() {
|
||||
this.loadTransport();
|
||||
@@ -73,7 +71,7 @@ export class Mailer {
|
||||
|
||||
async loadTransport() {
|
||||
if (process.env.SMTP_HOST) {
|
||||
let smtpConfig = {
|
||||
const smtpConfig = {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: process.env.SMTP_PORT,
|
||||
secure:
|
||||
@@ -83,17 +81,21 @@ export class Mailer {
|
||||
auth: undefined,
|
||||
tls:
|
||||
"SMTP_TLS_CIPHERS" in process.env
|
||||
? { ciphers: process.env.SMTP_TLS_CIPHERS }
|
||||
? {
|
||||
ciphers: process.env.SMTP_TLS_CIPHERS,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (process.env.SMTP_USERNAME) {
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ user: string; pass: string | undefined; }'... Remove this comment to see the full error message
|
||||
smtpConfig.auth = {
|
||||
user: process.env.SMTP_USERNAME,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error config
|
||||
this.transporter = nodemailer.createTransport(smtpConfig);
|
||||
return;
|
||||
}
|
||||
@@ -105,8 +107,7 @@ export class Mailer {
|
||||
);
|
||||
|
||||
try {
|
||||
let testAccount = await nodemailer.createTestAccount();
|
||||
|
||||
const testAccount = await nodemailer.createTestAccount();
|
||||
const smtpConfig = {
|
||||
host: "smtp.ethereal.email",
|
||||
port: 587,
|
||||
@@ -116,7 +117,6 @@ export class Mailer {
|
||||
pass: testAccount.pass,
|
||||
},
|
||||
};
|
||||
|
||||
this.transporter = nodemailer.createTransport(smtpConfig);
|
||||
} catch (err) {
|
||||
Logger.error(
|
||||
@@ -127,7 +127,10 @@ export class Mailer {
|
||||
}
|
||||
}
|
||||
|
||||
sendMail = async (data: EmailSendOptions): ?Promise<*> => {
|
||||
sendMail = async (
|
||||
data: EmailSendOptions
|
||||
// @ts-expect-error ts-migrate(1064) FIXME: The return type of an async function or method mus... Remove this comment to see the full error message
|
||||
): Promise<any> | null | undefined => {
|
||||
const { transporter } = this;
|
||||
|
||||
if (transporter) {
|
||||
@@ -161,7 +164,7 @@ export class Mailer {
|
||||
}
|
||||
};
|
||||
|
||||
welcome = async (opts: { to: string, teamUrl: string }) => {
|
||||
welcome = async (opts: { to: string; teamUrl: string }) => {
|
||||
this.sendMail({
|
||||
to: opts.to,
|
||||
title: "Welcome to Outline",
|
||||
@@ -172,7 +175,7 @@ export class Mailer {
|
||||
});
|
||||
};
|
||||
|
||||
exportSuccess = async (opts: { to: string, id: string, teamUrl: string }) => {
|
||||
exportSuccess = async (opts: { to: string; id: string; teamUrl: string }) => {
|
||||
this.sendMail({
|
||||
to: opts.to,
|
||||
title: "Your requested export",
|
||||
@@ -182,7 +185,7 @@ export class Mailer {
|
||||
});
|
||||
};
|
||||
|
||||
exportFailure = async (opts: { to: string, teamUrl: string }) => {
|
||||
exportFailure = async (opts: { to: string; teamUrl: string }) => {
|
||||
this.sendMail({
|
||||
to: opts.to,
|
||||
title: "Your requested export",
|
||||
@@ -192,7 +195,11 @@ export class Mailer {
|
||||
});
|
||||
};
|
||||
|
||||
invite = async (opts: { to: string } & InviteEmailT) => {
|
||||
invite = async (
|
||||
opts: {
|
||||
to: string;
|
||||
} & InviteEmailT
|
||||
) => {
|
||||
this.sendMail({
|
||||
to: opts.to,
|
||||
title: `${opts.actorName} invited you to join ${opts.teamName}’s knowledge base`,
|
||||
@@ -203,7 +210,7 @@ export class Mailer {
|
||||
});
|
||||
};
|
||||
|
||||
signin = async (opts: { to: string, token: string, teamUrl: string }) => {
|
||||
signin = async (opts: { to: string; token: string; teamUrl: string }) => {
|
||||
const signInLink = signinEmailText(opts);
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
@@ -220,7 +227,9 @@ export class Mailer {
|
||||
};
|
||||
|
||||
documentNotification = async (
|
||||
opts: { to: string } & DocumentNotificationEmailT
|
||||
opts: {
|
||||
to: string;
|
||||
} & DocumentNotificationEmailT
|
||||
) => {
|
||||
this.sendMail({
|
||||
to: opts.to,
|
||||
@@ -232,7 +241,9 @@ export class Mailer {
|
||||
};
|
||||
|
||||
collectionNotification = async (
|
||||
opts: { to: string } & CollectionNotificationEmailT
|
||||
opts: {
|
||||
to: string;
|
||||
} & CollectionNotificationEmailT
|
||||
) => {
|
||||
this.sendMail({
|
||||
to: opts.to,
|
||||
@@ -243,7 +254,7 @@ export class Mailer {
|
||||
});
|
||||
};
|
||||
|
||||
sendTemplate = async (type: EmailTypes, opts?: Object = {}) => {
|
||||
sendTemplate = async (type: EmailTypes, opts: Record<string, any> = {}) => {
|
||||
await emailsQueue.add(
|
||||
{
|
||||
type,
|
||||
@@ -261,4 +272,5 @@ export class Mailer {
|
||||
}
|
||||
|
||||
const mailer = new Mailer();
|
||||
|
||||
export default mailer;
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import type { Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
|
||||
export default function apexRedirect() {
|
||||
return async function apexRedirectMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
if (ctx.headers.host === "getoutline.com") {
|
||||
ctx.redirect(`https://www.${ctx.headers.host}${ctx.path}`);
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import randomstring from "randomstring";
|
||||
import { ApiKey } from "../models";
|
||||
import { buildUser, buildTeam } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { ApiKey } from "@server/models";
|
||||
import { buildUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import auth from "./authentication";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
@@ -13,20 +12,21 @@ describe("Authentication middleware", () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it("should return error with invalid token", async () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
@@ -35,9 +35,11 @@ describe("Authentication middleware", () => {
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}error`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -48,7 +50,6 @@ describe("Authentication middleware", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("with API key", () => {
|
||||
it("should authenticate user with valid API key", async () => {
|
||||
const state = {};
|
||||
@@ -57,20 +58,21 @@ describe("Authentication middleware", () => {
|
||||
const key = await ApiKey.create({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${key.secret}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it("should return error with invalid API key", async () => {
|
||||
const state = {};
|
||||
const authMiddleware = auth();
|
||||
@@ -78,9 +80,11 @@ describe("Authentication middleware", () => {
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${randomstring.generate(38)}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -99,9 +103,11 @@ describe("Authentication middleware", () => {
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => "error"),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -118,21 +124,23 @@ describe("Authentication middleware", () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Mock<null, []>' is not assignable to type '(... Remove this comment to see the full error message
|
||||
get: jest.fn(() => null),
|
||||
query: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
},
|
||||
body: {},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
@@ -140,20 +148,22 @@ describe("Authentication middleware", () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Mock<null, []>' is not assignable to type '(... Remove this comment to see the full error message
|
||||
get: jest.fn(() => null),
|
||||
},
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
@@ -165,14 +175,16 @@ describe("Authentication middleware", () => {
|
||||
suspendedById: admin.id,
|
||||
});
|
||||
const authMiddleware = auth();
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -181,6 +193,7 @@ describe("Authentication middleware", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"Your access has been suspended by the team admin"
|
||||
);
|
||||
@@ -190,18 +203,21 @@ describe("Authentication middleware", () => {
|
||||
it("should return an error for deleted team", async () => {
|
||||
const state = {};
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
await team.destroy();
|
||||
|
||||
const authMiddleware = auth();
|
||||
let error;
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -210,6 +226,7 @@ describe("Authentication middleware", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("Invalid token");
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,23 @@
|
||||
// @flow
|
||||
import { User, Team, ApiKey } from "@server/models";
|
||||
import { getUserForJWT } from "@server/utils/jwt";
|
||||
import { AuthenticationError, UserSuspendedError } from "../errors";
|
||||
import { User, Team, ApiKey } from "../models";
|
||||
import type { ContextWithState } from "../types";
|
||||
import { getUserForJWT } from "../utils/jwt";
|
||||
import { ContextWithState } from "../types";
|
||||
|
||||
export default function auth(options?: { required?: boolean } = {}) {
|
||||
export default function auth(
|
||||
options: {
|
||||
required?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
return async function authMiddleware(
|
||||
ctx: ContextWithState,
|
||||
next: () => Promise<mixed>
|
||||
next: () => Promise<unknown>
|
||||
) {
|
||||
let token;
|
||||
|
||||
const authorizationHeader = ctx.request.get("authorization");
|
||||
|
||||
if (authorizationHeader) {
|
||||
const parts = authorizationHeader.split(" ");
|
||||
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0];
|
||||
const credentials = parts[1];
|
||||
@@ -22,11 +26,13 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
token = credentials;
|
||||
}
|
||||
} else {
|
||||
throw new AuthenticationError(
|
||||
throw AuthenticationError(
|
||||
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
||||
);
|
||||
}
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
} else if (ctx.body && ctx.body.token) {
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
token = ctx.body.token;
|
||||
} else if (ctx.request.query.token) {
|
||||
token = ctx.request.query.token;
|
||||
@@ -35,15 +41,16 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
}
|
||||
|
||||
if (!token && options.required !== false) {
|
||||
throw new AuthenticationError("Authentication required");
|
||||
throw AuthenticationError("Authentication required");
|
||||
}
|
||||
|
||||
let user;
|
||||
|
||||
if (token) {
|
||||
if (String(token).match(/^[\w]{38}$/)) {
|
||||
ctx.state.authType = "api";
|
||||
|
||||
let apiKey;
|
||||
|
||||
try {
|
||||
apiKey = await ApiKey.findOne({
|
||||
where: {
|
||||
@@ -51,11 +58,11 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AuthenticationError("Invalid API key");
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new AuthenticationError("Invalid API key");
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
user = await User.findByPk(apiKey.userId, {
|
||||
@@ -67,26 +74,29 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthenticationError("Invalid API key");
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
} else {
|
||||
ctx.state.authType = "app";
|
||||
|
||||
user = await getUserForJWT(String(token));
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
const suspendingAdmin = await User.findOne({
|
||||
where: { id: user.suspendedById },
|
||||
where: {
|
||||
id: user.suspendedById,
|
||||
},
|
||||
paranoid: false,
|
||||
});
|
||||
throw new UserSuspendedError({ adminEmail: suspendingAdmin.email });
|
||||
throw UserSuspendedError({
|
||||
adminEmail: suspendingAdmin.email,
|
||||
});
|
||||
}
|
||||
|
||||
// not awaiting the promise here so that the request is not blocked
|
||||
user.updateActiveAt(ctx.request.ip);
|
||||
|
||||
ctx.state.token = String(token);
|
||||
ctx.state.user = user;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
import { snakeCase } from "lodash";
|
||||
import Sequelize from "sequelize";
|
||||
|
||||
export default function errorHandling() {
|
||||
return async function errorHandlingMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
try {
|
||||
await next();
|
||||
@@ -18,6 +17,7 @@ export default function errorHandling() {
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
// super basic form error handling
|
||||
ctx.status = 400;
|
||||
|
||||
if (err.errors && err.errors[0]) {
|
||||
message = `${err.errors[0].message} (${err.errors[0].path})`;
|
||||
}
|
||||
@@ -47,7 +47,9 @@ export default function errorHandling() {
|
||||
data: err.errorData,
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
if (!ctx.body.data) {
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
delete ctx.body.data;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
import queryString from "query-string";
|
||||
|
||||
export default function methodOverride() {
|
||||
return async function methodOverrideMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
if (ctx.method === "POST") {
|
||||
// $FlowFixMe
|
||||
ctx.body = ctx.request.body;
|
||||
} else if (ctx.method === "GET") {
|
||||
ctx.method = 'POST'; // eslint-disable-line
|
||||
|
||||
ctx.body = queryString.parse(ctx.querystring);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { type Context } from "koa";
|
||||
import type { AccountProvisionerResult } from "../commands/accountProvisioner";
|
||||
import Logger from "../logging/logger";
|
||||
import { signIn } from "../utils/authentication";
|
||||
import { Context } from "koa";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { signIn } from "@server/utils/authentication";
|
||||
import { AccountProvisionerResult } from "../commands/accountProvisioner";
|
||||
|
||||
export default function createMiddleware(providerName: string) {
|
||||
return function passportMiddleware(ctx: Context) {
|
||||
return passport.authorize(
|
||||
providerName,
|
||||
{ session: false },
|
||||
{
|
||||
session: false,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'err' implicitly has an 'any' type.
|
||||
async (err, user, result: AccountProvisionerResult) => {
|
||||
if (err) {
|
||||
Logger.error("Error during authentication", err);
|
||||
@@ -22,6 +25,7 @@ export default function createMiddleware(providerName: string) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ctx.redirect(`/?notice=auth-error`);
|
||||
}
|
||||
|
||||
@@ -36,13 +40,15 @@ export default function createMiddleware(providerName: string) {
|
||||
// Handle errors from Azure which come in the format: message, Trace ID,
|
||||
// Correlation ID, Timestamp in these two query string parameters.
|
||||
const { error, error_description } = ctx.request.query;
|
||||
|
||||
if (error && error_description) {
|
||||
Logger.error(
|
||||
"Error from Azure during authentication",
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[]' is not assign... Remove this comment to see the full error message
|
||||
new Error(error_description)
|
||||
);
|
||||
|
||||
// Display only the descriptive message to the user, log the rest
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'split' does not exist on type 'string | ... Remove this comment to see the full error message
|
||||
const description = error_description.split("Trace ID")[0];
|
||||
return ctx.redirect(`/?notice=auth-error&description=${description}`);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { isArrayLike } from "lodash";
|
||||
import validator from "validator";
|
||||
import { validateColorHex } from "../../shared/utils/color";
|
||||
import { validateIndexCharacters } from "../../shared/utils/indexCharacters";
|
||||
import { ParamRequiredError, ValidationError } from "../errors";
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
ctx.assertPresent = (value, message) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
throw new ParamRequiredError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertArray = (value, message) => {
|
||||
if (!isArrayLike(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertIn = (value, options, message) => {
|
||||
if (!options.includes(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertSort = (value, model, message = "Invalid sort parameter") => {
|
||||
if (!Object.keys(model.rawAttributes).includes(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertNotEmpty = (value, message) => {
|
||||
if (value === "") {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertEmail = (value = "", message) => {
|
||||
if (!validator.isEmail(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertUuid = (value = "", message) => {
|
||||
if (!validator.isUUID(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertPositiveInteger = (value, message) => {
|
||||
if (!validator.isInt(String(value), { min: 0 })) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertHexColor = (value, message) => {
|
||||
if (!validateColorHex(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertValueInArray = (value, values, message) => {
|
||||
if (!values.includes(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertIndexCharacters = (value, message) => {
|
||||
if (!validateIndexCharacters(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
return next();
|
||||
};
|
||||
}
|
||||
@@ -1,179 +1,175 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('teams', {
|
||||
await queryInterface.createTable("teams", {
|
||||
id: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: true,
|
||||
},
|
||||
slackId: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
slackData: {
|
||||
type: 'JSONB',
|
||||
type: "JSONB",
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.createTable('atlases', {
|
||||
await queryInterface.createTable("atlases", {
|
||||
id: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: true,
|
||||
},
|
||||
description: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: true,
|
||||
},
|
||||
type: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: true,
|
||||
},
|
||||
navigationTree: {
|
||||
type: 'JSONB',
|
||||
type: "JSONB",
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
teamId: {
|
||||
type: 'UUID',
|
||||
allowNull: false
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.createTable('users', {
|
||||
await queryInterface.createTable("users", {
|
||||
id: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
email: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
},
|
||||
username: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
},
|
||||
name: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
},
|
||||
isAdmin: {
|
||||
type: 'BOOLEAN',
|
||||
type: "BOOLEAN",
|
||||
allowNull: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
slackAccessToken: {
|
||||
type: 'bytea',
|
||||
type: "bytea",
|
||||
allowNull: true,
|
||||
},
|
||||
slackId: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
slackData: {
|
||||
type: 'JSONB',
|
||||
type: "JSONB",
|
||||
allowNull: true,
|
||||
},
|
||||
jwtSecret: {
|
||||
type: 'bytea',
|
||||
type: "bytea",
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
teamId: {
|
||||
type: 'UUID',
|
||||
allowNull: true
|
||||
type: "UUID",
|
||||
allowNull: true,
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.createTable('documents', {
|
||||
await queryInterface.createTable("documents", {
|
||||
id: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
urlId: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
private: {
|
||||
type: 'BOOLEAN',
|
||||
type: "BOOLEAN",
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
title: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
},
|
||||
text: {
|
||||
type: 'TEXT',
|
||||
type: "TEXT",
|
||||
allowNull: true,
|
||||
},
|
||||
html: {
|
||||
type: 'TEXT',
|
||||
type: "TEXT",
|
||||
allowNull: true,
|
||||
},
|
||||
preview: {
|
||||
type: 'TEXT',
|
||||
type: "TEXT",
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'UUID',
|
||||
allowNull: true
|
||||
type: "UUID",
|
||||
allowNull: true,
|
||||
},
|
||||
atlasId: {
|
||||
type: 'UUID',
|
||||
allowNull: true
|
||||
type: "UUID",
|
||||
allowNull: true,
|
||||
},
|
||||
teamId: {
|
||||
type: 'UUID',
|
||||
allowNull: true
|
||||
type: "UUID",
|
||||
allowNull: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropAllTables();
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'parentDocumentId', {
|
||||
await queryInterface.addColumn("documents", "parentDocumentId", {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'parentDocumentId');
|
||||
await queryInterface.removeColumn("documents", "parentDocumentId");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addIndex('documents', ['urlId']);
|
||||
await queryInterface.addIndex('documents', ['id', 'atlasId']);
|
||||
await queryInterface.addIndex('documents', ['id', 'teamId']);
|
||||
await queryInterface.addIndex('documents', ['parentDocumentId', 'atlasId']);
|
||||
|
||||
await queryInterface.addIndex('atlases', ['id', 'teamId']);
|
||||
|
||||
await queryInterface.addIndex('teams', ['slackId']);
|
||||
|
||||
await queryInterface.addIndex('users', ['slackId']);
|
||||
await queryInterface.addIndex("documents", ["urlId"]);
|
||||
await queryInterface.addIndex("documents", ["id", "atlasId"]);
|
||||
await queryInterface.addIndex("documents", ["id", "teamId"]);
|
||||
await queryInterface.addIndex("documents", ["parentDocumentId", "atlasId"]);
|
||||
await queryInterface.addIndex("atlases", ["id", "teamId"]);
|
||||
await queryInterface.addIndex("teams", ["slackId"]);
|
||||
await queryInterface.addIndex("users", ["slackId"]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('documents', ['urlId']);
|
||||
await queryInterface.removeIndex('documents', ['id', 'atlasId']);
|
||||
await queryInterface.removeIndex('documents', ['id', 'teamId']);
|
||||
await queryInterface.removeIndex('documents', ['parentDocumentId', 'atlasId']);
|
||||
|
||||
await queryInterface.removeIndex('atlases', ['id', 'teamId']);
|
||||
|
||||
await queryInterface.removeIndex('teams', ['slackId']);
|
||||
|
||||
await queryInterface.removeIndex('users', ['slackId']);
|
||||
await queryInterface.removeIndex("documents", ["urlId"]);
|
||||
await queryInterface.removeIndex("documents", ["id", "atlasId"]);
|
||||
await queryInterface.removeIndex("documents", ["id", "teamId"]);
|
||||
await queryInterface.removeIndex("documents", [
|
||||
"parentDocumentId",
|
||||
"atlasId",
|
||||
]);
|
||||
await queryInterface.removeIndex("atlases", ["id", "teamId"]);
|
||||
await queryInterface.removeIndex("teams", ["slackId"]);
|
||||
await queryInterface.removeIndex("users", ["slackId"]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,70 +1,66 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('revisions', {
|
||||
await queryInterface.createTable("revisions", {
|
||||
id: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
title: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
},
|
||||
text: {
|
||||
type: 'TEXT',
|
||||
type: "TEXT",
|
||||
allowNull: true,
|
||||
},
|
||||
html: {
|
||||
type: 'TEXT',
|
||||
type: "TEXT",
|
||||
allowNull: true,
|
||||
},
|
||||
preview: {
|
||||
type: 'TEXT',
|
||||
type: "TEXT",
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
model: "users",
|
||||
},
|
||||
},
|
||||
documentId: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'documents',
|
||||
onDelete: 'CASCADE',
|
||||
model: "documents",
|
||||
onDelete: "CASCADE",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('documents', 'lastModifiedById', {
|
||||
type: 'UUID',
|
||||
await queryInterface.addColumn("documents", "lastModifiedById", {
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
model: "users",
|
||||
},
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('documents', 'revisionCount', {
|
||||
type: 'INTEGER',
|
||||
await queryInterface.addColumn("documents", "revisionCount", {
|
||||
type: "INTEGER",
|
||||
defaultValue: 0,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('revisions');
|
||||
|
||||
await queryInterface.removeColumn('documents', 'lastModifiedById');
|
||||
await queryInterface.removeColumn('documents', 'revisionCount');
|
||||
await queryInterface.dropTable("revisions");
|
||||
await queryInterface.removeColumn("documents", "lastModifiedById");
|
||||
await queryInterface.removeColumn("documents", "revisionCount");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ $$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER documents_tsvectorupdate BEFORE INSERT OR UPDATE
|
||||
ON documents FOR EACH ROW EXECUTE PROCEDURE documents_search_trigger();
|
||||
`;
|
||||
|
||||
const searchCollection = `
|
||||
ALTER TABLE atlases ADD COLUMN "searchVector" tsvector;
|
||||
CREATE INDEX atlases_tsv_idx ON atlases USING gin("searchVector");
|
||||
@@ -33,11 +32,9 @@ $$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER atlases_tsvectorupdate BEFORE INSERT OR UPDATE
|
||||
ON atlases FOR EACH ROW EXECUTE PROCEDURE atlases_search_trigger();
|
||||
`;
|
||||
|
||||
await queryInterface.sequelize.query(searchDocument);
|
||||
await queryInterface.sequelize.query(searchCollection);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// TODO?
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('atlases', 'creatorId', {
|
||||
await queryInterface.addColumn("atlases", "creatorId", {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('atlases', 'creatorId');
|
||||
await queryInterface.removeColumn("atlases", "creatorId");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('atlases', 'deletedAt', {
|
||||
await queryInterface.addColumn("atlases", "deletedAt", {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('documents', 'deletedAt', {
|
||||
await queryInterface.addColumn("documents", "deletedAt", {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('atlases', 'deletedAt');
|
||||
await queryInterface.removeColumn('documents', 'deletedAt');
|
||||
await queryInterface.removeColumn("atlases", "deletedAt");
|
||||
await queryInterface.removeColumn("documents", "deletedAt");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Remove old indeces
|
||||
await queryInterface.removeIndex('documents', ['urlId']);
|
||||
await queryInterface.removeIndex('documents', ['id', 'atlasId']);
|
||||
await queryInterface.removeIndex('documents', ['id', 'teamId']);
|
||||
await queryInterface.removeIndex('documents', ['parentDocumentId', 'atlasId']);
|
||||
|
||||
await queryInterface.removeIndex('atlases', ['id', 'teamId']);
|
||||
|
||||
await queryInterface.removeIndex("documents", ["urlId"]);
|
||||
await queryInterface.removeIndex("documents", ["id", "atlasId"]);
|
||||
await queryInterface.removeIndex("documents", ["id", "teamId"]);
|
||||
await queryInterface.removeIndex("documents", [
|
||||
"parentDocumentId",
|
||||
"atlasId",
|
||||
]);
|
||||
await queryInterface.removeIndex("atlases", ["id", "teamId"]);
|
||||
// Add new ones
|
||||
await queryInterface.addIndex('documents', ['id', 'deletedAt']);
|
||||
await queryInterface.addIndex('documents', ['urlId', 'deletedAt']);
|
||||
await queryInterface.addIndex('documents', ['id', 'atlasId', 'deletedAt']);
|
||||
await queryInterface.addIndex('documents', ['id', 'teamId', 'deletedAt']);
|
||||
await queryInterface.addIndex('documents', [
|
||||
'parentDocumentId',
|
||||
'atlasId',
|
||||
'deletedAt',
|
||||
await queryInterface.addIndex("documents", ["id", "deletedAt"]);
|
||||
await queryInterface.addIndex("documents", ["urlId", "deletedAt"]);
|
||||
await queryInterface.addIndex("documents", ["id", "atlasId", "deletedAt"]);
|
||||
await queryInterface.addIndex("documents", ["id", "teamId", "deletedAt"]);
|
||||
await queryInterface.addIndex("documents", [
|
||||
"parentDocumentId",
|
||||
"atlasId",
|
||||
"deletedAt",
|
||||
]);
|
||||
|
||||
await queryInterface.addIndex('atlases', ['id', 'deletedAt']);
|
||||
await queryInterface.addIndex('atlases', ['id', 'teamId', 'deletedAt']);
|
||||
await queryInterface.addIndex("atlases", ["id", "deletedAt"]);
|
||||
await queryInterface.addIndex("atlases", ["id", "teamId", "deletedAt"]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addIndex('documents', ['urlId']);
|
||||
await queryInterface.addIndex('documents', ['id', 'atlasId']);
|
||||
await queryInterface.addIndex('documents', ['id', 'teamId']);
|
||||
await queryInterface.addIndex('documents', ['parentDocumentId', 'atlasId']);
|
||||
|
||||
await queryInterface.addIndex('atlases', ['id', 'teamId']);
|
||||
|
||||
await queryInterface.removeIndex('documents', ['id', 'deletedAt']);
|
||||
await queryInterface.removeIndex('documents', ['urlId', 'deletedAt']);
|
||||
await queryInterface.removeIndex('documents', ['id', 'atlasId', 'deletedAt']);
|
||||
await queryInterface.removeIndex('documents', ['id', 'teamId', 'deletedAt']);
|
||||
await queryInterface.removeIndex('documents', [
|
||||
'parentDocumentId',
|
||||
'atlasId',
|
||||
'deletedAt',
|
||||
await queryInterface.addIndex("documents", ["urlId"]);
|
||||
await queryInterface.addIndex("documents", ["id", "atlasId"]);
|
||||
await queryInterface.addIndex("documents", ["id", "teamId"]);
|
||||
await queryInterface.addIndex("documents", ["parentDocumentId", "atlasId"]);
|
||||
await queryInterface.addIndex("atlases", ["id", "teamId"]);
|
||||
await queryInterface.removeIndex("documents", ["id", "deletedAt"]);
|
||||
await queryInterface.removeIndex("documents", ["urlId", "deletedAt"]);
|
||||
await queryInterface.removeIndex("documents", [
|
||||
"id",
|
||||
"atlasId",
|
||||
"deletedAt",
|
||||
]);
|
||||
|
||||
await queryInterface.removeIndex('atlases', ['id', 'deletedAt']);
|
||||
await queryInterface.removeIndex('atlases', ['id', 'teamId', 'deletedAt']);
|
||||
await queryInterface.removeIndex("documents", [
|
||||
"id",
|
||||
"teamId",
|
||||
"deletedAt",
|
||||
]);
|
||||
await queryInterface.removeIndex("documents", [
|
||||
"parentDocumentId",
|
||||
"atlasId",
|
||||
"deletedAt",
|
||||
]);
|
||||
await queryInterface.removeIndex("atlases", ["id", "deletedAt"]);
|
||||
await queryInterface.removeIndex("atlases", ["id", "teamId", "deletedAt"]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'createdById', {
|
||||
type: 'UUID',
|
||||
await queryInterface.addColumn("documents", "createdById", {
|
||||
type: "UUID",
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'users',
|
||||
model: "users",
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'createdById');
|
||||
await queryInterface.removeColumn("documents", "createdById");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'collaboratorIds', {
|
||||
await queryInterface.addColumn("documents", "collaboratorIds", {
|
||||
type: Sequelize.ARRAY(Sequelize.UUID),
|
||||
});
|
||||
},
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'collaboratorIds');
|
||||
await queryInterface.removeColumn("documents", "collaboratorIds");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('atlases', 'urlId', {
|
||||
await queryInterface.addColumn("atlases", "urlId", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('atlases', 'urlId');
|
||||
await queryInterface.removeColumn("atlases", "urlId");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addIndex('revisions', ['documentId']);
|
||||
await queryInterface.addIndex("revisions", ["documentId"]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('revisions', ['documentId']);
|
||||
await queryInterface.removeIndex("revisions", ["documentId"]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('apiKeys', {
|
||||
await queryInterface.createTable("apiKeys", {
|
||||
id: {
|
||||
type: 'UUID',
|
||||
type: "UUID",
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: true,
|
||||
},
|
||||
secret: {
|
||||
type: 'CHARACTER VARYING',
|
||||
type: "CHARACTER VARYING",
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
userId: {
|
||||
type: 'UUID',
|
||||
allowNull: true
|
||||
type: "UUID",
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: false,
|
||||
},
|
||||
deletedAt: {
|
||||
type: 'TIMESTAMP WITH TIME ZONE',
|
||||
type: "TIMESTAMP WITH TIME ZONE",
|
||||
allowNull: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('apiKeys');
|
||||
await queryInterface.dropTable("apiKeys");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addIndex('apiKeys', ['secret', 'deletedAt']);
|
||||
await queryInterface.addIndex('apiKeys', ['userId', 'deletedAt']);
|
||||
await queryInterface.addIndex("apiKeys", ["secret", "deletedAt"]);
|
||||
await queryInterface.addIndex("apiKeys", ["userId", "deletedAt"]);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('apiKeys', ['secret', 'deletedAt']);
|
||||
await queryInterface.removeIndex('apiKeys', ['userId', 'deletedAt']);
|
||||
await queryInterface.removeIndex("apiKeys", ["secret", "deletedAt"]);
|
||||
await queryInterface.removeIndex("apiKeys", ["userId", "deletedAt"]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'slackId', {
|
||||
await queryInterface.changeColumn("users", "slackId", {
|
||||
type: Sequelize.STRING,
|
||||
unique: false,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.changeColumn('teams', 'slackId', {
|
||||
await queryInterface.changeColumn("teams", "slackId", {
|
||||
type: Sequelize.STRING,
|
||||
unique: false,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'slackId', {
|
||||
await queryInterface.changeColumn("users", "slackId", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.changeColumn('teams', 'slackId', {
|
||||
await queryInterface.changeColumn("teams", "slackId", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'email', {
|
||||
await queryInterface.changeColumn("users", "email", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.changeColumn('users', 'username', {
|
||||
await queryInterface.changeColumn("users", "username", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'email', {
|
||||
await queryInterface.changeColumn("users", "email", {
|
||||
type: Sequelize.STRING,
|
||||
unique: false,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.changeColumn('users', 'username', {
|
||||
await queryInterface.changeColumn("users", "username", {
|
||||
type: Sequelize.STRING,
|
||||
unique: false,
|
||||
allowNull: true,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'passwordDigest', {
|
||||
await queryInterface.addColumn("users", "passwordDigest", {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, _Sequelize) => {
|
||||
await queryInterface.removeColumn('users', 'passwordDigest');
|
||||
await queryInterface.removeColumn("users", "passwordDigest");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.renameTable('atlases', 'collections');
|
||||
await queryInterface.addColumn('collections', 'documentStructure', {
|
||||
await queryInterface.renameTable("atlases", "collections");
|
||||
await queryInterface.addColumn("collections", "documentStructure", {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, _Sequelize) => {
|
||||
await queryInterface.renameTable('collections', 'atlases');
|
||||
await queryInterface.removeColumn('atlases', 'documentStructure');
|
||||
await queryInterface.renameTable("collections", "atlases");
|
||||
await queryInterface.removeColumn("atlases", "documentStructure");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface
|
||||
.createTable('views', {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
documentId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
count: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
await queryInterface.addIndex('views', ['documentId', 'userId'], {
|
||||
indicesType: 'UNIQUE',
|
||||
});
|
||||
await queryInterface.createTable("views", {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
documentId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
count: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
await queryInterface.addIndex("views", ["documentId", "userId"], {
|
||||
indicesType: "UNIQUE",
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('views', ['documentId', 'userId']);
|
||||
await queryInterface.dropTable('views');
|
||||
await queryInterface.removeIndex("views", ["documentId", "userId"]);
|
||||
await queryInterface.dropTable("views");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface
|
||||
.createTable('stars', {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
documentId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
await queryInterface.addIndex('stars', ['documentId', 'userId'], {
|
||||
indicesType: 'UNIQUE',
|
||||
await queryInterface.createTable("stars", {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
},
|
||||
documentId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
await queryInterface.addIndex("stars", ["documentId", "userId"], {
|
||||
indicesType: "UNIQUE",
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('stars', ['documentId', 'userId']);
|
||||
await queryInterface.dropTable('stars');
|
||||
await queryInterface.removeIndex("stars", ["documentId", "userId"]);
|
||||
await queryInterface.dropTable("stars");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeConstraint('users', 'users_email_key', {})
|
||||
await queryInterface.removeConstraint('users', 'users_username_key', {})
|
||||
await queryInterface.removeConstraint("users", "users_email_key", {});
|
||||
await queryInterface.removeConstraint("users", "users_username_key", {});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'email', {
|
||||
await queryInterface.changeColumn("users", "email", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.changeColumn('users', 'username', {
|
||||
await queryInterface.changeColumn("users", "username", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.changeColumn('users', 'slackId', {
|
||||
await queryInterface.changeColumn("users", "slackId", {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeConstraint('users', 'users_slack_id_idx');
|
||||
await queryInterface.removeConstraint("users", "users_slack_id_idx");
|
||||
},
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user