auto commit

This commit is contained in:
bangdk 2024-11-09 06:22:41 +09:00
parent 5a6cfa01b8
commit 103c475541
7 changed files with 664 additions and 16 deletions

View File

@ -17,6 +17,7 @@
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"pg": "^8.11.0", "pg": "^8.11.0",

View File

@ -5,6 +5,7 @@ const authService = require("../../../services/auth.service");
const { body } = require("express-validator"); const { body } = require("express-validator");
const validate = require("../../../middleware/validator.middleware"); const validate = require("../../../middleware/validator.middleware");
const authMiddleware = require("../../../middleware/auth.middleware"); const authMiddleware = require("../../../middleware/auth.middleware");
const PermissionUtils = require("../../../utils/permission.utils");
router.post( router.post(
"/login", "/login",
@ -34,4 +35,16 @@ router.post("/logout", authMiddleware, async (req, res, next) => {
} }
}); });
// src/controllers/app/auth/auth.controller.js
router.get("/menu", authMiddleware, async (req, res, next) => {
try {
const accessibleMenus = await PermissionUtils.getUserAccessibleMenus(
req.user
);
res.json(accessibleMenus);
} catch (error) {
next(error);
}
});
module.exports = router; module.exports = router;

View File

@ -1,32 +1,208 @@
// src/middleware/auth.middleware.js // src/middleware/auth.middleware.js
const jwt = require('jsonwebtoken'); const jwt = require("jsonwebtoken");
const config = require('../config/config'); const { promisify } = require("util");
const { User } = require('../models'); const config = require("../config/config");
const { User, Role, UserRole } = require("../models");
const { AuthenticationError, AuthorizationError } = require("../utils/errors");
const logger = require("../utils/logger");
const redis = require("../config/redis");
/**
* 토큰 검증 사용자 인증 미들웨어
*/
const authMiddleware = async (req, res, next) => { const authMiddleware = async (req, res, next) => {
try { try {
const authHeader = req.headers.authorization; // 토큰 추출
if (!authHeader || !authHeader.startsWith('Bearer ')) { const token = extractTokenFromHeader(req);
return res.status(401).json({ message: 'Authentication token is required' }); if (!token) {
throw new AuthenticationError("Authentication token is required");
} }
const token = authHeader.split(' ')[1]; // 토큰 검증
const decoded = jwt.verify(token, config.jwt.secret); const decoded = await verifyToken(token);
const user = await User.findByPk(decoded.id, { // 세션 확인
attributes: { exclude: ['password'] } await validateSession(decoded.id, token);
});
if (!user || !user.isActive) { // 사용자 정보 조회 및 검증
return res.status(401).json({ message: 'User not found or inactive' }); const user = await getUserWithRoles(decoded.id);
} validateUser(user);
// 마지막 활동 시간 업데이트
await updateLastActivity(user.id);
// 요청 객체에 사용자 정보 추가
req.user = user; req.user = user;
next(); next();
} catch (error) { } catch (error) {
return res.status(401).json({ message: 'Invalid token' }); handleAuthError(error, res);
} }
}; };
module.exports = authMiddleware; /**
* 특정 권한 필요한 엔드포인트용 미들웨어
*/
const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: "Authentication required" });
}
const hasRequiredRole = req.user.Roles.some(
(role) => roles.includes(role.name) && role.isActive
);
if (!hasRequiredRole) {
return res.status(403).json({
message: `Required role: ${roles.join(" or ")}`,
});
}
next();
};
};
/**
* 세부 권한 체크 미들웨어
*/
const checkPermission = (resource, action) => {
return async (req, res, next) => {
try {
if (!req.user) {
throw new AuthenticationError("Authentication required");
}
const hasPermission = await validatePermission(
req.user.id,
resource,
action
);
if (!hasPermission) {
throw new AuthorizationError(
`Permission denied: ${resource}:${action}`
);
}
next();
} catch (error) {
handleAuthError(error, res);
}
};
};
// 유틸리티 함수들
const extractTokenFromHeader = (req) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
return authHeader.split(" ")[1];
};
const verifyToken = async (token) => {
try {
// 토큰 블랙리스트 체크
const isBlacklisted = await redis.get(`blacklist:${token}`);
if (isBlacklisted) {
throw new AuthenticationError("Token has been revoked");
}
return await promisify(jwt.verify)(token, config.jwt.secret);
} catch (error) {
if (error.name === "TokenExpiredError") {
throw new AuthenticationError("Token has expired");
}
throw new AuthenticationError("Invalid token");
}
};
const validateSession = async (userId, token) => {
const currentSession = await redis.get(`session:${userId}`);
if (currentSession && currentSession !== token) {
throw new AuthenticationError("Session has been invalidated");
}
};
const getUserWithRoles = async (userId) => {
return await User.findByPk(userId, {
attributes: { exclude: ["password"] },
include: [
{
model: Role,
through: UserRole,
attributes: ["id", "name", "permissions", "isActive"],
where: { isActive: true },
},
],
});
};
const validateUser = (user) => {
if (!user) {
throw new AuthenticationError("User not found");
}
if (!user.isActive) {
throw new AuthenticationError("User account is inactive");
}
if (user.Roles.length === 0) {
throw new AuthorizationError("User has no active roles");
}
};
const validatePermission = async (userId, resource, action) => {
// Redis에서 캐시된 권한 확인
const cacheKey = `permissions:${userId}`;
let permissions = await redis.get(cacheKey);
if (!permissions) {
// DB에서 권한 조회 및 캐싱
const user = await User.findByPk(userId, {
include: [
{
model: Role,
attributes: ["permissions"],
where: { isActive: true },
},
],
});
permissions = user.Roles.reduce((acc, role) => {
const rolePerms = role.permissions || {};
Object.keys(rolePerms).forEach((res) => {
acc[res] = acc[res] || [];
acc[res].push(...rolePerms[res]);
});
return acc;
}, {});
await redis.set(cacheKey, JSON.stringify(permissions), "EX", 3600); // 1시간 캐시
} else {
permissions = JSON.parse(permissions);
}
return permissions[resource]?.includes(action) || false;
};
const updateLastActivity = async (userId) => {
await redis.set(`lastActivity:${userId}`, Date.now());
};
const handleAuthError = (error, res) => {
logger.error("Authentication error", { error });
if (error instanceof AuthenticationError) {
return res.status(401).json({ message: error.message });
}
if (error instanceof AuthorizationError) {
return res.status(403).json({ message: error.message });
}
return res.status(500).json({ message: "Internal server error" });
};
module.exports = {
authMiddleware,
requireRole,
checkPermission,
validatePermission, // Export for testing
};

View File

@ -0,0 +1,130 @@
// src/middleware/menu.middleware.js
const menuConfig = require("../config/menuConfig");
const { AuthorizationError } = require("../utils/errors");
const logger = require("../config/logger");
/**
* 메뉴 접근 권한 검사 미들웨어
*/
const menuAccessMiddleware = (menuPath) => {
return async (req, res, next) => {
try {
// 메뉴 설정 찾기
const menuSettings = findMenuSettings(menuPath);
if (!menuSettings) {
logger.warn(`Menu configuration not found for path: ${menuPath}`);
return res.status(404).json({
message: "Menu configuration not found",
});
}
const { user } = req;
const action = getActionFromMethod(req.method);
// 권한 설정 확인
if (!menuSettings.permissions || !menuSettings.permissions[action]) {
logger.warn(
`No permission configuration for action: ${action} in menu: ${menuPath}`
);
return res.status(403).json({
message: "Permission configuration not found",
});
}
// 사용자 권한 확인
const allowedRoles = menuSettings.permissions[action];
const hasAccess = checkUserAccess(user, allowedRoles);
if (!hasAccess) {
throw new AuthorizationError(
`Access denied to menu: ${menuPath}, action: ${action}, user role: ${user.role}`
);
}
// 요청 객체에 메뉴 설정 추가 (후속 처리에서 사용 가능)
req.menuSettings = menuSettings;
next();
} catch (error) {
logger.error("Menu access check failed", {
path: menuPath,
userId: req.user?.id,
error: error.message,
});
next(error);
}
};
};
/**
* 메뉴 설정 찾기
*/
const findMenuSettings = (menuPath) => {
// admin 또는 app 영역 확인
const [area, ...pathParts] = menuPath.split("/").filter(Boolean);
let currentConfig = menuConfig[area];
// 경로를 따라 메뉴 설정 탐색
for (const part of pathParts) {
if (!currentConfig) return null;
if (currentConfig[part]) {
currentConfig = currentConfig[part];
} else if (currentConfig.subMenus?.[part]) {
currentConfig = currentConfig.subMenus[part];
} else {
return null;
}
}
return currentConfig;
};
/**
* 사용자 접근 권한 확인
*/
const checkUserAccess = (user, allowedRoles) => {
// 슈퍼 관리자는 모든 접근 허용
if (user.role === "super_admin") {
return true;
}
// 회사 관리자는 자신의 회사 리소스만 접근 가능
if (
user.role === "company_admin" &&
!allowedRoles.includes("company_admin")
) {
return false;
}
// 지점 관리자는 자신의 지점 리소스만 접근 가능
if (user.role === "branch_admin" && !allowedRoles.includes("branch_admin")) {
return false;
}
return allowedRoles.includes(user.role);
};
/**
* HTTP 메소드를 권한 액션으로 변환
*/
const getActionFromMethod = (method) => {
switch (method.toUpperCase()) {
case "GET":
return "view";
case "POST":
return "create";
case "PUT":
case "PATCH":
return "update";
case "DELETE":
return "delete";
default:
return "view";
}
};
module.exports = {
menuAccessMiddleware,
findMenuSettings, // Export for testing
checkUserAccess, // Export for testing
};

View File

@ -0,0 +1,77 @@
// src/routes/registerRoutes.js
const express = require("express");
const menuConfig = require("../config/menuConfig");
const menuAccessMiddleware = require("../middleware/menu.middleware");
const logger = require("../config/logger");
function registerRoutes(app) {
// Admin 라우트 등록
const adminRouter = express.Router();
Object.entries(menuConfig.admin).forEach(([key, config]) => {
registerRouteWithPermissions(adminRouter, key, config);
});
app.use("/api/v1/admin", adminRouter);
// App 라우트 등록
const appRouter = express.Router();
Object.entries(menuConfig.app).forEach(([key, config]) => {
registerRouteWithPermissions(appRouter, key, config);
});
app.use("/api/v1/app", appRouter);
// 등록된 라우트 로깅
logRegisteredRoutes(app);
}
/**
* 권한이 적용된 라우트 등록
*/
function registerRouteWithPermissions(router, key, config) {
const controller = require(`../controllers/${config.path.slice(
1
)}.controller`);
const path = `/${key}`;
router.use(path, menuAccessMiddleware(config.path), controller);
// 서브메뉴 등록
if (config.subMenus) {
Object.entries(config.subMenus).forEach(([subKey, subConfig]) => {
const subController = require(`../controllers/${subConfig.path.slice(
1
)}.controller`);
router.use(
`${path}/${subKey}`,
menuAccessMiddleware(subConfig.path),
subController
);
});
}
}
/**
* 등록된 라우트 로깅
*/
function logRegisteredRoutes(app) {
const routes = [];
app._router.stack.forEach((middleware) => {
if (middleware.route) {
routes.push(
`${Object.keys(middleware.route.methods)} ${middleware.route.path}`
);
} else if (middleware.name === "router") {
middleware.handle.stack.forEach((handler) => {
if (handler.route) {
routes.push(
`${Object.keys(handler.route.methods)} ${handler.route.path}`
);
}
});
}
});
logger.info("Registered routes:");
routes.forEach((route) => logger.info(route));
}
module.exports = registerRoutes;

View File

@ -0,0 +1,194 @@
// src/utils/permissions.js
const { Role, UserRole } = require("../models");
const menuConfig = require("../config/menuConfig");
const redis = require("../config/redis");
const logger = require("../config/logger");
class PermissionUtils {
/**
* 사용자의 메뉴 권한 검사
*/
static async checkUserPermissions(user, menuPath, action) {
try {
// 캐시된 권한 확인
const permissions = await this.getUserPermissions(user.id);
// 메뉴 설정에서 권한 정보 찾기
const menuPermissions = this.findMenuPermissions(menuPath);
if (!menuPermissions) {
logger.warn(`No permission configuration found for menu: ${menuPath}`);
return false;
}
// 사용자 역할이 해당 작업의 허용 목록에 있는지 확인
const allowedRoles = menuPermissions.permissions[action] || [];
return this.hasRequiredRole(permissions.roles, allowedRoles);
} catch (error) {
logger.error("Permission check failed", {
userId: user.id,
menuPath,
action,
error,
});
return false;
}
}
/**
* 사용자의 권한 정보 조회 (캐시 활용)
*/
static async getUserPermissions(userId) {
const cacheKey = `permissions:${userId}`;
// 캐시 확인
let permissions = await redis.get(cacheKey);
if (permissions) {
return JSON.parse(permissions);
}
// DB에서 권한 조회
permissions = await this.loadUserPermissions(userId);
// 캐시 저장 (1시간)
await redis.set(cacheKey, JSON.stringify(permissions), "EX", 3600);
return permissions;
}
/**
* DB에서 사용자 권한 로드
*/
static async loadUserPermissions(userId) {
const userRoles = await UserRole.findAll({
where: { userId },
include: [
{
model: Role,
where: { isActive: true },
attributes: ["name", "permissions"],
},
],
});
const roles = userRoles.map((ur) => ur.Role.name);
const permissions = userRoles.reduce((acc, ur) => {
return this.mergePermissions(acc, ur.Role.permissions);
}, {});
return { roles, permissions };
}
/**
* 메뉴 설정에서 권한 정보 찾기
*/
static findMenuPermissions(menuPath) {
const parts = menuPath.split("/").filter(Boolean);
let config = menuConfig;
for (const part of parts) {
if (config[part]) {
config = config[part];
} else if (config.subMenus?.[part]) {
config = config.subMenus[part];
} else {
return null;
}
}
return config;
}
/**
* 권한 병합
*/
static mergePermissions(base, additional) {
const merged = { ...base };
Object.entries(additional || {}).forEach(([key, value]) => {
if (Array.isArray(value)) {
merged[key] = [...new Set([...(merged[key] || []), ...value])];
} else if (typeof value === "object") {
merged[key] = this.mergePermissions(merged[key] || {}, value);
} else {
merged[key] = value;
}
});
return merged;
}
/**
* 필요한 역할 확인
*/
static hasRequiredRole(userRoles, allowedRoles) {
return userRoles.some((role) => allowedRoles.includes(role));
}
/**
* 사용자의 권한 캐시 무효화
*/
static async invalidateUserPermissions(userId) {
const cacheKey = `permissions:${userId}`;
await redis.del(cacheKey);
}
/**
* 사용자의 접근 가능한 메뉴 목록 조회
*/
static async getUserAccessibleMenus(user) {
const permissions = await this.getUserPermissions(user.id);
return this.filterAccessibleMenus(menuConfig, permissions.roles);
}
/**
* 접근 가능한 메뉴 필터링
*/
static filterAccessibleMenus(menuConfig, userRoles, parentPath = "") {
const accessibleMenus = {};
Object.entries(menuConfig).forEach(([key, config]) => {
const currentPath = `${parentPath}/${key}`;
// 메뉴에 대한 조회 권한 확인
const hasAccess = config.permissions?.view?.some((role) =>
userRoles.includes(role)
);
if (hasAccess) {
accessibleMenus[key] = {
path: config.path || currentPath,
permissions: this.filterUserPermissions(
config.permissions,
userRoles
),
};
// 서브메뉴 처리
if (config.subMenus) {
accessibleMenus[key].subMenus = this.filterAccessibleMenus(
config.subMenus,
userRoles,
currentPath
);
}
}
});
return accessibleMenus;
}
/**
* 사용자 권한 필터링
*/
static filterUserPermissions(permissions, userRoles) {
const filtered = {};
Object.entries(permissions || {}).forEach(([action, allowedRoles]) => {
filtered[action] = allowedRoles.some((role) => userRoles.includes(role));
});
return filtered;
}
}
module.exports = PermissionUtils;

View File

@ -338,6 +338,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1073,6 +1078,11 @@ cliui@^8.0.1:
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
co@^4.6.0: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@ -1280,6 +1290,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
denque@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
depd@2.0.0: depd@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@ -1951,6 +1966,21 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ioredis@^5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.1.tgz#1c56b70b759f01465913887375ed809134296f40"
integrity sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
debug "^4.3.4"
denque "^2.1.0"
lodash.defaults "^4.2.0"
lodash.isarguments "^3.1.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
ipaddr.js@1.9.1: ipaddr.js@1.9.1:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@ -2570,11 +2600,21 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
lodash.includes@^4.3.0: lodash.includes@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
lodash.isarguments@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==
lodash.isboolean@^3.0.3: lodash.isboolean@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
@ -3174,6 +3214,18 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
redis-errors "^1.0.0"
require-directory@^2.1.1: require-directory@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -3433,6 +3485,11 @@ stack-utils@^2.0.3:
dependencies: dependencies:
escape-string-regexp "^2.0.0" escape-string-regexp "^2.0.0"
standard-as-callback@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
statuses@2.0.1: statuses@2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"