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,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}`);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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