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,10 +1,9 @@
|
||||
// @flow
|
||||
import type { Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
|
||||
export default function apexRedirect() {
|
||||
return async function apexRedirectMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
if (ctx.headers.host === "getoutline.com") {
|
||||
ctx.redirect(`https://www.${ctx.headers.host}${ctx.path}`);
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import randomstring from "randomstring";
|
||||
import { ApiKey } from "../models";
|
||||
import { buildUser, buildTeam } from "../test/factories";
|
||||
import { flushdb } from "../test/support";
|
||||
import { ApiKey } from "@server/models";
|
||||
import { buildUser, buildTeam } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import auth from "./authentication";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
@@ -13,20 +12,21 @@ describe("Authentication middleware", () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it("should return error with invalid token", async () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
@@ -35,9 +35,11 @@ describe("Authentication middleware", () => {
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}error`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -48,7 +50,6 @@ describe("Authentication middleware", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("with API key", () => {
|
||||
it("should authenticate user with valid API key", async () => {
|
||||
const state = {};
|
||||
@@ -57,20 +58,21 @@ describe("Authentication middleware", () => {
|
||||
const key = await ApiKey.create({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${key.secret}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it("should return error with invalid API key", async () => {
|
||||
const state = {};
|
||||
const authMiddleware = auth();
|
||||
@@ -78,9 +80,11 @@ describe("Authentication middleware", () => {
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${randomstring.generate(38)}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -99,9 +103,11 @@ describe("Authentication middleware", () => {
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => "error"),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -118,21 +124,23 @@ describe("Authentication middleware", () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Mock<null, []>' is not assignable to type '(... Remove this comment to see the full error message
|
||||
get: jest.fn(() => null),
|
||||
query: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
},
|
||||
body: {},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
@@ -140,20 +148,22 @@ describe("Authentication middleware", () => {
|
||||
const state = {};
|
||||
const user = await buildUser();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Mock<null, []>' is not assignable to type '(... Remove this comment to see the full error message
|
||||
get: jest.fn(() => null),
|
||||
},
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type '{}'.
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
@@ -165,14 +175,16 @@ describe("Authentication middleware", () => {
|
||||
suspendedById: admin.id,
|
||||
});
|
||||
const authMiddleware = auth();
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -181,6 +193,7 @@ describe("Authentication middleware", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"Your access has been suspended by the team admin"
|
||||
);
|
||||
@@ -190,18 +203,21 @@ describe("Authentication middleware", () => {
|
||||
it("should return an error for deleted team", async () => {
|
||||
const state = {};
|
||||
const team = await buildTeam();
|
||||
const user = await buildUser({ teamId: team.id });
|
||||
|
||||
const user = await buildUser({
|
||||
teamId: team.id,
|
||||
});
|
||||
await team.destroy();
|
||||
|
||||
const authMiddleware = auth();
|
||||
let error;
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2740) FIXME: Type '{ get: Mock<string, []>; }' is missing the f... Remove this comment to see the full error message
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{}' is not assignable to type 'DefaultState ... Remove this comment to see the full error message
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
@@ -210,6 +226,7 @@ describe("Authentication middleware", () => {
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("Invalid token");
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,23 @@
|
||||
// @flow
|
||||
import { User, Team, ApiKey } from "@server/models";
|
||||
import { getUserForJWT } from "@server/utils/jwt";
|
||||
import { AuthenticationError, UserSuspendedError } from "../errors";
|
||||
import { User, Team, ApiKey } from "../models";
|
||||
import type { ContextWithState } from "../types";
|
||||
import { getUserForJWT } from "../utils/jwt";
|
||||
import { ContextWithState } from "../types";
|
||||
|
||||
export default function auth(options?: { required?: boolean } = {}) {
|
||||
export default function auth(
|
||||
options: {
|
||||
required?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
return async function authMiddleware(
|
||||
ctx: ContextWithState,
|
||||
next: () => Promise<mixed>
|
||||
next: () => Promise<unknown>
|
||||
) {
|
||||
let token;
|
||||
|
||||
const authorizationHeader = ctx.request.get("authorization");
|
||||
|
||||
if (authorizationHeader) {
|
||||
const parts = authorizationHeader.split(" ");
|
||||
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0];
|
||||
const credentials = parts[1];
|
||||
@@ -22,11 +26,13 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
token = credentials;
|
||||
}
|
||||
} else {
|
||||
throw new AuthenticationError(
|
||||
throw AuthenticationError(
|
||||
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
||||
);
|
||||
}
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
} else if (ctx.body && ctx.body.token) {
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
token = ctx.body.token;
|
||||
} else if (ctx.request.query.token) {
|
||||
token = ctx.request.query.token;
|
||||
@@ -35,15 +41,16 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
}
|
||||
|
||||
if (!token && options.required !== false) {
|
||||
throw new AuthenticationError("Authentication required");
|
||||
throw AuthenticationError("Authentication required");
|
||||
}
|
||||
|
||||
let user;
|
||||
|
||||
if (token) {
|
||||
if (String(token).match(/^[\w]{38}$/)) {
|
||||
ctx.state.authType = "api";
|
||||
|
||||
let apiKey;
|
||||
|
||||
try {
|
||||
apiKey = await ApiKey.findOne({
|
||||
where: {
|
||||
@@ -51,11 +58,11 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AuthenticationError("Invalid API key");
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new AuthenticationError("Invalid API key");
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
user = await User.findByPk(apiKey.userId, {
|
||||
@@ -67,26 +74,29 @@ export default function auth(options?: { required?: boolean } = {}) {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthenticationError("Invalid API key");
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
} else {
|
||||
ctx.state.authType = "app";
|
||||
|
||||
user = await getUserForJWT(String(token));
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
const suspendingAdmin = await User.findOne({
|
||||
where: { id: user.suspendedById },
|
||||
where: {
|
||||
id: user.suspendedById,
|
||||
},
|
||||
paranoid: false,
|
||||
});
|
||||
throw new UserSuspendedError({ adminEmail: suspendingAdmin.email });
|
||||
throw UserSuspendedError({
|
||||
adminEmail: suspendingAdmin.email,
|
||||
});
|
||||
}
|
||||
|
||||
// not awaiting the promise here so that the request is not blocked
|
||||
user.updateActiveAt(ctx.request.ip);
|
||||
|
||||
ctx.state.token = String(token);
|
||||
ctx.state.user = user;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
import { snakeCase } from "lodash";
|
||||
import Sequelize from "sequelize";
|
||||
|
||||
export default function errorHandling() {
|
||||
return async function errorHandlingMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
try {
|
||||
await next();
|
||||
@@ -18,6 +17,7 @@ export default function errorHandling() {
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
// super basic form error handling
|
||||
ctx.status = 400;
|
||||
|
||||
if (err.errors && err.errors[0]) {
|
||||
message = `${err.errors[0].message} (${err.errors[0].path})`;
|
||||
}
|
||||
@@ -47,7 +47,9 @@ export default function errorHandling() {
|
||||
data: err.errorData,
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
if (!ctx.body.data) {
|
||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||
delete ctx.body.data;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { Context } from "koa";
|
||||
import queryString from "query-string";
|
||||
|
||||
export default function methodOverride() {
|
||||
return async function methodOverrideMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
next: () => Promise<any>
|
||||
) {
|
||||
if (ctx.method === "POST") {
|
||||
// $FlowFixMe
|
||||
ctx.body = ctx.request.body;
|
||||
} else if (ctx.method === "GET") {
|
||||
ctx.method = 'POST'; // eslint-disable-line
|
||||
|
||||
ctx.body = queryString.parse(ctx.querystring);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
// @flow
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { type Context } from "koa";
|
||||
import type { AccountProvisionerResult } from "../commands/accountProvisioner";
|
||||
import Logger from "../logging/logger";
|
||||
import { signIn } from "../utils/authentication";
|
||||
import { Context } from "koa";
|
||||
import Logger from "@server/logging/logger";
|
||||
import { signIn } from "@server/utils/authentication";
|
||||
import { AccountProvisionerResult } from "../commands/accountProvisioner";
|
||||
|
||||
export default function createMiddleware(providerName: string) {
|
||||
return function passportMiddleware(ctx: Context) {
|
||||
return passport.authorize(
|
||||
providerName,
|
||||
{ session: false },
|
||||
{
|
||||
session: false,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'err' implicitly has an 'any' type.
|
||||
async (err, user, result: AccountProvisionerResult) => {
|
||||
if (err) {
|
||||
Logger.error("Error during authentication", err);
|
||||
@@ -22,6 +25,7 @@ export default function createMiddleware(providerName: string) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ctx.redirect(`/?notice=auth-error`);
|
||||
}
|
||||
|
||||
@@ -36,13 +40,15 @@ export default function createMiddleware(providerName: string) {
|
||||
// Handle errors from Azure which come in the format: message, Trace ID,
|
||||
// Correlation ID, Timestamp in these two query string parameters.
|
||||
const { error, error_description } = ctx.request.query;
|
||||
|
||||
if (error && error_description) {
|
||||
Logger.error(
|
||||
"Error from Azure during authentication",
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[]' is not assign... Remove this comment to see the full error message
|
||||
new Error(error_description)
|
||||
);
|
||||
|
||||
// Display only the descriptive message to the user, log the rest
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'split' does not exist on type 'string | ... Remove this comment to see the full error message
|
||||
const description = error_description.split("Trace ID")[0];
|
||||
return ctx.redirect(`/?notice=auth-error&description=${description}`);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// @flow
|
||||
import { type Context } from "koa";
|
||||
import { isArrayLike } from "lodash";
|
||||
import validator from "validator";
|
||||
import { validateColorHex } from "../../shared/utils/color";
|
||||
import { validateIndexCharacters } from "../../shared/utils/indexCharacters";
|
||||
import { ParamRequiredError, ValidationError } from "../errors";
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
ctx.assertPresent = (value, message) => {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
throw new ParamRequiredError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertArray = (value, message) => {
|
||||
if (!isArrayLike(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertIn = (value, options, message) => {
|
||||
if (!options.includes(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertSort = (value, model, message = "Invalid sort parameter") => {
|
||||
if (!Object.keys(model.rawAttributes).includes(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertNotEmpty = (value, message) => {
|
||||
if (value === "") {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertEmail = (value = "", message) => {
|
||||
if (!validator.isEmail(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertUuid = (value = "", message) => {
|
||||
if (!validator.isUUID(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertPositiveInteger = (value, message) => {
|
||||
if (!validator.isInt(String(value), { min: 0 })) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertHexColor = (value, message) => {
|
||||
if (!validateColorHex(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertValueInArray = (value, values, message) => {
|
||||
if (!values.includes(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertIndexCharacters = (value, message) => {
|
||||
if (!validateIndexCharacters(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
return next();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user