fix: Allow viewers to upload avatar (#4349)
* fix: Allow viewers to upload avatar * DeleteAttachmentTask * fix: Previous avatar should be deleted on change, if possible * fix: Also cleanup team logo on change
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
buildCollection,
|
||||
buildAttachment,
|
||||
buildDocument,
|
||||
buildViewer,
|
||||
} from "@server/test/factories";
|
||||
import { getTestServer } from "@server/test/support";
|
||||
|
||||
@@ -18,32 +19,50 @@ describe("#attachments.create", () => {
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it("should allow simple image upload for public attachments", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.png",
|
||||
contentType: "image/png",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
describe("member", () => {
|
||||
it("should allow simple image upload for public attachments", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.png",
|
||||
contentType: "image/png",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not allow file upload for public attachments", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.pdf",
|
||||
contentType: "application/pdf",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not allow file upload for public attachments", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.pdf",
|
||||
contentType: "application/pdf",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
describe("viewer", () => {
|
||||
it("should allow simple image upload for public attachments", async () => {
|
||||
const user = await buildViewer();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.png",
|
||||
contentType: "image/png",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,22 +19,24 @@ const router = new Router();
|
||||
const AWS_S3_ACL = process.env.AWS_S3_ACL || "private";
|
||||
|
||||
router.post("attachments.create", auth(), async (ctx) => {
|
||||
const isPublic = ctx.request.body.public;
|
||||
const {
|
||||
name,
|
||||
documentId,
|
||||
contentType = "application/octet-stream",
|
||||
size,
|
||||
public: isPublic,
|
||||
} = ctx.request.body;
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(size, "size is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createAttachment", user.team);
|
||||
|
||||
// Public attachments are only used for avatars, so this is loosely coupled.
|
||||
// Public attachments are only used for avatars, so this is loosely coupled –
|
||||
// all user types can upload an avatar so no additional authorization is needed.
|
||||
if (isPublic) {
|
||||
assertIn(contentType, AttachmentValidation.avatarContentTypes);
|
||||
} else {
|
||||
authorize(user, "createAttachment", user.team);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -48,11 +50,11 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
);
|
||||
}
|
||||
|
||||
const s3Key = uuidv4();
|
||||
const modelId = uuidv4();
|
||||
const acl =
|
||||
isPublic === undefined ? AWS_S3_ACL : isPublic ? "public-read" : "private";
|
||||
const bucket = acl === "public-read" ? "public" : "uploads";
|
||||
const keyPrefix = `${bucket}/${user.id}/${s3Key}`;
|
||||
const keyPrefix = `${bucket}/${user.id}/${modelId}`;
|
||||
const key = `${keyPrefix}/${name}`;
|
||||
const presignedPost = await getPresignedPost(key, acl, contentType);
|
||||
const endpoint = publicS3Endpoint();
|
||||
@@ -69,6 +71,7 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
const attachment = await sequelize.transaction(async (transaction) => {
|
||||
const attachment = await Attachment.create(
|
||||
{
|
||||
id: modelId,
|
||||
key,
|
||||
acl,
|
||||
size,
|
||||
@@ -86,6 +89,7 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
modelId,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
ip: ctx.request.ip,
|
||||
|
||||
@@ -18,6 +18,10 @@ import { ValidationError } from "@server/errors";
|
||||
import logger from "@server/logging/Logger";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { rateLimiter } from "@server/middlewares/rateLimiter";
|
||||
import {
|
||||
transaction,
|
||||
TransactionContext,
|
||||
} from "@server/middlewares/transaction";
|
||||
import { Event, User, Team } from "@server/models";
|
||||
import { UserFlag, UserRole } from "@server/models/User";
|
||||
import { can, authorize } from "@server/policies";
|
||||
@@ -176,43 +180,51 @@ router.post("users.info", auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("users.update", auth(), async (ctx) => {
|
||||
const { user } = ctx.state;
|
||||
const { name, avatarUrl, language, preferences } = ctx.request.body;
|
||||
if (name) {
|
||||
user.name = name;
|
||||
}
|
||||
if (avatarUrl) {
|
||||
user.avatarUrl = avatarUrl;
|
||||
}
|
||||
if (language) {
|
||||
user.language = language;
|
||||
}
|
||||
if (preferences) {
|
||||
assertKeysIn(preferences, UserPreference);
|
||||
router.post(
|
||||
"users.update",
|
||||
auth(),
|
||||
transaction(),
|
||||
async (ctx: TransactionContext) => {
|
||||
const { user, transaction } = ctx.state;
|
||||
const { name, avatarUrl, language, preferences } = ctx.request.body;
|
||||
if (name) {
|
||||
user.name = name;
|
||||
}
|
||||
if (avatarUrl) {
|
||||
user.avatarUrl = avatarUrl;
|
||||
}
|
||||
if (language) {
|
||||
user.language = language;
|
||||
}
|
||||
if (preferences) {
|
||||
assertKeysIn(preferences, UserPreference);
|
||||
|
||||
for (const value of Object.values(UserPreference)) {
|
||||
if (has(preferences, value)) {
|
||||
assertBoolean(preferences[value]);
|
||||
user.setPreference(value, preferences[value]);
|
||||
for (const value of Object.values(UserPreference)) {
|
||||
if (has(preferences, value)) {
|
||||
assertBoolean(preferences[value]);
|
||||
user.setPreference(value, preferences[value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await user.save();
|
||||
await Event.create({
|
||||
name: "users.update",
|
||||
actorId: user.id,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
await user.save({ transaction });
|
||||
await Event.create(
|
||||
{
|
||||
name: "users.update",
|
||||
actorId: user.id,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
ip: ctx.request.ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
data: presentUser(user, {
|
||||
includeDetails: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
ctx.body = {
|
||||
data: presentUser(user, {
|
||||
includeDetails: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Admin specific
|
||||
router.post("users.promote", auth(), async (ctx) => {
|
||||
|
||||
Reference in New Issue
Block a user