Merge pull request #4 from Slasherss1/1.1.0

v1.1.0
This commit is contained in:
2025-06-03 13:31:08 +02:00
committed by GitHub
27 changed files with 458 additions and 193 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "backend2", "name": "backend2",
"version": "1.0.1", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "backend2", "name": "backend2",
"version": "1.0.1", "version": "1.1.0",
"description": "", "description": "",
"main": "src/index.js", "main": "src/index.js",
"type": "module", "type": "module",

View File

@@ -25,14 +25,14 @@ class Attendence {
this.attendence.delete(room) this.attendence.delete(room)
} }
getRoom (room: string) { getRoom (room: string): IAttendence | undefined {
return this.attendence.get(room) return this.attendence.get(room)
} }
summary () { summary () {
var summary: {room: string, hours: string[], notes: string}[] = [] var summary: {room: string, hours: string[], notes: string, auto: boolean}[] = []
this.attendence.forEach((v, k) => { 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 return summary
} }

57
src/helpers/security.ts Normal file
View File

@@ -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<string, IAccTimeout>();
private onTimeout = new Map<string, Job>(); // 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()

View File

@@ -1,5 +1,7 @@
import { project } from "@/utility";
import { readFileSync, writeFileSync } from "node:fs"; import { readFileSync, writeFileSync } from "node:fs";
interface IUSettings {
export interface IUSettings {
keyrooms: string[]; keyrooms: string[];
rooms: string[]; rooms: string[];
cleanThings: string[]; cleanThings: string[];
@@ -8,6 +10,13 @@ interface IUSettings {
sn: string[]; sn: string[];
kol: string[]; kol: string[];
} }
},
security: {
loginTimeout: {
attempts: number;
time: number;
lockout: number;
}
} }
} }
@@ -17,7 +26,7 @@ class UOptions {
return this._settings; return this._settings;
} }
public set settings(value: IUSettings) { public set settings(value: IUSettings) {
this._settings = value; this._settings = project<typeof value>(value, ['cleanThings', 'keyrooms', 'menu', 'rooms', 'security']) as typeof value
this.save() this.save()
} }

View File

@@ -7,9 +7,10 @@ import session from "express-session";
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import MongoStore from "connect-mongo"; import MongoStore from "connect-mongo";
import mongoose from "mongoose" import mongoose from "mongoose"
import User from "./schemas/User"; import User, { IUser } from "./schemas/User";
import routes from "./routes/index"; import routes from "./routes/index";
import process from "node:process" import process from "node:process"
import security from "./helpers/security";
const connectionString = process.env.ATLAS_URI || "mongodb://mongodb:27017/ipwa"; const connectionString = process.env.ATLAS_URI || "mongodb://mongodb:27017/ipwa";
if (!process.env.DOMAIN) { if (!process.env.DOMAIN) {
@@ -19,13 +20,13 @@ if (!process.env.DOMAIN) {
declare global { declare global {
namespace Express { namespace Express {
export interface User { export interface User extends IUser {
_id: mongoose.Types.ObjectId; _id: mongoose.Types.ObjectId;
pass: string; // pass: string;
uname: string; // uname: string;
admin?: number; // admin?: number;
locked?: boolean; // locked?: boolean;
room?: string // room?: string
} }
} }
} }
@@ -35,7 +36,7 @@ var app = express();
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true})) app.use(bodyParser.urlencoded({extended: true}))
app.use(cors({ app.use(cors({
origin: ["http://localhost:4200", "http://localhost:3000", `https://${process.env.DOMAIN}`,], origin: ["http://localhost:4200", `https://${process.env.DOMAIN}`,],
credentials: true credentials: true
})) }))
app.use(session({ app.use(session({
@@ -55,12 +56,20 @@ app.use(passport.session())
passport.use("normal",new LocalStrategy(async function verify(uname,pass,done) { passport.use("normal",new LocalStrategy(async function verify(uname,pass,done) {
let query = await User.findOne({uname: uname.toLowerCase()}) let query = await User.findOne({uname: uname.toLowerCase()})
if (query) { 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)) { if (await bcrypt.compare(pass, query.pass)) {
return done(null, query) return done(null, query)
} else done(null, false) } else {
security.addAttempt(query._id)
done({type: "unf"}, false)
}
} else { } else {
done(null, false) done({type: "unf"}, false)
} }
})) }))
//#endregion //#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); await mongoose.connect(connectionString);
if (process.send) process.send("ready") if (process.send) process.send("ready")
}) })
@@ -86,5 +95,6 @@ app.listen(8080, async () => {
app.use('/', routes) app.use('/', routes)
process.on('SIGINT', () => { process.on('SIGINT', () => {
server.close()
mongoose.disconnect().then(() => process.exit(0), () => process.exit(1)) mongoose.disconnect().then(() => process.exit(0), () => process.exit(1))
}) })

View File

@@ -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 Notification from "./schemas/Notification";
import vapidKeys from "./vapidKeys"; 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 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 let keys: VapidKeys = vapidKeys.keys
this.options = { this.options = {
vapidDetails: { vapidDetails: {
@@ -14,27 +30,66 @@ export class NotifcationHelper {
publicKey: keys.publicKey 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<IUser, 'uname'> & {_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<IUser, 'room'> & {_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<IUser, 'groups'> & {_id: Types.ObjectId}}>('user', ['groups', '_id']).exec()
return notif.filter(val => val.user.groups.find(x => x.toString() == groupId))
}
public async send(): Promise<PushResult> {
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 count = 0;
var subslen = subscriptions.length var subslen = subscriptions.length
for (const v of subscriptions) { for (const v of subscriptions) {
var result var result: SendResult
try { try {
result = await sendNotification(v, message, this.options) result = await sendNotification(v, JSON.stringify(this.message), this.options)
count++ count++
} catch (error) { } catch (error) {
if (error instanceof WebPushError) { if (error instanceof WebPushError) {
switch (error.statusCode) { switch (error.statusCode) {
case 410: case 410:
console.log("GONE") console.log("GONE")
await Notification.findOneAndDelete({endpoint: v.endpoint, keys: v.keys}) await Notification.findByIdAndRemove(v._id)
subslen-- subslen--
break; break;
case 404: case 404:
console.warn("NOT FOUND", error.message) console.warn("NOT FOUND", error.message)
await Notification.findOneAndDelete(v) await Notification.findByIdAndRemove(v._id)
subslen-- subslen--
break; break;
default: default:
@@ -44,39 +99,7 @@ export class NotifcationHelper {
} }
} }
} }
return {sent: count, possible: subslen} 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<Array<any>> {
var notif = await Notification.find().populate<{user: Pick<IUser, 'uname'>}>('user', 'uname').exec()
return notif.filter(val => val.user.uname == uname)
}
async findRoomNotif(room: string): Promise<Array<any>> {
var notif = await Notification.find().populate<{user: Pick<IUser, 'room'>}>('user', 'room').exec()
return notif.filter(val => val.user.room == room)
}
async findGroupNotif(groupId: string): Promise<Array<any>> {
var notif = await Notification.find().populate<{user: Pick<IUser, 'groups'>}>('user', 'groups').exec()
return notif.filter(val => val.user.groups.find(x => x.toString() == groupId))
}
} }

View File

@@ -1,8 +1,10 @@
import User from "@schemas/User"; import User from "@schemas/User";
import { Router } from "express" import { Router } from "express"
import { Perms, adminCond, adminPerm } from "@/utility"; import { Perms, adminCond, adminPerm } from "@/utility";
import capability from "@/capability"; import capability from "@/helpers/capability";
import Group from "@/schemas/Group"; import Group from "@/schemas/Group";
import security from "@/helpers/security";
import { Types } from "mongoose";
const accsRouter = Router() const accsRouter = Router()
@@ -16,6 +18,13 @@ accsRouter.get('/', async (req, res)=> {
res.send(data) 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)=> { accsRouter.post('/', async (req, res)=> {
if (req.body.uname == "admin") return res.status(400).send("This name is reserved").end() if (req.body.uname == "admin") return res.status(400).send("This name is reserved").end()
if (req.body.flags) { if (req.body.flags) {
@@ -39,7 +48,7 @@ accsRouter.put('/:id', async (req, res)=> {
res.status(404).send("User not found") res.status(404).send("User not found")
return return
} }
if (req.body.flags != undefined) { if (req.body.flags) {
if (adminCond(req.user.admin, Perms.Superadmin)) { if (adminCond(req.user.admin, Perms.Superadmin)) {
if (adminCond(user.admin, Perms.Superadmin)) { if (adminCond(user.admin, Perms.Superadmin)) {
res.status(400).send("Cannot edit other superadmins") 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}; export {accsRouter};

View File

@@ -1,10 +1,10 @@
import { Router } from "express"; import { Router } from "express";
import { Perms, adminPerm } from "@/utility"; import { Perms, adminPerm } from "@/utility";
import capability, { Features } from "@/capability"; import capability, { Features } from "@/helpers/capability";
import usettings from "@/usettings"; import usettings from "@/helpers/usettings";
import Grade from "@schemas/Grade"; import Grade from "@schemas/Grade";
import User from "@/schemas/User"; import User from "@/schemas/User";
import attendence from "@/attendence"; import attendence from "@/helpers/attendence";
const cleanRouter = Router() const cleanRouter = Router()
cleanRouter.use(adminPerm(Perms.Clean)) cleanRouter.use(adminPerm(Perms.Clean))
@@ -88,7 +88,12 @@ cleanRouter.delete('/attendence/:room', async (req, res) => {
}) })
cleanRouter.get('/attendenceSummary', 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} export {cleanRouter}

View File

@@ -1,7 +1,7 @@
import Group from "@schemas/Group"; import Group from "@schemas/Group";
import { Router } from "express" import { Router } from "express"
import { Perms, adminPerm } from "@/utility"; import { Perms, adminPerm } from "@/utility";
import capability, { Features } from "@/capability"; import capability, { Features } from "@/helpers/capability";
const groupsRouter = Router() const groupsRouter = Router()

View File

@@ -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)
})

View File

@@ -1,7 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import capability, { Features } from "@/capability"; import capability, { Features } from "@/helpers/capability";
import Key from "@schemas/Key"; import Key from "@schemas/Key";
import usettings from "@/usettings"; import usettings from "@/helpers/usettings";
import User, { IUser } from "@schemas/User"; import User, { IUser } from "@schemas/User";
import { Perms, adminPerm } from "@/utility"; import { Perms, adminPerm } from "@/utility";
@@ -16,17 +16,15 @@ keysRouter.get("/", async (req, res) => {
}) })
keysRouter.post("/", async (req, res) => { keysRouter.post("/", async (req, res) => {
var newKey: { var user = await User.findById(req.body.whom._id)
room: string; if (!user) {
whom: string;
} = req.body
var user = await User.findOne({uname: newKey.whom})
if (user) {
newKey.whom = user._id.toString()
} else {
return res.status(404).send("User not found").end() 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}) res.status(201).send({status: 201})
} else { } else {
res.sendStatus(500) res.sendStatus(500)

View File

@@ -4,9 +4,9 @@ import multer from "multer"
import * as XLSX from "xlsx" import * as XLSX from "xlsx"
import Menu from "@schemas/Menu" import Menu from "@schemas/Menu"
import Vote from "@schemas/Vote" import Vote from "@schemas/Vote"
import capability, { Features } from "@/capability" import capability, { Features } from "@/helpers/capability"
import { editorRouter } from "./editor" import { editorRouter } from "./editor"
import usettings from "@/usettings" import usettings from "@/helpers/usettings"
const menuRouter = Router() const menuRouter = Router()

View File

@@ -1,7 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import News from "@schemas/News" import News from "@schemas/News"
import { Perms, adminPerm } from "@/utility"; import { Perms, adminPerm } from "@/utility";
import capability, { Features } from "@/capability"; import capability, { Features } from "@/helpers/capability";
const newsRouter = Router() const newsRouter = Router()

View File

@@ -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}

View File

@@ -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<undefined, PushResult, PushSendBody>, res: Response<PushResult>) => {
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 }

View File

@@ -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<IUser, "uname" | "room" | "fname" | "surname">}>({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"})
}
})

View File

@@ -1,6 +1,6 @@
import { Router } from "express"; import { Router } from "express";
import { adminPerm, Perms, project } from "@/utility"; import { adminPerm, Perms } from "@/utility";
import usettings from "@/usettings"; import usettings from "@/helpers/usettings";
export const settingsRouter = Router() export const settingsRouter = Router()
@@ -11,7 +11,7 @@ settingsRouter.get('/', (req, res) => {
}) })
settingsRouter.post('/', (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}) res.send({status: 200})
}) })

View File

@@ -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};

View File

@@ -4,12 +4,14 @@ import News from "@schemas/News";
import Menu from "@schemas/Menu"; import Menu from "@schemas/Menu";
import Vote from "@schemas/Vote"; import Vote from "@schemas/Vote";
import { vote } from "@/pipelines/vote"; import { vote } from "@/pipelines/vote";
import capability, { Features } from "@/capability"; import capability, { Features } from "@/helpers/capability";
import Key, { IKey } from "@schemas/Key"; import Key, { IKey } from "@schemas/Key";
import usettings from "@/usettings"; import usettings from "@/helpers/usettings";
import Grade from "@schemas/Grade"; import Grade from "@schemas/Grade";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
const appRouter = Router(); import Inbox from "@/schemas/Inbox";
export const appRouter = Router();
appRouter.use(islogged) appRouter.use(islogged)
@@ -75,4 +77,26 @@ appRouter.get("/clean/:date", capability.mw(Features.Clean), async (req, res) =>
})) }))
}) })
export {appRouter}; 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"})
}
})

8
src/routes/api/index.ts Normal file
View File

@@ -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)

View File

@@ -3,16 +3,44 @@ import passport from "passport";
import User from "@schemas/User"; import User from "@schemas/User";
import { islogged } from "@/utility"; import { islogged } from "@/utility";
import bcrypt from "bcryptjs" import bcrypt from "bcryptjs"
import cap from "@/capability"; import cap from "@/helpers/capability";
import usettings from "@/usettings"; import usettings from "@/helpers/usettings";
import { readFileSync } from "node:fs";
import vapidKeys from "@/vapidKeys"; import vapidKeys from "@/vapidKeys";
import { IVerifyOptions } from "passport-local";
const authRouter = Router() const authRouter = Router()
authRouter.post("/login", passport.authenticate('normal'), (req, res) => { authRouter.post("/login", (req, res) => {
if (req.user.admin != null) res.send({status: 200, admin: req.user.admin}) passport.authenticate('normal', (err: {type: string, message: string} | null, user?: Express.User | false, options?: IVerifyOptions) => {
else res.send({status: 200}) 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) => { authRouter.post("/chpass", islogged, async (req,res) => {
@@ -51,10 +79,20 @@ authRouter.get("/check", islogged, (req, res, next) => {
if (req.user.locked) { if (req.user.locked) {
req.logout((err) => { req.logout((err) => {
if (err) next(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}) 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 }; export { authRouter };

View File

@@ -1,22 +1,30 @@
import { Router } from "express"; import { Router } from "express";
import Notification from "@schemas/Notification"; import Notification from "@schemas/Notification";
import { islogged } from "@/utility"; import { islogged } from "@/utility";
import { adminRouter } from "./api/adminRouter";
import { appRouter } from "./api/appRouter";
import { authRouter } from "./auth/index"; import { authRouter } from "./auth/index";
import { Schema } from 'mongoose' import capability, { Features } from "@/helpers/capability";
import capability, { Features } from "@/capability"; import mongoose from "mongoose";
import { apiRouter } from "./api";
const router = Router(); const router = Router();
router.use('/app', appRouter) router.use('/', apiRouter)
router.use('/admin', adminRouter)
router.use('/auth', authRouter) 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) => { router.post("/notif", islogged, capability.mw(Features.Notif), async (req, res) => {
var obj = {user: req.user._id, ...req.body} var obj = {user: req.user._id, ...req.body}
await Notification.findOneAndUpdate(obj, obj, {upsert: true}) await Notification.findOneAndUpdate(obj, obj, {upsert: true})
res.send({"status": 200}) res.send({"status": 200})
}) })
router.use("/", apiRouter)
export default router; export default router;

18
src/schemas/Inbox.ts Normal file
View File

@@ -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<IInbox>({
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)

View File

@@ -1,7 +1,5 @@
import mongoose, { Types, Schema } from "mongoose" import mongoose, { Types, Schema } from "mongoose"
// TODO: Unify `fname` and `surename` into single field
export interface IUser { export interface IUser {
uname: string; uname: string;
pass: string; pass: string;
@@ -11,17 +9,23 @@ export interface IUser {
fname?: string; fname?: string;
surname?: string; surname?: string;
groups: Types.ObjectId[]; groups: Types.ObjectId[];
regDate: Date;
defaultPage: string;
} }
const userSchema = new Schema<IUser>({ const userSchema = new Schema<IUser>({
uname: {type: String, required: true}, uname: {type: String, required: true},
pass: {type: String, required: true, default: "$2y$10$wxDhf.XiXkmdKrFqYUEa0.F4Bf.pDykZaMmgjvyLyeRP3E/Xy0hbC"}, pass: {type: String, required: true, default: "$2y$10$wxDhf.XiXkmdKrFqYUEa0.F4Bf.pDykZaMmgjvyLyeRP3E/Xy0hbC"},
room: String, room: {type: String, default: ""},
admin: Number, admin: Number,
locked: {type: Boolean, default: false}, locked: {type: Boolean, default: false},
fname: String, fname: {type: String, default: ""},
surname: String, surname: {type: String, default: ""},
groups: [{type: mongoose.Types.ObjectId, ref: "Group"}] 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) export default mongoose.model("logins", userSchema)

View File

@@ -8,7 +8,7 @@ var islogged = (req: Request, res: Response, next: NextFunction) => {
} }
var isadmin = (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() return next()
} }
res.sendStatus(401) res.sendStatus(401)
@@ -38,12 +38,18 @@ var adminCond = (adminInt = 0, perm: Perms) => {
return (adminInt & perm) == perm return (adminInt & perm) == perm
} }
var project = (obj: any, projection: any) => { export function project<T extends object>(obj: T | any, projection: (keyof T)[] | { [key in keyof T]: any}): Partial<T> {
let obj2: any = {} let obj2: Partial<T> = {}
for (let key in projection) { if (projection instanceof Array) {
if (key in obj) obj2[key] = obj[key] 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 return obj2
} }
export {islogged, isadmin, adminPerm, Perms, adminCond, project}; export {islogged, isadmin, adminPerm, Perms, adminCond};