1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

refactor: html render middleware improvements (#3603)

* refactor: render middleware

* refactor: render middleware
This commit is contained in:
Juan Picado 2023-02-12 20:26:18 +01:00 committed by GitHub
parent 1b38fb2d30
commit 45c03819e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 576 additions and 783 deletions

@ -0,0 +1,15 @@
---
'@verdaccio/api': minor
'@verdaccio/config': minor
'@verdaccio/types': minor
'@verdaccio/hooks': minor
'@verdaccio/middleware': minor
'verdaccio-audit': minor
'@verdaccio/proxy': minor
'@verdaccio/server': minor
'@verdaccio/store': minor
'@verdaccio/web': minor
'@verdaccio/ui-theme': minor
---
refactor: render html middleware

@ -13,6 +13,7 @@ import {
validatioUtils,
} from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { rateLimit } from '@verdaccio/middleware';
import { Config, RemoteUser } from '@verdaccio/types';
import { getAuthenticatedMessage, mask } from '@verdaccio/utils';
@ -23,6 +24,7 @@ const debug = buildDebug('verdaccio:api:user');
export default function (route: Router, auth: Auth, config: Config): void {
route.get(
'/-/user/:org_couchdb_user',
rateLimit(config?.userRateLimit),
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
debug('verifying user');
const message = getAuthenticatedMessage(req.remote_user.name);
@ -53,6 +55,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
*/
route.put(
'/-/user/:org_couchdb_user/:_rev?/:revision?',
rateLimit(config?.userRateLimit),
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
const { name, password } = req.body;
debug('login or adduser');

@ -10,6 +10,7 @@ import {
errorUtils,
validatioUtils,
} from '@verdaccio/core';
import { rateLimit } from '@verdaccio/middleware';
import { Config } from '@verdaccio/types';
import { $NextFunctionVer, $RequestExtend } from '../../types/custom';
@ -41,6 +42,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
route.get(
'/-/npm/v1/user',
rateLimit(config?.userRateLimit),
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name) === false) {
return next(buildProfile(req.remote_user.name));
@ -55,6 +57,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
route.post(
'/-/npm/v1/user',
rateLimit(config?.userRateLimit),
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name)) {
res.status(HTTP_STATUS.UNAUTHORIZED);

@ -5,6 +5,7 @@ import { getApiToken } from '@verdaccio/auth';
import { Auth } from '@verdaccio/auth';
import { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, errorUtils } from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { rateLimit } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
import { Config, RemoteUser, Token } from '@verdaccio/types';
import { mask, stringToMD5 } from '@verdaccio/utils';
@ -26,6 +27,7 @@ function normalizeToken(token: Token): NormalizeToken {
export default function (route: Router, auth: Auth, storage: Storage, config: Config): void {
route.get(
'/-/npm/v1/tokens',
rateLimit(config?.userRateLimit),
async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { name } = req.remote_user;
@ -53,6 +55,7 @@ export default function (route: Router, auth: Auth, storage: Storage, config: Co
route.post(
'/-/npm/v1/tokens',
rateLimit(config?.userRateLimit),
function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { password, readonly, cidr_whitelist } = req.body;
const { name } = req.remote_user;
@ -123,6 +126,7 @@ export default function (route: Router, auth: Auth, storage: Storage, config: Co
route.delete(
'/-/npm/v1/tokens/token/:tokenKey',
rateLimit(config?.userRateLimit),
async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
const {
params: { tokenKey },

@ -1,5 +1,17 @@
const pkgVersion = require('../package.json').version;
import _ from 'lodash';
export function getUserAgent(): string {
return `verdaccio/${pkgVersion}`;
export function getUserAgent(
customUserAgent?: boolean | string,
version?: string,
name?: string
): string {
if (customUserAgent === true) {
return `${name}/${version}`;
} else if (_.isString(customUserAgent) && _.isEmpty(customUserAgent) === false) {
return customUserAgent;
} else if (customUserAgent === false) {
return 'hidden';
}
return `${name}/${version}`;
}

@ -10,6 +10,7 @@ import {
FlagsConfig,
PackageAccess,
PackageList,
RateLimit,
Security,
ServerSettingsConf,
} from '@verdaccio/types';
@ -28,11 +29,17 @@ const debug = buildDebug('verdaccio:config');
export const WEB_TITLE = 'Verdaccio';
// we limit max 1000 request per 15 minutes on user endpoints
export const defaultUserRateLimiting = {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000,
};
/**
* Coordinates the application configuration
*/
class Config implements AppConfig {
public user_agent: string;
public user_agent: string | undefined;
public uplinks: any;
public packages: PackageList;
public users: any;
@ -49,7 +56,7 @@ class Config implements AppConfig {
// @ts-ignore
public secret: string;
public flags: FlagsConfig;
public userRateLimit: RateLimit;
public constructor(config: ConfigYaml & { config_path: string }) {
const self = this;
this.storage = process.env.VERDACCIO_STORAGE_PATH || config.storage;
@ -65,6 +72,7 @@ class Config implements AppConfig {
this.flags = {
searchRemote: config.flags?.searchRemote ?? true,
};
this.user_agent = config.user_agent;
for (const configProp in config) {
if (self[configProp] == null) {
@ -72,11 +80,14 @@ class Config implements AppConfig {
}
}
// @ts-ignore
if (_.isNil(this.user_agent)) {
this.user_agent = getUserAgent();
if (typeof this.user_agent === 'undefined') {
// by default user agent is hidden
debug('set default user agent');
this.user_agent = getUserAgent(false);
}
this.userRateLimit = { ...defaultUserRateLimiting, ...config?.userRateLimit };
// some weird shell scripts are valid yaml files parsed as string
assert(_.isObject(config), APP_ERROR.CONFIG_NOT_VALID);

@ -5,6 +5,7 @@ export * from './package-access';
export { fromJStoYAML, parseConfigFile } from './parse';
export * from './uplinks';
export * from './security';
export * from './agent';
export * from './user';
export { default as ConfigBuilder } from './builder';
export { getDefaultConfig } from './conf';

@ -252,6 +252,7 @@ export interface ConfigYaml {
store?: any;
listen?: ListenAddress;
https?: HttpsConf;
user_agent?: string;
http_proxy?: string;
plugins?: string | void | null;
https_proxy?: string;
@ -264,6 +265,7 @@ export interface ConfigYaml {
url_prefix?: string;
server?: ServerSettingsConf;
flags?: FlagsConfig;
userRateLimit?: RateLimit;
// internal objects, added by internal yaml to JS config parser
// @deprecated use configPath instead
config_path?: string;
@ -277,7 +279,6 @@ export interface ConfigYaml {
* @extends {ConfigYaml}
*/
export interface Config extends Omit<ConfigYaml, 'packages' | 'security' | 'configPath'> {
user_agent: string;
server_id: string;
secret: string;
// save the configuration file path, it's fails without thi configPath

@ -15,7 +15,7 @@ const singleHeaderNotificationConfig = parseConfigFile(
);
const multiNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('multiple.notify'));
setup([]);
setup({});
const domain = 'http://slack-service';

@ -40,9 +40,14 @@
"dependencies": {
"@verdaccio/core": "workspace:6.0.0-6-next.59",
"@verdaccio/utils": "workspace:6.0.0-6-next.27",
"@verdaccio/config": "workspace:6.0.0-6-next.59",
"@verdaccio/url": "workspace:11.0.0-6-next.25",
"debug": "4.3.4",
"lru-cache": "7.14.1",
"express": "4.18.2",
"lodash": "4.17.21",
"mime": "2.6.0"
"mime": "2.6.0",
"express-rate-limit": "5.5.1"
},
"funding": {
"type": "opencollective",

@ -7,6 +7,9 @@ export { expectJson } from './middlewares/json';
export { antiLoop } from './middlewares/antiLoop';
export { final } from './middlewares/final';
export { allow } from './middlewares/allow';
export { rateLimit } from './middlewares/rate-limit';
export { userAgent } from './middlewares/user-agent';
export { webMiddleware } from './middlewares/web';
export { errorReportingMiddleware, handleError } from './middlewares/error';
export {
log,

@ -0,0 +1,8 @@
import RateLimit from 'express-rate-limit';
import { RateLimit as RateLimitType } from '@verdaccio/types';
export function rateLimit(rateLimitOptions?: RateLimitType) {
const limiter = new RateLimit(rateLimitOptions);
return limiter;
}

@ -0,0 +1,10 @@
import { getUserAgent } from '@verdaccio/config';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
export function userAgent(config) {
return function (_req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
res.setHeader('x-powered-by', getUserAgent(config?.user_agent));
next();
};
}

@ -4,15 +4,7 @@ import {
validatePackage as utilValidatePackage,
} from '@verdaccio/utils';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
export function validateName(
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer,
value: string,
name: string
): void {
export function validateName(_req, _res, next, value: string, name: string) {
if (value === '-') {
// special case in couchdb usually
next('route');
@ -23,13 +15,7 @@ export function validateName(
}
}
export function validatePackage(
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer,
value: string,
name: string
): void {
export function validatePackage(_req, _res, next, value: string, name: string) {
if (value === '-') {
// special case in couchdb usually
next('route');

@ -0,0 +1 @@
export { default as webMiddleware } from './web-middleware';

@ -1,39 +1,16 @@
import buildDebug from 'debug';
import express from 'express';
import _ from 'lodash';
import fs from 'fs';
import path from 'path';
import { HTTP_STATUS } from '@verdaccio/core';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { isURLhasValidProtocol } from '@verdaccio/url';
import renderHTML from '../renderHTML';
import { setSecurityWebHeaders } from './security';
import renderHTML, { isHTTPProtocol } from './utils/renderHTML';
const debug = buildDebug('verdaccio:web:render');
export async function loadTheme(config: any) {
if (_.isNil(config.theme) === false) {
const plugin = await asyncLoadPlugin(
config.theme,
{ config, logger },
// TODO: add types { staticPath: string; manifest: unknown; manifestFiles: unknown }
function (plugin: any) {
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
},
config?.serverSettings?.pluginPrefix ?? 'verdaccio-theme'
);
if (plugin.length > 1) {
logger.warn(
'multiple ui themes has been detected and is not supported, only the first one will be used'
);
}
return _.head(plugin);
}
}
const sendFileCallback = (next) => (err) => {
if (!err) {
return;
@ -45,14 +22,15 @@ const sendFileCallback = (next) => (err) => {
}
};
export async function renderWebMiddleware(config, auth): Promise<any> {
const { staticPath, manifest, manifestFiles } =
(await loadTheme(config)) || require('@verdaccio/ui-theme')();
export function renderWebMiddleware(config, tokenMiddleware, pluginOptions) {
const { staticPath, manifest, manifestFiles } = pluginOptions;
debug('static path %o', staticPath);
/* eslint new-cap:off */
const router = express.Router();
router.use(auth.webUIJWTmiddleware());
if (typeof tokenMiddleware === 'function') {
router.use(tokenMiddleware);
}
router.use(setSecurityWebHeaders);
// Logo
@ -77,6 +55,36 @@ export async function renderWebMiddleware(config, auth): Promise<any> {
res.sendFile(file, sendFileCallback(next));
});
// logo
if (config?.web?.logo && !isHTTPProtocol(config?.web?.logo)) {
// URI related to a local file
const absoluteLocalFile = path.posix.resolve(config.web.logo);
debug('serve local logo %s', absoluteLocalFile);
try {
// TODO: remove existsSync by async alternative
if (
fs.existsSync(absoluteLocalFile) &&
typeof fs.accessSync(absoluteLocalFile, fs.constants.R_OK) === 'undefined'
) {
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
// Use POSIX version `path.posix.join` instead.
config.web.logo = path.posix.join('/-/static/', path.basename(config.web.logo));
router.get(config.web.logo, function (_req, res, next) {
// @ts-ignore
debug('serve custom logo web:%s - local:%s', config.web.logo, absoluteLocalFile);
res.sendFile(absoluteLocalFile, sendFileCallback(next));
});
debug('enabled custom logo %s', config.web.logo);
} else {
config.web.logo = undefined;
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
}
} catch {
config.web.logo = undefined;
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
}
}
router.get('/-/web/:section/*', function (req, res) {
renderHTML(config, manifest, manifestFiles, req, res);
debug('render html section');

@ -1,11 +1,6 @@
import { HEADERS } from '@verdaccio/core';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '@verdaccio/middleware';
export function setSecurityWebHeaders(
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer
): void {
export function setSecurityWebHeaders(_req, res, next): void {
// disable loading in frames (clickjacking, etc.)
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
// avoid stablish connections outside of domain

@ -1,5 +1,6 @@
import buildDebug from 'debug';
import LRU from 'lru-cache';
import path from 'path';
import { URL } from 'url';
import { WEB_TITLE } from '@verdaccio/config';
@ -8,9 +9,8 @@ import { TemplateUIOptions } from '@verdaccio/types';
import { getPublicUrl } from '@verdaccio/url';
import renderTemplate from './template';
import { hasLogin, validatePrimaryColor } from './utils/web-utils';
import { hasLogin, validatePrimaryColor } from './web-utils';
const pkgJSON = require('../package.json');
const DEFAULT_LANGUAGE = 'es-US';
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 60 });
@ -21,6 +21,26 @@ const defaultManifestFiles = {
ico: 'favicon.ico',
};
/**
* Check if URI is starting with "http://", "https://" or "//"
* @param {string} uri
*/
export function isHTTPProtocol(uri: string): boolean {
return /^(https?:)?\/\//.test(uri);
}
export function resolveLogo(config, req) {
const isLocalFile = config?.web?.logo && !isHTTPProtocol(config?.web?.logo);
if (isLocalFile) {
return `${getPublicUrl(config?.url_prefix, req)}-/static/${path.basename(config?.web?.logo)}`;
} else if (isHTTPProtocol(config?.web?.logo)) {
return config?.web?.logo;
} else {
return '';
}
}
export default function renderHTML(config, manifest, manifestFiles, req, res) {
const { url_prefix } = config;
const base = getPublicUrl(config?.url_prefix, req);
@ -33,11 +53,13 @@ export default function renderHTML(config, manifest, manifestFiles, req, res) {
const title = config?.web?.title ?? WEB_TITLE;
const login = hasLogin(config);
const scope = config?.web?.scope ?? '';
const logoURI = config?.web?.logo ?? '';
const logoURI = resolveLogo(config, req);
const pkgManagers = config?.web?.pkgManagers ?? ['yarn', 'pnpm', 'npm'];
const version = pkgJSON.version;
const version = config?.web?.version;
const flags = {
...config.flags,
// legacy from 5.x
...config.experiments,
};
const primaryColor = validatePrimaryColor(config?.web?.primary_color) ?? '#4b5e40';
const {

@ -2,7 +2,7 @@ import buildDebug from 'debug';
import { TemplateUIOptions } from '@verdaccio/types';
import { Manifest, getManifestValue } from './utils/manifest';
import { Manifest, getManifestValue } from './manifest';
const debug = buildDebug('verdaccio:web:render:template');

@ -0,0 +1,18 @@
import buildDebug from 'debug';
import _ from 'lodash';
const debug = buildDebug('verdaccio:web:middlwares');
export function validatePrimaryColor(primaryColor) {
const isHex = /^#([0-9A-F]{3}){1,2}$/i.test(primaryColor);
if (!isHex) {
debug('invalid primary color %o', primaryColor);
return;
}
return primaryColor;
}
export function hasLogin(config: any) {
return _.isNil(config?.web?.login) || config?.web?.login === true;
}

@ -0,0 +1,27 @@
import express from 'express';
import { Router } from 'express';
import { validateName, validatePackage } from '../validation';
import { setSecurityWebHeaders } from './security';
export function webMiddleware(tokenMiddleware, webEndpointsApi) {
// eslint-disable-next-line new-cap
const route = Router();
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble=
route.param('package', validatePackage);
route.param('filename', validateName);
route.param('version', validateName);
route.use(express.urlencoded({ extended: false }));
if (typeof tokenMiddleware === 'function') {
route.use(tokenMiddleware);
}
route.use(setSecurityWebHeaders);
if (webEndpointsApi) {
route.use(webEndpointsApi);
}
return route;
}

@ -0,0 +1,15 @@
import express from 'express';
import { renderWebMiddleware } from './render-web';
import { webMiddleware } from './web-api';
export default (config, middlewares, pluginOptions): any => {
// eslint-disable-next-line new-cap
const router = express.Router();
const { tokenMiddleware, webEndpointsApi } = middlewares;
// render web
router.use('/', renderWebMiddleware(config, tokenMiddleware, pluginOptions));
// web endpoints, search, packages, etc
router.use('/-/verdaccio/', webMiddleware(tokenMiddleware, webEndpointsApi));
return router;
};

@ -0,0 +1,8 @@
import path from 'path';
import { parseConfigFile } from '@verdaccio/config';
export const getConf = (configName: string) => {
const configPath = path.join(__dirname, 'config', configName);
return parseConfigFile(configPath);
};

@ -0,0 +1,28 @@
auth:
auth-memory:
users:
test:
name: test
password: test
web:
title: verdaccio
publish:
allow_offline: false
uplinks:
log: { type: stdout, format: pretty, level: trace }
packages:
'@*/*':
access: $anonymous
publish: $anonymous
'**':
access: $anonymous
publish: $anonymous
_debug: true
flags:
changePassword: true

@ -0,0 +1,29 @@
auth:
auth-memory:
users:
test:
name: test
password: test
web:
title: verdaccio
login: false
publish:
allow_offline: false
uplinks:
log: { type: stdout, format: pretty, level: trace }
packages:
'@*/*':
access: $anonymous
publish: $anonymous
'**':
access: $anonymous
publish: $anonymous
_debug: true
flags:
changePassword: true

@ -0,0 +1,23 @@
web:
title: verdaccio web
login: true
scope: '@scope'
pkgManagers:
- pnpm
- yarn
showInfo: true
showSettings: true
showSearch: true
showFooter: true
showThemeSwitch: true
showDownloadTarball: true
showRaw: true
primary_color: '#ffffff'
logoURI: 'http://logo.org/logo.png'
url_prefix: /prefix
log: { type: stdout, format: pretty, level: trace }
flags:
changePassword: true

@ -1,4 +1,4 @@
import { getManifestValue } from '../src/utils/manifest';
import { getManifestValue } from '../src/middlewares/web/utils/manifest';
const manifest = require('./partials/manifest/manifest.json');

@ -0,0 +1 @@
export const parseHtml = (html) => require('node-html-parser').parse(html);

@ -0,0 +1,64 @@
{
"main.js": "/-/static/main.6126058572f989c948b1.js",
"main.css": "/-/static/main.6f2f2cccce0c813b509f.css",
"main.woff2": "/-/static/fonts/roboto-latin-900italic.woff2",
"main.woff": "/-/static/fonts/roboto-latin-900italic.woff",
"main.svg": "/-/static/93df1ce974e744e7d98f5d842da74ba0.svg",
"runtime.js": "/-/static/runtime.6126058572f989c948b1.js",
"NotFound.js": "/-/static/NotFound.6126058572f989c948b1.js",
"NotFound.svg": "/-/static/4743f1431b042843890a8644e89bb852.svg",
"Provider.js": "/-/static/Provider.6126058572f989c948b1.js",
"Version.css": "/-/static/454.97490e2b7f0dca05ddf3.css",
"Home.js": "/-/static/Home.6126058572f989c948b1.js",
"Home.css": "/-/static/268.97490e2b7f0dca05ddf3.css",
"Versions.js": "/-/static/Versions.6126058572f989c948b1.js",
"UpLinks.js": "/-/static/UpLinks.6126058572f989c948b1.js",
"Dependencies.js": "/-/static/Dependencies.6126058572f989c948b1.js",
"Engines.js": "/-/static/Engines.6126058572f989c948b1.js",
"Engines.svg": "/-/static/737531cc93ceb77b82b1c2e074a2557a.svg",
"Engines.png": "/-/static/2939f26c293bff8f35ba87194742aea8.png",
"Dist.js": "/-/static/Dist.6126058572f989c948b1.js",
"Install.js": "/-/static/Install.6126058572f989c948b1.js",
"Install.svg": "/-/static/1f07aa4bad48cd09088966736d1ed121.svg",
"Repository.js": "/-/static/Repository.6126058572f989c948b1.js",
"Repository.png": "/-/static/728ff5a8e44d74cd0f2359ef0a9ec88a.png",
"vendors.js": "/-/static/vendors.6126058572f989c948b1.js",
"38.6126058572f989c948b1.js": "/-/static/38.6126058572f989c948b1.js",
"26.6126058572f989c948b1.js": "/-/static/26.6126058572f989c948b1.js",
"761.6126058572f989c948b1.js": "/-/static/761.6126058572f989c948b1.js",
"4743f1431b042843890a8644e89bb852.svg": "/-/static/4743f1431b042843890a8644e89bb852.svg",
"node.png": "/-/static/2939f26c293bff8f35ba87194742aea8.png",
"fonts/roboto-latin-900italic.woff": "/-/static/fonts/roboto-latin-900italic.woff",
"fonts/roboto-latin-300italic.woff": "/-/static/fonts/roboto-latin-300italic.woff",
"fonts/roboto-latin-500italic.woff": "/-/static/fonts/roboto-latin-500italic.woff",
"fonts/roboto-latin-400italic.woff": "/-/static/fonts/roboto-latin-400italic.woff",
"fonts/roboto-latin-100italic.woff": "/-/static/fonts/roboto-latin-100italic.woff",
"fonts/roboto-latin-700italic.woff": "/-/static/fonts/roboto-latin-700italic.woff",
"fonts/roboto-latin-500.woff": "/-/static/fonts/roboto-latin-500.woff",
"fonts/roboto-latin-900.woff": "/-/static/fonts/roboto-latin-900.woff",
"fonts/roboto-latin-100.woff": "/-/static/fonts/roboto-latin-100.woff",
"fonts/roboto-latin-700.woff": "/-/static/fonts/roboto-latin-700.woff",
"fonts/roboto-latin-300.woff": "/-/static/fonts/roboto-latin-300.woff",
"fonts/roboto-latin-400.woff": "/-/static/fonts/roboto-latin-400.woff",
"fonts/roboto-latin-900italic.woff2": "/-/static/fonts/roboto-latin-900italic.woff2",
"fonts/roboto-latin-300italic.woff2": "/-/static/fonts/roboto-latin-300italic.woff2",
"fonts/roboto-latin-400italic.woff2": "/-/static/fonts/roboto-latin-400italic.woff2",
"fonts/roboto-latin-500italic.woff2": "/-/static/fonts/roboto-latin-500italic.woff2",
"fonts/roboto-latin-700italic.woff2": "/-/static/fonts/roboto-latin-700italic.woff2",
"fonts/roboto-latin-100italic.woff2": "/-/static/fonts/roboto-latin-100italic.woff2",
"fonts/roboto-latin-500.woff2": "/-/static/fonts/roboto-latin-500.woff2",
"fonts/roboto-latin-700.woff2": "/-/static/fonts/roboto-latin-700.woff2",
"fonts/roboto-latin-100.woff2": "/-/static/fonts/roboto-latin-100.woff2",
"fonts/roboto-latin-300.woff2": "/-/static/fonts/roboto-latin-300.woff2",
"fonts/roboto-latin-400.woff2": "/-/static/fonts/roboto-latin-400.woff2",
"fonts/roboto-latin-900.woff2": "/-/static/fonts/roboto-latin-900.woff2",
"favicon.ico": "/-/static/favicon.ico",
"git.png": "/-/static/728ff5a8e44d74cd0f2359ef0a9ec88a.png",
"logo.svg": "/-/static/93df1ce974e744e7d98f5d842da74ba0.svg",
"pnpm.svg": "/-/static/81ca2d852b9bc86713fe993bf5c7104c.svg",
"yarn.svg": "/-/static/1f07aa4bad48cd09088966736d1ed121.svg",
"logo-black-and-white.svg": "/-/static/983328eca26f265748c004651ca0e6c8.svg",
"npm.svg": "/-/static/737531cc93ceb77b82b1c2e074a2557a.svg",
"index.html": "/-/static/index.html",
"package.svg": "/-/static/4743f1431b042843890a8644e89bb852.svg"
}

@ -1,3 +1,4 @@
import express from 'express';
import { JSDOM } from 'jsdom';
import path from 'path';
import supertest from 'supertest';
@ -5,33 +6,30 @@ import supertest from 'supertest';
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { setup } from '@verdaccio/logger';
import { initializeServer } from './helper';
import { webMiddleware } from '../src';
import { getConf } from './_helper';
setup([]);
const pluginOptions = {
manifestFiles: {
js: ['runtime.js', 'vendors.js', 'main.js'],
},
staticPath: path.join(__dirname, 'static'),
manifest: require('./partials/manifest/manifest.json'),
};
const mockManifest = jest.fn();
jest.mock('@verdaccio/ui-theme', () => mockManifest());
const initializeServer = (configName: string, middlewares = {}) => {
const app = express();
app.use(webMiddleware(getConf(configName), middlewares, pluginOptions));
return app;
};
setup({});
describe('test web server', () => {
beforeAll(() => {
mockManifest.mockReturnValue(() => ({
manifestFiles: {
js: ['runtime.js', 'vendors.js', 'main.js'],
},
staticPath: path.join(__dirname, 'static'),
manifest: require('./partials/manifest/manifest.json'),
}));
});
afterEach(() => {
jest.clearAllMocks();
mockManifest.mockClear();
});
describe('render', () => {
describe('output', () => {
const render = async (config = 'default-test.yaml') => {
const response = await supertest(await initializeServer(config))
const response = await supertest(initializeServer(config))
.get('/')
.set('Accept', HEADERS.TEXT_HTML)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_HTML_UTF8)
@ -59,7 +57,7 @@ describe('test web server', () => {
// base: 'http://127.0.0.1:60864/prefix/',
// version: '6.0.0-6-next.28',
logoURI: '',
flags: { searchRemote: true },
flags: { changePassword: true },
login: true,
pkgManagers: ['pnpm', 'yarn'],
title: 'verdaccio web',

@ -0,0 +1,3 @@
{
main: '';
}

@ -0,0 +1,3 @@
{
'vendors': '';
}

@ -1,4 +1,4 @@
import template from '../src/template';
import template from '../src/middlewares/web/utils/template';
const manifest = require('./partials/manifest/manifest.json');

@ -1,4 +1,4 @@
import { validatePrimaryColor } from '../src/utils/web-utils';
import { validatePrimaryColor } from '../src/middlewares/web/utils/web-utils';
describe('Utilities', () => {
describe('validatePrimaryColor', () => {

@ -10,6 +10,12 @@
{
"path": "../auth"
},
{
"path": "../core/url"
},
{
"path": "../core/core"
},
{
"path": "../logger/logger"
},

@ -27,21 +27,19 @@
"main": "build/index.js",
"types": "build/index.d.ts",
"engines": {
"node": ">=14",
"npm": ">=6"
"node": ">=12"
},
"dependencies": {
"@verdaccio/core": "workspace:6.0.0-6-next.59",
"@verdaccio/config": "workspace:6.0.0-6-next.59",
"@verdaccio/logger": "workspace:6.0.0-6-next.27",
"express": "4.18.2",
"body-parser": "1.20.1",
"https-proxy-agent": "5.0.1",
"node-fetch": "cjs"
},
"devDependencies": {
"@verdaccio/types": "workspace:11.0.0-6-next.19",
"@verdaccio/auth": "workspace:6.0.0-6-next.38",
"@verdaccio/logger": "workspace:6.0.0-6-next.27",
"nock": "13.2.9",
"supertest": "6.3.3"
},

@ -1,4 +1,3 @@
import { json as jsonParser } from 'body-parser';
import express, { Express, Request, Response } from 'express';
import https from 'https';
import createHttpsProxyAgent from 'https-proxy-agent';
@ -84,10 +83,10 @@ export default class ProxyAudit
const router = express.Router();
/* eslint new-cap:off */
router.post('/audits', jsonParser({ limit: '10mb' }), handleAudit);
router.post('/audits/quick', jsonParser({ limit: '10mb' }), handleAudit);
router.post('/audits', express.json({ limit: '10mb' }), handleAudit);
router.post('/audits/quick', express.json({ limit: '10mb' }), handleAudit);
router.post('/advisories/bulk', jsonParser({ limit: '10mb' }), handleAudit);
router.post('/advisories/bulk', express.json({ limit: '10mb' }), handleAudit);
app.use('/-/npm/v1/security', router);
}

@ -1,4 +1,5 @@
export interface ConfigAudit {
enabled: boolean;
strict_ssl?: boolean | void;
max_body?: string;
strict_ssl?: boolean;
}

@ -9,7 +9,7 @@ import { logger, setup } from '@verdaccio/logger';
import { HTTP_STATUS } from '../../local-storage/node_modules/@verdaccio/core/build';
import ProxyAudit, { ConfigAudit } from '../src/index';
setup();
setup({});
const auditConfig: ConfigAudit = {
enabled: true,

@ -70,8 +70,8 @@ export interface IProxy {
fail_timeout: number;
upname: string;
search(options: ProxySearchParams): Promise<Stream.Readable>;
getRemoteMetadataNext(name: string, options: ISyncUplinksOptions): Promise<[Manifest, string]>;
fetchTarballNext(
getRemoteMetadata(name: string, options: ISyncUplinksOptions): Promise<[Manifest, string]>;
fetchTarball(
url: string,
options: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
): PassThrough;
@ -116,7 +116,7 @@ class ProxyStorage implements IProxy {
public constructor(config: UpLinkConfLocal, mainConfig: Config, agent?: Agents) {
this.config = config;
this.failed_requests = 0;
this.userAgent = mainConfig.user_agent;
this.userAgent = mainConfig.user_agent ?? 'hidden';
this.ca = config.ca;
this.logger = LoggerApi.logger.child({ sub: 'out' });
this.server_id = mainConfig.server_id;
@ -294,7 +294,7 @@ class ProxyStorage implements IProxy {
return headers;
}
public async getRemoteMetadataNext(
public async getRemoteMetadata(
name: string,
options: ISyncUplinksOptions
): Promise<[Manifest, string]> {
@ -443,7 +443,7 @@ class ProxyStorage implements IProxy {
}
// FIXME: handle stream and retry
public fetchTarballNext(
public fetchTarball(
url: string,
overrideOptions: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
): any {

@ -59,7 +59,7 @@ describe('proxy', () => {
const proxyPath = getConf('proxy1.yaml');
const conf = new Config(parseConfigFile(proxyPath));
describe('getRemoteMetadataNext', () => {
describe('getRemoteMetadata', () => {
beforeEach(() => {
nock.cleanAll();
nock.abortPendingRequests();
@ -78,7 +78,7 @@ describe('proxy', () => {
.get('/jquery')
.reply(200, { body: 'test' });
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
const [manifest] = await prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
});
expect(manifest).toEqual({ body: 'test' });
@ -104,7 +104,7 @@ describe('proxy', () => {
}
);
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
const [manifest, etag] = await prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
});
expect(etag).toEqual('_ref_4444');
@ -131,7 +131,7 @@ describe('proxy', () => {
}
);
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
const [manifest, etag] = await prox1.getRemoteMetadata('jquery', {
etag: 'foo',
remoteAddress: '127.0.0.1',
});
@ -146,7 +146,7 @@ describe('proxy', () => {
.get('/jquery')
.reply(200, { body: { name: 'foo', version: '1.0.0' } }, {});
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await prox1.getRemoteMetadataNext('jquery', {
await prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
});
expect(mockHttp).toHaveBeenCalledTimes(2);
@ -175,7 +175,7 @@ describe('proxy', () => {
test('proxy call with 304', async () => {
nock(domain).get('/jquery').reply(304);
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(prox1.getRemoteMetadataNext('jquery', { etag: 'rev_3333' })).rejects.toThrow(
await expect(prox1.getRemoteMetadata('jquery', { etag: 'rev_3333' })).rejects.toThrow(
'no data'
);
});
@ -184,7 +184,7 @@ describe('proxy', () => {
nock(domain).get('/jquery').replyWithError('something awful happened');
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
})
).rejects.toThrowError(new Error('something awful happened'));
@ -193,7 +193,7 @@ describe('proxy', () => {
test('reply with 409 error', async () => {
nock(domain).get('/jquery').reply(409);
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(prox1.getRemoteMetadataNext('jquery', { retry: 0 })).rejects.toThrow(
await expect(prox1.getRemoteMetadata('jquery', { retry: 0 })).rejects.toThrow(
new Error('bad status code: 409')
);
});
@ -202,7 +202,7 @@ describe('proxy', () => {
nock(domain).get('/jquery').reply(200, 'some-text');
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
})
).rejects.toThrowError(
@ -216,7 +216,7 @@ describe('proxy', () => {
nock(domain).get('/jquery').reply(409);
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
})
).rejects.toThrowError(
@ -228,7 +228,7 @@ describe('proxy', () => {
nock(domain).get('/jquery').reply(404);
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
})
).rejects.toThrowError(errorUtils.getNotFound(API_ERROR.NOT_PACKAGE_UPLINK));
@ -254,7 +254,7 @@ describe('proxy', () => {
.reply(200, { body: { name: 'foo', version: '1.0.0' } });
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
const [manifest] = await prox1.getRemoteMetadata('jquery', {
retry: { limit: 2 },
});
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
@ -274,13 +274,13 @@ describe('proxy', () => {
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
retry: { limit: 2 },
})
).rejects.toThrowError();
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
retry: { limit: 2 },
})
@ -311,14 +311,14 @@ describe('proxy', () => {
);
// force retry
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
retry: { limit: 2 },
})
).rejects.toThrowError();
// display offline error on exausted retry
await expect(
prox1.getRemoteMetadataNext('jquery', {
prox1.getRemoteMetadata('jquery', {
remoteAddress: '127.0.0.1',
retry: { limit: 2 },
})
@ -338,7 +338,7 @@ describe('proxy', () => {
);
// this is based on max_fails, if change that also change here acordingly
await setTimeout(3000);
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
const [manifest] = await prox1.getRemoteMetadata('jquery', {
retry: { limit: 2 },
});
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });

@ -39,13 +39,13 @@ describe('tarball proxy', () => {
const proxyPath = getConf('proxy1.yaml');
const conf = new Config(parseConfigFile(proxyPath));
describe('fetchTarballNext', () => {
describe('fetchTarball', () => {
test('get file tarball fetch', (done) => {
nock('https://registry.verdaccio.org')
.get('/jquery/-/jquery-0.0.1.tgz')
.replyWithFile(201, path.join(__dirname, 'partials/jquery-0.0.1.tgz'));
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
const stream = prox1.fetchTarballNext(
const stream = prox1.fetchTarball(
'https://registry.verdaccio.org/jquery/-/jquery-0.0.1.tgz',
{}
);
@ -66,7 +66,7 @@ describe('tarball proxy', () => {
.once()
.replyWithFile(201, path.join(__dirname, 'partials/jquery-0.0.1.tgz'));
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
const stream = prox1.fetchTarballNext(
const stream = prox1.fetchTarball(
'https://registry.verdaccio.org/jquery/-/jquery-0.0.1.tgz',
{ retry: { limit: 2 } }
);

@ -45,7 +45,6 @@
"cors": "2.8.5",
"debug": "4.3.4",
"express": "4.18.2",
"express-rate-limit": "5.5.1",
"lodash": "4.17.21"
},
"devDependencies": {

@ -2,7 +2,6 @@ import compression from 'compression';
import cors from 'cors';
import buildDebug from 'debug';
import express from 'express';
import RateLimit from 'express-rate-limit';
import { HttpError } from 'http-errors';
import _ from 'lodash';
import AuditMiddleware from 'verdaccio-audit';
@ -13,7 +12,7 @@ import { Config as AppConfig } from '@verdaccio/config';
import { API_ERROR, HTTP_STATUS, errorUtils, pluginUtils } from '@verdaccio/core';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { errorReportingMiddleware, final, log } from '@verdaccio/middleware';
import { errorReportingMiddleware, final, log, rateLimit, userAgent } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
import { ConfigYaml } from '@verdaccio/types';
import { Config as IConfig } from '@verdaccio/types';
@ -21,7 +20,6 @@ import webMiddleware from '@verdaccio/web';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
import hookDebug from './debug';
import { getUserAgent } from './utils';
const debug = buildDebug('verdaccio:server');
@ -29,23 +27,18 @@ const defineAPI = async function (config: IConfig, storage: Storage): Promise<an
const auth: Auth = new Auth(config);
await auth.init();
const app = express();
const limiter = new RateLimit(config.serverSettings.rateLimit);
// run in production mode by default, just in case
// it shouldn't make any difference anyway
app.set('env', process.env.NODE_ENV || 'production');
app.use(cors());
app.use(limiter);
app.use(rateLimit(config.serverSettings.rateLimit));
const errorReportingMiddlewareWrap = errorReportingMiddleware(logger);
// Router setup
app.use(log(logger));
app.use(errorReportingMiddlewareWrap);
app.use(function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
res.setHeader('x-powered-by', getUserAgent(config.user_agent));
next();
});
app.use(userAgent(config));
app.use(compression());
app.get(

@ -1,11 +0,0 @@
const pkgVersion = require('../package.json').version;
export function getUserAgent(userAgent: string): string {
if (typeof userAgent === 'string') {
return userAgent;
} else if (userAgent === false) {
return 'hidden';
}
return `verdaccio/${pkgVersion}`;
}

@ -55,15 +55,15 @@ test('should contains etag', async () => {
expect(typeof etag === 'string').toBeTruthy();
});
test('should contains powered by header', async () => {
test('should be hidden by default', async () => {
const app = await initializeServer('conf.yaml');
const response = await supertest(app)
.get('/')
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_HTML_UTF8)
.expect(HTTP_STATUS.OK);
const powered = response.get('x-powered-by');
expect(powered).toMatch('verdaccio/6');
});
expect(powered).toMatch('hidden');
}, 40000);
test('should not contains powered header', async () => {
const app = await initializeServer('powered-disabled.yaml');

@ -298,7 +298,7 @@ class Storage {
let expected_length;
const passThroughRemoteStream = new PassThrough();
const proxy = this.getUpLinkForDistFile(name, distFile);
const remoteStream = proxy.fetchTarballNext(distFile.url, {});
const remoteStream = proxy.fetchTarball(distFile.url, {});
remoteStream.on('request', async () => {
try {
@ -392,7 +392,7 @@ class Storage {
}
const proxy = this.getUpLinkForDistFile(name, distFile);
const remoteStream = proxy.fetchTarballNext(distFile.url, {});
const remoteStream = proxy.fetchTarball(distFile.url, {});
remoteStream.on('response', async () => {
try {
const storage = this.getPrivatePackageStorage(name);
@ -1732,7 +1732,7 @@ class Storage {
});
// get the latest metadata from the uplink
const [remoteManifest, etag] = await uplink.getRemoteMetadataNext(
const [remoteManifest, etag] = await uplink.getRemoteMetadata(
_cacheManifest.name,
remoteOptions
);

@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 79,
lines: 72,
},
},
});

@ -35,11 +35,9 @@
"@verdaccio/tarball": "workspace:11.0.0-6-next.28",
"@verdaccio/url": "workspace:11.0.0-6-next.25",
"@verdaccio/utils": "workspace:6.0.0-6-next.27",
"body-parser": "1.20.1",
"debug": "4.3.4",
"express": "4.18.2",
"lodash": "4.17.21",
"lru-cache": "7.14.1"
"lodash": "4.17.21"
},
"devDependencies": {
"@types/node": "16.18.10",

@ -1,6 +1,8 @@
import { Router } from 'express';
import { hasLogin } from '../utils/web-utils';
import { rateLimit } from '@verdaccio/middleware';
import { hasLogin } from '../web-utils';
import packageApi from './package';
import readme from './readme';
import search from './search';
@ -9,6 +11,14 @@ import user from './user';
export default (auth, storage, config) => {
const route = Router(); /* eslint new-cap: 0 */
route.use(
'/data/',
rateLimit({
windowMs: 2 * 60 * 1000, // 2 minutes
max: 5000, // limit each IP to 1000 requests per windowMs
...config?.web?.rateLimit,
})
);
route.use('/data/', packageApi(storage, auth, config));
route.use('/data/', search(storage, auth));
route.use('/data/', sidebar(config, storage, auth));

@ -10,7 +10,7 @@ import { getLocalRegistryTarballUri } from '@verdaccio/tarball';
import { Config, RemoteUser, Version } from '@verdaccio/types';
import { formatAuthor, generateGravatarUrl } from '@verdaccio/utils';
import { sortByName } from '../utils/web-utils';
import { sortByName } from '../web-utils';
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages

@ -8,7 +8,7 @@ import { $NextFunctionVer, $RequestExtend, $ResponseExtend, allow } from '@verda
import { Storage } from '@verdaccio/store';
import { Manifest } from '@verdaccio/types';
import { AuthorAvatar, addScope } from '../utils/web-utils';
import { AuthorAvatar, addScope } from '../web-utils';
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages

@ -10,7 +10,7 @@ import { convertDistRemoteToLocalTarballUrls } from '@verdaccio/tarball';
import { Config, Manifest, Version } from '@verdaccio/types';
import { addGravatarSupport, formatAuthor, isVersionValid } from '@verdaccio/utils';
import { AuthorAvatar, addScope, deleteProperties } from '../utils/web-utils';
import { AuthorAvatar, addScope, deleteProperties } from '../web-utils';
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages

@ -12,6 +12,7 @@ import {
errorUtils,
validatioUtils,
} from '@verdaccio/core';
import { rateLimit } from '@verdaccio/middleware';
import { Config, JWTSignOptions, RemoteUser } from '@verdaccio/types';
import { $NextFunctionVer } from './package';
@ -20,33 +21,38 @@ const debug = buildDebug('verdaccio:web:api:user');
function addUserAuthApi(auth: Auth, config: Config): Router {
const route = Router(); /* eslint new-cap: 0 */
route.post('/login', function (req: Request, res: Response, next: $NextFunctionVer): void {
const { username, password } = req.body;
debug('authenticate %o', username);
auth.authenticate(
username,
password,
async (err: VerdaccioError | null, user?: RemoteUser): Promise<void> => {
if (err) {
const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
debug('error authenticate %o', errorCode);
next(errorUtils.getCode(errorCode, err.message));
} else {
req.remote_user = user as RemoteUser;
const jWTSignOptions: JWTSignOptions = config.security.web.sign;
res.set(HEADERS.CACHE_CONTROL, 'no-cache, no-store');
next({
token: await auth.jwtEncrypt(user as RemoteUser, jWTSignOptions),
username: req.remote_user.name,
});
route.post(
'/login',
rateLimit(config?.userRateLimit),
function (req: Request, res: Response, next: $NextFunctionVer): void {
const { username, password } = req.body;
debug('authenticate %o', username);
auth.authenticate(
username,
password,
async (err: VerdaccioError | null, user?: RemoteUser): Promise<void> => {
if (err) {
const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
debug('error authenticate %o', errorCode);
next(errorUtils.getCode(errorCode, err.message));
} else {
req.remote_user = user as RemoteUser;
const jWTSignOptions: JWTSignOptions = config.security.web.sign;
res.set(HEADERS.CACHE_CONTROL, 'no-cache, no-store');
next({
token: await auth.jwtEncrypt(user as RemoteUser, jWTSignOptions),
username: req.remote_user.name,
});
}
}
}
);
});
);
}
);
if (config?.flags?.changePassword === true) {
route.put(
'/reset_password',
rateLimit(config?.userRateLimit),
function (req: Request, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name)) {
res.status(HTTP_STATUS.UNAUTHORIZED);

@ -1 +1 @@
export { default } from './web-middleware';
export { default } from './middleware';

@ -0,0 +1,46 @@
import express from 'express';
import _ from 'lodash';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { webMiddleware } from '@verdaccio/middleware';
import webEndpointsApi from './api';
export async function loadTheme(config: any) {
if (_.isNil(config.theme) === false) {
const plugin = await asyncLoadPlugin(
config.theme,
{ config, logger },
// TODO: add types { staticPath: string; manifest: unknown; manifestFiles: unknown }
function (plugin: any) {
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
},
config?.serverSettings?.pluginPrefix ?? 'verdaccio-theme'
);
if (plugin.length > 1) {
logger.warn('multiple ui themes are not supported , only the first plugin is used used');
}
return _.head(plugin);
}
}
export default async (config, auth, storage) => {
const pluginOptions = (await loadTheme(config)) || require('@verdaccio/ui-theme')();
// eslint-disable-next-line new-cap
const router = express.Router();
// load application
router.use(
webMiddleware(
config,
{
tokenMiddleware: auth.webUIJWTmiddleware(),
webEndpointsApi: webEndpointsApi(auth, storage, config),
},
pluginOptions
)
);
return router;
};

@ -1,25 +0,0 @@
import bodyParser from 'body-parser';
import { Router } from 'express';
import { Auth } from '@verdaccio/auth';
import { validateName, validatePackage } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
import { Config } from '@verdaccio/types';
import webEndpointsApi from '../api';
import { setSecurityWebHeaders } from './security';
export function webAPI(config: Config, auth: Auth, storage: Storage): Router {
// eslint-disable-next-line new-cap
const route = Router();
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble=
route.param('package', validatePackage);
route.param('filename', validateName);
route.param('version', validateName);
route.use(bodyParser.urlencoded({ extended: false }));
route.use(auth.webUIJWTmiddleware());
route.use(setSecurityWebHeaders);
route.use(webEndpointsApi(auth, storage, config));
return route;
}

@ -1,14 +0,0 @@
import express from 'express';
import { renderWebMiddleware } from './middleware/render-web';
import { webAPI } from './middleware/web-api';
export default async (config, auth, storage) => {
// eslint-disable-next-line new-cap
const app = express.Router();
// load application
app.use('/', await renderWebMiddleware(config, auth));
// web endpoints, search, packages, etc
app.use('/-/verdaccio/', webAPI(config, auth, storage));
return app;
};

@ -1,34 +1,9 @@
import buildDebug from 'debug';
import _ from 'lodash';
// import { normalizeContributors } from '@verdaccio/store';
import { Author, ConfigYaml } from '@verdaccio/types';
export type AuthorAvatar = Author & { avatar?: string };
const debug = buildDebug('verdaccio:web:utils');
export function validatePrimaryColor(primaryColor) {
const isHex = /^#([0-9A-F]{3}){1,2}$/i.test(primaryColor);
if (!isHex) {
debug('invalid primary color %o', primaryColor);
return;
}
return primaryColor;
}
export function deleteProperties(propertiesToDelete: string[], objectItem: any): any {
debug('deleted unused version properties');
_.forEach(propertiesToDelete, (property): any => {
delete objectItem[property];
});
return objectItem;
}
export function addScope(scope: string, packageName: string): string {
return `@${scope}/${packageName}`;
export function hasLogin(config: ConfigYaml) {
return _.isNil(config?.web?.login) || config?.web?.login === true;
}
export function sortByName(packages: any[], orderAscending: boolean | void = true): string[] {
@ -38,6 +13,16 @@ export function sortByName(packages: any[], orderAscending: boolean | void = tru
});
}
export function hasLogin(config: ConfigYaml) {
return _.isNil(config?.web?.login) || config?.web?.login === true;
export type AuthorAvatar = Author & { avatar?: string };
export function addScope(scope: string, packageName: string): string {
return `@${scope}/${packageName}`;
}
export function deleteProperties(propertiesToDelete: string[], objectItem: any): any {
_.forEach(propertiesToDelete, (property): any => {
delete objectItem[property];
});
return objectItem;
}

@ -9,7 +9,7 @@ import { initializeServer as initializeServerHelper } from '@verdaccio/test-help
import routes from '../src';
setup([]);
setup({});
export const getConf = (configName: string) => {
const configPath = path.join(__dirname, 'config', configName);

@ -1,4 +1,4 @@
import { sortByName } from '../src/utils/web-utils';
import { sortByName } from '../src/web-utils';
describe('Utilities', () => {
describe('Sort packages', () => {

553
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff