This repository has been archived on 2022-03-12. You can view files and clone it, but cannot push or open issues or pull requests.
Lateinicus/backend/src/api/user.ts
Alexander Polynomdivision 3b7e55d957 refactor: Simplify API calls
API calls can now make with a simple wrapper function.

Additionally, the error code "200" now means success for all API calls.
2018-10-17 18:28:29 +02:00

432 lines
11 KiB
TypeScript

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 {
ISchedulingData, AnswerType, updateSchedulingData
} from "../algorithms/sm2";
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;
delete copy.lastReview;
delete copy.lastLevel;
delete copy.vocabMetadata;
res.send({
error: "200",
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: "200",
data: {},
});
});
userRouter.get("/vocab", async (req: LRequest, res) => {
// Get the user
const { db, token } = req;
// TODO: if (!user)
const user = await userFromSession(token, db);
const { vocabMetadata } = await db.collection("users").findOne({
username: user.username,
});
if (!vocabMetadata) {
res.send({
error: "500",
data: {
msg: "Failed to retrieve Review Queue",
},
});
return;
}
const vocabIds = Object.keys(vocabMetadata).map(id => parseInt(id));
// Extract the vocabulary items
// TODO: Errorhandling
const vocab = await db.collection("vocabulary").find({
id: {
$in: vocabIds,
},
}).toArray();
res.send({
error: "200",
data: vocab,
});
});
// 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: "200",
data: {
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: "200",
data: user.lastReview,
});
});
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) || !("delta" 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;
}
// NOTE: Oh oh
// Update the user's score based on what they sent us
user.score += parseInt(req.body.delta);
// Itterate over all of the vocabulary that needs to have its
// metadata updated.
// Use our SM2 implementation to update the vocabulary and modify our
// copy of the user object, so that we can write the copied (and modified)
// user object back into the database.
Object.keys(req.body.sm2).forEach((id: string) => {
const vocabId = parseInt(id);
const correct: boolean = req.body.sm2[id];
let vocab_sm2: ISchedulingData = Object.assign({},
user.vocabMetadata[vocabId]);
const answer = correct ? AnswerType.CORRECT : AnswerType.WRONG;
user.vocabMetadata[vocabId] = updateSchedulingData(vocab_sm2, answer);
});
// Update the last review
user.lastReview = req.body.meta;
// NOTE: Error handling could be implemented here, but we can ensure that
// the user exists, as we just got his username (and entire data)
// from `userFromSession`.
await db.collection("users").updateOne({
username: user.username,
}, {
$set: {
lastReview: user.lastReview,
vocabMetadata: user.vocabMetadata,
score: user.score,
},
});
res.send({
error: "200",
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<any> {
// 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: "200",
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: if (!level)
const level = await db.collection("levels").findOne({
level: id,
});
// Convert the level's vocabulary to SM2 metadata
let sm2: { [id: number]: ISchedulingData } = {};
level.vocab.forEach((id: number) => {
sm2[id] = {
easiness: 1.3,
consecutiveCorrectAnswers: 0,
nextDueDate: Date.parse((new Date()).toString()),
};
});
const newVocabMetadata = Object.assign({}, user.vocabMetadata, sm2);
// Also update the lastLevel attribute
Object.assign(update, {
lastLevel: id,
vocabMetadata: newVocabMetadata,
});
}
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;