diff --git a/app/components/Sidebar/App.tsx b/app/components/Sidebar/App.tsx
index 5bef8d8f7758..b92be2181098 100644
--- a/app/components/Sidebar/App.tsx
+++ b/app/components/Sidebar/App.tsx
@@ -10,6 +10,7 @@ import Scrollable from "~/components/Scrollable";
import Text from "~/components/Text";
import { inviteUser } from "~/actions/definitions/users";
import useCurrentTeam from "~/hooks/useCurrentTeam";
+import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import OrganizationMenu from "~/menus/OrganizationMenu";
@@ -34,12 +35,15 @@ function AppSidebar() {
const { t } = useTranslation();
const { documents } = useStores();
const team = useCurrentTeam();
+ const user = useCurrentUser();
const can = usePolicy(team.id);
React.useEffect(() => {
- documents.fetchDrafts();
- documents.fetchTemplates();
- }, [documents]);
+ if (!user.isViewer) {
+ documents.fetchDrafts();
+ documents.fetchTemplates();
+ }
+ }, [documents, user.isViewer]);
const [dndArea, setDndArea] = React.useState();
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
diff --git a/app/routes/authenticated.tsx b/app/routes/authenticated.tsx
index 9f9533cbdbec..8bce9bb62838 100644
--- a/app/routes/authenticated.tsx
+++ b/app/routes/authenticated.tsx
@@ -1,3 +1,4 @@
+import { observer } from "mobx-react";
import * as React from "react";
import { Switch, Redirect, RouteComponentProps } from "react-router-dom";
import Archive from "~/scenes/Archive";
@@ -11,6 +12,8 @@ import CenteredContent from "~/components/CenteredContent";
import PlaceholderDocument from "~/components/PlaceholderDocument";
import Route from "~/components/ProfiledRoute";
import SocketProvider from "~/components/SocketProvider";
+import useCurrentTeam from "~/hooks/useCurrentTeam";
+import usePolicy from "~/hooks/usePolicy";
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
const SettingsRoutes = React.lazy(
@@ -59,7 +62,10 @@ const RedirectDocument = ({
/>
);
-export default function AuthenticatedRoutes() {
+function AuthenticatedRoutes() {
+ const team = useCurrentTeam();
+ const can = usePolicy(team.id);
+
return (
@@ -71,14 +77,24 @@ export default function AuthenticatedRoutes() {
}
>
+ {can.createDocument && (
+
+ )}
+ {can.createDocument && (
+
+ )}
+ {can.createDocument && (
+
+ )}
+ {can.createDocument && (
+
+ )}
+ {can.createDocument && (
+
+ )}
-
-
-
-
-
@@ -103,3 +119,5 @@ export default function AuthenticatedRoutes() {
);
}
+
+export default observer(AuthenticatedRoutes);
diff --git a/server/middlewares/authentication.ts b/server/middlewares/authentication.ts
index c998d5395604..809a56cb7fe3 100644
--- a/server/middlewares/authentication.ts
+++ b/server/middlewares/authentication.ts
@@ -1,15 +1,28 @@
import { Next } from "koa";
+import Logger from "@server/logging/Logger";
import tracer, { APM } from "@server/logging/tracing";
import { User, Team, ApiKey } from "@server/models";
import { getUserForJWT } from "@server/utils/jwt";
-import { AuthenticationError, UserSuspendedError } from "../errors";
-import { ContextWithState } from "../types";
-
-export default function auth(
- options: {
- required?: boolean;
- } = {}
-) {
+import {
+ AuthenticationError,
+ AuthorizationError,
+ UserSuspendedError,
+} from "../errors";
+import { ContextWithState, AuthenticationTypes } from "../types";
+
+type AuthenticationOptions = {
+ /* An admin user role is required to access the route */
+ admin?: boolean;
+ /* A member or admin user role is required to access the route */
+ member?: boolean;
+ /**
+ * Authentication is parsed, but optional. Note that if a token is provided
+ * in the request it must be valid or the requst will be rejected.
+ */
+ optional?: boolean;
+};
+
+export default function auth(options: AuthenticationOptions = {}) {
return async function authMiddleware(ctx: ContextWithState, next: Next) {
let token;
const authorizationHeader = ctx.request.get("authorization");
@@ -29,8 +42,11 @@ export default function auth(
`Bad Authorization header format. Format is "Authorization: Bearer "`
);
}
- // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
- } else if (ctx.body && ctx.body.token) {
+ } else if (
+ ctx.body &&
+ typeof ctx.body === "object" &&
+ "token" in ctx.body
+ ) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
token = ctx.body.token;
} else if (ctx.request.query.token) {
@@ -39,15 +55,15 @@ export default function auth(
token = ctx.cookies.get("accessToken");
}
- if (!token && options.required !== false) {
+ if (!token && options.optional !== true) {
throw AuthenticationError("Authentication required");
}
- let user;
+ let user: User | null | undefined;
if (token) {
if (String(token).match(/^[\w]{38}$/)) {
- ctx.state.authType = "api";
+ ctx.state.authType = AuthenticationTypes.API;
let apiKey;
try {
@@ -78,7 +94,7 @@ export default function auth(
throw AuthenticationError("Invalid API key");
}
} else {
- ctx.state.authType = "app";
+ ctx.state.authType = AuthenticationTypes.APP;
user = await getUserForJWT(String(token));
}
@@ -94,8 +110,23 @@ export default function auth(
});
}
+ if (options.admin) {
+ if (!user.isAdmin) {
+ throw AuthorizationError("Admin role required");
+ }
+ }
+
+ if (options.member) {
+ if (user.isViewer) {
+ throw AuthorizationError("Member role required");
+ }
+ }
+
// not awaiting the promise here so that the request is not blocked
- user.updateActiveAt(ctx.request.ip);
+ user.updateActiveAt(ctx.request.ip).catch((err) => {
+ Logger.error("Failed to update user activeAt", err);
+ });
+
ctx.state.token = String(token);
ctx.state.user = user;
diff --git a/server/models/User.ts b/server/models/User.ts
index b6f450f1b327..87e446083c90 100644
--- a/server/models/User.ts
+++ b/server/models/User.ts
@@ -276,7 +276,7 @@ class User extends ParanoidModel {
.map((c) => c.id);
};
- updateActiveAt = (ip: string, force = false) => {
+ updateActiveAt = async (ip: string, force = false) => {
const fiveMinutesAgo = subMinutes(new Date(), 5);
// ensure this is updated only every few minutes otherwise
diff --git a/server/routes/api/apiKeys.ts b/server/routes/api/apiKeys.ts
index 33b16b02d8cd..fc5e33104631 100644
--- a/server/routes/api/apiKeys.ts
+++ b/server/routes/api/apiKeys.ts
@@ -8,7 +8,7 @@ import pagination from "./middlewares/pagination";
const router = new Router();
-router.post("apiKeys.create", auth(), async (ctx) => {
+router.post("apiKeys.create", auth({ member: true }), async (ctx) => {
const { name } = ctx.body;
assertPresent(name, "name is required");
const { user } = ctx.state;
@@ -35,24 +35,29 @@ router.post("apiKeys.create", auth(), async (ctx) => {
};
});
-router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
- const { user } = ctx.state;
- const keys = await ApiKey.findAll({
- where: {
- userId: user.id,
- },
- order: [["createdAt", "DESC"]],
- offset: ctx.state.pagination.offset,
- limit: ctx.state.pagination.limit,
- });
+router.post(
+ "apiKeys.list",
+ auth({ member: true }),
+ pagination(),
+ async (ctx) => {
+ const { user } = ctx.state;
+ const keys = await ApiKey.findAll({
+ where: {
+ userId: user.id,
+ },
+ order: [["createdAt", "DESC"]],
+ offset: ctx.state.pagination.offset,
+ limit: ctx.state.pagination.limit,
+ });
- ctx.body = {
- pagination: ctx.state.pagination,
- data: keys.map(presentApiKey),
- };
-});
+ ctx.body = {
+ pagination: ctx.state.pagination,
+ data: keys.map(presentApiKey),
+ };
+ }
+);
-router.post("apiKeys.delete", auth(), async (ctx) => {
+router.post("apiKeys.delete", auth({ member: true }), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
diff --git a/server/routes/api/documents.test.ts b/server/routes/api/documents.test.ts
index 28efc5c0ca9e..54c0fe45c7b7 100644
--- a/server/routes/api/documents.test.ts
+++ b/server/routes/api/documents.test.ts
@@ -15,6 +15,7 @@ import {
buildCollection,
buildUser,
buildDocument,
+ buildViewer,
} from "@server/test/factories";
import { flushdb, seed } from "@server/test/support";
@@ -1432,12 +1433,76 @@ describe("#documents.archived", () => {
expect(body.data.length).toEqual(0);
});
+ it("should require member", async () => {
+ const viewer = await buildViewer();
+ const res = await server.post("/api/documents.archived", {
+ body: {
+ token: viewer.getJwtToken(),
+ },
+ });
+ expect(res.status).toEqual(403);
+ });
+
it("should require authentication", async () => {
const res = await server.post("/api/documents.archived");
expect(res.status).toEqual(401);
});
});
+describe("#documents.deleted", () => {
+ it("should return deleted documents", async () => {
+ const { user } = await seed();
+ const document = await buildDocument({
+ userId: user.id,
+ teamId: user.teamId,
+ });
+ await document.delete(user.id);
+ const res = await server.post("/api/documents.deleted", {
+ body: {
+ token: user.getJwtToken(),
+ },
+ });
+ const body = await res.json();
+ expect(res.status).toEqual(200);
+ expect(body.data.length).toEqual(1);
+ });
+
+ it("should not return documents in private collections not a member of", async () => {
+ const { user } = await seed();
+ const collection = await buildCollection({
+ permission: null,
+ });
+ const document = await buildDocument({
+ teamId: user.teamId,
+ collectionId: collection.id,
+ });
+ await document.delete(user.id);
+ const res = await server.post("/api/documents.deleted", {
+ body: {
+ token: user.getJwtToken(),
+ },
+ });
+ const body = await res.json();
+ expect(res.status).toEqual(200);
+ expect(body.data.length).toEqual(0);
+ });
+
+ it("should require member", async () => {
+ const viewer = await buildViewer();
+ const res = await server.post("/api/documents.deleted", {
+ body: {
+ token: viewer.getJwtToken(),
+ },
+ });
+ expect(res.status).toEqual(403);
+ });
+
+ it("should require authentication", async () => {
+ const res = await server.post("/api/documents.deleted");
+ expect(res.status).toEqual(401);
+ });
+});
+
describe("#documents.viewed", () => {
it("should return empty result if no views", async () => {
const { user } = await seed();
diff --git a/server/routes/api/documents.ts b/server/routes/api/documents.ts
index cc8469a2662e..cb7abd660675 100644
--- a/server/routes/api/documents.ts
+++ b/server/routes/api/documents.ts
@@ -162,104 +162,117 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
};
});
-router.post("documents.archived", auth(), pagination(), async (ctx) => {
- const { sort = "updatedAt" } = ctx.body;
+router.post(
+ "documents.archived",
+ auth({ member: true }),
+ pagination(),
+ async (ctx) => {
+ const { sort = "updatedAt" } = ctx.body;
- assertSort(sort, Document);
- let direction = ctx.body.direction;
- if (direction !== "ASC") {
- direction = "DESC";
- }
- const { user } = ctx.state;
- const collectionIds = await user.collectionIds();
- const collectionScope: Readonly = {
- method: ["withCollectionPermissions", user.id],
- };
- const viewScope: Readonly = {
- method: ["withViews", user.id],
- };
- const documents = await Document.scope([
- "defaultScope",
- collectionScope,
- viewScope,
- ]).findAll({
- where: {
- teamId: user.teamId,
- collectionId: collectionIds,
- archivedAt: {
- [Op.ne]: null,
+ assertSort(sort, Document);
+ let direction = ctx.body.direction;
+ if (direction !== "ASC") {
+ direction = "DESC";
+ }
+ const { user } = ctx.state;
+ const collectionIds = await user.collectionIds();
+ const collectionScope: Readonly = {
+ method: ["withCollectionPermissions", user.id],
+ };
+ const viewScope: Readonly = {
+ method: ["withViews", user.id],
+ };
+ const documents = await Document.scope([
+ "defaultScope",
+ collectionScope,
+ viewScope,
+ ]).findAll({
+ where: {
+ teamId: user.teamId,
+ collectionId: collectionIds,
+ archivedAt: {
+ [Op.ne]: null,
+ },
},
- },
- order: [[sort, direction]],
- offset: ctx.state.pagination.offset,
- limit: ctx.state.pagination.limit,
- });
- const data = await Promise.all(
- documents.map((document) => presentDocument(document))
- );
- const policies = presentPolicies(user, documents);
+ order: [[sort, direction]],
+ offset: ctx.state.pagination.offset,
+ limit: ctx.state.pagination.limit,
+ });
+ const data = await Promise.all(
+ documents.map((document) => presentDocument(document))
+ );
+ const policies = presentPolicies(user, documents);
- ctx.body = {
- pagination: ctx.state.pagination,
- data,
- policies,
- };
-});
+ ctx.body = {
+ pagination: ctx.state.pagination,
+ data,
+ policies,
+ };
+ }
+);
-router.post("documents.deleted", auth(), pagination(), async (ctx) => {
- const { sort = "deletedAt" } = ctx.body;
+router.post(
+ "documents.deleted",
+ auth({ member: true }),
+ pagination(),
+ async (ctx) => {
+ const { sort = "deletedAt" } = ctx.body;
- assertSort(sort, Document);
- let direction = ctx.body.direction;
- if (direction !== "ASC") {
- direction = "DESC";
- }
- const { user } = ctx.state;
- const collectionIds = await user.collectionIds({
- paranoid: false,
- });
- const collectionScope: Readonly = {
- method: ["withCollectionPermissions", user.id],
- };
- const viewScope: Readonly = {
- method: ["withViews", user.id],
- };
- const documents = await Document.scope([collectionScope, viewScope]).findAll({
- where: {
- teamId: user.teamId,
- collectionId: collectionIds,
- deletedAt: {
- [Op.ne]: null,
- },
- },
- include: [
- {
- model: User,
- as: "createdBy",
- paranoid: false,
- },
- {
- model: User,
- as: "updatedBy",
- paranoid: false,
+ assertSort(sort, Document);
+ let direction = ctx.body.direction;
+ if (direction !== "ASC") {
+ direction = "DESC";
+ }
+ const { user } = ctx.state;
+ const collectionIds = await user.collectionIds({
+ paranoid: false,
+ });
+ const collectionScope: Readonly = {
+ method: ["withCollectionPermissions", user.id],
+ };
+ const viewScope: Readonly = {
+ method: ["withViews", user.id],
+ };
+ const documents = await Document.scope([
+ collectionScope,
+ viewScope,
+ ]).findAll({
+ where: {
+ teamId: user.teamId,
+ collectionId: collectionIds,
+ deletedAt: {
+ [Op.ne]: null,
+ },
},
- ],
- paranoid: false,
- order: [[sort, direction]],
- offset: ctx.state.pagination.offset,
- limit: ctx.state.pagination.limit,
- });
- const data = await Promise.all(
- documents.map((document) => presentDocument(document))
- );
- const policies = presentPolicies(user, documents);
+ include: [
+ {
+ model: User,
+ as: "createdBy",
+ paranoid: false,
+ },
+ {
+ model: User,
+ as: "updatedBy",
+ paranoid: false,
+ },
+ ],
+ paranoid: false,
+ order: [[sort, direction]],
+ offset: ctx.state.pagination.offset,
+ limit: ctx.state.pagination.limit,
+ });
+ const data = await Promise.all(
+ documents.map((document) => presentDocument(document))
+ );
+ const policies = presentPolicies(user, documents);
- ctx.body = {
- pagination: ctx.state.pagination,
- data,
- policies,
- };
-});
+ ctx.body = {
+ pagination: ctx.state.pagination,
+ data,
+ policies,
+ };
+ }
+);
router.post("documents.viewed", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body;
@@ -314,76 +327,81 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
};
});
-router.post("documents.drafts", auth(), pagination(), async (ctx) => {
- let { direction } = ctx.body;
- const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
+router.post(
+ "documents.drafts",
+ auth({ member: true }),
+ pagination(),
+ async (ctx) => {
+ let { direction } = ctx.body;
+ const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
- assertSort(sort, Document);
- if (direction !== "ASC") {
- direction = "DESC";
- }
- const { user } = ctx.state;
+ assertSort(sort, Document);
+ if (direction !== "ASC") {
+ direction = "DESC";
+ }
+ const { user } = ctx.state;
- if (collectionId) {
- assertUuid(collectionId, "collectionId must be a UUID");
- const collection = await Collection.scope({
- method: ["withMembership", user.id],
- }).findByPk(collectionId);
- authorize(user, "read", collection);
- }
+ if (collectionId) {
+ assertUuid(collectionId, "collectionId must be a UUID");
+ const collection = await Collection.scope({
+ method: ["withMembership", user.id],
+ }).findByPk(collectionId);
+ authorize(user, "read", collection);
+ }
- const collectionIds = collectionId
- ? [collectionId]
- : await user.collectionIds();
- const where: WhereOptions = {
- createdById: user.id,
- collectionId: collectionIds,
- publishedAt: {
- [Op.is]: null,
- },
- };
+ const collectionIds = collectionId
+ ? [collectionId]
+ : await user.collectionIds();
+ const where: WhereOptions = {
+ createdById: user.id,
+ collectionId: collectionIds,
+ publishedAt: {
+ [Op.is]: null,
+ },
+ };
- if (dateFilter) {
- assertIn(
- dateFilter,
- ["day", "week", "month", "year"],
- "dateFilter must be one of day,week,month,year"
+ if (dateFilter) {
+ assertIn(
+ dateFilter,
+ ["day", "week", "month", "year"],
+ "dateFilter must be one of day,week,month,year"
+ );
+ where.updatedAt = {
+ [Op.gte]: subtractDate(new Date(), dateFilter),
+ };
+ } else {
+ delete where.updatedAt;
+ }
+
+ const collectionScope: Readonly = {
+ method: ["withCollectionPermissions", user.id],
+ };
+ const documents = await Document.scope([
+ "defaultScope",
+ collectionScope,
+ ]).findAll({
+ where,
+ order: [[sort, direction]],
+ offset: ctx.state.pagination.offset,
+ limit: ctx.state.pagination.limit,
+ });
+ const data = await Promise.all(
+ documents.map((document) => presentDocument(document))
);
- where.updatedAt = {
- [Op.gte]: subtractDate(new Date(), dateFilter),
+ const policies = presentPolicies(user, documents);
+
+ ctx.body = {
+ pagination: ctx.state.pagination,
+ data,
+ policies,
};
- } else {
- delete where.updatedAt;
}
-
- const collectionScope: Readonly = {
- method: ["withCollectionPermissions", user.id],
- };
- const documents = await Document.scope([
- "defaultScope",
- collectionScope,
- ]).findAll({
- where,
- order: [[sort, direction]],
- offset: ctx.state.pagination.offset,
- limit: ctx.state.pagination.limit,
- });
- const data = await Promise.all(
- documents.map((document) => presentDocument(document))
- );
- const policies = presentPolicies(user, documents);
-
- ctx.body = {
- pagination: ctx.state.pagination,
- data,
- policies,
- };
-});
+);
router.post(
"documents.info",
auth({
- required: false,
+ optional: true,
}),
async (ctx) => {
const { id, shareId, apiVersion } = ctx.body;
@@ -421,7 +439,7 @@ router.post(
router.post(
"documents.export",
auth({
- required: false,
+ optional: true,
}),
async (ctx) => {
const { id, shareId } = ctx.body;
@@ -438,7 +456,7 @@ router.post(
}
);
-router.post("documents.restore", auth(), async (ctx) => {
+router.post("documents.restore", auth({ member: true }), async (ctx) => {
const { id, collectionId, revisionId } = ctx.body;
assertPresent(id, "id is required");
const { user } = ctx.state;
@@ -588,7 +606,7 @@ router.post("documents.search_titles", auth(), pagination(), async (ctx) => {
router.post(
"documents.search",
auth({
- required: false,
+ optional: true,
}),
pagination(),
async (ctx) => {
@@ -782,7 +800,7 @@ router.post("documents.unstar", auth(), async (ctx) => {
};
});
-router.post("documents.templatize", auth(), async (ctx) => {
+router.post("documents.templatize", auth({ member: true }), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const { user } = ctx.state;
@@ -829,7 +847,7 @@ router.post("documents.templatize", auth(), async (ctx) => {
};
});
-router.post("documents.update", auth(), async (ctx) => {
+router.post("documents.update", auth({ member: true }), async (ctx) => {
const {
id,
title,
@@ -889,7 +907,7 @@ router.post("documents.update", auth(), async (ctx) => {
};
});
-router.post("documents.move", auth(), async (ctx) => {
+router.post("documents.move", auth({ member: true }), async (ctx) => {
const { id, collectionId, parentDocumentId, index } = ctx.body;
assertUuid(id, "id must be a uuid");
assertUuid(collectionId, "collectionId must be a uuid");
@@ -955,7 +973,7 @@ router.post("documents.move", auth(), async (ctx) => {
};
});
-router.post("documents.archive", auth(), async (ctx) => {
+router.post("documents.archive", auth({ member: true }), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const { user } = ctx.state;
@@ -984,7 +1002,7 @@ router.post("documents.archive", auth(), async (ctx) => {
};
});
-router.post("documents.delete", auth(), async (ctx) => {
+router.post("documents.delete", auth({ member: true }), async (ctx) => {
const { id, permanent } = ctx.body;
assertPresent(id, "id is required");
const { user } = ctx.state;
@@ -1045,7 +1063,7 @@ router.post("documents.delete", auth(), async (ctx) => {
};
});
-router.post("documents.unpublish", auth(), async (ctx) => {
+router.post("documents.unpublish", auth({ member: true }), async (ctx) => {
const { id } = ctx.body;
assertPresent(id, "id is required");
const { user } = ctx.state;
@@ -1079,7 +1097,7 @@ router.post("documents.unpublish", auth(), async (ctx) => {
};
});
-router.post("documents.import", auth(), async (ctx) => {
+router.post("documents.import", auth({ member: true }), async (ctx) => {
const { publish, collectionId, parentDocumentId, index } = ctx.body;
if (!ctx.is("multipart/form-data")) {
@@ -1162,7 +1180,7 @@ router.post("documents.import", auth(), async (ctx) => {
});
});
-router.post("documents.create", auth(), async (ctx) => {
+router.post("documents.create", auth({ member: true }), async (ctx) => {
const {
title = "",
text = "",
diff --git a/server/routes/api/fileOperations.ts b/server/routes/api/fileOperations.ts
index df10d8ab06f9..b04f6a762a50 100644
--- a/server/routes/api/fileOperations.ts
+++ b/server/routes/api/fileOperations.ts
@@ -13,7 +13,7 @@ import pagination from "./middlewares/pagination";
const router = new Router();
-router.post("fileOperations.info", auth(), async (ctx) => {
+router.post("fileOperations.info", auth({ admin: true }), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
const { user } = ctx.state;
@@ -28,42 +28,47 @@ router.post("fileOperations.info", auth(), async (ctx) => {
};
});
-router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
- let { direction } = ctx.body;
- const { sort = "createdAt", type } = ctx.body;
- assertIn(type, Object.values(FileOperationType));
- assertSort(sort, FileOperation);
-
- if (direction !== "ASC") {
- direction = "DESC";
+router.post(
+ "fileOperations.list",
+ auth({ admin: true }),
+ pagination(),
+ async (ctx) => {
+ let { direction } = ctx.body;
+ const { sort = "createdAt", type } = ctx.body;
+ assertIn(type, Object.values(FileOperationType));
+ assertSort(sort, FileOperation);
+
+ if (direction !== "ASC") {
+ direction = "DESC";
+ }
+ const { user } = ctx.state;
+ const where: WhereOptions = {
+ teamId: user.teamId,
+ type,
+ };
+ const team = await Team.findByPk(user.teamId);
+ authorize(user, "manage", team);
+
+ const [exports, total] = await Promise.all([
+ await FileOperation.findAll({
+ where,
+ order: [[sort, direction]],
+ offset: ctx.state.pagination.offset,
+ limit: ctx.state.pagination.limit,
+ }),
+ await FileOperation.count({
+ where,
+ }),
+ ]);
+
+ ctx.body = {
+ pagination: { ...ctx.state.pagination, total },
+ data: exports.map(presentFileOperation),
+ };
}
- const { user } = ctx.state;
- const where: WhereOptions = {
- teamId: user.teamId,
- type,
- };
- const team = await Team.findByPk(user.teamId);
- authorize(user, "manage", team);
-
- const [exports, total] = await Promise.all([
- await FileOperation.findAll({
- where,
- order: [[sort, direction]],
- offset: ctx.state.pagination.offset,
- limit: ctx.state.pagination.limit,
- }),
- await FileOperation.count({
- where,
- }),
- ]);
-
- ctx.body = {
- pagination: { ...ctx.state.pagination, total },
- data: exports.map(presentFileOperation),
- };
-});
+);
-router.post("fileOperations.redirect", auth(), async (ctx) => {
+router.post("fileOperations.redirect", auth({ admin: true }), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
@@ -81,7 +86,7 @@ router.post("fileOperations.redirect", auth(), async (ctx) => {
ctx.redirect(accessUrl);
});
-router.post("fileOperations.delete", auth(), async (ctx) => {
+router.post("fileOperations.delete", auth({ admin: true }), async (ctx) => {
const { id } = ctx.body;
assertUuid(id, "id is required");
diff --git a/server/routes/api/webhookSubscriptions.ts b/server/routes/api/webhookSubscriptions.ts
index 085031a9e728..71753935e94a 100644
--- a/server/routes/api/webhookSubscriptions.ts
+++ b/server/routes/api/webhookSubscriptions.ts
@@ -11,127 +11,144 @@ import pagination from "./middlewares/pagination";
const router = new Router();
-router.post("webhookSubscriptions.list", auth(), pagination(), async (ctx) => {
- const { user } = ctx.state;
- authorize(user, "listWebhookSubscription", user.team);
- const webhooks = await WebhookSubscription.findAll({
- where: {
- teamId: user.teamId,
- },
- order: [["createdAt", "DESC"]],
- offset: ctx.state.pagination.offset,
- limit: ctx.state.pagination.limit,
- });
-
- ctx.body = {
- pagination: ctx.state.pagination,
- data: webhooks.map(presentWebhookSubscription),
- };
-});
-
-router.post("webhookSubscriptions.create", auth(), async (ctx) => {
- const { user } = ctx.state;
- authorize(user, "createWebhookSubscription", user.team);
-
- const { name, url } = ctx.request.body;
- const events: string[] = compact(ctx.request.body.events);
- assertPresent(name, "name is required");
- assertPresent(url, "url is required");
- assertArray(events, "events is required");
- if (events.length === 0) {
- throw ValidationError("events are required");
+router.post(
+ "webhookSubscriptions.list",
+ auth({ admin: true }),
+ pagination(),
+ async (ctx) => {
+ const { user } = ctx.state;
+ authorize(user, "listWebhookSubscription", user.team);
+ const webhooks = await WebhookSubscription.findAll({
+ where: {
+ teamId: user.teamId,
+ },
+ order: [["createdAt", "DESC"]],
+ offset: ctx.state.pagination.offset,
+ limit: ctx.state.pagination.limit,
+ });
+
+ ctx.body = {
+ pagination: ctx.state.pagination,
+ data: webhooks.map(presentWebhookSubscription),
+ };
}
-
- const webhookSubscription = await WebhookSubscription.create({
- name,
- events,
- createdById: user.id,
- teamId: user.teamId,
- url,
- enabled: true,
- });
-
- const event: WebhookSubscriptionEvent = {
- name: "webhook_subscriptions.create",
- modelId: webhookSubscription.id,
- teamId: user.teamId,
- actorId: user.id,
- data: {
+);
+
+router.post(
+ "webhookSubscriptions.create",
+ auth({ admin: true }),
+ async (ctx) => {
+ const { user } = ctx.state;
+ authorize(user, "createWebhookSubscription", user.team);
+
+ const { name, url } = ctx.request.body;
+ const events: string[] = compact(ctx.request.body.events);
+ assertPresent(name, "name is required");
+ assertPresent(url, "url is required");
+ assertArray(events, "events is required");
+ if (events.length === 0) {
+ throw ValidationError("events are required");
+ }
+
+ const webhookSubscription = await WebhookSubscription.create({
name,
- url,
events,
- },
- ip: ctx.request.ip,
- };
- await Event.create(event);
-
- ctx.body = {
- data: presentWebhookSubscription(webhookSubscription),
- };
-});
-
-router.post("webhookSubscriptions.delete", auth(), async (ctx) => {
- const { id } = ctx.body;
- assertUuid(id, "id is required");
- const { user } = ctx.state;
- const webhookSubscription = await WebhookSubscription.findByPk(id);
-
- authorize(user, "delete", webhookSubscription);
-
- await webhookSubscription.destroy();
-
- const event: WebhookSubscriptionEvent = {
- name: "webhook_subscriptions.delete",
- modelId: webhookSubscription.id,
- teamId: user.teamId,
- actorId: user.id,
- data: {
- name: webhookSubscription.name,
- url: webhookSubscription.url,
- events: webhookSubscription.events,
- },
- ip: ctx.request.ip,
- };
- await Event.create(event);
-});
-
-router.post("webhookSubscriptions.update", auth(), async (ctx) => {
- const { id } = ctx.body;
- assertUuid(id, "id is required");
- const { user } = ctx.state;
-
- const { name, url } = ctx.request.body;
- const events: string[] = compact(ctx.request.body.events);
- assertPresent(name, "name is required");
- assertPresent(url, "url is required");
- assertArray(events, "events is required");
- if (events.length === 0) {
- throw ValidationError("events are required");
+ createdById: user.id,
+ teamId: user.teamId,
+ url,
+ enabled: true,
+ });
+
+ const event: WebhookSubscriptionEvent = {
+ name: "webhook_subscriptions.create",
+ modelId: webhookSubscription.id,
+ teamId: user.teamId,
+ actorId: user.id,
+ data: {
+ name,
+ url,
+ events,
+ },
+ ip: ctx.request.ip,
+ };
+ await Event.create(event);
+
+ ctx.body = {
+ data: presentWebhookSubscription(webhookSubscription),
+ };
}
+);
- const webhookSubscription = await WebhookSubscription.findByPk(id);
-
- authorize(user, "update", webhookSubscription);
-
- await webhookSubscription.update({ name, url, events, enabled: true });
-
- const event: WebhookSubscriptionEvent = {
- name: "webhook_subscriptions.update",
- modelId: webhookSubscription.id,
- teamId: user.teamId,
- actorId: user.id,
- data: {
- name: webhookSubscription.name,
- url: webhookSubscription.url,
- events: webhookSubscription.events,
- },
- ip: ctx.request.ip,
- };
- await Event.create(event);
-
- ctx.body = {
- data: presentWebhookSubscription(webhookSubscription),
- };
-});
+router.post(
+ "webhookSubscriptions.delete",
+ auth({ admin: true }),
+ async (ctx) => {
+ const { id } = ctx.body;
+ assertUuid(id, "id is required");
+ const { user } = ctx.state;
+ const webhookSubscription = await WebhookSubscription.findByPk(id);
+
+ authorize(user, "delete", webhookSubscription);
+
+ await webhookSubscription.destroy();
+
+ const event: WebhookSubscriptionEvent = {
+ name: "webhook_subscriptions.delete",
+ modelId: webhookSubscription.id,
+ teamId: user.teamId,
+ actorId: user.id,
+ data: {
+ name: webhookSubscription.name,
+ url: webhookSubscription.url,
+ events: webhookSubscription.events,
+ },
+ ip: ctx.request.ip,
+ };
+ await Event.create(event);
+ }
+);
+
+router.post(
+ "webhookSubscriptions.update",
+ auth({ admin: true }),
+ async (ctx) => {
+ const { id } = ctx.body;
+ assertUuid(id, "id is required");
+ const { user } = ctx.state;
+
+ const { name, url } = ctx.request.body;
+ const events: string[] = compact(ctx.request.body.events);
+ assertPresent(name, "name is required");
+ assertPresent(url, "url is required");
+ assertArray(events, "events is required");
+ if (events.length === 0) {
+ throw ValidationError("events are required");
+ }
+
+ const webhookSubscription = await WebhookSubscription.findByPk(id);
+
+ authorize(user, "update", webhookSubscription);
+
+ await webhookSubscription.update({ name, url, events, enabled: true });
+
+ const event: WebhookSubscriptionEvent = {
+ name: "webhook_subscriptions.update",
+ modelId: webhookSubscription.id,
+ teamId: user.teamId,
+ actorId: user.id,
+ data: {
+ name: webhookSubscription.name,
+ url: webhookSubscription.url,
+ events: webhookSubscription.events,
+ },
+ ip: ctx.request.ip,
+ };
+ await Event.create(event);
+
+ ctx.body = {
+ data: presentWebhookSubscription(webhookSubscription),
+ };
+ }
+);
export default router;
diff --git a/server/routes/auth/providers/slack.ts b/server/routes/auth/providers/slack.ts
index 9810f9b64b37..f020edda7ce6 100644
--- a/server/routes/auth/providers/slack.ts
+++ b/server/routes/auth/providers/slack.ts
@@ -117,7 +117,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
router.get(
"slack.commands",
auth({
- required: false,
+ optional: true,
}),
async (ctx) => {
const { code, state, error } = ctx.request.query;
@@ -135,9 +135,11 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
if (!user) {
if (state) {
try {
- const team = await Team.findByPk(state as string);
+ const team = await Team.findByPk(String(state), {
+ rejectOnEmpty: true,
+ });
return ctx.redirect(
- `${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}`
+ `${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
);
} catch (err) {
return ctx.redirect(
@@ -152,8 +154,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
}
const endpoint = `${env.URL}/auth/slack.commands`;
- // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
- const data = await Slack.oauthAccess(code, endpoint);
+ const data = await Slack.oauthAccess(String(code), endpoint);
const authentication = await IntegrationAuthentication.create({
service: "slack",
userId: user.id,
@@ -178,7 +179,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
router.get(
"slack.post",
auth({
- required: false,
+ optional: true,
}),
async (ctx) => {
const { code, error, state } = ctx.request.query;
@@ -198,10 +199,17 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
// appropriate subdomain to complete the oauth flow
if (!user) {
try {
- const collection = await Collection.findByPk(state as string);
- const team = await Team.findByPk(collection!.teamId);
+ const collection = await Collection.findOne({
+ where: {
+ id: String(state),
+ },
+ rejectOnEmpty: true,
+ });
+ const team = await Team.findByPk(collection.teamId, {
+ rejectOnEmpty: true,
+ });
return ctx.redirect(
- `${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}`
+ `${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
);
} catch (err) {
return ctx.redirect(
diff --git a/server/test/factories.ts b/server/test/factories.ts
index d45edd37c66a..d9ff3cc3ce44 100644
--- a/server/test/factories.ts
+++ b/server/test/factories.ts
@@ -161,6 +161,10 @@ export async function buildAdmin(overrides: Partial = {}) {
return buildUser({ ...overrides, isAdmin: true });
}
+export async function buildViewer(overrides: Partial = {}) {
+ return buildUser({ ...overrides, isViewer: true });
+}
+
export async function buildInvite(overrides: Partial = {}) {
if (!overrides.teamId) {
const team = await buildTeam();
diff --git a/server/types.ts b/server/types.ts
index c527b5d6caef..eb40f2f8cb58 100644
--- a/server/types.ts
+++ b/server/types.ts
@@ -1,11 +1,16 @@
import { Context } from "koa";
import { FileOperation, Team, User } from "./models";
+export enum AuthenticationTypes {
+ API = "api",
+ APP = "app",
+}
+
export type ContextWithState = Context & {
state: {
user: User;
token: string;
- authType: "app" | "api";
+ authType: AuthenticationTypes;
};
};