packages/fxa-profile-server/lib/routes/profile.js (121 lines of code) (raw):
/* 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 Joi = require('joi');
const crypto = require('crypto');
const checksum = require('checksum');
const {
determineClientVisibleSubscriptionCapabilities,
} = require('../subscriptions');
const config = require('../config');
const db = require('../db');
const logger = require('../logging')('routes.profile');
const avatarShared = require('./avatar/_shared');
const monogramUrl = config.get('publicUrl');
const ALPHANUMERIC = /^[a-zA-Z0-9]/;
function computeEtag(profile) {
if (profile) {
return checksum(JSON.stringify(profile));
}
return false;
}
function nextAvatar(result) {
if (
result.avatar &&
(result.avatarDefault || result.avatar.startsWith(monogramUrl))
) {
const displayName = result.displayName;
let avatarUrl = result.avatar;
if (displayName && ALPHANUMERIC.test(displayName)) {
avatarUrl = `${monogramUrl}/v1/avatar/${displayName[0]}`;
} else if (ALPHANUMERIC.test(result.email)) {
avatarUrl = `${monogramUrl}/v1/avatar/${result.email[0]}`;
} else {
avatarUrl = avatarShared.DEFAULT_AVATAR.avatar;
}
return avatarUrl;
}
return result.avatar;
}
async function changeAvatar(avatarUrl, uid) {
if (avatarUrl === avatarShared.DEFAULT_AVATAR.avatar) {
await db.deleteUserAvatars(uid);
} else {
await db.addAvatar(
crypto.randomBytes(16).toString('hex'),
uid,
avatarUrl,
'fxa'
);
}
}
module.exports = {
auth: {
strategy: 'oauth',
},
response: {
schema: Joi.object({
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),
locale: Joi.string().allow(null),
amrValues: Joi.array().items(Joi.string().required()).allow(null),
twoFactorAuthentication: Joi.boolean().allow(null),
subscriptions: Joi.array().items(Joi.string().required()).optional(),
metricsEnabled: Joi.boolean().optional(),
//openid-connect
sub: Joi.string().allow(null),
accountDisabledAt: Joi.number().optional(),
accountLockedAt: Joi.number().optional(),
}),
},
handler: async function profile(req, h) {
const server = req.server;
const creds = req.auth.credentials;
function createResponse(response) {
const { value, cached, report } = response;
const result = value;
// `profileChangedAt` is an internal implementation detail that we don't
// return to reliers. As of now, we don't expect them to have any
// use for this.
delete result.profileChangedAt;
if (creds.scope.indexOf('openid') !== -1) {
result.sub = creds.user;
}
// Need to filter subscriptions by client ID for the request, since ALL
// capabilities for all clients is what we cache on user ID.
if (result.subscriptionsByClientId) {
result.subscriptions = determineClientVisibleSubscriptionCapabilities(
req.auth.credentials.client_id,
result.subscriptionsByClientId
);
delete result.subscriptionsByClientId;
}
let rep = h.response(result);
const etag = computeEtag(result);
if (etag) {
rep = h.response(result).etag(etag);
}
const lastModified = cached ? new Date(cached.stored) : new Date();
if (cached) {
logger.verbose('batch.cached', {
storedAt: cached.stored,
error: report && report.error,
ttl: cached.ttl,
});
} else {
logger.verbose('batch.db');
}
return rep.header('last-modified', lastModified.toUTCString());
}
let response = await server.methods.profileCache.get(req);
const result = response.value;
const newAvatar = nextAvatar(result);
const avatarChanged = result.avatar !== newAvatar;
if (avatarChanged) {
// Check if the db needs to be updated or just the profileCache
const selectedAvatar = await db.getSelectedAvatar(creds.user);
if (!selectedAvatar || selectedAvatar.url !== newAvatar) {
await changeAvatar(newAvatar, creds.user);
}
}
// Check to see if the oauth-server is reporting a newer `profileChangedAt`
// timestamp from validating the token, if so, lets invalidate the cache
// and set new value.
if (avatarChanged || result.profileChangedAt < creds.profile_changed_at) {
await server.methods.profileCache.drop(creds.user);
logger.info('profileChangedAt:cacheCleared', { uid: creds.user });
response = await server.methods.profileCache.get(req);
}
return createResponse(response);
},
};