feat(avatars): enable default avatar (#307) r=@rfk

Fixes #295
This commit is contained in:
Vlad Filippov 2018-02-28 14:55:10 -05:00 коммит произвёл GitHub
Родитель 158eb63a0e
Коммит 9b3366652e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 137 добавлений и 15 удалений

Просмотреть файл

@ -84,7 +84,8 @@ curl -v \
{
"uid": "6d940dd41e636cc156074109b8092f96",
"email": "user@example.domain",
"avatar": "https://secure.gravatar.com/avatar/6d940dd41e636cc156074109b8092f96"
"avatar": "https://firefoxusercontent.com/a9bff302615cd015692a099f691205cc",
"avatarDefault": false
}
```
@ -166,15 +167,29 @@ curl -v \
"https://profile.accounts.firefox.com/v1/avatar"
```
#### Response
```json
{
"id": "81625c14128d46c2b600e74a017fa4a8",
"url": "https://secure.gravatar.com/avatar/6d940dd41e636cc156074109b8092f96"
"id": "a9bff302615cd015692a099f691205cc",
"avatarDefault": false,
"avatar": "https://firefoxusercontent.com/a9bff302615cd015692a099f691205cc"
}
```
#### Response (no avatar set)
```json
{
"id": "00000000000000000000000000000000",
"avatarDefault": true,
"avatar": "https://firefoxusercontent.com/00000000000000000000000000000000"
}
```
### POST /v1/avatar/upload
- scope: `profile:avatar:write`

Двоичные данные
lib/assets/default-profile.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.5 KiB

Двоичные данные
lib/assets/default-profile_large.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.0 KiB

Двоичные данные
lib/assets/default-profile_small.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Просмотреть файл

@ -133,6 +133,12 @@ const conf = convict({
doc: 'Pattern to generate FxA avatar URLs. {id} will be replaced.',
default: 'http://127.0.0.1:1112/a/{id}',
env: 'IMG_URL'
},
defaultAvatarId: {
default: '00000000000000000000000000000000',
doc: 'Default avatar id',
env: 'DEFAULT_AVATAR_ID',
format: String
}
},
logging: {

Просмотреть файл

@ -0,0 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const config = require('../../config');
const FXA_URL_TEMPLATE = config.get('img.url');
function fxaUrl(id) {
return FXA_URL_TEMPLATE.replace('{id}', id);
}
module.exports = {
fxaUrl: fxaUrl
};

Просмотреть файл

@ -7,12 +7,14 @@ const Joi = require('joi');
const P = require('../../promise');
const AppError = require('../../error');
const config = require('../../config');
const db = require('../../db');
const logger = require('../../logging')('routes.avatar.delete');
const notifyProfileUpdated = require('../../updates-queue');
const validate = require('../../validate');
const workers = require('../../img-workers');
const DEFAULT_AVATAR_ID = config.get('img.defaultAvatarId');
const EMPTY = Object.create(null);
const FXA_PROVIDER = 'fxa';
@ -30,6 +32,11 @@ module.exports = {
}
},
handler: function deleteAvatar(req, reply) {
if (req.params.id === DEFAULT_AVATAR_ID) {
// if we are clearing the default avatar then do nothing
return reply({});
}
req.server.methods.batch.cache.drop(req, function() {
const uid = req.auth.credentials.user;
var avatar, lookup;

Просмотреть файл

@ -5,19 +5,27 @@
const Joi = require('joi');
const db = require('../../db');
const config = require('../../config');
const hex = require('buf').to.hex;
const validate = require('../../validate');
const logger = require('../../logging')('routes.avatar.get');
const avatarShared = require('./_shared');
const EMPTY = Object.create(null);
function avatarOrEmpty(avatar) {
const DEFAULT_AVATAR = {
avatar: avatarShared.fxaUrl(config.get('img.defaultAvatarId')),
avatarDefault: true,
id: config.get('img.defaultAvatarId')
};
function avatarOrDefault(avatar) {
if (avatar) {
return {
avatar: avatar.url,
avatarDefault: false,
id: hex(avatar.id),
avatar: avatar.url
};
}
return EMPTY;
return DEFAULT_AVATAR;
}
module.exports = {
@ -30,13 +38,14 @@ module.exports = {
id: Joi.string()
.regex(validate.hex)
.length(32),
avatarDefault: Joi.boolean(),
avatar: Joi.string().max(256)
}
},
handler: function avatar(req, reply) {
var uid = req.auth.credentials.user;
db.getSelectedAvatar(uid)
.then(avatarOrEmpty)
.then(avatarOrDefault)
.done(function (result) {
var rep = reply(result);
if (result.id) {

Просмотреть файл

@ -13,15 +13,14 @@ const img = require('../../img');
const notifyProfileUpdated = require('../../updates-queue');
const validate = require('../../validate');
const workers = require('../../img-workers');
const avatarShared = require('./_shared');
const FXA_PROVIDER = 'fxa';
const FXA_URL_TEMPLATE = config.get('img.url');
assert(FXA_URL_TEMPLATE.indexOf('{id}') !== -1,
'img.url must contain the string "{id}"');
function fxaUrl(id) {
return FXA_URL_TEMPLATE.replace('{id}', id);
}
const DEFAULT_AVATAR_ID = config.get('img.defaultAvatarId');
assert(DEFAULT_AVATAR_ID.length === 32, 'img.default');
module.exports = {
auth: {
@ -50,7 +49,9 @@ module.exports = {
handler: function upload(req, reply) {
req.server.methods.batch.cache.drop(req, function() {
var id = img.id();
var url = fxaUrl(id);
// precaution to avoid the default id from being overwritten
assert(id !== DEFAULT_AVATAR_ID);
var url = avatarShared.fxaUrl(id);
var uid = req.auth.credentials.user;
workers.upload(id, req.payload, req.headers)
.then(function save() {

Просмотреть файл

@ -6,8 +6,12 @@ const Boom = require('boom');
const Joi = require('joi');
const checksum = require('checksum');
const avatarShared = require('./avatar/_shared');
const config = require('../config');
const logger = require('../logging')('routes.profile');
const DEFAULT_AVATAR_URL = avatarShared.fxaUrl(config.get('img.defaultAvatarId'));
function hasAllowedScope(scopes) {
for (var i = 0, len = scopes.length; i < len; i++) {
var scope = scopes[i];
@ -36,6 +40,7 @@ module.exports = {
email: Joi.string().allow(null),
uid: Joi.string().allow(null),
avatar: Joi.string().allow(null),
avatarDefault: Joi.boolean().allow(null),
displayName: Joi.string().allow(null),
//openid-connect
@ -65,6 +70,14 @@ module.exports = {
if (creds.scope.indexOf('openid') !== -1) {
result.sub = creds.user;
}
if (result.avatar) {
// currently the batch requests extract a single property.
// to avoid refactoring the batch requests to support multiple properties,
// set the default flag here
result.avatarDefault = result.avatar === DEFAULT_AVATAR_URL;
}
var rep = reply(result);
var etag = computeEtag(result);
if (etag) {

Просмотреть файл

@ -3,11 +3,19 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Hapi = require('hapi');
const Boom = require('boom');
const path = require('path');
const Inert = require('inert');
const config = require('../config').getProperties();
const logger = require('../logging')('server.static');
const DEFAULT_AVATAR_DIR = path.resolve(__dirname, '..', 'assets');
const DEFAULT_AVATAR_ID = config.img.defaultAvatarId;
const DEFAULT_AVATAR = path.resolve(DEFAULT_AVATAR_DIR, 'default-profile.png');
const DEFAULT_AVATAR_LARGE = path.resolve(DEFAULT_AVATAR_DIR, 'default-profile_large.png');
const DEFAULT_AVATAR_SMALL = path.resolve(DEFAULT_AVATAR_DIR, 'default-profile_small.png');
exports.create = function() {
var server = new Hapi.Server({
debug: false
@ -20,6 +28,26 @@ exports.create = function() {
port: config.server.port + 1
});
server.route({
method: 'GET',
path: '/a/' + DEFAULT_AVATAR_ID + '{type?}',
handler: function (request, reply) {
switch (request.params.type) {
case '':
reply.file(DEFAULT_AVATAR);
break;
case '_small':
reply.file(DEFAULT_AVATAR_SMALL);
break;
case '_large':
reply.file(DEFAULT_AVATAR_LARGE);
break;
default:
reply(Boom.notFound());
}
}
});
server.route({
method: 'GET',
path: '/a/{id}',

Просмотреть файл

@ -13,8 +13,12 @@ const checksum = require('checksum');
const assert = require('insist');
const P = require('../lib/promise');
const config = require('../lib/config');
const avatarShared = require('../lib/routes/avatar/_shared');
const assertSecurityHeaders = require('./lib/util').assertSecurityHeaders;
const DEFAULT_AVATAR_ID = config.get('img.defaultAvatarId');
function randomHex(bytes) {
return crypto.randomBytes(bytes).toString('hex');
}
@ -71,9 +75,11 @@ describe('/profile', function() {
}
}).then(function(res) {
assert.equal(res.statusCode, 200);
assert.equal(Object.keys(res.result).length, 4);
assert.equal(res.result.uid, USERID);
assert.equal(res.result.email, 'user@example.domain');
assert.equal(res.result.avatar, null);
assert.equal(res.result.avatar, avatarShared.fxaUrl(DEFAULT_AVATAR_ID), 'return default avatar');
assert.equal(res.result.avatarDefault, true, 'has the default avatar flag');
assertSecurityHeaders(res);
});
});
@ -452,8 +458,10 @@ describe('/avatar', function() {
}
}).then(function(res) {
assert.equal(res.statusCode, 200);
assert.equal(Object.keys(res.result).length, 3);
assert.equal(res.result.avatar, GRAVATAR);
assert.equal(res.result.id, id2);
assert.equal(res.result.avatarDefault, false);
assertSecurityHeaders(res);
});
});

Просмотреть файл

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const assert = require('insist');
const avatarShared = require('../../../lib/routes/avatar/_shared');
const config = require('../../../lib/config').getProperties();
/*global describe,it,beforeEach*/
describe('routes/avatar/_shared', function () {
describe('fxaUrl', function () {
it('creates a proper avatarUrl', function () {
const id = 'foo';
assert.equal(avatarShared.fxaUrl(id), config.img.url.replace('{id}', id));
});
});
});