diff --git a/src/helpers/security.ts b/src/helpers/security.ts new file mode 100644 index 0000000..9d8deb9 --- /dev/null +++ b/src/helpers/security.ts @@ -0,0 +1,53 @@ +import { Job, scheduleJob } from "node-schedule"; +import usettings from "./usettings"; +import { Types } from "mongoose"; + +interface IAccTimeout { + firstAttempt: Date; + expire: Job; + attempts: number; +} + +class SecurityHelper { + private timeouts = new Map(); + private onTimeout = new Map(); // key: user id, value: unlock date + constructor () { } + + addAttempt (userId: Types.ObjectId) { + var uid = userId.toString() + if (this.timeouts.has(uid)) { + var t = this.timeouts.get(uid) + t.attempts += 1 + if (t.attempts > usettings.settings.security.loginTimeout.attempts) { + this.onTimeout.set(uid, scheduleJob(new Date(Date.now() + usettings.settings.security.loginTimeout.lockout * 1000), () => { + this.onTimeout.get(uid).cancel() + this.onTimeout.delete(uid) + })) + } else { + this.timeouts.set(uid, t) + } + } else { + this.timeouts.set(uid, { + attempts: 1, + firstAttempt: new Date(), + expire: scheduleJob(new Date(Date.now() + usettings.settings.security.loginTimeout.time * 1000), () => { + this.timeouts.get(uid).expire.cancel() + this.timeouts.delete(uid) + }) + }) + + } + } + + check(userId: Types.ObjectId) { + var timeout = this.onTimeout.get(userId.toString()) + if (timeout) { + // @ts-ignore + return timeout.nextInvocation().toDate().valueOf() - Date.now().valueOf() + } else { + return false + } + } +} + +export default new SecurityHelper() \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7c448d0..1ca15c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import mongoose from "mongoose" import User from "./schemas/User"; import routes from "./routes/index"; import process from "node:process" +import security from "./helpers/security"; const connectionString = process.env.ATLAS_URI || "mongodb://mongodb:27017/ipwa"; if (!process.env.DOMAIN) { @@ -55,12 +56,20 @@ app.use(passport.session()) passport.use("normal",new LocalStrategy(async function verify(uname,pass,done) { let query = await User.findOne({uname: uname.toLowerCase()}) if (query) { - if (query.locked == true) return done(null, false) + if (query.locked == true) return done({type: "locked", message: "Twoje konto jest zablokowane. Skontaktuj się z administratorem."}, false) + var timeout = security.check(query._id) + if (timeout) { + timeout = Math.ceil(timeout / 1000 / 60) + return done({type: "timeout", message: `Zbyt wiele nieudanych prób logowania. Odczekaj ${timeout} minut lub skontaktuj się z administratorem.`}, false) + } if (await bcrypt.compare(pass, query.pass)) { return done(null, query) - } else done(null, false) + } else { + security.addAttempt(query._id) + done({type: "unf"}, false) + } } else { - done(null, false) + done({type: "unf"}, false) } })) //#endregion diff --git a/src/routes/auth/index.ts b/src/routes/auth/index.ts index b10339e..cd7e06b 100644 --- a/src/routes/auth/index.ts +++ b/src/routes/auth/index.ts @@ -6,12 +6,45 @@ import bcrypt from "bcryptjs" import cap from "@/helpers/capability"; import usettings from "@/helpers/usettings"; import vapidKeys from "@/vapidKeys"; +import { IVerifyOptions } from "passport-local"; const authRouter = Router() -authRouter.post("/login", passport.authenticate('normal'), (req, res) => { - if (req.user.admin != null) res.send({status: 200, admin: req.user.admin}) - else res.send({status: 200}) +authRouter.post("/login", (req, res) => { + passport.authenticate('normal', (err: {type: string, message: string} | null, user?: Express.User | false, options?: IVerifyOptions) => { + if (user) { + req.login(user, (error) => { + if (error) { + res.status(500).send(error) + } else { + if (req.user.admin != null) { + res.send({status: 200, admin: req.user.admin}) + } else { + res.send({status: 200}) + } + } + }) + } else { + if (err) { + switch (err.type) { + case "unf": + res.status(404).send({status: 404, message: "Zła nazwa użytkownika lub hasło."}) + break; + case "timeout": + res.status(403).send({status: 403, message: err.message}) + break; + case "locked": + res.status(403).send({status: 403, message: err.message}) + break; + default: + res.status(500).send({status: 500, message: err.message}) + break; + } + } else { + res.status(403).send({status: 403, message: "Brak hasła lub loginu."}) + } + } + })(req, res) }) authRouter.post("/chpass", islogged, async (req,res) => {