diff --git a/.dockerignore b/.dockerignore index b99e7de969338ea4dab5eedd05299349aa8c7049..f5a5e249bfe45608d8d00a85d90918a0d9062ffc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ node_modules .git .gitignore +data diff --git a/.eslintignore b/.eslintignore index ed4598e7fabda7e5f6950645c5570c66ac5cfd28..cff71ae1a6ce1e94e00f0ec28298bb3a56d89fc2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules bin +data diff --git a/.gitignore b/.gitignore index 349f2b1937a1bcf134e2e996fd86b8945de07636..c9156895f6e72132faecaf379c297f032f3c43f9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ coverage .netlify kong.yml + + +data diff --git a/.prettierignore b/.prettierignore index 6895bf07a960c02734f68bf0ef353f557ec1a7e1..161501f965cbb072852ab334a693a67d0a7e6aa4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ node_modules coverage - +data diff --git a/README.md b/README.md index 40e017d78154d9569ff37fb1ff4ec42c51d99bcc..bc73802b541ec12a8f6d72ce7308359449343498 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RESTful API Node Server Boilerplate +# RESTful API Node Server Boilerplate - te [](https://travis-ci.org/hagopj13/node-express-boilerplate) [](https://coveralls.io/github/hagopj13/node-express-boilerplate?branch=master) diff --git a/docker-compose.yml b/docker-compose.yml index 65a108e4be87a423e686e612d6e40081f376af1b..26dcb0a5dcc1c133cc1bdeea7776efd2596f6240 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,8 +36,6 @@ services: playground-db: container_name: ${DB_CONTAINER_NAME:-playground-db} image: mongo:4.4.6-bionic - ports: - - '${MONGO_EXPOSE_PORT}:27017' volumes: - dbdata:/data/db networks: @@ -47,8 +45,9 @@ services: upload: platform: linux/amd64 container_name: ${ENV:-dev}-upload - build: ./upload/ - image: boschvn/upload:${IMAGE_TAG:-latest} + image: boschvn/upload:latest + env_file: + - .env volumes: - '${UPLOAD_PATH}:/usr/src/upload/data' networks: diff --git a/kong.yml.template b/kong.yml.template index 657cfb6d9104855585501412acd00f42b7739d15..1a60994422ecfd383608083258acce58aa5dcb3d 100644 --- a/kong.yml.template +++ b/kong.yml.template @@ -18,6 +18,7 @@ services: - HEAD - CONNECT - TRACE + read_timeout: 180000 plugins: - name: rate-limiting enabled: true @@ -29,7 +30,7 @@ plugins: config: fault_tolerant: true hide_client_headers: false - minute: 60 + minute: 120 limit_by: header header_name: x-forwarded-for policy: local diff --git a/package.json b/package.json index 9aecb60654f97dd31c7862df8d2d5f38675ac212..ba4e2736e19b01b6dbb250d91913e0c34ecc1334 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "scripts": { "start": "pm2 start ecosystem.config.json --no-daemon", - "dev": "cross-env NODE_ENV=development nodemon src/index.js", + "dev": "cross-env NODE_ENV=development nodemon --ignore 'data/*' src/index.js", "test": "jest -i --colors --verbose --detectOpenHandles", "test:watch": "jest -i --watchAll", "coverage": "jest -i --coverage", @@ -69,6 +69,7 @@ "jimp": "^0.22.12", "joi": "^17.3.0", "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.21", "moment": "^2.24.0", "mongoose": "^5.7.7", "morgan": "^1.9.1", @@ -76,15 +77,16 @@ "passport": "^0.4.0", "passport-jwt": "^4.0.0", "pm2": "^5.1.0", - "socket.io": "^4.7.5", + "socket.io": "^4.8.0", "swagger-jsdoc": "^6.0.8", "swagger-ui-express": "^4.1.6", "utf-8-validate": "^6.0.4", "validator": "^13.0.0", - "winston": "^3.2.1", - "xss-clean": "^0.1.1" + "websocket": "^1.0.35", + "winston": "^3.2.1" }, "devDependencies": { + "@types/lodash": "^4.17.12", "@types/mongoose": "^5.11.97", "coveralls": "^3.0.7", "eslint": "^7.0.0", diff --git a/src/app.js b/src/app.js index 97a3c4eae5a320a6f18d0419e22287d3158a5ec5..947ff0cdec5bfdf1b966cbaff08fce44927e0138 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,5 @@ const express = require('express'); const helmet = require('helmet'); -const xss = require('xss-clean'); const cookies = require('cookie-parser'); const mongoSanitize = require('express-mongo-sanitize'); const compression = require('compression'); @@ -16,6 +15,7 @@ const routesV2 = require('./routes/v2'); const { errorConverter, errorHandler } = require('./middlewares/error'); const ApiError = require('./utils/ApiError'); const setupProxy = require('./config/proxyHandler'); +const { init: initSocketIO } = require('./config/socket'); const app = express(); @@ -37,7 +37,6 @@ app.use(express.json({ limit: '50mb', strict: false })); app.use(express.urlencoded({ extended: true, limit: '50mb', parameterLimit: 10000 })); // sanitize request data -app.use(xss()); app.use(mongoSanitize()); // gzip compression @@ -46,14 +45,7 @@ app.use(compression()); // enable cors app.use( cors({ - origin: [ - /localhost:\d+/, - /\.digitalauto\.tech$/, - /\.digitalauto\.asia$/, - /\.digital\.auto$/, - 'https://digitalauto.netlify.app', - /127\.0\.0\.1:\d+/, - ], + origin: config.cors.regex, credentials: true, }) ); @@ -63,18 +55,14 @@ app.options('*', cors()); app.use(passport.initialize()); passport.use('jwt', jwtStrategy); -// limit repeated failed requests to auth endpoints -// if (config.env === 'production') { -// app.use('/v1/auth', authLimiter); -// app.use('/v2/auth', authLimiter); -// } - // v1 api routes app.use('/v1', routes); app.use('/v2', routesV2); // Setup proxy to other services setupProxy(app); +const server = require('http').createServer(app); +initSocketIO(server); // send back a 404 error for any unknown api request app.use((req, res, next) => { diff --git a/src/config/config.js b/src/config/config.js index eddc37db4241d2307c176f9be4f104ce7713f67d..49739f25bc61d7d17b3820749fb3ed38ab1b726d 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -18,6 +18,8 @@ const envVarsSchema = Joi.object() JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number() .default(10) .description('minutes after which verify email token expires'), + JWT_COOKIE_NAME: Joi.string().required().description('JWT cookie name'), + JWT_COOKIE_DOMAIN: Joi.string().required().description('JWT cookie domain'), SMTP_HOST: Joi.string().description('server that will send the emails'), SMTP_PORT: Joi.number().description('port to connect to the email server'), SMTP_USERNAME: Joi.string().description('username for email server'), @@ -45,6 +47,7 @@ const envVarsSchema = Joi.object() ETAS_CLIENT_SECRET: Joi.string().description('ETAS client secret'), ETAS_SCOPE: Joi.string().description('ETAS scope'), ETAS_INSTANCE_ENDPOINT: Joi.string().description('ETAS instance endpoint'), + ETAS_DEV_INSTANCE_ENDPOINT: Joi.string().description('ETAS dev instance endpoint'), // Certivity CERTIVITY_CLIENT_ID: Joi.string().required().description('Certivity client id'), CERTIVITY_CLIENT_SECRET: Joi.string().required().description('Certivity client secret'), @@ -62,6 +65,16 @@ const config = { env: envVars.NODE_ENV, port: envVars.PORT, strictAuth: envVars.STRICT_AUTH, + cors: { + regex: [ + /localhost:\d+/, + /\.digitalauto\.tech$/, + /\.digitalauto\.asia$/, + /\.digital\.auto$/, + 'https://digitalauto.netlify.app', + /127\.0\.0\.1:\d+/, + ], + }, mongoose: { url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''), options: { @@ -77,10 +90,14 @@ const config = { refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS, resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES, verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES, - cookieRefreshOptions: { - secure: true, - httpOnly: true, - sameSite: 'None', + cookie: { + name: envVars.JWT_COOKIE_NAME, + options: { + secure: true, + httpOnly: true, + sameSite: 'None', + ...(envVars.NODE_ENV === 'production' && { domain: envVars.JWT_COOKIE_DOMAIN }), + }, }, }, email: { @@ -140,6 +157,7 @@ const config = { clientSecret: envVars.ETAS_CLIENT_SECRET, scope: envVars.ETAS_SCOPE, instanceEndpoint: envVars.ETAS_INSTANCE_ENDPOINT, + developmentEndpoint: envVars.ETAS_DEV_INSTANCE_ENDPOINT, }, githubIssueSubmitUrl: 'https://api.github.com/repos/digital-auto/vehicle_signal_specification/issues', certivity: { diff --git a/src/config/passport.js b/src/config/passport.js index d92ebf346bf726f3ea2187f77cc586589bb42b03..73feed0d02550af82781b415828e24de3c1906d8 100644 --- a/src/config/passport.js +++ b/src/config/passport.js @@ -31,4 +31,5 @@ const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify); module.exports = { jwtStrategy, + jwtVerify, }; diff --git a/src/config/roles.js b/src/config/roles.js index 227987ee5dc1d726bb24c9e2d8f0c457f4578731..9abcebf755118802ef43b43d7aed52a024ad3cb7 100644 --- a/src/config/roles.js +++ b/src/config/roles.js @@ -21,6 +21,9 @@ const PERMISSIONS = { // read assets, READ_ASSET: 'readAsset', WRITE_ASSET: 'writeAsset', + + // deploy hardware + DEPLOY_HARDWARE: 'deployHardware', }; const PERMISSIONS_DESCRIPTION = { @@ -29,6 +32,7 @@ const PERMISSIONS_DESCRIPTION = { [PERMISSIONS.READ_MODEL]: 'Read model', [PERMISSIONS.WRITE_MODEL]: 'Write model', [PERMISSIONS.GENERATIVE_AI]: 'Generative AI', + [PERMISSIONS.DEPLOY_HARDWARE]: 'Deploy hardware', }; // The role here is applied for the resources that the user is not the owner of @@ -64,6 +68,7 @@ const ROLES = { PERMISSIONS.GENERATIVE_AI, PERMISSIONS.READ_ASSET, PERMISSIONS.WRITE_ASSET, + PERMISSIONS.DEPLOY_HARDWARE, ], ref: 'admin', name: 'Admin', @@ -78,6 +83,11 @@ const ROLES = { ref: 'write_asset', name: 'Write asset', }, + deploy_hardware: { + permissions: [PERMISSIONS.DEPLOY_HARDWARE], + ref: 'deploy_hardware', + name: 'Deploy hardware', + }, }; const RESOURCES = { diff --git a/src/config/socket.js b/src/config/socket.js index 5b9790ad575f2077840a42221fdc3de663adf1f6..0e6f87c839ef65d3bcb08d874d605846e6277e78 100644 --- a/src/config/socket.js +++ b/src/config/socket.js @@ -1,22 +1,50 @@ const { Server } = require('socket.io'); const logger = require('./logger'); +const ApiError = require('../utils/ApiError'); +const httpStatus = require('http-status'); +const jwt = require('jsonwebtoken'); +const config = require('./config'); +const { jwtVerify } = require('./passport'); +const { tokenTypes } = require('./tokens'); +const permissionService = require('../services/permission.service'); +const { PERMISSIONS } = require('./roles'); let io = null; const init = (server) => { io = new Server(server, { cors: { - origin: [ - /localhost:\d+/, - /\.digitalauto\.tech$/, - /\.digitalauto\.asia$/, - /\.digital\.auto$/, - 'https://digitalauto.netlify.app', - ], + origin: config.cors.regex, credentials: true, }, }); + io.use(function (socket, next) { + if (socket.handshake.query && socket.handshake.query.access_token) { + jwt.verify(socket.handshake.query.access_token, config.jwt.secret, async function (err, decoded) { + if (err) return next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate')); + await jwtVerify( + { + type: tokenTypes.ACCESS, + sub: decoded.sub, + }, + async (error, user) => { + if (error || !user) { + return next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate')); + } + // if (!(await permissionService.hasPermission(user.id, PERMISSIONS.GENERATIVE_AI))) + // return next(new ApiError(httpStatus.UNAUTHORIZED, 'You are not authorized to access this')); + // } + socket.user = user; + next(); + } + ); + }); + } else { + next(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate')); + } + }); + io.on('connection', () => { logger.info('a user connected'); }); diff --git a/src/controllers/api.controller.js b/src/controllers/api.controller.js index 8cf7522b27f73c63d05a1bd943235e2cff8a40f1..498268eec70b18249727c43334245cb0cc39f2a7 100644 --- a/src/controllers/api.controller.js +++ b/src/controllers/api.controller.js @@ -31,10 +31,22 @@ const deleteApi = catchAsync(async (req, res) => { res.status(httpStatus.NO_CONTENT).send(); }); +const listVSSVersions = catchAsync(async (req, res) => { + const versions = await apiService.listVSSVersions(); + res.send(versions); +}); + +const getVSSVersion = catchAsync(async (req, res) => { + const version = await apiService.getVSSVersion(req.params.name); + res.send(version); +}); + module.exports = { createApi, getApiByModelId, getApi, updateApi, deleteApi, + listVSSVersions, + getVSSVersion, }; diff --git a/src/controllers/asset.controller.js b/src/controllers/asset.controller.js index 772f97763d955f371c6d8ceae6d9c7e43542bbe0..45bca37b869350bbc664e9d79f69306e5515a4db 100644 --- a/src/controllers/asset.controller.js +++ b/src/controllers/asset.controller.js @@ -18,11 +18,13 @@ const getAssets = catchAsync(async (req, res) => { const filter = pick(req.query, ['name', 'type']); const options = pick(req.query, ['sortBy', 'limit', 'page']); - const isAdmin = await permissionService.hasPermission(userId, PERMISSIONS.ADMIN); - if (!isAdmin) { - filter.created_by = userId; - } + const result = await assetService.queryAssets(filter, options, userId); + res.send(result); +}); +const getAllAssets = catchAsync(async (req, res) => { + const filter = pick(req.query, ['name', 'type']); + const options = pick(req.query, ['sortBy', 'limit', 'page']); const result = await assetService.queryAssets(filter, options); res.send(result); }); @@ -104,4 +106,5 @@ module.exports = { generateToken, addAuthorizedUser, deleteAuthorizedUser, + getAllAssets, }; diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 27ab779059045a7a4a7e6092a7f6fd131a67c041..eaa9a7a76ea44022e108a095caa7ff85b5e31143 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,10 +1,9 @@ const httpStatus = require('http-status'); const catchAsync = require('../utils/catchAsync'); -const { authService, userService, tokenService, emailService, fileService } = require('../services'); +const { authService, userService, tokenService, emailService, logService } = require('../services'); const config = require('../config/config'); const ApiError = require('../utils/ApiError'); const logger = require('../config/logger'); -const image = require('../utils/image'); const register = catchAsync(async (req, res) => { const user = await userService.createUser({ @@ -12,10 +11,11 @@ const register = catchAsync(async (req, res) => { provider: req.body?.provider || 'Email', }); const tokens = await tokenService.generateAuthTokens(user); - res.cookie('token', tokens.refresh.token, { + res.cookie(config.jwt.cookie.name, tokens.refresh.token, { expires: tokens.refresh.expires, - ...config.jwt.cookieRefreshOptions, + ...config.jwt.cookie.options, }); + delete tokens.refresh; res.status(httpStatus.CREATED).send({ user, tokens }); }); @@ -24,25 +24,28 @@ const login = catchAsync(async (req, res) => { const { email, password } = req.body; const user = await authService.loginUserWithEmailAndPassword(email, password); const tokens = await tokenService.generateAuthTokens(user); - res.cookie('token', tokens.refresh.token, { + res.cookie(config.jwt.cookie.name, tokens.refresh.token, { expires: tokens.refresh.expires, - ...config.jwt.cookieRefreshOptions, + ...config.jwt.cookie.options, }); delete tokens.refresh; res.send({ user, tokens }); }); const logout = catchAsync(async (req, res) => { - await authService.logout(req.cookies.token); - res.clearCookie('token'); + await authService.logout(req.cookies[config.jwt.cookie.name]); + res.clearCookie(config.jwt.cookie.name); + res.clearCookie(config.jwt.cookie.name, { + ...config.jwt.cookie.options, + }); res.status(httpStatus.NO_CONTENT).send(); }); const refreshTokens = catchAsync(async (req, res) => { - const tokens = await authService.refreshAuth(req.cookies.token); - res.cookie('token', tokens.refresh.token, { + const tokens = await authService.refreshAuth(req.cookies[config.jwt.cookie.name]); + res.cookie(config.jwt.cookie.name, tokens.refresh.token, { expires: tokens.refresh.expires, - ...config.jwt.cookieRefreshOptions, + ...config.jwt.cookie.options, }); delete tokens.refresh; @@ -50,14 +53,75 @@ const refreshTokens = catchAsync(async (req, res) => { }); const forgotPassword = catchAsync(async (req, res) => { + const returnRawToken = req.query.return_raw_token; + const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email); - await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken); - res.status(httpStatus.NO_CONTENT).send(); + + let domain = undefined; + try { + const hostname = new URL(req.get('referer')).hostname; + if (hostname === 'auth.digital.auto') { + domain = hostname; + } + } catch (error) {} + + if (returnRawToken) { + res.status(httpStatus.OK).send({ resetPasswordToken }); + } else { + await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken, domain); + res.status(httpStatus.NO_CONTENT).send(); + } + + try { + await logService.createLog( + { + name: 'Forgot password', + type: 'forgot_password', + created_by: req.body.email, + description: `User with email ${req.body.email} has triggered forgot password flow`, + }, + { + headers: { + origin: req.get('origin'), + referer: req.get('referer'), + }, + } + ); + } catch (error) { + logger.warn(`Failed to create log - forgot password log: ${error}`); + } }); const resetPassword = catchAsync(async (req, res) => { - await authService.resetPassword(req.query.token, req.body.password); - res.status(httpStatus.NO_CONTENT).send(); + let user; + try { + user = await authService.resetPassword(req.query.token, req.body.password); + } catch (error) { + logger.info(`Failed to reset password: ${error}`); + } finally { + res.status(httpStatus.NO_CONTENT).send(); + } + + try { + await logService.createLog( + { + name: 'Password reset', + type: 'password_reset', + created_by: user.email || user.id || user._id, + description: `User with email ${user.email}, id ${user.id || user._id} has reset their password`, + ref_type: 'user', + ref_id: user.id || user._id, + }, + { + headers: { + origin: req.get('origin'), + referer: req.get('referer'), + }, + } + ); + } catch (error) { + logger.warn(`Failed to create log: ${error}`); + } }); const sendVerificationEmail = catchAsync(async (req, res) => { @@ -77,6 +141,7 @@ const githubCallback = catchAsync(async (req, res) => { await authService.githubCallback(code, userId); res.redirect(`${origin || 'http://127.0.0.1:3000'}/auth/github/success`); } catch (error) { + logger.error(error); res.status(httpStatus.UNAUTHORIZED).send('Unauthorized. Please try again.'); } }); @@ -100,9 +165,9 @@ const sso = catchAsync(async (req, res) => { } const tokens = await tokenService.generateAuthTokens(user); - res.cookie('token', tokens.refresh.token, { + res.cookie(config.jwt.cookie.name, tokens.refresh.token, { expires: tokens.refresh.expires, - ...config.jwt.cookieRefreshOptions, + ...config.jwt.cookie.options, }); delete tokens.refresh; diff --git a/src/controllers/extendedApi.controller.js b/src/controllers/extendedApi.controller.js index 90fda16fd8eecaf2b59ae125f33bec2703b19e6f..fefe6a52f867cd047d8e173ec9abe52e76ff13ee 100644 --- a/src/controllers/extendedApi.controller.js +++ b/src/controllers/extendedApi.controller.js @@ -1,15 +1,23 @@ const httpStatus = require('http-status'); const catchAsync = require('../utils/catchAsync'); -const { extendedApiService } = require('../services'); +const { extendedApiService, permissionService } = require('../services'); const pick = require('../utils/pick'); const ApiError = require('../utils/ApiError'); +const { PERMISSIONS } = require('../config/roles'); const createExtendedApi = catchAsync(async (req, res) => { + if (!(await permissionService.hasPermission(req.user?.id, PERMISSIONS.WRITE_MODEL, req.body.model))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model'); + } const extendedApi = await extendedApiService.createExtendedApi(req.body); res.status(httpStatus.CREATED).send(extendedApi); }); const getExtendedApis = catchAsync(async (req, res) => { + const { model } = req.query; + if (!(await permissionService.canAccessModel(req.user?.id, model))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden'); + } const filter = pick(req.query, ['apiName', 'model', 'skeleton']); const options = pick(req.query, ['sortBy', 'limit', 'page']); const result = await extendedApiService.queryExtendedApis(filter, options); @@ -18,6 +26,10 @@ const getExtendedApis = catchAsync(async (req, res) => { const getExtendedApi = catchAsync(async (req, res) => { const extendedApi = await extendedApiService.getExtendedApiById(req.params.id); + const { model } = extendedApi; + if (!(await permissionService.canAccessModel(req.user?.id, model))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden'); + } if (!extendedApi) { throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found'); } @@ -26,6 +38,9 @@ const getExtendedApi = catchAsync(async (req, res) => { const getExtendedApiByApiNameAndModel = catchAsync(async (req, res) => { const { apiName, model } = req.query; + if (!(await permissionService.canAccessModel(req.user?.id, model))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden'); + } const extendedApi = await extendedApiService.getExtendedApiByApiNameAndModel(apiName, model); if (!extendedApi) { throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found'); @@ -34,12 +49,12 @@ const getExtendedApiByApiNameAndModel = catchAsync(async (req, res) => { }); const updateExtendedApi = catchAsync(async (req, res) => { - const extendedApi = await extendedApiService.updateExtendedApiById(req.params.id, req.body); + const extendedApi = await extendedApiService.updateExtendedApiById(req.params.id, req.body, req.user.id); res.send(extendedApi); }); const deleteExtendedApi = catchAsync(async (req, res) => { - await extendedApiService.deleteExtendedApiById(req.params.id); + await extendedApiService.deleteExtendedApiById(req.params.id, req.user.id); res.status(httpStatus.NO_CONTENT).send(); }); diff --git a/src/controllers/genai.controller.js b/src/controllers/genai.controller.js index 45b616f7a1f62ec87bcece3733835a4ebd3b6373..c2c62c71dc322e7bf4d10741b06afc52901c403a 100644 --- a/src/controllers/genai.controller.js +++ b/src/controllers/genai.controller.js @@ -10,6 +10,7 @@ const config = require('../config/config'); const axios = require('axios'); const etasAuthorizationData = require('../states/etasAuthorization'); const moment = require('moment'); +const { setupClient } = require('../utils/setupEtasStream'); dotenv.config(); @@ -169,6 +170,8 @@ async function BedrockGenCode({ endpointURL, publicKey, secretKey, inputPrompt, } } } catch (error) { + console.log('Error in BedrockGenCode'); + console.log(error); try { const bedrockResponse = await bedrock.send(new InvokeModelCommand(input)); const response = JSON.parse(new TextDecoder().decode(bedrockResponse.body)); @@ -176,7 +179,7 @@ async function BedrockGenCode({ endpointURL, publicKey, secretKey, inputPrompt, generation = response.completions[0].data.text; } } catch (err) { - throw new Error('Failed to generate response from Bedrock, please check your endpoint URL, region, keys'); + throw err; } } return generation; @@ -251,8 +254,20 @@ const getAccessToken = async () => { } }; +const getInstance = (environment = 'prod') => { + switch (environment) { + case 'prod': + return config.etas.instanceEndpoint; + case 'dev': + return config.etas.developmentEndpoint; + default: + return config.etas.instanceEndpoint; + } +}; + const generateAIContent = async (req, res) => { try { + const { environment } = req.params; const { prompt } = req.body; const authorizationData = etasAuthorizationData.getAuthorizationData(); let token = authorizationData.accessToken; @@ -265,9 +280,9 @@ const generateAIContent = async (req, res) => { }); } - const instance = config.etas.instanceEndpoint; + const instance = getInstance(environment); - // console.log('ETAS_INSTANCE_ENDPOINT', instance); + setupClient(token); const response = await axios.post( `https://${instance}/r2mm/GENERATE_AI`, diff --git a/src/controllers/model.controller.js b/src/controllers/model.controller.js index 978f373e2cb0de3eb80553023a0f4fd7ab667d9a..c0bdf5100d32852270189e3ac60855126486ef81 100644 --- a/src/controllers/model.controller.js +++ b/src/controllers/model.controller.js @@ -1,27 +1,91 @@ const httpStatus = require('http-status'); -const { modelService, apiService, permissionService } = require('../services'); +const { modelService, apiService, permissionService, extendedApiService } = require('../services'); const catchAsync = require('../utils/catchAsync'); const pick = require('../utils/pick'); const ApiError = require('../utils/ApiError'); const { PERMISSIONS } = require('../config/roles'); +const logger = require('../config/logger'); const createModel = catchAsync(async (req, res) => { - const { cvi, custom_apis, ...reqBody } = req.body; + let { cvi, custom_apis, extended_apis, api_data_url, ...reqBody } = req.body; + + if (api_data_url) { + const result = await modelService.processApiDataUrl(api_data_url); + if (result) { + extended_apis = result.extended_apis || extended_apis; + reqBody.api_version = result.api_version || reqBody.api_version; + } + } + const model = await modelService.createModel(req.user.id, { ...reqBody, - ...(reqBody.custom_apis && { custom_apis: JSON.parse(reqBody.custom_apis) }), - }); - await apiService.createApi({ - model: model._id, - cvi: JSON.parse(cvi), - created_by: req.user.id, }); + try { + if (extended_apis) { + await Promise.all( + extended_apis.map((api) => + extendedApiService.createExtendedApi({ + model: model._id, + apiName: api.apiName, + description: api.description, + skeleton: api.skeleton, + tags: api.tags, + type: api.type, + datatype: api.datatype, + isWishlist: api.isWishlist || false, + }) + ) + ); + } + } catch (error) { + logger.warn(`Error in creating model (creating extended_apis): ${error}`); + } + + try { + if (custom_apis) { + let apis = custom_apis; + try { + apis = JSON.parse(custom_apis); + } catch (error) { + // Do nothing + } + + if (Array.isArray(apis)) { + await Promise.all( + apis.map((api) => + extendedApiService.createExtendedApi({ + model: model._id, + apiName: api.name || api.apiName || 'Vehicle', + description: api.description || '', + skeleton: api.skeleton || '{}', + tags: api.tags || [], + type: api.type || 'branch', + datatype: api.datatype || (api.type !== 'branch' ? 'string' : null), + isWishlist: api.isWishlist || false, + }) + ) + ); + } + } + } catch (error) { + logger.warn(`Error in creating model (creating extended_apis): ${error}`); + } + res.status(httpStatus.CREATED).send(model); }); const listModels = catchAsync(async (req, res) => { - const filter = pick(req.query, ['name', 'visibility', 'tenant_id', 'vehicle_category', 'main_api', 'id', 'created_by']); + const filter = pick(req.query, [ + 'name', + 'visibility', + 'state', + 'tenant_id', + 'vehicle_category', + 'main_api', + 'id', + 'created_by', + ]); const options = pick(req.query, ['sortBy', 'limit', 'page', 'fields']); const advanced = pick(req.query, ['is_contributor']); const models = await modelService.queryModels(filter, options, advanced, req.user?.id); @@ -50,7 +114,6 @@ const getModel = catchAsync(async (req, res) => { finalResult.contributors = contributors; finalResult.members = members; } - res.send(finalResult); }); @@ -94,6 +157,14 @@ const deleteAuthorizedUser = catchAsync(async (req, res) => { res.status(httpStatus.NO_CONTENT).send(); }); +const getComputedVSSApi = catchAsync(async (req, res) => { + if (!(await permissionService.canAccessModel(req.user?.id, req.params.id))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden'); + } + const data = await apiService.computeVSSApi(req.params.id); + res.send(data); +}); + module.exports = { createModel, listModels, @@ -102,4 +173,5 @@ module.exports = { deleteModel, addAuthorizedUser, deleteAuthorizedUser, + getComputedVSSApi, }; diff --git a/src/controllers/prototype.controller.js b/src/controllers/prototype.controller.js index e61540142322f348b04ee248ae245a9cdc224b14..e962df6fd62bb9d7b28282de3c63ddb3e2fa6ab1 100644 --- a/src/controllers/prototype.controller.js +++ b/src/controllers/prototype.controller.js @@ -14,6 +14,20 @@ const createPrototype = catchAsync(async (req, res) => { res.status(201).send(prototypeId); }); +const bulkCreatePrototypes = catchAsync(async (req, res) => { + const modelIds = new Set(req.body.map((prototype) => prototype.model_id)); + if (modelIds.size !== 1) { + throw new ApiError(httpStatus.BAD_REQUEST, 'All prototypes must belong to the same model'); + } + + if (!(await permissionService.hasPermission(req.user.id, PERMISSIONS.READ_MODEL, modelIds.values().next().value))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden'); + } + + const prototypeIds = await prototypeService.bulkCreatePrototypes(req.user.id, req.body); + res.status(201).send(prototypeIds); +}); + const listPrototypes = catchAsync(async (req, res) => { const readableModelIds = await permissionService.listReadableModelIds(req.user?.id); @@ -65,7 +79,7 @@ const executeCode = catchAsync(async (req, res) => { }); const listPopularPrototypes = catchAsync(async (req, res) => { - const prototypes = await prototypeService.listPopularPrototypes(req.user.id); + const prototypes = await prototypeService.listPopularPrototypes(req.user?.id); res.send(prototypes); }); @@ -78,4 +92,5 @@ module.exports = { listRecentPrototypes, listPopularPrototypes, executeCode, + bulkCreatePrototypes, }; diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 865dab58f0c289c5e631631ca460c45f33e939b4..9472b385069fa026ca0692435f02441f0a9e220d 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -14,7 +14,8 @@ const createUser = catchAsync(async (req, res) => { const getUsers = catchAsync(async (req, res) => { const filter = pick(req.query, ['name', 'role']); const options = pick(req.query, ['sortBy', 'limit', 'page']); - const advanced = pick(req.query, ['search', 'includeFullDetails']); + const advanced = pick(req.query, ['search', 'includeFullDetails', 'id']); + if (advanced.includeFullDetails) { // Check if has permission if (!(await permissionService.hasPermission(req.user?.id, PERMISSIONS.ADMIN))) { diff --git a/src/index.js b/src/index.js index dd72d881158a6d32839455687d6c89894fa8b92b..54664d15eb7690b8596688159e08309f6daca44f 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ const config = require('./config/config'); const logger = require('./config/logger'); const initializeRoles = require('./utils/initializeRoles'); const { init } = require('./config/socket'); +const setupScheduledCheck = require('./scripts/checkVSSUpdate'); let server; mongoose.connect(config.mongoose.url, config.mongoose.options).then(() => { @@ -40,3 +41,5 @@ process.on('SIGTERM', () => { server.close(); } }); + +setupScheduledCheck(); diff --git a/src/models/extendedApi.model.js b/src/models/extendedApi.model.js index 19da0b11b5966632b0888a443c8d3712c752de99..1ddde6857bd443610df86dd399e0d9cb1c5a6327 100644 --- a/src/models/extendedApi.model.js +++ b/src/models/extendedApi.model.js @@ -3,9 +3,15 @@ const { toJSON, paginate } = require('./plugins'); const tagSchema = mongoose.Schema( { - tagCategoryId: String, - tagCategoryName: String, - tag: String, + title: { + type: String, + required: true, + trim: true, + }, + description: { + type: String, + trim: true, + }, }, { _id: false, @@ -26,14 +32,21 @@ const extendedApiSchema = mongoose.Schema( skeleton: { type: String, }, + unit: { + type: String, + }, type: { type: String, }, - data_type: { + datatype: { type: String, }, description: String, tags: [tagSchema], + isWishlist: { + type: Boolean, + default: false, + }, }, { timestamps: true, diff --git a/src/models/model.model.js b/src/models/model.model.js index a427b7938f9ba1bd65a63174a76dea13e6be5f77..0dc112a07f7f9afd1d3814d88e160256b4347ce1 100644 --- a/src/models/model.model.js +++ b/src/models/model.model.js @@ -4,17 +4,14 @@ const { visibilityTypes } = require('../config/enums'); const tagSchema = mongoose.Schema( { - tag: { - type: String, - required: true, - }, - tagCategoryId: { + title: { type: String, required: true, + trim: true, }, - tagCategoryName: { + description: { type: String, - required: true, + trim: true, }, }, { @@ -50,6 +47,12 @@ const modelSchema = mongoose.Schema( required: true, enums: Object.values(visibilityTypes), }, + state: { + type: String, + default: 'draft', + trim: true, + maxLength: 255, + }, vehicle_category: { type: String, required: true, @@ -73,6 +76,11 @@ const modelSchema = mongoose.Schema( extend: { type: mongoose.SchemaTypes.Mixed, }, + api_version: { + type: String, + trim: true, + lowercase: true, + }, }, { timestamps: true, diff --git a/src/models/plugins/toJSON.plugin.js b/src/models/plugins/toJSON.plugin.js index d511c9db59253303a2f9b73b3986b1ce1f0636a4..97a7df2e74b560a6ade0c28ebea4517f27d5c701 100644 --- a/src/models/plugins/toJSON.plugin.js +++ b/src/models/plugins/toJSON.plugin.js @@ -31,7 +31,7 @@ const toJSON = (schema) => { ret.id = ret._id.toString(); delete ret._id; delete ret.__v; - // ret.created_at = ret.createdAt; + ret.created_at = ret.createdAt; delete ret.createdAt; delete ret.updatedAt; if (transform) { diff --git a/src/models/prototype.model.js b/src/models/prototype.model.js index 878efebdfe594c5869df15c397bc4cbebba44cc2..e172d3b430b18712e59a0ffc1a45c62570881b06 100644 --- a/src/models/prototype.model.js +++ b/src/models/prototype.model.js @@ -32,6 +32,9 @@ const descriptionSchema = mongoose.Schema( type: String, max: 255, }, + text: { + type: String, + }, }, { _id: false, @@ -51,17 +54,14 @@ const portfolioSchema = mongoose.Schema( const tagSchema = mongoose.Schema( { - tag: { - type: String, - required: true, - }, - tagCategoryId: { + title: { type: String, required: true, + trim: true, }, - tagCategoryName: { + description: { type: String, - required: true, + trim: true, }, }, { @@ -81,116 +81,131 @@ const ratingSchema = mongoose.Schema({ }, }); -const prototypeSchema = mongoose.Schema({ - apis: { - type: apiSchema, - default: { - VSC: [], - VSS: [], +const prototypeSchema = mongoose.Schema( + { + apis: { + type: apiSchema, + default: { + VSC: [], + VSS: [], + }, }, - }, - code: { - type: String, - required: true, - default: - 'from sdv_model import Vehicle import plugins from browser import aio vehicle = Vehicle() # write your code here', - }, - extend: { - type: mongoose.SchemaTypes.Mixed, - }, - complexity_level: { - type: Number, - required: true, - min: 1, - max: 5, - default: 3, - }, - customer_journey: { - type: String, - required: true, - default: - ' #Step 1 Who: Driver What: Wipers turned on manually Customer TouchPoints: Windshield wiper switch #Step 2 Who: User What: User opens the car door/trunk and the open status of door/trunk is set to true Customer TouchPoints: Door/trunk handle #Step 3 Who: System What: The wiping is immediately turned off by the software and user is notified Customer TouchPoints: Notification on car dashboard and mobile app ', - }, - description: { - type: descriptionSchema, - default: { - problem: '', - says_who: '', - solution: '', - status: '', + code: { + type: String, + default: + 'from sdv_model import Vehicle import plugins from browser import aio vehicle = Vehicle() # write your code here', }, - }, - image_file: { - type: String, - }, - journey_image_file: { - type: String, - }, - analysis_image_file: { - type: String, - }, - model_id: { - type: mongoose.SchemaTypes.ObjectId, - ref: 'Model', - required: true, - }, - name: { - type: String, - required: true, - trim: true, - maxLength: 255, - }, - portfolio: { - type: portfolioSchema, - default: { - effort_estimation: 0, - needs_addressed: 0, - relevance: 0, + extend: { + type: mongoose.SchemaTypes.Mixed, + }, + complexity_level: { + type: Number, + required: true, + min: 1, + max: 5, + default: 3, + }, + customer_journey: { + type: String, + required: true, + default: + ' #Step 1 Who: Driver What: Wipers turned on manually Customer TouchPoints: Windshield wiper switch #Step 2 Who: User What: User opens the car door/trunk and the open status of door/trunk is set to true Customer TouchPoints: Door/trunk handle #Step 3 Who: System What: The wiping is immediately turned off by the software and user is notified Customer TouchPoints: Notification on car dashboard and mobile app ', + }, + description: { + type: descriptionSchema, + default: { + problem: '', + says_who: '', + solution: '', + status: '', + }, + }, + image_file: { + type: String, + }, + journey_image_file: { + type: String, + }, + analysis_image_file: { + type: String, + }, + model_id: { + type: mongoose.SchemaTypes.ObjectId, + ref: 'Model', + required: true, + }, + name: { + type: String, + required: true, + trim: true, + maxLength: 255, + }, + portfolio: { + type: portfolioSchema, + default: { + effort_estimation: 0, + needs_addressed: 0, + relevance: 0, + }, + }, + skeleton: { + type: String, + }, + state: { + type: String, + required: true, + default: stateTypes.DEVELOPMENT, + enums: Object.values(stateTypes), + }, + tags: { + type: [tagSchema], + }, + widget_config: { + type: String, + }, + last_viewed: { + type: Date, + }, + rated_by: { + type: Map, + of: ratingSchema, + default: {}, + }, + autorun: { + type: Boolean, + default: false, + }, + related_ea_components: { + type: String, + }, + partner_logo: { + type: String, + }, + created_by: { + type: mongoose.SchemaTypes.ObjectId, + ref: 'User', + required: true, + }, + executed_turns: { + type: Number, + default: 0, + }, + language: { + type: String, + default: 'python', + maxLength: 20, + }, + requirements: { + type: String, + }, + flow: { + type: mongoose.SchemaTypes.Mixed, }, }, - skeleton: { - type: String, - }, - state: { - type: String, - required: true, - default: stateTypes.DEVELOPMENT, - enums: Object.values(stateTypes), - }, - tags: { - type: [tagSchema], - }, - widget_config: { - type: String, - }, - last_viewed: { - type: Date, - }, - rated_by: { - type: Map, - of: ratingSchema, - default: {}, - }, - autorun: { - type: Boolean, - default: false, - }, - related_ea_components: { - type: String, - }, - partner_logo: { - type: String, - }, - created_by: { - type: mongoose.SchemaTypes.ObjectId, - ref: 'User', - required: true, - }, - executed_turns: { - type: Number, - default: 0, - }, -}); + { + timestamps: true, + } +); // add plugin that converts mongoose to json prototypeSchema.plugin(toJSON); diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js index 585ad5e096fa5160944a8cfe15c40446bd9d1526..467c64c16084a315b5889ff8de8563a4bb1830cd 100644 --- a/src/routes/v1/index.js +++ b/src/routes/v1/index.js @@ -56,10 +56,10 @@ const defaultRoutes = [ path: '/plugins', route: pluginsRoute, }, - { - path: '/tags', - route: tagsRoute, - }, + // { + // path: '/tags', + // route: tagsRoute, + // }, { path: '/medias', route: mediasRoute, diff --git a/src/routes/v1/tag.route.js b/src/routes/v1/tag.route.js index ab174899e36d961fa36cbac367b8a398db9f5612..327a1f4dcdbe2194f0937b6d5fe47f2e0e0b7cde 100644 --- a/src/routes/v1/tag.route.js +++ b/src/routes/v1/tag.route.js @@ -6,12 +6,12 @@ const validate = require('../../middlewares/validate'); const router = express.Router(); -router.route('/').post(validate(tagValidation.createTag), tagController.createTag); -router - .route('/categories') - .get(validate(tagValidation.listTagCategories), tagController.listTagCategories) - .post(validate(tagValidation.createTagCategory), tagController.createTagCategory); -router.route('/categories/:id').put(validate(tagValidation.updateTagCategory), tagController.updateTagCategory); +// router.route('/').post(validate(tagValidation.createTag), tagController.createTag); +// router +// .route('/categories') +// .get(validate(tagValidation.listTagCategories), tagController.listTagCategories) +// .post(validate(tagValidation.createTagCategory), tagController.createTagCategory); +// router.route('/categories/:id').put(validate(tagValidation.updateTagCategory), tagController.updateTagCategory); // router.get('/categories/:tenantId', async (req, res) => { // try { diff --git a/src/routes/v2/api.route.js b/src/routes/v2/api.route.js index b9bd30b50f954b26f6175ba7408dfd9affe79736..b0874c84a4a4e36112840c71fb1419545059466b 100644 --- a/src/routes/v2/api.route.js +++ b/src/routes/v2/api.route.js @@ -7,6 +7,9 @@ const config = require('../../config/config'); const router = express.Router(); +router.route('/vss').get(apiController.listVSSVersions); +router.route('/vss/:name').get(validate(apiValidation.getVSSVersion), apiController.getVSSVersion); + router.route('/').post(auth(), validate(apiValidation.createApi), apiController.createApi); router .route('/:id') diff --git a/src/routes/v2/asset.route.js b/src/routes/v2/asset.route.js index b6324ef75d46c6d3ac533e139d58c6ff90bcbdb5..0987599c724d4e60fca274b8e98bb4b387eb929a 100644 --- a/src/routes/v2/asset.route.js +++ b/src/routes/v2/asset.route.js @@ -13,24 +13,28 @@ router .post(auth(), validate(assetValidation.createAsset), assetController.createAsset) .get(auth(), validate(assetValidation.getAssets), assetController.getAssets); +router + .route('/manage') + .get(auth(), checkPermission(PERMISSIONS.ADMIN), validate(assetValidation.getAssets), assetController.getAllAssets); + router .route('/:id') .get( auth(), - checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET), validate(assetValidation.getAsset), + checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET), assetController.getAsset ) .patch( auth(), - checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), validate(assetValidation.updateAsset), + checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), assetController.updateAsset ) .delete( auth(), - checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), validate(assetValidation.deleteAsset), + checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), assetController.deleteAsset ); @@ -38,8 +42,8 @@ router .route('/:id/generate-token') .post( auth(), - checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET), validate(assetValidation.generateToken), + checkPermission(PERMISSIONS.READ_ASSET, RESOURCES.ASSET), assetController.generateToken ); @@ -47,14 +51,14 @@ router .route('/:id/permissions') .post( auth(), - checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), validate(assetValidation.addAuthorizedUser), + checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), assetController.addAuthorizedUser ) .delete( auth(), - checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), validate(assetValidation.deleteAuthorizedUser), + checkPermission(PERMISSIONS.WRITE_ASSET, RESOURCES.ASSET), assetController.deleteAuthorizedUser ); diff --git a/src/routes/v2/auth.route.js b/src/routes/v2/auth.route.js index b4a9908b4f205fe7cdd4703ff0f102fa9d8d89b1..e5598ec0f25d76115ecc30bb4885801ac6ec7b1a 100644 --- a/src/routes/v2/auth.route.js +++ b/src/routes/v2/auth.route.js @@ -11,8 +11,8 @@ router.get('/github/callback', authController.githubCallback); router.post('/sso', validate(authValidation.sso), authController.sso); if (!config.strictAuth) { router.post('/register', validate(authValidation.register), authController.register); - router.post('/login', validate(authValidation.login), authController.login); } +router.post('/login', validate(authValidation.login), authController.login); router.post('/logout', authController.logout); router.post('/refresh-tokens', authController.refreshTokens); router.post('/forgot-password', validate(authValidation.forgotPassword), authController.forgotPassword); diff --git a/src/routes/v2/email.route.js b/src/routes/v2/email.route.js index 71a2bd454b160b43e3fe479e976e4ecfc480389f..a0dc24ed54b0cb323b7a3198ec208b4b03ec307d 100644 --- a/src/routes/v2/email.route.js +++ b/src/routes/v2/email.route.js @@ -7,21 +7,21 @@ const config = require('../../config/config'); const router = express.Router(); -if (config.env === 'development') { - router.route('').post( - validate(emailValidation.sendEmail), - catchAsync(async (req, res) => { - const { to, subject, content } = req.body; - let html; - try { - html = decodeURIComponent(content); - } catch (error) { - html = content; - } - await emailService.sendEmail(to, subject, html); - res.status(200).send(); - }) - ); -} +// if (config.env === 'development') { +router.route('').post( + validate(emailValidation.sendEmail), + catchAsync(async (req, res) => { + const { to, subject, content } = req.body; + let html; + try { + html = decodeURIComponent(content); + } catch (error) { + html = content; + } + await emailService.sendEmail(to, subject, html); + res.status(200).send(); + }) +); +// } module.exports = router; diff --git a/src/routes/v2/genai.route.js b/src/routes/v2/genai.route.js index d2af9a09bfb9385db632c67e028b2c3da64f3497..991d05c5432a52036802ef898f8a1ec690116120 100644 --- a/src/routes/v2/genai.route.js +++ b/src/routes/v2/genai.route.js @@ -33,6 +33,16 @@ router.post( auth({ optional: !config.strictAuth, }), + genaiPermission, + genaiController.generateAIContent +); + +router.post( + '/etas/:environment', + auth({ + optional: !config.strictAuth, + }), + genaiPermission, genaiController.generateAIContent ); diff --git a/src/routes/v2/model.route.js b/src/routes/v2/model.route.js index e64fcc3329c2a043d20405122649d39b0fd49536..322016f5348e2db77dfbdc0e9008742f6f7060bc 100644 --- a/src/routes/v2/model.route.js +++ b/src/routes/v2/model.route.js @@ -42,6 +42,14 @@ router modelController.deleteModel ); +router.route('/:id/api').get( + auth({ + optional: !config.strictAuth, + }), + validate(modelValidation.getApiByModelId), + modelController.getComputedVSSApi +); + router .route('/:id/permissions') .post( diff --git a/src/routes/v2/prototype.route.js b/src/routes/v2/prototype.route.js index ce01ca60d74ff107193d4a353462368c5f294e1f..f6add8371c43daf814c251d29fac3a4df4e889a3 100644 --- a/src/routes/v2/prototype.route.js +++ b/src/routes/v2/prototype.route.js @@ -20,6 +20,10 @@ router prototypeController.listPrototypes ); +router + .route('/bulk') + .post(auth(), validate(prototypeValidation.bulkCreatePrototypes), prototypeController.bulkCreatePrototypes); + router.route('/recent').get(auth(), prototypeController.listRecentPrototypes); router.route('/popular').get( auth({ diff --git a/src/scripts/checkVSSUpdate.js b/src/scripts/checkVSSUpdate.js new file mode 100644 index 0000000000000000000000000000000000000000..b3d8c537f6413f29f08ca4d413772efa96ed3dc7 --- /dev/null +++ b/src/scripts/checkVSSUpdate.js @@ -0,0 +1,92 @@ +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const moment = require('moment'); +const _ = require('lodash'); +const logger = require('../config/logger'); +const { fileService } = require('../services'); + +const setLastCheckTime = () => { + fs.writeFileSync(path.join(__dirname, '../../data/clock.txt'), moment().format()); +}; + +const getLastCheckTime = () => { + try { + const lastCheckTime = fs.readFileSync(path.join(__dirname, '../../data/clock.txt'), 'utf8'); + return moment(lastCheckTime); + } catch (error) { + return moment().subtract(1, 'hour'); + } +}; + +/** + * + * @param {{name: string; published_at: string; browser_download_url: string}[]} releases + */ +const updateVSS = async (releases) => { + try { + fs.writeFileSync(path.join(__dirname, '../../data/vss.json'), JSON.stringify(releases, null, 2)); + logger.info('Updated VSS version list'); + logger.info('Downloading VSS versions data'); + const promises = releases.map((release) => + fileService.downloadFile(release.browser_download_url, path.join(__dirname, `../../data/${release.name}.json`)) + ); + await Promise.all(promises); + logger.info('Downloaded VSS versions data'); + } catch (error) { + logger.error(error); + } +}; + +const checkUpdateVSS = async () => { + try { + const vssReleases = (await axios.get('https://api.github.com/repos/COVESA/vehicle_signal_specification/releases')).data; + setLastCheckTime(); + const regex = /v(\d+\.\d+)/; + + const filtered = vssReleases + ?.filter((release) => { + const match = release.name.match(regex); + if (match) { + return Number(match[1]) >= 3.0; + } + }) + ?.map((release) => { + return { + name: release.name, + published_at: release.published_at, + browser_download_url: release.assets?.find((asset) => asset.name.endsWith('.json'))?.browser_download_url, + }; + }); + + const vssFilePath = path.join(__dirname, '../../data/vss.json'); + + try { + if (fs.existsSync(vssFilePath) && _.isEqual(filtered, JSON.parse(fs.readFileSync(vssFilePath, 'utf8')))) return; + } catch (error) { + logger.warn(error); + } + + await updateVSS(filtered); + } catch (error) { + logger.error(error); + } +}; + +let interval = null; + +const setupScheduledCheck = () => { + const dataDirExist = fs.existsSync(path.join(__dirname, '../../data')); + if (!dataDirExist) { + fs.mkdirSync(path.join(__dirname, '../../data')); + } + const lastCheckTime = getLastCheckTime(); + if (moment().diff(lastCheckTime, 'seconds') > 120) { + checkUpdateVSS(); + } + if (!interval) { + interval = setInterval(checkUpdateVSS, 1000 * 60 * 60 * 24); + } +}; + +module.exports = setupScheduledCheck; diff --git a/src/scripts/migrate-api.js b/src/scripts/migrate-api.js new file mode 100644 index 0000000000000000000000000000000000000000..44b868864a051be80abe3e3744bbe1e610089630 --- /dev/null +++ b/src/scripts/migrate-api.js @@ -0,0 +1,48 @@ +const Client = require('mongodb').MongoClient; + +const connect = async (url) => { + const client = new Client(url, { useUnifiedTopology: true }); + await client.connect(); + return client.db(); +}; + +const main = async () => { + db = await connect('mongodb://etas-prod-playground-db:27017/playground-be'); + const models = await db + .collection('models') + .find({ + custom_apis: { + $exists: true, + }, + }) + .toArray(); + + await db.collection('models').updateMany({ api_version: { $exists: false } }, { $set: { api_version: 'v4.1' } }); + + const before = await db.collection('extendedapis').count(); + console.log('Before:', before); + const promises = []; + + models.forEach(async (model) => { + const custom_apis = model.custom_apis; + custom_apis.forEach(async (custom_api) => { + const newData = { + apiName: custom_api.name, + model: model._id, + skeleton: custom_api.skeleton || '{}', + tags: custom_api.tags || [], + type: custom_api.type || 'branch', + datatype: custom_api.datatype || (custom_api.type !== 'branch' ? 'string' : null), + description: custom_api.description || '', + isWishlist: true + }; + promises.push(db.collection('extendedapis').insertOne(newData)); + }); + }); + + await Promise.allSettled(promises).catch((err) => console.error(err)); + const after = await db.collection('extendedapis').count(); + console.log('after:', after); +}; + +main(); diff --git a/src/services/api.service.js b/src/services/api.service.js index b7da797f53e09b15c84600c73156956f3bef86fb..a71fcbbc8dd382332effee84d5a6a3333c2c17d4 100644 --- a/src/services/api.service.js +++ b/src/services/api.service.js @@ -1,7 +1,11 @@ const httpStatus = require('http-status'); const { userService } = require('.'); -const { Api } = require('../models'); +const { Api, Model, ExtendedApi } = require('../models'); const ApiError = require('../utils/ApiError'); +const fs = require('fs'); +const path = require('path'); +const logger = require('../config/logger'); +const { sortObject } = require('../utils/sort'); /** * @@ -78,10 +82,110 @@ const deleteApi = async (apiId, userId) => { await api.remove(); }; +const listVSSVersions = async () => { + let versions; + try { + const rawData = fs.readFileSync(path.join(__dirname, '../../data/vss.json')); + versions = rawData ? JSON.parse(rawData, 'utf8') : []; + } catch (error) { + logger.error(error); + versions = []; + } + return versions; +}; + +/** + * + * @param {string} name + * @returns {Promise<object>} + */ +const getVSSVersion = async (name) => { + const filePath = path.join(__dirname, `../../data/${name}.json`); + if (!fs.existsSync(filePath)) { + throw new ApiError(httpStatus.NOT_FOUND, 'VSS version not found'); + } + const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + return data; +}; + +/** + * + * @param {string} modelId + */ +const computeVSSApi = async (modelId) => { + const model = await Model.findById(modelId); + if (!model) { + throw new ApiError(httpStatus.NOT_FOUND, 'Model not found'); + } + let ret = null; + + const mainApi = model.main_api || 'Vehicle'; + const apiVersion = model.api_version; + if (!apiVersion) { + ret = { + [mainApi]: { + description: mainApi, + type: 'branch', + children: {}, + }, + }; + } else { + ret = await getVSSVersion(apiVersion); + } + + const extendedApis = await ExtendedApi.find({ + model: modelId, + isWishlist: true, + }); + extendedApis.forEach((extendedApi) => { + try { + const name = extendedApi.apiName.split('.').slice(1).join('.'); + if (!name) return; + ret[mainApi].children[name] = { + description: extendedApi.description, + type: extendedApi.type || 'branch', + id: extendedApi._id, + datatype: extendedApi.datatype, + name: extendedApi.apiName, + isWishlist: extendedApi.isWishlist, + }; + if (extendedApi.unit) { + ret[mainApi].children[name].unit = extendedApi.unit; + } + } catch (error) { + logger.warn(`Error while processing extended API ${extendedApi._id} with name ${extendedApi.apiName}: ${error}`); + } + }); + + try { + ret[mainApi].children = sortObject(ret[mainApi].children); + } catch (error) { + logger.warn(`Error while sorting object: ${error}`); + } + + // Nest parent/children apis + const len = Object.keys(ret[mainApi].children).length; + for (let i = len - 1; i >= 0; i--) { + const key = Object.keys(ret[mainApi].children)[i]; + const parent = key.split('.').slice(0, -1).join('.'); + if (parent && ret[mainApi].children[parent]) { + ret[mainApi].children[parent].children = ret[mainApi].children[parent].children || {}; + const childKey = key.replace(`${parent}.`, ''); + ret[mainApi].children[parent].children[childKey] = ret[mainApi].children[key]; + delete ret[mainApi].children[key]; + } + } + + return ret; +}; + module.exports = { createApi, getApi, getApiByModelId, updateApi, deleteApi, + listVSSVersions, + getVSSVersion, + computeVSSApi, }; diff --git a/src/services/asset.service.js b/src/services/asset.service.js index c74a836a930c7e7e4a29e423d0c154bd6f0935c8..10199d597bd9291d5e2d54c8fa093195119a5480 100644 --- a/src/services/asset.service.js +++ b/src/services/asset.service.js @@ -4,6 +4,8 @@ const { Role } = require('../models'); const Asset = require('../models/asset.model'); const ApiError = require('../utils/ApiError'); const httpStatus = require('http-status'); +const logger = require('../config/logger'); +const { isValidObjectId } = require('mongoose'); /** * @@ -21,14 +23,45 @@ const createAsset = (data) => { * @param {string} options.sortBy * @param {number} options.limit * @param {number} options.page + * @param {string} [options.userId] */ -const queryAssets = (filter, options) => { +const queryAssets = async (filter, options, userId) => { if (filter.name) { filter.name = new RegExp(filter.name, 'i'); } if (filter.type) { filter.type = new RegExp(filter.type, 'i'); } + if (userId) { + let accessibleIds = []; + try { + const roles = permissionService.getMappedRoles(await permissionService.getUserRoles(userId)); + roles?.forEach?.((value, key) => { + if (!Array.isArray(value)) { + logger.error(`Unexpected role value for ${key}: ${value}`); + } else if ( + (value.includes(PERMISSIONS.READ_ASSET) || value.includes(PERMISSIONS.WRITE_ASSET)) && + isValidObjectId(key) + ) { + accessibleIds.push(key); + } + }); + } catch (error) { + logger.error(`Error while find accessible assetIds for user ${userId}: ${error}`); + } + + filter.$or = [ + { + _id: { + $in: accessibleIds, + }, + }, + { + created_by: userId, + }, + ]; + } + return Asset.paginate(filter, options); }; diff --git a/src/services/auth.service.js b/src/services/auth.service.js index 70bfbecf7ea18c4954fd53024aced5b2ede1388c..97875c1d1a2e8df24735b5d2d0319230a6dc00f0 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -52,6 +52,9 @@ const loginUserWithEmailAndPassword = async (email, password) => { if (!user || !(await user.isPasswordMatch(password))) { throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password'); } + if (user.provider_user_id && !user.password) { + throw new ApiError(httpStatus.BAD_REQUEST, 'Please login with SSO'); + } return user; }; @@ -100,17 +103,14 @@ const refreshAuth = async (refreshToken) => { * @returns {Promise} */ const resetPassword = async (resetPasswordToken, newPassword) => { - try { - const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD); - const user = await userService.getUserById(resetPasswordTokenDoc.user); - if (!user) { - throw new Error(); - } - await userService.updateUserById(user.id, { password: newPassword }); - await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD }); - } catch (error) { - throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed'); + const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD); + const user = await userService.getUserById(resetPasswordTokenDoc.user, true); + if (!user) { + throw new ApiError(httpStatus.BAD_REQUEST, 'Password reset failed'); } + await userService.updateUserById(user.id, { password: newPassword }); + await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD }); + return user; }; /** diff --git a/src/services/email.service.js b/src/services/email.service.js index 18ee8fe9300c031850013f2865693fb2f165463c..9110ccdfdfe3154bbfda978f93e9feb6272741f8 100644 --- a/src/services/email.service.js +++ b/src/services/email.service.js @@ -40,10 +40,10 @@ const sendEmail = async (to, subject, html) => { * @param {string} token * @returns {Promise} */ -const sendResetPasswordEmail = async (to, token) => { +const sendResetPasswordEmail = async (to, token, domain) => { const subject = 'Reset password'; // replace this url with the link to the reset password page of your front-end app - const resetPasswordUrl = `${config.client.baseUrl}/reset-password?token=${token}`; + const resetPasswordUrl = `${domain || config.client.baseUrl}/reset-password?token=${token}`; const html = resetPasswordTemplate(to, resetPasswordUrl); await sendEmail(to, subject, html); }; diff --git a/src/services/extendedApi.service.js b/src/services/extendedApi.service.js index 95ea869fc7683a62f791076318f6cc199012da52..264bb5c1f4016eace709720b70670253d2287df5 100644 --- a/src/services/extendedApi.service.js +++ b/src/services/extendedApi.service.js @@ -1,6 +1,8 @@ const httpStatus = require('http-status'); const { ExtendedApi } = require('../models'); const ApiError = require('../utils/ApiError'); +const { permissionService } = require('.'); +const { PERMISSIONS } = require('../config/roles'); /** * Create a new ExtendedApi @@ -8,6 +10,10 @@ const ApiError = require('../utils/ApiError'); * @returns {Promise<ExtendedApi>} */ const createExtendedApi = async (extendedApiBody) => { + const existingExtendedApi = await ExtendedApi.findOne({ apiName: extendedApiBody.apiName, model: extendedApiBody.model }); + if (existingExtendedApi) { + throw new ApiError(httpStatus.BAD_REQUEST, 'An extended API with the same name already exists for this model'); + } return ExtendedApi.create(extendedApiBody); }; @@ -38,13 +44,17 @@ const getExtendedApiById = async (id) => { * Update ExtendedApi by id * @param {ObjectId} extendedApiId * @param {Object} updateBody + * @param {string} userId * @returns {Promise<ExtendedApi>} */ -const updateExtendedApiById = async (extendedApiId, updateBody) => { +const updateExtendedApiById = async (extendedApiId, updateBody, userId) => { const extendedApi = await getExtendedApiById(extendedApiId); if (!extendedApi) { throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found'); } + if (!(await permissionService.hasPermission(userId, PERMISSIONS.WRITE_MODEL, extendedApi.model))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model'); + } Object.assign(extendedApi, updateBody); await extendedApi.save(); return extendedApi; @@ -53,13 +63,17 @@ const updateExtendedApiById = async (extendedApiId, updateBody) => { /** * Delete ExtendedApi by id * @param {ObjectId} extendedApiId + * @param {string} userId * @returns {Promise<ExtendedApi>} */ -const deleteExtendedApiById = async (extendedApiId) => { +const deleteExtendedApiById = async (extendedApiId, userId) => { const extendedApi = await getExtendedApiById(extendedApiId); if (!extendedApi) { throw new ApiError(httpStatus.NOT_FOUND, 'ExtendedApi not found'); } + if (!(await permissionService.hasPermission(userId, PERMISSIONS.WRITE_MODEL, extendedApi.model))) { + throw new ApiError(httpStatus.FORBIDDEN, 'Forbidden. You do not have permission to update extended API for this model'); + } await extendedApi.remove(); return extendedApi; }; diff --git a/src/services/file.service.js b/src/services/file.service.js index 3c49c71c3a55f21344510c4eac06f2f37e4f4fdb..fdcc13f86a542f027f54176d5aad39181aed8049 100644 --- a/src/services/file.service.js +++ b/src/services/file.service.js @@ -3,6 +3,7 @@ const ApiError = require('../utils/ApiError'); const httpStatus = require('http-status'); const logger = require('../config/logger'); const config = require('../config/config'); +const fs = require('fs'); /** * @@ -47,6 +48,16 @@ const getFileFromURL = async (url, encoding = 'File') => { } }; +const downloadFile = async (url, path) => { + try { + const arrayBuffer = await (await fetch(url)).arrayBuffer(); + fs.writeFileSync(path, new Uint8Array(arrayBuffer)); + } catch (error) { + logger.error(`Failed to download file from ${url}`); + logger.error(error); + } +}; + /** * * @param {File} file1 @@ -57,4 +68,5 @@ const compareImages = async (file1, file2) => {}; module.exports = { upload, getFileFromURL, + downloadFile, }; diff --git a/src/services/index.js b/src/services/index.js index 9e1107f585164b0c2f6983ff84d2c2d7920fa7bb..84926b5270805dd23c6557c1d909105574c8c075 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -18,3 +18,4 @@ module.exports.searchService = require('./search.service'); module.exports.certivityService = require('./certivity.service'); module.exports.assetService = require('./asset.service'); module.exports.fileService = require('./file.service'); +module.exports.logService = require('./log.service'); diff --git a/src/services/listener.service.js b/src/services/listener.service.js index ef9d8529a7776bd9cf756916503d4319e6869814..eec861b4a3226c9a59dbb21e1cf27763b2f802a4 100644 --- a/src/services/listener.service.js +++ b/src/services/listener.service.js @@ -4,7 +4,11 @@ const findSocketByUser = (userId) => { const io = getIO(); let socket = null; io.sockets.sockets.forEach((value) => { - if (String(value.request._query.userId) === String(userId)) { + const user = value.user; + if (!user) { + return; + } + if (String(user._id) === String(userId)) { socket = value; } }); diff --git a/src/services/log.service.js b/src/services/log.service.js new file mode 100644 index 0000000000000000000000000000000000000000..171ed24ddc2b1ba211202a4f56629f69a8484fb4 --- /dev/null +++ b/src/services/log.service.js @@ -0,0 +1,24 @@ +const { logAxios } = require('../config/axios'); + +/** + * + * @param {{ + * name: string; + * type: string; + * created_by: string; + * ref_type?: string; + * ref_id?: string; + * parent_id?: string + * project_id?: string + * image?: string + * description?: string + * }} message + * @returns + */ +const createLog = async (message, options) => { + return (await logAxios.post('/', message, options)).data; +}; + +module.exports = { + createLog, +}; diff --git a/src/services/model.service.js b/src/services/model.service.js index dc3f2cf4d72978d563159f9429427aef57cb2506..7d540f7e2038f04bf60b390dd892e0b917783602 100644 --- a/src/services/model.service.js +++ b/src/services/model.service.js @@ -1,10 +1,13 @@ const httpStatus = require('http-status'); const { userService } = require('.'); +const prototypeService = require('./prototype.service'); const permissionService = require('./permission.service'); const { Model, Role } = require('../models'); const ApiError = require('../utils/ApiError'); const { PERMISSIONS } = require('../config/roles'); const mongoose = require('mongoose'); +const logger = require('../config/logger'); +const _ = require('lodash'); /** * @@ -236,6 +239,7 @@ const deleteModelById = async (id, userId) => { } await model.remove(); + await prototypeService.deleteMany({ model_id: id }); }; /** @@ -319,19 +323,65 @@ const getAccessibleModels = async (userId) => { /** * - * @param {string} userId - * @returns {Promise<string[]>} + * @param {object} api + * @returns {object} */ -const listReadableModelIds = async (userId) => {}; - -module.exports = { - createModel, - getModels, - queryModels, - getModelById, - updateModelById, - deleteModelById, - addAuthorizedUser, - deleteAuthorizedUser, - getAccessibleModels, +const convertToExtendedApiFormat = (api) => { + const { name, ...rest } = api; + return { + apiName: name, + ...rest, + }; }; + +/** + * + * @param {string} apiDataUrl + * @returns {Promise<{api_version: string; extended_apis: any[]} | undefined>} + */ +const processApiDataUrl = async (apiDataUrl) => { + try { + const response = await fetch(apiDataUrl); + const data = await response.json(); + const wishlist = []; + + const mainApi = Object.keys(data).at(0) || 'Vehicle'; + + Object.entries(data[mainApi].children).forEach(([key, value]) => { + if (value.isWishlist) { + wishlist.push(convertToExtendedApiFormat(data[mainApi].children[key])); + delete data[mainApi].children[key]; + } + }); + + const result = {}; + if (wishlist.length > 0) { + result.extended_apis = wishlist; + } + + const versionList = require('../../data/vss.json'); + for (const version of versionList) { + const file = require(`../../data/${version.name}.json`); + const isEqual = _.isEqual(file, data); + if (isEqual) { + result.api_version = version.name; + break; + } + } + + return result; + } catch (error) { + logger.warn(`Error in processing api data url: ${error}`); + } +}; + +module.exports.createModel = createModel; +module.exports.getModels = getModels; +module.exports.queryModels = queryModels; +module.exports.getModelById = getModelById; +module.exports.updateModelById = updateModelById; +module.exports.deleteModelById = deleteModelById; +module.exports.addAuthorizedUser = addAuthorizedUser; +module.exports.deleteAuthorizedUser = deleteAuthorizedUser; +module.exports.getAccessibleModels = getAccessibleModels; +module.exports.processApiDataUrl = processApiDataUrl; diff --git a/src/services/permission.service.js b/src/services/permission.service.js index d9e914c3105d402e4648664259f7711c964427af..499246387ebac11fc5b4dd2ce9147f2e30a1a9d1 100644 --- a/src/services/permission.service.js +++ b/src/services/permission.service.js @@ -121,12 +121,12 @@ const getMappedRoles = (roles) => { if (!Array.isArray(existingRolePermissions)) { existingRolePermissions = []; } else { - const newArray = existingRolePermissions.concat(role.role.permissions); + const newArray = existingRolePermissions.concat(role?.role?.permissions || []); existingRolePermissions = Array.from(new Set(newArray)); } map.set(roleRef, existingRolePermissions); } else { - map.set(roleRef, role.role.permissions); + map.set(roleRef, role?.role?.permissions || []); } }); return map; @@ -135,7 +135,9 @@ const getMappedRoles = (roles) => { // Check if the role map contains the permission const containsPermission = (roleMap, permission, id) => { const stringId = String(id); + // In case user is admin, have access to all type of resources const firstCondition = roleMap.has('*') && roleMap.get('*').includes(permission); + // In case user has access to specific resource const secondCondition = roleMap.has(stringId) && roleMap.get(stringId).includes(permission); return firstCondition || secondCondition; }; @@ -154,7 +156,7 @@ const checkModelPermission = (model, userId, permission) => { }; const checkPrototypePermission = (prototype, userId, permission) => { - if (String(prototype.created_by) === String(userId)) { + if (String(prototype.created_by) === String(userId) || String(prototype.model_id?.created_by) === String(userId)) { return true; } @@ -256,6 +258,21 @@ const listReadableModelIds = async (userId) => { return Array.from(results); }; +/** + * + * @param {string} userId + * @param {string} modelId + * @returns {Promise<boolean>} + */ +const canAccessModel = async (userId, modelId) => { + const model = await Model.findById(modelId); + if (!model) { + throw new ApiError(httpStatus.NOT_FOUND, 'Model not found'); + } + if (model.visibility === 'public') return true; + return hasPermission(userId, PERMISSIONS.READ_MODEL, modelId); +}; + module.exports = { listAuthorizedUser, assignRoleToUser, @@ -268,4 +285,5 @@ module.exports = { getRoles, getPermissions, listReadableModelIds, + canAccessModel, }; diff --git a/src/services/prototype.service.js b/src/services/prototype.service.js index 6014f45c20c1266219a26b48348e6bb4aa2e5bba..d2c331cbfd9bcbceef68d7af3f6df702a1f3ec26 100644 --- a/src/services/prototype.service.js +++ b/src/services/prototype.service.js @@ -7,6 +7,7 @@ const { default: axios, isAxiosError } = require('axios'); const config = require('../config/config'); const logger = require('../config/logger'); const modelService = require('./model.service'); +const _ = require('lodash'); /** * @@ -29,6 +30,31 @@ const createPrototype = async (userId, prototypeBody) => { return prototype; }; +/** + * + * @param {string} userId + * @param {Object[]} prototypes + * @returns {Promise<string>}create + */ +const bulkCreatePrototypes = async (userId, prototypes) => { + for (const prototype of prototypes) { + if (await Prototype.existsPrototypeInModel(prototype.model_id, prototype.name)) { + throw new ApiError( + httpStatus.BAD_REQUEST, + `Duplicate prototype name '${prototype.name}' in model ${prototype.model_id}` + ); + } + } + + const data = await Prototype.insertMany( + prototypes.map((prototype) => ({ + ...prototype, + created_by: userId, + })) + ); + return data.map((item) => item._id); +}; + /** * Query for users * @param {Object} filter - Mongo filter @@ -149,7 +175,8 @@ const listRecentPrototypes = async (userId) => { const prototypes = await Prototype.find({ _id: { $in: Array.from(prototypeMap.keys()) } }) .select('name model_id description image_file executed_turns') - .populate('model', 'name visibility'); + .populate('model', 'name visibility') + .populate('created_by', 'name image_file'); const results = []; recentData.forEach((data) => { @@ -189,20 +216,33 @@ const listPopularPrototypes = async () => { ).map((model) => String(model._id)); return Prototype.find({ model_id: { $in: publicModelIds }, + state: 'Released', }) .sort({ executed_turns: -1 }) .limit(8) .select('name model_id description image_file executed_turns') - .populate('model', 'name visibility'); + .populate('model', 'name visibility') + .populate('created_by', 'name image_file'); }; -module.exports = { - createPrototype, - queryPrototypes, - getPrototypeById, - updatePrototypeById, - deletePrototypeById, - listRecentPrototypes, - executeCode, - listPopularPrototypes, +/** + * + * @param {object} filter + */ +const deleteMany = async (filter) => { + if (_.isEmpty(filter)) { + throw new Error('Filter is required'); + } + await Prototype.deleteMany(filter); }; + +module.exports.createPrototype = createPrototype; +module.exports.queryPrototypes = queryPrototypes; +module.exports.getPrototypeById = getPrototypeById; +module.exports.updatePrototypeById = updatePrototypeById; +module.exports.deletePrototypeById = deletePrototypeById; +module.exports.listRecentPrototypes = listRecentPrototypes; +module.exports.executeCode = executeCode; +module.exports.listPopularPrototypes = listPopularPrototypes; +module.exports.bulkCreatePrototypes = bulkCreatePrototypes; +module.exports.deleteMany = deleteMany; diff --git a/src/services/user.service.js b/src/services/user.service.js index 4493a5c32080675da40e54e53b0ff81b9da379f4..1ee016f3a6916cc1c343ea9dac02e8f453cc337f 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -3,6 +3,8 @@ const { User } = require('../models'); const ApiError = require('../utils/ApiError'); const image = require('../utils/image'); const fileService = require('./file.service'); +const logger = require('../config/logger'); +const { isValidObjectId } = require('mongoose'); /** * Create a user @@ -26,9 +28,23 @@ const createUser = async (userBody) => { * @param {Object} advanced - Advanced search options * @param {string} [advanced.search] - Full text search * @param {string} [advanced.includeFullDetails] - Whether to include full user details or not + * @param {string} [advanced.id] - Whether to filter users by id * @returns {Promise<QueryResult>} */ const queryUsers = async (filter, options, advanced) => { + if (advanced.id) { + const ids = advanced.id.split(','); + for (const id of ids) { + if (!isValidObjectId(id)) { + throw new ApiError(httpStatus.BAD_REQUEST, `Invalid id ${id}`); + } + } + filter = { + ...filter, + _id: { $in: ids }, + }; + } + if (advanced.search) { filter = { $and: [ @@ -140,13 +156,18 @@ const updateSSOUser = async (user, graphData) => { updateBody.name = graphData.displayName; } - if (userPhoto) { - const photoBuffer = await userPhoto.arrayBuffer(); - const diff = await image.diff(user?.image_file, photoBuffer); - if (diff > 0.1 || diff === -1) { - const { url } = await fileService.upload(userPhoto); - updateBody.image_file = url; + try { + if (userPhoto) { + const photoBuffer = await userPhoto.arrayBuffer(); + const diff = await image.diff(user?.image_file, photoBuffer); + if (diff > 0.1 || diff === -1) { + const { url } = await fileService.upload(userPhoto); + updateBody.image_file = url; + } } + } catch (error) { + logger.error('Error updating user photo'); + logger.error(error); } if (Object.keys(updateBody).length === 0) { @@ -170,9 +191,14 @@ const createSSOUser = async (graphData) => { provider_user_id: graphData.id, }; - if (userPhoto) { - const { url } = await fileService.upload(userPhoto); - userBody.image_file = url; + try { + if (userPhoto) { + const { url } = await fileService.upload(userPhoto); + userBody.image_file = url; + } + } catch (error) { + logger.error('Error creating user photo'); + logger.error(error); } return createUser(userBody); diff --git a/src/utils/emailTemplates.js b/src/utils/emailTemplates.js index e6e1194178d53e9dd1a279cbe8f76b56a1fa21b2..e0973eafed7aa724ced0db3abda31d20fa1f45e6 100644 --- a/src/utils/emailTemplates.js +++ b/src/utils/emailTemplates.js @@ -42,9 +42,12 @@ const resetPasswordTemplate = (fullName, link) => ` display:block; " href="${link}">Reset Password</a> </button> + <p> + or <a href="${link}">click here</a> to reset password if you cannot open the page. + </p> <p> If you did not reset your password, you should visit - <a style="text-decoration: none" href="#">your recent accesses</a> + <a href="#">your recent accesses</a> to this account. </p> <p class="author" style="font-size:0.875rem;font-weight:600;">digital.auto</p> diff --git a/src/utils/setupEtasStream.js b/src/utils/setupEtasStream.js new file mode 100644 index 0000000000000000000000000000000000000000..4e3e1942965098cde9fbfa3480c9ffe52130348c --- /dev/null +++ b/src/utils/setupEtasStream.js @@ -0,0 +1,36 @@ +const WebSocketClient = require('websocket').client; +const { getIO } = require('../config/socket'); +const config = require('../config/config.js'); +let client = null; + +const setupClient = (token) => { + if (!client) { + const client = new WebSocketClient(); + + client.on('connectFailed', function (error) { + console.log('Connect etas websocket error: ' + error.toString()); + }); + client.on('connect', function (connection) { + console.log('Etas WebSocket Client Connected'); + connection.on('error', function (error) { + console.log('Connection Error: ' + error.toString()); + }); + connection.on('close', function () { + console.log('echo-protocol Connection Closed'); + }); + connection.on('message', function (message) { + const io = getIO(); + console.log(message.utf8Data); + io?.sockets?.emit('etas-stream', message.utf8Data); + }); + }); + + client.connect(`wss://${config.etas.instanceEndpoint}/ws`, null, null, { + authorization: `Bearer ${token}`, + }); + } +}; + +module.exports = { + setupClient, +}; diff --git a/src/utils/sort.js b/src/utils/sort.js new file mode 100644 index 0000000000000000000000000000000000000000..a5e37d4873cf595c0c72384e6ea3b5b2aa728454 --- /dev/null +++ b/src/utils/sort.js @@ -0,0 +1,12 @@ +const sortObject = (obj) => { + return Object.keys(obj) + .sort() + .reduce((acc, key) => { + acc[key] = obj[key]; + return acc; + }, {}); +}; + +module.exports = { + sortObject, +}; diff --git a/src/validations/api.validation.js b/src/validations/api.validation.js index 862cd5db6ef865650167f2861c8d98839469c887..be0edfe190ecd27763cd12b567df31170a72f8a8 100644 --- a/src/validations/api.validation.js +++ b/src/validations/api.validation.js @@ -38,10 +38,17 @@ const deleteApi = { }), }; +const getVSSVersion = { + params: Joi.object().keys({ + name: Joi.string().required(), + }), +}; + module.exports = { createApi, getApi, getApiByModelId, updateApi, deleteApi, + getVSSVersion, }; diff --git a/src/validations/auth.validation.js b/src/validations/auth.validation.js index 92787640bd2c38f8f577194501dcc5867c921ffe..78560c66dc14eb3ac52a6068d641165a7ca9e7c4 100644 --- a/src/validations/auth.validation.js +++ b/src/validations/auth.validation.js @@ -25,6 +25,9 @@ const refreshTokens = { }; const forgotPassword = { + query: Joi.object().keys({ + return_raw_token: Joi.boolean().default(false), + }), body: Joi.object().keys({ email: Joi.string().email().required(), }), diff --git a/src/validations/discussion.validation.js b/src/validations/discussion.validation.js index 1fc48db1d5844fb3527e89a39e8da046625a1ae7..5315e98262f716c1664910bd7fdbf4ee557af34f 100644 --- a/src/validations/discussion.validation.js +++ b/src/validations/discussion.validation.js @@ -1,5 +1,6 @@ const Joi = require('joi'); const { objectId } = require('./custom.validation'); +const { populate } = require('../models/token.model'); const createDiscussion = { body: Joi.object().keys({ @@ -20,6 +21,7 @@ const listDiscussions = { limit: Joi.number().integer(), page: Joi.number().integer(), fields: Joi.string(), + populate: Joi.any(), }), }; diff --git a/src/validations/extendedApi.validation.js b/src/validations/extendedApi.validation.js index e589b7ed6e2fa51206f911d64f0ce5b508b940cd..8fc0b4437c724b6dbb9b3a687ac34ef5d44ee180 100644 --- a/src/validations/extendedApi.validation.js +++ b/src/validations/extendedApi.validation.js @@ -7,24 +7,23 @@ const createExtendedApi = { model: Joi.string().custom(objectId).required(), skeleton: Joi.string().optional(), type: Joi.string(), - data_type: Joi.string(), - description: Joi.string(), - tags: Joi.array() - .items( - Joi.object({ - tagCategoryId: Joi.string(), - tagCategoryName: Joi.string(), - tag: Joi.string(), - }) - ) - .optional(), + datatype: Joi.string(), + description: Joi.string().allow('').default(''), + tags: Joi.array().items( + Joi.object().keys({ + title: Joi.string().required(), + description: Joi.string().allow(''), + }) + ), + isWishlist: Joi.boolean().default(false), + unit: Joi.string(), }), }; const getExtendedApis = { query: Joi.object().keys({ apiName: Joi.string(), - model: Joi.string().custom(objectId), + model: Joi.string().custom(objectId).required(), sortBy: Joi.string(), limit: Joi.number().integer(), page: Joi.number().integer(), @@ -46,20 +45,17 @@ const updateExtendedApi = { apiName: Joi.string() .regex(/^Vehicle\./) .message('apiName must start with Vehicle.'), - model: Joi.string().custom(objectId), skeleton: Joi.string().optional(), type: Joi.string(), - data_type: Joi.string(), - description: Joi.string(), - tags: Joi.array() - .items( - Joi.object({ - tagCategoryId: Joi.string(), - tagCategoryName: Joi.string(), - tag: Joi.string(), - }) - ) - .optional(), + datatype: Joi.string(), + description: Joi.string().allow(''), + tags: Joi.array().items( + Joi.object().keys({ + title: Joi.string().required(), + description: Joi.string().allow(''), + }) + ), + isWishlist: Joi.boolean(), }) .min(1), }; diff --git a/src/validations/model.validation.js b/src/validations/model.validation.js index adac0ac8e5a170228308854145feeb6bb23ea23a..47b1f5beccfe75f008f2e2c75a15ea0f1ca576bc 100644 --- a/src/validations/model.validation.js +++ b/src/validations/model.validation.js @@ -6,7 +6,10 @@ const createModel = { body: Joi.object().keys({ extend: Joi.any(), custom_apis: Joi.string().custom(jsonString), - cvi: Joi.string().required().custom(jsonString), + api_version: Joi.string(), + api_data_url: Joi.string(), + cvi: Joi.string().custom(jsonString), + extended_apis: Joi.array().items(Joi.any()), main_api: Joi.string().required().max(255), model_home_image_file: Joi.string() .allow('') @@ -23,11 +26,11 @@ const createModel = { skeleton: Joi.string().custom(jsonString), tags: Joi.array().items( Joi.object().keys({ - tag: Joi.string().required(), - tagCategoryId: Joi.string().required().custom(slug), - tagCategoryName: Joi.string().required(), + title: Joi.string().required(), + description: Joi.string().allow(''), }) ), + state: Joi.string().max(255).default('draft'), }), }; @@ -53,6 +56,7 @@ const updateModel = { .keys({ extend: Joi.any(), custom_apis: Joi.string().custom(jsonString), + api_version: Joi.string(), cvi: Joi.string().custom(jsonString), main_api: Joi.string().max(255), model_home_image_file: Joi.string().allow(''), @@ -64,11 +68,11 @@ const updateModel = { skeleton: Joi.string().custom(jsonString), tags: Joi.array().items( Joi.object().keys({ - tag: Joi.string().required(), - tagCategoryId: Joi.string().required().custom(slug), - tagCategoryName: Joi.string().required(), + title: Joi.string().required(), + description: Joi.string().allow(''), }) ), + state: Joi.string().max(255), }) .min(1), params: Joi.object().keys({ @@ -108,6 +112,12 @@ const deleteAuthorizedUser = { }), }; +const getApiByModelId = { + params: Joi.object().keys({ + id: Joi.string().custom(objectId), + }), +}; + module.exports = { createModel, listModels, @@ -116,4 +126,5 @@ module.exports = { deleteModel, addAuthorizedUser, deleteAuthorizedUser, + getApiByModelId, }; diff --git a/src/validations/prototype.validation.js b/src/validations/prototype.validation.js index e8f5bee45d1f69704cf9395bae9453842d34bfe4..2e83c2c6b67c8649d255055354c0992fe22c33ed 100644 --- a/src/validations/prototype.validation.js +++ b/src/validations/prototype.validation.js @@ -2,54 +2,55 @@ const Joi = require('joi'); const { stateTypes } = require('../config/enums'); const { objectId, jsonString, slug } = require('./custom.validation'); -const createPrototype = { - body: Joi.object().keys({ - extend: Joi.any(), - state: Joi.string().allow(...Object.values(stateTypes)), - apis: Joi.object().keys({ - VSC: Joi.array().items(Joi.string()), - VSS: Joi.array().items(Joi.string()), - }), - code: Joi.string().allow(''), - complexity_level: Joi.number().min(1).max(5), - customer_journey: Joi.string().allow(''), - description: Joi.object().keys({ - problem: Joi.string().allow('').max(4095), - says_who: Joi.string().allow('').max(4095), - solution: Joi.string().allow('').max(4095), - status: Joi.string().allow('').max(255), - }), - image_file: Joi.string().allow(''), - journey_image_file: Joi.string().allow(''), - analysis_image_file: Joi.string().allow(''), - model_id: Joi.string().required().custom(objectId), - name: Joi.string().required().max(255), - portfolio: Joi.object().keys({ - effort_estimation: Joi.number(), - needs_addressed: Joi.number(), - relevance: Joi.number(), - }), - skeleton: Joi.string().custom(jsonString), - tags: Joi.array().items( - Joi.object().keys({ - tag: Joi.string().required(), - tagCategoryId: Joi.string().required().custom(slug), - tagCategoryName: Joi.string().required(), - }) - ), - widget_config: Joi.string().custom(jsonString), - autorun: Joi.boolean(), - related_ea_components: Joi.string().allow(''), - partner_logo: Joi.string().allow(''), - // rated_by: Joi.object().pattern( - // /^[0-9a-fA-F]{24}$/, - // Joi.object() - // .required() - // .keys({ - // rating: Joi.number().min(1).max(5), - // }) - // ), +const bodyValidation = Joi.object().keys({ + extend: Joi.any(), + flow: Joi.any(), + state: Joi.string().allow(...Object.values(stateTypes)), + apis: Joi.object().keys({ + VSC: Joi.array().items(Joi.string()), + VSS: Joi.array().items(Joi.string()), }), + code: Joi.string().allow(''), + complexity_level: Joi.number().min(1).max(5), + customer_journey: Joi.string().allow(''), + description: Joi.object().keys({ + problem: Joi.string().allow('').max(4095), + says_who: Joi.string().allow('').max(4095), + solution: Joi.string().allow('').max(4095), + status: Joi.string().allow('').max(255), + text: Joi.string().allow(''), + }), + image_file: Joi.string().allow(''), + journey_image_file: Joi.string().allow(''), + analysis_image_file: Joi.string().allow(''), + model_id: Joi.string().required().custom(objectId), + name: Joi.string().required().max(255), + portfolio: Joi.object().keys({ + effort_estimation: Joi.number(), + needs_addressed: Joi.number(), + relevance: Joi.number(), + }), + skeleton: Joi.string().custom(jsonString), + tags: Joi.array().items( + Joi.object().keys({ + title: Joi.string().required(), + description: Joi.string().allow(''), + }) + ), + widget_config: Joi.string().custom(jsonString), + autorun: Joi.boolean(), + related_ea_components: Joi.string().allow(''), + partner_logo: Joi.string().allow(''), + language: Joi.string().default('python'), + requirements: Joi.string().allow(''), +}); + +const createPrototype = { + body: bodyValidation, +}; + +const bulkCreatePrototypes = { + body: Joi.array().items(bodyValidation).min(1), }; const listPrototypes = { @@ -76,6 +77,7 @@ const getPrototype = { const updatePrototype = { body: Joi.object().keys({ + flow: Joi.any(), extend: Joi.any(), state: Joi.string().allow(...Object.values(stateTypes)), apis: Joi.object().keys({ @@ -90,6 +92,7 @@ const updatePrototype = { says_who: Joi.string().allow('').max(4095), solution: Joi.string().allow('').max(4095), status: Joi.string().allow('').max(255), + text: Joi.string().allow(''), }), image_file: Joi.string().allow(''), journey_image_file: Joi.string().allow(''), @@ -103,15 +106,16 @@ const updatePrototype = { skeleton: Joi.string().custom(jsonString), tags: Joi.array().items( Joi.object().keys({ - tag: Joi.string().required(), - tagCategoryId: Joi.string().required().custom(slug), - tagCategoryName: Joi.string().required(), + title: Joi.string().required(), + description: Joi.string().allow(''), }) ), widget_config: Joi.string().custom(jsonString), autorun: Joi.boolean(), related_ea_components: Joi.string().allow(''), partner_logo: Joi.string().allow(''), + requirements: Joi.string().allow(''), + language: Joi.string(), // rated_by: Joi.object().pattern( // /^[0-9a-fA-F]{24}$/, // Joi.object() @@ -151,4 +155,5 @@ module.exports = { updatePrototype, deletePrototype, executeCode, + bulkCreatePrototypes, }; diff --git a/src/validations/user.validation.js b/src/validations/user.validation.js index 5dbcb0618c55db6640feebf43eec356c80e9266f..dfdd5d57d4ea98b5fcbf0f6f19c63f80d71ab2e8 100644 --- a/src/validations/user.validation.js +++ b/src/validations/user.validation.js @@ -44,6 +44,7 @@ const getUsers = { limit: Joi.number().integer(), page: Joi.number().integer(), search: Joi.string(), + id: Joi.string(), includeFullDetails: Joi.boolean().default(false), }), }; diff --git a/yarn.lock b/yarn.lock index 70731548a8ccb3cbde22be8128d9e37b660ce4c8..ac02d3d2e64fe50a078406689cd6f256a5104431 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2245,6 +2245,11 @@ dependencies: "@types/node" "*" +"@types/lodash@^4.17.12": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689" + integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ== + "@types/long@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -3058,7 +3063,7 @@ buffer@^5.2.0, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -bufferutil@^4.0.8: +bufferutil@^4.0.1, bufferutil@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== @@ -3598,6 +3603,14 @@ culvert@^0.1.2: resolved "https://registry.yarnpkg.com/culvert/-/culvert-0.1.2.tgz#9502f5f0154a2d5a22a023e79f71cc936fa6ef6f" integrity sha1-lQL18BVKLVoioCPnn3HMk2+m728= +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3939,10 +3952,10 @@ engine.io-parser@~5.2.1: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== -engine.io@~6.5.2: - version "6.5.5" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.5.tgz#430b80d8840caab91a50e9e23cb551455195fc93" - integrity sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA== +engine.io@~6.6.0: + version "6.6.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.1.tgz#a82b1e5511239a0e95fac14516870ee9138febc8" + integrity sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -4000,6 +4013,33 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4204,6 +4244,16 @@ eslint@^7.0.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + espree@^7.3.0, espree@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" @@ -4252,6 +4302,14 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -4416,6 +4474,13 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -6605,7 +6670,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.17.14, lodash@^4.7.0: +lodash@^4.17.14, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7071,6 +7136,11 @@ netmask@^2.0.1: resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -8668,16 +8738,16 @@ socket.io-parser@~4.2.4: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.7.5: - version "4.7.5" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8" - integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA== +socket.io@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.0.tgz#33d05ae0915fad1670bd0c4efcc07ccfabebe3b1" + integrity sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA== dependencies: accepts "~1.3.4" base64id "~2.0.0" cors "~2.8.5" debug "~4.3.2" - engine.io "~6.5.2" + engine.io "~6.6.0" socket.io-adapter "~2.5.2" socket.io-parser "~4.2.4" @@ -9392,6 +9462,11 @@ type-is@^1.6.18, type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -9499,6 +9574,13 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + utf-8-validate@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.4.tgz#1305a1bfd94cecb5a866e6fc74fd07f3ed7292e5" @@ -9649,6 +9731,18 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +websocket@^1.0.35: + version "1.0.35" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885" + integrity sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.63" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -9852,18 +9946,6 @@ xregexp@2.0.0: resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= -xss-clean@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/xss-clean/-/xss-clean-0.1.1.tgz#d3ba684d85ccd52054963d01ad6ab36d662db1a5" - integrity sha1-07poTYXM1SBUlj0BrWqzbWYtsaU= - dependencies: - xss-filters "1.2.6" - -xss-filters@1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/xss-filters/-/xss-filters-1.2.6.tgz#68b39089cb1dff8b9dbc889484839b2f507f5c55" - integrity sha1-aLOQicsd/4udvIiUhIObL1B/XFU= - xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -9879,6 +9961,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + yallist@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"