Add ability to link Slack <-> Outline accounts (#6682)
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
Table,
|
||||
IsUUID,
|
||||
PrimaryKey,
|
||||
Scopes,
|
||||
} from "sequelize-typescript";
|
||||
import Model from "@server/models/base/Model";
|
||||
import { ValidationError } from "../errors";
|
||||
@@ -28,6 +29,20 @@ import AzureClient from "plugins/azure/server/azure";
|
||||
import GoogleClient from "plugins/google/server/google";
|
||||
import OIDCClient from "plugins/oidc/server/oidc";
|
||||
|
||||
@Scopes(() => ({
|
||||
withUserAuthentication: (userId: string) => ({
|
||||
include: [
|
||||
{
|
||||
model: UserAuthentication,
|
||||
as: "userAuthentications",
|
||||
required: true,
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}))
|
||||
@Table({
|
||||
tableName: "authentication_providers",
|
||||
modelName: "authentication_provider",
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
IsIn,
|
||||
} from "sequelize-typescript";
|
||||
import { IntegrationType, IntegrationService } from "@shared/types";
|
||||
import type {
|
||||
IntegrationSettings,
|
||||
UserCreatableIntegrationService,
|
||||
} from "@shared/types";
|
||||
import type { IntegrationSettings } from "@shared/types";
|
||||
import Collection from "./Collection";
|
||||
import IntegrationAuthentication from "./IntegrationAuthentication";
|
||||
import Team from "./Team";
|
||||
@@ -43,7 +40,7 @@ class Integration<T = unknown> extends IdModel<
|
||||
|
||||
@IsIn([Object.values(IntegrationService)])
|
||||
@Column(DataType.STRING)
|
||||
service: IntegrationService | UserCreatableIntegrationService;
|
||||
service: IntegrationService;
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
settings: IntegrationSettings<T>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import { Integration, User, Team } from "@server/models";
|
||||
import { AdminRequiredError } from "../errors";
|
||||
import { allow } from "./cancan";
|
||||
@@ -21,10 +22,16 @@ allow(
|
||||
);
|
||||
|
||||
allow(User, ["update", "delete"], Integration, (user, integration) => {
|
||||
if (user.isViewer) {
|
||||
if (!integration || user.teamId !== integration.teamId) {
|
||||
return false;
|
||||
}
|
||||
if (!integration || user.teamId !== integration.teamId) {
|
||||
if (
|
||||
integration.userId === user.id &&
|
||||
integration.type === IntegrationType.LinkedAccount
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (user.isViewer) {
|
||||
return false;
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
IntegrationService,
|
||||
UserCreatableIntegrationService,
|
||||
IntegrationType,
|
||||
} from "@shared/types";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import { IntegrationAuthentication, User } from "@server/models";
|
||||
import Integration from "@server/models/Integration";
|
||||
import {
|
||||
@@ -108,7 +104,7 @@ describe("#integrations.create", () => {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
type: IntegrationType.Embed,
|
||||
service: UserCreatableIntegrationService.Diagrams,
|
||||
service: IntegrationService.Diagrams,
|
||||
settings: { url: "not a url" },
|
||||
},
|
||||
});
|
||||
@@ -124,16 +120,14 @@ describe("#integrations.create", () => {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
type: IntegrationType.Analytics,
|
||||
service: UserCreatableIntegrationService.GoogleAnalytics,
|
||||
service: IntegrationService.GoogleAnalytics,
|
||||
settings: { measurementId: "123" },
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.type).toEqual(IntegrationType.Analytics);
|
||||
expect(body.data.service).toEqual(
|
||||
UserCreatableIntegrationService.GoogleAnalytics
|
||||
);
|
||||
expect(body.data.service).toEqual(IntegrationService.GoogleAnalytics);
|
||||
expect(body.data.settings).not.toBeFalsy();
|
||||
expect(body.data.settings.measurementId).toEqual("123");
|
||||
});
|
||||
@@ -145,14 +139,14 @@ describe("#integrations.create", () => {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
type: IntegrationType.Embed,
|
||||
service: UserCreatableIntegrationService.Grist,
|
||||
service: IntegrationService.Grist,
|
||||
settings: { url: "https://grist.example.com" },
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.type).toEqual(IntegrationType.Embed);
|
||||
expect(body.data.service).toEqual(UserCreatableIntegrationService.Grist);
|
||||
expect(body.data.service).toEqual(IntegrationService.Grist);
|
||||
expect(body.data.settings).not.toBeFalsy();
|
||||
expect(body.data.settings.url).toEqual("https://grist.example.com");
|
||||
});
|
||||
@@ -183,9 +177,7 @@ describe("#integrations.delete", () => {
|
||||
id: integration.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body.message).toEqual("Admin role required");
|
||||
});
|
||||
|
||||
it("should fail with status 400 bad request when id is not sent", async () => {
|
||||
@@ -200,6 +192,23 @@ describe("#integrations.delete", () => {
|
||||
expect(body.message).toEqual("id: Required");
|
||||
});
|
||||
|
||||
it("should succeed as user deleting own linked account integration", async () => {
|
||||
const user = await buildUser();
|
||||
const linkedAccount = await buildIntegration({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
service: IntegrationService.Slack,
|
||||
type: IntegrationType.LinkedAccount,
|
||||
});
|
||||
const res = await server.post("/api/integrations.delete", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: linkedAccount.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should succeed with status 200 ok when integration is deleted", async () => {
|
||||
const res = await server.post("/api/integrations.delete", {
|
||||
body: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Router from "koa-router";
|
||||
import { WhereOptions } from "sequelize";
|
||||
import { WhereOptions, Op } from "sequelize";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { transaction } from "@server/middlewares/transaction";
|
||||
@@ -20,19 +20,31 @@ router.post(
|
||||
pagination(),
|
||||
validate(T.IntegrationsListSchema),
|
||||
async (ctx: APIContext<T.IntegrationsListReq>) => {
|
||||
const { direction, type, sort } = ctx.input.body;
|
||||
const { direction, service, type, sort } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
let where: WhereOptions<Integration> = {
|
||||
teamId: user.teamId,
|
||||
};
|
||||
|
||||
if (type) {
|
||||
where = {
|
||||
...where,
|
||||
type,
|
||||
};
|
||||
where = { ...where, type };
|
||||
}
|
||||
if (service) {
|
||||
where = { ...where, service };
|
||||
}
|
||||
|
||||
// Linked account is special as these are user-specific, other integrations are workspace-wide.
|
||||
where = {
|
||||
...where,
|
||||
[Op.or]: [
|
||||
{ userId: user.id, type: IntegrationType.LinkedAccount },
|
||||
{
|
||||
type: {
|
||||
[Op.not]: IntegrationType.LinkedAccount,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const integrations = await Integration.findAll({
|
||||
where,
|
||||
@@ -121,7 +133,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
"integrations.delete",
|
||||
auth({ admin: true }),
|
||||
auth(),
|
||||
validate(T.IntegrationsDeleteSchema),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.IntegrationsDeleteReq>) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
IntegrationType,
|
||||
IntegrationService,
|
||||
UserCreatableIntegrationService,
|
||||
} from "@shared/types";
|
||||
import { Integration } from "@server/models";
|
||||
@@ -24,6 +25,9 @@ export const IntegrationsListSchema = BaseSchema.extend({
|
||||
|
||||
/** Integration type */
|
||||
type: z.nativeEnum(IntegrationType).optional(),
|
||||
|
||||
/** Integration service */
|
||||
service: z.nativeEnum(IntegrationService).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user