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:
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user