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,5 +1,3 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
|
||||
export const uploadToS3FromBuffer = jest.fn().mockReturnValue("/endpoint/key");
|
||||
|
||||
export const publicS3Endpoint = jest.fn().mockReturnValue("http://mock");
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Get the value of a command line argument
|
||||
*
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import querystring from "querystring";
|
||||
import { addMonths } from "date-fns";
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
import { pick } from "lodash";
|
||||
import Logger from "../logging/logger";
|
||||
import { User, Event, Team, Collection, View } from "../models";
|
||||
import { getCookieDomain } from "../utils/domains";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { User, Event, Team, Collection, View } from "@server/models";
|
||||
import { getCookieDomain } from "@server/utils/domains";
|
||||
|
||||
export function getAllowedDomains(): string[] {
|
||||
// GOOGLE_ALLOWED_DOMAINS included here for backwards compatability
|
||||
@@ -15,11 +14,13 @@ export function getAllowedDomains(): string[] {
|
||||
|
||||
export async function signIn(
|
||||
ctx: Context,
|
||||
// @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,
|
||||
service: string,
|
||||
isNewUser: boolean = false,
|
||||
isNewTeam: boolean = false
|
||||
_isNewUser = false,
|
||||
isNewTeam = false
|
||||
) {
|
||||
if (user.isSuspended) {
|
||||
return ctx.redirect("/?notice=suspended");
|
||||
@@ -36,7 +37,9 @@ export async function signIn(
|
||||
JSON.parse(querystring.unescape(cookie)),
|
||||
["ref", "utm_content", "utm_medium", "utm_source", "utm_campaign"]
|
||||
);
|
||||
await team.update({ signupQueryParams });
|
||||
await team.update({
|
||||
signupQueryParams,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error(`Error persisting signup query params`, error);
|
||||
}
|
||||
@@ -45,7 +48,6 @@ export async function signIn(
|
||||
|
||||
// update the database when the user last signed in
|
||||
user.updateSignedIn(ctx.request.ip);
|
||||
|
||||
// don't await event creation for a faster sign-in
|
||||
Event.create({
|
||||
name: "users.signin",
|
||||
@@ -58,10 +60,8 @@ export async function signIn(
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
const domain = getCookieDomain(ctx.request.hostname);
|
||||
const expires = addMonths(new Date(), 3);
|
||||
|
||||
// set a cookie for which service we last signed in with. This is
|
||||
// only used to display a UI hint for the user for next time
|
||||
ctx.cookies.set("lastSignedIn", service, {
|
||||
@@ -92,21 +92,20 @@ export async function signIn(
|
||||
expires,
|
||||
domain,
|
||||
});
|
||||
|
||||
ctx.redirect(`${team.url}/auth/redirect?token=${user.getTransferToken()}`);
|
||||
} else {
|
||||
ctx.cookies.set("accessToken", user.getJwtToken(), {
|
||||
httpOnly: false,
|
||||
expires,
|
||||
});
|
||||
|
||||
const [collection, view] = await Promise.all([
|
||||
Collection.findFirstCollectionForUser(user),
|
||||
View.findOne({
|
||||
where: { userId: user.id },
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const hasViewedDocuments = !!view;
|
||||
ctx.redirect(
|
||||
!hasViewedDocuments && collection
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { generateAvatarUrl } from "./avatars";
|
||||
|
||||
it("should return clearbit url if available", async () => {
|
||||
@@ -9,7 +8,6 @@ it("should return clearbit url if available", async () => {
|
||||
});
|
||||
expect(url).toBe("https://logo.clearbit.com/google.com");
|
||||
});
|
||||
|
||||
it("should return tiley url if clearbit unavailable", async () => {
|
||||
const url = await generateAvatarUrl({
|
||||
id: "invalid",
|
||||
@@ -20,7 +18,6 @@ it("should return tiley url if clearbit unavailable", async () => {
|
||||
"https://tiley.herokuapp.com/avatar/f1234d75178d892a133a410355a5a990cf75d2f33eba25d575943d4df632f3a4/I.png"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return tiley url if domain not provided", async () => {
|
||||
const url = await generateAvatarUrl({
|
||||
id: "google",
|
||||
@@ -30,7 +27,6 @@ it("should return tiley url if domain not provided", async () => {
|
||||
"https://tiley.herokuapp.com/avatar/bbdefa2950f49882f295b1285d4fa9dec45fc4144bfb07ee6acc68762d12c2e3/G.png"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return tiley url if name not provided", async () => {
|
||||
const url = await generateAvatarUrl({
|
||||
id: "google",
|
||||
@@ -39,7 +35,6 @@ it("should return tiley url if name not provided", async () => {
|
||||
"https://tiley.herokuapp.com/avatar/bbdefa2950f49882f295b1285d4fa9dec45fc4144bfb07ee6acc68762d12c2e3/U.png"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return tiley url with encoded name", async () => {
|
||||
const url = await generateAvatarUrl({
|
||||
id: "google",
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import crypto from "crypto";
|
||||
import fetch from "fetch-with-proxy";
|
||||
|
||||
@@ -9,19 +8,20 @@ export async function generateAvatarUrl({
|
||||
domain,
|
||||
name = "Unknown",
|
||||
}: {
|
||||
id: string,
|
||||
domain?: string,
|
||||
name?: string,
|
||||
id: string;
|
||||
domain?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
// attempt to get logo from Clearbit API. If one doesn't exist then
|
||||
// fall back to using tiley to generate a placeholder logo
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(id);
|
||||
const hashedId = hash.digest("hex");
|
||||
|
||||
let cbResponse, cbUrl;
|
||||
|
||||
if (domain) {
|
||||
cbUrl = `https://logo.clearbit.com/${domain}`;
|
||||
|
||||
try {
|
||||
cbResponse = await fetch(cbUrl);
|
||||
} catch (err) {
|
||||
@@ -1,23 +1,25 @@
|
||||
// @flow
|
||||
import fractionalIndex from "fractional-index";
|
||||
import naturalSort from "../../shared/utils/naturalSort";
|
||||
import { Collection } from "../models";
|
||||
import naturalSort from "@shared/utils/naturalSort";
|
||||
import { Collection } from "@server/models";
|
||||
|
||||
export default async function collectionIndexing(teamId: string) {
|
||||
const collections = await Collection.findAll({
|
||||
where: { teamId, deletedAt: null }, //no point in maintaining index of deleted collections.
|
||||
where: {
|
||||
teamId,
|
||||
deletedAt: null,
|
||||
},
|
||||
//no point in maintaining index of deleted collections.
|
||||
attributes: ["id", "index", "name"],
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
let sortableCollections = collections.map((collection) => {
|
||||
return [collection, collection.index];
|
||||
});
|
||||
|
||||
sortableCollections = naturalSort(
|
||||
sortableCollections,
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(collection: any) => any' is not... Remove this comment to see the full error message
|
||||
(collection) => collection[0].name
|
||||
);
|
||||
|
||||
//for each collection with null index, use previous collection index to create new index
|
||||
let previousCollectionIndex = null;
|
||||
|
||||
@@ -27,13 +29,14 @@ export default async function collectionIndexing(teamId: string) {
|
||||
collection[0].index = index;
|
||||
await collection[0].save();
|
||||
}
|
||||
|
||||
previousCollectionIndex = collection[0].index;
|
||||
}
|
||||
|
||||
const indexedCollections = {};
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'collection' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
sortableCollections.forEach((collection) => {
|
||||
indexedCollections[collection[0].id] = collection[0].index;
|
||||
});
|
||||
|
||||
return indexedCollections;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { darken } from "polished";
|
||||
import theme from "../../shared/theme";
|
||||
import theme from "@shared/theme";
|
||||
|
||||
export const palette = [
|
||||
theme.brand.red,
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import dataURItoBuffer from "./dataURItoBuffer";
|
||||
|
||||
it("should parse value data URI", () => {
|
||||
@@ -8,13 +7,14 @@ it("should parse value data URI", () => {
|
||||
expect(response.buffer).toBeTruthy();
|
||||
expect(response.type).toBe("image/png");
|
||||
});
|
||||
|
||||
it("should throw an error with junk input", () => {
|
||||
let err;
|
||||
|
||||
try {
|
||||
dataURItoBuffer("what");
|
||||
} catch (error) {
|
||||
err = error;
|
||||
}
|
||||
|
||||
expect(err).toBeTruthy();
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export default function dataURItoBuffer(dataURI: string) {
|
||||
const split = dataURI.split(",");
|
||||
|
||||
@@ -9,10 +7,8 @@ export default function dataURItoBuffer(dataURI: string) {
|
||||
|
||||
// separate out the mime component
|
||||
const type = split[0].split(":")[1].split(";")[0];
|
||||
|
||||
// convert base64 to buffer
|
||||
const buffer = Buffer.from(split[1], "base64");
|
||||
|
||||
return {
|
||||
buffer,
|
||||
type,
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { parseDomain, stripSubdomain } from "../../shared/utils/domains";
|
||||
import { parseDomain, stripSubdomain } from "@shared/utils/domains";
|
||||
|
||||
export function getCookieDomain(domain: string) {
|
||||
return process.env.SUBDOMAINS_ENABLED === "true"
|
||||
@@ -9,6 +8,7 @@ export function getCookieDomain(domain: string) {
|
||||
|
||||
export function isCustomDomain(hostname: string) {
|
||||
const parsed = parseDomain(hostname);
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message
|
||||
const main = parseDomain(process.env.URL);
|
||||
return (
|
||||
parsed && main && (main.domain !== parsed.domain || main.tld !== parsed.tld)
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { serializeFilename, deserializeFilename } from "./fs";
|
||||
|
||||
describe("serializeFilename", () => {
|
||||
@@ -16,7 +15,6 @@ describe("serializeFilename", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deserializeFilename", () => {
|
||||
it("should deserialize forward slashes", () => {
|
||||
expect(deserializeFilename("%2F")).toBe("/");
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
@@ -16,15 +15,13 @@ export function requireDirectory<T>(dirName: string): [T, string][] {
|
||||
.filter(
|
||||
(file) =>
|
||||
file.indexOf(".") !== 0 &&
|
||||
file.endsWith(".js") &&
|
||||
file.match(/\.[jt]s$/) &&
|
||||
file !== path.basename(__filename) &&
|
||||
!file.includes(".test")
|
||||
)
|
||||
.map((fileName) => {
|
||||
const filePath = path.join(dirName, fileName);
|
||||
const name = path.basename(filePath.replace(/\.js$/, ""));
|
||||
|
||||
// $FlowIssue
|
||||
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
|
||||
return [require(filePath), name];
|
||||
});
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// @flow
|
||||
import { subMinutes } from "date-fns";
|
||||
import JWT from "jsonwebtoken";
|
||||
import { AuthenticationError } from "../errors";
|
||||
import { Team, User } from "../models";
|
||||
|
||||
function getJWTPayload(token) {
|
||||
let payload;
|
||||
try {
|
||||
payload = JWT.decode(token);
|
||||
} catch (err) {
|
||||
throw new AuthenticationError("Unable to decode JWT token");
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw new AuthenticationError("Invalid token");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function getUserForJWT(token: string): Promise<User> {
|
||||
const payload = getJWTPayload(token);
|
||||
|
||||
if (payload.type === "email-signin") {
|
||||
throw new AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
// check the token is within it's expiration time
|
||||
if (payload.expiresAt) {
|
||||
if (new Date(payload.expiresAt) < new Date()) {
|
||||
throw new AuthenticationError("Expired token");
|
||||
}
|
||||
}
|
||||
|
||||
const user = await User.findByPk(payload.id, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (payload.type === "transfer") {
|
||||
// If the user has made a single API request since the transfer token was
|
||||
// created then it's no longer valid, they'll need to sign in again.
|
||||
if (user.lastActiveAt > new Date(payload.createdAt)) {
|
||||
throw new AuthenticationError("Token has already been used");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JWT.verify(token, user.jwtSecret);
|
||||
} catch (err) {
|
||||
throw new AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getUserForEmailSigninToken(token: string): Promise<User> {
|
||||
const payload = getJWTPayload(token);
|
||||
|
||||
if (payload.type !== "email-signin") {
|
||||
throw new AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
// check the token is within it's expiration time
|
||||
if (payload.createdAt) {
|
||||
if (new Date(payload.createdAt) < subMinutes(new Date(), 10)) {
|
||||
throw new AuthenticationError("Expired token");
|
||||
}
|
||||
}
|
||||
|
||||
const user = await User.findByPk(payload.id, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// if user has signed in at all since the token was created then
|
||||
// it's no longer valid, they'll need a new one.
|
||||
if (user.lastSignedInAt > payload.createdAt) {
|
||||
throw new AuthenticationError("Token has already been used");
|
||||
}
|
||||
|
||||
try {
|
||||
JWT.verify(token, user.jwtSecret);
|
||||
} catch (err) {
|
||||
throw new AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
114
server/utils/jwt.ts
Normal file
114
server/utils/jwt.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { subMinutes } from "date-fns";
|
||||
import JWT from "jsonwebtoken";
|
||||
import { Team, User } from "@server/models";
|
||||
import { AuthenticationError } from "../errors";
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'token' implicitly has an 'any' type.
|
||||
function getJWTPayload(token) {
|
||||
let payload;
|
||||
|
||||
try {
|
||||
payload = JWT.decode(token);
|
||||
} catch (err) {
|
||||
throw AuthenticationError("Unable to decode JWT token");
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
// @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
|
||||
export async function getUserForJWT(token: string): Promise<User> {
|
||||
const payload = getJWTPayload(token);
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'string | J... Remove this comment to see the full error message
|
||||
if (payload.type === "email-signin") {
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
// check the token is within it's expiration time
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'expiresAt' does not exist on type 'strin... Remove this comment to see the full error message
|
||||
if (payload.expiresAt) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'expiresAt' does not exist on type 'strin... Remove this comment to see the full error message
|
||||
if (new Date(payload.expiresAt) < new Date()) {
|
||||
throw AuthenticationError("Expired token");
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'string | Jwt... Remove this comment to see the full error message
|
||||
const user = await User.findByPk(payload.id, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'string | J... Remove this comment to see the full error message
|
||||
if (payload.type === "transfer") {
|
||||
// If the user has made a single API request since the transfer token was
|
||||
// created then it's no longer valid, they'll need to sign in again.
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message
|
||||
if (user.lastActiveAt > new Date(payload.createdAt)) {
|
||||
throw AuthenticationError("Token has already been used");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
JWT.verify(token, user.jwtSecret);
|
||||
} catch (err) {
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
return 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
|
||||
export async function getUserForEmailSigninToken(token: string): Promise<User> {
|
||||
const payload = getJWTPayload(token);
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'string | J... Remove this comment to see the full error message
|
||||
if (payload.type !== "email-signin") {
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
// check the token is within it's expiration time
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message
|
||||
if (payload.createdAt) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message
|
||||
if (new Date(payload.createdAt) < subMinutes(new Date(), 10)) {
|
||||
throw AuthenticationError("Expired token");
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'string | Jwt... Remove this comment to see the full error message
|
||||
const user = await User.findByPk(payload.id, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// if user has signed in at all since the token was created then
|
||||
// it's no longer valid, they'll need a new one.
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'createdAt' does not exist on type 'strin... Remove this comment to see the full error message
|
||||
if (user.lastSignedInAt > payload.createdAt) {
|
||||
throw AuthenticationError("Token has already been used");
|
||||
}
|
||||
|
||||
try {
|
||||
JWT.verify(token, user.jwtSecret);
|
||||
} catch (err) {
|
||||
throw AuthenticationError("Invalid token");
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
export const opensearchResponse = (): string => {
|
||||
return `
|
||||
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { expect } from "@jest/globals";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import parseAttachmentIds from "./parseAttachmentIds";
|
||||
@@ -6,7 +5,6 @@ import parseAttachmentIds from "./parseAttachmentIds";
|
||||
it("should return an empty array with no matches", () => {
|
||||
expect(parseAttachmentIds(`some random text`).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not return orphaned UUID's", () => {
|
||||
const uuid = uuidv4();
|
||||
expect(
|
||||
@@ -15,80 +13,64 @@ it("should not return orphaned UUID's", () => {
|
||||
`).length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("should parse attachment ID from markdown", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
``
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
|
||||
it("should parse attachment ID from markdown with additional query params", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
``
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
|
||||
it("should parse attachment ID from markdown with fully qualified url", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
``
|
||||
);
|
||||
|
||||
expect(process.env.URL).toBeTruthy();
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
|
||||
it("should parse attachment ID from markdown with title", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
``
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
|
||||
it("should parse multiple attachment IDs from markdown", () => {
|
||||
const uuid = uuidv4();
|
||||
const uuid2 = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
`
|
||||
const results = parseAttachmentIds(`
|
||||
|
||||
some text
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
`);
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0]).toBe(uuid);
|
||||
expect(results[1]).toBe(uuid2);
|
||||
});
|
||||
|
||||
it("should parse attachment ID from html", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
`<img src="/api/attachments.redirect?id=${uuid}" />`
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
});
|
||||
|
||||
it("should parse attachment ID from html with fully qualified url", () => {
|
||||
const uuid = uuidv4();
|
||||
const results = parseAttachmentIds(
|
||||
`<img src="${process.env.URL}/api/attachments.redirect?id=${uuid}" />`
|
||||
);
|
||||
|
||||
expect(process.env.URL).toBeTruthy();
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toBe(uuid);
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
const attachmentRegex = /\/api\/attachments\.redirect\?id=(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;
|
||||
|
||||
export default function parseAttachmentIds(text: any): string[] {
|
||||
@@ -1,38 +1,30 @@
|
||||
// @flow
|
||||
import parseDocumentIds from "./parseDocumentIds";
|
||||
|
||||
it("should not return non links", () => {
|
||||
expect(parseDocumentIds(`# Header`).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should return an array of document ids", () => {
|
||||
const result = parseDocumentIds(`# Header
|
||||
|
||||
[internal](/doc/test-456733)
|
||||
`);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe("test-456733");
|
||||
});
|
||||
|
||||
it("should not return duplicate document ids", () => {
|
||||
expect(parseDocumentIds(`# Header`).length).toBe(0);
|
||||
|
||||
const result = parseDocumentIds(`# Header
|
||||
|
||||
[internal](/doc/test-456733)
|
||||
|
||||
[another link to the same doc](/doc/test-456733)
|
||||
`);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe("test-456733");
|
||||
});
|
||||
|
||||
it("should not return non document links", () => {
|
||||
expect(parseDocumentIds(`[google](http://www.google.com)`).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not return non document relative links", () => {
|
||||
expect(parseDocumentIds(`[relative](/developers)`).length).toBe(0);
|
||||
});
|
||||
@@ -1,24 +1,28 @@
|
||||
// @flow
|
||||
import { parser } from "rich-markdown-editor";
|
||||
|
||||
export default function parseDocumentIds(text: string): string[] {
|
||||
const value = parser.parse(text);
|
||||
let links = [];
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'links' implicitly has type 'any[]' in so... Remove this comment to see the full error message
|
||||
const links = [];
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'node' implicitly has an 'any' type.
|
||||
function findLinks(node) {
|
||||
// get text nodes
|
||||
if (node.type.name === "text") {
|
||||
// get marks for text nodes
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'mark' implicitly has an 'any' type.
|
||||
node.marks.forEach((mark) => {
|
||||
// any of the marks links?
|
||||
if (mark.type.name === "link") {
|
||||
const { href } = mark.attrs;
|
||||
|
||||
// any of the links to other docs?
|
||||
if (href.startsWith("/doc")) {
|
||||
const tokens = href.replace(/\/$/, "").split("/");
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
|
||||
// don't return the same link more than once
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'links' implicitly has an 'any[]' type.
|
||||
if (!links.includes(lastToken)) {
|
||||
links.push(lastToken);
|
||||
}
|
||||
@@ -35,5 +39,6 @@ export default function parseDocumentIds(text: string): string[] {
|
||||
}
|
||||
|
||||
findLinks(value);
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'links' implicitly has an 'any[]' type.
|
||||
return links;
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
// @flow
|
||||
import parseImages from "./parseImages";
|
||||
|
||||
it("should not return non images", () => {
|
||||
expect(parseImages(`# Header`).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should return an array of images", () => {
|
||||
const result = parseImages(`# Header
|
||||
|
||||

|
||||
`);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe("/attachments/image.png");
|
||||
});
|
||||
|
||||
it("should not return non document links", () => {
|
||||
expect(parseImages(`[google](http://www.google.com)`).length).toBe(0);
|
||||
});
|
||||
|
||||
it("should not return non document relative links", () => {
|
||||
expect(parseImages(`[relative](/developers)`).length).toBe(0);
|
||||
});
|
||||
@@ -1,12 +1,14 @@
|
||||
// @flow
|
||||
import { parser } from "rich-markdown-editor";
|
||||
|
||||
export default function parseImages(text: string): string[] {
|
||||
const value = parser.parse(text);
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'images' implicitly has type 'any[]' in s... Remove this comment to see the full error message
|
||||
const images = [];
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'node' implicitly has an 'any' type.
|
||||
function findImages(node) {
|
||||
if (node.type.name === "image") {
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'images' implicitly has an 'any[]' type.
|
||||
if (!images.includes(node.attrs.src)) {
|
||||
images.push(node.attrs.src);
|
||||
}
|
||||
@@ -22,5 +24,6 @@ export default function parseImages(text: string): string[] {
|
||||
}
|
||||
|
||||
findImages(value);
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'images' implicitly has an 'any[]' type.
|
||||
return images;
|
||||
}
|
||||
@@ -1,41 +1,38 @@
|
||||
// @flow
|
||||
import { addMinutes, subMinutes } from "date-fns";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import { type Request } from "koa";
|
||||
import { Request } from "koa";
|
||||
import { OAuthStateMismatchError } from "../errors";
|
||||
import { getCookieDomain } from "./domains";
|
||||
|
||||
export class StateStore {
|
||||
key: string = "state";
|
||||
key = "state";
|
||||
|
||||
store = (req: Request, callback: (err: ?Error, state?: string) => void) => {
|
||||
store = (req: Request, callback: () => void) => {
|
||||
// Produce an 8-character random string as state
|
||||
const state = Math.random().toString(36).slice(-8);
|
||||
|
||||
// $FlowFixMe
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'cookies' does not exist on type 'Request... Remove this comment to see the full error message
|
||||
req.cookies.set(this.key, state, {
|
||||
httpOnly: false,
|
||||
expires: addMinutes(new Date(), 10),
|
||||
domain: getCookieDomain(req.hostname),
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 2.
|
||||
callback(null, state);
|
||||
};
|
||||
|
||||
verify = (
|
||||
req: Request,
|
||||
providedState: string,
|
||||
callback: (err: ?Error, ?boolean) => void
|
||||
) => {
|
||||
// $FlowFixMe
|
||||
verify = (req: Request, providedState: string, callback: () => void) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'cookies' does not exist on type 'Request... Remove this comment to see the full error message
|
||||
const state = req.cookies.get(this.key);
|
||||
|
||||
if (!state) {
|
||||
return callback(
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
||||
new OAuthStateMismatchError("State not return in OAuth flow")
|
||||
);
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'cookies' does not exist on type 'Request... Remove this comment to see the full error message
|
||||
req.cookies.set(this.key, "", {
|
||||
httpOnly: false,
|
||||
expires: subMinutes(new Date(), 1),
|
||||
@@ -43,9 +40,11 @@ export class StateStore {
|
||||
});
|
||||
|
||||
if (state !== providedState) {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
||||
return callback(new OAuthStateMismatchError());
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 2.
|
||||
callback(null, true);
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as React from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import env from "../env";
|
||||
import env from "@server/env";
|
||||
|
||||
const prefetchTags = [];
|
||||
|
||||
@@ -18,6 +17,7 @@ if (process.env.AWS_S3_UPLOAD_BUCKET_URL) {
|
||||
}
|
||||
|
||||
let manifestData = {};
|
||||
|
||||
try {
|
||||
const manifest = fs.readFileSync(
|
||||
path.join(__dirname, "../../app/manifest.json"),
|
||||
@@ -40,7 +40,6 @@ Object.values(manifestData).forEach((filename) => {
|
||||
filename.includes("/main") ||
|
||||
filename.includes("/runtime") ||
|
||||
filename.includes("/vendors");
|
||||
|
||||
// only prefetch the first few javascript chunks or it gets out of hand fast
|
||||
const shouldPrefetch = ++index <= 6;
|
||||
|
||||
@@ -61,4 +60,5 @@ Object.values(manifestData).forEach((filename) => {
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Element[]' is not assignable to ... Remove this comment to see the full error message
|
||||
export default ReactDOMServer.renderToString(prefetchTags);
|
||||
@@ -1,8 +1,7 @@
|
||||
// @flow
|
||||
import Queue from "bull";
|
||||
import Redis from "ioredis";
|
||||
import { snakeCase } from "lodash";
|
||||
import Metrics from "../logging/metrics";
|
||||
import Metrics from "@server/logging/metrics";
|
||||
import { client, subscriber } from "../redis";
|
||||
|
||||
export function createQueue(name: string) {
|
||||
@@ -12,38 +11,35 @@ export function createQueue(name: string) {
|
||||
switch (type) {
|
||||
case "client":
|
||||
return client;
|
||||
|
||||
case "subscriber":
|
||||
return subscriber;
|
||||
|
||||
default:
|
||||
return new Redis(process.env.REDIS_URL);
|
||||
}
|
||||
},
|
||||
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
});
|
||||
|
||||
queue.on("stalled", () => {
|
||||
Metrics.increment(`${prefix}.jobs.stalled`);
|
||||
});
|
||||
|
||||
queue.on("completed", () => {
|
||||
Metrics.increment(`${prefix}.jobs.completed`);
|
||||
});
|
||||
|
||||
queue.on("error", (err) => {
|
||||
queue.on("error", () => {
|
||||
Metrics.increment(`${prefix}.jobs.errored`);
|
||||
});
|
||||
|
||||
queue.on("failed", () => {
|
||||
Metrics.increment(`${prefix}.jobs.failed`);
|
||||
});
|
||||
|
||||
setInterval(async () => {
|
||||
Metrics.gauge(`${prefix}.count`, await queue.count());
|
||||
Metrics.gauge(`${prefix}.delayed_count`, await queue.getDelayedCount());
|
||||
}, 5 * 1000);
|
||||
|
||||
return queue;
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
// @flow
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { Collection } from "../models";
|
||||
import { Collection } from "@server/models";
|
||||
import { sequelize, Op } from "../sequelize";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param teamId The team id whose collections has to be fetched
|
||||
* @param index the index for which collision has to be checked
|
||||
* @returns An index, if there is collision returns a new index otherwise the same index
|
||||
*/
|
||||
|
||||
export default async function removeIndexCollision(
|
||||
teamId: string,
|
||||
index: string
|
||||
) {
|
||||
const collection = await Collection.findOne({
|
||||
where: { teamId, deletedAt: null, index },
|
||||
where: {
|
||||
teamId,
|
||||
deletedAt: null,
|
||||
index,
|
||||
},
|
||||
});
|
||||
|
||||
if (!collection) {
|
||||
@@ -36,10 +39,8 @@ export default async function removeIndexCollision(
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
});
|
||||
|
||||
const nextCollectionIndex = nextCollection.length
|
||||
? nextCollection[0].index
|
||||
: null;
|
||||
|
||||
return fractionalIndex(index, nextCollectionIndex);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
|
||||
const DISALLOW_ROBOTS = `User-agent: *
|
||||
Disallow: /`;
|
||||
|
||||
export const robotsResponse = (ctx: Context): ?string => {
|
||||
export const robotsResponse = () => {
|
||||
if (process.env.DEPLOYMENT !== "hosted") {
|
||||
return DISALLOW_ROBOTS;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -1,27 +1,27 @@
|
||||
// @flow
|
||||
import crypto from "crypto";
|
||||
import AWS from "aws-sdk";
|
||||
import { addHours, format } from "date-fns";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import Logger from "../logging/logger";
|
||||
import Logger from "@server/logging/logger";
|
||||
|
||||
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
|
||||
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
|
||||
const AWS_REGION = process.env.AWS_REGION;
|
||||
const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME || "";
|
||||
const AWS_S3_FORCE_PATH_STYLE = process.env.AWS_S3_FORCE_PATH_STYLE !== "false";
|
||||
|
||||
const s3 = new AWS.S3({
|
||||
s3ForcePathStyle: AWS_S3_FORCE_PATH_STYLE,
|
||||
accessKeyId: AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: AWS_SECRET_ACCESS_KEY,
|
||||
region: AWS_REGION,
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
endpoint: process.env.AWS_S3_UPLOAD_BUCKET_URL.includes(
|
||||
AWS_S3_UPLOAD_BUCKET_NAME
|
||||
)
|
||||
? undefined
|
||||
: new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL),
|
||||
: // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message
|
||||
new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL),
|
||||
signatureVersion: "v4",
|
||||
});
|
||||
|
||||
@@ -47,36 +47,49 @@ export const makePolicy = (
|
||||
credential: string,
|
||||
longDate: string,
|
||||
acl: string,
|
||||
contentType: string = "image"
|
||||
contentType = "image"
|
||||
) => {
|
||||
const tomorrow = addHours(new Date(), 24);
|
||||
const policy = {
|
||||
conditions: [
|
||||
{ bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME },
|
||||
{
|
||||
bucket: process.env.AWS_S3_UPLOAD_BUCKET_NAME,
|
||||
},
|
||||
["starts-with", "$key", ""],
|
||||
{ acl },
|
||||
{
|
||||
acl,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
["content-length-range", 0, +process.env.AWS_S3_UPLOAD_MAX_SIZE],
|
||||
["starts-with", "$Content-Type", contentType],
|
||||
["starts-with", "$Cache-Control", ""],
|
||||
{ "x-amz-algorithm": "AWS4-HMAC-SHA256" },
|
||||
{ "x-amz-credential": credential },
|
||||
{ "x-amz-date": longDate },
|
||||
{
|
||||
"x-amz-algorithm": "AWS4-HMAC-SHA256",
|
||||
},
|
||||
{
|
||||
"x-amz-credential": credential,
|
||||
},
|
||||
{
|
||||
"x-amz-date": longDate,
|
||||
},
|
||||
],
|
||||
expiration: format(tomorrow, "yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
};
|
||||
|
||||
return Buffer.from(JSON.stringify(policy)).toString("base64");
|
||||
};
|
||||
|
||||
export const getSignature = (policy: any) => {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
||||
const kDate = hmac(
|
||||
"AWS4" + AWS_SECRET_ACCESS_KEY,
|
||||
format(new Date(), "yyyyMMdd")
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
||||
const kRegion = hmac(kDate, AWS_REGION);
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
||||
const kService = hmac(kRegion, "s3");
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
||||
const kCredentials = hmac(kService, "aws4_request");
|
||||
|
||||
const signature = hmac(kCredentials, policy, "hex");
|
||||
return signature;
|
||||
};
|
||||
@@ -84,7 +97,10 @@ export const getSignature = (policy: any) => {
|
||||
export const publicS3Endpoint = (isServerUpload?: boolean) => {
|
||||
// lose trailing slash if there is one and convert fake-s3 url to localhost
|
||||
// for access outside of docker containers in local development
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/);
|
||||
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
const host = process.env.AWS_S3_UPLOAD_BUCKET_URL.replace(
|
||||
"s3:",
|
||||
"localhost:"
|
||||
@@ -119,19 +135,19 @@ export const uploadToS3FromBuffer = async (
|
||||
Body: buffer,
|
||||
})
|
||||
.promise();
|
||||
|
||||
const endpoint = publicS3Endpoint(true);
|
||||
return `${endpoint}/${key}`;
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7030) FIXME: Not all code paths return a value.
|
||||
export const uploadToS3FromUrl = async (
|
||||
url: string,
|
||||
key: string,
|
||||
acl: string
|
||||
) => {
|
||||
try {
|
||||
// $FlowIssue https://github.com/facebook/flow/issues/2171
|
||||
const res = await fetch(url);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'buffer' does not exist on type 'Response... Remove this comment to see the full error message
|
||||
const buffer = await res.buffer();
|
||||
await s3
|
||||
.putObject({
|
||||
@@ -143,7 +159,6 @@ export const uploadToS3FromUrl = async (
|
||||
Body: buffer,
|
||||
})
|
||||
.promise();
|
||||
|
||||
const endpoint = publicS3Endpoint(true);
|
||||
return `${endpoint}/${key}`;
|
||||
} catch (err) {
|
||||
@@ -165,14 +180,13 @@ export const deleteFromS3 = (key: string) => {
|
||||
};
|
||||
|
||||
export const getSignedUrl = async (key: string) => {
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
const isDocker = process.env.AWS_S3_UPLOAD_BUCKET_URL.match(/http:\/\/s3:/);
|
||||
|
||||
const params = {
|
||||
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
|
||||
Key: key,
|
||||
Expires: 60,
|
||||
};
|
||||
|
||||
return isDocker
|
||||
? `${publicS3Endpoint()}/${key}`
|
||||
: s3.getSignedUrl("getObject", params);
|
||||
@@ -184,6 +198,7 @@ export const getAWSKeyForFileOp = (teamId: string, name: string) => {
|
||||
return `${bucket}/${teamId}/${uuidv4()}/${name}-export.zip`;
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7030) FIXME: Not all code paths return a value.
|
||||
export const getFileByKey = async (key: string) => {
|
||||
const params = {
|
||||
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
import querystring from "querystring";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import { InvalidRequestError } from "../errors";
|
||||
|
||||
const SLACK_API_URL = "https://slack.com/api";
|
||||
|
||||
export async function post(endpoint: string, body: Object) {
|
||||
export async function post(endpoint: string, body: Record<string, any>) {
|
||||
let data;
|
||||
|
||||
const token = body.token;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SLACK_API_URL}/${endpoint}`, {
|
||||
method: "POST",
|
||||
@@ -20,31 +19,36 @@ export async function post(endpoint: string, body: Object) {
|
||||
});
|
||||
data = await response.json();
|
||||
} catch (err) {
|
||||
throw new InvalidRequestError(err.message);
|
||||
throw InvalidRequestError(err.message);
|
||||
}
|
||||
if (!data.ok) throw new InvalidRequestError(data.error);
|
||||
|
||||
if (!data.ok) {
|
||||
throw InvalidRequestError(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function request(endpoint: string, body: Object) {
|
||||
export async function request(endpoint: string, body: Record<string, any>) {
|
||||
let data;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${SLACK_API_URL}/${endpoint}?${querystring.stringify(body)}`
|
||||
);
|
||||
data = await response.json();
|
||||
} catch (err) {
|
||||
throw new InvalidRequestError(err.message);
|
||||
throw InvalidRequestError(err.message);
|
||||
}
|
||||
if (!data.ok) throw new InvalidRequestError(data.error);
|
||||
|
||||
if (!data.ok) {
|
||||
throw InvalidRequestError(data.error);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function oauthAccess(
|
||||
code: string,
|
||||
redirect_uri: string = `${process.env.URL || ""}/auth/slack.callback`
|
||||
redirect_uri = `${process.env.URL || ""}/auth/slack.callback`
|
||||
) {
|
||||
return request("oauth.access", {
|
||||
client_id: process.env.SLACK_KEY,
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import slug from "slug";
|
||||
|
||||
slug.defaults.mode = "rfc3986";
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import chalk from "chalk";
|
||||
import Logger from "../logging/logger";
|
||||
import { Team, AuthenticationProvider } from "../models";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Team, AuthenticationProvider } from "@server/models";
|
||||
|
||||
export async function checkMigrations() {
|
||||
if (process.env.DEPLOYMENT === "hosted") {
|
||||
@@ -12,14 +11,12 @@ export async function checkMigrations() {
|
||||
const providers = await AuthenticationProvider.count();
|
||||
|
||||
if (teams && !providers) {
|
||||
Logger.warn(
|
||||
`
|
||||
Logger.warn(`
|
||||
This version of Outline cannot start until a data migration is complete.
|
||||
Backup your database, run the database migrations and the following script:
|
||||
|
||||
$ node ./build/server/scripts/20210226232041-migrate-authentication.js
|
||||
`
|
||||
);
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -105,11 +102,9 @@ export function checkEnv() {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Logger.info(
|
||||
"lifecycle",
|
||||
chalk.green(
|
||||
`
|
||||
chalk.green(`
|
||||
Is your team enjoying Outline? Consider supporting future development by sponsoring the project:\n\nhttps://github.com/sponsors/outline
|
||||
`
|
||||
)
|
||||
`)
|
||||
);
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
Logger.warn(
|
||||
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import crypto from "crypto";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import invariant from "invariant";
|
||||
import { User, Team, Collection, Document } from "@server/models";
|
||||
import packageInfo from "../../package.json";
|
||||
|
||||
import { User, Team, Collection, Document } from "../models";
|
||||
import { client } from "../redis";
|
||||
|
||||
const UPDATES_URL = "https://updates.getoutline.com";
|
||||
@@ -17,7 +15,6 @@ export async function checkUpdates() {
|
||||
);
|
||||
const secret = process.env.SECRET_KEY.slice(0, 6) + process.env.URL;
|
||||
const id = crypto.createHash("sha256").update(secret).digest("hex");
|
||||
|
||||
const [
|
||||
userCount,
|
||||
teamCount,
|
||||
@@ -29,7 +26,6 @@ export async function checkUpdates() {
|
||||
Collection.count(),
|
||||
Document.count(),
|
||||
]);
|
||||
|
||||
const body = JSON.stringify({
|
||||
id,
|
||||
version: 1,
|
||||
@@ -41,7 +37,6 @@ export async function checkUpdates() {
|
||||
documentCount,
|
||||
},
|
||||
});
|
||||
|
||||
await client.del(UPDATES_KEY);
|
||||
|
||||
try {
|
||||
@@ -53,8 +48,8 @@ export async function checkUpdates() {
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.severity) {
|
||||
await client.set(
|
||||
UPDATES_KEY,
|
||||
@@ -1,22 +1,25 @@
|
||||
// @flow
|
||||
import fs from "fs";
|
||||
import JSZip from "jszip";
|
||||
import tmp from "tmp";
|
||||
import Logger from "../logging/logger";
|
||||
import { Attachment, Collection, Document } from "../models";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { Attachment, Collection, Document } from "@server/models";
|
||||
import { serializeFilename } from "./fs";
|
||||
import { getFileByKey } from "./s3";
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'zip' implicitly has an 'any' type.
|
||||
async function addToArchive(zip, documents) {
|
||||
for (const doc of documents) {
|
||||
const document = await Document.findByPk(doc.id);
|
||||
|
||||
if (!document) {
|
||||
continue;
|
||||
}
|
||||
let text = document.toMarkdown();
|
||||
|
||||
let text = document.toMarkdown();
|
||||
const attachments = await Attachment.findAll({
|
||||
where: { documentId: document.id },
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (const attachment of attachments) {
|
||||
@@ -25,7 +28,6 @@ async function addToArchive(zip, documents) {
|
||||
}
|
||||
|
||||
const title = serializeFilename(document.title) || "Untitled";
|
||||
|
||||
zip.file(`${title}.md`, text, {
|
||||
date: document.updatedAt,
|
||||
comment: JSON.stringify({
|
||||
@@ -42,10 +44,13 @@ async function addToArchive(zip, documents) {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'zip' implicitly has an 'any' type.
|
||||
async function addImageToArchive(zip, key) {
|
||||
try {
|
||||
const img = await getFileByKey(key);
|
||||
zip.file(key, img, { createFolders: true });
|
||||
zip.file(key, img, {
|
||||
createFolders: true,
|
||||
});
|
||||
} catch (err) {
|
||||
Logger.error("Error loading image attachment from S3", err, {
|
||||
key,
|
||||
@@ -53,20 +58,30 @@ async function addImageToArchive(zip, key) {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'zip' implicitly has an 'any' type.
|
||||
async function archiveToPath(zip) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file({ prefix: "export-", postfix: ".zip" }, (err, path) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
zip
|
||||
.generateNodeStream({ type: "nodebuffer", streamFiles: true })
|
||||
.pipe(fs.createWriteStream(path))
|
||||
.on("finish", () => resolve(path))
|
||||
.on("error", reject);
|
||||
});
|
||||
tmp.file(
|
||||
{
|
||||
prefix: "export-",
|
||||
postfix: ".zip",
|
||||
},
|
||||
(err, path) => {
|
||||
if (err) return reject(err);
|
||||
zip
|
||||
.generateNodeStream({
|
||||
type: "nodebuffer",
|
||||
streamFiles: true,
|
||||
})
|
||||
.pipe(fs.createWriteStream(path))
|
||||
.on("finish", () => resolve(path))
|
||||
.on("error", reject);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// @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
|
||||
export async function archiveCollections(collections: Collection[]) {
|
||||
const zip = new JSZip();
|
||||
|
||||
@@ -76,5 +91,6 @@ export async function archiveCollections(collections: Collection[]) {
|
||||
await addToArchive(folder, collection.documentStructure);
|
||||
}
|
||||
}
|
||||
|
||||
return archiveToPath(zip);
|
||||
}
|
||||
Reference in New Issue
Block a user