mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
feat: Add username/password authentication (#777)
This commit is contained in:
parent
b7b19de442
commit
9bd425c193
@ -67,6 +67,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^3.1.0",
|
"async": "^3.1.0",
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
|
"bcrypt": "^5.0.1",
|
||||||
"compression": "^1.7.3",
|
"compression": "^1.7.3",
|
||||||
"connect-session-knex": "^2.0.0",
|
"connect-session-knex": "^2.0.0",
|
||||||
"cookie-parser": "^1.4.4",
|
"cookie-parser": "^1.4.4",
|
||||||
@ -92,13 +93,14 @@
|
|||||||
"mustache": "^4.1.0",
|
"mustache": "^4.1.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nodemailer": "^6.5.0",
|
"nodemailer": "^6.5.0",
|
||||||
|
"owasp-password-strength-test": "^1.3.0",
|
||||||
"parse-database-url": "^0.3.0",
|
"parse-database-url": "^0.3.0",
|
||||||
"pg": "^8.0.3",
|
"pg": "^8.0.3",
|
||||||
"pkginfo": "^0.4.1",
|
"pkginfo": "^0.4.1",
|
||||||
"prom-client": "^13.1.0",
|
"prom-client": "^13.1.0",
|
||||||
"response-time": "^2.3.2",
|
"response-time": "^2.3.2",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"unleash-frontend": "4.0.0-alpha.1",
|
"unleash-frontend": "4.0.0-alpha.2",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"yargs": "^16.0.3"
|
"yargs": "^16.0.3"
|
||||||
},
|
},
|
||||||
@ -111,6 +113,8 @@
|
|||||||
"@types/nodemailer": "^6.4.1",
|
"@types/nodemailer": "^6.4.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.15.2",
|
"@typescript-eslint/eslint-plugin": "^4.15.2",
|
||||||
"@typescript-eslint/parser": "^4.15.2",
|
"@typescript-eslint/parser": "^4.15.2",
|
||||||
|
"@types/bcrypt": "^3.0.0",
|
||||||
|
"@types/owasp-password-strength-test": "^1.3.0",
|
||||||
"ava": "^3.7.0",
|
"ava": "^3.7.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"coveralls": "^3.1.0",
|
"coveralls": "^3.1.0",
|
||||||
|
@ -15,6 +15,7 @@ const unleashDbSession = require('./middleware/session-db');
|
|||||||
|
|
||||||
const requestLogger = require('./middleware/request-logger');
|
const requestLogger = require('./middleware/request-logger');
|
||||||
const simpleAuthentication = require('./middleware/simple-authentication');
|
const simpleAuthentication = require('./middleware/simple-authentication');
|
||||||
|
const ossAuthentication = require('./middleware/oss-authentication');
|
||||||
const noAuthentication = require('./middleware/no-authentication');
|
const noAuthentication = require('./middleware/no-authentication');
|
||||||
const secureHeaders = require('./middleware/secure-headers');
|
const secureHeaders = require('./middleware/secure-headers');
|
||||||
|
|
||||||
@ -57,7 +58,12 @@ module.exports = function(config, services = {}) {
|
|||||||
// Deprecated. Will go away in v4.
|
// Deprecated. Will go away in v4.
|
||||||
if (config.adminAuthentication === AuthenticationType.unsecure) {
|
if (config.adminAuthentication === AuthenticationType.unsecure) {
|
||||||
app.use(baseUriPath, apiTokenMiddleware(config, services));
|
app.use(baseUriPath, apiTokenMiddleware(config, services));
|
||||||
simpleAuthentication(baseUriPath, app);
|
simpleAuthentication(app, config, services);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.adminAuthentication === AuthenticationType.openSource) {
|
||||||
|
app.use(baseUriPath, apiTokenMiddleware(config, services));
|
||||||
|
ossAuthentication(app, config, services);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.adminAuthentication === AuthenticationType.enterprise) {
|
if (config.adminAuthentication === AuthenticationType.enterprise) {
|
||||||
|
@ -22,6 +22,11 @@ export interface IRole {
|
|||||||
project?: string;
|
project?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserRole {
|
||||||
|
roleId: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class AccessStore {
|
export class AccessStore {
|
||||||
private logger: Function;
|
private logger: Function;
|
||||||
|
|
||||||
@ -82,6 +87,13 @@ export class AccessStore {
|
|||||||
.andWhere('type', 'project');
|
.andWhere('type', 'project');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRootRoles(): Promise<IRole[]> {
|
||||||
|
return this.db
|
||||||
|
.select(['id', 'name', 'type', 'project', 'description'])
|
||||||
|
.from<IRole>(T.ROLES)
|
||||||
|
.andWhere('type', 'root');
|
||||||
|
}
|
||||||
|
|
||||||
async removeRolesForProject(projectId: string): Promise<void> {
|
async removeRolesForProject(projectId: string): Promise<void> {
|
||||||
return this.db(T.ROLES)
|
return this.db(T.ROLES)
|
||||||
.where({
|
.where({
|
||||||
@ -122,6 +134,20 @@ export class AccessStore {
|
|||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeRolesOfTypeForUser(
|
||||||
|
userId: number,
|
||||||
|
roleType: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const rolesToRemove = this.db(T.ROLES)
|
||||||
|
.select('id')
|
||||||
|
.where({ type: roleType });
|
||||||
|
|
||||||
|
return this.db(T.ROLE_USER)
|
||||||
|
.where({ user_id: userId })
|
||||||
|
.whereIn('role_id', rolesToRemove)
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
|
|
||||||
async createRole(
|
async createRole(
|
||||||
name: string,
|
name: string,
|
||||||
type: string,
|
type: string,
|
||||||
@ -160,4 +186,18 @@ export class AccessStore {
|
|||||||
})
|
})
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
||||||
|
const rows = await this.db
|
||||||
|
.select('id', 'user_id')
|
||||||
|
.distinctOn('user_id')
|
||||||
|
.from(`${T.ROLES} AS r`)
|
||||||
|
.leftJoin(`${T.ROLE_USER} AS ru`, 'r.id', 'ru.role_id')
|
||||||
|
.where('r.type', '=', 'root');
|
||||||
|
|
||||||
|
return rows.map(row => ({
|
||||||
|
roleId: +row.id,
|
||||||
|
userId: +row.user_id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
/* eslint camelcase: "off" */
|
/* eslint camelcase: "off" */
|
||||||
|
|
||||||
'use strict';
|
import { Knex } from 'knex';
|
||||||
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
import User from '../user';
|
||||||
|
|
||||||
const NotFoundError = require('../error/notfound-error');
|
const NotFoundError = require('../error/notfound-error');
|
||||||
const User = require('../user');
|
|
||||||
|
|
||||||
const TABLE = 'users';
|
const TABLE = 'users';
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ const USER_COLUMNS = [
|
|||||||
'username',
|
'username',
|
||||||
'email',
|
'email',
|
||||||
'image_url',
|
'image_url',
|
||||||
'permissions',
|
'permissions', // TODO: remove in v4
|
||||||
'login_attempts',
|
'login_attempts',
|
||||||
'seen_at',
|
'seen_at',
|
||||||
'created_at',
|
'created_at',
|
||||||
@ -53,27 +54,57 @@ const rowToUser = row => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IUserLookup {
|
||||||
|
id?: number;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserSearch {
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserUpdateFields {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
class UserStore {
|
class UserStore {
|
||||||
constructor(db, getLogger) {
|
private db: Knex;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(db: Knex, getLogger: LogProvider) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('user-store.js');
|
this.logger = getLogger('user-store.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id, user) {
|
async update(id: number, fields: IUserUpdateFields): Promise<User> {
|
||||||
await this.db(TABLE)
|
await this.db(TABLE)
|
||||||
.where('id', id)
|
.where('id', id)
|
||||||
.update(mapUserToColumns(user));
|
.update(mapUserToColumns(fields));
|
||||||
return this.get({ id });
|
return this.get({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async insert(user) {
|
async insert(user: User): Promise<User> {
|
||||||
const [id] = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.insert(mapUserToColumns(user))
|
.insert(mapUserToColumns(user))
|
||||||
.returning('id');
|
.returning(USER_COLUMNS);
|
||||||
return this.get({ id });
|
return rowToUser(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSelectUser(q) {
|
async upsert(user: User): Promise<User> {
|
||||||
|
const id = await this.hasUser(user);
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
return this.update(id, user);
|
||||||
|
}
|
||||||
|
return this.insert(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSelectUser(q: IUserLookup): any {
|
||||||
const query = this.db(TABLE);
|
const query = this.db(TABLE);
|
||||||
if (q.id) {
|
if (q.id) {
|
||||||
return query.where('id', q.id);
|
return query.where('id', q.id);
|
||||||
@ -87,27 +118,18 @@ class UserStore {
|
|||||||
throw new Error('Can only find users with id, username or email.');
|
throw new Error('Can only find users with id, username or email.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasUser(idQuery) {
|
async hasUser(idQuery: IUserLookup): Promise<number | undefined> {
|
||||||
const query = this.buildSelectUser(idQuery);
|
const query = this.buildSelectUser(idQuery);
|
||||||
const item = await query.first('id');
|
const item = await query.first('id');
|
||||||
return item ? item.id : undefined;
|
return item ? item.id : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsert(user) {
|
async getAll(): Promise<User[]> {
|
||||||
const id = await this.hasUser(user);
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
return this.update(id, user);
|
|
||||||
}
|
|
||||||
return this.insert(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll() {
|
|
||||||
const users = await this.db.select(USER_COLUMNS).from(TABLE);
|
const users = await this.db.select(USER_COLUMNS).from(TABLE);
|
||||||
return users.map(rowToUser);
|
return users.map(rowToUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query: IUserSearch): Promise<User[]> {
|
||||||
const users = await this.db
|
const users = await this.db
|
||||||
.select(USER_COLUMNS_PUBLIC)
|
.select(USER_COLUMNS_PUBLIC)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -117,7 +139,7 @@ class UserStore {
|
|||||||
return users.map(rowToUser);
|
return users.map(rowToUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllWithId(userIdList) {
|
async getAllWithId(userIdList: number[]): Promise<User[]> {
|
||||||
const users = await this.db
|
const users = await this.db
|
||||||
.select(USER_COLUMNS_PUBLIC)
|
.select(USER_COLUMNS_PUBLIC)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -125,18 +147,18 @@ class UserStore {
|
|||||||
return users.map(rowToUser);
|
return users.map(rowToUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(idQuery) {
|
async get(idQuery: IUserLookup): Promise<User> {
|
||||||
const row = await this.buildSelectUser(idQuery).first(USER_COLUMNS);
|
const row = await this.buildSelectUser(idQuery).first(USER_COLUMNS);
|
||||||
return rowToUser(row);
|
return rowToUser(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id: number): Promise<void> {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where({ id })
|
.where({ id })
|
||||||
.del();
|
.del();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPasswordHash(userId) {
|
async getPasswordHash(userId: number): Promise<string> {
|
||||||
const item = await this.db(TABLE)
|
const item = await this.db(TABLE)
|
||||||
.where('id', userId)
|
.where('id', userId)
|
||||||
.first('password_hash');
|
.first('password_hash');
|
||||||
@ -148,7 +170,7 @@ class UserStore {
|
|||||||
return item.password_hash;
|
return item.password_hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPasswordHash(userId, passwordHash) {
|
async setPasswordHash(userId: number, passwordHash: string): Promise<void> {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where('id', userId)
|
.where('id', userId)
|
||||||
.update({
|
.update({
|
||||||
@ -156,13 +178,11 @@ class UserStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async incLoginAttempts(user) {
|
async incLoginAttempts(user: User): Promise<void> {
|
||||||
return this.buildSelectUser(user).increment({
|
return this.buildSelectUser(user).increment('login_attempts', 1);
|
||||||
login_attempts: 1,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async succesfullLogin(user) {
|
async successfullyLogin(user: User): Promise<void> {
|
||||||
return this.buildSelectUser(user).update({
|
return this.buildSelectUser(user).update({
|
||||||
login_attempts: 0,
|
login_attempts: 0,
|
||||||
seen_at: new Date(),
|
seen_at: new Date(),
|
||||||
@ -171,3 +191,4 @@ class UserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserStore;
|
module.exports = UserStore;
|
||||||
|
export default UserStore;
|
@ -96,8 +96,10 @@ test('should not add user if disabled', async t => {
|
|||||||
|
|
||||||
const disabledConfig = {
|
const disabledConfig = {
|
||||||
getLogger,
|
getLogger,
|
||||||
|
baseUriPath: '',
|
||||||
authentication: {
|
authentication: {
|
||||||
enableApiToken: false,
|
enableApiToken: false,
|
||||||
|
createAdminUser: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
32
src/lib/middleware/oss-authentication.js
Normal file
32
src/lib/middleware/oss-authentication.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const AuthenticationRequired = require('../authentication-required');
|
||||||
|
|
||||||
|
function ossAuthHook(app, config) {
|
||||||
|
const { baseUriPath } = config;
|
||||||
|
|
||||||
|
const generateAuthResponse = async () => {
|
||||||
|
return new AuthenticationRequired({
|
||||||
|
type: 'password',
|
||||||
|
path: `${baseUriPath}/auth/simple/login`,
|
||||||
|
message: 'You must sign in order to use Unleash',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(`${baseUriPath}/api`, async (req, res, next) => {
|
||||||
|
if (req.session && req.session.user) {
|
||||||
|
req.user = req.session.user;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
if (req.user) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
if (req.header('authorization')) {
|
||||||
|
// API clients should get 401 without body
|
||||||
|
return res.sendStatus(401);
|
||||||
|
}
|
||||||
|
// Admin UI users should get auth-response
|
||||||
|
const authRequired = await generateAuthResponse();
|
||||||
|
return res.status(401).json(authRequired);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ossAuthHook;
|
58
src/lib/middleware/oss-authentication.test.js
Normal file
58
src/lib/middleware/oss-authentication.test.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('ava');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const store = require('../../test/fixtures/store');
|
||||||
|
const ossAuth = require('./oss-authentication');
|
||||||
|
const getApp = require('../app');
|
||||||
|
const getLogger = require('../../test/fixtures/no-logger');
|
||||||
|
const { User } = require('../server-impl');
|
||||||
|
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
|
function getSetup(preRouterHook) {
|
||||||
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
|
const stores = store.createStores();
|
||||||
|
const app = getApp({
|
||||||
|
baseUriPath: base,
|
||||||
|
stores,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
preRouterHook(_app) {
|
||||||
|
preRouterHook(_app);
|
||||||
|
ossAuth(_app, { baseUriPath: base });
|
||||||
|
|
||||||
|
_app.get(`${base}/api/protectedResource`, (req, res) => {
|
||||||
|
res.status(200)
|
||||||
|
.json({ message: 'OK' })
|
||||||
|
.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
base,
|
||||||
|
request: supertest(app),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('should return 401 when missing user', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const { base, request } = getSetup(() => {});
|
||||||
|
|
||||||
|
return request.get(`${base}/api/protectedResource`).expect(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return 200 when user exists', t => {
|
||||||
|
t.plan(0);
|
||||||
|
const user = new User({ id: 1, email: 'some@mail.com' });
|
||||||
|
const { base, request } = getSetup(app =>
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return request.get(`${base}/api/protectedResource`).expect(200);
|
||||||
|
});
|
@ -2,10 +2,11 @@ const auth = require('basic-auth');
|
|||||||
const User = require('../user');
|
const User = require('../user');
|
||||||
const AuthenticationRequired = require('../authentication-required');
|
const AuthenticationRequired = require('../authentication-required');
|
||||||
|
|
||||||
function insecureAuthentication(basePath = '', app) {
|
function insecureAuthentication(app, { basePath = '' }, { userService }) {
|
||||||
app.post(`${basePath}/api/admin/login`, (req, res) => {
|
app.post(`${basePath}/api/admin/login`, async (req, res) => {
|
||||||
const user = req.body;
|
const { email } = req.body;
|
||||||
req.session.user = new User({ email: user.email });
|
const user = await userService.loginUserWithoutPassword(email, true);
|
||||||
|
req.session.user = user;
|
||||||
res.status(200)
|
res.status(200)
|
||||||
.json(req.session.user)
|
.json(req.session.user)
|
||||||
.end();
|
.end();
|
||||||
|
@ -81,6 +81,7 @@ function defaultOptions() {
|
|||||||
enableApiToken: process.env.AUTH_ENABLE_API_TOKEN || true,
|
enableApiToken: process.env.AUTH_ENABLE_API_TOKEN || true,
|
||||||
type: process.env.AUTH_TYPE || 'open-source',
|
type: process.env.AUTH_TYPE || 'open-source',
|
||||||
customHook: () => {},
|
customHook: () => {},
|
||||||
|
createAdminUser: true,
|
||||||
},
|
},
|
||||||
ui: {},
|
ui: {},
|
||||||
importFile: process.env.IMPORT_FILE,
|
importFile: process.env.IMPORT_FILE,
|
||||||
|
@ -14,16 +14,7 @@ import { AccessService } from '../../services/access-service';
|
|||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
import { isRbacEnabled } from '../../util/feature-enabled';
|
import { isRbacEnabled } from '../../util/feature-enabled';
|
||||||
import User from '../../user';
|
import User from '../../user';
|
||||||
|
import { IUnleashConfig } from '../../types/core';
|
||||||
interface IExperimentalFlags {
|
|
||||||
[key: string]: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IConfig {
|
|
||||||
getLogger: LogProvider;
|
|
||||||
extendedPermissions: boolean;
|
|
||||||
experimental: IExperimentalFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IServices {
|
interface IServices {
|
||||||
apiTokenService: ApiTokenService;
|
apiTokenService: ApiTokenService;
|
||||||
@ -41,7 +32,7 @@ class ApiTokenController extends Controller {
|
|||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(config: IConfig, services: IServices) {
|
constructor(config: IUnleashConfig, services: IServices) {
|
||||||
super(config);
|
super(config);
|
||||||
this.apiTokenService = services.apiTokenService;
|
this.apiTokenService = services.apiTokenService;
|
||||||
this.accessService = services.accessService;
|
this.accessService = services.accessService;
|
||||||
|
@ -16,6 +16,7 @@ const TagTypeController = require('./tag-type');
|
|||||||
const AddonController = require('./addon');
|
const AddonController = require('./addon');
|
||||||
const ApiTokenController = require('./api-token-controller');
|
const ApiTokenController = require('./api-token-controller');
|
||||||
const EmailController = require('./email');
|
const EmailController = require('./email');
|
||||||
|
const UserAdminController = require('./user-admin');
|
||||||
const apiDef = require('./api-def.json');
|
const apiDef = require('./api-def.json');
|
||||||
|
|
||||||
class AdminApi extends Controller {
|
class AdminApi extends Controller {
|
||||||
@ -65,6 +66,10 @@ class AdminApi extends Controller {
|
|||||||
new ApiTokenController(config, services).router,
|
new ApiTokenController(config, services).router,
|
||||||
);
|
);
|
||||||
this.app.use('/email', new EmailController(config, services).router);
|
this.app.use('/email', new EmailController(config, services).router);
|
||||||
|
this.app.use(
|
||||||
|
'/user-admin',
|
||||||
|
new UserAdminController(config, services).router,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
index(req, res) {
|
index(req, res) {
|
||||||
|
125
src/lib/routes/admin-api/user-admin.ts
Normal file
125
src/lib/routes/admin-api/user-admin.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import Controller from '../controller';
|
||||||
|
import { ADMIN } from '../../permissions';
|
||||||
|
import { IUnleashConfig } from '../../types/core';
|
||||||
|
import UserService from '../../services/user-service';
|
||||||
|
import { AccessService, RoleName } from '../../services/access-service';
|
||||||
|
import { Logger } from '../../logger';
|
||||||
|
|
||||||
|
class UserAdminController extends Controller {
|
||||||
|
private userService: UserService;
|
||||||
|
|
||||||
|
private accessService: AccessService;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(config: IUnleashConfig, { userService, accessService }) {
|
||||||
|
super(config);
|
||||||
|
this.userService = userService;
|
||||||
|
this.accessService = accessService;
|
||||||
|
this.logger = config.getLogger('routes/user-controller.js');
|
||||||
|
|
||||||
|
this.get('/', this.getUsers);
|
||||||
|
this.get('/search', this.search);
|
||||||
|
this.post('/', this.createUser, ADMIN);
|
||||||
|
this.post('/validate-password', this.validatePassword);
|
||||||
|
this.put('/:id', this.updateUser, ADMIN);
|
||||||
|
this.post('/:id/change-password', this.changePassword, ADMIN);
|
||||||
|
this.delete('/:id', this.deleteUser, ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsers(req, res) {
|
||||||
|
try {
|
||||||
|
const users = await this.userService.getAll();
|
||||||
|
const rootRoles = await this.accessService.getRootRoles();
|
||||||
|
|
||||||
|
res.json({ users, rootRoles });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
res.status(500).send({ msg: 'server errors' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(req, res) {
|
||||||
|
const { q } = req.query;
|
||||||
|
try {
|
||||||
|
const users =
|
||||||
|
q && q.length > 1 ? await this.userService.search(q) : [];
|
||||||
|
res.json(users);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
res.status(500).send({ msg: 'server errors' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(req, res) {
|
||||||
|
const { username, email, name, rootRole } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await this.userService.createUser({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
rootRole: Number(rootRole),
|
||||||
|
});
|
||||||
|
res.status(201).send({ ...user, rootRole });
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(e.message);
|
||||||
|
res.status(400).send([{ msg: e.message }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, email, rootRole } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await this.userService.updateUser({
|
||||||
|
id: Number(id),
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
rootRole: Number(rootRole),
|
||||||
|
});
|
||||||
|
res.status(200).send({ ...user, rootRole });
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(e.message);
|
||||||
|
res.status(400).send([{ msg: e.message }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.userService.deleteUser(+id);
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(error);
|
||||||
|
res.status(500).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validatePassword(req, res) {
|
||||||
|
const { password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.userService.validatePassword(password);
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (e) {
|
||||||
|
res.status(400).send([{ msg: e.message }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { password } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.userService.changePassword(+id, password);
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (e) {
|
||||||
|
res.status(400).send([{ msg: e.message }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserAdminController;
|
@ -5,6 +5,7 @@ const Controller = require('../controller');
|
|||||||
class UserController extends Controller {
|
class UserController extends Controller {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.get('/', this.getUser);
|
this.get('/', this.getUser);
|
||||||
this.get('/logout', this.logout);
|
this.get('/logout', this.logout);
|
||||||
}
|
}
|
||||||
@ -25,7 +26,7 @@ class UserController extends Controller {
|
|||||||
return res.status(404).end();
|
return res.status(404).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Depcreated, use "/logout" instead. Will be removed in later release.
|
// Deprecated, use "/logout" instead. Will be removed in v4.
|
||||||
logout(req, res) {
|
logout(req, res) {
|
||||||
if (req.session) {
|
if (req.session) {
|
||||||
req.session = null;
|
req.session = null;
|
||||||
|
31
src/lib/routes/auth/simple-password-provider.js
Normal file
31
src/lib/routes/auth/simple-password-provider.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const Controller = require('../controller');
|
||||||
|
|
||||||
|
class PasswordProvider extends Controller {
|
||||||
|
constructor({ getLogger }, { userService }) {
|
||||||
|
super();
|
||||||
|
this.logger = getLogger('/auth/password-provider.js');
|
||||||
|
this.userService = userService;
|
||||||
|
|
||||||
|
this.post('/login', this.login);
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(req, res) {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: 'You must provide username and password',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await this.userService.loginUser(username, password);
|
||||||
|
req.session.user = user;
|
||||||
|
return res.status(200).json(user);
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(401).json({ message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PasswordProvider;
|
83
src/lib/routes/auth/simple-password-provider.test.js
Normal file
83
src/lib/routes/auth/simple-password-provider.test.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
const test = require('ava');
|
||||||
|
const request = require('supertest');
|
||||||
|
const express = require('express');
|
||||||
|
const User = require('../../user');
|
||||||
|
const PasswordProvider = require('./simple-password-provider');
|
||||||
|
|
||||||
|
const getLogger = () => ({ info: () => {}, error: () => {} });
|
||||||
|
|
||||||
|
test('Should require password', async t => {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
const userService = () => {};
|
||||||
|
const ctr = new PasswordProvider({ getLogger }, { userService });
|
||||||
|
|
||||||
|
app.use('/auth/simple', ctr.router);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/auth/simple/login')
|
||||||
|
.send({ name: 'john' });
|
||||||
|
|
||||||
|
t.is(400, res.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should login user', async t => {
|
||||||
|
const username = 'ola';
|
||||||
|
const password = 'simplepass';
|
||||||
|
const user = new User({ username, permissions: ['ADMIN'] });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.session = {};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
const userService = {
|
||||||
|
loginUser: (u, p) => {
|
||||||
|
if (u === username && p === password) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
throw new Error('Wrong password');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ctr = new PasswordProvider({ getLogger }, { userService });
|
||||||
|
|
||||||
|
app.use('/auth/simple', ctr.router);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/auth/simple/login')
|
||||||
|
.send({ username, password });
|
||||||
|
|
||||||
|
t.is(200, res.status);
|
||||||
|
t.is(user.username, res.body.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not login user with wrong password', async t => {
|
||||||
|
const username = 'ola';
|
||||||
|
const password = 'simplepass';
|
||||||
|
const user = new User({ username, permissions: ['ADMIN'] });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
req.session = {};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
const userService = {
|
||||||
|
loginUser: (u, p) => {
|
||||||
|
if (u === username && p === password) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
throw new Error('Wrong password');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ctr = new PasswordProvider({ getLogger }, { userService });
|
||||||
|
|
||||||
|
app.use('/auth/simple', ctr.router);
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/auth/simple/login')
|
||||||
|
.send({ username, password: 'not-correct' });
|
||||||
|
|
||||||
|
t.is(401, res.status);
|
||||||
|
});
|
@ -8,6 +8,7 @@ const Controller = require('./controller');
|
|||||||
const HealthCheckController = require('./health-check');
|
const HealthCheckController = require('./health-check');
|
||||||
const LogoutController = require('./logout');
|
const LogoutController = require('./logout');
|
||||||
const api = require('./api-def');
|
const api = require('./api-def');
|
||||||
|
const SimplePasswordProvider = require('./auth/simple-password-provider');
|
||||||
|
|
||||||
class IndexRouter extends Controller {
|
class IndexRouter extends Controller {
|
||||||
constructor(config, services) {
|
constructor(config, services) {
|
||||||
@ -15,6 +16,10 @@ class IndexRouter extends Controller {
|
|||||||
this.use('/health', new HealthCheckController(config).router);
|
this.use('/health', new HealthCheckController(config).router);
|
||||||
this.use('/internal-backstage', new BackstageController(config).router);
|
this.use('/internal-backstage', new BackstageController(config).router);
|
||||||
this.use('/logout', new LogoutController(config).router);
|
this.use('/logout', new LogoutController(config).router);
|
||||||
|
this.use(
|
||||||
|
'/auth/simple',
|
||||||
|
new SimplePasswordProvider(config, services).router,
|
||||||
|
);
|
||||||
this.get(api.uri, this.index);
|
this.get(api.uri, this.index);
|
||||||
this.use(api.links.admin.uri, new AdminApi(config, services).router);
|
this.use(api.links.admin.uri, new AdminApi(config, services).router);
|
||||||
this.use(api.links.client.uri, new ClientApi(config, services).router);
|
this.use(api.links.client.uri, new ClientApi(config, services).router);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AccessStore, IRole, IUserPermission } from '../db/access-store';
|
import { AccessStore, IRole, IUserPermission, IUserRole } from '../db/access-store';
|
||||||
import p from '../permissions';
|
import p from '../permissions';
|
||||||
import User from '../user';
|
import User from '../user';
|
||||||
|
|
||||||
@ -37,13 +37,13 @@ export interface IUserWithRole {
|
|||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRoleData {
|
export interface IRoleData {
|
||||||
role: IRole;
|
role: IRole;
|
||||||
users: User[];
|
users: User[];
|
||||||
permissions: IUserPermission[];
|
permissions: IUserPermission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPermission {
|
export interface IPermission {
|
||||||
name: string;
|
name: string;
|
||||||
type: PermissionType;
|
type: PermissionType;
|
||||||
}
|
}
|
||||||
@ -64,6 +64,11 @@ export enum RoleType {
|
|||||||
PROJECT = 'project',
|
PROJECT = 'project',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRoleIdentifier {
|
||||||
|
roleId?: number;
|
||||||
|
roleName?: RoleName;
|
||||||
|
}
|
||||||
|
|
||||||
export class AccessService {
|
export class AccessService {
|
||||||
public RoleName = RoleName;
|
public RoleName = RoleName;
|
||||||
private store: AccessStore;
|
private store: AccessStore;
|
||||||
@ -101,6 +106,10 @@ export class AccessService {
|
|||||||
.some(p => p.permission === permission || p.permission === ADMIN);
|
.some(p => p.permission === permission || p.permission === ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPermissionsForUser(user: User) {
|
||||||
|
return this.store.getPermissionsForUser(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
getPermissions(): IPermission[] {
|
getPermissions(): IPermission[] {
|
||||||
return this.permissions;
|
return this.permissions;
|
||||||
}
|
}
|
||||||
@ -109,20 +118,25 @@ export class AccessService {
|
|||||||
return this.store.addUserToRole(userId, roleId);
|
return this.store.addUserToRole(userId, roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserRootRole(userId: number, roleName: RoleName ) {
|
async setUserRootRole(userId: number, roleId: number) {
|
||||||
const userRoles = await this.store.getRolesForUserId(userId);
|
const roles = await this.getRootRoles();
|
||||||
const currentRootRoles = userRoles.filter(r => r.type === RoleType.ROOT);
|
const newRootRole = roles.find(r => r.id === roleId);
|
||||||
|
|
||||||
const roles = await this.getRoles();
|
if(newRootRole) {
|
||||||
const role = roles.find(r => r.type === RoleType.ROOT && r.name === roleName);
|
|
||||||
if(role) {
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(currentRootRoles.map(r => this.store.removeUserFromRole(userId, r.id)));
|
await this.store.removeRolesOfTypeForUser(userId, RoleType.ROOT);
|
||||||
await this.store.addUserToRole(userId, role.id);
|
await this.store.addUserToRole(userId, newRootRole.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn('Could not add role=${roleName} to userId=${userId}');
|
throw new Error('Could not add role=${roleName} to userId=${userId}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Could not find rootRole with id=${roleId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserRootRoles(userId: number) {
|
||||||
|
const userRoles = await this.store.getRolesForUserId(userId);
|
||||||
|
return userRoles.filter(r => r.type === RoleType.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeUserFromRole(userId: number, roleId: number) {
|
async removeUserFromRole(userId: number, roleId: number) {
|
||||||
@ -220,4 +234,17 @@ export class AccessService {
|
|||||||
this.logger.info(`Removing project roles for ${projectId}`);
|
this.logger.info(`Removing project roles for ${projectId}`);
|
||||||
return this.store.removeRolesForProject(projectId);
|
return this.store.removeRolesForProject(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
||||||
|
return this.store.getRootRoleForAllUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRootRoles(): Promise<IRole[]> {
|
||||||
|
return this.store.getRootRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRootRole(roleName: RoleName): Promise<IRole> {
|
||||||
|
const roles = await this.store.getRootRoles();
|
||||||
|
return roles.find(r => r.name === roleName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ const VersionService = require('./version-service');
|
|||||||
const { EmailService } = require('./email-service');
|
const { EmailService } = require('./email-service');
|
||||||
const { AccessService } = require('./access-service');
|
const { AccessService } = require('./access-service');
|
||||||
const { ApiTokenService } = require('./api-token-service');
|
const { ApiTokenService } = require('./api-token-service');
|
||||||
|
const UserService = require('./user-service');
|
||||||
|
|
||||||
module.exports.createServices = (stores, config) => {
|
module.exports.createServices = (stores, config) => {
|
||||||
const accessService = new AccessService(stores, config);
|
const accessService = new AccessService(stores, config);
|
||||||
@ -30,6 +31,7 @@ module.exports.createServices = (stores, config) => {
|
|||||||
const versionService = new VersionService(stores, config);
|
const versionService = new VersionService(stores, config);
|
||||||
const apiTokenService = new ApiTokenService(stores, config);
|
const apiTokenService = new ApiTokenService(stores, config);
|
||||||
const emailService = new EmailService(config.email, config.getLogger);
|
const emailService = new EmailService(config.email, config.getLogger);
|
||||||
|
const userService = new UserService(stores, config, accessService);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
@ -45,5 +47,6 @@ module.exports.createServices = (stores, config) => {
|
|||||||
versionService,
|
versionService,
|
||||||
apiTokenService,
|
apiTokenService,
|
||||||
emailService,
|
emailService,
|
||||||
|
userService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
102
src/lib/services/user-service.test.ts
Normal file
102
src/lib/services/user-service.test.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import UserService from './user-service';
|
||||||
|
import UserStoreMock from '../../test/fixtures/fake-user-store';
|
||||||
|
import AccessServiceMock from '../../test/fixtures/access-service-mock';
|
||||||
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
|
import { RoleName } from './access-service';
|
||||||
|
import { IUnleashConfig } from '../types/core';
|
||||||
|
|
||||||
|
const config: IUnleashConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
baseUriPath: '',
|
||||||
|
authentication: { enableApiToken: true, createAdminUser: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Should create new user', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
const user = await service.createUser({
|
||||||
|
username: 'test',
|
||||||
|
rootRole: 1,
|
||||||
|
});
|
||||||
|
const storedUser = await userStore.get(user);
|
||||||
|
const allUsers = await userStore.getAll();
|
||||||
|
|
||||||
|
t.truthy(user.id);
|
||||||
|
t.is(user.username, 'test');
|
||||||
|
t.is(allUsers.length, 1);
|
||||||
|
t.is(storedUser.username, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should create default user', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
await service.initAdminUser();
|
||||||
|
|
||||||
|
const user = await service.loginUser('admin', 'admin');
|
||||||
|
t.is(user.username, 'admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should be a valid password', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
const valid = service.validatePassword('this is a strong password!');
|
||||||
|
|
||||||
|
t.true(valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Password must be at least 10 chars', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
t.throws(() => service.validatePassword('admin'), {
|
||||||
|
message: 'The password must be at least 10 characters long.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('The password must contain at least one uppercase letter.', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
t.throws(() => service.validatePassword('qwertyabcde'), {
|
||||||
|
message: 'The password must contain at least one uppercase letter.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('The password must contain at least one number', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
t.throws(() => service.validatePassword('qwertyabcdE'), {
|
||||||
|
message: 'The password must contain at least one number.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('The password must contain at least one special character', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
t.throws(() => service.validatePassword('qwertyabcdE2'), {
|
||||||
|
message: 'The password must contain at least one special character.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should be a valid password with special chars', async t => {
|
||||||
|
const userStore = new UserStoreMock();
|
||||||
|
const accessService = new AccessServiceMock();
|
||||||
|
const service = new UserService({ userStore }, config, accessService);
|
||||||
|
|
||||||
|
const valid = service.validatePassword('this is a strong password!');
|
||||||
|
|
||||||
|
t.true(valid);
|
||||||
|
});
|
237
src/lib/services/user-service.ts
Normal file
237
src/lib/services/user-service.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import owasp from 'owasp-password-strength-test';
|
||||||
|
import Joi from 'joi';
|
||||||
|
|
||||||
|
import UserStore, { IUserSearch } from '../db/user-store';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import { IUnleashConfig } from '../types/core';
|
||||||
|
import User, { IUser } from '../user';
|
||||||
|
import isEmail from '../util/is-email';
|
||||||
|
import { AccessService, RoleName } from './access-service';
|
||||||
|
import { ADMIN } from '../permissions';
|
||||||
|
|
||||||
|
export interface ICreateUser {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
rootRole: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpdateUser {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
rootRole?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUserWithRole extends IUser {
|
||||||
|
rootRole: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStores {
|
||||||
|
userStore: UserStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saltRounds = 10;
|
||||||
|
|
||||||
|
class UserService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private store: UserStore;
|
||||||
|
|
||||||
|
private accessService: AccessService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
stores: IStores,
|
||||||
|
config: IUnleashConfig,
|
||||||
|
accessService: AccessService,
|
||||||
|
) {
|
||||||
|
this.logger = config.getLogger('service/user-service.js');
|
||||||
|
this.store = stores.userStore;
|
||||||
|
this.accessService = accessService;
|
||||||
|
|
||||||
|
if (config.authentication && config.authentication.createAdminUser) {
|
||||||
|
process.nextTick(() => this.initAdminUser());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePassword(password: string): boolean {
|
||||||
|
const result = owasp.test(password);
|
||||||
|
if (!result.strong) {
|
||||||
|
throw new Error(result.errors[0]);
|
||||||
|
} else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initAdminUser(): Promise<void> {
|
||||||
|
const hasAdminUser = await this.store.hasUser({ username: 'admin' });
|
||||||
|
|
||||||
|
if (!hasAdminUser) {
|
||||||
|
// create default admin user
|
||||||
|
try {
|
||||||
|
this.logger.info(
|
||||||
|
'Creating default user "admin" with password "admin"',
|
||||||
|
);
|
||||||
|
const user = await this.store.insert(
|
||||||
|
new User({
|
||||||
|
username: 'admin',
|
||||||
|
permissions: [ADMIN], // TODO: remove in v4
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const passwordHash = await bcrypt.hash('admin', saltRounds);
|
||||||
|
await this.store.setPasswordHash(user.id, passwordHash);
|
||||||
|
|
||||||
|
const rootRoles = await this.accessService.getRootRoles();
|
||||||
|
const adminRole = rootRoles.find(
|
||||||
|
r => r.name === RoleName.ADMIN,
|
||||||
|
);
|
||||||
|
await this.accessService.setUserRootRole(user.id, adminRole.id);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error('Unable to create default user "admin"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<IUserWithRole[]> {
|
||||||
|
const users = await this.store.getAll();
|
||||||
|
const defaultRole = await this.accessService.getRootRole(RoleName.READ);
|
||||||
|
const userRoles = await this.accessService.getRootRoleForAllUsers();
|
||||||
|
const usersWithRootRole = users.map(u => {
|
||||||
|
const rootRole = userRoles.find(r => r.userId === u.id);
|
||||||
|
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
||||||
|
return { ...u, rootRole: roleId };
|
||||||
|
});
|
||||||
|
return usersWithRootRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUser(id: number): Promise<IUserWithRole> {
|
||||||
|
const roles = await this.accessService.getUserRootRoles(id);
|
||||||
|
const defaultRole = await this.accessService.getRootRole(RoleName.READ);
|
||||||
|
const roleId = roles.length > 0 ? roles[0].id : defaultRole.id;
|
||||||
|
const user = await this.store.get({ id });
|
||||||
|
return { ...user, rootRole: roleId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: IUserSearch): Promise<User[]> {
|
||||||
|
return this.store.search(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
rootRole,
|
||||||
|
}: ICreateUser): Promise<User> {
|
||||||
|
assert.ok(username || email, 'You must specify username or email');
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
Joi.assert(email, Joi.string().email(), 'Email');
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await this.store.hasUser({ username, email });
|
||||||
|
if (exists) {
|
||||||
|
throw new Error('User already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.store.insert(
|
||||||
|
// TODO: remove permission in v4.
|
||||||
|
new User({ username, email, name, permissions: [ADMIN] }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.accessService.setUserRootRole(user.id, rootRole);
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||||
|
await this.store.setPasswordHash(user.id, passwordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
rootRole,
|
||||||
|
}: IUpdateUser): Promise<User> {
|
||||||
|
if (email) {
|
||||||
|
Joi.assert(email, Joi.string().email(), 'Email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootRole) {
|
||||||
|
await this.accessService.setUserRootRole(id, rootRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.store.update(id, { name, email });
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginUser(usernameOrEmail: string, password: string): Promise<User> {
|
||||||
|
const idQuery = isEmail(usernameOrEmail)
|
||||||
|
? { email: usernameOrEmail }
|
||||||
|
: { username: usernameOrEmail };
|
||||||
|
const user = await this.store.get(idQuery);
|
||||||
|
const passwordHash = await this.store.getPasswordHash(user.id);
|
||||||
|
|
||||||
|
const match = await bcrypt.compare(password, passwordHash);
|
||||||
|
if (match) {
|
||||||
|
await this.store.successfullyLogin(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
throw new Error('Wrong password, try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to login users without specifying password. Used when integrating
|
||||||
|
* with external identity providers.
|
||||||
|
*
|
||||||
|
* @param usernameOrEmail
|
||||||
|
* @param autoCreateUser
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async loginUserWithoutPassword(
|
||||||
|
email: string,
|
||||||
|
autoCreateUser: boolean = false,
|
||||||
|
): Promise<User> {
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = await this.store.get({ email });
|
||||||
|
} catch (e) {
|
||||||
|
if (autoCreateUser) {
|
||||||
|
const defaultRole = await this.accessService.getRootRole(
|
||||||
|
RoleName.REGULAR,
|
||||||
|
);
|
||||||
|
user = await this.createUser({
|
||||||
|
email,
|
||||||
|
rootRole: defaultRole.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.store.successfullyLogin(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword(userId: number, password: string): Promise<void> {
|
||||||
|
this.validatePassword(password);
|
||||||
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||||
|
return this.store.setPasswordHash(userId, passwordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(userId: number): Promise<void> {
|
||||||
|
const roles = await this.accessService.getRolesForUser(userId);
|
||||||
|
await Promise.all(
|
||||||
|
roles.map(role =>
|
||||||
|
this.accessService.removeUserFromRole(userId, role.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.store.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserService;
|
||||||
|
export default UserService;
|
@ -1,9 +1,17 @@
|
|||||||
import { LogProvider } from '../logger';
|
import { LogProvider } from '../logger';
|
||||||
|
|
||||||
|
interface IExperimentalFlags {
|
||||||
|
[key: string]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUnleashConfig {
|
export interface IUnleashConfig {
|
||||||
getLogger: LogProvider;
|
getLogger: LogProvider;
|
||||||
|
baseUriPath: string;
|
||||||
|
extendedPermissions?: boolean;
|
||||||
|
experimental?: IExperimentalFlags;
|
||||||
authentication: {
|
authentication: {
|
||||||
enableApiToken: boolean;
|
enableApiToken: boolean;
|
||||||
|
createAdminUser: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,15 @@ export interface UserData {
|
|||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class User {
|
export interface IUser {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class User implements IUser {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
isAPI: boolean;
|
isAPI: boolean;
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
interface IExperimentalFlags {
|
import { IUnleashConfig } from '../types/core';
|
||||||
[key: string]: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IConfig {
|
export const isRbacEnabled = (config: IUnleashConfig): boolean => {
|
||||||
experimental: IExperimentalFlags;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isRbacEnabled = (config: IConfig): boolean => {
|
|
||||||
return config && config.experimental && config.experimental.rbac;
|
return config && config.experimental && config.experimental.rbac;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
19
src/lib/util/is-email.ts
Normal file
19
src/lib/util/is-email.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Email address matcher.
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
const matcher = /.+\@.+\..+/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loosely validate an email address.
|
||||||
|
*
|
||||||
|
* @param {string} string
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function isEmail(value: string): boolean {
|
||||||
|
return matcher.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default isEmail;
|
@ -1,5 +1,3 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import { setupApp, setupAppWithCustomAuth } from '../../helpers/test-helper';
|
import { setupApp, setupAppWithCustomAuth } from '../../helpers/test-helper';
|
||||||
import dbInit from '../../helpers/database-init';
|
import dbInit from '../../helpers/database-init';
|
||||||
|
221
src/test/e2e/api/admin/user-admin.e2e.test.ts
Normal file
221
src/test/e2e/api/admin/user-admin.e2e.test.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import { setupApp } from '../../helpers/test-helper';
|
||||||
|
import dbInit from '../../helpers/database-init';
|
||||||
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
import User from '../../../../lib/user';
|
||||||
|
import UserStore from '../../../../lib/db/user-store';
|
||||||
|
import { AccessStore, IRole } from '../../../../lib/db/access-store';
|
||||||
|
import { RoleName } from '../../../../lib/services/access-service';
|
||||||
|
|
||||||
|
let stores;
|
||||||
|
let db;
|
||||||
|
|
||||||
|
let userStore: UserStore;
|
||||||
|
let accessStore: AccessStore;
|
||||||
|
let regularRole: IRole;
|
||||||
|
let adminRole: IRole;
|
||||||
|
|
||||||
|
test.before(async () => {
|
||||||
|
db = await dbInit('user_admin_api_serial', getLogger);
|
||||||
|
stores = db.stores;
|
||||||
|
userStore = stores.userStore;
|
||||||
|
accessStore = stores.accessStore;
|
||||||
|
const roles = await accessStore.getRootRoles();
|
||||||
|
regularRole = roles.find(r => r.name === RoleName.REGULAR);
|
||||||
|
adminRole = roles.find(r => r.name === RoleName.ADMIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach.always(async () => {
|
||||||
|
const users = await userStore.getAll();
|
||||||
|
const deleteAll = users.map((u: User) => userStore.delete(u.id));
|
||||||
|
await Promise.all(deleteAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('returns empty list of users', async t => {
|
||||||
|
t.plan(1);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.get('/api/admin/user-admin')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.users.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('creates and returns all users', async t => {
|
||||||
|
t.plan(2);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
|
||||||
|
const createUserRequests = [...Array(20).keys()].map(i =>
|
||||||
|
request
|
||||||
|
.post('/api/admin/user-admin')
|
||||||
|
.send({
|
||||||
|
email: `some${i}@getunleash.ai`,
|
||||||
|
name: `Some Name ${i}`,
|
||||||
|
rootRole: regularRole.id,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(createUserRequests);
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get('/api/admin/user-admin')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.users.length, 20);
|
||||||
|
t.is(res.body.users[2].rootRole, regularRole.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('creates regular-user without password', async t => {
|
||||||
|
t.plan(3);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.post('/api/admin/user-admin')
|
||||||
|
.send({
|
||||||
|
email: 'some@getunelash.ai',
|
||||||
|
name: 'Some Name',
|
||||||
|
rootRole: regularRole.id,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.email, 'some@getunelash.ai');
|
||||||
|
t.is(res.body.rootRole, regularRole.id);
|
||||||
|
t.truthy(res.body.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('creates admin-user with password', async t => {
|
||||||
|
t.plan(6);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
const { body } = await request
|
||||||
|
.post('/api/admin/user-admin')
|
||||||
|
.send({
|
||||||
|
email: 'some@getunelash.ai',
|
||||||
|
name: 'Some Name',
|
||||||
|
password: 'some-strange-pass-123-GH',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
t.is(body.rootRole, adminRole.id);
|
||||||
|
|
||||||
|
const user = await userStore.get({ id: body.id });
|
||||||
|
t.is(user.email, 'some@getunelash.ai');
|
||||||
|
t.is(user.name, 'Some Name');
|
||||||
|
|
||||||
|
const passwordHash = userStore.getPasswordHash(body.id);
|
||||||
|
t.truthy(passwordHash);
|
||||||
|
|
||||||
|
const roles = await stores.accessStore.getRolesForUserId(body.id);
|
||||||
|
t.is(roles.length, 1);
|
||||||
|
t.is(roles[0].name, RoleName.ADMIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('requires known root role', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.post('/api/admin/user-admin')
|
||||||
|
.send({
|
||||||
|
email: 'some@getunelash.ai',
|
||||||
|
name: 'Some Name',
|
||||||
|
rootRole: 'Unknown',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('update user name', async t => {
|
||||||
|
t.plan(3);
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
const { body } = await request
|
||||||
|
.post('/api/admin/user-admin')
|
||||||
|
.send({
|
||||||
|
email: 'some@getunelash.ai',
|
||||||
|
name: 'Some Name',
|
||||||
|
rootRole: regularRole.id,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
return request
|
||||||
|
.put(`/api/admin/user-admin/${body.id}`)
|
||||||
|
.send({
|
||||||
|
name: 'New name',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.email, 'some@getunelash.ai');
|
||||||
|
t.is(res.body.name, 'New name');
|
||||||
|
// t.is(res.body.rootRole, 'Regular');
|
||||||
|
t.is(res.body.id, body.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should delete user', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
|
||||||
|
const user = await userStore.insert(new User({ email: 'some@mail.com' }));
|
||||||
|
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request.delete(`/api/admin/user-admin/${user.id}`).expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('validator should require strong password', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.post('/api/admin/user-admin/validate-password')
|
||||||
|
.send({ password: 'simple' })
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('validator should accept strong password', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.post('/api/admin/user-admin/validate-password')
|
||||||
|
.send({ password: 'simple123-_ASsad' })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should change password', async t => {
|
||||||
|
t.plan(0);
|
||||||
|
|
||||||
|
const user = await userStore.insert(new User({ email: 'some@mail.com' }));
|
||||||
|
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.post(`/api/admin/user-admin/${user.id}/change-password`)
|
||||||
|
.send({ password: 'simple123-_ASsad' })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should search for users', async t => {
|
||||||
|
t.plan(2);
|
||||||
|
|
||||||
|
await userStore.insert(new User({ email: 'some@mail.com' }));
|
||||||
|
await userStore.insert(new User({ email: 'another@mail.com' }));
|
||||||
|
await userStore.insert(new User({ email: 'another2@mail.com' }));
|
||||||
|
|
||||||
|
const request = await setupApp(stores);
|
||||||
|
return request
|
||||||
|
.get('/api/admin/user-admin/search?q=another')
|
||||||
|
.expect(200)
|
||||||
|
.expect(res => {
|
||||||
|
t.is(res.body.length, 2);
|
||||||
|
t.true(res.body.some(u => u.email === 'another@mail.com'));
|
||||||
|
});
|
||||||
|
});
|
@ -17,12 +17,13 @@ let accessService;
|
|||||||
|
|
||||||
let regularUser;
|
let regularUser;
|
||||||
let superUser;
|
let superUser;
|
||||||
|
let regularRole;
|
||||||
|
let adminRole;
|
||||||
|
let readRole;
|
||||||
|
|
||||||
const createUserWithRegularAccess = async (name, email) => {
|
const createUserWithRegularAccess = async (name, email) => {
|
||||||
const { userStore } = stores;
|
const { userStore } = stores;
|
||||||
const user = await userStore.insert(new User({ name, email }));
|
const user = await userStore.insert(new User({ name, email }));
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(r => r.name === 'Regular');
|
|
||||||
await accessService.addUserToRole(user.id, regularRole.id);
|
await accessService.addUserToRole(user.id, regularRole.id);
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
@ -32,9 +33,7 @@ const createSuperUser = async () => {
|
|||||||
const user = await userStore.insert(
|
const user = await userStore.insert(
|
||||||
new User({ name: 'Alice Admin', email: 'admin@getunleash.io' }),
|
new User({ name: 'Alice Admin', email: 'admin@getunleash.io' }),
|
||||||
);
|
);
|
||||||
const roles = await accessService.getRoles();
|
await accessService.addUserToRole(user.id, adminRole.id);
|
||||||
const superRole = roles.find(r => r.name === 'Admin');
|
|
||||||
await accessService.addUserToRole(user.id, superRole.id);
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,6 +42,11 @@ test.before(async () => {
|
|||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
// projectStore = stores.projectStore;
|
// projectStore = stores.projectStore;
|
||||||
accessService = new AccessService(stores, { getLogger });
|
accessService = new AccessService(stores, { getLogger });
|
||||||
|
const roles = await accessService.getRootRoles();
|
||||||
|
regularRole = roles.find(r => r.name === RoleName.REGULAR);
|
||||||
|
adminRole = roles.find(r => r.name === RoleName.ADMIN);
|
||||||
|
readRole = roles.find(r => r.name === RoleName.READ);
|
||||||
|
|
||||||
regularUser = await createUserWithRegularAccess(
|
regularUser = await createUserWithRegularAccess(
|
||||||
'Bob Test',
|
'Bob Test',
|
||||||
'bob@getunleash.io',
|
'bob@getunleash.io',
|
||||||
@ -120,11 +124,6 @@ test.serial('should grant regular CREATE_FEATURE on all projects', async t => {
|
|||||||
const { CREATE_FEATURE } = permissions;
|
const { CREATE_FEATURE } = permissions;
|
||||||
const user = regularUser;
|
const user = regularUser;
|
||||||
|
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(
|
|
||||||
r => r.name === 'Regular' && r.type === 'root',
|
|
||||||
);
|
|
||||||
|
|
||||||
await accessService.addPermissionToRole(
|
await accessService.addPermissionToRole(
|
||||||
regularRole.id,
|
regularRole.id,
|
||||||
permissions.CREATE_FEATURE,
|
permissions.CREATE_FEATURE,
|
||||||
@ -137,11 +136,6 @@ test.serial('should grant regular CREATE_FEATURE on all projects', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.serial('cannot add CREATE_FEATURE without defining project', async t => {
|
test.serial('cannot add CREATE_FEATURE without defining project', async t => {
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(
|
|
||||||
r => r.name === 'Regular' && r.type === 'root',
|
|
||||||
);
|
|
||||||
|
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
async () => {
|
async () => {
|
||||||
await accessService.addPermissionToRole(
|
await accessService.addPermissionToRole(
|
||||||
@ -159,11 +153,6 @@ test.serial('cannot add CREATE_FEATURE without defining project', async t => {
|
|||||||
test.serial(
|
test.serial(
|
||||||
'cannot remove CREATE_FEATURE without defining project',
|
'cannot remove CREATE_FEATURE without defining project',
|
||||||
async t => {
|
async t => {
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(
|
|
||||||
r => r.name === 'Regular' && r.type === 'root',
|
|
||||||
);
|
|
||||||
|
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
async () => {
|
async () => {
|
||||||
await accessService.removePermissionFromRole(
|
await accessService.removePermissionFromRole(
|
||||||
@ -184,11 +173,6 @@ test.serial('should remove CREATE_FEATURE on all projects', async t => {
|
|||||||
const { CREATE_FEATURE } = permissions;
|
const { CREATE_FEATURE } = permissions;
|
||||||
const user = regularUser;
|
const user = regularUser;
|
||||||
|
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(
|
|
||||||
r => r.name === 'Regular' && r.type === 'root',
|
|
||||||
);
|
|
||||||
|
|
||||||
await accessService.addPermissionToRole(
|
await accessService.addPermissionToRole(
|
||||||
regularRole.id,
|
regularRole.id,
|
||||||
permissions.CREATE_FEATURE,
|
permissions.CREATE_FEATURE,
|
||||||
@ -272,10 +256,10 @@ test.serial('should grant user access to project', async t => {
|
|||||||
|
|
||||||
const roles = await accessService.getRolesForProject(project);
|
const roles = await accessService.getRolesForProject(project);
|
||||||
|
|
||||||
const regularRole = roles.find(
|
const projectRole = roles.find(
|
||||||
r => r.name === 'Regular' && r.project === project,
|
r => r.name === 'Regular' && r.project === project,
|
||||||
);
|
);
|
||||||
await accessService.addUserToRole(sUser.id, regularRole.id);
|
await accessService.addUserToRole(sUser.id, projectRole.id);
|
||||||
|
|
||||||
// Should be able to update feature toggles inside the project
|
// Should be able to update feature toggles inside the project
|
||||||
t.true(await accessService.hasPermission(sUser, CREATE_FEATURE, project));
|
t.true(await accessService.hasPermission(sUser, CREATE_FEATURE, project));
|
||||||
@ -299,10 +283,10 @@ test.serial('should not get access if not specifying project', async t => {
|
|||||||
|
|
||||||
const roles = await accessService.getRolesForProject(project);
|
const roles = await accessService.getRolesForProject(project);
|
||||||
|
|
||||||
const regularRole = roles.find(
|
const projectRole = roles.find(
|
||||||
r => r.name === 'Regular' && r.project === project,
|
r => r.name === 'Regular' && r.project === project,
|
||||||
);
|
);
|
||||||
await accessService.addUserToRole(sUser.id, regularRole.id);
|
await accessService.addUserToRole(sUser.id, projectRole.id);
|
||||||
|
|
||||||
// Should not be able to update feature toggles outside project
|
// Should not be able to update feature toggles outside project
|
||||||
t.false(await accessService.hasPermission(sUser, CREATE_FEATURE));
|
t.false(await accessService.hasPermission(sUser, CREATE_FEATURE));
|
||||||
@ -316,8 +300,6 @@ test.serial('should remove user from role', async t => {
|
|||||||
new User({ name: 'Some User', email: 'random123@getunleash.io' }),
|
new User({ name: 'Some User', email: 'random123@getunleash.io' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(r => r.name === 'Regular');
|
|
||||||
await accessService.addUserToRole(user.id, regularRole.id);
|
await accessService.addUserToRole(user.id, regularRole.id);
|
||||||
|
|
||||||
// check user has one role
|
// check user has one role
|
||||||
@ -336,8 +318,6 @@ test.serial('should return role with users', async t => {
|
|||||||
new User({ name: 'Some User', email: 'random2223@getunleash.io' }),
|
new User({ name: 'Some User', email: 'random2223@getunleash.io' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(r => r.name === 'Regular');
|
|
||||||
await accessService.addUserToRole(user.id, regularRole.id);
|
await accessService.addUserToRole(user.id, regularRole.id);
|
||||||
|
|
||||||
const roleWithUsers = await accessService.getRole(regularRole.id);
|
const roleWithUsers = await accessService.getRole(regularRole.id);
|
||||||
@ -354,8 +334,6 @@ test.serial('should return role with permissions and users', async t => {
|
|||||||
new User({ name: 'Some User', email: 'random2244@getunleash.io' }),
|
new User({ name: 'Some User', email: 'random2244@getunleash.io' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const roles = await accessService.getRoles();
|
|
||||||
const regularRole = roles.find(r => r.name === 'Regular');
|
|
||||||
await accessService.addUserToRole(user.id, regularRole.id);
|
await accessService.addUserToRole(user.id, regularRole.id);
|
||||||
|
|
||||||
const roleWithPermission = await accessService.getRole(regularRole.id);
|
const roleWithPermission = await accessService.getRole(regularRole.id);
|
||||||
@ -397,7 +375,7 @@ test.serial('should set root role for user', async t => {
|
|||||||
new User({ name: 'Some User', email: 'random2255@getunleash.io' }),
|
new User({ name: 'Some User', email: 'random2255@getunleash.io' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await accessService.setUserRootRole(user.id, RoleName.REGULAR);
|
await accessService.setUserRootRole(user.id, regularRole.id);
|
||||||
|
|
||||||
const roles = await accessService.getRolesForUser(user.id);
|
const roles = await accessService.getRolesForUser(user.id);
|
||||||
|
|
||||||
@ -411,8 +389,8 @@ test.serial('should switch root role for user', async t => {
|
|||||||
new User({ name: 'Some User', email: 'random22Read@getunleash.io' }),
|
new User({ name: 'Some User', email: 'random22Read@getunleash.io' }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await accessService.setUserRootRole(user.id, RoleName.REGULAR);
|
await accessService.setUserRootRole(user.id, regularRole.id);
|
||||||
await accessService.setUserRootRole(user.id, RoleName.READ);
|
await accessService.setUserRootRole(user.id, readRole.id);
|
||||||
|
|
||||||
const roles = await accessService.getRolesForUser(user.id);
|
const roles = await accessService.getRolesForUser(user.id);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ let stores;
|
|||||||
let apiTokenService: ApiTokenService;
|
let apiTokenService: ApiTokenService;
|
||||||
|
|
||||||
test.before(async () => {
|
test.before(async () => {
|
||||||
db = await dbInit('api_tokens_serial', getLogger);
|
db = await dbInit('api_token_service_serial', getLogger);
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
// projectStore = stores.projectStore;
|
// projectStore = stores.projectStore;
|
||||||
apiTokenService = new ApiTokenService(stores, {
|
apiTokenService = new ApiTokenService(stores, {
|
||||||
|
93
src/test/e2e/services/user-service.e2e.test.ts
Normal file
93
src/test/e2e/services/user-service.e2e.test.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import dbInit from '../helpers/database-init';
|
||||||
|
import getLogger from '../../fixtures/no-logger';
|
||||||
|
import UserService from '../../../lib/services/user-service';
|
||||||
|
import { AccessService, RoleName } from '../../../lib/services/access-service';
|
||||||
|
import UserStore from '../../../lib/db/user-store';
|
||||||
|
import User from '../../../lib/user';
|
||||||
|
import { IUnleashConfig } from '../../../lib/types/core';
|
||||||
|
import { IRole } from '../../../lib/db/access-store';
|
||||||
|
|
||||||
|
let db;
|
||||||
|
let stores;
|
||||||
|
let userService: UserService;
|
||||||
|
let userStore: UserStore;
|
||||||
|
let adminRole: IRole;
|
||||||
|
|
||||||
|
test.before(async () => {
|
||||||
|
db = await dbInit('user_service_serial', getLogger);
|
||||||
|
stores = db.stores;
|
||||||
|
const config: IUnleashConfig = {
|
||||||
|
getLogger,
|
||||||
|
baseUriPath: '/test',
|
||||||
|
authentication: {
|
||||||
|
enableApiToken: false,
|
||||||
|
createAdminUser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const accessService = new AccessService(stores, config);
|
||||||
|
userService = new UserService(stores, config, accessService);
|
||||||
|
userStore = stores.userStore;
|
||||||
|
const rootRoles = await accessService.getRootRoles();
|
||||||
|
adminRole = rootRoles.find(r => r.name === RoleName.ADMIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
const users = await userStore.getAll();
|
||||||
|
const deleteAll = users.map((u: User) => userStore.delete(u.id));
|
||||||
|
await Promise.all(deleteAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should create initial admin user', async t => {
|
||||||
|
await userService.initAdminUser();
|
||||||
|
await t.notThrowsAsync(userService.loginUser('admin', 'admin'));
|
||||||
|
await t.throwsAsync(userService.loginUser('admin', 'wrong-password'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should not be allowed to create existing user', async t => {
|
||||||
|
await userStore.insert(new User({ username: 'test', name: 'Hans Mola' }));
|
||||||
|
await t.throwsAsync(
|
||||||
|
userService.createUser({ username: 'test', rootRole: adminRole.id }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should create user with password', async t => {
|
||||||
|
await userService.createUser({
|
||||||
|
username: 'test',
|
||||||
|
password: 'A very strange P4ssw0rd_',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
});
|
||||||
|
const user = await userService.loginUser(
|
||||||
|
'test',
|
||||||
|
'A very strange P4ssw0rd_',
|
||||||
|
);
|
||||||
|
t.is(user.username, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should login for user _without_ password', async t => {
|
||||||
|
const email = 'some@test.com';
|
||||||
|
await userService.createUser({
|
||||||
|
email,
|
||||||
|
password: 'A very strange P4ssw0rd_',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
});
|
||||||
|
const user = await userService.loginUserWithoutPassword(email);
|
||||||
|
t.is(user.email, email);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.serial('should get user with root role', async t => {
|
||||||
|
const email = 'some@test.com';
|
||||||
|
const u = await userService.createUser({
|
||||||
|
email,
|
||||||
|
password: 'A very strange P4ssw0rd_',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
});
|
||||||
|
const user = await userService.getUser(u.id);
|
||||||
|
t.is(user.email, email);
|
||||||
|
t.is(user.id, u.id);
|
||||||
|
t.is(user.rootRole, adminRole.id);
|
||||||
|
});
|
@ -127,14 +127,14 @@ test.serial('should reset user after successful login', async t => {
|
|||||||
await store.incLoginAttempts(user);
|
await store.incLoginAttempts(user);
|
||||||
await store.incLoginAttempts(user);
|
await store.incLoginAttempts(user);
|
||||||
|
|
||||||
await store.succesfullLogin(user);
|
await store.successfullyLogin(user);
|
||||||
const storedUser = await store.get(user);
|
const storedUser = await store.get(user);
|
||||||
|
|
||||||
t.is(storedUser.loginAttempts, 0);
|
t.is(storedUser.loginAttempts, 0);
|
||||||
t.true(storedUser.seenAt >= user.seenAt);
|
t.true(storedUser.seenAt >= user.seenAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should store and get permsissions', async t => {
|
test.serial('should store and get permissions', async t => {
|
||||||
const store = stores.userStore;
|
const store = stores.userStore;
|
||||||
const email = 'userWithPermissions@mail.com';
|
const email = 'userWithPermissions@mail.com';
|
||||||
const user = new User({
|
const user = new User({
|
||||||
|
99
src/test/fixtures/access-service-mock.ts
vendored
Normal file
99
src/test/fixtures/access-service-mock.ts
vendored
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { IRole } from '../../lib/db/access-store';
|
||||||
|
import {
|
||||||
|
AccessService,
|
||||||
|
IUserWithRole,
|
||||||
|
RoleName,
|
||||||
|
IPermission,
|
||||||
|
IRoleData,
|
||||||
|
} from '../../lib/services/access-service';
|
||||||
|
import User from '../../lib/user';
|
||||||
|
import noLoggerProvider from './no-logger';
|
||||||
|
|
||||||
|
class AccessServiceMock extends AccessService {
|
||||||
|
public roleName: RoleName;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
{ accessStore: undefined, userStore: undefined },
|
||||||
|
{ getLogger: noLoggerProvider },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermission(
|
||||||
|
user: User,
|
||||||
|
permission: string,
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getPermissions(): IPermission[] {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
addUserToRole(userId: number, roleId: number): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserRootRole(userId: number, roleId: number): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserFromRole(userId: number, roleId: number): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
addPermissionToRole(
|
||||||
|
roleId: number,
|
||||||
|
permission: string,
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removePermissionFromRole(
|
||||||
|
roleId: number,
|
||||||
|
permission: string,
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoles(): Promise<IRole[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRole(roleId: number): Promise<IRoleData> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolesForProject(projectId: string): Promise<IRole[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolesForUser(userId: number): Promise<IRole[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsersForRole(roleId: any): Promise<User[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjectRoleUsers(
|
||||||
|
projectId: string,
|
||||||
|
): Promise<[IRole[], IUserWithRole[]]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
createDefaultProjectRoles(owner: User, projectId: string): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDefaultProjectRoles(owner: User, projectId: string): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccessServiceMock;
|
87
src/test/fixtures/fake-access-store.ts
vendored
Normal file
87
src/test/fixtures/fake-access-store.ts
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import {
|
||||||
|
AccessStore,
|
||||||
|
IRole,
|
||||||
|
IUserRole,
|
||||||
|
IUserPermission,
|
||||||
|
} from '../../lib/db/access-store';
|
||||||
|
import noLoggerProvider from './no-logger';
|
||||||
|
|
||||||
|
class AccessStoreMock extends AccessStore {
|
||||||
|
constructor() {
|
||||||
|
super(undefined, undefined, noLoggerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPermissionsForUser(userId: Number): Promise<IUserPermission[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getPermissionsForRole(roleId: number): Promise<IUserPermission[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoles(): Promise<IRole[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoleWithId(id: number): Promise<IRole> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolesForProject(projectId: string): Promise<IRole[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRolesForProject(projectId: string): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRolesForUserId(userId: number): Promise<IRole[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserIdsForRole(roleId: number): Promise<IRole[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
addUserToRole(userId: number, roleId: number): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUserFromRole(userId: number, roleId: number): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
createRole(
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
project?: string,
|
||||||
|
description?: string,
|
||||||
|
): Promise<IRole> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
addPermissionsToRole(
|
||||||
|
role_id: number,
|
||||||
|
permissions: string[],
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removePermissionFromRole(
|
||||||
|
roleId: number,
|
||||||
|
permission: string,
|
||||||
|
projectId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
getRootRoleForAllUsers(): Promise<IUserRole[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AccessStoreMock;
|
||||||
|
|
||||||
|
export default AccessStoreMock;
|
106
src/test/fixtures/fake-user-store.ts
vendored
Normal file
106
src/test/fixtures/fake-user-store.ts
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import UserStore, { IUserLookup } from '../../lib/db/user-store';
|
||||||
|
import User from '../../lib/user';
|
||||||
|
import noLoggerProvider from './no-logger';
|
||||||
|
|
||||||
|
class UserStoreMock extends UserStore {
|
||||||
|
data: any[];
|
||||||
|
|
||||||
|
idSeq: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(undefined, noLoggerProvider);
|
||||||
|
this.idSeq = 1;
|
||||||
|
this.data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasUser({
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
}: IUserLookup): Promise<number | undefined> {
|
||||||
|
const user = this.data.find(i => {
|
||||||
|
if (id && i.id === id) return true;
|
||||||
|
if (username && i.username === username) return true;
|
||||||
|
if (email && i.email === email) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(user: User): Promise<User> {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
user.id = this.idSeq;
|
||||||
|
this.idSeq += 1;
|
||||||
|
this.data.push(user);
|
||||||
|
return Promise.resolve(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, user: User): Promise<User> {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
this.data = this.data.map(o => {
|
||||||
|
if (o.id === id) return { ...o, name: user.name };
|
||||||
|
return o;
|
||||||
|
});
|
||||||
|
return Promise.resolve(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get({ id, username, email }: IUserLookup): Promise<User> {
|
||||||
|
const user = this.data.find(i => {
|
||||||
|
if (i.id && i.id === id) return true;
|
||||||
|
if (i.username && i.username === username) return true;
|
||||||
|
if (i.email && i.email === email) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
throw new Error('Could not find user');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<User[]> {
|
||||||
|
return Promise.resolve(this.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPasswordHash(userId: number, passwordHash: string): Promise<void> {
|
||||||
|
const u = this.data.find(a => a.id === userId);
|
||||||
|
u.passwordHash = passwordHash;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPasswordHash(id: number): Promise<string> {
|
||||||
|
const u = this.data.find(i => i.id === id);
|
||||||
|
return Promise.resolve(u.passwordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<void> {
|
||||||
|
this.data = this.data.filter(item => item.id !== id);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async successfullyLogin(user: User): Promise<void> {
|
||||||
|
const u = this.data.find(i => i.id === user.id);
|
||||||
|
u.login_attempts = 0;
|
||||||
|
u.seen_at = new Date();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSelectUser(): any {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(): Promise<User[]> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllWithId(): Promise<User[]> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
async incLoginAttempts(): Promise<void> {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserStoreMock;
|
||||||
|
|
||||||
|
export default UserStoreMock;
|
4
src/test/fixtures/store.js
vendored
4
src/test/fixtures/store.js
vendored
@ -12,6 +12,8 @@ const contextFieldStore = require('./fake-context-store');
|
|||||||
const settingStore = require('./fake-setting-store');
|
const settingStore = require('./fake-setting-store');
|
||||||
const addonStore = require('./fake-addon-store');
|
const addonStore = require('./fake-addon-store');
|
||||||
const projectStore = require('./fake-project-store');
|
const projectStore = require('./fake-project-store');
|
||||||
|
const UserStore = require('./fake-user-store');
|
||||||
|
const AccessStore = require('./fake-access-store');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createStores: (databaseIsUp = true) => {
|
createStores: (databaseIsUp = true) => {
|
||||||
@ -35,6 +37,8 @@ module.exports = {
|
|||||||
settingStore: settingStore(databaseIsUp),
|
settingStore: settingStore(databaseIsUp),
|
||||||
addonStore: addonStore(databaseIsUp),
|
addonStore: addonStore(databaseIsUp),
|
||||||
projectStore: projectStore(databaseIsUp),
|
projectStore: projectStore(databaseIsUp),
|
||||||
|
userStore: new UserStore(),
|
||||||
|
accessStore: new AccessStore(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
260
yarn.lock
260
yarn.lock
@ -390,6 +390,21 @@
|
|||||||
resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz"
|
resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz"
|
||||||
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
|
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
|
||||||
|
|
||||||
|
"@mapbox/node-pre-gyp@^1.0.0":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.3.tgz#c740c23ec1007b9278d4c28f767b6e843a88c3d3"
|
||||||
|
integrity sha512-9dTIfQW8HVCxLku5QrJ/ysS/b2MdYngs9+/oPrOTLvp3TrggdANYVW2h8FGJGDf0J7MYfp44W+c90cVJx+ASuA==
|
||||||
|
dependencies:
|
||||||
|
detect-libc "^1.0.3"
|
||||||
|
https-proxy-agent "^5.0.0"
|
||||||
|
make-dir "^3.1.0"
|
||||||
|
node-fetch "^2.6.1"
|
||||||
|
nopt "^5.0.0"
|
||||||
|
npmlog "^4.1.2"
|
||||||
|
rimraf "^3.0.2"
|
||||||
|
semver "^7.3.4"
|
||||||
|
tar "^6.1.0"
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.3":
|
"@nodelib/fs.scandir@2.1.3":
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz"
|
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz"
|
||||||
@ -503,6 +518,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect "^1.0.1"
|
defer-to-connect "^1.0.1"
|
||||||
|
|
||||||
|
"@types/bcrypt@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-3.0.0.tgz#851489a9065a067cb7f3c9cbe4ce9bed8bba0876"
|
||||||
|
integrity sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ==
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.19.0"
|
version "1.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
||||||
@ -587,6 +607,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
|
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
|
||||||
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
|
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
|
||||||
|
|
||||||
|
"@types/owasp-password-strength-test@^1.3.0":
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/owasp-password-strength-test/-/owasp-password-strength-test-1.3.0.tgz#f639e38847eeb0db14bf7b70896cecd4342ac571"
|
||||||
|
integrity sha512-eKYl6svyRua5OVUFm+AXSxdBrKo7snzrCcFv0KoqKNvNgB3fJzRq3s/xphf+jNTllqYxgsx1uWLeAcL4MjLWQQ==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz"
|
||||||
@ -680,6 +705,11 @@
|
|||||||
"@typescript-eslint/types" "4.15.2"
|
"@typescript-eslint/types" "4.15.2"
|
||||||
eslint-visitor-keys "^2.0.0"
|
eslint-visitor-keys "^2.0.0"
|
||||||
|
|
||||||
|
abbrev@1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
|
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||||
|
|
||||||
abort-controller@^3.0.0:
|
abort-controller@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
|
||||||
@ -710,6 +740,13 @@ acorn@^7.1.1, acorn@^7.3.1:
|
|||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz"
|
resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz"
|
||||||
integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
|
integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
|
||||||
|
|
||||||
|
agent-base@6:
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||||
|
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
|
||||||
|
dependencies:
|
||||||
|
debug "4"
|
||||||
|
|
||||||
agent-base@^4.3.0:
|
agent-base@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz"
|
||||||
@ -754,6 +791,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^0.11.0"
|
type-fest "^0.11.0"
|
||||||
|
|
||||||
|
ansi-regex@^2.0.0:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||||
|
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
|
||||||
|
|
||||||
ansi-regex@^3.0.0:
|
ansi-regex@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz"
|
||||||
@ -804,11 +846,24 @@ append-transform@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
default-require-extensions "^3.0.0"
|
default-require-extensions "^3.0.0"
|
||||||
|
|
||||||
|
aproba@^1.0.3:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||||
|
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
|
||||||
|
|
||||||
archy@^1.0.0:
|
archy@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz"
|
||||||
integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
|
integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
|
||||||
|
|
||||||
|
are-we-there-yet@~1.1.2:
|
||||||
|
version "1.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
||||||
|
integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
|
||||||
|
dependencies:
|
||||||
|
delegates "^1.0.0"
|
||||||
|
readable-stream "^2.0.6"
|
||||||
|
|
||||||
arg@^4.1.0:
|
arg@^4.1.0:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||||
@ -1058,6 +1113,14 @@ bcrypt-pbkdf@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
tweetnacl "^0.14.3"
|
||||||
|
|
||||||
|
bcrypt@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71"
|
||||||
|
integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==
|
||||||
|
dependencies:
|
||||||
|
"@mapbox/node-pre-gyp" "^1.0.0"
|
||||||
|
node-addon-api "^3.1.0"
|
||||||
|
|
||||||
bignumber.js@^9.0.0:
|
bignumber.js@^9.0.0:
|
||||||
version "9.0.1"
|
version "9.0.1"
|
||||||
resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz"
|
resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz"
|
||||||
@ -1283,6 +1346,11 @@ chokidar@^3.4.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.1.2"
|
fsevents "~2.1.2"
|
||||||
|
|
||||||
|
chownr@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||||
|
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
|
||||||
|
|
||||||
chunkd@^2.0.1:
|
chunkd@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz"
|
||||||
@ -1394,6 +1462,11 @@ code-excerpt@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
convert-to-spaces "^1.0.1"
|
convert-to-spaces "^1.0.1"
|
||||||
|
|
||||||
|
code-point-at@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||||
|
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||||
|
|
||||||
collection-visit@^1.0.0:
|
collection-visit@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz"
|
||||||
@ -1562,6 +1635,11 @@ connect-session-knex@^2.0.0:
|
|||||||
bluebird "^3.7.2"
|
bluebird "^3.7.2"
|
||||||
knex "^0.21.5"
|
knex "^0.21.5"
|
||||||
|
|
||||||
|
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||||
|
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
|
||||||
|
|
||||||
contains-path@^0.1.0:
|
contains-path@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz"
|
||||||
@ -1813,7 +1891,7 @@ debug@3.2.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debug@4.3.1:
|
debug@4, debug@4.3.1:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz"
|
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz"
|
||||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||||
@ -1948,6 +2026,11 @@ delayed-stream@~1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||||
|
|
||||||
|
delegates@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||||
|
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||||
|
|
||||||
depd@~1.1.0, depd@~1.1.2:
|
depd@~1.1.0, depd@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
|
||||||
@ -1968,6 +2051,11 @@ detect-file@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz"
|
||||||
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
|
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
|
||||||
|
|
||||||
|
detect-libc@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||||
|
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
|
||||||
|
|
||||||
dicer@0.2.5:
|
dicer@0.2.5:
|
||||||
version "0.2.5"
|
version "0.2.5"
|
||||||
resolved "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz"
|
resolved "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz"
|
||||||
@ -2889,6 +2977,13 @@ fs-extra@^8.1.0:
|
|||||||
jsonfile "^4.0.0"
|
jsonfile "^4.0.0"
|
||||||
universalify "^0.1.0"
|
universalify "^0.1.0"
|
||||||
|
|
||||||
|
fs-minipass@^2.0.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||||
|
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
|
||||||
|
dependencies:
|
||||||
|
minipass "^3.0.0"
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||||
@ -2909,6 +3004,20 @@ functional-red-black-tree@^1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
|
||||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||||
|
|
||||||
|
gauge@~2.7.3:
|
||||||
|
version "2.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||||
|
integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
|
||||||
|
dependencies:
|
||||||
|
aproba "^1.0.3"
|
||||||
|
console-control-strings "^1.0.0"
|
||||||
|
has-unicode "^2.0.0"
|
||||||
|
object-assign "^4.1.0"
|
||||||
|
signal-exit "^3.0.0"
|
||||||
|
string-width "^1.0.1"
|
||||||
|
strip-ansi "^3.0.1"
|
||||||
|
wide-align "^1.1.0"
|
||||||
|
|
||||||
gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.2.2:
|
gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.2.2:
|
||||||
version "1.8.4"
|
version "1.8.4"
|
||||||
resolved "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz"
|
resolved "https://registry.npmjs.org/gaxios/-/gaxios-1.8.4.tgz"
|
||||||
@ -3193,6 +3302,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz"
|
||||||
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
|
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
|
||||||
|
|
||||||
|
has-unicode@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||||
|
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
|
||||||
|
|
||||||
has-value@^0.3.1:
|
has-value@^0.3.1:
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz"
|
resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz"
|
||||||
@ -3299,6 +3413,14 @@ https-proxy-agent@^2.2.1:
|
|||||||
agent-base "^4.3.0"
|
agent-base "^4.3.0"
|
||||||
debug "^3.1.0"
|
debug "^3.1.0"
|
||||||
|
|
||||||
|
https-proxy-agent@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||||
|
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
||||||
|
dependencies:
|
||||||
|
agent-base "6"
|
||||||
|
debug "4"
|
||||||
|
|
||||||
human-signals@^1.1.1:
|
human-signals@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz"
|
||||||
@ -3562,6 +3684,13 @@ is-extglob@^2.1.1:
|
|||||||
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
|
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
|
||||||
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
||||||
|
|
||||||
|
is-fullwidth-code-point@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
|
||||||
|
integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
|
||||||
|
dependencies:
|
||||||
|
number-is-nan "^1.0.0"
|
||||||
|
|
||||||
is-fullwidth-code-point@^2.0.0:
|
is-fullwidth-code-point@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
|
||||||
@ -4233,6 +4362,13 @@ lru-cache@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^3.0.2"
|
yallist "^3.0.2"
|
||||||
|
|
||||||
|
lru-cache@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||||
|
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||||
|
dependencies:
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
lru-queue@^0.1.0:
|
lru-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
|
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
|
||||||
@ -4240,7 +4376,7 @@ lru-queue@^0.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
es5-ext "~0.10.2"
|
es5-ext "~0.10.2"
|
||||||
|
|
||||||
make-dir@^3.0.0, make-dir@^3.0.2:
|
make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz"
|
||||||
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
|
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
|
||||||
@ -4420,6 +4556,21 @@ minimist@^1.2.0, minimist@^1.2.5:
|
|||||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
|
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
|
minipass@^3.0.0:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
|
||||||
|
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
|
||||||
|
dependencies:
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
minizlib@^2.1.1:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||||
|
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
|
||||||
|
dependencies:
|
||||||
|
minipass "^3.0.0"
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz"
|
resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz"
|
||||||
@ -4435,7 +4586,7 @@ mkdirp@0.x.x, mkdirp@^0.5.1, mkdirp@~0.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
mkdirp@^1.0.4:
|
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
@ -4552,6 +4703,11 @@ nise@^4.0.4:
|
|||||||
just-extend "^4.0.2"
|
just-extend "^4.0.2"
|
||||||
path-to-regexp "^1.7.0"
|
path-to-regexp "^1.7.0"
|
||||||
|
|
||||||
|
node-addon-api@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
|
||||||
|
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
|
||||||
|
|
||||||
node-cleanup@^2.1.2:
|
node-cleanup@^2.1.2:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
|
resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
|
||||||
@ -4592,6 +4748,13 @@ noms@0.0.0:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
readable-stream "~1.0.31"
|
readable-stream "~1.0.31"
|
||||||
|
|
||||||
|
nopt@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
||||||
|
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
|
||||||
|
dependencies:
|
||||||
|
abbrev "1"
|
||||||
|
|
||||||
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
|
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz"
|
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz"
|
||||||
@ -4619,6 +4782,21 @@ npm-run-path@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-key "^3.0.0"
|
path-key "^3.0.0"
|
||||||
|
|
||||||
|
npmlog@^4.1.2:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||||
|
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
||||||
|
dependencies:
|
||||||
|
are-we-there-yet "~1.1.2"
|
||||||
|
console-control-strings "~1.1.0"
|
||||||
|
gauge "~2.7.3"
|
||||||
|
set-blocking "~2.0.0"
|
||||||
|
|
||||||
|
number-is-nan@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||||
|
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
||||||
|
|
||||||
nyc@^15.1.0:
|
nyc@^15.1.0:
|
||||||
version "15.1.0"
|
version "15.1.0"
|
||||||
resolved "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz"
|
resolved "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz"
|
||||||
@ -4662,7 +4840,7 @@ oauth@0.9.x:
|
|||||||
resolved "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz"
|
resolved "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz"
|
||||||
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
|
||||||
|
|
||||||
object-assign@^4.1.1:
|
object-assign@^4.1.0, object-assign@^4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
||||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||||
@ -4819,6 +4997,11 @@ os-tmpdir@~1.0.2:
|
|||||||
resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
|
||||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||||
|
|
||||||
|
owasp-password-strength-test@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/owasp-password-strength-test/-/owasp-password-strength-test-1.3.0.tgz#4f629e42903e8f6d279b230d657ab61e58e44b12"
|
||||||
|
integrity sha1-T2KeQpA+j20nmyMNZXq2HljkSxI=
|
||||||
|
|
||||||
p-cancelable@^1.0.0:
|
p-cancelable@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz"
|
||||||
@ -5438,7 +5621,7 @@ readable-stream@1.1.x:
|
|||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readable-stream@^2.2.2, readable-stream@~2.3.6:
|
readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6:
|
||||||
version "2.3.7"
|
version "2.3.7"
|
||||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
|
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz"
|
||||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||||
@ -5776,6 +5959,13 @@ semver@^7.3.2:
|
|||||||
resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz"
|
resolved "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz"
|
||||||
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
|
||||||
|
|
||||||
|
semver@^7.3.4:
|
||||||
|
version "7.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
|
||||||
|
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
|
||||||
|
dependencies:
|
||||||
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
send@0.17.1:
|
send@0.17.1:
|
||||||
version "0.17.1"
|
version "0.17.1"
|
||||||
resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz"
|
resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz"
|
||||||
@ -5821,7 +6011,7 @@ serve-static@1.14.1:
|
|||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
send "0.17.1"
|
send "0.17.1"
|
||||||
|
|
||||||
set-blocking@^2.0.0:
|
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
|
||||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||||
@ -5865,7 +6055,7 @@ shebang-regex@^3.0.0:
|
|||||||
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||||
|
|
||||||
signal-exit@^3.0.2:
|
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz"
|
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz"
|
||||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||||
@ -6129,6 +6319,23 @@ string-argv@^0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738"
|
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.1.2.tgz#c5b7bc03fb2b11983ba3a72333dd0559e77e4738"
|
||||||
integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==
|
integrity sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==
|
||||||
|
|
||||||
|
string-width@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||||
|
integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
|
||||||
|
dependencies:
|
||||||
|
code-point-at "^1.0.0"
|
||||||
|
is-fullwidth-code-point "^1.0.0"
|
||||||
|
strip-ansi "^3.0.0"
|
||||||
|
|
||||||
|
"string-width@^1.0.2 || 2":
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||||
|
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||||
|
dependencies:
|
||||||
|
is-fullwidth-code-point "^2.0.0"
|
||||||
|
strip-ansi "^4.0.0"
|
||||||
|
|
||||||
string-width@^3.0.0:
|
string-width@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz"
|
||||||
@ -6191,6 +6398,13 @@ stringify-object@^3.3.0:
|
|||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
|
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||||
|
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^2.0.0"
|
||||||
|
|
||||||
strip-ansi@^4.0.0:
|
strip-ansi@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz"
|
||||||
@ -6297,6 +6511,18 @@ table@^5.2.3:
|
|||||||
slice-ansi "^2.1.0"
|
slice-ansi "^2.1.0"
|
||||||
string-width "^3.0.0"
|
string-width "^3.0.0"
|
||||||
|
|
||||||
|
tar@^6.1.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
|
||||||
|
integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
|
||||||
|
dependencies:
|
||||||
|
chownr "^2.0.0"
|
||||||
|
fs-minipass "^2.0.0"
|
||||||
|
minipass "^3.0.0"
|
||||||
|
minizlib "^2.1.1"
|
||||||
|
mkdirp "^1.0.3"
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
tarn@^3.0.1:
|
tarn@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz"
|
||||||
@ -6615,10 +6841,10 @@ universalify@^0.1.0:
|
|||||||
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
|
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
unleash-frontend@4.0.0-alpha.1:
|
unleash-frontend@4.0.0-alpha.2:
|
||||||
version "4.0.0-alpha.1"
|
version "4.0.0-alpha.2"
|
||||||
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.0.0-alpha.1.tgz#dff9da220e406a46d70530fcd4fa2ee838c68d22"
|
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.0.0-alpha.2.tgz#c0db66b038f79adae8189840dce18271b58b5172"
|
||||||
integrity sha512-fdUh1b9qN6W8LiBdT9v2V4m5NWq9kn2dZSiZbKScRmkP3yofObKlhVeUN6WYu7gPOGVjNz108DdgF7aEURmBkQ==
|
integrity sha512-6JiXrVp5AtldnWxFe2zXDP72n7DHq1dVF6sg6b5gMwhBwLUKlkw4nh27TvGVhwCCFuay82cSwfDrSErsXLXZQg==
|
||||||
|
|
||||||
unpipe@1.0.0, unpipe@~1.0.0:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@ -6807,6 +7033,13 @@ which@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
|
wide-align@^1.1.0:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
|
||||||
|
integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
|
||||||
|
dependencies:
|
||||||
|
string-width "^1.0.2 || 2"
|
||||||
|
|
||||||
widest-line@^3.1.0:
|
widest-line@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz"
|
||||||
@ -6902,6 +7135,11 @@ yallist@^3.0.2:
|
|||||||
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
||||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||||
|
|
||||||
|
yallist@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
|
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||||
|
|
||||||
yaml@^1.7.2:
|
yaml@^1.7.2:
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz"
|
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user