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:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -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");

View File

@@ -1,5 +1,3 @@
// @flow
/**
* Get the value of a command line argument
*

View File

@@ -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

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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();
});

View File

@@ -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,

View File

@@ -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)

View File

@@ -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("/");

View File

@@ -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];
});
}

View File

@@ -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
View 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;
}

View File

@@ -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/">

View File

@@ -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", () => {
![caption](/images/${uuid}.png)`).length
).toBe(0);
});
it("should parse attachment ID from markdown", () => {
const uuid = uuidv4();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid})`
);
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(
`![caption text](/api/attachments.redirect?id=${uuid}&size=2)`
);
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(
`![caption text](${process.env.URL}/api/attachments.redirect?id=${uuid})`
);
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(
`![caption text](/api/attachments.redirect?id=${uuid} "align-left")`
);
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(
`![caption text](/api/attachments.redirect?id=${uuid})
const results = parseAttachmentIds(`![caption text](/api/attachments.redirect?id=${uuid})
some text
![another caption](/api/attachments.redirect?id=${uuid2})`
);
![another caption](/api/attachments.redirect?id=${uuid2})`);
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);

View File

@@ -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[] {

View File

@@ -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);
});

View File

@@ -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;
}

View File

@@ -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
![internal](/attachments/image.png)
`);
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);
});

View File

@@ -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;
}

View File

@@ -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);
};
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -1,4 +1,3 @@
// @flow
import slug from "slug";
slug.defaults.mode = "rfc3986";

View File

@@ -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(

View File

@@ -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,

View File

@@ -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);
}