From 9efeba001005cc0b9092a6621ae10c1b462596e2 Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Thu, 15 May 2025 15:58:44 +0200 Subject: [PATCH 01/13] fix: moved helpers to separate folder, added `security` to usettings (part of #3). --- src/{ => helpers}/attendence.ts | 0 src/{ => helpers}/capability.ts | 0 src/{ => helpers}/usettings.ts | 7 +++++++ src/routes/api/admin/accs.ts | 2 +- src/routes/api/admin/clean.ts | 6 +++--- src/routes/api/admin/groups.ts | 2 +- src/routes/api/admin/keys.ts | 4 ++-- src/routes/api/admin/menu.ts | 4 ++-- src/routes/api/admin/news.ts | 2 +- src/routes/api/admin/notif.ts | 2 +- src/routes/api/admin/settings.ts | 4 ++-- src/routes/api/appRouter.ts | 4 ++-- src/routes/auth/index.ts | 5 ++--- src/routes/index.ts | 3 +-- 14 files changed, 25 insertions(+), 20 deletions(-) rename src/{ => helpers}/attendence.ts (100%) rename src/{ => helpers}/capability.ts (100%) rename src/{ => helpers}/usettings.ts (85%) diff --git a/src/attendence.ts b/src/helpers/attendence.ts similarity index 100% rename from src/attendence.ts rename to src/helpers/attendence.ts diff --git a/src/capability.ts b/src/helpers/capability.ts similarity index 100% rename from src/capability.ts rename to src/helpers/capability.ts diff --git a/src/usettings.ts b/src/helpers/usettings.ts similarity index 85% rename from src/usettings.ts rename to src/helpers/usettings.ts index 2cd73e0..c4d06cb 100644 --- a/src/usettings.ts +++ b/src/helpers/usettings.ts @@ -8,6 +8,13 @@ interface IUSettings { sn: string[]; kol: string[]; } + }, + security: { + loginTimeout: { + attempts: number; + time: number; + lockout: number; + } } } diff --git a/src/routes/api/admin/accs.ts b/src/routes/api/admin/accs.ts index d692231..66c8547 100644 --- a/src/routes/api/admin/accs.ts +++ b/src/routes/api/admin/accs.ts @@ -1,7 +1,7 @@ import User from "@schemas/User"; import { Router } from "express" import { Perms, adminCond, adminPerm } from "@/utility"; -import capability from "@/capability"; +import capability from "@/helpers/capability"; import Group from "@/schemas/Group"; const accsRouter = Router() diff --git a/src/routes/api/admin/clean.ts b/src/routes/api/admin/clean.ts index 57deec4..a0f48f6 100644 --- a/src/routes/api/admin/clean.ts +++ b/src/routes/api/admin/clean.ts @@ -1,10 +1,10 @@ import { Router } from "express"; import { Perms, adminPerm } from "@/utility"; -import capability, { Features } from "@/capability"; -import usettings from "@/usettings"; +import capability, { Features } from "@/helpers/capability"; +import usettings from "@/helpers/usettings"; import Grade from "@schemas/Grade"; import User from "@/schemas/User"; -import attendence from "@/attendence"; +import attendence from "@/helpers/attendence"; const cleanRouter = Router() cleanRouter.use(adminPerm(Perms.Clean)) diff --git a/src/routes/api/admin/groups.ts b/src/routes/api/admin/groups.ts index bf1aeeb..6d8957d 100644 --- a/src/routes/api/admin/groups.ts +++ b/src/routes/api/admin/groups.ts @@ -1,7 +1,7 @@ import Group from "@schemas/Group"; import { Router } from "express" import { Perms, adminPerm } from "@/utility"; -import capability, { Features } from "@/capability"; +import capability, { Features } from "@/helpers/capability"; const groupsRouter = Router() diff --git a/src/routes/api/admin/keys.ts b/src/routes/api/admin/keys.ts index 106c57b..c631b2f 100644 --- a/src/routes/api/admin/keys.ts +++ b/src/routes/api/admin/keys.ts @@ -1,7 +1,7 @@ import { Router } from "express"; -import capability, { Features } from "@/capability"; +import capability, { Features } from "@/helpers/capability"; import Key from "@schemas/Key"; -import usettings from "@/usettings"; +import usettings from "@/helpers/usettings"; import User, { IUser } from "@schemas/User"; import { Perms, adminPerm } from "@/utility"; diff --git a/src/routes/api/admin/menu.ts b/src/routes/api/admin/menu.ts index 87ba928..7886fbf 100644 --- a/src/routes/api/admin/menu.ts +++ b/src/routes/api/admin/menu.ts @@ -4,9 +4,9 @@ import multer from "multer" import * as XLSX from "xlsx" import Menu from "@schemas/Menu" import Vote from "@schemas/Vote" -import capability, { Features } from "@/capability" +import capability, { Features } from "@/helpers/capability" import { editorRouter } from "./editor" -import usettings from "@/usettings" +import usettings from "@/helpers/usettings" const menuRouter = Router() diff --git a/src/routes/api/admin/news.ts b/src/routes/api/admin/news.ts index 4e2c2ca..f339838 100644 --- a/src/routes/api/admin/news.ts +++ b/src/routes/api/admin/news.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import News from "@schemas/News" import { Perms, adminPerm } from "@/utility"; -import capability, { Features } from "@/capability"; +import capability, { Features } from "@/helpers/capability"; const newsRouter = Router() diff --git a/src/routes/api/admin/notif.ts b/src/routes/api/admin/notif.ts index c7c9f4f..80164be 100644 --- a/src/routes/api/admin/notif.ts +++ b/src/routes/api/admin/notif.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import { Perms, adminPerm } from "@/utility"; import Group from "@schemas/Group"; import { NotifcationHelper } from "@/notif"; -import capability, { Features } from "@/capability"; +import capability, { Features } from "@/helpers/capability"; const notifRouter = Router() diff --git a/src/routes/api/admin/settings.ts b/src/routes/api/admin/settings.ts index 70d5959..15bc750 100644 --- a/src/routes/api/admin/settings.ts +++ b/src/routes/api/admin/settings.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { adminPerm, Perms, project } from "@/utility"; -import usettings from "@/usettings"; +import usettings from "@/helpers/usettings"; export const settingsRouter = Router() @@ -11,7 +11,7 @@ settingsRouter.get('/', (req, res) => { }) settingsRouter.post('/', (req, res) => { - usettings.settings = project(req.body, {keyrooms: true, cleanThings: true, rooms: true, menu: true}) + usettings.settings = project(req.body, {keyrooms: true, cleanThings: true, rooms: true, menu: true, security: true}) res.send({status: 200}) }) diff --git a/src/routes/api/appRouter.ts b/src/routes/api/appRouter.ts index 72543d9..1cdc23e 100644 --- a/src/routes/api/appRouter.ts +++ b/src/routes/api/appRouter.ts @@ -4,9 +4,9 @@ import News from "@schemas/News"; import Menu from "@schemas/Menu"; import Vote from "@schemas/Vote"; import { vote } from "@/pipelines/vote"; -import capability, { Features } from "@/capability"; +import capability, { Features } from "@/helpers/capability"; import Key, { IKey } from "@schemas/Key"; -import usettings from "@/usettings"; +import usettings from "@/helpers/usettings"; import Grade from "@schemas/Grade"; import { createHash } from "node:crypto"; const appRouter = Router(); diff --git a/src/routes/auth/index.ts b/src/routes/auth/index.ts index 52db104..b10339e 100644 --- a/src/routes/auth/index.ts +++ b/src/routes/auth/index.ts @@ -3,9 +3,8 @@ import passport from "passport"; import User from "@schemas/User"; import { islogged } from "@/utility"; import bcrypt from "bcryptjs" -import cap from "@/capability"; -import usettings from "@/usettings"; -import { readFileSync } from "node:fs"; +import cap from "@/helpers/capability"; +import usettings from "@/helpers/usettings"; import vapidKeys from "@/vapidKeys"; const authRouter = Router() diff --git a/src/routes/index.ts b/src/routes/index.ts index fe18dc7..60e2810 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,8 +4,7 @@ import { islogged } from "@/utility"; import { adminRouter } from "./api/adminRouter"; import { appRouter } from "./api/appRouter"; import { authRouter } from "./auth/index"; -import { Schema } from 'mongoose' -import capability, { Features } from "@/capability"; +import capability, { Features } from "@/helpers/capability"; const router = Router(); From b708fe8c18f5f3dcc9ef7d78beccd8b80eecab18 Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Fri, 16 May 2025 00:39:55 +0200 Subject: [PATCH 02/13] feat: Added brute force prevention. Closes #3 --- src/helpers/security.ts | 53 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 15 +++++++++--- src/routes/auth/index.ts | 39 ++++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 src/helpers/security.ts 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) => { From 92aaee9bcc06d08275f994ef70b0be0b070d2a0d Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Tue, 20 May 2025 20:57:41 +0200 Subject: [PATCH 03/13] fix: Removed redundant cors url --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1ca15c2..2600d57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ var app = express(); app.use(bodyParser.json()) app.use(bodyParser.urlencoded({extended: true})) app.use(cors({ - origin: ["http://localhost:4200", "http://localhost:3000", `https://${process.env.DOMAIN}`,], + origin: ["http://localhost:4200", `https://${process.env.DOMAIN}`,], credentials: true })) app.use(session({ From 432b4dc4e5a321e1fc7516ece1c32914b962da45 Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Wed, 21 May 2025 19:54:37 +0200 Subject: [PATCH 04/13] fix: Redesigned user cards --- src/helpers/security.ts | 4 ++++ src/routes/api/admin/accs.ts | 17 +++++++++++++++++ src/schemas/User.ts | 4 +++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/helpers/security.ts b/src/helpers/security.ts index 9d8deb9..741e8a6 100644 --- a/src/helpers/security.ts +++ b/src/helpers/security.ts @@ -48,6 +48,10 @@ class SecurityHelper { return false } } + + clearAcc(userId: string) { + return this.onTimeout.delete(userId) + } } export default new SecurityHelper() \ No newline at end of file diff --git a/src/routes/api/admin/accs.ts b/src/routes/api/admin/accs.ts index 66c8547..03f0934 100644 --- a/src/routes/api/admin/accs.ts +++ b/src/routes/api/admin/accs.ts @@ -3,6 +3,8 @@ import { Router } from "express" import { Perms, adminCond, adminPerm } from "@/utility"; import capability from "@/helpers/capability"; import Group from "@/schemas/Group"; +import security from "@/helpers/security"; +import { Types } from "mongoose"; const accsRouter = Router() @@ -16,6 +18,13 @@ accsRouter.get('/', async (req, res)=> { res.send(data) }) +accsRouter.get('/:id', async (req, res) => { + res.send({ + ...(await User.findById(req.params.id, {pass: 0})).toJSON(), + lockout: !!security.check(new Types.ObjectId(req.params.id)) + }) +}) + accsRouter.post('/', async (req, res)=> { if (req.body.uname == "admin") return res.status(400).send("This name is reserved").end() if (req.body.flags) { @@ -82,4 +91,12 @@ accsRouter.delete('/:id', async (req, res) => { } }) +accsRouter.delete('/:id/lockout', async (req, res) => { + if (security.clearAcc(req.params.id)) { + res.send({status: 200}).end() + } else { + res.sendStatus(400) + } +}) + export {accsRouter}; \ No newline at end of file diff --git a/src/schemas/User.ts b/src/schemas/User.ts index fa9e821..9edb23e 100644 --- a/src/schemas/User.ts +++ b/src/schemas/User.ts @@ -11,6 +11,7 @@ export interface IUser { fname?: string; surname?: string; groups: Types.ObjectId[]; + regDate: Date; } const userSchema = new Schema({ @@ -21,7 +22,8 @@ const userSchema = new Schema({ locked: {type: Boolean, default: false}, fname: String, surname: String, - groups: [{type: mongoose.Types.ObjectId, ref: "Group"}] + groups: [{type: mongoose.Types.ObjectId, ref: "Group"}], + regDate: {type: Date, default: Date.now} }) export default mongoose.model("logins", userSchema) \ No newline at end of file From 4de7af4bc01252c21a4111b1c31c6f574605339a Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sat, 24 May 2025 11:27:12 +0200 Subject: [PATCH 05/13] feat: Added redirect after login for users. --- src/index.ts | 17 +++++++++-------- src/routes/api/admin/accs.ts | 2 +- src/routes/auth/index.ts | 18 ++++++++++++------ src/schemas/User.ts | 10 ++++++---- src/utility.ts | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2600d57..46bc8db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import session from "express-session"; import bcrypt from 'bcryptjs'; import MongoStore from "connect-mongo"; import mongoose from "mongoose" -import User from "./schemas/User"; +import User, { IUser } from "./schemas/User"; import routes from "./routes/index"; import process from "node:process" import security from "./helpers/security"; @@ -20,13 +20,13 @@ if (!process.env.DOMAIN) { declare global { namespace Express { - export interface User { + export interface User extends IUser { _id: mongoose.Types.ObjectId; - pass: string; - uname: string; - admin?: number; - locked?: boolean; - room?: string + // pass: string; + // uname: string; + // admin?: number; + // locked?: boolean; + // room?: string } } } @@ -87,7 +87,7 @@ passport.deserializeUser(async function(id, done) { } }); -app.listen(8080, async () => { +var server = app.listen(8080, async () => { await mongoose.connect(connectionString); if (process.send) process.send("ready") }) @@ -95,5 +95,6 @@ app.listen(8080, async () => { app.use('/', routes) process.on('SIGINT', () => { + server.close() mongoose.disconnect().then(() => process.exit(0), () => process.exit(1)) }) \ No newline at end of file diff --git a/src/routes/api/admin/accs.ts b/src/routes/api/admin/accs.ts index 03f0934..f47d83f 100644 --- a/src/routes/api/admin/accs.ts +++ b/src/routes/api/admin/accs.ts @@ -48,7 +48,7 @@ accsRouter.put('/:id', async (req, res)=> { res.status(404).send("User not found") return } - if (req.body.flags != undefined) { + if (req.body.flags) { if (adminCond(req.user.admin, Perms.Superadmin)) { if (adminCond(user.admin, Perms.Superadmin)) { res.status(400).send("Cannot edit other superadmins") diff --git a/src/routes/auth/index.ts b/src/routes/auth/index.ts index cd7e06b..2528f8a 100644 --- a/src/routes/auth/index.ts +++ b/src/routes/auth/index.ts @@ -17,11 +17,7 @@ authRouter.post("/login", (req, res) => { 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}) - } + res.send({status: 200, admin: req.user.admin || undefined, redirect: req.user.defaultPage}) } }) } else { @@ -83,10 +79,20 @@ authRouter.get("/check", islogged, (req, res, next) => { if (req.user.locked) { req.logout((err) => { if (err) next(err) - res.status(401).send("Your account has been locked.") + res.status(401).send({status: 401, message: "Your account has been locked."}) }) } res.send({"admin": req.user.admin, "features": cap.flags, "room": req.user.room, "menu": {"defaultItems": usettings.settings.menu.defaultItems}, "vapid": vapidKeys.keys.publicKey}) }) +authRouter.put("/redirect", islogged, async (req, res) => { + if (["", "/", "/login", "/login/", "login"].find(v => v == req.body.redirect)) return res.status(400).send({status: 400, message: "Path in blacklist"}) + const update = await User.findByIdAndUpdate(req.user._id, {defaultPage: req.body.redirect}) + if (update) { + res.send({status: 200}).end() + } else { + res.status(500).send({status: 500}).end() + } +}) + export { authRouter }; diff --git a/src/schemas/User.ts b/src/schemas/User.ts index 9edb23e..5bb7820 100644 --- a/src/schemas/User.ts +++ b/src/schemas/User.ts @@ -12,18 +12,20 @@ export interface IUser { surname?: string; groups: Types.ObjectId[]; regDate: Date; + defaultPage: string; } const userSchema = new Schema({ uname: {type: String, required: true}, pass: {type: String, required: true, default: "$2y$10$wxDhf.XiXkmdKrFqYUEa0.F4Bf.pDykZaMmgjvyLyeRP3E/Xy0hbC"}, - room: String, + room: {type: String, default: ""}, admin: Number, locked: {type: Boolean, default: false}, - fname: String, - surname: String, + fname: {type: String, default: ""}, + surname: {type: String, default: ""}, groups: [{type: mongoose.Types.ObjectId, ref: "Group"}], - regDate: {type: Date, default: Date.now} + regDate: {type: Date, default: Date.now}, + defaultPage: {type: String, default: ""}, }) export default mongoose.model("logins", userSchema) \ No newline at end of file diff --git a/src/utility.ts b/src/utility.ts index 504bd8d..7705c89 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -8,7 +8,7 @@ var islogged = (req: Request, res: Response, next: NextFunction) => { } var isadmin = (req: Request, res: Response, next: NextFunction) => { - if (req.user.admin != null) { + if (req.user.admin) { return next() } res.sendStatus(401) From ec787e232163a4c156a1016e3944b7543332f34f Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sat, 31 May 2025 15:34:04 +0200 Subject: [PATCH 06/13] feat: Added a healthcheck to backend --- src/routes/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routes/index.ts b/src/routes/index.ts index 60e2810..00370fc 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,6 +5,7 @@ import { adminRouter } from "./api/adminRouter"; import { appRouter } from "./api/appRouter"; import { authRouter } from "./auth/index"; import capability, { Features } from "@/helpers/capability"; +import mongoose from "mongoose"; const router = Router(); @@ -12,6 +13,14 @@ router.use('/app', appRouter) router.use('/admin', adminRouter) router.use('/auth', authRouter) +router.get("/healthcheck", async (req, res) => { + res.status(200).send({ + uptime: process.uptime(), + date: new Date(), + db: mongoose.connection.readyState + }) +}) + router.post("/notif", islogged, capability.mw(Features.Notif), async (req, res) => { var obj = {user: req.user._id, ...req.body} await Notification.findOneAndUpdate(obj, obj, {upsert: true}) From dd17fd52947769d5b8b70014f008603bf2eded7f Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sat, 31 May 2025 16:57:53 +0200 Subject: [PATCH 07/13] feat: Added notifications outbox to admin panel --- src/notif.ts | 107 +++++++++++++++++++++------------- src/routes/api/admin/notif.ts | 43 +++++++++----- src/schemas/Inbox.ts | 18 ++++++ src/schemas/User.ts | 2 - 4 files changed, 113 insertions(+), 57 deletions(-) create mode 100644 src/schemas/Inbox.ts diff --git a/src/notif.ts b/src/notif.ts index f330b8f..654d939 100644 --- a/src/notif.ts +++ b/src/notif.ts @@ -1,11 +1,27 @@ -import { PushSubscription, RequestOptions, VapidKeys, WebPushError, sendNotification } from "web-push"; +import { RequestOptions, SendResult, VapidKeys, WebPushError, sendNotification } from "web-push"; import Notification from "./schemas/Notification"; import vapidKeys from "./vapidKeys"; -import { IUser } from "./schemas/User"; +import User, { IUser } from "./schemas/User"; +import Inbox from "./schemas/Inbox"; +import { Types } from "mongoose"; -export class NotifcationHelper { +export interface SimpleMessage { + title: string; + body: string; +} + +export interface PushResult { + sent: number; + possible: number; +} + +export class Message { private options: RequestOptions - constructor () { + private message: { notification: SimpleMessage } + private rcptType: "uname" | "room" | "group" + private rcpt: string + + constructor (title: string, body: string, rcptType: "uname" | "room" | "group", rcpt: string) { let keys: VapidKeys = vapidKeys.keys this.options = { vapidDetails: { @@ -14,27 +30,66 @@ export class NotifcationHelper { publicKey: keys.publicKey } } + this.message = { notification: { title: title, body: body } } + this.rcptType = rcptType + this.rcpt = rcpt } - private async send(message: string, subscriptions: PushSubscription[]) { + async findUserNotif(uname: string) { + var notif = await Notification.find().populate<{user: Pick & {_id: Types.ObjectId}}>('user', ['uname', '_id']).exec() + return notif.filter(val => val.user.uname == uname) + } + + async findRoomNotif(room: string) { + var notif = await Notification.find().populate<{user: Pick & {_id: Types.ObjectId}}>('user', ['room', '_id']).exec() + return notif.filter(val => val.user.room == room) + } + + async findGroupNotif(groupId: string) { + var notif = await Notification.find().populate<{user: Pick & {_id: Types.ObjectId}}>('user', ['groups', '_id']).exec() + return notif.filter(val => val.user.groups.find(x => x.toString() == groupId)) + } + + public async send(): Promise { + var subscriptions + var rcptIds: Types.ObjectId[] + switch (this.rcptType) { + case "uname": + subscriptions = await this.findUserNotif(this.rcpt) + rcptIds = (await User.find({uname: this.rcpt})).map(v => v._id) + break; + case "room": + subscriptions = await this.findRoomNotif(this.rcpt) + rcptIds = (await User.find({room: this.rcpt})).map(v => v._id) + break; + case "group": + subscriptions = await this.findGroupNotif(this.rcpt) + rcptIds = (await User.find({groups: this.rcpt})).map(v => v._id) + break; + default: + throw new Error(`Wrong recipient type used: ${this.rcptType}`); + } + + await Inbox.create({message: this.message.notification, rcpt: rcptIds}) + var count = 0; var subslen = subscriptions.length for (const v of subscriptions) { - var result + var result: SendResult try { - result = await sendNotification(v, message, this.options) + result = await sendNotification(v, JSON.stringify(this.message), this.options) count++ } catch (error) { if (error instanceof WebPushError) { switch (error.statusCode) { case 410: console.log("GONE") - await Notification.findOneAndDelete({endpoint: v.endpoint, keys: v.keys}) + await Notification.findByIdAndRemove(v._id) subslen-- break; case 404: console.warn("NOT FOUND", error.message) - await Notification.findOneAndDelete(v) + await Notification.findByIdAndRemove(v._id) subslen-- break; default: @@ -44,39 +99,7 @@ export class NotifcationHelper { } } } + return {sent: count, possible: subslen} } - - private rcpt(message: string) { - return { - user: async (uname: string) => { - return await this.send(message, await this.findUserNotif(uname)) - }, - room: async (room: string) => { - return await this.send(message, await this.findRoomNotif(room)) - }, - group: async (group: string) => { - return await this.send(message, await this.findGroupNotif(group)) - } - } - } - - simpleMessage(title: string, body: string) { - return this.rcpt(JSON.stringify({notification: {title: title, body: body}})) - } - - async findUserNotif(uname: string): Promise> { - var notif = await Notification.find().populate<{user: Pick}>('user', 'uname').exec() - return notif.filter(val => val.user.uname == uname) - } - - async findRoomNotif(room: string): Promise> { - var notif = await Notification.find().populate<{user: Pick}>('user', 'room').exec() - return notif.filter(val => val.user.room == room) - } - - async findGroupNotif(groupId: string): Promise> { - var notif = await Notification.find().populate<{user: Pick}>('user', 'groups').exec() - return notif.filter(val => val.user.groups.find(x => x.toString() == groupId)) - } } \ No newline at end of file diff --git a/src/routes/api/admin/notif.ts b/src/routes/api/admin/notif.ts index 80164be..25fc549 100644 --- a/src/routes/api/admin/notif.ts +++ b/src/routes/api/admin/notif.ts @@ -1,38 +1,44 @@ -import { Router } from "express"; +import { Request, Response, Router } from "express"; import { Perms, adminPerm } from "@/utility"; import Group from "@schemas/Group"; -import { NotifcationHelper } from "@/notif"; +import { PushResult, Message } from "@/notif"; import capability, { Features } from "@/helpers/capability"; +import Inbox from "@/schemas/Inbox"; +import { Types } from "mongoose"; +import { IUser } from "@/schemas/User"; const notifRouter = Router() -const nh = new NotifcationHelper() - notifRouter.use(adminPerm(Perms.Notif)) notifRouter.use(capability.mw(Features.Notif)) -notifRouter.post("/send", async (req, res) => { - const message = nh.simpleMessage(req.body.title, req.body.body) +type PushSendBody = {recp: + {type: "uname", uname: string} | + {type: "room", room: string} | + {type: "group", group: string}, + title: string, + body: string +} + +notifRouter.post("/send", async (req: Request, res: Response) => { let recp: string - let result; switch (req.body.recp.type) { case "uname": recp = req.body.recp.uname - result = await message.user(recp); break; case "room": recp = req.body.recp.room - result = await message.room(recp) break; case "group": if (!capability.settings.groups) return res.sendStatus(406).end() recp = req.body.recp.group - result = await message.group(recp) break; default: res.status(400).end() break; } + const message = new Message(req.body.title, req.body.body, req.body.recp.type, recp) + let result: PushResult = await message.send() console.log(` From: ${req.user.uname} (${req.user._id}) To: ${recp} @@ -43,8 +49,19 @@ notifRouter.post("/send", async (req, res) => { res.send(result) }) -notifRouter.get("/groups", async (req,res) => { - res.send(await Group.find({}, {name: 1, _id: 1})) +notifRouter.get("/outbox", async (req, res: Response) => { + var result = await Inbox.find({}, {}, {sort: {sentDate: -1}}).populate<{rcpt: IUser & {_id: Types.ObjectId}}>("rcpt", ['fname', 'surname', 'uname', '_id', 'room']).exec() + var final = result.map(v => { + return { + ...v.toJSON(), + ack: v.ack.length + } + }) + res.send(final) }) -export {notifRouter} \ No newline at end of file +notifRouter.get("/groups", async (req, res) => { + res.send(await Group.find({}, { name: 1, _id: 1 })) +}) + +export { notifRouter } \ No newline at end of file diff --git a/src/schemas/Inbox.ts b/src/schemas/Inbox.ts new file mode 100644 index 0000000..d7f2570 --- /dev/null +++ b/src/schemas/Inbox.ts @@ -0,0 +1,18 @@ +import { SimpleMessage } from "@/notif" +import mongoose, { Types, Schema } from "mongoose" + +export interface IInbox { + message: SimpleMessage, + sentDate: Date, + rcpt: Types.ObjectId[], + ack: Types.ObjectId[] +} + +const inboxSchema = new Schema({ + message: {type: Object, required: true}, + sentDate: {type: Date, required: true, default: Date.now()}, + rcpt: [{type: Schema.Types.ObjectId, ref: "logins", required: true}], + ack: [{type: Schema.Types.ObjectId, ref: "logins", required: true, default: []}], +}) + +export default mongoose.model("inbox", inboxSchema) \ No newline at end of file diff --git a/src/schemas/User.ts b/src/schemas/User.ts index 5bb7820..669550c 100644 --- a/src/schemas/User.ts +++ b/src/schemas/User.ts @@ -1,7 +1,5 @@ import mongoose, { Types, Schema } from "mongoose" -// TODO: Unify `fname` and `surename` into single field - export interface IUser { uname: string; pass: string; From 334402d8c501d4168fc76a7d81a781625fd9e854 Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sat, 31 May 2025 19:56:38 +0200 Subject: [PATCH 08/13] feat: Added notification dialog on frontend --- .../api/{adminRouter.ts => admin/index.ts} | 22 ++++++------ .../api/admin/{notif.ts => notif/index.ts} | 14 ++------ src/routes/api/admin/notif/outbox.ts | 35 +++++++++++++++++++ src/routes/api/{appRouter.ts => app/index.ts} | 28 +++++++++++++-- src/routes/api/index.ts | 8 +++++ src/routes/index.ts | 8 ++--- 6 files changed, 86 insertions(+), 29 deletions(-) rename src/routes/api/{adminRouter.ts => admin/index.ts} (56%) rename src/routes/api/admin/{notif.ts => notif/index.ts} (81%) create mode 100644 src/routes/api/admin/notif/outbox.ts rename src/routes/api/{appRouter.ts => app/index.ts} (74%) create mode 100644 src/routes/api/index.ts diff --git a/src/routes/api/adminRouter.ts b/src/routes/api/admin/index.ts similarity index 56% rename from src/routes/api/adminRouter.ts rename to src/routes/api/admin/index.ts index 6e2087a..dcf5664 100644 --- a/src/routes/api/adminRouter.ts +++ b/src/routes/api/admin/index.ts @@ -1,15 +1,15 @@ import { Router } from "express"; import { islogged, isadmin} from "@/utility"; -import { newsRouter } from "./admin/news"; -import { accsRouter } from "./admin/accs"; -import { menuRouter } from "./admin/menu"; -import { groupsRouter } from "./admin/groups"; -import { notifRouter } from "./admin/notif"; -import { keysRouter } from "./admin/keys"; -import { cleanRouter } from "./admin/clean"; -import { settingsRouter } from "./admin/settings"; +import { newsRouter } from "./news"; +import { accsRouter } from "./accs"; +import { menuRouter } from "./menu"; +import { groupsRouter } from "./groups"; +import { notifRouter } from "./notif"; +import { keysRouter } from "./keys"; +import { cleanRouter } from "./clean"; +import { settingsRouter } from "./settings"; -const adminRouter = Router() +export const adminRouter = Router() adminRouter.use(islogged, isadmin) adminRouter.use('/news', newsRouter) @@ -24,6 +24,4 @@ adminRouter.use('/settings', settingsRouter) adminRouter.get('/usearch', (req, res) => { // TODO: Add search res.send([req.query['q']]) -}) - -export {adminRouter}; \ No newline at end of file +}) \ No newline at end of file diff --git a/src/routes/api/admin/notif.ts b/src/routes/api/admin/notif/index.ts similarity index 81% rename from src/routes/api/admin/notif.ts rename to src/routes/api/admin/notif/index.ts index 25fc549..b084dcc 100644 --- a/src/routes/api/admin/notif.ts +++ b/src/routes/api/admin/notif/index.ts @@ -6,6 +6,7 @@ import capability, { Features } from "@/helpers/capability"; import Inbox from "@/schemas/Inbox"; import { Types } from "mongoose"; import { IUser } from "@/schemas/User"; +import { outboxRouter } from "./outbox"; const notifRouter = Router() @@ -49,19 +50,10 @@ notifRouter.post("/send", async (req: Request { - var result = await Inbox.find({}, {}, {sort: {sentDate: -1}}).populate<{rcpt: IUser & {_id: Types.ObjectId}}>("rcpt", ['fname', 'surname', 'uname', '_id', 'room']).exec() - var final = result.map(v => { - return { - ...v.toJSON(), - ack: v.ack.length - } - }) - res.send(final) -}) - notifRouter.get("/groups", async (req, res) => { res.send(await Group.find({}, { name: 1, _id: 1 })) }) +notifRouter.use("/outbox", outboxRouter) + export { notifRouter } \ No newline at end of file diff --git a/src/routes/api/admin/notif/outbox.ts b/src/routes/api/admin/notif/outbox.ts new file mode 100644 index 0000000..23ea332 --- /dev/null +++ b/src/routes/api/admin/notif/outbox.ts @@ -0,0 +1,35 @@ +import Inbox from "@/schemas/Inbox"; +import { IUser } from "@/schemas/User"; +import { Response, Router } from "express"; + +export const outboxRouter = Router() + +outboxRouter.get("/", async (req, res: Response) => { + var result = await Inbox.find({}, {message: 1, sentDate: 1}, {sort: {sentDate: -1}}) + var final = result.map(v => { + return { + _id: v._id, + sentDate: v.sentDate, + title: v.message.title + } + }) + res.send(final) +}) + +outboxRouter.get("/:id/message", async (req, res) => { + var msg = await Inbox.findById(req.params.id, {message: 1}) + if (msg) { + res.send(msg.message.body) + } else { + res.status(404).send({message: "ERR: 404 Message id not found"}) + } +}) + +outboxRouter.get("/:id/rcpts", async (req, res) => { + var msg = await Inbox.findById(req.params.id, {rcpt: 1}).populate<{rcpt: Pick}>({path: "rcpt", select: ["uname", "room", "fname", "surname"]}).exec() + if (msg) { + res.send(msg.rcpt) + } else { + res.status(404).send({message: "ERR: 404 Message id not found"}) + } +}) \ No newline at end of file diff --git a/src/routes/api/appRouter.ts b/src/routes/api/app/index.ts similarity index 74% rename from src/routes/api/appRouter.ts rename to src/routes/api/app/index.ts index 1cdc23e..be3642b 100644 --- a/src/routes/api/appRouter.ts +++ b/src/routes/api/app/index.ts @@ -9,7 +9,9 @@ import Key, { IKey } from "@schemas/Key"; import usettings from "@/helpers/usettings"; import Grade from "@schemas/Grade"; import { createHash } from "node:crypto"; -const appRouter = Router(); +import Inbox from "@/schemas/Inbox"; + +export const appRouter = Router(); appRouter.use(islogged) @@ -75,4 +77,26 @@ appRouter.get("/clean/:date", capability.mw(Features.Clean), async (req, res) => })) }) -export {appRouter}; \ No newline at end of file +appRouter.get("/notif/check", capability.mw(Features.Notif), async (req, res) => { + var result = await Inbox.find({rcpt: req.user._id, $nor: [{ack: req.user._id}]}, {message: 1, sentDate: 1}) + if (result) { + res.send(result) + } else { + res.send([]) + } +}) + +appRouter.post("/notif/:id/ack", capability.mw(Features.Notif), async (req, res) => { + var result = await Inbox.findById(req.params.id) + if (result) { + if (result.rcpt.includes(req.user._id) && !result.ack.includes(req.user._id)) { + result.ack.push(req.user._id) + await result.save({}) + res.send({status: 200}) + } else { + res.status(403).send({status: 401, message: "User doesn't have access or message already acknowledged"}) + } + } else { + res.status(404).send({status: 404, message: "Message not found"}) + } +}) \ No newline at end of file diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts new file mode 100644 index 0000000..215940d --- /dev/null +++ b/src/routes/api/index.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import { appRouter } from "./app"; +import { adminRouter } from "./admin"; + +export const apiRouter = Router(); + +apiRouter.use("/app", appRouter) +apiRouter.use("/admin", adminRouter) \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 00370fc..b137bef 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,16 +1,14 @@ import { Router } from "express"; import Notification from "@schemas/Notification"; import { islogged } from "@/utility"; -import { adminRouter } from "./api/adminRouter"; -import { appRouter } from "./api/appRouter"; import { authRouter } from "./auth/index"; import capability, { Features } from "@/helpers/capability"; import mongoose from "mongoose"; +import { apiRouter } from "./api"; const router = Router(); -router.use('/app', appRouter) -router.use('/admin', adminRouter) +router.use('/', apiRouter) router.use('/auth', authRouter) router.get("/healthcheck", async (req, res) => { @@ -27,4 +25,6 @@ router.post("/notif", islogged, capability.mw(Features.Notif), async (req, res) res.send({"status": 200}) }) +router.use("/", apiRouter) + export default router; \ No newline at end of file From ec24700b8a9032679ee2ca584b1d4d942ee181a0 Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sun, 1 Jun 2025 13:54:54 +0200 Subject: [PATCH 09/13] feat: Added user search component. --- src/routes/api/admin/index.ts | 7 ++++--- src/schemas/User.ts | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/routes/api/admin/index.ts b/src/routes/api/admin/index.ts index dcf5664..b47e3aa 100644 --- a/src/routes/api/admin/index.ts +++ b/src/routes/api/admin/index.ts @@ -8,6 +8,7 @@ import { notifRouter } from "./notif"; import { keysRouter } from "./keys"; import { cleanRouter } from "./clean"; import { settingsRouter } from "./settings"; +import User from "@/schemas/User"; export const adminRouter = Router() @@ -21,7 +22,7 @@ adminRouter.use('/keys', keysRouter) adminRouter.use('/clean', cleanRouter) adminRouter.use('/settings', settingsRouter) -adminRouter.get('/usearch', (req, res) => { - // TODO: Add search - res.send([req.query['q']]) +adminRouter.get('/usearch', async (req, res) => { + var results = await User.find({$text: {$search: req.query['q'].toString()}}, {uname: 1, surname: 1, fname: 1, room: 1}) + res.send(results) }) \ No newline at end of file diff --git a/src/schemas/User.ts b/src/schemas/User.ts index 669550c..ecc482f 100644 --- a/src/schemas/User.ts +++ b/src/schemas/User.ts @@ -26,4 +26,6 @@ const userSchema = new Schema({ defaultPage: {type: String, default: ""}, }) +userSchema.index({uname: "text", room: "text", fname: "text", surname: "text"}, {weights: {fname: 3, surname: 4, room: 2, uname: 1}, default_language: "none"}) + export default mongoose.model("logins", userSchema) \ No newline at end of file From df745c78e26d649c9e2dda3a0835175295aa70a7 Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sun, 1 Jun 2025 17:44:43 +0200 Subject: [PATCH 10/13] feat: added user search to various components --- src/notif.ts | 12 ++++++------ src/routes/api/admin/keys.ts | 16 +++++++--------- src/routes/api/admin/notif/index.ts | 9 +++------ 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/notif.ts b/src/notif.ts index 654d939..538e911 100644 --- a/src/notif.ts +++ b/src/notif.ts @@ -18,10 +18,10 @@ export interface PushResult { export class Message { private options: RequestOptions private message: { notification: SimpleMessage } - private rcptType: "uname" | "room" | "group" + private rcptType: "uid" | "room" | "group" private rcpt: string - constructor (title: string, body: string, rcptType: "uname" | "room" | "group", rcpt: string) { + constructor (title: string, body: string, rcptType: "uid" | "room" | "group", rcpt: string) { let keys: VapidKeys = vapidKeys.keys this.options = { vapidDetails: { @@ -35,9 +35,9 @@ export class Message { this.rcpt = rcpt } - async findUserNotif(uname: string) { + async findUserNotif(uid: string) { var notif = await Notification.find().populate<{user: Pick & {_id: Types.ObjectId}}>('user', ['uname', '_id']).exec() - return notif.filter(val => val.user.uname == uname) + return notif.filter(val => val.user._id.toString() == uid) } async findRoomNotif(room: string) { @@ -54,9 +54,9 @@ export class Message { var subscriptions var rcptIds: Types.ObjectId[] switch (this.rcptType) { - case "uname": + case "uid": subscriptions = await this.findUserNotif(this.rcpt) - rcptIds = (await User.find({uname: this.rcpt})).map(v => v._id) + rcptIds = [new Types.ObjectId(this.rcpt)] break; case "room": subscriptions = await this.findRoomNotif(this.rcpt) diff --git a/src/routes/api/admin/keys.ts b/src/routes/api/admin/keys.ts index c631b2f..9c6f195 100644 --- a/src/routes/api/admin/keys.ts +++ b/src/routes/api/admin/keys.ts @@ -16,17 +16,15 @@ keysRouter.get("/", async (req, res) => { }) keysRouter.post("/", async (req, res) => { - var newKey: { - room: string; - whom: string; - } = req.body - var user = await User.findOne({uname: newKey.whom}) - if (user) { - newKey.whom = user._id.toString() - } else { + var user = await User.findById(req.body.whom._id) + if (!user) { return res.status(404).send("User not found").end() } - if (await Key.create(newKey)) { + const newKey = new Key({ + room: req.body.room, + whom: user._id + }) + if (await newKey.save()) { res.status(201).send({status: 201}) } else { res.sendStatus(500) diff --git a/src/routes/api/admin/notif/index.ts b/src/routes/api/admin/notif/index.ts index b084dcc..5051d49 100644 --- a/src/routes/api/admin/notif/index.ts +++ b/src/routes/api/admin/notif/index.ts @@ -3,9 +3,6 @@ import { Perms, adminPerm } from "@/utility"; import Group from "@schemas/Group"; import { PushResult, Message } from "@/notif"; import capability, { Features } from "@/helpers/capability"; -import Inbox from "@/schemas/Inbox"; -import { Types } from "mongoose"; -import { IUser } from "@/schemas/User"; import { outboxRouter } from "./outbox"; const notifRouter = Router() @@ -14,7 +11,7 @@ notifRouter.use(adminPerm(Perms.Notif)) notifRouter.use(capability.mw(Features.Notif)) type PushSendBody = {recp: - {type: "uname", uname: string} | + {type: "uid", uid: string} | {type: "room", room: string} | {type: "group", group: string}, title: string, @@ -24,8 +21,8 @@ type PushSendBody = {recp: notifRouter.post("/send", async (req: Request, res: Response) => { let recp: string switch (req.body.recp.type) { - case "uname": - recp = req.body.recp.uname + case "uid": + recp = req.body.recp.uid break; case "room": recp = req.body.recp.room From d50aa79c5da4a3acd6cd13fd1b943b5c6229ff9b Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Sun, 1 Jun 2025 21:48:49 +0200 Subject: [PATCH 11/13] feat: Added menu items and account security to settings --- src/helpers/usettings.ts | 6 ++++-- src/routes/api/admin/settings.ts | 4 ++-- src/utility.ts | 16 +++++++++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/helpers/usettings.ts b/src/helpers/usettings.ts index c4d06cb..14626a1 100644 --- a/src/helpers/usettings.ts +++ b/src/helpers/usettings.ts @@ -1,5 +1,7 @@ +import { project } from "@/utility"; import { readFileSync, writeFileSync } from "node:fs"; -interface IUSettings { + +export interface IUSettings { keyrooms: string[]; rooms: string[]; cleanThings: string[]; @@ -24,7 +26,7 @@ class UOptions { return this._settings; } public set settings(value: IUSettings) { - this._settings = value; + this._settings = project(value, ['cleanThings', 'keyrooms', 'menu', 'rooms', 'security']) as typeof value this.save() } diff --git a/src/routes/api/admin/settings.ts b/src/routes/api/admin/settings.ts index 15bc750..6b2192a 100644 --- a/src/routes/api/admin/settings.ts +++ b/src/routes/api/admin/settings.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { adminPerm, Perms, project } from "@/utility"; +import { adminPerm, Perms } from "@/utility"; import usettings from "@/helpers/usettings"; export const settingsRouter = Router() @@ -11,7 +11,7 @@ settingsRouter.get('/', (req, res) => { }) settingsRouter.post('/', (req, res) => { - usettings.settings = project(req.body, {keyrooms: true, cleanThings: true, rooms: true, menu: true, security: true}) + usettings.settings = req.body res.send({status: 200}) }) diff --git a/src/utility.ts b/src/utility.ts index 7705c89..cf676e7 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -38,12 +38,18 @@ var adminCond = (adminInt = 0, perm: Perms) => { return (adminInt & perm) == perm } -var project = (obj: any, projection: any) => { - let obj2: any = {} - for (let key in projection) { - if (key in obj) obj2[key] = obj[key] +export function project(obj: T | any, projection: (keyof T)[] | { [key in keyof T]: any}): Partial { + let obj2: Partial = {} + if (projection instanceof Array) { + for (let key of projection) { + if (key in obj) obj2[key] = obj[key] + } + } else { + for (let key in projection) { + if (key in obj) obj2[key] = obj[key] + } } return obj2 } -export {islogged, isadmin, adminPerm, Perms, adminCond, project}; \ No newline at end of file +export {islogged, isadmin, adminPerm, Perms, adminCond}; \ No newline at end of file From b7c84fbe14cbfecc02d2b215cba60df8523a4c5e Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Tue, 3 Jun 2025 13:24:02 +0200 Subject: [PATCH 12/13] feat: Added unchecked room highlighting --- src/helpers/attendence.ts | 6 +++--- src/routes/api/admin/clean.ts | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/helpers/attendence.ts b/src/helpers/attendence.ts index 5aed945..112e21d 100644 --- a/src/helpers/attendence.ts +++ b/src/helpers/attendence.ts @@ -25,14 +25,14 @@ class Attendence { this.attendence.delete(room) } - getRoom (room: string) { + getRoom (room: string): IAttendence | undefined { return this.attendence.get(room) } summary () { - var summary: {room: string, hours: string[], notes: string}[] = [] + var summary: {room: string, hours: string[], notes: string, auto: boolean}[] = [] this.attendence.forEach((v, k) => { - summary.push({room: k, hours: v.auto.map(i => i.hour), notes: v.notes}) + summary.push({room: k, hours: v.auto.map(i => i.hour), notes: v.notes, auto: false}) }) return summary } diff --git a/src/routes/api/admin/clean.ts b/src/routes/api/admin/clean.ts index a0f48f6..29f8975 100644 --- a/src/routes/api/admin/clean.ts +++ b/src/routes/api/admin/clean.ts @@ -88,7 +88,12 @@ cleanRouter.delete('/attendence/:room', async (req, res) => { }) cleanRouter.get('/attendenceSummary', async (req, res) => { - res.send(attendence.summary()) + var allRooms = usettings.settings.rooms + var graded = (await Grade.find({date: new Date().setUTCHours(24,0,0,0)})).map(v => v.room) + var ungraded = allRooms.filter(x => !graded.includes(x)) + var summary = attendence.summary() + var unchecked: typeof summary = ungraded.filter(x => !summary.map(v => v.room).includes(x)).map(v => ({room: v, hours: [] as string[], notes: "Nie sprawdzono", auto: true})) + res.send([...summary, ...unchecked]) }) export {cleanRouter} \ No newline at end of file From 5b87634813faa9b16c0244e015a6fddac90bdc8c Mon Sep 17 00:00:00 2001 From: Jan Szumotalski Date: Tue, 3 Jun 2025 13:26:24 +0200 Subject: [PATCH 13/13] chore: Bumped version numbers --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 003ae76..33358e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "backend2", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 2d9d18c..b292b2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "backend2", - "version": "1.0.1", + "version": "1.1.0", "description": "", "main": "src/index.js", "type": "module",