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", diff --git a/src/attendence.ts b/src/helpers/attendence.ts similarity index 88% rename from src/attendence.ts rename to src/helpers/attendence.ts index 5aed945..112e21d 100644 --- a/src/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/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/helpers/security.ts b/src/helpers/security.ts new file mode 100644 index 0000000..741e8a6 --- /dev/null +++ b/src/helpers/security.ts @@ -0,0 +1,57 @@ +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 + } + } + + clearAcc(userId: string) { + return this.onTimeout.delete(userId) + } +} + +export default new SecurityHelper() \ No newline at end of file diff --git a/src/usettings.ts b/src/helpers/usettings.ts similarity index 70% rename from src/usettings.ts rename to src/helpers/usettings.ts index 2cd73e0..14626a1 100644 --- a/src/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[]; @@ -8,6 +10,13 @@ interface IUSettings { sn: string[]; kol: string[]; } + }, + security: { + loginTimeout: { + attempts: number; + time: number; + lockout: number; + } } } @@ -17,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/index.ts b/src/index.ts index 7c448d0..46bc8db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,10 @@ 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"; const connectionString = process.env.ATLAS_URI || "mongodb://mongodb:27017/ipwa"; if (!process.env.DOMAIN) { @@ -19,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 } } } @@ -35,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({ @@ -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 @@ -78,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") }) @@ -86,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/notif.ts b/src/notif.ts index f330b8f..538e911 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: "uid" | "room" | "group" + private rcpt: string + + constructor (title: string, body: string, rcptType: "uid" | "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(uid: string) { + var notif = await Notification.find().populate<{user: Pick & {_id: Types.ObjectId}}>('user', ['uname', '_id']).exec() + return notif.filter(val => val.user._id.toString() == uid) + } + + 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 "uid": + subscriptions = await this.findUserNotif(this.rcpt) + rcptIds = [new Types.ObjectId(this.rcpt)] + 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/accs.ts b/src/routes/api/admin/accs.ts index d692231..f47d83f 100644 --- a/src/routes/api/admin/accs.ts +++ b/src/routes/api/admin/accs.ts @@ -1,8 +1,10 @@ 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"; +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) { @@ -39,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") @@ -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/routes/api/admin/clean.ts b/src/routes/api/admin/clean.ts index 57deec4..29f8975 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)) @@ -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 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/index.ts b/src/routes/api/admin/index.ts new file mode 100644 index 0000000..b47e3aa --- /dev/null +++ b/src/routes/api/admin/index.ts @@ -0,0 +1,28 @@ +import { Router } from "express"; +import { islogged, isadmin} from "@/utility"; +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"; +import User from "@/schemas/User"; + +export const adminRouter = Router() + +adminRouter.use(islogged, isadmin) +adminRouter.use('/news', newsRouter) +adminRouter.use('/accs', accsRouter) +adminRouter.use('/menu', menuRouter) +adminRouter.use('/groups', groupsRouter) +adminRouter.use('/notif', notifRouter) +adminRouter.use('/keys', keysRouter) +adminRouter.use('/clean', cleanRouter) +adminRouter.use('/settings', settingsRouter) + +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/routes/api/admin/keys.ts b/src/routes/api/admin/keys.ts index 106c57b..9c6f195 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"; @@ -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/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 deleted file mode 100644 index c7c9f4f..0000000 --- a/src/routes/api/admin/notif.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Router } from "express"; -import { Perms, adminPerm } from "@/utility"; -import Group from "@schemas/Group"; -import { NotifcationHelper } from "@/notif"; -import capability, { Features } from "@/capability"; - -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) - 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; - } - console.log(` - From: ${req.user.uname} (${req.user._id}) - To: ${recp} - Subject: ${req.body.title} - - ${req.body.body} - `); - res.send(result) -}) - -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/routes/api/admin/notif/index.ts b/src/routes/api/admin/notif/index.ts new file mode 100644 index 0000000..5051d49 --- /dev/null +++ b/src/routes/api/admin/notif/index.ts @@ -0,0 +1,56 @@ +import { Request, Response, Router } from "express"; +import { Perms, adminPerm } from "@/utility"; +import Group from "@schemas/Group"; +import { PushResult, Message } from "@/notif"; +import capability, { Features } from "@/helpers/capability"; +import { outboxRouter } from "./outbox"; + +const notifRouter = Router() + +notifRouter.use(adminPerm(Perms.Notif)) +notifRouter.use(capability.mw(Features.Notif)) + +type PushSendBody = {recp: + {type: "uid", uid: string} | + {type: "room", room: string} | + {type: "group", group: string}, + title: string, + body: string +} + +notifRouter.post("/send", async (req: Request, res: Response) => { + let recp: string + switch (req.body.recp.type) { + case "uid": + recp = req.body.recp.uid + break; + case "room": + recp = req.body.recp.room + break; + case "group": + if (!capability.settings.groups) return res.sendStatus(406).end() + recp = req.body.recp.group + 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} + Subject: ${req.body.title} + + ${req.body.body} + `); + res.send(result) +}) + +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/admin/settings.ts b/src/routes/api/admin/settings.ts index 70d5959..6b2192a 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 { 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}) + usettings.settings = req.body res.send({status: 200}) }) diff --git a/src/routes/api/adminRouter.ts b/src/routes/api/adminRouter.ts deleted file mode 100644 index 6e2087a..0000000 --- a/src/routes/api/adminRouter.ts +++ /dev/null @@ -1,29 +0,0 @@ -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"; - -const adminRouter = Router() - -adminRouter.use(islogged, isadmin) -adminRouter.use('/news', newsRouter) -adminRouter.use('/accs', accsRouter) -adminRouter.use('/menu', menuRouter) -adminRouter.use('/groups', groupsRouter) -adminRouter.use('/notif', notifRouter) -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']]) -}) - -export {adminRouter}; \ No newline at end of file diff --git a/src/routes/api/appRouter.ts b/src/routes/api/app/index.ts similarity index 72% rename from src/routes/api/appRouter.ts rename to src/routes/api/app/index.ts index 72543d9..be3642b 100644 --- a/src/routes/api/appRouter.ts +++ b/src/routes/api/app/index.ts @@ -4,12 +4,14 @@ 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(); +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/auth/index.ts b/src/routes/auth/index.ts index 52db104..2528f8a 100644 --- a/src/routes/auth/index.ts +++ b/src/routes/auth/index.ts @@ -3,16 +3,44 @@ 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"; +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 { + res.send({status: 200, admin: req.user.admin || undefined, redirect: req.user.defaultPage}) + } + }) + } 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) => { @@ -51,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/routes/index.ts b/src/routes/index.ts index fe18dc7..b137bef 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,22 +1,30 @@ 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 { Schema } from 'mongoose' -import capability, { Features } from "@/capability"; +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) => { + 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}) res.send({"status": 200}) }) +router.use("/", apiRouter) + export default router; \ 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 fa9e821..ecc482f 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; @@ -11,17 +9,23 @@ export interface IUser { fname?: string; 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, - groups: [{type: mongoose.Types.ObjectId, ref: "Group"}] + fname: {type: String, default: ""}, + surname: {type: String, default: ""}, + groups: [{type: mongoose.Types.ObjectId, ref: "Group"}], + regDate: {type: Date, default: Date.now}, + 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 diff --git a/src/utility.ts b/src/utility.ts index 504bd8d..cf676e7 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) @@ -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