Refactor to accommodate authentication, transaction and pagination states together (#4636)

* fix: refactor to accommodate authentication, transaction and pagination together into ctx.state

* feat: allow passing response type to APIContext
This commit is contained in:
Apoorv Mishra
2023-01-04 23:51:44 +05:30
committed by GitHub
parent bb568d2e62
commit f4461573de
31 changed files with 753 additions and 675 deletions

View File

@@ -1,6 +1,6 @@
import * as Sentry from "@sentry/node";
import env from "@server/env";
import { ContextWithState } from "../types";
import { AppContext } from "@server/types";
if (env.SENTRY_DSN) {
Sentry.init({
@@ -29,7 +29,7 @@ if (env.SENTRY_DSN) {
});
}
export function requestErrorHandler(error: any, ctx: ContextWithState) {
export function requestErrorHandler(error: any, ctx: AppContext) {
// we don't need to report every time a request stops to the bug tracker
if (error.code === "EPIPE" || error.code === "ECONNRESET") {
console.warn("Connection error", {
@@ -46,17 +46,17 @@ export function requestErrorHandler(error: any, ctx: ContextWithState) {
scope.setTag("request_id", requestId as string);
}
const authType = ctx.state?.authType ?? undefined;
const authType = ctx.state?.auth?.type ?? undefined;
if (authType) {
scope.setTag("auth_type", authType);
}
const teamId = ctx.state?.user?.teamId ?? undefined;
const teamId = ctx.state?.auth?.user?.teamId ?? undefined;
if (teamId) {
scope.setTag("team_id", teamId);
}
const userId = ctx.state?.user?.id ?? undefined;
const userId = ctx.state?.auth?.user?.id ?? undefined;
if (userId) {
scope.setUser({
id: userId,

View File

@@ -24,7 +24,7 @@ describe("Authentication middleware", () => {
},
jest.fn()
);
expect(state.user.id).toEqual(user.id);
expect(state.auth.user.id).toEqual(user.id);
});
it("should return error with invalid token", async () => {
@@ -68,7 +68,7 @@ describe("Authentication middleware", () => {
},
jest.fn()
);
expect(state.user.id).toEqual(user.id);
expect(state.auth.user.id).toEqual(user.id);
});
it("should return error with invalid API key", async () => {
const state = {} as DefaultState;
@@ -133,7 +133,7 @@ describe("Authentication middleware", () => {
},
jest.fn()
);
expect(state.user.id).toEqual(user.id);
expect(state.auth.user.id).toEqual(user.id);
});
it("should allow passing auth token in body params", async () => {
@@ -154,7 +154,7 @@ describe("Authentication middleware", () => {
},
jest.fn()
);
expect(state.user.id).toEqual(user.id);
expect(state.auth.user.id).toEqual(user.id);
});
it("should return an error for suspended users", async () => {

View File

@@ -5,13 +5,13 @@ import tracer, {
getRootSpanFromRequestContext,
} from "@server/logging/tracer";
import { User, Team, ApiKey } from "@server/models";
import { AppContext, AuthenticationType } from "@server/types";
import { getUserForJWT } from "@server/utils/jwt";
import {
AuthenticationError,
AuthorizationError,
UserSuspendedError,
} from "../errors";
import { ContextWithState, AuthenticationType } from "../types";
type AuthenticationOptions = {
/* An admin user role is required to access the route */
@@ -26,7 +26,7 @@ type AuthenticationOptions = {
};
export default function auth(options: AuthenticationOptions = {}) {
return async function authMiddleware(ctx: ContextWithState, next: Next) {
return async function authMiddleware(ctx: AppContext, next: Next) {
let token;
const authorizationHeader = ctx.request.get("authorization");
@@ -61,11 +61,12 @@ export default function auth(options: AuthenticationOptions = {}) {
throw AuthenticationError("Authentication required");
}
let user: User | null | undefined;
let user: User | null;
let type: AuthenticationType;
if (token) {
if (ApiKey.match(String(token))) {
ctx.state.authType = AuthenticationType.API;
type = AuthenticationType.API;
let apiKey;
try {
@@ -96,7 +97,7 @@ export default function auth(options: AuthenticationOptions = {}) {
throw AuthenticationError("Invalid API key");
}
} else {
ctx.state.authType = AuthenticationType.APP;
type = AuthenticationType.APP;
user = await getUserForJWT(String(token));
}
@@ -129,19 +130,24 @@ export default function auth(options: AuthenticationOptions = {}) {
Logger.error("Failed to update user activeAt", err);
});
ctx.state.token = String(token);
ctx.state.user = user;
ctx.state.auth = {
user,
token: String(token),
type,
};
if (tracer) {
addTags(
{
"request.userId": user.id,
"request.teamId": user.teamId,
"request.authType": ctx.state.authType,
"request.authType": type,
},
getRootSpanFromRequestContext(ctx)
);
}
} else {
ctx.state.auth = {};
}
return next();

View File

@@ -1,12 +1,7 @@
import { Context, Next } from "koa";
import { Next } from "koa";
import { Transaction } from "sequelize";
import { sequelize } from "@server/database/sequelize";
export type TransactionContext = Context & {
state: Context["state"] & {
transaction: Transaction;
};
};
import { AppContext } from "@server/types";
/**
* Middleware that wraps a route in a database transaction, useful for mutations
@@ -16,7 +11,7 @@ export type TransactionContext = Context & {
* @returns The middleware function.
*/
export function transaction() {
return async function transactionMiddleware(ctx: Context, next: Next) {
return async function transactionMiddleware(ctx: AppContext, next: Next) {
await sequelize.transaction(async (t: Transaction) => {
ctx.state.transaction = t;
return next();

View File

@@ -3,44 +3,49 @@ import auth from "@server/middlewares/authentication";
import { ApiKey, Event } from "@server/models";
import { authorize } from "@server/policies";
import { presentApiKey } from "@server/presenters";
import { APIContext } from "@server/types";
import { assertUuid, assertPresent } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("apiKeys.create", auth({ member: true }), async (ctx) => {
const { name } = ctx.request.body;
assertPresent(name, "name is required");
const { user } = ctx.state;
router.post(
"apiKeys.create",
auth({ member: true }),
async (ctx: APIContext) => {
const { name } = ctx.request.body;
assertPresent(name, "name is required");
const { user } = ctx.state.auth;
authorize(user, "createApiKey", user.team);
const key = await ApiKey.create({
name,
userId: user.id,
});
await Event.create({
name: "api_keys.create",
modelId: key.id,
teamId: user.teamId,
actorId: user.id,
data: {
authorize(user, "createApiKey", user.team);
const key = await ApiKey.create({
name,
},
ip: ctx.request.ip,
});
userId: user.id,
});
ctx.body = {
data: presentApiKey(key),
};
});
await Event.create({
name: "api_keys.create",
modelId: key.id,
teamId: user.teamId,
actorId: user.id,
data: {
name,
},
ip: ctx.request.ip,
});
ctx.body = {
data: presentApiKey(key),
};
}
);
router.post(
"apiKeys.list",
auth({ member: true }),
pagination(),
async (ctx) => {
const { user } = ctx.state;
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const keys = await ApiKey.findAll({
where: {
userId: user.id,
@@ -57,28 +62,32 @@ router.post(
}
);
router.post("apiKeys.delete", auth({ member: true }), async (ctx) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const key = await ApiKey.findByPk(id);
authorize(user, "delete", key);
router.post(
"apiKeys.delete",
auth({ member: true }),
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state.auth;
const key = await ApiKey.findByPk(id);
authorize(user, "delete", key);
await key.destroy();
await Event.create({
name: "api_keys.delete",
modelId: key.id,
teamId: user.teamId,
actorId: user.id,
data: {
name: key.name,
},
ip: ctx.request.ip,
});
await key.destroy();
await Event.create({
name: "api_keys.delete",
modelId: key.id,
teamId: user.teamId,
actorId: user.id,
data: {
name: key.name,
},
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
});
ctx.body = {
success: true,
};
}
);
export default router;

View File

@@ -11,7 +11,7 @@ import { Attachment, Document, Event } from "@server/models";
import AttachmentHelper from "@server/models/helpers/AttachmentHelper";
import { authorize } from "@server/policies";
import { presentAttachment } from "@server/presenters";
import { APIContext, ContextWithState } from "@server/types";
import { APIContext } from "@server/types";
import { getPresignedPost, publicS3Endpoint } from "@server/utils/s3";
import { assertIn, assertUuid } from "@server/validation";
import * as T from "./schema";
@@ -25,7 +25,8 @@ router.post(
transaction(),
async (ctx: APIContext<T.AttachmentCreateReq>) => {
const { name, documentId, contentType, size, preset } = ctx.input;
const { user, transaction } = ctx.state;
const { auth, transaction } = ctx.state;
const { user } = auth;
// All user types can upload an avatar so no additional authorization is needed.
if (preset === AttachmentPreset.Avatar) {
@@ -113,7 +114,7 @@ router.post(
validate(T.AttachmentDeleteSchema),
async (ctx: APIContext<T.AttachmentDeleteReq>) => {
const { id } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const attachment = await Attachment.findByPk(id, {
rejectOnEmpty: true,
});
@@ -140,11 +141,11 @@ router.post(
}
);
const handleAttachmentsRedirect = async (ctx: ContextWithState) => {
const handleAttachmentsRedirect = async (ctx: APIContext) => {
const id = ctx.request.body?.id ?? ctx.request.query?.id;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const attachment = await Attachment.findByPk(id, {
rejectOnEmpty: true,
});

View File

@@ -4,10 +4,7 @@ import { TeamPreference } from "@shared/types";
import { parseDomain } from "@shared/utils/domains";
import env from "@server/env";
import auth from "@server/middlewares/authentication";
import {
transaction,
TransactionContext,
} from "@server/middlewares/transaction";
import { transaction } from "@server/middlewares/transaction";
import { Event, Team } from "@server/models";
import {
presentUser,
@@ -16,6 +13,7 @@ import {
presentAvailableTeam,
} from "@server/presenters";
import ValidateSSOAccessTask from "@server/queues/tasks/ValidateSSOAccessTask";
import { APIContext } from "@server/types";
import { getSessionsInCookie } from "@server/utils/authentication";
import providers from "../auth/providers";
@@ -47,7 +45,7 @@ function filterProviders(team?: Team) {
}));
}
router.post("auth.config", async (ctx) => {
router.post("auth.config", async (ctx: APIContext) => {
// If self hosted AND there is only one team then that team becomes the
// brand for the knowledge base and it's guest signin option is used for the
// root login page.
@@ -124,8 +122,8 @@ router.post("auth.config", async (ctx) => {
};
});
router.post("auth.info", auth(), async (ctx) => {
const { user } = ctx.state;
router.post("auth.info", auth(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const sessions = getSessionsInCookie(ctx);
const signedInTeamIds = Object.keys(sessions);
@@ -163,34 +161,30 @@ router.post("auth.info", auth(), async (ctx) => {
};
});
router.post(
"auth.delete",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { user, transaction } = ctx.state;
router.post("auth.delete", auth(), transaction(), async (ctx: APIContext) => {
const { auth, transaction } = ctx.state;
const { user } = auth;
await user.rotateJwtSecret({ transaction });
await Event.create(
{
name: "users.signout",
actorId: user.id,
userId: user.id,
teamId: user.teamId,
data: {
name: user.name,
},
ip: ctx.request.ip,
await user.rotateJwtSecret({ transaction });
await Event.create(
{
name: "users.signout",
actorId: user.id,
userId: user.id,
teamId: user.teamId,
data: {
name: user.name,
},
{
transaction,
}
);
ip: ctx.request.ip,
},
{
transaction,
}
);
ctx.body = {
success: true,
};
}
);
ctx.body = {
success: true,
};
});
export default router;

View File

@@ -7,6 +7,7 @@ import {
presentAuthenticationProvider,
presentPolicies,
} from "@server/presenters";
import { APIContext } from "@server/types";
import { assertUuid, assertPresent } from "@server/validation";
import allAuthenticationProviders from "../auth/providers";
@@ -15,11 +16,11 @@ const router = new Router();
router.post(
"authenticationProviders.info",
auth({ admin: true }),
async (ctx) => {
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const authenticationProvider = await AuthenticationProvider.findByPk(id);
authorize(user, "read", authenticationProvider);
@@ -33,11 +34,11 @@ router.post(
router.post(
"authenticationProviders.update",
auth({ admin: true }),
async (ctx) => {
async (ctx: APIContext) => {
const { id, isEnabled } = ctx.request.body;
assertUuid(id, "id is required");
assertPresent(isEnabled, "isEnabled is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const authenticationProvider = await sequelize.transaction(
async (transaction) => {
@@ -86,8 +87,8 @@ router.post(
router.post(
"authenticationProviders.list",
auth({ admin: true }),
async (ctx) => {
const { user } = ctx.state;
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
authorize(user, "read", user.team);
const teamAuthenticationProviders = (await user.team.$get(

View File

@@ -16,10 +16,7 @@ import { sequelize } from "@server/database/sequelize";
import { AuthorizationError, ValidationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import {
TransactionContext,
transaction,
} from "@server/middlewares/transaction";
import { transaction } from "@server/middlewares/transaction";
import {
Collection,
CollectionUser,
@@ -41,6 +38,7 @@ import {
presentCollectionGroupMembership,
presentFileOperation,
} from "@server/presenters";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { collectionIndexing } from "@server/utils/indexing";
import removeIndexCollision from "@server/utils/removeIndexCollision";
@@ -56,7 +54,7 @@ import pagination from "./middlewares/pagination";
const router = new Router();
router.post("collections.create", auth(), async (ctx) => {
router.post("collections.create", auth(), async (ctx: APIContext) => {
const {
name,
color = randomElement(colorPalette),
@@ -73,7 +71,7 @@ router.post("collections.create", auth(), async (ctx) => {
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const { user } = ctx.state;
const { user } = ctx.state.auth;
authorize(user, "createCollection", user.team);
if (index) {
@@ -134,10 +132,10 @@ router.post("collections.create", auth(), async (ctx) => {
};
});
router.post("collections.info", auth(), async (ctx) => {
router.post("collections.info", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertPresent(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
@@ -154,14 +152,14 @@ router.post(
"collections.import",
auth(),
rateLimiter(RateLimiterStrategy.TenPerHour),
async (ctx) => {
async (ctx: APIContext) => {
const {
attachmentId,
format = FileOperationFormat.MarkdownZip,
} = ctx.request.body;
assertUuid(attachmentId, "attachmentId is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
authorize(user, "importCollection", user.team);
const attachment = await Attachment.findByPk(attachmentId);
@@ -207,7 +205,7 @@ router.post(
}
);
router.post("collections.add_group", auth(), async (ctx) => {
router.post("collections.add_group", auth(), async (ctx: APIContext) => {
const {
id,
groupId,
@@ -217,13 +215,15 @@ router.post("collections.add_group", auth(), async (ctx) => {
assertUuid(groupId, "groupId is required");
assertCollectionPermission(permission);
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
authorize(user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, "read", group);
authorize(user, "read", group);
let membership = await CollectionGroup.findOne({
where: {
@@ -237,7 +237,7 @@ router.post("collections.add_group", auth(), async (ctx) => {
collectionId: id,
groupId,
permission,
createdById: ctx.state.user.id,
createdById: user.id,
});
} else if (permission) {
membership.permission = permission;
@@ -248,7 +248,7 @@ router.post("collections.add_group", auth(), async (ctx) => {
name: "collections.add_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
actorId: user.id,
modelId: groupId,
data: {
name: group.name,
@@ -265,25 +265,27 @@ router.post("collections.add_group", auth(), async (ctx) => {
};
});
router.post("collections.remove_group", auth(), async (ctx) => {
router.post("collections.remove_group", auth(), async (ctx: APIContext) => {
const { id, groupId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(groupId, "groupId is required");
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
method: ["withMembership", user.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
authorize(user, "update", collection);
const group = await Group.findByPk(groupId);
authorize(ctx.state.user, "read", group);
authorize(user, "read", group);
await collection.$remove("group", group);
await Event.create({
name: "collections.remove_group",
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
actorId: user.id,
modelId: groupId,
data: {
name: group.name,
@@ -300,10 +302,10 @@ router.post(
"collections.group_memberships",
auth(),
pagination(),
async (ctx) => {
async (ctx: APIContext) => {
const { id, query, permission } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
@@ -365,19 +367,20 @@ router.post(
"collections.add_user",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { transaction } = ctx.state;
async (ctx: APIContext) => {
const { auth, transaction } = ctx.state;
const actor = auth.user;
const { id, userId, permission } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
method: ["withMembership", actor.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
authorize(actor, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
authorize(actor, "read", user);
let membership = await CollectionUser.findOne({
where: {
@@ -388,7 +391,7 @@ router.post(
lock: transaction.LOCK.UPDATE,
});
if (userId === ctx.state.user.id) {
if (userId === actor.id) {
throw AuthorizationError("You cannot add yourself to a collection");
}
@@ -402,7 +405,7 @@ router.post(
collectionId: id,
userId,
permission: permission || user.defaultCollectionPermission,
createdById: ctx.state.user.id,
createdById: actor.id,
},
{
transaction,
@@ -419,7 +422,7 @@ router.post(
userId,
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
actorId: actor.id,
data: {
name: user.name,
},
@@ -443,19 +446,20 @@ router.post(
"collections.remove_user",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { transaction } = ctx.state;
async (ctx: APIContext) => {
const { auth, transaction } = ctx.state;
const actor = auth.user;
const { id, userId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const collection = await Collection.scope({
method: ["withMembership", ctx.state.user.id],
method: ["withMembership", actor.id],
}).findByPk(id);
authorize(ctx.state.user, "update", collection);
authorize(actor, "update", collection);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
authorize(actor, "read", user);
await collection.$remove("user", user, { transaction });
await Event.create(
@@ -464,7 +468,7 @@ router.post(
userId,
collectionId: collection.id,
teamId: collection.teamId,
actorId: ctx.state.user.id,
actorId: actor.id,
data: {
name: user.name,
},
@@ -479,77 +483,82 @@ router.post(
}
);
router.post("collections.memberships", auth(), pagination(), async (ctx) => {
const { id, query, permission } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
router.post(
"collections.memberships",
auth(),
pagination(),
async (ctx: APIContext) => {
const { id, query, permission } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
authorize(user, "read", collection);
let where: WhereOptions<CollectionUser> = {
collectionId: id,
};
let userWhere;
let where: WhereOptions<CollectionUser> = {
collectionId: id,
};
let userWhere;
if (query) {
userWhere = {
name: {
[Op.iLike]: `%${query}%`,
if (query) {
userWhere = {
name: {
[Op.iLike]: `%${query}%`,
},
};
}
if (permission) {
assertCollectionPermission(permission);
where = { ...where, permission };
}
const options = {
where,
include: [
{
model: User,
as: "user",
where: userWhere,
required: true,
},
],
};
const [total, memberships] = await Promise.all([
CollectionUser.count(options),
CollectionUser.findAll({
...options,
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
}),
]);
ctx.body = {
pagination: { ...ctx.state.pagination, total },
data: {
memberships: memberships.map(presentMembership),
users: memberships.map((membership) => presentUser(membership.user)),
},
};
}
if (permission) {
assertCollectionPermission(permission);
where = { ...where, permission };
}
const options = {
where,
include: [
{
model: User,
as: "user",
where: userWhere,
required: true,
},
],
};
const [total, memberships] = await Promise.all([
CollectionUser.count(options),
CollectionUser.findAll({
...options,
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
}),
]);
ctx.body = {
pagination: { ...ctx.state.pagination, total },
data: {
memberships: memberships.map(presentMembership),
users: memberships.map((membership) => presentUser(membership.user)),
},
};
});
);
router.post(
"collections.export",
auth(),
rateLimiter(RateLimiterStrategy.TenPerHour),
async (ctx) => {
async (ctx: APIContext) => {
const { id } = ctx.request.body;
const { format = FileOperationFormat.MarkdownZip } = ctx.request.body;
assertUuid(id, "id is required");
assertIn(format, Object.values(FileOperationFormat), "Invalid format");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId);
authorize(user, "createExport", team);
@@ -582,9 +591,9 @@ router.post(
"collections.export_all",
auth(),
rateLimiter(RateLimiterStrategy.FivePerHour),
async (ctx) => {
async (ctx: APIContext) => {
const { format = FileOperationFormat.MarkdownZip } = ctx.request.body;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId);
authorize(user, "createExport", team);
@@ -609,7 +618,7 @@ router.post(
}
);
router.post("collections.update", auth(), async (ctx) => {
router.post("collections.update", auth(), async (ctx: APIContext) => {
const {
id,
name,
@@ -625,7 +634,7 @@ router.post("collections.update", auth(), async (ctx) => {
assertHexColor(color, "Invalid hex value (please use format #FFFFFF)");
}
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(id);
@@ -736,43 +745,48 @@ router.post("collections.update", auth(), async (ctx) => {
};
});
router.post("collections.list", auth(), pagination(), async (ctx) => {
const { user } = ctx.state;
const collectionIds = await user.collectionIds();
const where: WhereOptions<Collection> = {
teamId: user.teamId,
id: collectionIds,
};
const collections = await Collection.scope({
method: ["withMembership", user.id],
}).findAll({
where,
order: [["updatedAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const nullIndex = collections.findIndex(
(collection) => collection.index === null
);
if (nullIndex !== -1) {
const indexedCollections = await collectionIndexing(user.teamId);
collections.forEach((collection) => {
collection.index = indexedCollections[collection.id];
router.post(
"collections.list",
auth(),
pagination(),
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const collectionIds = await user.collectionIds();
const where: WhereOptions<Collection> = {
teamId: user.teamId,
id: collectionIds,
};
const collections = await Collection.scope({
method: ["withMembership", user.id],
}).findAll({
where,
order: [["updatedAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const nullIndex = collections.findIndex(
(collection) => collection.index === null
);
if (nullIndex !== -1) {
const indexedCollections = await collectionIndexing(user.teamId);
collections.forEach((collection) => {
collection.index = indexedCollections[collection.id];
});
}
ctx.body = {
pagination: ctx.state.pagination,
data: collections.map(presentCollection),
policies: presentPolicies(user, collections),
};
}
);
ctx.body = {
pagination: ctx.state.pagination,
data: collections.map(presentCollection),
policies: presentPolicies(user, collections),
};
});
router.post("collections.delete", auth(), async (ctx) => {
router.post("collections.delete", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
const { user } = ctx.state;
const { user } = ctx.state.auth;
assertUuid(id, "id is required");
const collection = await Collection.scope({
@@ -814,13 +828,13 @@ router.post("collections.delete", auth(), async (ctx) => {
};
});
router.post("collections.move", auth(), async (ctx) => {
router.post("collections.move", auth(), async (ctx: APIContext) => {
const id = ctx.request.body.id;
let index = ctx.request.body.index;
assertPresent(index, "index is required");
assertIndexCharacters(index);
assertUuid(id, "id must be a uuid");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collection = await Collection.findByPk(id);
authorize(user, "move", collection);

View File

@@ -6,6 +6,7 @@ import env from "@server/env";
import Logger from "@server/logging/Logger";
import auth from "@server/middlewares/authentication";
import { presentUser } from "@server/presenters";
import { APIContext } from "@server/types";
const router = new Router();
@@ -19,40 +20,45 @@ function dev() {
};
}
router.post("developer.create_test_users", dev(), auth(), async (ctx) => {
const { count = 10 } = ctx.request.body;
const { user } = ctx.state;
const invites = Array(Math.min(count, 100))
.fill(0)
.map(() => {
const rando = randomstring.generate(10);
router.post(
"developer.create_test_users",
dev(),
auth(),
async (ctx: APIContext) => {
const { count = 10 } = ctx.request.body;
const { user } = ctx.state.auth;
const invites = Array(Math.min(count, 100))
.fill(0)
.map(() => {
const rando = randomstring.generate(10);
return {
email: `${rando}@example.com`,
name: `${rando.slice(0, 5)} Tester`,
role: "member",
} as Invite;
return {
email: `${rando}@example.com`,
name: `${rando.slice(0, 5)} Tester`,
role: "member",
} as Invite;
});
Logger.info("utils", `Creating ${count} test users`, invites);
// Generate a bunch of invites
const response = await userInviter({
user,
invites,
ip: ctx.request.ip,
});
Logger.info("utils", `Creating ${count} test users`, invites);
// Convert from invites to active users by marking as active
await Promise.all(
response.users.map((user) => user.updateActiveAt(ctx, true))
);
// Generate a bunch of invites
const response = await userInviter({
user,
invites,
ip: ctx.request.ip,
});
// Convert from invites to active users by marking as active
await Promise.all(
response.users.map((user) => user.updateActiveAt(ctx, true))
);
ctx.body = {
data: {
users: response.users.map((user) => presentUser(user)),
},
};
});
ctx.body = {
data: {
users: response.users.map((user) => presentUser(user)),
},
};
}
);
export default router;

View File

@@ -70,7 +70,7 @@ router.post(
} = ctx.input;
// always filter by the current team
const { user } = ctx.state;
const { user } = ctx.state.auth;
let where: WhereOptions<Document> = {
teamId: user.teamId,
archivedAt: {
@@ -177,7 +177,7 @@ router.post(
validate(T.DocumentsArchivedSchema),
async (ctx: APIContext<T.DocumentsArchivedReq>) => {
const { sort, direction } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collectionIds = await user.collectionIds();
const collectionScope: Readonly<ScopeOptions> = {
method: ["withCollectionPermissions", user.id],
@@ -221,7 +221,7 @@ router.post(
validate(T.DocumentsDeletedSchema),
async (ctx: APIContext<T.DocumentsDeletedReq>) => {
const { sort, direction } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collectionIds = await user.collectionIds({
paranoid: false,
});
@@ -281,7 +281,7 @@ router.post(
validate(T.DocumentsViewedSchema),
async (ctx: APIContext<T.DocumentsViewedReq>) => {
const { sort, direction } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collectionIds = await user.collectionIds();
const userId = user.id;
const views = await View.findAll({
@@ -334,7 +334,7 @@ router.post(
validate(T.DocumentsDraftsSchema),
async (ctx: APIContext<T.DocumentsDraftsReq>) => {
const { collectionId, dateFilter, direction, sort } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
if (collectionId) {
const collection = await Collection.scope({
@@ -397,7 +397,7 @@ router.post(
validate(T.DocumentsInfoSchema),
async (ctx: APIContext<T.DocumentsInfoReq>) => {
const { id, shareId, apiVersion } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const teamFromCtx = await getTeamFromContext(ctx);
const { document, share, collection } = await documentLoader({
id,
@@ -443,7 +443,7 @@ router.post(
validate(T.DocumentsExportSchema),
async (ctx: APIContext<T.DocumentsExportReq>) => {
const { id } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const accept = ctx.request.headers["accept"];
const { document } = await documentLoader({
@@ -495,7 +495,7 @@ router.post(
validate(T.DocumentsRestoreSchema),
async (ctx: APIContext<T.DocumentsRestoreReq>) => {
const { id, collectionId, revisionId } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(id, {
userId: user.id,
paranoid: false,
@@ -606,7 +606,7 @@ router.post(
userId,
} = ctx.input;
const { offset, limit } = ctx.state.pagination;
const { user } = ctx.state;
const { user } = ctx.state.auth;
let collaboratorIds = undefined;
if (collectionId) {
@@ -663,9 +663,8 @@ router.post(
} = ctx.input;
const { offset, limit } = ctx.state.pagination;
// this typing is a bit ugly, would be better to use a type like ContextWithState
// but that doesn't adequately handle cases when auth is optional
const { user }: { user: User | undefined } = ctx.state;
// Unfortunately, this still doesn't adequately handle cases when auth is optional
const { user } = ctx.state.auth;
let teamId;
let response;
@@ -747,7 +746,7 @@ router.post(
userId: user?.id,
teamId,
shareId,
source: ctx.state.authType || "app", // we'll consider anything that isn't "api" to be "app"
source: ctx.state.auth.type || "app", // we'll consider anything that isn't "api" to be "app"
query,
results: totalCount,
});
@@ -767,7 +766,7 @@ router.post(
validate(T.DocumentsTemplatizeSchema),
async (ctx: APIContext<T.DocumentsTemplatizeReq>) => {
const { id } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const original = await Document.findByPk(id, {
userId: user.id,
@@ -829,7 +828,7 @@ router.post(
append,
} = ctx.input;
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
const { user } = ctx.state;
const { user } = ctx.state.auth;
let collection: Collection | null | undefined;
const document = await Document.findByPk(id, {
@@ -889,7 +888,7 @@ router.post(
validate(T.DocumentsMoveSchema),
async (ctx: APIContext<T.DocumentsMoveReq>) => {
const { id, collectionId, parentDocumentId, index } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(id, {
userId: user.id,
});
@@ -943,7 +942,7 @@ router.post(
validate(T.DocumentsArchiveSchema),
async (ctx: APIContext<T.DocumentsArchiveReq>) => {
const { id } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(id, {
userId: user.id,
@@ -976,7 +975,7 @@ router.post(
validate(T.DocumentsDeleteSchema),
async (ctx: APIContext<T.DocumentsDeleteReq>) => {
const { id, permanent } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
if (permanent) {
const document = await Document.findByPk(id, {
@@ -1041,7 +1040,7 @@ router.post(
validate(T.DocumentsUnpublishSchema),
async (ctx: APIContext<T.DocumentsUnpublishReq>) => {
const { id } = ctx.input;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(id, {
userId: user.id,
@@ -1103,7 +1102,7 @@ router.post(
);
}
const { user } = ctx.state;
const { user } = ctx.state.auth;
const collection = await Collection.scope({
method: ["withMembership", user.id],
@@ -1177,7 +1176,7 @@ router.post(
} = ctx.input;
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
const { user } = ctx.state;
const { user } = ctx.state.auth;
let collection;

View File

@@ -17,7 +17,7 @@ router.post(
pagination(),
validate(T.EventsListSchema),
async (ctx: APIContext<T.EventsListReq>) => {
const { user } = ctx.state;
const { user } = ctx.state.auth;
const {
sort,
direction,

View File

@@ -7,33 +7,37 @@ import auth from "@server/middlewares/authentication";
import { FileOperation, Team } from "@server/models";
import { authorize } from "@server/policies";
import { presentFileOperation } from "@server/presenters";
import { ContextWithState } from "@server/types";
import { APIContext } from "@server/types";
import { getSignedUrl } from "@server/utils/s3";
import { assertIn, assertSort, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("fileOperations.info", auth({ admin: true }), async (ctx) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const fileOperation = await FileOperation.findByPk(id, {
rejectOnEmpty: true,
});
router.post(
"fileOperations.info",
auth({ admin: true }),
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state.auth;
const fileOperation = await FileOperation.findByPk(id, {
rejectOnEmpty: true,
});
authorize(user, "read", fileOperation);
authorize(user, "read", fileOperation);
ctx.body = {
data: presentFileOperation(fileOperation),
};
});
ctx.body = {
data: presentFileOperation(fileOperation),
};
}
);
router.post(
"fileOperations.list",
auth({ admin: true }),
pagination(),
async (ctx) => {
async (ctx: APIContext) => {
let { direction } = ctx.request.body;
const { sort = "createdAt", type } = ctx.request.body;
assertIn(type, Object.values(FileOperationType));
@@ -42,7 +46,7 @@ router.post(
if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state;
const { user } = ctx.state.auth;
const where: WhereOptions<FileOperation> = {
teamId: user.teamId,
type,
@@ -69,11 +73,11 @@ router.post(
}
);
const handleFileOperationsRedirect = async (ctx: ContextWithState) => {
const handleFileOperationsRedirect = async (ctx: APIContext) => {
const id = ctx.request.body?.id ?? ctx.request.query?.id;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const fileOperation = await FileOperation.unscoped().findByPk(id, {
rejectOnEmpty: true,
});
@@ -98,21 +102,25 @@ router.post(
handleFileOperationsRedirect
);
router.post("fileOperations.delete", auth({ admin: true }), async (ctx) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
router.post(
"fileOperations.delete",
auth({ admin: true }),
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const fileOperation = await FileOperation.unscoped().findByPk(id, {
rejectOnEmpty: true,
});
authorize(user, "delete", fileOperation);
const { user } = ctx.state.auth;
const fileOperation = await FileOperation.unscoped().findByPk(id, {
rejectOnEmpty: true,
});
authorize(user, "delete", fileOperation);
await fileOperationDeleter(fileOperation, user, ctx.request.ip);
await fileOperationDeleter(fileOperation, user, ctx.request.ip);
ctx.body = {
success: true,
};
});
ctx.body = {
success: true,
};
}
);
export default router;

View File

@@ -10,12 +10,13 @@ import {
presentUser,
presentGroupMembership,
} from "@server/presenters";
import { APIContext } from "@server/types";
import { assertPresent, assertUuid, assertSort } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("groups.list", auth(), pagination(), async (ctx) => {
router.post("groups.list", auth(), pagination(), async (ctx: APIContext) => {
let { direction } = ctx.request.body;
const { sort = "updatedAt" } = ctx.request.body;
if (direction !== "ASC") {
@@ -23,7 +24,7 @@ router.post("groups.list", auth(), pagination(), async (ctx) => {
}
assertSort(sort, Group);
const { user } = ctx.state;
const { user } = ctx.state.auth;
const groups = await Group.findAll({
where: {
teamId: user.teamId,
@@ -52,11 +53,11 @@ router.post("groups.list", auth(), pagination(), async (ctx) => {
};
});
router.post("groups.info", auth(), async (ctx) => {
router.post("groups.info", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const group = await Group.findByPk(id);
authorize(user, "read", group);
@@ -66,11 +67,11 @@ router.post("groups.info", auth(), async (ctx) => {
};
});
router.post("groups.create", auth(), async (ctx) => {
router.post("groups.create", auth(), async (ctx: APIContext) => {
const { name } = ctx.request.body;
assertPresent(name, "name is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
authorize(user, "createGroup", user.team);
const g = await Group.create({
name,
@@ -98,12 +99,12 @@ router.post("groups.create", auth(), async (ctx) => {
};
});
router.post("groups.update", auth(), async (ctx) => {
router.post("groups.update", auth(), async (ctx: APIContext) => {
const { id, name } = ctx.request.body;
assertPresent(name, "name is required");
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const group = await Group.findByPk(id);
authorize(user, "update", group);
@@ -129,11 +130,11 @@ router.post("groups.update", auth(), async (ctx) => {
};
});
router.post("groups.delete", auth(), async (ctx) => {
router.post("groups.delete", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const group = await Group.findByPk(id);
authorize(user, "delete", group);
@@ -154,61 +155,68 @@ router.post("groups.delete", auth(), async (ctx) => {
};
});
router.post("groups.memberships", auth(), pagination(), async (ctx) => {
const { id, query } = ctx.request.body;
assertUuid(id, "id is required");
router.post(
"groups.memberships",
auth(),
pagination(),
async (ctx: APIContext) => {
const { id, query } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const group = await Group.findByPk(id);
authorize(user, "read", group);
let userWhere;
const { user } = ctx.state.auth;
const group = await Group.findByPk(id);
authorize(user, "read", group);
let userWhere;
if (query) {
userWhere = {
name: {
[Op.iLike]: `%${query}%`,
if (query) {
userWhere = {
name: {
[Op.iLike]: `%${query}%`,
},
};
}
const memberships = await GroupUser.findAll({
where: {
groupId: id,
},
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
include: [
{
model: User,
as: "user",
where: userWhere,
required: true,
},
],
});
ctx.body = {
pagination: ctx.state.pagination,
data: {
groupMemberships: memberships.map((membership) =>
presentGroupMembership(membership, { includeUser: true })
),
users: memberships.map((membership) => presentUser(membership.user)),
},
};
}
);
const memberships = await GroupUser.findAll({
where: {
groupId: id,
},
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
include: [
{
model: User,
as: "user",
where: userWhere,
required: true,
},
],
});
ctx.body = {
pagination: ctx.state.pagination,
data: {
groupMemberships: memberships.map((membership) =>
presentGroupMembership(membership, { includeUser: true })
),
users: memberships.map((membership) => presentUser(membership.user)),
},
};
});
router.post("groups.add_user", auth(), async (ctx) => {
router.post("groups.add_user", auth(), async (ctx: APIContext) => {
const { id, userId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const actor = ctx.state.auth.user;
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
authorize(actor, "read", user);
let group = await Group.findByPk(id);
authorize(ctx.state.user, "update", group);
authorize(actor, "update", group);
let membership = await GroupUser.findOne({
where: {
@@ -220,7 +228,7 @@ router.post("groups.add_user", auth(), async (ctx) => {
if (!membership) {
await group.$add("user", user, {
through: {
createdById: ctx.state.user.id,
createdById: actor.id,
},
});
// reload to get default scope
@@ -240,7 +248,7 @@ router.post("groups.add_user", auth(), async (ctx) => {
userId,
teamId: user.teamId,
modelId: group.id,
actorId: ctx.state.user.id,
actorId: actor.id,
data: {
name: user.name,
},
@@ -259,16 +267,18 @@ router.post("groups.add_user", auth(), async (ctx) => {
};
});
router.post("groups.remove_user", auth(), async (ctx) => {
router.post("groups.remove_user", auth(), async (ctx: APIContext) => {
const { id, userId } = ctx.request.body;
assertUuid(id, "id is required");
assertUuid(userId, "userId is required");
const actor = ctx.state.auth.user;
let group = await Group.findByPk(id);
authorize(ctx.state.user, "update", group);
authorize(actor, "update", group);
const user = await User.findByPk(userId);
authorize(ctx.state.user, "read", user);
authorize(actor, "read", user);
await group.$remove("user", user);
await Event.create({
@@ -276,7 +286,7 @@ router.post("groups.remove_user", auth(), async (ctx) => {
userId,
modelId: group.id,
teamId: user.teamId,
actorId: ctx.state.user.id,
actorId: actor.id,
data: {
name: user.name,
},

View File

@@ -17,6 +17,7 @@ import {
} from "@server/models";
import SearchHelper from "@server/models/helpers/SearchHelper";
import { presentSlackAttachment } from "@server/presenters";
import { APIContext } from "@server/types";
import * as Slack from "@server/utils/slack";
import { assertPresent } from "@server/validation";
@@ -41,7 +42,7 @@ function verifySlackToken(token: string) {
}
// triggered by a user posting a getoutline.com link in Slack
router.post("hooks.unfurl", async (ctx) => {
router.post("hooks.unfurl", async (ctx: APIContext) => {
const { challenge, token, event } = ctx.request.body;
if (challenge) {
return (ctx.body = ctx.request.body.challenge);
@@ -104,7 +105,7 @@ router.post("hooks.unfurl", async (ctx) => {
});
// triggered by interactions with actions, dialogs, message buttons in Slack
router.post("hooks.interactive", async (ctx) => {
router.post("hooks.interactive", async (ctx: APIContext) => {
const { payload } = ctx.request.body;
assertPresent(payload, "payload is required");
@@ -142,7 +143,7 @@ router.post("hooks.interactive", async (ctx) => {
});
// triggered by the /outline command in Slack
router.post("hooks.slack", async (ctx) => {
router.post("hooks.slack", async (ctx: APIContext) => {
const { token, team_id, user_id, text = "" } = ctx.request.body;
assertPresent(token, "token is required");
assertPresent(team_id, "team_id is required");

View File

@@ -1,10 +1,10 @@
import Koa, { BaseContext, DefaultContext, DefaultState } from "koa";
import Koa, { BaseContext } from "koa";
import bodyParser from "koa-body";
import Router from "koa-router";
import userAgent, { UserAgentContext } from "koa-useragent";
import env from "@server/env";
import { NotFoundError } from "@server/errors";
import { AuthenticatedState } from "@server/types";
import { AppState, AppContext } from "@server/types";
import apiKeys from "./apiKeys";
import attachments from "./attachments";
import auth from "./auth";
@@ -32,10 +32,7 @@ import users from "./users";
import views from "./views";
import webhookSubscriptions from "./webhookSubscriptions";
const api = new Koa<
DefaultState & AuthenticatedState,
DefaultContext & { body: Record<string, any> }
>();
const api = new Koa<AppState, AppContext>();
const router = new Router();
// middlewares

View File

@@ -9,6 +9,7 @@ import Integration, {
} from "@server/models/Integration";
import { authorize } from "@server/policies";
import { presentIntegration } from "@server/presenters";
import { APIContext } from "@server/types";
import {
assertSort,
assertUuid,
@@ -20,116 +21,133 @@ import pagination from "./middlewares/pagination";
const router = new Router();
router.post("integrations.list", auth(), pagination(), async (ctx) => {
let { direction } = ctx.request.body;
const { user } = ctx.state;
const { type, sort = "updatedAt" } = ctx.request.body;
if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Integration);
router.post(
"integrations.list",
auth(),
pagination(),
async (ctx: APIContext) => {
let { direction } = ctx.request.body;
const { user } = ctx.state.auth;
const { type, sort = "updatedAt" } = ctx.request.body;
if (direction !== "ASC") {
direction = "DESC";
}
assertSort(sort, Integration);
let where: WhereOptions<Integration> = {
teamId: user.teamId,
};
let where: WhereOptions<Integration> = {
teamId: user.teamId,
};
if (type) {
assertIn(type, Object.values(IntegrationType));
where = {
...where,
type,
if (type) {
assertIn(type, Object.values(IntegrationType));
where = {
...where,
type,
};
}
const integrations = await Integration.findAll({
where,
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
data: integrations.map(presentIntegration),
};
}
);
const integrations = await Integration.findAll({
where,
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
router.post(
"integrations.create",
auth({ admin: true }),
async (ctx: APIContext) => {
const { type, service, settings } = ctx.request.body;
ctx.body = {
pagination: ctx.state.pagination,
data: integrations.map(presentIntegration),
};
});
assertIn(type, Object.values(IntegrationType));
router.post("integrations.create", auth({ admin: true }), async (ctx) => {
const { type, service, settings } = ctx.request.body;
const { user } = ctx.state.auth;
authorize(user, "createIntegration", user.team);
assertIn(type, Object.values(IntegrationType));
assertIn(service, Object.values(UserCreatableIntegrationService));
const { user } = ctx.state;
authorize(user, "createIntegration", user.team);
if (has(settings, "url")) {
assertUrl(settings.url);
}
assertIn(service, Object.values(UserCreatableIntegrationService));
const integration = await Integration.create({
userId: user.id,
teamId: user.teamId,
service,
settings,
type,
});
if (has(settings, "url")) {
assertUrl(settings.url);
ctx.body = {
data: presentIntegration(integration),
};
}
);
const integration = await Integration.create({
userId: user.id,
teamId: user.teamId,
service,
settings,
type,
});
router.post(
"integrations.update",
auth({ admin: true }),
async (ctx: APIContext) => {
const { id, events = [], settings } = ctx.request.body;
assertUuid(id, "id is required");
ctx.body = {
data: presentIntegration(integration),
};
});
const { user } = ctx.state.auth;
const integration = await Integration.findByPk(id);
authorize(user, "update", integration);
router.post("integrations.update", auth({ admin: true }), async (ctx) => {
const { id, events = [], settings } = ctx.request.body;
assertUuid(id, "id is required");
assertArray(events, "events must be an array");
const { user } = ctx.state;
const integration = await Integration.findByPk(id);
authorize(user, "update", integration);
if (has(settings, "url")) {
assertUrl(settings.url);
}
assertArray(events, "events must be an array");
if (integration.type === IntegrationType.Post) {
integration.events = events.filter((event: string) =>
["documents.update", "documents.publish"].includes(event)
);
}
if (has(settings, "url")) {
assertUrl(settings.url);
integration.settings = settings;
await integration.save();
ctx.body = {
data: presentIntegration(integration),
};
}
);
if (integration.type === IntegrationType.Post) {
integration.events = events.filter((event: string) =>
["documents.update", "documents.publish"].includes(event)
);
router.post(
"integrations.delete",
auth({ admin: true }),
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state.auth;
const integration = await Integration.findByPk(id);
authorize(user, "delete", integration);
await integration.destroy();
await Event.create({
name: "integrations.delete",
modelId: integration.id,
teamId: integration.teamId,
actorId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
}
integration.settings = settings;
await integration.save();
ctx.body = {
data: presentIntegration(integration),
};
});
router.post("integrations.delete", auth({ admin: true }), async (ctx) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const integration = await Integration.findByPk(id);
authorize(user, "delete", integration);
await integration.destroy();
await Event.create({
name: "integrations.delete",
modelId: integration.id,
teamId: integration.teamId,
actorId: user.id,
ip: ctx.request.ip,
});
ctx.body = {
success: true,
};
});
);
export default router;

View File

@@ -1,14 +1,14 @@
import querystring from "querystring";
import { Context, Next } from "koa";
import { Next } from "koa";
import { InvalidRequestError } from "@server/errors";
import { AppContext } from "@server/types";
export default function pagination(options?: Record<string, any>) {
return async function paginationMiddleware(ctx: Context, next: Next) {
export default function pagination() {
return async function paginationMiddleware(ctx: AppContext, next: Next) {
const opts = {
defaultLimit: 15,
defaultOffset: 0,
maxLimit: 100,
...options,
};
const query = ctx.request.query;
const body = ctx.request.body;
@@ -42,18 +42,15 @@ export default function pagination(options?: Record<string, any>) {
);
}
query.limit = String(limit);
query.offset = String(limit + offset);
ctx.state.pagination = {
limit,
offset,
nextPath: `/api${ctx.request.path}?${querystring.stringify(query)}`,
};
query.limit = ctx.state.pagination.limit;
query.offset = ctx.state.pagination.offset + query.limit;
ctx.state.pagination.nextPath = `/api${
ctx.request.path
}?${querystring.stringify(query)}`;
return next();
};
}

View File

@@ -4,16 +4,16 @@ import auth from "@server/middlewares/authentication";
import { Team, NotificationSetting } from "@server/models";
import { authorize } from "@server/policies";
import { presentNotificationSetting } from "@server/presenters";
import { ContextWithState } from "@server/types";
import { APIContext } from "@server/types";
import { assertPresent, assertUuid } from "@server/validation";
const router = new Router();
router.post("notificationSettings.create", auth(), async (ctx) => {
router.post("notificationSettings.create", auth(), async (ctx: APIContext) => {
const { event } = ctx.request.body;
assertPresent(event, "event is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
authorize(user, "createNotificationSetting", user.team);
const [setting] = await NotificationSetting.findOrCreate({
where: {
@@ -28,8 +28,8 @@ router.post("notificationSettings.create", auth(), async (ctx) => {
};
});
router.post("notificationSettings.list", auth(), async (ctx) => {
const { user } = ctx.state;
router.post("notificationSettings.list", auth(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const settings = await NotificationSetting.findAll({
where: {
userId: user.id,
@@ -41,11 +41,11 @@ router.post("notificationSettings.list", auth(), async (ctx) => {
};
});
router.post("notificationSettings.delete", auth(), async (ctx) => {
router.post("notificationSettings.delete", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const setting = await NotificationSetting.findByPk(id);
authorize(user, "delete", setting);
@@ -56,7 +56,7 @@ router.post("notificationSettings.delete", auth(), async (ctx) => {
};
});
const handleUnsubscribe = async (ctx: ContextWithState) => {
const handleUnsubscribe = async (ctx: APIContext) => {
const { id, token } = (ctx.method === "POST"
? ctx.request.body
: ctx.request.query) as {

View File

@@ -11,17 +11,18 @@ import {
presentDocument,
presentPolicies,
} from "@server/presenters";
import { APIContext } from "@server/types";
import { assertUuid, assertIndexCharacters } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("pins.create", auth(), async (ctx) => {
router.post("pins.create", auth(), async (ctx: APIContext) => {
const { documentId, collectionId } = ctx.request.body;
const { index } = ctx.request.body;
assertUuid(documentId, "documentId is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
@@ -55,9 +56,9 @@ router.post("pins.create", auth(), async (ctx) => {
};
});
router.post("pins.list", auth(), pagination(), async (ctx) => {
router.post("pins.list", auth(), pagination(), async (ctx: APIContext) => {
const { collectionId } = ctx.request.body;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const [pins, collectionIds] = await Promise.all([
Pin.findAll({
@@ -98,13 +99,13 @@ router.post("pins.list", auth(), pagination(), async (ctx) => {
};
});
router.post("pins.update", auth(), async (ctx) => {
router.post("pins.update", auth(), async (ctx: APIContext) => {
const { id, index } = ctx.request.body;
assertUuid(id, "id is required");
assertIndexCharacters(index);
const { user } = ctx.state;
const { user } = ctx.state.auth;
let pin = await Pin.findByPk(id, { rejectOnEmpty: true });
const document = await Document.findByPk(pin.documentId, {
@@ -130,11 +131,11 @@ router.post("pins.update", auth(), async (ctx) => {
};
});
router.post("pins.delete", auth(), async (ctx) => {
router.post("pins.delete", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const pin = await Pin.findByPk(id, { rejectOnEmpty: true });
const document = await Document.findByPk(pin.documentId, {

View File

@@ -6,16 +6,17 @@ import { Document, Revision } from "@server/models";
import DocumentHelper from "@server/models/helpers/DocumentHelper";
import { authorize } from "@server/policies";
import { presentRevision } from "@server/presenters";
import { APIContext } from "@server/types";
import slugify from "@server/utils/slugify";
import { assertPresent, assertSort, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("revisions.info", auth(), async (ctx) => {
router.post("revisions.info", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const revision = await Revision.findByPk(id, {
rejectOnEmpty: true,
});
@@ -38,11 +39,11 @@ router.post("revisions.info", auth(), async (ctx) => {
};
});
router.post("revisions.diff", auth(), async (ctx) => {
router.post("revisions.diff", auth(), async (ctx: APIContext) => {
const { id, compareToId } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const revision = await Revision.findByPk(id, {
rejectOnEmpty: true,
});
@@ -92,7 +93,7 @@ router.post("revisions.diff", auth(), async (ctx) => {
};
});
router.post("revisions.list", auth(), pagination(), async (ctx) => {
router.post("revisions.list", auth(), pagination(), async (ctx: APIContext) => {
let { direction } = ctx.request.body;
const { documentId, sort = "updatedAt" } = ctx.request.body;
if (direction !== "ASC") {
@@ -101,7 +102,7 @@ router.post("revisions.list", auth(), pagination(), async (ctx) => {
assertSort(sort, Revision);
assertPresent(documentId, "documentId is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(documentId, {
userId: user.id,
});

View File

@@ -2,13 +2,14 @@ import Router from "koa-router";
import auth from "@server/middlewares/authentication";
import { SearchQuery } from "@server/models";
import { presentSearchQuery } from "@server/presenters";
import { APIContext } from "@server/types";
import { assertPresent, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("searches.list", auth(), pagination(), async (ctx) => {
const { user } = ctx.state;
router.post("searches.list", auth(), pagination(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const searches = await SearchQuery.findAll({
where: {
@@ -25,14 +26,14 @@ router.post("searches.list", auth(), pagination(), async (ctx) => {
};
});
router.post("searches.delete", auth(), async (ctx) => {
router.post("searches.delete", auth(), async (ctx: APIContext) => {
const { id, query } = ctx.request.body;
assertPresent(id || query, "id or query is required");
if (id) {
assertUuid(id, "id is must be a uuid");
}
const { user } = ctx.state;
const { user } = ctx.state.auth;
await SearchQuery.destroy({
where: {
...(id ? { id } : { query }),

View File

@@ -6,12 +6,13 @@ import auth from "@server/middlewares/authentication";
import { Document, User, Event, Share, Team, Collection } from "@server/models";
import { authorize } from "@server/policies";
import { presentShare, presentPolicies } from "@server/presenters";
import { APIContext } from "@server/types";
import { assertUuid, assertSort, assertPresent } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("shares.info", auth(), async (ctx) => {
router.post("shares.info", auth(), async (ctx: APIContext) => {
const { id, documentId } = ctx.request.body;
assertPresent(id || documentId, "id or documentId is required");
if (id) {
@@ -21,7 +22,7 @@ router.post("shares.info", auth(), async (ctx) => {
assertUuid(documentId, "documentId is must be a uuid");
}
const { user } = ctx.state;
const { user } = ctx.state.auth;
const shares = [];
const share = await Share.scope({
method: ["withCollectionPermissions", user.id],
@@ -92,7 +93,7 @@ router.post("shares.info", auth(), async (ctx) => {
};
});
router.post("shares.list", auth(), pagination(), async (ctx) => {
router.post("shares.list", auth(), pagination(), async (ctx: APIContext) => {
let { direction } = ctx.request.body;
const { sort = "updatedAt" } = ctx.request.body;
if (direction !== "ASC") {
@@ -100,7 +101,7 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
}
assertSort(sort, Share);
const { user } = ctx.state;
const { user } = ctx.state.auth;
const where: WhereOptions<Share> = {
teamId: user.teamId,
userId: user.id,
@@ -162,11 +163,11 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
};
});
router.post("shares.update", auth(), async (ctx) => {
router.post("shares.update", auth(), async (ctx: APIContext) => {
const { id, includeChildDocuments, published, urlId } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId);
authorize(user, "share", team);
@@ -215,11 +216,11 @@ router.post("shares.update", auth(), async (ctx) => {
};
});
router.post("shares.create", auth(), async (ctx) => {
router.post("shares.create", auth(), async (ctx: APIContext) => {
const { documentId } = ctx.request.body;
assertPresent(documentId, "documentId is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
@@ -267,11 +268,11 @@ router.post("shares.create", auth(), async (ctx) => {
};
});
router.post("shares.revoke", auth(), async (ctx) => {
router.post("shares.revoke", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const share = await Share.findByPk(id);
if (!share?.document) {

View File

@@ -12,16 +12,17 @@ import {
presentDocument,
presentPolicies,
} from "@server/presenters";
import { APIContext } from "@server/types";
import { starIndexing } from "@server/utils/indexing";
import { assertUuid, assertIndexCharacters } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("stars.create", auth(), async (ctx) => {
router.post("stars.create", auth(), async (ctx: APIContext) => {
const { documentId, collectionId } = ctx.request.body;
const { index } = ctx.request.body;
const { user } = ctx.state;
const { user } = ctx.state.auth;
assertUuid(
documentId || collectionId,
@@ -62,8 +63,8 @@ router.post("stars.create", auth(), async (ctx) => {
};
});
router.post("stars.list", auth(), pagination(), async (ctx) => {
const { user } = ctx.state;
router.post("stars.list", auth(), pagination(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const [stars, collectionIds] = await Promise.all([
Star.findAll({
@@ -115,13 +116,13 @@ router.post("stars.list", auth(), pagination(), async (ctx) => {
};
});
router.post("stars.update", auth(), async (ctx) => {
router.post("stars.update", auth(), async (ctx: APIContext) => {
const { id, index } = ctx.request.body;
assertUuid(id, "id is required");
assertIndexCharacters(index);
const { user } = ctx.state;
const { user } = ctx.state.auth;
let star = await Star.findByPk(id);
authorize(user, "update", star);
@@ -138,11 +139,11 @@ router.post("stars.update", auth(), async (ctx) => {
};
});
router.post("stars.delete", auth(), async (ctx) => {
router.post("stars.delete", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const star = await Star.findByPk(id);
authorize(user, "delete", star);

View File

@@ -2,53 +2,56 @@ import Router from "koa-router";
import subscriptionCreator from "@server/commands/subscriptionCreator";
import subscriptionDestroyer from "@server/commands/subscriptionDestroyer";
import auth from "@server/middlewares/authentication";
import {
transaction,
TransactionContext,
} from "@server/middlewares/transaction";
import { transaction } from "@server/middlewares/transaction";
import { Subscription, Document } from "@server/models";
import { authorize } from "@server/policies";
import { presentSubscription } from "@server/presenters";
import { APIContext } from "@server/types";
import { assertIn, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
const router = new Router();
router.post("subscriptions.list", auth(), pagination(), async (ctx) => {
const { user } = ctx.state;
const { documentId, event } = ctx.request.body;
router.post(
"subscriptions.list",
auth(),
pagination(),
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const { documentId, event } = ctx.request.body;
assertUuid(documentId, "documentId is required");
assertUuid(documentId, "documentId is required");
assertIn(
event,
["documents.update"],
`Not a valid subscription event for documents`
);
const document = await Document.findByPk(documentId, { userId: user.id });
authorize(user, "read", document);
const subscriptions = await Subscription.findAll({
where: {
documentId: document.id,
userId: user.id,
assertIn(
event,
},
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
["documents.update"],
`Not a valid subscription event for documents`
);
ctx.body = {
pagination: ctx.state.pagination,
data: subscriptions.map(presentSubscription),
};
});
const document = await Document.findByPk(documentId, { userId: user.id });
router.post("subscriptions.info", auth(), async (ctx) => {
const { user } = ctx.state;
authorize(user, "read", document);
const subscriptions = await Subscription.findAll({
where: {
documentId: document.id,
userId: user.id,
event,
},
order: [["createdAt", "DESC"]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
ctx.body = {
pagination: ctx.state.pagination,
data: subscriptions.map(presentSubscription),
};
}
);
router.post("subscriptions.info", auth(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const { documentId, event } = ctx.request.body;
assertUuid(documentId, "documentId is required");
@@ -82,8 +85,9 @@ router.post(
"subscriptions.create",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { user, transaction } = ctx.state;
async (ctx: APIContext) => {
const { auth, transaction } = ctx.state;
const { user } = auth;
const { documentId, event } = ctx.request.body;
assertUuid(documentId, "documentId is required");
@@ -119,8 +123,9 @@ router.post(
"subscriptions.delete",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { user, transaction } = ctx.state;
async (ctx: APIContext) => {
const { auth, transaction } = ctx.state;
const { user } = auth;
const { id } = ctx.request.body;
assertUuid(id, "id is required");

View File

@@ -8,6 +8,7 @@ import { rateLimiter } from "@server/middlewares/rateLimiter";
import { Event, Team, TeamDomain, User } from "@server/models";
import { authorize } from "@server/policies";
import { presentTeam, presentPolicies } from "@server/presenters";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { assertUuid } from "@server/validation";
@@ -17,7 +18,7 @@ router.post(
"team.update",
auth(),
rateLimiter(RateLimiterStrategy.TenPerHour),
async (ctx) => {
async (ctx: APIContext) => {
const {
name,
avatarUrl,
@@ -34,7 +35,7 @@ router.post(
preferences,
} = ctx.request.body;
const { user } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId, {
include: [{ model: TeamDomain }],
});
@@ -76,8 +77,8 @@ router.post(
"teams.create",
auth(),
rateLimiter(RateLimiterStrategy.FivePerHour),
async (ctx) => {
const { user } = ctx.state;
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const { name } = ctx.request.body;
const existingTeam = await Team.scope(

View File

@@ -16,14 +16,12 @@ import { ValidationError } from "@server/errors";
import logger from "@server/logging/Logger";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import {
transaction,
TransactionContext,
} from "@server/middlewares/transaction";
import { transaction } from "@server/middlewares/transaction";
import { Event, User, Team } from "@server/models";
import { UserFlag, UserRole } from "@server/models/User";
import { can, authorize } from "@server/policies";
import { presentUser, presentPolicies } from "@server/presenters";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import {
assertIn,
@@ -39,7 +37,7 @@ import pagination from "./middlewares/pagination";
const router = new Router();
const emailEnabled = !!(env.SMTP_HOST || env.ENVIRONMENT === "development");
router.post("users.list", auth(), pagination(), async (ctx) => {
router.post("users.list", auth(), pagination(), async (ctx: APIContext) => {
let { direction } = ctx.request.body;
const { sort = "createdAt", query, filter, ids } = ctx.request.body;
if (direction !== "ASC") {
@@ -55,7 +53,7 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
);
}
const actor = ctx.state.user;
const actor = ctx.state.auth.user;
let where: WhereOptions<User> = {
teamId: actor.teamId,
};
@@ -158,8 +156,8 @@ router.post("users.list", auth(), pagination(), async (ctx) => {
};
});
router.post("users.count", auth(), async (ctx) => {
const { user } = ctx.state;
router.post("users.count", auth(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const counts = await User.getCounts(user.teamId);
ctx.body = {
@@ -169,9 +167,9 @@ router.post("users.count", auth(), async (ctx) => {
};
});
router.post("users.info", auth(), async (ctx) => {
router.post("users.info", auth(), async (ctx: APIContext) => {
const { id } = ctx.request.body;
const actor = ctx.state.user;
const actor = ctx.state.auth.user;
const user = id ? await User.findByPk(id) : actor;
authorize(actor, "read", user);
const includeDetails = can(actor, "readDetails", user);
@@ -184,57 +182,53 @@ router.post("users.info", auth(), async (ctx) => {
};
});
router.post(
"users.update",
auth(),
transaction(),
async (ctx: TransactionContext) => {
const { user, transaction } = ctx.state;
const { name, avatarUrl, language, preferences } = ctx.request.body;
if (name) {
user.name = name;
}
if (avatarUrl) {
user.avatarUrl = avatarUrl;
}
if (language) {
user.language = language;
}
if (preferences) {
assertKeysIn(preferences, UserPreference);
router.post("users.update", auth(), transaction(), async (ctx: APIContext) => {
const { auth, transaction } = ctx.state;
const { user } = auth;
const { name, avatarUrl, language, preferences } = ctx.request.body;
if (name) {
user.name = name;
}
if (avatarUrl) {
user.avatarUrl = avatarUrl;
}
if (language) {
user.language = language;
}
if (preferences) {
assertKeysIn(preferences, UserPreference);
for (const value of Object.values(UserPreference)) {
if (has(preferences, value)) {
assertBoolean(preferences[value]);
user.setPreference(value, preferences[value]);
}
for (const value of Object.values(UserPreference)) {
if (has(preferences, value)) {
assertBoolean(preferences[value]);
user.setPreference(value, preferences[value]);
}
}
await user.save({ transaction });
await Event.create(
{
name: "users.update",
actorId: user.id,
userId: user.id,
teamId: user.teamId,
ip: ctx.request.ip,
},
{ transaction }
);
ctx.body = {
data: presentUser(user, {
includeDetails: true,
}),
};
}
);
await user.save({ transaction });
await Event.create(
{
name: "users.update",
actorId: user.id,
userId: user.id,
teamId: user.teamId,
ip: ctx.request.ip,
},
{ transaction }
);
ctx.body = {
data: presentUser(user, {
includeDetails: true,
}),
};
});
// Admin specific
router.post("users.promote", auth(), async (ctx) => {
router.post("users.promote", auth(), async (ctx: APIContext) => {
const userId = ctx.request.body.id;
const teamId = ctx.state.user.teamId;
const actor = ctx.state.user;
const actor = ctx.state.auth.user;
const teamId = actor.teamId;
assertPresent(userId, "id is required");
const user = await User.findByPk(userId);
authorize(actor, "promote", user);
@@ -260,10 +254,10 @@ router.post("users.promote", auth(), async (ctx) => {
};
});
router.post("users.demote", auth(), async (ctx) => {
router.post("users.demote", auth(), async (ctx: APIContext) => {
const userId = ctx.request.body.id;
let { to } = ctx.request.body;
const actor = ctx.state.user as User;
const actor = ctx.state.auth.user;
assertPresent(userId, "id is required");
to = (to === "viewer" ? "viewer" : "member") as UserRole;
@@ -289,9 +283,9 @@ router.post("users.demote", auth(), async (ctx) => {
};
});
router.post("users.suspend", auth(), async (ctx) => {
router.post("users.suspend", auth(), async (ctx: APIContext) => {
const userId = ctx.request.body.id;
const actor = ctx.state.user;
const actor = ctx.state.auth.user;
assertPresent(userId, "id is required");
const user = await User.findByPk(userId, {
rejectOnEmpty: true,
@@ -313,9 +307,9 @@ router.post("users.suspend", auth(), async (ctx) => {
};
});
router.post("users.activate", auth(), async (ctx) => {
router.post("users.activate", auth(), async (ctx: APIContext) => {
const userId = ctx.request.body.id;
const actor = ctx.state.user;
const actor = ctx.state.auth.user;
assertPresent(userId, "id is required");
const user = await User.findByPk(userId, {
rejectOnEmpty: true,
@@ -341,10 +335,10 @@ router.post(
"users.invite",
auth(),
rateLimiter(RateLimiterStrategy.TenPerHour),
async (ctx) => {
async (ctx: APIContext) => {
const { invites } = ctx.request.body;
assertArray(invites, "invites must be an array");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const team = await Team.findByPk(user.teamId);
authorize(user, "inviteUser", team);
@@ -367,10 +361,10 @@ router.post(
"users.resendInvite",
auth(),
transaction(),
async (ctx: TransactionContext) => {
async (ctx: APIContext) => {
const { id } = ctx.request.body;
const actor = ctx.state.user;
const { transaction } = ctx.state;
const { auth, transaction } = ctx.state;
const actor = auth.user;
const user = await User.findByPk(id, {
lock: transaction.LOCK.UPDATE,
@@ -413,8 +407,8 @@ router.post(
"users.requestDelete",
auth(),
rateLimiter(RateLimiterStrategy.FivePerHour),
async (ctx) => {
const { user } = ctx.state;
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
authorize(user, "delete", user);
if (emailEnabled) {
@@ -434,9 +428,9 @@ router.post(
"users.delete",
auth(),
rateLimiter(RateLimiterStrategy.TenPerHour),
async (ctx) => {
async (ctx: APIContext) => {
const { id, code = "" } = ctx.request.body;
const actor = ctx.state.user;
const actor = ctx.state.auth.user;
let user: User;
if (id) {

View File

@@ -4,16 +4,17 @@ import { rateLimiter } from "@server/middlewares/rateLimiter";
import { View, Document, Event } from "@server/models";
import { authorize } from "@server/policies";
import { presentView } from "@server/presenters";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { assertUuid } from "@server/validation";
const router = new Router();
router.post("views.list", auth(), async (ctx) => {
router.post("views.list", auth(), async (ctx: APIContext) => {
const { documentId, includeSuspended = false } = ctx.request.body;
assertUuid(documentId, "documentId is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
@@ -29,11 +30,11 @@ router.post(
"views.create",
auth(),
rateLimiter(RateLimiterStrategy.OneThousandPerHour),
async (ctx) => {
async (ctx: APIContext) => {
const { documentId } = ctx.request.body;
assertUuid(documentId, "documentId is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const document = await Document.findByPk(documentId, {
userId: user.id,
});

View File

@@ -5,7 +5,7 @@ import auth from "@server/middlewares/authentication";
import { WebhookSubscription, Event } from "@server/models";
import { authorize } from "@server/policies";
import { presentWebhookSubscription } from "@server/presenters";
import { WebhookSubscriptionEvent } from "@server/types";
import { WebhookSubscriptionEvent, APIContext } from "@server/types";
import { assertArray, assertPresent, assertUuid } from "@server/validation";
import pagination from "./middlewares/pagination";
@@ -15,8 +15,8 @@ router.post(
"webhookSubscriptions.list",
auth({ admin: true }),
pagination(),
async (ctx) => {
const { user } = ctx.state;
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
authorize(user, "listWebhookSubscription", user.team);
const webhooks = await WebhookSubscription.findAll({
where: {
@@ -37,8 +37,8 @@ router.post(
router.post(
"webhookSubscriptions.create",
auth({ admin: true }),
async (ctx) => {
const { user } = ctx.state;
async (ctx: APIContext) => {
const { user } = ctx.state.auth;
authorize(user, "createWebhookSubscription", user.team);
const { name, url, secret } = ctx.request.body;
@@ -83,10 +83,10 @@ router.post(
router.post(
"webhookSubscriptions.delete",
auth({ admin: true }),
async (ctx) => {
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const webhookSubscription = await WebhookSubscription.findByPk(id);
authorize(user, "delete", webhookSubscription);
@@ -112,10 +112,10 @@ router.post(
router.post(
"webhookSubscriptions.update",
auth({ admin: true }),
async (ctx) => {
async (ctx: APIContext) => {
const { id } = ctx.request.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
const { user } = ctx.state.auth;
const { name, url, secret } = ctx.request.body;
const events: string[] = compact(ctx.request.body.events);

View File

@@ -6,9 +6,10 @@ import Router from "koa-router";
import { AuthenticationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { Collection, Team, View } from "@server/models";
import { AppState, AppContext, APIContext } from "@server/types";
import providers from "./providers";
const app = new Koa();
const app = new Koa<AppState, AppContext>();
const router = new Router();
router.use(passport.initialize());
@@ -20,8 +21,8 @@ providers.forEach((provider) => {
}
});
router.get("/redirect", auth(), async (ctx) => {
const { user } = ctx.state;
router.get("/redirect", auth(), async (ctx: APIContext) => {
const { user } = ctx.state.auth;
const jwtToken = user.getJwtToken();
if (jwtToken === ctx.params.token) {

View File

@@ -1,5 +1,6 @@
import { Context } from "koa";
import { RouterContext } from "koa-router";
import { ParameterizedContext, DefaultContext } from "koa";
import { IRouterParamContext } from "koa-router";
import { Transaction } from "sequelize/types";
import { Client } from "@shared/types";
import { AccountProvisionerResult } from "./commands/accountProvisioner";
import { FileOperation, Team, User } from "./models";
@@ -13,18 +14,32 @@ export type AuthenticationResult = AccountProvisionerResult & {
client: Client;
};
export type AuthenticatedState = {
export type Authentication = {
user: User;
token: string;
authType: AuthenticationType;
type: AuthenticationType;
};
export type ContextWithState = Context & {
state: AuthenticatedState;
export type Pagination = {
limit: number;
offset: number;
nextPath: string;
};
export interface APIContext<ReqT = Record<string, unknown>>
extends RouterContext<AuthenticatedState, Context> {
export type AppState = {
auth: Authentication | Record<string, never>;
transaction: Transaction;
pagination: Pagination;
};
export type AppContext = ParameterizedContext<AppState, DefaultContext>;
export interface APIContext<ReqT = Record<string, unknown>, ResT = unknown>
extends ParameterizedContext<
AppState,
DefaultContext & IRouterParamContext<AppState>,
ResT
> {
input: ReqT;
}