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/unit/partials/store/test-jwt-storage/*
|
||||
test/unit/partials/store/test-*-storage/*
|
||||
.verdaccio-db.json
|
||||
.sinopia-db.json
|
||||
|
||||
###
|
||||
!bin/verdaccio
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"request": "2.88.0",
|
||||
"semver": "5.5.1",
|
||||
"verdaccio-audit": "0.2.0",
|
||||
"verdaccio-htpasswd": "0.2.2",
|
||||
"verdaccio-htpasswd": "1.0.1",
|
||||
"verror": "1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -56,7 +56,7 @@
|
|||
"@commitlint/config-conventional": "7.1.2",
|
||||
"@material-ui/core": "3.1.0",
|
||||
"@material-ui/icons": "3.0.1",
|
||||
"@verdaccio/types": "3.7.2",
|
||||
"@verdaccio/types": "4.0.0",
|
||||
"babel-cli": "6.26.0",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-eslint": "10.0.0",
|
||||
|
|
|
@ -7,8 +7,8 @@ import _ from 'lodash';
|
|||
import Cookies from 'cookies';
|
||||
|
||||
import { ErrorCode } from '../../../lib/utils';
|
||||
import { API_MESSAGE, HTTP_STATUS } from '../../../lib/constants';
|
||||
import { createSessionToken, getApiToken, getAuthenticatedMessage } from '../../../lib/auth-utils';
|
||||
import { API_ERROR, API_MESSAGE, HTTP_STATUS } from '../../../lib/constants';
|
||||
import { createSessionToken, getApiToken, getAuthenticatedMessage, validatePassword } from '../../../lib/auth-utils';
|
||||
|
||||
import type { Config } from '@verdaccio/types';
|
||||
import type { $Response, Router } from 'express';
|
||||
|
@ -35,6 +35,11 @@ export default function(route: Router, auth: IAuth, config: Config) {
|
|||
token,
|
||||
});
|
||||
} 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) {
|
||||
if (err) {
|
||||
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 search from './api/search';
|
||||
import pkg from './api/package';
|
||||
import profile from './api/v1/profile';
|
||||
|
||||
const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware');
|
||||
|
||||
|
@ -48,6 +49,7 @@ export default function(config: Config, auth: IAuth, storage: IStorageHandler) {
|
|||
// for "npm whoami"
|
||||
whoami(app);
|
||||
pkg(app, auth, storage, config);
|
||||
profile(app, auth);
|
||||
search(app, auth, storage);
|
||||
user(app, auth, config);
|
||||
distTags(app, auth, storage);
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
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 { CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload } from '../../types';
|
||||
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
|
||||
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
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 { aesEncrypt, signPayload } from './crypto-utils';
|
||||
import {
|
||||
|
@ -60,6 +60,31 @@ class Auth implements IAuth {
|
|||
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) {
|
||||
const plugins = this.plugins.slice(0);
|
||||
const self = this;
|
||||
|
|
|
@ -10,6 +10,7 @@ export const DEFAULT_DOMAIN: string = 'localhost';
|
|||
export const TIME_EXPIRATION_24H: string = '24h';
|
||||
export const TIME_EXPIRATION_7D: string = '7d';
|
||||
export const DIST_TAGS = 'dist-tags';
|
||||
export const DEFAULT_MIN_LIMIT_PASSWORD: number = 3;
|
||||
|
||||
export const keyPem = 'verdaccio-key.pem';
|
||||
export const certPem = 'verdaccio-cert.pem';
|
||||
|
@ -87,7 +88,15 @@ export const API_MESSAGE = {
|
|||
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 = {
|
||||
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',
|
||||
CONFIG_BAD_FORMAT: 'config file must be an object',
|
||||
BAD_USERNAME_PASSWORD: 'bad username/password, access denied',
|
||||
|
@ -116,6 +125,7 @@ export const API_ERROR = {
|
|||
|
||||
export const APP_ERROR = {
|
||||
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!';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
// this file is not aim to be tested, just to check flow definitions
|
||||
|
@ -5,92 +9,93 @@
|
|||
import Config from '../../../../src/lib/config';
|
||||
import LoggerApi from '../../../../src/lib/logger';
|
||||
|
||||
import type {
|
||||
Config as AppConfig,
|
||||
PackageAccess,
|
||||
IPluginAuth,
|
||||
RemoteUser,
|
||||
Logger,
|
||||
PluginOptions
|
||||
} from '@verdaccio/types';
|
||||
import type { Config as AppConfig, PackageAccess, IPluginAuth, RemoteUser, Logger, PluginOptions } from '@verdaccio/types';
|
||||
|
||||
class ExampleAuthPlugin implements IPluginAuth {
|
||||
config: AppConfig;
|
||||
logger: Logger;
|
||||
config: AppConfig;
|
||||
logger: Logger;
|
||||
|
||||
constructor(config: AppConfig, options: PluginOptions) {
|
||||
this.config = config;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
constructor(config: AppConfig, options: PluginOptions) {
|
||||
this.config = config;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
|
||||
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
changePassword(username, password, newPassword, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
|
||||
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
|
||||
allow_access(user: RemoteUser, pkg: PackageAccess, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
allow_publish(user: RemoteUser, pkg: PackageAccess, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
type SubTypePackageAccess = PackageAccess & {
|
||||
sub?: boolean
|
||||
}
|
||||
sub?: boolean,
|
||||
};
|
||||
|
||||
class ExampleAuthCustomPlugin implements IPluginAuth {
|
||||
config: AppConfig;
|
||||
logger: Logger;
|
||||
config: AppConfig;
|
||||
logger: Logger;
|
||||
|
||||
constructor(config: AppConfig, options: PluginOptions) {
|
||||
this.config = config;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
constructor(config: AppConfig, options: PluginOptions) {
|
||||
this.config = config;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
adduser(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
|
||||
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
changePassword(username, password, newPassword, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
|
||||
authenticate(user: string, password: string, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
|
||||
allow_access(user: RemoteUser, pkg: SubTypePackageAccess, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
allow_publish(user: RemoteUser, pkg: SubTypePackageAccess, cb: verdaccio$Callback): void {
|
||||
cb();
|
||||
}
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
const config1: AppConfig = new Config({
|
||||
storage: './storage',
|
||||
self_path: '/home/sotrage'
|
||||
storage: './storage',
|
||||
self_path: '/home/sotrage',
|
||||
});
|
||||
|
||||
const options: PluginOptions = {
|
||||
config: config1,
|
||||
logger: LoggerApi.logger.child()
|
||||
}
|
||||
config: config1,
|
||||
logger: LoggerApi.logger.child(),
|
||||
};
|
||||
|
||||
const auth = new ExampleAuthPlugin(config1, options);
|
||||
const authSub = new ExampleAuthCustomPlugin(config1, options);
|
||||
const remoteUser: RemoteUser = {
|
||||
groups: [],
|
||||
real_groups: [],
|
||||
name: 'test'
|
||||
groups: [],
|
||||
real_groups: [],
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
auth.authenticate('user', 'pass', () => {});
|
||||
auth.allow_access(remoteUser, {}, () => {});
|
||||
auth.allow_publish(remoteUser, {}, () => {});
|
||||
authSub.authenticate('user', 'pass', () => {});
|
||||
authSub.allow_access(remoteUser, {sub: true}, () => {});
|
||||
authSub.allow_publish(remoteUser, {sub: true}, () => {});
|
||||
authSub.allow_access(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,
|
||||
statusCode: number = HTTP_STATUS.CREATED) {
|
||||
// $FlowFixMe
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
request.put(`/-/user/org.couchdb.user:${user}`)
|
||||
.send(credentials)
|
||||
.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).toMatch(/username and password is required/);
|
||||
expect(res.body.error).toMatch('username and password is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -208,7 +208,7 @@ describe('endpoint unit test', () => {
|
|||
|
||||
expect(res.body.error).toBeDefined();
|
||||
//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();
|
||||
});
|
||||
});
|
||||
|
|
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>;
|
||||
}
|
||||
|
||||
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 $ResponseExtend = $Response & {cookies?: any}
|
||||
export type $NextFunctionVer = NextFunction & mixed;
|
||||
|
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in a new issue