0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-01-13 22:48:31 -05:00

feat: adds gravatar support for maintainers

This commit is contained in:
ayusharma 2018-07-29 00:45:02 +02:00
parent 61e4e56a76
commit 4df6b3b252
4 changed files with 357 additions and 133 deletions

View file

@ -63,7 +63,6 @@ export const API_MESSAGE = {
TAG_UPDATED: 'tags updated', TAG_UPDATED: 'tags updated',
TAG_REMOVED: 'tag removed', TAG_REMOVED: 'tag removed',
TAG_ADDED: 'package tagged', TAG_ADDED: 'package tagged',
}; };
export const API_ERROR = { export const API_ERROR = {
@ -86,6 +85,7 @@ export const API_ERROR = {
WEB_DISABLED: 'Web interface is disabled in the config file', WEB_DISABLED: 'Web interface is disabled in the config file',
DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead', DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead',
BAD_FORMAT_USER_GROUP: 'user groups is different than an array', BAD_FORMAT_USER_GROUP: 'user groups is different than an array',
RESOURCE_UNAVAILABLE: 'resource unavailable',
}; };
export const APP_ERROR = { export const APP_ERROR = {

View file

@ -9,7 +9,12 @@ import asciidoctor from 'asciidoctor.js';
import createError from 'http-errors'; import createError from 'http-errors';
import marked from 'marked'; import marked from 'marked';
import {HTTP_STATUS, API_ERROR, DEFAULT_PORT, DEFAULT_DOMAIN} from './constants'; import {
HTTP_STATUS,
API_ERROR,
DEFAULT_PORT,
DEFAULT_DOMAIN,
} from './constants';
import {generateGravatarUrl} from '../utils/user'; import {generateGravatarUrl} from '../utils/user';
import type {Package} from '@verdaccio/types'; import type {Package} from '@verdaccio/types';
@ -44,7 +49,11 @@ function validate_package(name: any): boolean {
return validateName(name[0]); return validateName(name[0]);
} else { } else {
// scoped package // scoped package
return name[0][0] === '@' && validateName(name[0].slice(1)) && validateName(name[1]); return (
name[0][0] === '@' &&
validateName(name[0].slice(1)) &&
validateName(name[1])
);
} }
} }
@ -60,13 +69,14 @@ function validateName(name: string): boolean {
name = name.toLowerCase(); name = name.toLowerCase();
// all URL-safe characters and "@" for issue #75 // all URL-safe characters and "@" for issue #75
return !(!name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/) return !(
|| name.charAt(0) === '.' // ".bin", etc. !name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/) ||
|| name.charAt(0) === '-' // "-" is reserved by couchdb name.charAt(0) === '.' || // ".bin", etc.
|| name === 'node_modules' name.charAt(0) === '-' || // "-" is reserved by couchdb
|| name === '__proto__' name === 'node_modules' ||
|| name === 'package.json' name === '__proto__' ||
|| name === 'favicon.ico' name === 'package.json' ||
name === 'favicon.ico'
); );
} }
@ -109,15 +119,17 @@ function validate_metadata(object: Package, name: string) {
* Create base url for registry. * Create base url for registry.
* @return {String} base registry url * @return {String} base registry url
*/ */
function combineBaseUrl(protocol: string, host: string, prefix?: string): string { function combineBaseUrl(
protocol: string,
host: string,
prefix?: string
): string {
let result = `${protocol}://${host}`; let result = `${protocol}://${host}`;
if (prefix) { if (prefix) {
prefix = prefix.replace(/\/$/, ''); prefix = prefix.replace(/\/$/, '');
result = (prefix.indexOf('/') === 0) result = prefix.indexOf('/') === 0 ? `${result}${prefix}` : prefix;
? `${result}${prefix}`
: prefix;
} }
return result; return result;
@ -135,13 +147,25 @@ export function extractTarballFromUrl(url: string) {
* @param {*} config * @param {*} config
* @return {String} a filtered package * @return {String} a filtered package
*/ */
export function convertDistRemoteToLocalTarballUrls(pkg: Package, req: $Request, urlPrefix: string | void) { export function convertDistRemoteToLocalTarballUrls(
pkg: Package,
req: $Request,
urlPrefix: string | void
) {
for (let ver in pkg.versions) { for (let ver in pkg.versions) {
if (Object.prototype.hasOwnProperty.call(pkg.versions, ver)) { if (Object.prototype.hasOwnProperty.call(pkg.versions, ver)) {
const distName = pkg.versions[ver].dist; const distName = pkg.versions[ver].dist;
if (_.isNull(distName) === false && _.isNull(distName.tarball) === false) { if (
distName.tarball = getLocalRegistryTarballUri(distName.tarball, pkg.name, req, urlPrefix); _.isNull(distName) === false &&
_.isNull(distName.tarball) === false
) {
distName.tarball = getLocalRegistryTarballUri(
distName.tarball,
pkg.name,
req,
urlPrefix
);
} }
} }
} }
@ -153,14 +177,23 @@ export function convertDistRemoteToLocalTarballUrls(pkg: Package, req: $Request,
* @param {*} uri * @param {*} uri
* @return {String} a parsed url * @return {String} a parsed url
*/ */
export function getLocalRegistryTarballUri(uri: string, pkgName: string, req: $Request, urlPrefix: string | void) { export function getLocalRegistryTarballUri(
uri: string,
pkgName: string,
req: $Request,
urlPrefix: string | void
) {
const currentHost = req.headers.host; const currentHost = req.headers.host;
if (!currentHost) { if (!currentHost) {
return uri; return uri;
} }
const tarballName = extractTarballFromUrl(uri); const tarballName = extractTarballFromUrl(uri);
const domainRegistry = combineBaseUrl(getWebProtocol(req), req.headers.host, urlPrefix); const domainRegistry = combineBaseUrl(
getWebProtocol(req),
req.headers.host,
urlPrefix
);
return `${domainRegistry}/${pkgName.replace(/\//g, '%2f')}/-/${tarballName}`; return `${domainRegistry}/${pkgName.replace(/\//g, '%2f')}/-/${tarballName}`;
} }
@ -227,7 +260,9 @@ function parse_address(urlAddress: any) {
// TODO: refactor it to something more reasonable? // TODO: refactor it to something more reasonable?
// //
// protocol : // ( host )|( ipv6 ): port / // protocol : // ( host )|( ipv6 ): port /
let urlPattern = /^((https?):(\/\/)?)?((([^\/:]*)|\[([^\[\]]+)\]):)?(\d+)\/?$/.exec(urlAddress); let urlPattern = /^((https?):(\/\/)?)?((([^\/:]*)|\[([^\[\]]+)\]):)?(\d+)\/?$/.exec(
urlAddress
);
if (urlPattern) { if (urlPattern) {
return { return {
@ -254,7 +289,8 @@ function parse_address(urlAddress: any) {
* @return {Array} sorted Array * @return {Array} sorted Array
*/ */
function semverSort(listVersions: Array<string>): string[] { function semverSort(listVersions: Array<string>): string[] {
return listVersions.filter(function(x) { return listVersions
.filter(function(x) {
if (!semver.parse(x, true)) { if (!semver.parse(x, true)) {
Logger.logger.warn({ver: x}, 'ignoring bad version @{ver}'); Logger.logger.warn({ver: x}, 'ignoring bad version @{ver}');
return false; return false;
@ -319,7 +355,7 @@ const parseIntervalTable = {
* @return {Number} * @return {Number}
*/ */
function parseInterval(interval: any) { function parseInterval(interval: any) {
if (typeof(interval) === 'number') { if (typeof interval === 'number') {
return interval * 1000; return interval * 1000;
} }
let result = 0; let result = 0;
@ -327,9 +363,11 @@ function parseInterval(interval: any) {
interval.split(/\s+/).forEach(function(x) { interval.split(/\s+/).forEach(function(x) {
if (!x) return; if (!x) return;
let m = x.match(/^((0|[1-9][0-9]*)(\.[0-9]+)?)(ms|s|m|h|d|w|M|y|)$/); let m = x.match(/^((0|[1-9][0-9]*)(\.[0-9]+)?)(ms|s|m|h|d|w|M|y|)$/);
if (!m if (
|| parseIntervalTable[m[4]] >= last_suffix !m ||
|| (m[4] === '' && last_suffix !== Infinity)) { parseIntervalTable[m[4]] >= last_suffix ||
(m[4] === '' && last_suffix !== Infinity)
) {
throw Error('invalid interval: ' + interval); throw Error('invalid interval: ' + interval);
} }
last_suffix = parseIntervalTable[m[4]]; last_suffix = parseIntervalTable[m[4]];
@ -362,24 +400,31 @@ const ErrorCode = {
return createError(HTTP_STATUS.BAD_REQUEST, customMessage); return createError(HTTP_STATUS.BAD_REQUEST, customMessage);
}, },
getInternalError: (customMessage?: string) => { getInternalError: (customMessage?: string) => {
return customMessage ? createError(HTTP_STATUS.INTERNAL_ERROR, customMessage) return customMessage
? createError(HTTP_STATUS.INTERNAL_ERROR, customMessage)
: createError(HTTP_STATUS.INTERNAL_ERROR); : createError(HTTP_STATUS.INTERNAL_ERROR);
}, },
getForbidden: (message: string = 'can\'t use this filename') => { getForbidden: (message: string = 'can\'t use this filename') => {
return createError(HTTP_STATUS.FORBIDDEN, message); return createError(HTTP_STATUS.FORBIDDEN, message);
}, },
getServiceUnavailable: (message: string = 'resource temporarily unavailable') => { getServiceUnavailable: (
message: string = API_ERROR.RESOURCE_UNAVAILABLE
) => {
return createError(HTTP_STATUS.SERVICE_UNAVAILABLE, message); return createError(HTTP_STATUS.SERVICE_UNAVAILABLE, message);
}, },
getNotFound: (customMessage?: string) => { getNotFound: (customMessage?: string) => {
return createError(HTTP_STATUS.NOT_FOUND, customMessage || API_ERROR.NO_PACKAGE); return createError(
HTTP_STATUS.NOT_FOUND,
customMessage || API_ERROR.NO_PACKAGE
);
}, },
getCode: (statusCode: number, customMessage: string) => { getCode: (statusCode: number, customMessage: string) => {
return createError(statusCode, customMessage); return createError(statusCode, customMessage);
}, },
}; };
const parseConfigFile = (configPath: string) => YAML.safeLoad(fs.readFileSync(configPath, 'utf8')); const parseConfigFile = (configPath: string) =>
YAML.safeLoad(fs.readFileSync(configPath, 'utf8'));
/** /**
* Check whether the path already exist. * Check whether the path already exist.
@ -431,28 +476,42 @@ function deleteProperties(propertiesToDelete: Array<string>, objectItem: any) {
return objectItem; return objectItem;
} }
function addGravatarSupport(pkgInfo: any) { function addGravatarSupport(pkgInfo: Object): Object {
if (_.isString(_.get(pkgInfo, 'latest.author.email'))) { const pkgInfoCopy = {...pkgInfo};
pkgInfo.latest.author.avatar = generateGravatarUrl(pkgInfo.latest.author.email); const author = _.get(pkgInfo, 'latest.author', null);
} else { const contributors = _.get(pkgInfo, 'latest.contributors', []);
// _.get can't guarantee author property exist const maintainers = _.get(pkgInfo, 'latest.maintainers', []);
_.set(pkgInfo, 'latest.author.avatar', generateGravatarUrl());
// for author.
if (author && _.isObject(author)) {
pkgInfoCopy.latest.author.avatar = generateGravatarUrl(author.email);
} }
if (_.get(pkgInfo, 'latest.contributors.length', 0) > 0) { if (author && _.isString(author)) {
pkgInfo.latest.contributors = _.map(pkgInfo.latest.contributors, (contributor) => { pkgInfoCopy.latest.author = {
if (_.isString(contributor.email)) { avatar: generateGravatarUrl(),
email: '',
author,
};
}
// for contributors
if (_.isEmpty(contributors) === false) {
pkgInfoCopy.latest.contributors = contributors.map((contributor) => {
contributor.avatar = generateGravatarUrl(contributor.email); contributor.avatar = generateGravatarUrl(contributor.email);
} else {
contributor.avatar = generateGravatarUrl();
}
return contributor; return contributor;
} });
);
} }
return pkgInfo; // for maintainers
if (_.isEmpty(maintainers) === false) {
pkgInfoCopy.latest.maintainers = maintainers.map((maintainer) => {
maintainer.avatar = generateGravatarUrl(maintainer.email);
return maintainer;
});
}
return pkgInfoCopy;
} }
/** /**
@ -467,7 +526,10 @@ function parseReadme(packageName: string, readme: string): string {
// asciidoc // asciidoc
if (docTypeIdentifier.test(readme)) { if (docTypeIdentifier.test(readme)) {
const ascii = asciidoctor(); const ascii = asciidoctor();
return ascii.convert(readme, {safe: 'safe', attributes: {showtitle: true, icons: 'font'}}); return ascii.convert(readme, {
safe: 'safe',
attributes: {showtitle: true, icons: 'font'},
});
} }
if (readme) { if (readme) {

View file

@ -1,18 +1,18 @@
// @flow // @flow
import {stringToMD5} from '../lib/crypto-utils'; import {stringToMD5} from '../lib/crypto-utils';
import _ from 'lodash';
export const GRAVATAR_DEFAULT =
export const GRAVATAR_DEFAULT = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm'; 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm';
/** /**
* Generate gravatar url from email address * Generate gravatar url from email address
*/ */
export function generateGravatarUrl(email?: string): string { export function generateGravatarUrl(email: string = ''): string {
if (typeof email === 'string') { let emailCopy = email;
email = email.trim().toLocaleLowerCase(); if (_.isString(email) && _.size(email) > 0) {
const emailMD5 = stringToMD5(email); emailCopy = email.trim().toLocaleLowerCase();
const emailMD5 = stringToMD5(emailCopy);
return `https://www.gravatar.com/avatar/${emailMD5}`; return `https://www.gravatar.com/avatar/${emailMD5}`;
} else { }
return GRAVATAR_DEFAULT; return GRAVATAR_DEFAULT;
} }
}

View file

@ -2,17 +2,22 @@
import assert from 'assert'; import assert from 'assert';
import { generateGravatarUrl, GRAVATAR_DEFAULT } from '../../../src/utils/user'; import { generateGravatarUrl, GRAVATAR_DEFAULT } from '../../../src/utils/user';
import { spliceURL } from '../../../src/utils/string'; import { spliceURL } from '../../../src/utils/string';
import Package from "../../../src/webui/components/Package/index"; import Package from '../../../src/webui/components/Package/index';
import {validateName as validate, convertDistRemoteToLocalTarballUrls, parseReadme} from '../../../src/lib/utils'; import {
validateName as validate,
convertDistRemoteToLocalTarballUrls,
parseReadme,
addGravatarSupport
} from '../../../src/lib/utils';
import Logger, { setup } from '../../../src/lib/logger'; import Logger, { setup } from '../../../src/lib/logger';
import { readFile } from '../../functional/lib/test.utils'; import { readFile } from '../../functional/lib/test.utils';
const readmeFile = (fileName: string = 'markdown.md') => readFile(`../../unit/partials/readme/${fileName}`); const readmeFile = (fileName: string = 'markdown.md') =>
readFile(`../../unit/partials/readme/${fileName}`);
setup([]); setup([]);
describe('Utilities', () => { describe('Utilities', () => {
describe('String utilities', () => { describe('String utilities', () => {
test('should splice two strings and generate a url', () => { test('should splice two strings and generate a url', () => {
const url: string = spliceURL('http://domain.com', '/-/static/logo.png'); const url: string = spliceURL('http://domain.com', '/-/static/logo.png');
@ -84,51 +89,66 @@ describe('Utilities', () => {
describe('Packages utilities', () => { describe('Packages utilities', () => {
const metadata: Package = { const metadata: Package = {
"name": "npm_test", name: 'npm_test',
"versions": { versions: {
"1.0.0": { '1.0.0': {
"dist": { dist: {
"tarball": "http:\/\/registry.org\/npm_test\/-\/npm_test-1.0.0.tgz" tarball: 'http://registry.org/npm_test/-/npm_test-1.0.0.tgz'
} }
}, },
"1.0.1": { '1.0.1': {
"dist": { dist: {
"tarball": "http:\/\/registry.org\/npm_test\/-\/npm_test-1.0.1.tgz" tarball: 'http://registry.org/npm_test/-/npm_test-1.0.1.tgz'
}
} }
} }
},
}; };
const buildURI = (host, version) => `http://${host}/npm_test/-/npm_test-${version}.tgz`; const buildURI = (host, version) =>
`http://${host}/npm_test/-/npm_test-${version}.tgz`;
const host = 'fake.com'; const host = 'fake.com';
test('convertDistRemoteToLocalTarballUrls', () => { test('convertDistRemoteToLocalTarballUrls', () => {
const convertDist = convertDistRemoteToLocalTarballUrls(
Object.assign({}, metadata),
// $FlowFixMe // $FlowFixMe
const convertDist = convertDistRemoteToLocalTarballUrls(Object.assign({}, metadata), { {
headers: { headers: {
host, host
}, },
get: () => 'http', get: () => 'http',
protocol: 'http' protocol: 'http'
}, ''); },
''
);
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(buildURI(host, '1.0.0')); expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(
expect(convertDist.versions['1.0.1'].dist.tarball).toEqual(buildURI(host, '1.0.1')); buildURI(host, '1.0.0')
);
expect(convertDist.versions['1.0.1'].dist.tarball).toEqual(
buildURI(host, '1.0.1')
);
}); });
}); });
describe('parseReadme', () => { describe('parseReadme', () => {
test('should pass for ascii text to html template', () => { test('should pass for ascii text to html template', () => {
const ascii = "= AsciiDoc"; const ascii = '= AsciiDoc';
expect(parseReadme('testPackage', ascii)).toEqual('<h1>AsciiDoc</h1>\n'); expect(parseReadme('testPackage', ascii)).toEqual('<h1>AsciiDoc</h1>\n');
expect(parseReadme('testPackage', String(readmeFile('ascii.adoc')))).toMatchSnapshot(); expect(
parseReadme('testPackage', String(readmeFile('ascii.adoc')))
).toMatchSnapshot();
}); });
test('should pass for makrdown text to html template', () => { test('should pass for makrdown text to html template', () => {
const markdown = '# markdown'; const markdown = '# markdown';
expect(parseReadme('testPackage', markdown)).toEqual('<h1 id="markdown">markdown</h1>\n'); expect(parseReadme('testPackage', markdown)).toEqual(
expect(parseReadme('testPackage', String(readmeFile('markdown.md')))).toMatchSnapshot(); '<h1 id="markdown">markdown</h1>\n'
);
expect(
parseReadme('testPackage', String(readmeFile('markdown.md')))
).toMatchSnapshot();
}); });
test('should pass for conversion of non-ascii to markdown text', () => { test('should pass for conversion of non-ascii to markdown text', () => {
@ -137,20 +157,162 @@ describe('Utilities', () => {
const randomTextNonAscii = 'simple text \n = ascii'; const randomTextNonAscii = 'simple text \n = ascii';
const randomTextMarkdown = 'simple text \n # markdown'; const randomTextMarkdown = 'simple text \n # markdown';
expect(parseReadme('testPackage', randomText)).toEqual('<p>%%%%%**##==</p>\n'); expect(parseReadme('testPackage', randomText)).toEqual(
expect(parseReadme('testPackage', simpleText)).toEqual('<p>simple text</p>\n'); '<p>%%%%%**##==</p>\n'
expect(parseReadme('testPackage', randomTextNonAscii)) );
.toEqual('<p>simple text \n = ascii</p>\n'); expect(parseReadme('testPackage', simpleText)).toEqual(
expect(parseReadme('testPackage', randomTextMarkdown)) '<p>simple text</p>\n'
.toEqual('<p>simple text </p>\n<h1 id="markdown">markdown</h1>\n'); );
expect(parseReadme('testPackage', randomTextNonAscii)).toEqual(
'<p>simple text \n = ascii</p>\n'
);
expect(parseReadme('testPackage', randomTextMarkdown)).toEqual(
'<p>simple text </p>\n<h1 id="markdown">markdown</h1>\n'
);
}); });
test('should show error for no readme data', () => { test('should show error for no readme data', () => {
const noData = ''; const noData = '';
const spy = jest.spyOn(Logger.logger, 'error') const spy = jest.spyOn(Logger.logger, 'error');
expect(parseReadme('testPackage', noData)) expect(parseReadme('testPackage', noData)).toEqual(
.toEqual('<p>ERROR: No README data found!</p>\n'); '<p>ERROR: No README data found!</p>\n'
expect(spy).toHaveBeenCalledWith({'packageName': 'testPackage'}, '@{packageName}: No readme found'); );
expect(spy).toHaveBeenCalledWith(
{ packageName: 'testPackage' },
'@{packageName}: No readme found'
);
});
});
describe('addGravatarSupport', () => {
test('check for blank object', () => {
expect(addGravatarSupport({})).toEqual({});
});
test('author, contributors and maintainers fields are not present', () => {
const packageInfo = {
latest: {}
};
expect(addGravatarSupport(packageInfo)).toEqual(packageInfo);
});
test('author field is a blank object', () => {
const packageInfo = { latest: { author: {} } };
expect(addGravatarSupport(packageInfo)).toEqual(packageInfo);
});
test('author field is a string type', () => {
const packageInfo = {
latest: { author: 'user@verdccio.org' }
};
const result = {
latest: {
author: {
author: 'user@verdccio.org',
avatar:
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm',
email: ''
}
}
};
expect(addGravatarSupport(packageInfo)).toEqual(result);
});
test('author field is an object type with author information', () => {
const packageInfo = {
latest: { author: { name: 'verdaccio', email: 'user@verdccio.org' } }
};
const result = {
latest: {
author: {
avatar:
'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
email: 'user@verdccio.org',
name: 'verdaccio'
}
}
};
expect(addGravatarSupport(packageInfo)).toEqual(result);
});
test('contributor field is a blank array', () => {
const packageInfo = {
latest: {
contributors: []
}
};
expect(addGravatarSupport(packageInfo)).toEqual(packageInfo);
});
test('contributors field has contributors', () => {
const packageInfo = {
latest: {
contributors: [
{ name: 'user', email: 'user@verdccio.org' },
{ name: 'user1', email: 'user1@verdccio.org' }
]
}
};
const result = {
latest: {
contributors: [
{
avatar:
'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
email: 'user@verdccio.org',
name: 'user'
},
{
avatar:
'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
email: 'user1@verdccio.org',
name: 'user1'
}
]
}
};
expect(addGravatarSupport(packageInfo)).toEqual(result);
});
test('maintainers field is a blank array', () => {
const packageInfo = {
latest: {
maintainers: []
}
};
expect(addGravatarSupport(packageInfo)).toEqual(packageInfo);
});
test('maintainers field has maintainers', () => {
const packageInfo = {
latest: {
maintainers: [
{ name: 'user', email: 'user@verdccio.org' },
{ name: 'user1', email: 'user1@verdccio.org' }
]
}
};
const result = {
latest: {
maintainers: [
{
avatar:
'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
email: 'user@verdccio.org',
name: 'user'
},
{
avatar:
'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
email: 'user1@verdccio.org',
name: 'user1'
}
]
}
};
expect(addGravatarSupport(packageInfo)).toEqual(result);
}); });
}); });
}); });