import * as express from "express"; import * as bodyparser from "body-parser"; import { Db } from "mongodb"; import { authRoute } from "../security/token"; import { userFromSession } from "../utils/user"; import { IUser, IUserDBModel } from "../models/user"; import { ISM2Metadata } from "../models/review"; import { LRequest } from "../types/express"; const userRouter = express.Router(); userRouter.use(authRoute); userRouter.use(bodyparser.json()); // Return the user object if the user is still authenticated userRouter.get("/me", async (req: LRequest, res) => { const { db, token } = req; const session = await db.collection("sessions").findOne({ token, }); if (session !== null) { const user = await db.collection("users").findOne({ username: session.username }); // Copy and strip unneeded attributes let copy = Object.assign({}, user, { sessionToken: token, }); delete copy._id; delete copy.hash; delete copy.salt; res.send({ error: "0", data: copy, }); } else { res.send({ error: "404", data: {}, }); } }); // Removes the user's session userRouter.get("/logout", async (req: LRequest, res) => { // Try to remove the session const { db, token } = req; await db.collection("sessions").findOneAndDelete({ token, }); res.send({ error: "0", data: {}, }); }); // TODO: This should be shared with the frontend, to remove code duplication export enum VocabType { NOMEN = 0, VERB = 1, ADJEKTIV = 2, ADVERB = 3, }; // Gets the user's review queue userRouter.get("/queue", async (req: LRequest, res) => { // TODO: if (user) const { token, db } = req; let user = Object.assign({}, await userFromSession(token, db)); const sm2 = user.vocabMetadata; const data = Object.keys(sm2).map(id => Object.assign({}, sm2[parseInt(id)], { id, })); const sorted = data.sort((a: any, b: any) => { if (a.nextDueDate > b.nextDueDate) return 1; if (a.nextDueDate < b.nextDueDate) return -1; return 0; }).slice(0, 20) .map((el: any) => { return parseInt(el.id); }); // Fetch all vocab ids from the vocabulary collection const vocabRaw = await db.collection("vocabulary").find({ id: { $in: sorted }, }, { // TODO: Make this configurable? sort: { }, limit: 20, }).toArray(); // Remove unneeded data const vocab = vocabRaw.map(el => { let tmp = Object.assign({}, el); delete tmp._id; return tmp; }); res.send({ error: "0", data: { queue: vocab, }, }); }); // Get ot set the last review results userRouter.get("/lastReview", async (req: LRequest, res) => { // TODO: if(user) const { token, db } = req; const user = await userFromSession(token, db); res.send({ error: "0", data: user.lastReview, }); }); function dateInNDays(n: number): number { let today = new Date(); today.setDate(today.getDate() + n); return Date.parse(today.toString()); } userRouter.post("/lastReview", async (req: LRequest, res) => { // Check if we get the needed data if (!req.body || !("meta" in req.body) || !("sm2" in req.body)) { res.send({ error: "400", data: { msg: "Last review state or SM2 data not specified!", }, }); return; } const { token, db } = req; let user = Object.assign({}, await userFromSession(token, db)); if (!user) { res.send({ error: "404", data: { msg: "Failed to find user with the specified session!", }, }); return; } Object.keys(req.body.sm2).forEach((id: string) => { const vocabId = parseInt(id); const correct: boolean = req.body.sm2[id]; let vocab_sm2: ISM2Metadata = Object.assign({}, user.vocabMetadata[vocabId]); // TODO: Tuning? // TODO: Move into another module const perf = correct ? 3 : 1; vocab_sm2.easiness += -0.8 + 0.28 * perf + 0.02 * Math.pow(perf, 2); // Update the consecutive correct answers and the due date if (correct) { vocab_sm2.consecutiveCorrectAnswers += 1; vocab_sm2.nextDueDate = dateInNDays(6 * Math.pow(vocab_sm2.easiness, vocab_sm2.consecutiveCorrectAnswers - 1)); } else { vocab_sm2.consecutiveCorrectAnswers = 0; vocab_sm2.nextDueDate = dateInNDays(1); } user.vocabMetadata[vocabId] = vocab_sm2; }); // Update the last review user.lastReview = req.body.meta; // TODO: Error handling await db.collection("users").updateOne({ username: user.username, }, { $set: { lastReview: user.lastReview, vocabMetadata: user.vocabMetadata, }, }); res.send({ error: "0", data: {}, }); }); // Returns the next level (level + 1) or the current level, if no higher level // can be found async function getNextLevel(token: string, db: Db): Promise { // TODO: if(user) const user = await userFromSession(token, db); const { lastLevel } = user; // Try to find a level, which level is lastLevel + 1 const level = await db.collection("levels").findOne({ level: lastLevel + 1, }); if (level) { return level; } else { // TODO: Send different data, so that the Client can say "Hey, no more levels" return await db.collection("levels").findOne({ level: lastLevel }); } } // Get the next level userRouter.get("/nextLevel", async (req: LRequest, res) => { const level = await getNextLevel(req.token, req.db); res.send({ error: "0", data: level, }); }); // Mark a level as done userRouter.post("/level/:id", async (req: LRequest, res) => { // Is everything specified? if (!req.params || !req.params.id) { res.send({ error: "400", data: { msg: "No level ID specified", }, }); return; } const id = parseInt(req.params.id); if (id === NaN) { res.send({ error: "400", data: { msg: "Invalid level ID", }, }); return; } // TODO: if (user) const { token, db } = req; const user = await userFromSession(token, db); if (id in user.levels) { // Nothing } else { // The level is new to the user // Is the new level higher than the "highest" already completed level? let update = { levels: user.levels.concat(id), }; if (id > Math.max(...user.levels)) { // TODO: Add the levels vocabulary to the users review queue // Also update the lastLevel attribute Object.assign(update, { lastLevel: id }); } await db.collection("users").updateOne({ username: user.username, }, { $set: update, }); } res.send({ error: "200", data: {}, }); }); // Get the data needed for the dashboard userRouter.get("/dashboard", async (req: LRequest, res) => { const { db, token } = req; // Get the user // TODO: if (user) const user = await userFromSession(token, db); const { classId } = user; // Fetch the top ten of the class const rawTopTen = await db.collection("users").find({ classId, }, { sort: { score: -1, }, limit: 10, }).toArray(); let nr = 1; const topTen = rawTopTen.map((user: IUser) => { return { username: user.username, score: user.score, // TODO: Calculate on the client? level: 1, nr: nr++, }; }); const nextLevel = await getNextLevel(token, db); res.send({ error: "200", data: { nextLevel, topTen, lastReview: user.lastReview, }, }); }); userRouter.post("/showWelcome", async (req: LRequest, res) => { const { db, token } = req; // Are all arguments specified? if (!req.body || !("state" in req.body)) { res.send({ error: "400", data: { msg: "State not specified", }, }); return; } // Find the user // TODO: if (user) const user = await userFromSession(token, db); await db.collection("users").updateOne({ username: user.username, }, { $set: { showWelcome: req.body.state, }, }); res.send({ error: "200", data: {}, }); }); userRouter.post("/score", async (req: LRequest, res) => { const { token, db } = req; // Are all arguments specified if (!req.body || !("score" in req.body)) { res.send({ error: "400", data: { msg: "No score specified!", }, }); return; } // TODO: if (user) const user = await userFromSession(token, db); // Update the score db.collection("users").updateOne({ username: user.username, }, { $set: { score: req.body.score, }, }); }); export default userRouter;