feat: Adds route-level role filtering. (#3734)
* feat: Adds route-level role filtering. Another layer in the onion of security and performance * fix: Regression in authentication middleware
This commit is contained in:
@@ -10,6 +10,7 @@ import Scrollable from "~/components/Scrollable";
|
|||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import { inviteUser } from "~/actions/definitions/users";
|
import { inviteUser } from "~/actions/definitions/users";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import OrganizationMenu from "~/menus/OrganizationMenu";
|
import OrganizationMenu from "~/menus/OrganizationMenu";
|
||||||
@@ -34,12 +35,15 @@ function AppSidebar() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { documents } = useStores();
|
const { documents } = useStores();
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
const user = useCurrentUser();
|
||||||
const can = usePolicy(team.id);
|
const can = usePolicy(team.id);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
documents.fetchDrafts();
|
if (!user.isViewer) {
|
||||||
documents.fetchTemplates();
|
documents.fetchDrafts();
|
||||||
}, [documents]);
|
documents.fetchTemplates();
|
||||||
|
}
|
||||||
|
}, [documents, user.isViewer]);
|
||||||
|
|
||||||
const [dndArea, setDndArea] = React.useState();
|
const [dndArea, setDndArea] = React.useState();
|
||||||
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
|
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Switch, Redirect, RouteComponentProps } from "react-router-dom";
|
import { Switch, Redirect, RouteComponentProps } from "react-router-dom";
|
||||||
import Archive from "~/scenes/Archive";
|
import Archive from "~/scenes/Archive";
|
||||||
@@ -11,6 +12,8 @@ import CenteredContent from "~/components/CenteredContent";
|
|||||||
import PlaceholderDocument from "~/components/PlaceholderDocument";
|
import PlaceholderDocument from "~/components/PlaceholderDocument";
|
||||||
import Route from "~/components/ProfiledRoute";
|
import Route from "~/components/ProfiledRoute";
|
||||||
import SocketProvider from "~/components/SocketProvider";
|
import SocketProvider from "~/components/SocketProvider";
|
||||||
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
const SettingsRoutes = React.lazy(
|
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 (
|
return (
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -71,14 +77,24 @@ export default function AuthenticatedRoutes() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
{can.createDocument && (
|
||||||
|
<Route exact path="/templates" component={Templates} />
|
||||||
|
)}
|
||||||
|
{can.createDocument && (
|
||||||
|
<Route exact path="/templates/:sort" component={Templates} />
|
||||||
|
)}
|
||||||
|
{can.createDocument && (
|
||||||
|
<Route exact path="/drafts" component={Drafts} />
|
||||||
|
)}
|
||||||
|
{can.createDocument && (
|
||||||
|
<Route exact path="/archive" component={Archive} />
|
||||||
|
)}
|
||||||
|
{can.createDocument && (
|
||||||
|
<Route exact path="/trash" component={Trash} />
|
||||||
|
)}
|
||||||
<Redirect from="/dashboard" to="/home" />
|
<Redirect from="/dashboard" to="/home" />
|
||||||
<Route path="/home/:tab" component={Home} />
|
<Route path="/home/:tab" component={Home} />
|
||||||
<Route path="/home" component={Home} />
|
<Route path="/home" component={Home} />
|
||||||
<Route exact path="/templates" component={Templates} />
|
|
||||||
<Route exact path="/templates/:sort" component={Templates} />
|
|
||||||
<Route exact path="/drafts" component={Drafts} />
|
|
||||||
<Route exact path="/archive" component={Archive} />
|
|
||||||
<Route exact path="/trash" component={Trash} />
|
|
||||||
<Redirect exact from="/starred" to="/home" />
|
<Redirect exact from="/starred" to="/home" />
|
||||||
<Redirect exact from="/collections/*" to="/collection/*" />
|
<Redirect exact from="/collections/*" to="/collection/*" />
|
||||||
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
||||||
@@ -103,3 +119,5 @@ export default function AuthenticatedRoutes() {
|
|||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default observer(AuthenticatedRoutes);
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import { Next } from "koa";
|
import { Next } from "koa";
|
||||||
|
import Logger from "@server/logging/Logger";
|
||||||
import tracer, { APM } from "@server/logging/tracing";
|
import tracer, { APM } from "@server/logging/tracing";
|
||||||
import { User, Team, ApiKey } from "@server/models";
|
import { User, Team, ApiKey } from "@server/models";
|
||||||
import { getUserForJWT } from "@server/utils/jwt";
|
import { getUserForJWT } from "@server/utils/jwt";
|
||||||
import { AuthenticationError, UserSuspendedError } from "../errors";
|
import {
|
||||||
import { ContextWithState } from "../types";
|
AuthenticationError,
|
||||||
|
AuthorizationError,
|
||||||
|
UserSuspendedError,
|
||||||
|
} from "../errors";
|
||||||
|
import { ContextWithState, AuthenticationTypes } from "../types";
|
||||||
|
|
||||||
export default function auth(
|
type AuthenticationOptions = {
|
||||||
options: {
|
/* An admin user role is required to access the route */
|
||||||
required?: boolean;
|
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) {
|
return async function authMiddleware(ctx: ContextWithState, next: Next) {
|
||||||
let token;
|
let token;
|
||||||
const authorizationHeader = ctx.request.get("authorization");
|
const authorizationHeader = ctx.request.get("authorization");
|
||||||
@@ -29,8 +42,11 @@ export default function auth(
|
|||||||
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
} else if (
|
||||||
} else if (ctx.body && ctx.body.token) {
|
ctx.body &&
|
||||||
|
typeof ctx.body === "object" &&
|
||||||
|
"token" in ctx.body
|
||||||
|
) {
|
||||||
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
||||||
token = ctx.body.token;
|
token = ctx.body.token;
|
||||||
} else if (ctx.request.query.token) {
|
} else if (ctx.request.query.token) {
|
||||||
@@ -39,15 +55,15 @@ export default function auth(
|
|||||||
token = ctx.cookies.get("accessToken");
|
token = ctx.cookies.get("accessToken");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token && options.required !== false) {
|
if (!token && options.optional !== true) {
|
||||||
throw AuthenticationError("Authentication required");
|
throw AuthenticationError("Authentication required");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user;
|
let user: User | null | undefined;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (String(token).match(/^[\w]{38}$/)) {
|
if (String(token).match(/^[\w]{38}$/)) {
|
||||||
ctx.state.authType = "api";
|
ctx.state.authType = AuthenticationTypes.API;
|
||||||
let apiKey;
|
let apiKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -78,7 +94,7 @@ export default function auth(
|
|||||||
throw AuthenticationError("Invalid API key");
|
throw AuthenticationError("Invalid API key");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.state.authType = "app";
|
ctx.state.authType = AuthenticationTypes.APP;
|
||||||
user = await getUserForJWT(String(token));
|
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
|
// 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.token = String(token);
|
||||||
ctx.state.user = user;
|
ctx.state.user = user;
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class User extends ParanoidModel {
|
|||||||
.map((c) => c.id);
|
.map((c) => c.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateActiveAt = (ip: string, force = false) => {
|
updateActiveAt = async (ip: string, force = false) => {
|
||||||
const fiveMinutesAgo = subMinutes(new Date(), 5);
|
const fiveMinutesAgo = subMinutes(new Date(), 5);
|
||||||
|
|
||||||
// ensure this is updated only every few minutes otherwise
|
// ensure this is updated only every few minutes otherwise
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import pagination from "./middlewares/pagination";
|
|||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post("apiKeys.create", auth(), async (ctx) => {
|
router.post("apiKeys.create", auth({ member: true }), async (ctx) => {
|
||||||
const { name } = ctx.body;
|
const { name } = ctx.body;
|
||||||
assertPresent(name, "name is required");
|
assertPresent(name, "name is required");
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
@@ -35,24 +35,29 @@ router.post("apiKeys.create", auth(), async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("apiKeys.list", auth(), pagination(), async (ctx) => {
|
router.post(
|
||||||
const { user } = ctx.state;
|
"apiKeys.list",
|
||||||
const keys = await ApiKey.findAll({
|
auth({ member: true }),
|
||||||
where: {
|
pagination(),
|
||||||
userId: user.id,
|
async (ctx) => {
|
||||||
},
|
const { user } = ctx.state;
|
||||||
order: [["createdAt", "DESC"]],
|
const keys = await ApiKey.findAll({
|
||||||
offset: ctx.state.pagination.offset,
|
where: {
|
||||||
limit: ctx.state.pagination.limit,
|
userId: user.id,
|
||||||
});
|
},
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
offset: ctx.state.pagination.offset,
|
||||||
|
limit: ctx.state.pagination.limit,
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
pagination: ctx.state.pagination,
|
pagination: ctx.state.pagination,
|
||||||
data: keys.map(presentApiKey),
|
data: keys.map(presentApiKey),
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.post("apiKeys.delete", auth(), async (ctx) => {
|
router.post("apiKeys.delete", auth({ member: true }), async (ctx) => {
|
||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
assertUuid(id, "id is required");
|
assertUuid(id, "id is required");
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
buildCollection,
|
buildCollection,
|
||||||
buildUser,
|
buildUser,
|
||||||
buildDocument,
|
buildDocument,
|
||||||
|
buildViewer,
|
||||||
} from "@server/test/factories";
|
} from "@server/test/factories";
|
||||||
import { flushdb, seed } from "@server/test/support";
|
import { flushdb, seed } from "@server/test/support";
|
||||||
|
|
||||||
@@ -1432,12 +1433,76 @@ describe("#documents.archived", () => {
|
|||||||
expect(body.data.length).toEqual(0);
|
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 () => {
|
it("should require authentication", async () => {
|
||||||
const res = await server.post("/api/documents.archived");
|
const res = await server.post("/api/documents.archived");
|
||||||
expect(res.status).toEqual(401);
|
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", () => {
|
describe("#documents.viewed", () => {
|
||||||
it("should return empty result if no views", async () => {
|
it("should return empty result if no views", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
|
|||||||
@@ -162,104 +162,117 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("documents.archived", auth(), pagination(), async (ctx) => {
|
router.post(
|
||||||
const { sort = "updatedAt" } = ctx.body;
|
"documents.archived",
|
||||||
|
auth({ member: true }),
|
||||||
|
pagination(),
|
||||||
|
async (ctx) => {
|
||||||
|
const { sort = "updatedAt" } = ctx.body;
|
||||||
|
|
||||||
assertSort(sort, Document);
|
assertSort(sort, Document);
|
||||||
let direction = ctx.body.direction;
|
let direction = ctx.body.direction;
|
||||||
if (direction !== "ASC") {
|
if (direction !== "ASC") {
|
||||||
direction = "DESC";
|
direction = "DESC";
|
||||||
|
}
|
||||||
|
const { user } = ctx.state;
|
||||||
|
const collectionIds = await user.collectionIds();
|
||||||
|
const collectionScope: Readonly<ScopeOptions> = {
|
||||||
|
method: ["withCollectionPermissions", user.id],
|
||||||
|
};
|
||||||
|
const viewScope: Readonly<ScopeOptions> = {
|
||||||
|
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);
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
pagination: ctx.state.pagination,
|
||||||
|
data,
|
||||||
|
policies,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const { user } = ctx.state;
|
);
|
||||||
const collectionIds = await user.collectionIds();
|
|
||||||
const collectionScope: Readonly<ScopeOptions> = {
|
router.post(
|
||||||
method: ["withCollectionPermissions", user.id],
|
"documents.deleted",
|
||||||
};
|
auth({ member: true }),
|
||||||
const viewScope: Readonly<ScopeOptions> = {
|
pagination(),
|
||||||
method: ["withViews", user.id],
|
async (ctx) => {
|
||||||
};
|
const { sort = "deletedAt" } = ctx.body;
|
||||||
const documents = await Document.scope([
|
|
||||||
"defaultScope",
|
assertSort(sort, Document);
|
||||||
collectionScope,
|
let direction = ctx.body.direction;
|
||||||
viewScope,
|
if (direction !== "ASC") {
|
||||||
]).findAll({
|
direction = "DESC";
|
||||||
where: {
|
}
|
||||||
teamId: user.teamId,
|
const { user } = ctx.state;
|
||||||
collectionId: collectionIds,
|
const collectionIds = await user.collectionIds({
|
||||||
archivedAt: {
|
paranoid: false,
|
||||||
[Op.ne]: null,
|
});
|
||||||
|
const collectionScope: Readonly<ScopeOptions> = {
|
||||||
|
method: ["withCollectionPermissions", user.id],
|
||||||
|
};
|
||||||
|
const viewScope: Readonly<ScopeOptions> = {
|
||||||
|
method: ["withViews", user.id],
|
||||||
|
};
|
||||||
|
const documents = await Document.scope([
|
||||||
|
collectionScope,
|
||||||
|
viewScope,
|
||||||
|
]).findAll({
|
||||||
|
where: {
|
||||||
|
teamId: user.teamId,
|
||||||
|
collectionId: collectionIds,
|
||||||
|
deletedAt: {
|
||||||
|
[Op.ne]: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
include: [
|
||||||
order: [[sort, direction]],
|
{
|
||||||
offset: ctx.state.pagination.offset,
|
model: User,
|
||||||
limit: ctx.state.pagination.limit,
|
as: "createdBy",
|
||||||
});
|
paranoid: false,
|
||||||
const data = await Promise.all(
|
},
|
||||||
documents.map((document) => presentDocument(document))
|
{
|
||||||
);
|
model: User,
|
||||||
const policies = presentPolicies(user, documents);
|
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 = {
|
ctx.body = {
|
||||||
pagination: ctx.state.pagination,
|
pagination: ctx.state.pagination,
|
||||||
data,
|
data,
|
||||||
policies,
|
policies,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
router.post("documents.deleted", auth(), 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<ScopeOptions> = {
|
|
||||||
method: ["withCollectionPermissions", user.id],
|
|
||||||
};
|
|
||||||
const viewScope: Readonly<ScopeOptions> = {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
||||||
let { direction } = ctx.body;
|
let { direction } = ctx.body;
|
||||||
@@ -314,76 +327,81 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("documents.drafts", auth(), pagination(), async (ctx) => {
|
router.post(
|
||||||
let { direction } = ctx.body;
|
"documents.drafts",
|
||||||
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
|
auth({ member: true }),
|
||||||
|
pagination(),
|
||||||
|
async (ctx) => {
|
||||||
|
let { direction } = ctx.body;
|
||||||
|
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;
|
||||||
|
|
||||||
assertSort(sort, Document);
|
assertSort(sort, Document);
|
||||||
if (direction !== "ASC") {
|
if (direction !== "ASC") {
|
||||||
direction = "DESC";
|
direction = "DESC";
|
||||||
}
|
}
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
|
|
||||||
if (collectionId) {
|
if (collectionId) {
|
||||||
assertUuid(collectionId, "collectionId must be a UUID");
|
assertUuid(collectionId, "collectionId must be a UUID");
|
||||||
const collection = await Collection.scope({
|
const collection = await Collection.scope({
|
||||||
method: ["withMembership", user.id],
|
method: ["withMembership", user.id],
|
||||||
}).findByPk(collectionId);
|
}).findByPk(collectionId);
|
||||||
authorize(user, "read", collection);
|
authorize(user, "read", collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionIds = collectionId
|
const collectionIds = collectionId
|
||||||
? [collectionId]
|
? [collectionId]
|
||||||
: await user.collectionIds();
|
: await user.collectionIds();
|
||||||
const where: WhereOptions<Document> = {
|
const where: WhereOptions<Document> = {
|
||||||
createdById: user.id,
|
createdById: user.id,
|
||||||
collectionId: collectionIds,
|
collectionId: collectionIds,
|
||||||
publishedAt: {
|
publishedAt: {
|
||||||
[Op.is]: null,
|
[Op.is]: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dateFilter) {
|
if (dateFilter) {
|
||||||
assertIn(
|
assertIn(
|
||||||
dateFilter,
|
dateFilter,
|
||||||
["day", "week", "month", "year"],
|
["day", "week", "month", "year"],
|
||||||
"dateFilter must be one of day,week,month,year"
|
"dateFilter must be one of day,week,month,year"
|
||||||
);
|
);
|
||||||
where.updatedAt = {
|
where.updatedAt = {
|
||||||
[Op.gte]: subtractDate(new Date(), dateFilter),
|
[Op.gte]: subtractDate(new Date(), dateFilter),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
delete where.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionScope: Readonly<ScopeOptions> = {
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
delete where.updatedAt;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const collectionScope: Readonly<ScopeOptions> = {
|
|
||||||
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(
|
router.post(
|
||||||
"documents.info",
|
"documents.info",
|
||||||
auth({
|
auth({
|
||||||
required: false,
|
optional: true,
|
||||||
}),
|
}),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { id, shareId, apiVersion } = ctx.body;
|
const { id, shareId, apiVersion } = ctx.body;
|
||||||
@@ -421,7 +439,7 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
"documents.export",
|
"documents.export",
|
||||||
auth({
|
auth({
|
||||||
required: false,
|
optional: true,
|
||||||
}),
|
}),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { id, shareId } = ctx.body;
|
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;
|
const { id, collectionId, revisionId } = ctx.body;
|
||||||
assertPresent(id, "id is required");
|
assertPresent(id, "id is required");
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
@@ -588,7 +606,7 @@ router.post("documents.search_titles", auth(), pagination(), async (ctx) => {
|
|||||||
router.post(
|
router.post(
|
||||||
"documents.search",
|
"documents.search",
|
||||||
auth({
|
auth({
|
||||||
required: false,
|
optional: true,
|
||||||
}),
|
}),
|
||||||
pagination(),
|
pagination(),
|
||||||
async (ctx) => {
|
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;
|
const { id } = ctx.body;
|
||||||
assertPresent(id, "id is required");
|
assertPresent(id, "id is required");
|
||||||
const { user } = ctx.state;
|
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 {
|
const {
|
||||||
id,
|
id,
|
||||||
title,
|
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;
|
const { id, collectionId, parentDocumentId, index } = ctx.body;
|
||||||
assertUuid(id, "id must be a uuid");
|
assertUuid(id, "id must be a uuid");
|
||||||
assertUuid(collectionId, "collectionId 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;
|
const { id } = ctx.body;
|
||||||
assertPresent(id, "id is required");
|
assertPresent(id, "id is required");
|
||||||
const { user } = ctx.state;
|
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;
|
const { id, permanent } = ctx.body;
|
||||||
assertPresent(id, "id is required");
|
assertPresent(id, "id is required");
|
||||||
const { user } = ctx.state;
|
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;
|
const { id } = ctx.body;
|
||||||
assertPresent(id, "id is required");
|
assertPresent(id, "id is required");
|
||||||
const { user } = ctx.state;
|
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;
|
const { publish, collectionId, parentDocumentId, index } = ctx.body;
|
||||||
|
|
||||||
if (!ctx.is("multipart/form-data")) {
|
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 {
|
const {
|
||||||
title = "",
|
title = "",
|
||||||
text = "",
|
text = "",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import pagination from "./middlewares/pagination";
|
|||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post("fileOperations.info", auth(), async (ctx) => {
|
router.post("fileOperations.info", auth({ admin: true }), async (ctx) => {
|
||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
assertUuid(id, "id is required");
|
assertUuid(id, "id is required");
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
@@ -28,42 +28,47 @@ router.post("fileOperations.info", auth(), async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("fileOperations.list", auth(), pagination(), async (ctx) => {
|
router.post(
|
||||||
let { direction } = ctx.body;
|
"fileOperations.list",
|
||||||
const { sort = "createdAt", type } = ctx.body;
|
auth({ admin: true }),
|
||||||
assertIn(type, Object.values(FileOperationType));
|
pagination(),
|
||||||
assertSort(sort, FileOperation);
|
async (ctx) => {
|
||||||
|
let { direction } = ctx.body;
|
||||||
|
const { sort = "createdAt", type } = ctx.body;
|
||||||
|
assertIn(type, Object.values(FileOperationType));
|
||||||
|
assertSort(sort, FileOperation);
|
||||||
|
|
||||||
if (direction !== "ASC") {
|
if (direction !== "ASC") {
|
||||||
direction = "DESC";
|
direction = "DESC";
|
||||||
|
}
|
||||||
|
const { user } = ctx.state;
|
||||||
|
const where: WhereOptions<FileOperation> = {
|
||||||
|
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<FileOperation> = {
|
|
||||||
teamId: user.teamId,
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
const team = await Team.findByPk(user.teamId);
|
|
||||||
authorize(user, "manage", team);
|
|
||||||
|
|
||||||
const [exports, total] = await Promise.all([
|
router.post("fileOperations.redirect", auth({ admin: true }), async (ctx) => {
|
||||||
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) => {
|
|
||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
assertUuid(id, "id is required");
|
assertUuid(id, "id is required");
|
||||||
|
|
||||||
@@ -81,7 +86,7 @@ router.post("fileOperations.redirect", auth(), async (ctx) => {
|
|||||||
ctx.redirect(accessUrl);
|
ctx.redirect(accessUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("fileOperations.delete", auth(), async (ctx) => {
|
router.post("fileOperations.delete", auth({ admin: true }), async (ctx) => {
|
||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
assertUuid(id, "id is required");
|
assertUuid(id, "id is required");
|
||||||
|
|
||||||
|
|||||||
@@ -11,127 +11,144 @@ import pagination from "./middlewares/pagination";
|
|||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post("webhookSubscriptions.list", auth(), pagination(), async (ctx) => {
|
router.post(
|
||||||
const { user } = ctx.state;
|
"webhookSubscriptions.list",
|
||||||
authorize(user, "listWebhookSubscription", user.team);
|
auth({ admin: true }),
|
||||||
const webhooks = await WebhookSubscription.findAll({
|
pagination(),
|
||||||
where: {
|
async (ctx) => {
|
||||||
teamId: user.teamId,
|
const { user } = ctx.state;
|
||||||
},
|
authorize(user, "listWebhookSubscription", user.team);
|
||||||
order: [["createdAt", "DESC"]],
|
const webhooks = await WebhookSubscription.findAll({
|
||||||
offset: ctx.state.pagination.offset,
|
where: {
|
||||||
limit: ctx.state.pagination.limit,
|
teamId: user.teamId,
|
||||||
});
|
},
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
offset: ctx.state.pagination.offset,
|
||||||
|
limit: ctx.state.pagination.limit,
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
pagination: ctx.state.pagination,
|
pagination: ctx.state.pagination,
|
||||||
data: webhooks.map(presentWebhookSubscription),
|
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");
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const webhookSubscription = await WebhookSubscription.create({
|
router.post(
|
||||||
name,
|
"webhookSubscriptions.create",
|
||||||
events,
|
auth({ admin: true }),
|
||||||
createdById: user.id,
|
async (ctx) => {
|
||||||
teamId: user.teamId,
|
const { user } = ctx.state;
|
||||||
url,
|
authorize(user, "createWebhookSubscription", user.team);
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const event: WebhookSubscriptionEvent = {
|
const { name, url } = ctx.request.body;
|
||||||
name: "webhook_subscriptions.create",
|
const events: string[] = compact(ctx.request.body.events);
|
||||||
modelId: webhookSubscription.id,
|
assertPresent(name, "name is required");
|
||||||
teamId: user.teamId,
|
assertPresent(url, "url is required");
|
||||||
actorId: user.id,
|
assertArray(events, "events is required");
|
||||||
data: {
|
if (events.length === 0) {
|
||||||
|
throw ValidationError("events are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhookSubscription = await WebhookSubscription.create({
|
||||||
name,
|
name,
|
||||||
url,
|
|
||||||
events,
|
events,
|
||||||
},
|
createdById: user.id,
|
||||||
ip: ctx.request.ip,
|
teamId: user.teamId,
|
||||||
};
|
url,
|
||||||
await Event.create(event);
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = {
|
const event: WebhookSubscriptionEvent = {
|
||||||
data: presentWebhookSubscription(webhookSubscription),
|
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);
|
||||||
|
|
||||||
router.post("webhookSubscriptions.delete", auth(), async (ctx) => {
|
ctx.body = {
|
||||||
const { id } = ctx.body;
|
data: presentWebhookSubscription(webhookSubscription),
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const webhookSubscription = await WebhookSubscription.findByPk(id);
|
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, "update", webhookSubscription);
|
authorize(user, "delete", webhookSubscription);
|
||||||
|
|
||||||
await webhookSubscription.update({ name, url, events, enabled: true });
|
await webhookSubscription.destroy();
|
||||||
|
|
||||||
const event: WebhookSubscriptionEvent = {
|
const event: WebhookSubscriptionEvent = {
|
||||||
name: "webhook_subscriptions.update",
|
name: "webhook_subscriptions.delete",
|
||||||
modelId: webhookSubscription.id,
|
modelId: webhookSubscription.id,
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
actorId: user.id,
|
actorId: user.id,
|
||||||
data: {
|
data: {
|
||||||
name: webhookSubscription.name,
|
name: webhookSubscription.name,
|
||||||
url: webhookSubscription.url,
|
url: webhookSubscription.url,
|
||||||
events: webhookSubscription.events,
|
events: webhookSubscription.events,
|
||||||
},
|
},
|
||||||
ip: ctx.request.ip,
|
ip: ctx.request.ip,
|
||||||
};
|
};
|
||||||
await Event.create(event);
|
await Event.create(event);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
ctx.body = {
|
router.post(
|
||||||
data: presentWebhookSubscription(webhookSubscription),
|
"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;
|
export default router;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
router.get(
|
router.get(
|
||||||
"slack.commands",
|
"slack.commands",
|
||||||
auth({
|
auth({
|
||||||
required: false,
|
optional: true,
|
||||||
}),
|
}),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { code, state, error } = ctx.request.query;
|
const { code, state, error } = ctx.request.query;
|
||||||
@@ -135,9 +135,11 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
if (state) {
|
if (state) {
|
||||||
try {
|
try {
|
||||||
const team = await Team.findByPk(state as string);
|
const team = await Team.findByPk(String(state), {
|
||||||
|
rejectOnEmpty: true,
|
||||||
|
});
|
||||||
return ctx.redirect(
|
return ctx.redirect(
|
||||||
`${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return ctx.redirect(
|
return ctx.redirect(
|
||||||
@@ -152,8 +154,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `${env.URL}/auth/slack.commands`;
|
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(String(code), endpoint);
|
||||||
const data = await Slack.oauthAccess(code, endpoint);
|
|
||||||
const authentication = await IntegrationAuthentication.create({
|
const authentication = await IntegrationAuthentication.create({
|
||||||
service: "slack",
|
service: "slack",
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -178,7 +179,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
|||||||
router.get(
|
router.get(
|
||||||
"slack.post",
|
"slack.post",
|
||||||
auth({
|
auth({
|
||||||
required: false,
|
optional: true,
|
||||||
}),
|
}),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { code, error, state } = ctx.request.query;
|
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
|
// appropriate subdomain to complete the oauth flow
|
||||||
if (!user) {
|
if (!user) {
|
||||||
try {
|
try {
|
||||||
const collection = await Collection.findByPk(state as string);
|
const collection = await Collection.findOne({
|
||||||
const team = await Team.findByPk(collection!.teamId);
|
where: {
|
||||||
|
id: String(state),
|
||||||
|
},
|
||||||
|
rejectOnEmpty: true,
|
||||||
|
});
|
||||||
|
const team = await Team.findByPk(collection.teamId, {
|
||||||
|
rejectOnEmpty: true,
|
||||||
|
});
|
||||||
return ctx.redirect(
|
return ctx.redirect(
|
||||||
`${team!.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
`${team.url}/auth${ctx.request.path}?${ctx.request.querystring}`
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return ctx.redirect(
|
return ctx.redirect(
|
||||||
|
|||||||
@@ -161,6 +161,10 @@ export async function buildAdmin(overrides: Partial<User> = {}) {
|
|||||||
return buildUser({ ...overrides, isAdmin: true });
|
return buildUser({ ...overrides, isAdmin: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function buildViewer(overrides: Partial<User> = {}) {
|
||||||
|
return buildUser({ ...overrides, isViewer: true });
|
||||||
|
}
|
||||||
|
|
||||||
export async function buildInvite(overrides: Partial<User> = {}) {
|
export async function buildInvite(overrides: Partial<User> = {}) {
|
||||||
if (!overrides.teamId) {
|
if (!overrides.teamId) {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { Context } from "koa";
|
import { Context } from "koa";
|
||||||
import { FileOperation, Team, User } from "./models";
|
import { FileOperation, Team, User } from "./models";
|
||||||
|
|
||||||
|
export enum AuthenticationTypes {
|
||||||
|
API = "api",
|
||||||
|
APP = "app",
|
||||||
|
}
|
||||||
|
|
||||||
export type ContextWithState = Context & {
|
export type ContextWithState = Context & {
|
||||||
state: {
|
state: {
|
||||||
user: User;
|
user: User;
|
||||||
token: string;
|
token: string;
|
||||||
authType: "app" | "api";
|
authType: AuthenticationTypes;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user