feat: Add setting to allow users to send invites (#6488)

This commit is contained in:
Tom Moor
2024-02-03 17:37:39 -08:00
committed by GitHub
parent 9046892864
commit c2b7d01c7d
14 changed files with 121 additions and 64 deletions

View File

@@ -58,8 +58,8 @@ export default async function userInviter({
teamId: user.teamId,
name: invite.name,
email: invite.email,
isAdmin: invite.role === UserRole.Admin,
isViewer: invite.role === UserRole.Viewer,
isAdmin: user.isAdmin && invite.role === UserRole.Admin,
isViewer: user.isViewer || invite.role === UserRole.Viewer,
invitedById: user.id,
flags: {
[UserFlag.InviteSent]: 1,

View File

@@ -1,7 +1,7 @@
import { DefaultState } from "koa";
import randomstring from "randomstring";
import ApiKey from "@server/models/ApiKey";
import { buildUser, buildTeam } from "@server/test/factories";
import { buildUser, buildTeam, buildAdmin } from "@server/test/factories";
import auth from "./authentication";
describe("Authentication middleware", () => {
@@ -156,7 +156,7 @@ describe("Authentication middleware", () => {
it("should return an error for suspended users", async () => {
const state = {} as DefaultState;
const admin = await buildUser();
const admin = await buildAdmin();
const user = await buildUser({
suspendedAt: new Date(),
suspendedById: admin.id,

View File

@@ -26,6 +26,7 @@ import {
AllowNull,
AfterUpdate,
BeforeUpdate,
BeforeCreate,
} from "sequelize-typescript";
import { TeamPreferenceDefaults } from "@shared/constants";
import {
@@ -347,6 +348,14 @@ class Team extends ParanoidModel<
// hooks
@BeforeCreate
static async setPreferences(model: Team) {
// Set here rather than in TeamPreferenceDefaults as we only want to enable by default for new
// workspaces.
model.setPreference(TeamPreference.MembersCanInvite, true);
return model;
}
@BeforeUpdate
static async checkDomain(model: Team, options: SaveOptions) {
if (!model.domain) {

View File

@@ -15,5 +15,5 @@ it("should serialize domain policies on Team", async () => {
});
const response = serialize(user, team);
expect(response.createDocument).toEqual(true);
expect(response.inviteUser).toEqual(false);
expect(response.inviteUser).toEqual(true);
});

View File

@@ -1,3 +1,4 @@
import { TeamPreference } from "@shared/types";
import { User, Team } from "@server/models";
import { AdminRequiredError } from "../errors";
import { allow } from "./cancan";
@@ -10,10 +11,10 @@ allow(
);
allow(User, "inviteUser", Team, (actor, team) => {
if (!team || actor.teamId !== team.id) {
if (!team || actor.teamId !== team.id || actor.isViewer) {
return false;
}
if (actor.isAdmin) {
if (actor.isAdmin || team.getPreference(TeamPreference.MembersCanInvite)) {
return true;
}

View File

@@ -35,6 +35,8 @@ export const TeamsUpdateSchema = BaseSchema.extend({
publicBranding: z.boolean().optional(),
/** Whether viewers should see download options. */
viewersCanExport: z.boolean().optional(),
/** Whether members can invite new people to the team. */
membersCanInvite: z.boolean().optional(),
/** Whether commenting is enabled */
commenting: z.boolean().optional(),
/** The custom theme for the team. */

View File

@@ -47,7 +47,7 @@ const handleTeamUpdate = async (ctx: APIContext<T.TeamsUpdateSchemaReq>) => {
router.post(
"team.update",
rateLimiter(RateLimiterStrategy.TenPerHour),
rateLimiter(RateLimiterStrategy.TenPerMinute),
auth(),
validate(T.TeamsUpdateSchema),
transaction(),
@@ -56,7 +56,7 @@ router.post(
router.post(
"teams.update",
rateLimiter(RateLimiterStrategy.TenPerHour),
rateLimiter(RateLimiterStrategy.TenPerMinute),
auth(),
validate(T.TeamsUpdateSchema),
transaction(),

View File

@@ -1,8 +1,10 @@
import { TeamPreference } from "@shared/types";
import {
buildTeam,
buildAdmin,
buildUser,
buildInvite,
buildViewer,
} from "@server/test/factories";
import { getTestServer } from "@server/test/support";
@@ -271,11 +273,51 @@ describe("#users.invite", () => {
expect(res.status).toEqual(400);
});
it("should require admin", async () => {
const admin = await buildUser();
it("should allow members to invite members", async () => {
const user = await buildUser();
const res = await server.post("/api/users.invite", {
body: {
token: admin.getJwtToken(),
token: user.getJwtToken(),
invites: [
{
email: "test@example.com",
name: "Test",
role: "member",
},
],
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.sent.length).toEqual(1);
});
it("should now allow viewers to invite", async () => {
const user = await buildViewer();
const res = await server.post("/api/users.invite", {
body: {
token: user.getJwtToken(),
invites: [
{
email: "test@example.com",
name: "Test",
role: "member",
},
],
},
});
expect(res.status).toEqual(403);
});
it("should allow restricting invites to admin", async () => {
const team = await buildTeam();
team.setPreference(TeamPreference.MembersCanInvite, false);
await team.save();
const user = await buildUser({ teamId: team.id });
const res = await server.post("/api/users.invite", {
body: {
token: user.getJwtToken(),
invites: [
{
email: "test@example.com",