diff --git a/app.js b/app.js index c65b7c1..4c5f133 100644 --- a/app.js +++ b/app.js @@ -31,6 +31,19 @@ app.use(express.json()) app.set('view engine', 'ejs') app.use(express.static('views/public')) +// Health check endpoint +app.get(['/health', '/healthz', '/api/health'], (req, res) => { + const mongoose = require('mongoose') + const isDbConnected = mongoose.connection.readyState === 1 + res.status(isDbConnected ? 200 : 503).json({ + status: isDbConnected ? 'healthy' : 'unhealthy', + uptime: process.uptime(), + timestamp: new Date().toISOString(), + service: 'belonging-manager-api', + database: isDbConnected ? 'connected' : 'disconnected', + }) +}) + app.use('/api', routerEndpoints) const { registerEventHandlers } = require('./events/index') diff --git a/components/user/loginAttemptModel.js b/components/user/loginAttemptModel.js new file mode 100644 index 0000000..6f21e6d --- /dev/null +++ b/components/user/loginAttemptModel.js @@ -0,0 +1,26 @@ +const mongoose = require('mongoose') + +const loginAttemptSchema = new mongoose.Schema( + { + key: { + type: String, + required: true, + unique: true, + index: true, + }, + attempts: { + type: Number, + default: 0, + }, + expiresAt: { + type: Date, + required: true, + index: { expires: 0 }, + }, + }, + { timestamps: true } +) + +const LoginAttempt = mongoose.model('LoginAttempt', loginAttemptSchema) + +module.exports = LoginAttempt diff --git a/components/user/userService.js b/components/user/userService.js index 7625d89..41da0b2 100644 --- a/components/user/userService.js +++ b/components/user/userService.js @@ -1,7 +1,7 @@ // userService.js const User = require('./userModel') const AppError = require('../../globalErrorHandler/AppError') -const redisClient = require('../../setup/redis') +const LoginAttempt = require('./loginAttemptModel') const jwt = require('jsonwebtoken') const bcrypt = require('bcryptjs') const maxAge = 60 * 60 * 24 * 90 @@ -44,39 +44,40 @@ const signup = async (name, email, password, passwordConfirm) => { } const loginAttempt = async (phrase) => { - const attemptExist = await redisClient.exists(phrase + '#attempts') + const key = phrase + '#attempts' + const limit = parseInt(process.env.LOGIN_ATTEMPT_LIMIT) || 10 + const ttlSeconds = 3600 + const now = new Date() - if (attemptExist === 0) { - await redisClient.set( - phrase + '#attempts', - 0, - { - EX: 3600, - }, - null - ) + let attemptRecord = await LoginAttempt.findOne({ key }) + + if (attemptRecord && attemptRecord.expiresAt <= now) { + await LoginAttempt.deleteOne({ _id: attemptRecord._id }) + attemptRecord = null + } + + if (!attemptRecord) { + const expiresAt = new Date(Date.now() + ttlSeconds * 1000) + await LoginAttempt.create({ + key, + attempts: 0, + expiresAt, + }) + return } // if user phrase(email or phone) exist return error with expire time left for login - if (parseInt(await redisClient.get(phrase + '#attempts')) >= parseInt(process.env.LOGIN_ATTEMPT_LIMIT)) { - const limitTime = await redisClient.ttl(phrase + '#attempts') + if (attemptRecord.attempts >= limit) { + const limitTime = Math.max(0, Math.ceil((attemptRecord.expiresAt.getTime() - Date.now()) / 1000)) const minutes = (limitTime / 60).toFixed(0) throw new AppError('MaximumLoginReached', 500, { minutes, }) } - if (attemptExist === 1) { - const attempts = parseInt(await redisClient.get(phrase + '#attempts')) - await redisClient.set( - phrase + '#attempts', - attempts + 1 + '', - { - EX: 3600, - }, - null - ) - } + attemptRecord.attempts += 1 + attemptRecord.expiresAt = new Date(Date.now() + ttlSeconds * 1000) + await attemptRecord.save() } const loginUser = async (email, password) => { diff --git a/config/.env b/config/.env index abbdb1d..2f0f2b8 100644 --- a/config/.env +++ b/config/.env @@ -2,7 +2,6 @@ MONGODB_URL=mongodb://admin:*135792468*@localhost:27018/marbm?authSource=admin NODE_ENV=development PORT=3000 JWT_SECRET=Y@RJ$WT!@K0NS@MPL! -REDIS_URL=redis://:135792468@localhost:6380/0 LOGIN_ATTEMPT_LIMIT=900 SUPER_ADMIN_ROLE_ID=66a2da5b9b989a8909948848 TYPE_SENSE_URL=localhost:8109 diff --git a/config/.env.sample b/config/.env.sample index afe3f52..91be5e3 100644 --- a/config/.env.sample +++ b/config/.env.sample @@ -2,7 +2,6 @@ MONGODB_URL=mongodb://admin:*135792468*@localhost:27018/marbm?authSource=admin NODE_ENV=development PORT=3000 JWT_SECRET=Y@RJNS@MPL! -REDIS_URL= redis://:135792468@localhost:6379/0 LOGIN_ATTEMPT_LIMIT=10 SUPER_ADMIN_ROLE_ID=66a2da5b9b989a8909948848 TYPE_SENSE_URL=localhost:8108 diff --git a/package-lock.json b/package-lock.json index 3103761..9e3cd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "mongoose": "^8.9.0", "mongoose-sequence": "^6.0.1", "multer": "^1.4.5-lts.1", - "redis": "^4.7.0", "typesense": "^1.8.2", "validator": "^13.12.0" }, @@ -51,59 +50,6 @@ "sparse-bitfield": "^3.0.3" } }, - "node_modules/@redis/bloom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", - "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/client": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", - "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", - "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/search": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -442,14 +388,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -949,14 +887,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "engines": { - "node": ">= 4" - } - }, "node_modules/get-intrinsic": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", @@ -1887,22 +1817,6 @@ "node": ">=8.10.0" } }, - "node_modules/redis": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", - "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", - "workspaces": [ - "./packages/*" - ], - "dependencies": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.6.0", - "@redis/graph": "1.1.1", - "@redis/json": "1.0.7", - "@redis/search": "1.2.0", - "@redis/time-series": "1.1.0" - } - }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -2411,11 +2325,6 @@ "engines": { "node": ">=0.4" } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/package.json b/package.json index 368f041..ba79a8e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "mongoose": "^8.9.0", "mongoose-sequence": "^6.0.1", "multer": "^1.4.5-lts.1", - "redis": "^4.7.0", "typesense": "^1.8.2", "validator": "^13.12.0" }, diff --git a/seeder/seederDBConnection.js b/seeder/seederDBConnection.js index e8016bd..08adef2 100644 --- a/seeder/seederDBConnection.js +++ b/seeder/seederDBConnection.js @@ -1,6 +1,5 @@ const mongoose = require('mongoose') const env = process.env.NODE_ENV || 'development' -const redisClient = require('../setup/redis') const seederRunner = require('./seederRunner') const chalk = require('chalk') @@ -12,8 +11,6 @@ module.exports = (app) => { .connect(connectionString, {}) .then(async function () { console.log('MongoDB Database Connected') - await redisClient.connect().catch((err) => console.log('Redis connect warning:', err.message)) - console.log('Redis connection Established') console.log(chalk.red.bold('-------- Running Seeders ---------')) try { diff --git a/setup/databaseConnection.js b/setup/databaseConnection.js index b6d412e..aaee673 100644 --- a/setup/databaseConnection.js +++ b/setup/databaseConnection.js @@ -1,5 +1,4 @@ const mongoose = require('mongoose') -const redisClient = require('./redis') const port = process.env.PORT || 3000 const env = process.env.NODE_ENV || 'development' module.exports = (app) => { @@ -20,12 +19,8 @@ module.exports = (app) => { // Create a document to force MongoDB to actually create the DB and collection await MyModel.create({ name: 'test' }) - redisClient.connect().then((r) => { - console.log('Redis connection Established') - - app.listen(port, function () { - console.log('Server Started On Port ' + port) - }) + app.listen(port, function () { + console.log('Server Started On Port ' + port) }) }) .catch(function (err) { diff --git a/setup/redis.js b/setup/redis.js deleted file mode 100644 index e3919cc..0000000 --- a/setup/redis.js +++ /dev/null @@ -1,18 +0,0 @@ -const redis = require('redis') -const env = process.env.NODE_ENV || 'development' - -let connectionString -if (env === 'development') { - connectionString = process.env.REDIS_URL -} else if (env === 'test') { - connectionString = process.env.TEST_REDIS_URL -} else if (env === 'production') { - connectionString = process.env.REDIS_URL -} -const client = redis.createClient({ url: connectionString }) - -client.on('error', function (err) { - console.log(err) -}) - -module.exports = client