mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-16 21:56:25 -05:00
* feat: add support for profile cli command #392 - it allows to update password npm profile set password - display current profile npm profile get https://docs.npmjs.com/cli/profile * chore: update @verdaccio/types@4.0.0 * feat: add min password length on npm by defaul is min 7 characters, this might be configurable in the future. * chore: update verdaccio-htpasswd@1.0.1 * refactor: update unit test * refactor: provide friendly error for tfa request * test: api profile unit test * chore: fix eslint comment * test: update profile test * chore: set mim as 3 characters
This commit is contained in:
parent
87092a5185
commit
f1416ed557
15 changed files with 396 additions and 62 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,7 +5,9 @@ build/
|
||||||
|
|
||||||
### Test
|
### Test
|
||||||
|
|
||||||
test/unit/partials/store/test-jwt-storage/*
|
test/unit/partials/store/test-*-storage/*
|
||||||
|
.verdaccio-db.json
|
||||||
|
.sinopia-db.json
|
||||||
|
|
||||||
###
|
###
|
||||||
!bin/verdaccio
|
!bin/verdaccio
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
"request": "2.88.0",
|
"request": "2.88.0",
|
||||||
"semver": "5.5.1",
|
"semver": "5.5.1",
|
||||||
"verdaccio-audit": "0.2.0",
|
"verdaccio-audit": "0.2.0",
|
||||||
"verdaccio-htpasswd": "0.2.2",
|
"verdaccio-htpasswd": "1.0.1",
|
||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
"@commitlint/config-conventional": "7.1.2",
|
"@commitlint/config-conventional": "7.1.2",
|
||||||
"@material-ui/core": "3.1.0",
|
"@material-ui/core": "3.1.0",
|
||||||
"@material-ui/icons": "3.0.1",
|
"@material-ui/icons": "3.0.1",
|
||||||
"@verdaccio/types": "3.7.2",
|
"@verdaccio/types": "4.0.0",
|
||||||
"babel-cli": "6.26.0",
|
"babel-cli": "6.26.0",
|
||||||
"babel-core": "6.26.3",
|
"babel-core": "6.26.3",
|
||||||
"babel-eslint": "10.0.0",
|
"babel-eslint": "10.0.0",
|
||||||
|
|
|
@ -7,8 +7,8 @@ import _ from 'lodash';
|
||||||
import Cookies from 'cookies';
|
import Cookies from 'cookies';
|
||||||
|
|
||||||
import { ErrorCode } from '../../../lib/utils';
|
import { ErrorCode } from '../../../lib/utils';
|
||||||
import { API_MESSAGE, HTTP_STATUS } from '../../../lib/constants';
|
import { API_ERROR, API_MESSAGE, HTTP_STATUS } from '../../../lib/constants';
|
||||||
import { createSessionToken, getApiToken, getAuthenticatedMessage } from '../../../lib/auth-utils';
|
import { createSessionToken, getApiToken, getAuthenticatedMessage, validatePassword } from '../../../lib/auth-utils';
|
||||||
|
|
||||||
import type { Config } from '@verdaccio/types';
|
import type { Config } from '@verdaccio/types';
|
||||||
import type { $Response, Router } from 'express';
|
import type { $Response, Router } from 'express';
|
||||||
|
@ -35,6 +35,11 @@ export default function(route: Router, auth: IAuth, config: Config) {
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (validatePassword(password) === false) {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT()));
|
||||||
|
}
|
||||||
|
|
||||||
auth.add_user(name, password, async function(err, user) {
|
auth.add_user(name, password, async function(err, user) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {
|
if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {
|
||||||
|
|
73
src/api/endpoint/api/v1/profile.js
Normal file
73
src/api/endpoint/api/v1/profile.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { API_ERROR, APP_ERROR, HTTP_STATUS, SUPPORT_ERRORS } from '../../../../lib/constants';
|
||||||
|
import { ErrorCode } from '../../../../lib/utils';
|
||||||
|
import { validatePassword } from '../../../../lib/auth-utils';
|
||||||
|
|
||||||
|
import type { $Response, Router } from 'express';
|
||||||
|
import type { $NextFunctionVer, $RequestExtend, IAuth } from '../../../../../types';
|
||||||
|
|
||||||
|
export default function(route: Router, auth: IAuth) {
|
||||||
|
const buildProfile = name => ({
|
||||||
|
tfa: false,
|
||||||
|
name,
|
||||||
|
email: '',
|
||||||
|
email_verified: false,
|
||||||
|
created: '',
|
||||||
|
updated: '',
|
||||||
|
cidr_whitelist: null,
|
||||||
|
fullname: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
route.get('/-/npm/v1/user', function(req: $RequestExtend, res: $Response, next: $NextFunctionVer) {
|
||||||
|
if (_.isNil(req.remote_user.name) === false) {
|
||||||
|
return next(buildProfile(req.remote_user.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
return next({
|
||||||
|
message: API_ERROR.MUST_BE_LOGGED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
route.post('/-/npm/v1/user', function(req: $RequestExtend, res: $Response, next: $NextFunctionVer) {
|
||||||
|
if (_.isNil(req.remote_user.name)) {
|
||||||
|
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
return next({
|
||||||
|
message: API_ERROR.MUST_BE_LOGGED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { password, tfa } = req.body;
|
||||||
|
const { name } = req.remote_user;
|
||||||
|
|
||||||
|
if (_.isNil(password) === false) {
|
||||||
|
if (validatePassword(password.new) === false) {
|
||||||
|
/* eslint new-cap:off */
|
||||||
|
return next(ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT()));
|
||||||
|
/* eslint new-cap:off */
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.changePassword(name, password.old, password.new, (err, isUpdated) => {
|
||||||
|
if (_.isNull(err) === false) {
|
||||||
|
return next(ErrorCode.getCode(err.status, err.message) || ErrorCode.getConflict(err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUpdated) {
|
||||||
|
return next(buildProfile(req.remote_user.name));
|
||||||
|
} else {
|
||||||
|
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (_.isNil(tfa) === false) {
|
||||||
|
return next(ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED));
|
||||||
|
} else {
|
||||||
|
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import distTags from './api/dist-tags';
|
||||||
import publish from './api/publish';
|
import publish from './api/publish';
|
||||||
import search from './api/search';
|
import search from './api/search';
|
||||||
import pkg from './api/package';
|
import pkg from './api/package';
|
||||||
|
import profile from './api/v1/profile';
|
||||||
|
|
||||||
const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware');
|
const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware');
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ export default function(config: Config, auth: IAuth, storage: IStorageHandler) {
|
||||||
// for "npm whoami"
|
// for "npm whoami"
|
||||||
whoami(app);
|
whoami(app);
|
||||||
pkg(app, auth, storage, config);
|
pkg(app, auth, storage, config);
|
||||||
|
profile(app, auth);
|
||||||
search(app, auth, storage);
|
search(app, auth, storage);
|
||||||
user(app, auth, config);
|
user(app, auth, config);
|
||||||
distTags(app, auth, storage);
|
distTags(app, auth, storage);
|
||||||
|
|
|
@ -5,12 +5,16 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { convertPayloadToBase64, ErrorCode } from './utils';
|
import { convertPayloadToBase64, ErrorCode } from './utils';
|
||||||
import { API_ERROR, HTTP_STATUS, ROLES, TIME_EXPIRATION_7D, TOKEN_BASIC, TOKEN_BEARER, CHARACTER_ENCODING } from './constants';
|
import { API_ERROR, HTTP_STATUS, ROLES, TIME_EXPIRATION_7D, TOKEN_BASIC, TOKEN_BEARER, CHARACTER_ENCODING, DEFAULT_MIN_LIMIT_PASSWORD } from './constants';
|
||||||
|
|
||||||
import type { RemoteUser, Package, Callback, Config, Security, APITokenOptions, JWTOptions } from '@verdaccio/types';
|
import type { RemoteUser, Package, Callback, Config, Security, APITokenOptions, JWTOptions } from '@verdaccio/types';
|
||||||
import type { CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload } from '../../types';
|
import type { CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload } from '../../types';
|
||||||
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
import { aesDecrypt, verifyPayload } from './crypto-utils';
|
||||||
|
|
||||||
|
export function validatePassword(password: string, minLength: number = DEFAULT_MIN_LIMIT_PASSWORD) {
|
||||||
|
return typeof password === 'string' && password.length >= minLength;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a RemoteUser object
|
* Create a RemoteUser object
|
||||||
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { API_ERROR, TOKEN_BASIC, TOKEN_BEARER } from './constants';
|
import { API_ERROR, SUPPORT_ERRORS, TOKEN_BASIC, TOKEN_BEARER } from './constants';
|
||||||
import loadPlugin from '../lib/plugin-loader';
|
import loadPlugin from '../lib/plugin-loader';
|
||||||
import { aesEncrypt, signPayload } from './crypto-utils';
|
import { aesEncrypt, signPayload } from './crypto-utils';
|
||||||
import {
|
import {
|
||||||
|
@ -60,6 +60,31 @@ class Auth implements IAuth {
|
||||||
this.plugins.push(getDefaultPlugins());
|
this.plugins.push(getDefaultPlugins());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changePassword(username: string, password: string, newPassword: string, cb: Callback) {
|
||||||
|
const validPlugins = _.filter(this.plugins, plugin => _.isFunction(plugin.changePassword));
|
||||||
|
|
||||||
|
if (_.isEmpty(validPlugins)) {
|
||||||
|
return cb(ErrorCode.getInternalError(SUPPORT_ERRORS.PLUGIN_MISSING_INTERFACE));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const plugin of validPlugins) {
|
||||||
|
this.logger.trace({ username }, 'updating password for @{username}');
|
||||||
|
plugin.changePassword(username, password, newPassword, (err, profile) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error(
|
||||||
|
{ username, err },
|
||||||
|
`An error has been produced
|
||||||
|
updating the password for @{username}. Error: @{err.message}`
|
||||||
|
);
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.trace({ username }, 'updated password for @{username} was successful');
|
||||||
|
return cb(null, profile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
authenticate(username: string, password: string, cb: Callback) {
|
authenticate(username: string, password: string, cb: Callback) {
|
||||||
const plugins = this.plugins.slice(0);
|
const plugins = this.plugins.slice(0);
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const DEFAULT_DOMAIN: string = 'localhost';
|
||||||
export const TIME_EXPIRATION_24H: string = '24h';
|
export const TIME_EXPIRATION_24H: string = '24h';
|
||||||
export const TIME_EXPIRATION_7D: string = '7d';
|
export const TIME_EXPIRATION_7D: string = '7d';
|
||||||
export const DIST_TAGS = 'dist-tags';
|
export const DIST_TAGS = 'dist-tags';
|
||||||
|
export const DEFAULT_MIN_LIMIT_PASSWORD: number = 3;
|
||||||
|
|
||||||
export const keyPem = 'verdaccio-key.pem';
|
export const keyPem = 'verdaccio-key.pem';
|
||||||
export const certPem = 'verdaccio-cert.pem';
|
export const certPem = 'verdaccio-cert.pem';
|
||||||
|
@ -87,7 +88,15 @@ export const API_MESSAGE = {
|
||||||
LOGGED_OUT: 'Logged out',
|
LOGGED_OUT: 'Logged out',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SUPPORT_ERRORS = {
|
||||||
|
PLUGIN_MISSING_INTERFACE: 'the plugin does not provide implementation of the requested feature',
|
||||||
|
TFA_DISABLED: 'the two-factor authentication is not yet supported',
|
||||||
|
};
|
||||||
|
|
||||||
export const API_ERROR = {
|
export const API_ERROR = {
|
||||||
|
PASSWORD_SHORT: (passLength: number = DEFAULT_MIN_LIMIT_PASSWORD) =>
|
||||||
|
`The provided password is too short. Please pick a password longer than ${passLength} characters.`,
|
||||||
|
MUST_BE_LOGGED: 'You must be logged in to publish packages.',
|
||||||
PLUGIN_ERROR: 'bug in the auth plugin system',
|
PLUGIN_ERROR: 'bug in the auth plugin system',
|
||||||
CONFIG_BAD_FORMAT: 'config file must be an object',
|
CONFIG_BAD_FORMAT: 'config file must be an object',
|
||||||
BAD_USERNAME_PASSWORD: 'bad username/password, access denied',
|
BAD_USERNAME_PASSWORD: 'bad username/password, access denied',
|
||||||
|
@ -116,6 +125,7 @@ export const API_ERROR = {
|
||||||
|
|
||||||
export const APP_ERROR = {
|
export const APP_ERROR = {
|
||||||
CONFIG_NOT_VALID: 'CONFIG: it does not look like a valid config file',
|
CONFIG_NOT_VALID: 'CONFIG: it does not look like a valid config file',
|
||||||
|
PROFILE_ERROR: 'profile unexpected error',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_NO_README = 'ERROR: No README data found!';
|
export const DEFAULT_NO_README = 'ERROR: No README data found!';
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
// this file is not aim to be tested, just to check flow definitions
|
// this file is not aim to be tested, just to check flow definitions
|
||||||
|
@ -5,92 +9,93 @@
|
||||||
import Config from '../../../../src/lib/config';
|
import Config from '../../../../src/lib/config';
|
||||||
import LoggerApi from '../../../../src/lib/logger';
|
import LoggerApi from '../../../../src/lib/logger';
|
||||||
|
|
||||||
import type {
|
import type { Config as AppConfig, PackageAccess, IPluginAuth, RemoteUser, Logger, PluginOptions } from '@verdaccio/types';
|
||||||
Config as AppConfig,
|
|
||||||
PackageAccess,
|
|
||||||
IPluginAuth,
|
|
||||||
RemoteUser,
|
|
||||||
Logger,
|
|
||||||
PluginOptions
|
|
||||||
} from '@verdaccio/types';
|
|
||||||
|
|
||||||
class ExampleAuthPlugin implements IPluginAuth {
|
class ExampleAuthPlugin implements IPluginAuth {
|
||||||
config: AppConfig;
|
config: AppConfig;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
|
||||||
constructor(config: AppConfig, options: PluginOptions) {
|
constructor(config: AppConfig, options: PluginOptions) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = options.logger;
|
this.logger = options.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
changePassword(username, password, newPassword, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
allow_access(user: RemoteUser, pkg: PackageAccess, cb: verdaccio$Callback): void {
|
allow_access(user: RemoteUser, pkg: PackageAccess, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
allow_publish(user: RemoteUser, pkg: PackageAccess, cb: verdaccio$Callback): void {
|
allow_publish(user: RemoteUser, pkg: PackageAccess, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubTypePackageAccess = PackageAccess & {
|
type SubTypePackageAccess = PackageAccess & {
|
||||||
sub?: boolean
|
sub?: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
class ExampleAuthCustomPlugin implements IPluginAuth {
|
class ExampleAuthCustomPlugin implements IPluginAuth {
|
||||||
config: AppConfig;
|
config: AppConfig;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
|
||||||
constructor(config: AppConfig, options: PluginOptions) {
|
constructor(config: AppConfig, options: PluginOptions) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = options.logger;
|
this.logger = options.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
changePassword(username, password, newPassword, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
allow_access(user: RemoteUser, pkg: SubTypePackageAccess, cb: verdaccio$Callback): void {
|
allow_access(user: RemoteUser, pkg: SubTypePackageAccess, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
allow_publish(user: RemoteUser, pkg: SubTypePackageAccess, cb: verdaccio$Callback): void {
|
allow_publish(user: RemoteUser, pkg: SubTypePackageAccess, cb: verdaccio$Callback): void {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const config1: AppConfig = new Config({
|
const config1: AppConfig = new Config({
|
||||||
storage: './storage',
|
storage: './storage',
|
||||||
self_path: '/home/sotrage'
|
self_path: '/home/sotrage',
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: PluginOptions = {
|
const options: PluginOptions = {
|
||||||
config: config1,
|
config: config1,
|
||||||
logger: LoggerApi.logger.child()
|
logger: LoggerApi.logger.child(),
|
||||||
}
|
};
|
||||||
|
|
||||||
const auth = new ExampleAuthPlugin(config1, options);
|
const auth = new ExampleAuthPlugin(config1, options);
|
||||||
const authSub = new ExampleAuthCustomPlugin(config1, options);
|
const authSub = new ExampleAuthCustomPlugin(config1, options);
|
||||||
const remoteUser: RemoteUser = {
|
const remoteUser: RemoteUser = {
|
||||||
groups: [],
|
groups: [],
|
||||||
real_groups: [],
|
real_groups: [],
|
||||||
name: 'test'
|
name: 'test',
|
||||||
};
|
};
|
||||||
|
|
||||||
auth.authenticate('user', 'pass', () => {});
|
auth.authenticate('user', 'pass', () => {});
|
||||||
auth.allow_access(remoteUser, {}, () => {});
|
auth.allow_access(remoteUser, {}, () => {});
|
||||||
auth.allow_publish(remoteUser, {}, () => {});
|
auth.allow_publish(remoteUser, {}, () => {});
|
||||||
authSub.authenticate('user', 'pass', () => {});
|
authSub.authenticate('user', 'pass', () => {});
|
||||||
authSub.allow_access(remoteUser, {sub: true}, () => {});
|
authSub.allow_access(remoteUser, { sub: true }, () => {});
|
||||||
authSub.allow_publish(remoteUser, {sub: true}, () => {});
|
authSub.allow_publish(remoteUser, { sub: true }, () => {});
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function getPackage(
|
||||||
export function addUser(request: any, user: string, credentials: any,
|
export function addUser(request: any, user: string, credentials: any,
|
||||||
statusCode: number = HTTP_STATUS.CREATED) {
|
statusCode: number = HTTP_STATUS.CREATED) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
request.put(`/-/user/org.couchdb.user:${user}`)
|
request.put(`/-/user/org.couchdb.user:${user}`)
|
||||||
.send(credentials)
|
.send(credentials)
|
||||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||||
|
@ -32,3 +32,43 @@ export function addUser(request: any, user: string, credentials: any,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getNewToken(request: any, credentials: any) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const [err, res] = await
|
||||||
|
addUser(request, credentials.name, credentials);
|
||||||
|
expect(err).toBeNull();
|
||||||
|
const {token, ok} = res.body;
|
||||||
|
expect(ok).toBeDefined();
|
||||||
|
expect(token).toBeDefined();
|
||||||
|
expect(typeof token).toBe('string');
|
||||||
|
resolve(token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProfile(request: any, token: string, statusCode: number = HTTP_STATUS.OK) {
|
||||||
|
// $FlowFixMe
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
request.get(`/-/npm/v1/user`)
|
||||||
|
.set('authorization', `Bearer ${token}`)
|
||||||
|
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||||
|
.expect(statusCode)
|
||||||
|
.end(function(err, res) {
|
||||||
|
return resolve([err, res]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postProfile(request: any, body: any, token: string, statusCode: number = HTTP_STATUS.OK) {
|
||||||
|
// $FlowFixMe
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
request.post(`/-/npm/v1/user`)
|
||||||
|
.send(body)
|
||||||
|
.set('authorization', `Bearer ${token}`)
|
||||||
|
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||||
|
.expect(statusCode)
|
||||||
|
.end(function(err, res) {
|
||||||
|
return resolve([err, res]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
130
test/unit/api/api.profile.spec.js
Normal file
130
test/unit/api/api.profile.spec.js
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import request from 'supertest';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import path from 'path';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
|
||||||
|
import Config from '../../../src/lib/config';
|
||||||
|
import endPointAPI from '../../../src/api/index';
|
||||||
|
import {mockServer} from './mock';
|
||||||
|
import {parseConfigFile} from '../../../src/lib/utils';
|
||||||
|
import {parseConfigurationFile} from '../__helper';
|
||||||
|
import {getNewToken, getProfile, postProfile} from './__api-helper';
|
||||||
|
import {setup} from '../../../src/lib/logger';
|
||||||
|
import {API_ERROR, HTTP_STATUS, SUPPORT_ERRORS} from '../../../src/lib/constants';
|
||||||
|
|
||||||
|
setup([]);
|
||||||
|
|
||||||
|
const parseConfigurationProfile = () => {
|
||||||
|
return parseConfigurationFile(`profile/profile`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
describe('endpoint user profile', () => {
|
||||||
|
let config;
|
||||||
|
let app;
|
||||||
|
let mockRegistry;
|
||||||
|
|
||||||
|
beforeAll(function(done) {
|
||||||
|
const store = path.join(__dirname, '../partials/store/test-profile-storage');
|
||||||
|
const mockServerPort = 55544;
|
||||||
|
rimraf(store, async () => {
|
||||||
|
const parsedConfig = parseConfigFile(parseConfigurationProfile());
|
||||||
|
const configForTest = _.clone(parsedConfig);
|
||||||
|
configForTest.storage = store;
|
||||||
|
configForTest.auth = {
|
||||||
|
htpasswd: {
|
||||||
|
file: './test-profile-storage/.htpasswd'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
configForTest.self_path = store;
|
||||||
|
config = new Config(configForTest);
|
||||||
|
app = await endPointAPI(config);
|
||||||
|
mockRegistry = await mockServer(mockServerPort).init();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(function(done) {
|
||||||
|
mockRegistry[0].stop();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fetch a profile of logged user', async (done) => {
|
||||||
|
const credentials = { name: 'JotaJWT', password: 'secretPass' };
|
||||||
|
const token = await getNewToken(request(app), credentials);
|
||||||
|
const [err1, res1] = await getProfile(request(app), token);
|
||||||
|
|
||||||
|
expect(err1).toBeNull();
|
||||||
|
expect(res1.body.name).toBe(credentials.name);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change password', () => {
|
||||||
|
test('should change password successfully', async (done) => {
|
||||||
|
const credentials = { name: 'userTest2000', password: 'secretPass000' };
|
||||||
|
const body = {
|
||||||
|
password: {
|
||||||
|
new: '12345678',
|
||||||
|
old: credentials.password,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const token = await getNewToken(request(app), credentials);
|
||||||
|
const [err1, res1] = await postProfile(request(app), body, token);
|
||||||
|
|
||||||
|
expect(err1).toBeNull();
|
||||||
|
expect(res1.body.name).toBe(credentials.name);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should change password is too short', async (done) => {
|
||||||
|
const credentials = { name: 'userTest2001', password: 'secretPass001' };
|
||||||
|
const body = {
|
||||||
|
password: {
|
||||||
|
new: 'p1',
|
||||||
|
old: credentials.password,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const token = await getNewToken(request(app), credentials);
|
||||||
|
const [, resp] = await postProfile(request(app), body, token, HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
|
||||||
|
expect(resp.error).not.toBeNull();
|
||||||
|
expect(resp.error.text).toMatch(API_ERROR.PASSWORD_SHORT());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change tfa', () => {
|
||||||
|
test('should report TFA is disabled', async (done) => {
|
||||||
|
const credentials = { name: 'userTest2002', password: 'secretPass002' };
|
||||||
|
const body = {
|
||||||
|
tfa: {}
|
||||||
|
};
|
||||||
|
const token = await getNewToken(request(app), credentials);
|
||||||
|
const [, resp] = await postProfile(request(app), body, token, HTTP_STATUS.SERVICE_UNAVAILABLE);
|
||||||
|
|
||||||
|
expect(resp.error).not.toBeNull();
|
||||||
|
expect(resp.error.text).toMatch(SUPPORT_ERRORS.TFA_DISABLED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
test('should forbid to fetch a profile with invalid token', async (done) => {
|
||||||
|
const [, resp] = await getProfile(request(app), `fakeToken`, HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
|
||||||
|
expect(resp.error).not.toBeNull();
|
||||||
|
expect(resp.error.text).toMatch(API_ERROR.MUST_BE_LOGGED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should forbid to update a profile with invalid token', async (done) => {
|
||||||
|
const [, resp] = await postProfile(request(app), {}, `fakeToken`, HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
|
||||||
|
expect(resp.error).not.toBeNull();
|
||||||
|
expect(resp.error.text).toMatch(API_ERROR.MUST_BE_LOGGED);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -186,7 +186,7 @@ describe('endpoint unit test', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(res.body.error).toBeDefined();
|
expect(res.body.error).toBeDefined();
|
||||||
expect(res.body.error).toMatch(/username and password is required/);
|
expect(res.body.error).toMatch('username and password is required');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -208,7 +208,7 @@ describe('endpoint unit test', () => {
|
||||||
|
|
||||||
expect(res.body.error).toBeDefined();
|
expect(res.body.error).toBeDefined();
|
||||||
//FIXME: message is not 100% accurate
|
//FIXME: message is not 100% accurate
|
||||||
expect(res.body.error).toMatch(/username and password is required/);
|
expect(res.body.error).toMatch(API_ERROR.PASSWORD_SHORT());
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
27
test/unit/partials/config/yaml/profile/profile.yaml
Normal file
27
test/unit/partials/config/yaml/profile/profile.yaml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
storage: ./storage
|
||||||
|
plugins: ./plugins
|
||||||
|
|
||||||
|
web:
|
||||||
|
title: Verdaccio
|
||||||
|
|
||||||
|
auth:
|
||||||
|
htpasswd:
|
||||||
|
file: ./htpasswd
|
||||||
|
uplinks:
|
||||||
|
npmjs:
|
||||||
|
url: https://registry.npmjs.org/
|
||||||
|
security:
|
||||||
|
api:
|
||||||
|
jwt:
|
||||||
|
sign:
|
||||||
|
expiresIn: 10m
|
||||||
|
notBefore: 0
|
||||||
|
packages:
|
||||||
|
'@*/*':
|
||||||
|
access: $authenticated
|
||||||
|
publish: $authenticated
|
||||||
|
'**':
|
||||||
|
access: $authenticated
|
||||||
|
publish: $authenticated
|
||||||
|
logs:
|
||||||
|
- { type: stdout, format: pretty, level: http }
|
|
@ -67,6 +67,17 @@ export type Utils = {
|
||||||
semverSort: (keys: Array<string>) => Array<string>;
|
semverSort: (keys: Array<string>) => Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Profile = {
|
||||||
|
tfa: boolean;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
email_verified: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
cidr_whitelist: any;
|
||||||
|
fullname: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type $RequestExtend = $Request & {remote_user?: any}
|
export type $RequestExtend = $Request & {remote_user?: any}
|
||||||
export type $ResponseExtend = $Response & {cookies?: any}
|
export type $ResponseExtend = $Response & {cookies?: any}
|
||||||
export type $NextFunctionVer = NextFunction & mixed;
|
export type $NextFunctionVer = NextFunction & mixed;
|
||||||
|
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in a new issue