diff --git a/src/api/web/api.js b/src/api/web/api.js index 9bec75a70..502ca0438 100644 --- a/src/api/web/api.js +++ b/src/api/web/api.js @@ -1,201 +1,35 @@ +import {Router} from 'express'; +import bodyParser from 'body-parser'; +import addUserAuthApi from './endpoint/user'; +import addPackageWebApi from './endpoint/package'; +import addSearchWebApi from './endpoint/search'; import Search from '../../lib/search'; -const bodyParser = require('body-parser'); -const express = require('express'); -const marked = require('marked'); -const _ = require('lodash'); -const Middleware = require('./middleware'); -const match = Middleware.match; -const validateName = Middleware.validate_name; -const validatePkg = Middleware.validate_package; -const securityIframe = Middleware.securityIframe; -const route = express.Router(); // eslint-disable-line -const async = require('async'); -const HTTPError = require('http-errors'); -const Utils = require('../../lib/utils'); -const {generateGravatarUrl} = require('../../utils/user'); +import {match, validate_name, validate_package, securityIframe} from './middleware'; + +const route = Router(); /* eslint new-cap: 0 */ /* This file include all verdaccio only API(Web UI), for npm API please see ../endpoint/ */ - module.exports = function(config, auth, storage) { + Search.configureStorage(storage); - const can = Middleware.allow(auth); // validate all of these params as a package name // this might be too harsh, so ask if it causes trouble - route.param('package', validatePkg); - route.param('filename', validateName); - route.param('version', validateName); + route.param('package', validate_package); + route.param('filename', validate_name); + route.param('version', validate_name); route.param('anything', match(/.*/)); route.use(bodyParser.urlencoded({extended: false})); route.use(auth.jwtMiddleware()); route.use(securityIframe); - // Get list of all visible package - route.get('/packages', function(req, res, next) { - storage.getLocalDatabase(function(err, packages) { - if (err) { - // that function shouldn't produce any - throw err; - } - - async.filterSeries( - packages, - function(pkg, cb) { - auth.allow_access(pkg.name, req.remote_user, function(err, allowed) { - setImmediate(function() { - if (err) { - cb(null, false); - } else { - cb(err, allowed); - } - }); - }); - }, - function(err, packages) { - if (err) throw err; - - packages.sort(function(a, b) { - if (a.name < b.name) { - return -1; - } else { - return 1; - } - }); - next(packages); - } - ); - }); - }); - - // Get package readme - route.get('/package/readme/(@:scope/)?:package/:version?', can('access'), function(req, res, next) { - let packageName = req.params.package; - if (req.params.scope) { - packageName = `@${req.params.scope}/${packageName}`; - } - storage.get_package({ - name: packageName, - req, - callback: function(err, info) { - if (err) { - return next(err); - } - res.set('Content-Type', 'text/plain'); - next(marked(info.readme || 'ERROR: No README data found!')); - }, - }); - }); - - // Search package - route.get('/search/:anything', function(req, res, next) { - const results = Search.query(req.params.anything); - const packages = []; - - const getPackageInfo = function(i) { - storage.get_package({ - name: results[i].ref, - callback: (err, entry) => { - if (!err && entry) { - auth.allow_access(entry.name, req.remote_user, function(err, allowed) { - if (err || !allowed) { - return; - } - - packages.push(entry.versions[entry['dist-tags'].latest]); - }); - } - - if (i >= results.length - 1) { - next(packages); - } else { - getPackageInfo(i + 1); - } - }, - }); - }; - - if (results.length) { - getPackageInfo(0); - } else { - next([]); - } - }); - - route.post('/login', function(req, res, next) { - auth.authenticate(req.body.username, req.body.password, (err, user) => { - if (!err) { - req.remote_user = user; - - next({ - token: auth.issue_token(user, '24h'), - username: req.remote_user.name, - }); - } else { - next(HTTPError[err.message ? 401 : 500](err.message)); - } - }); - }); - - route.post('/-/logout', function(req, res, next) { - let base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix); - res.cookies.set('token', ''); - res.redirect(base); - }); - - route.get('/sidebar/(@:scope/)?:package', function(req, res, next) { - let packageName = req.params.package; - if (req.params.scope) { - packageName = `@${req.params.scope}/${packageName}`; - } - - storage.get_package({ - name: packageName, - keepUpLinkData: true, - req, - callback: function(err, info) { - res.set('Content-Type', 'application/json'); - - if (!err) { - info.latest = info.versions[info['dist-tags'].latest]; - let propertyToDelete = ['readme', 'versions']; - - _.forEach(propertyToDelete, ((property) => { - delete info[property]; - })); - - - if (typeof _.get(info, 'latest.author.email') === 'string') { - info.latest.author.avatar = generateGravatarUrl(info.latest.author.email); - } else { - // _.get can't guarantee author property exist - _.set(info, 'latest.author.avatar', generateGravatarUrl()); - } - - if (_.get(info, 'latest.contributors.length', 0) > 0) { - info.latest.contributors = _.map(info.latest.contributors, (contributor) => { - if (typeof contributor.email === 'string') { - contributor.avatar = generateGravatarUrl(contributor.email); - } else { - contributor.avatar = generateGravatarUrl(); - } - - return contributor; - } - ); - } - - res.end(JSON.stringify(info)); - } else { - res.status(404); - res.end(); - } - }, - }); - }); + addPackageWebApi(route, storage, auth); + addSearchWebApi(route, storage, auth); + addUserAuthApi(route, auth, config); // What are you looking for? logout? client side will remove token when user click logout, // or it will auto expire after 24 hours. diff --git a/src/api/web/endpoint/package.js b/src/api/web/endpoint/package.js new file mode 100644 index 000000000..01381507a --- /dev/null +++ b/src/api/web/endpoint/package.js @@ -0,0 +1,82 @@ +import _ from 'lodash'; +import {addGravatarSupport, deleteProperties, sortByName} from '../../../lib/utils'; +import {addScope, allow} from '../middleware'; +import async from 'async'; +import marked from 'marked'; + +function addPackageWebApi(route, storage, auth) { + const can = allow(auth); + + // Get list of all visible package + route.get('/packages', function(req, res, next) { + storage.getLocalDatabase(function(err, packages) { + if (err) { + // that function shouldn't produce any + throw err; + } + + async.filterSeries( + packages, + function(pkg, cb) { + auth.allow_access(pkg.name, req.remote_user, function(err, allowed) { + setImmediate(function() { + if (err) { + cb(null, false); + } else { + cb(err, allowed); + } + }); + }); + }, + function(err, packages) { + if (err) { + throw err; + } + + next(sortByName(packages)); + } + ); + }); + }); + + // Get package readme + route.get('/package/readme/(@:scope/)?:package/:version?', can('access'), function(req, res, next) { + const packageName = req.params.scope ? addScope(req.params.scope, req.params.package) : req.params.package; + + storage.get_package({ + name: packageName, + req, + callback: function(err, info) { + if (err) { + return next(err); + } + + res.set('Content-Type', 'text/plain'); + next(marked(info.readme || 'ERROR: No README data found!')); + }, + }); + }); + + route.get('/sidebar/(@:scope/)?:package', function(req, res, next) { + const packageName = req.params.scope ? addScope(req.params.scope, req.params.package) : req.params.package; + + storage.get_package({ + name: packageName, + keepUpLinkData: true, + req, + callback: function(err, info) { + if (_.isNil(err)) { + info.latest = info.versions[info['dist-tags'].latest]; + info = deleteProperties(['readme', 'versions'], info); + info = addGravatarSupport(info); + next(info); + } else { + res.status(404); + res.end(); + } + }, + }); + }); +} + +export default addPackageWebApi; diff --git a/src/api/web/endpoint/search.js b/src/api/web/endpoint/search.js new file mode 100644 index 000000000..b9eb7f859 --- /dev/null +++ b/src/api/web/endpoint/search.js @@ -0,0 +1,40 @@ +import Search from '../../../lib/search'; + +function addSearchWebApi(route, storage, auth) { + // Search package + route.get('/search/:anything', function(req, res, next) { + const results = Search.query(req.params.anything); + const packages = []; + + const getPackageInfo = function(i) { + storage.get_package({ + name: results[i].ref, + callback: (err, entry) => { + if (!err && entry) { + auth.allow_access(entry.name, req.remote_user, function(err, allowed) { + if (err || !allowed) { + return; + } + + packages.push(entry.versions[entry['dist-tags'].latest]); + }); + } + + if (i >= results.length - 1) { + next(packages); + } else { + getPackageInfo(i + 1); + } + }, + }); + }; + + if (results.length) { + getPackageInfo(0); + } else { + next([]); + } + }); +} + +export default addSearchWebApi; diff --git a/src/api/web/endpoint/user.js b/src/api/web/endpoint/user.js new file mode 100644 index 000000000..9ea55e821 --- /dev/null +++ b/src/api/web/endpoint/user.js @@ -0,0 +1,28 @@ +import HTTPError from 'http-errors'; +import {combineBaseUrl, getWebProtocol} from '../../../lib/utils'; + +function addUserAuthApi(route, auth, config) { + route.post('/login', function(req, res, next) { + auth.authenticate(req.body.username, req.body.password, (err, user) => { + if (!err) { + req.remote_user = user; + + next({ + token: auth.issue_token(user, '24h'), + username: req.remote_user.name, + }); + } else { + next(HTTPError[err.message ? 401 : 500](err.message)); + } + }); + }); + + route.post('/-/logout', function(req, res, next) { + const base = combineBaseUrl(getWebProtocol(req), req.get('host'), config.url_prefix); + + res.cookies.set('token', ''); + res.redirect(base); + }); +} + +export default addUserAuthApi; diff --git a/src/lib/utils.js b/src/lib/utils.js index 5734e1a77..af8ebf7a6 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,13 +1,13 @@ -'use strict'; +import {generateGravatarUrl} from '../utils/user'; +import assert from 'assert'; +import semver from 'semver'; +import YAML from 'js-yaml'; +import URL from 'url'; +import fs from 'fs'; +import _ from 'lodash'; +import createError from 'http-errors'; -const assert = require('assert'); -const semver = require('semver'); -const YAML = require('js-yaml'); -const URL = require('url'); -const fs = require('fs'); -const _ = require('lodash'); const Logger = require('./logger'); -const createError = require('http-errors'); /** * Validate a package. @@ -380,6 +380,56 @@ function fileExists(path) { } } +function sortByName(packages) { + return packages.sort(function(a, b) { + if (a.name < b.name) { + return -1; + } else { + return 1; + } + }); +} + +function addScope(scope, packageName) { + return `@${scope}/${packageName}`; +} + +function deleteProperties(propertiesToDelete, packageInfo) { + _.forEach(propertiesToDelete, (property) => { + delete packageInfo[property]; + }); + + return packageInfo; +} + +function addGravatarSupport(info) { + if (_.isString(_.get(info, 'latest.author.email'))) { + info.latest.author.avatar = generateGravatarUrl(info.latest.author.email); + } else { + // _.get can't guarantee author property exist + _.set(info, 'latest.author.avatar', generateGravatarUrl()); + } + + if (_.get(info, 'latest.contributors.length', 0) > 0) { + info.latest.contributors = _.map(info.latest.contributors, (contributor) => { + if (_.isString(contributor.email)) { + contributor.avatar = generateGravatarUrl(contributor.email); + } else { + contributor.avatar = generateGravatarUrl(); + } + + return contributor; + } + ); + } + + return info; +} + +module.exports.addGravatarSupport = addGravatarSupport; +module.exports.deleteProperties = deleteProperties; +module.exports.addScope = addScope; +module.exports.sortByName = sortByName; module.exports.folder_exists = folder_exists; module.exports.file_exists = fileExists; module.exports.parseInterval = parseInterval; diff --git a/src/utils/user.js b/src/utils/user.js index 0405785e8..5eae16b7d 100644 --- a/src/utils/user.js +++ b/src/utils/user.js @@ -7,7 +7,8 @@ import {stringToMD5} from './string'; export function generateGravatarUrl(email?: string): string { if (typeof email === 'string') { email = email.trim().toLocaleLowerCase(); - let emailMD5 = stringToMD5(email); + const emailMD5 = stringToMD5(email); + return `https://www.gravatar.com/avatar/${emailMD5}`; } else { return 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mm';