diff --git a/migrations/schema.sql b/migrations/schema.sql index a3040daeb..33735a857 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS users( resetToken VARCHAR(128) DEFAULT "", resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, active BOOLEAN DEFAULT 1, - avatar MEDIUMBLOB NOT NULL, + avatar MEDIUMBLOB, backgroundImage MEDIUMBLOB, loginLocationsJson MEDIUMTEXT, // { locations: [{ ip, userAgent, city, country, ts }] } notificationConfigJson TEXT, diff --git a/src/routes/profile.js b/src/routes/profile.js index ef47d6302..d0fba81b6 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -8,6 +8,7 @@ exports = module.exports = { setFallbackEmail, getAvatarById, setAvatar, + unsetAvatar, setLanguage, getBackgroundImage, setBackgroundImage, @@ -126,6 +127,15 @@ async function setAvatar(req, res, next) { next(new HttpSuccess(204, {})); } +async function unsetAvatar(req, res, next) { + assert.strictEqual(typeof req.user, 'object'); + + const [error] = await safe(users.setAvatar(req.user, null)); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204, {})); +} + async function getAvatarById(req, res, next) { assert.strictEqual(typeof req.params, 'object'); diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index a9e072f84..d9c4db81c 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -328,6 +328,18 @@ describe('Profile API', function () { expect(parseInt(response.headers['content-length'])).to.equal(customAvatarSize); expect(response.status).to.equal(200); }); + + it('can unset custom avatar', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/profile/avatar`) + .query({ access_token: user.token }); + + expect(response.status).to.be(204); + }); + + it('did unset custom avatar', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/profile/avatar/${user.id}`).ok(() => true); + expect(response.status).to.be(404); + }); }); describe('background', function () { diff --git a/src/routes/test/user-directory-test.js b/src/routes/test/user-directory-test.js index 96c9e2591..9065e63c5 100644 --- a/src/routes/test/user-directory-test.js +++ b/src/routes/test/user-directory-test.js @@ -59,6 +59,27 @@ describe('User Directory API', function () { .ok(() => true); expect(response2.status).to.equal(403); // profile is locked + + const response3 = await superagent.post(`${serverUrl}/api/v1/profile/avatar`) + .query({ access_token: owner.token }) + .attach('avatar', './logo.png') + .ok(() => true); + + expect(response3.status).to.equal(403); // profile is locked + + const response4 = await superagent.post(`${serverUrl}/api/v1/profile/fallback_email`) + .query({ access_token: owner.token }) + .send({ email: 'newemail@example.Com', password: owner.password }) + .ok(() => true); + + expect(response4.status).to.equal(403); // profile is locked + + const response5 = await superagent.post(`${serverUrl}/api/v1/profile/display_name`) + .query({ access_token: owner.token }) + .send({ displayName: 'some new name' }) + .ok(() => true); + + expect(response5.status).to.equal(403); // profile is locked }); it('can set mandatory 2fa', async function() { diff --git a/src/routes/test/users-test.js b/src/routes/test/users-test.js index 9a4f5df6e..e303da46c 100644 --- a/src/routes/test/users-test.js +++ b/src/routes/test/users-test.js @@ -381,6 +381,38 @@ describe('Users API', function () { expect(response2.status).to.equal(200); expect(response2.body.displayName).to.equal(displayName); }); + + it('can change avatar', async function () { + let customAvatarSize = 0; + + const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}/avatar`) + .query({ access_token: owner.token }) + .attach('avatar', './logo.png'); + + customAvatarSize = require('fs').readFileSync('./logo.png').length; + + expect(response.status).to.equal(204); + + const response2 = await superagent.get(`${serverUrl}/api/v1/users/${user2.id}/avatar`) + .query({ access_token: owner.token }); + + expect(parseInt(response2.headers['content-length'])).to.equal(customAvatarSize); + expect(response2.status).to.equal(200); + }); + + it('can unset avatar', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/users/${user2.id}/avatar`) + .query({ access_token: owner.token }); + + expect(response.status).to.equal(204); + + const response2 = await superagent.get(`${serverUrl}/api/v1/users/${user2.id}/avatar`) + .query({ access_token: owner.token }) + .ok(() => true); + + expect(response2.status).to.equal(404); + }); + }); describe('password', function () { diff --git a/src/routes/users.js b/src/routes/users.js index 111bd5656..ea4246b2b 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -10,6 +10,7 @@ exports = module.exports = { setActive, getAvatar, setAvatar, + unsetAvatar, updateProfile, setPassword, @@ -112,10 +113,13 @@ async function getAvatar(req, res, next) { assert.strictEqual(typeof req.resources.user, 'object'); assert.strictEqual(typeof req.user, 'object'); + console.log('HERE'); const [avatarError, avatar] = await safe(users.getAvatar(req.resources.user)); if (avatarError) return next(BoxError.toHttpError(avatarError)); if (!avatar) return next(new HttpError(404, 'no avatar')); + console.log('GETT AVATAR TO', avatar.length, req.resources.user.id); + res.set('Content-Type', 'image/png'); res.status(200).send(avatar); } @@ -129,12 +133,24 @@ async function setAvatar(req, res, next) { safe.fs.unlinkSync(req.files.avatar.path); if (!avatar) return next(BoxError.toHttpError(new BoxError(BoxError.FS_ERROR, safe.error.message))); + console.log('SETTING AVATAR TO', avatar.length, req.resources.user.id); + const [error] = await safe(users.setAvatar(req.resources.user, avatar)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(204, {})); } +async function unsetAvatar(req, res, next) { + assert.strictEqual(typeof req.resources.user, 'object'); + assert.strictEqual(typeof req.user, 'object'); + + const [error] = await safe(users.setAvatar(req.resources.user, null)); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204, {})); +} + async function updateProfile(req, res, next) { assert.strictEqual(typeof req.resources.user, 'object'); assert.strictEqual(typeof req.user, 'object'); diff --git a/src/server.js b/src/server.js index e9247fcbb..d4ed18f6b 100644 --- a/src/server.js +++ b/src/server.js @@ -178,7 +178,8 @@ async function initializeExpressSync() { router.post('/api/v1/profile/fallback_email', json, token, authorizeUser, routes.profile.canEditProfile, routes.users.verifyPassword, routes.profile.setFallbackEmail); router.post('/api/v1/profile/language', json, token, authorizeUser, routes.profile.setLanguage); router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatarById); // this is not scoped so it can used directly in img tag - router.post('/api/v1/profile/avatar', token, authorizeUser, routes.profile.canEditProfile, multipart, routes.profile.setAvatar); // avatar is not exposed in LDAP. so it's personal and not locked + router.post('/api/v1/profile/avatar', token, authorizeUser, routes.profile.canEditProfile, multipart, routes.profile.setAvatar); + router.del ('/api/v1/profile/avatar', token, authorizeUser, routes.profile.canEditProfile, routes.profile.unsetAvatar); router.get ('/api/v1/profile/background_image', token, authorizeUser, routes.profile.getBackgroundImage); router.post('/api/v1/profile/background_image', token, authorizeUser, multipart, routes.profile.setBackgroundImage); // backgroundImage is not exposed in LDAP. so it's personal and not locked router.del ('/api/v1/profile/background_image', token, authorizeUser, routes.profile.unsetBackgroundImage); // backgroundImage is not exposed in LDAP. so it's personal and not locked @@ -211,6 +212,7 @@ async function initializeExpressSync() { router.post('/api/v1/users/:userId/profile', json, token, authorizeUserManager, routes.users.load, routes.users.updateProfile); router.get ('/api/v1/users/:userId/avatar', token, authorizeUserManager, routes.users.load, routes.users.getAvatar); router.post('/api/v1/users/:userId/avatar', token, authorizeUserManager, multipart, routes.users.load, routes.users.setAvatar); + router.del ('/api/v1/users/:userId/avatar', token, authorizeUserManager, routes.users.load, routes.users.unsetAvatar); router.post('/api/v1/users/:userId/password', json, token, authorizeUserManager, routes.users.load, routes.users.setPassword); router.post('/api/v1/users/:userId/ghost', json, token, authorizeAdmin, routes.users.load, routes.users.setGhost); router.put ('/api/v1/users/:userId/groups', json, token, authorizeUserManager, routes.users.load, routes.users.setLocalGroups); diff --git a/src/test/users-test.js b/src/test/users-test.js index 5e68daebf..30544c266 100644 --- a/src/test/users-test.js +++ b/src/test/users-test.js @@ -578,6 +578,32 @@ describe('User', function () { }); }); + describe('avatar', function () { + before(createOwner); + + it('default avatar is empty', async function () { + const result = await users.get(admin.id); + expect(result.avatar).to.be(undefined); // should not be in 'get' + + const result2 = await users.getAvatar(result); + expect(result2).to.be(null); + }); + + it('set avatar', async function () { + await users.setAvatar(admin, Buffer.from('ABC')); + + const avatar = await users.getAvatar(admin); + expect(avatar).to.be.eql(Buffer.from('ABC')); + }); + + it('reset avatar', async function () { + await users.setAvatar(admin, null); + + const avatar = await users.getAvatar(admin); + expect(avatar).to.be(null); + }); + }); + describe('invite', function () { before(createOwner); diff --git a/src/users.js b/src/users.js index 3eafdcf9a..24567dec9 100644 --- a/src/users.js +++ b/src/users.js @@ -985,7 +985,7 @@ async function getAvatar(user) { async function setAvatar(user, avatar) { assert.strictEqual(typeof user, 'object'); - assert(Buffer.isBuffer(avatar)); + assert(Buffer.isBuffer(avatar) || avatar === null); const result = await database.query('UPDATE users SET avatar=? WHERE id = ?', [ avatar, user.id ]); if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found');