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:
parent
1b38fb2d30
commit
45c03819e2
15
.changeset/weak-mangos-taste.md
Normal file
15
.changeset/weak-mangos-taste.md
Normal file
@ -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,
|
||||
|
8
packages/middleware/src/middlewares/rate-limit.ts
Normal file
8
packages/middleware/src/middlewares/rate-limit.ts
Normal file
@ -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;
|
||||
}
|
10
packages/middleware/src/middlewares/user-agent.ts
Normal file
10
packages/middleware/src/middlewares/user-agent.ts
Normal file
@ -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');
|
||||
|
1
packages/middleware/src/middlewares/web/index.ts
Normal file
1
packages/middleware/src/middlewares/web/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as webMiddleware } from './web-middleware';
|
66
packages/web/src/middleware/render-web.ts → packages/middleware/src/middlewares/web/render-web.ts
66
packages/web/src/middleware/render-web.ts → packages/middleware/src/middlewares/web/render-web.ts
@ -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');
|
||||
|
18
packages/middleware/src/middlewares/web/utils/web-utils.ts
Normal file
18
packages/middleware/src/middlewares/web/utils/web-utils.ts
Normal file
@ -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;
|
||||
}
|
27
packages/middleware/src/middlewares/web/web-api.ts
Normal file
27
packages/middleware/src/middlewares/web/web-api.ts
Normal file
@ -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;
|
||||
}
|
15
packages/middleware/src/middlewares/web/web-middleware.ts
Normal file
15
packages/middleware/src/middlewares/web/web-middleware.ts
Normal file
@ -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;
|
||||
};
|
8
packages/middleware/test/_helper.ts
Normal file
8
packages/middleware/test/_helper.ts
Normal file
@ -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);
|
||||
};
|
28
packages/middleware/test/config/default-test.yaml
Normal file
28
packages/middleware/test/config/default-test.yaml
Normal file
@ -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
|
29
packages/middleware/test/config/login-disabled.yaml
Normal file
29
packages/middleware/test/config/login-disabled.yaml
Normal file
@ -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
|
23
packages/middleware/test/config/web.yaml
Normal file
23
packages/middleware/test/config/web.yaml
Normal file
@ -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');
|
||||
|
1
packages/middleware/test/partials/htmlParser.ts
Normal file
1
packages/middleware/test/partials/htmlParser.ts
Normal file
@ -0,0 +1 @@
|
||||
export const parseHtml = (html) => require('node-html-parser').parse(html);
|
64
packages/middleware/test/partials/manifest/manifest.json
Normal file
64
packages/middleware/test/partials/manifest/manifest.json
Normal file
@ -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',
|
3
packages/middleware/test/static/main.js
Normal file
3
packages/middleware/test/static/main.js
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
main: '';
|
||||
}
|
3
packages/middleware/test/static/vendor.js
Normal file
3
packages/middleware/test/static/vendor.js
Normal file
@ -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';
|
||||
|
46
packages/web/src/middleware.ts
Normal file
46
packages/web/src/middleware.ts
Normal file
@ -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
553
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user