diff --git a/nodemon.json b/nodemon.json index a6c46bd997c2a437d6944ca8d25f8a17bbc548e1..e33fc45722c7966c12bd1c91320e004d3eb10b2b 100644 --- a/nodemon.json +++ b/nodemon.json @@ -7,9 +7,9 @@ }, "runOnChangeOnly": false, - "watch": ["src/**/*.js"], + "watch": ["src/**/*"], "env": { "NODE_ENV": "development" }, - "ext": "js,json" + "ext": "js,json,yml" } diff --git a/package-lock.json b/package-lock.json index f887e7809c5c636707a6f8465b69168c9b0285ef..1d4e4ef59f82b52c27cb19b5a36cdaec1035a086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,40 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apidevtools/json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", + "requires": { + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "@apidevtools/openapi-schemas": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.4.tgz", + "integrity": "sha512-ob5c4UiaMYkb24pNhvfSABShAwpREvUGCkqjiz/BX9gKZ32y/S22M+ALIHftTAuv9KsFVSpVdIDzi9ZzFh5TCA==" + }, + "@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "@apidevtools/swagger-parser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz", + "integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^8.0.0", + "@apidevtools/openapi-schemas": "^2.0.2", + "@apidevtools/swagger-methods": "^3.0.0", + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "openapi-types": "^1.3.5", + "z-schema": "^4.2.2" + } + }, "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", @@ -627,6 +661,11 @@ "chalk": "^4.0.0" } }, + "@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -1043,7 +1082,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -1209,8 +1247,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -1427,7 +1464,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1522,6 +1558,11 @@ } } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1755,6 +1796,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.0.0.tgz", + "integrity": "sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ==" + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1770,8 +1816,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "configstore": { "version": "5.0.1", @@ -2091,7 +2136,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "requires": { "esutils": "^2.0.2" } @@ -2513,8 +2557,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.3.1", @@ -2551,8 +2594,7 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "etag": { "version": "1.8.1", @@ -3036,8 +3078,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.1.3", @@ -3104,7 +3145,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3373,7 +3413,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4269,7 +4308,6 @@ "version": "3.14.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4477,6 +4515,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -4601,7 +4649,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4998,7 +5045,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -5012,6 +5058,11 @@ "mimic-fn": "^2.1.0" } }, + "openapi-types": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", + "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -5173,8 +5224,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -6248,8 +6298,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -6486,6 +6535,50 @@ } } }, + "swagger-jsdoc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-4.0.0.tgz", + "integrity": "sha512-wHrmRvE/OQa3d387YIrRNPvsPwxkJc0tAYeCVa359gUIKPjC4ReduFhqq/+4erLUS79kY1T5Fv0hE0SV/PgBig==", + "requires": { + "commander": "5.0.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "js-yaml": "3.13.1", + "swagger-parser": "9.0.1" + }, + "dependencies": { + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "swagger-parser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-9.0.1.tgz", + "integrity": "sha512-oxOHUaeNetO9ChhTJm2fD+48DbGbLD09ZEOwPOWEqcW8J6zmjWxutXtSuOiXsoRgDWvORYlImbwM21Pn+EiuvQ==", + "requires": { + "@apidevtools/swagger-parser": "9.0.1" + } + }, + "swagger-ui-dist": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.30.2.tgz", + "integrity": "sha512-hAu/ig5N8i0trXXbrC7rwbXV4DhpEAsZhYXDs1305OjmDgjGC0thINbb0197idy3Pp+B6w7u426SUM43GAP7qw==" + }, + "swagger-ui-express": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", + "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -7158,8 +7251,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", @@ -7288,6 +7380,30 @@ "dev": true } } + }, + "z-schema": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz", + "integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==", + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^12.0.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true + }, + "validator": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", + "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==" + } + } } } } diff --git a/package.json b/package.json index f39a4c319147f40367f8291ba478b2e3a2f8d5b1..90025e9b38a15023166ea015bed7dd66370ec595 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "morgan": "^1.10.0", "passport": "^0.4.1", "passport-oauth2": "^1.5.0", + "swagger-jsdoc": "^4.0.0", + "swagger-ui-express": "^4.1.4", "validator": "^13.1.1" }, "devDependencies": { diff --git a/src/resources/activity/activityControllers.js b/src/resources/activity/activityControllers.js index cec720f870970e1a660b1ca228154cb9139c277b..76c2898781cb1f105564bcb74bc4ba4c8162c60e 100644 --- a/src/resources/activity/activityControllers.js +++ b/src/resources/activity/activityControllers.js @@ -1,10 +1,208 @@ -const { crudControllers } = require('../../utils/crud') const { Activity } = require('./activityModel') +const { User } = require('../user/userModel') +const { Attendance } = require('../attendance/attendanceModel') +const { Comment } = require('../comment/commentModel') +const { omit, pick } = require('lodash') -exports.default = crudControllers(Activity, [ +// Config +const pickedKeys = [ '_id', 'title', 'description', 'date', 'type', -]) + 'createdAt', + 'updatedAt', + 'attendance', + 'comments', +] + +// Controller + +module.exports.createOne = async function createOne(req, res) { + try { + // Invalid Date provided + if (!req.body.date || Date.parse(req.body.date) < new Date().getTime()) + return res.status(403).json({ messages: ['Invalid date'] }) + + let activity = await Activity.create({ ...req.body }) + + // Create Initial participants + if ( + !( + req.body.initialParticipants != null && + req.body.initialParticipants == false + ) + ) { + let acceptedUsers = await User.find({ role: 'accepted' }).lean().exec() + acceptedUsers = acceptedUsers.map(function getUserSchaccs(user) { + return { schacc: user.schacc } + }) + + let attendances = activity.attendance + for await (let user of acceptedUsers) { + const newAttendance = await Attendance.create({ + activity: activity._id, + user: user.schacc, + }) + attendances.push(newAttendance._id) + } + + await Activity.findOneAndUpdate( + { _id: activity._id }, + { attendance: attendances }, + { new: true } + ) + .lean() + .exec() + } + + return res + .status(200) + .json({ + data: pick(activity, pickedKeys), + }) + .end() + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +module.exports.getOne = async function getOne(req, res) { + try { + const activity = await Activity.findOne({ _id: req.params.id }) + .lean() + .exec() + + if (!activity) + return res.status(404).json({ messages: ['No such activity.'] }) + + if (req.user.role == 'mentor') + return res.status(200).json({ + data: pick(activity, pickedKeys), + }) + return res.status(200).json({ + data: pick(activity, [ + '_id', + 'title', + 'description', + 'date', + 'type', + 'createdAt', + 'updatedAt', + ]), + }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +module.exports.getMany = async function getMany(req, res) { + try { + const activity = await Activity.find().select('-__v').lean().exec() + + if (!activity) + return res.status(404).json({ message: 'Activity not found!' }) + + res.status(200).json({ + data: activity.map(function pickKeys(doc) { + return pick(populatedActivity, pickedKeys) + }), + }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + res.status(500).json({ message: err.message }) + } +} + +module.exports.removeOne = async function removeOne(req, res) { + try { + const activity = await Activity.findByIdAndDelete({ + _id: req.params.id, + }) + .lean() + .exec() + + if (!activity) + return res.status(404).json({ message: 'Activity not found!' }) + + await Attendance.deleteMany({ + _id: activity.attendance.map(function getAttendanceIds(element) { + return element._id + }), + }) + + await Comment.deleteMany({ + _id: activity.comments.map(function getCommentIds(element) { + return element._id + }), + }) + + return res.status(200).json({ data: pick(activity, pickedKeys) }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +module.exports.updateOne = async function updateOne(req, res) { + try { + const activity = await Activity.findOneAndUpdate( + { _id: req.params.id }, + omit(req.body, ['attendance', 'comments']), + { new: true } + ) + .lean() + .exec() + + if (!activity) + return res.status(404).json({ message: 'Activity not found!' }) + + res.status(200).json({ data: pick(activity, pickedKeys) }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + res.status(500).json({ message: err.message }) + } +} diff --git a/src/resources/activity/activityDocs.yml b/src/resources/activity/activityDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..d16d409a2ff3ff47b022edc04ea1bb623d648475 --- /dev/null +++ b/src/resources/activity/activityDocs.yml @@ -0,0 +1,119 @@ +openapi: '3.0.2' +info: + title: 'Activity Endpoint' + version: '1.0' + +paths: + /activity: + get: + tags: + - 'Activity' + summary: 'Get a List of comments' + description: 'Have to be logged in for this.' + operationId: 'getAllActivity' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Activity' + post: + tags: + - 'Activity' + summary: 'Create a activity' + description: 'Only mentors can create activity.' + operationId: 'createActivity' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + /activity/{id}: + get: + tags: + - 'Activity' + summary: 'Get a activity by ID' + description: 'Have to be logged in for this.' + operationId: 'getActivity' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + put: + tags: + - 'Activity' + summary: 'Update an activity by ID' + description: 'Only mentors can update an activity.' + operationId: 'updateOneActivity' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + delete: + tags: + - 'Activity' + summary: 'Delete an activity by ID' + description: 'Only mentors can delete an activity.' + operationId: 'deleteActivity' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + +components: + schemas: + Activity: + type: object + properties: + title: + type: string + description: + type: string + date: + type: string + format: date-time + type: + type: string + enum: + - class + - optional + - camp + attendance: + type: array + items: + type: string + description: cuid of each attendance + comments: + type: array + items: + type: string + description: cuid of each comment + required: + - title + - description + - date + - type diff --git a/src/resources/activity/activityModel.js b/src/resources/activity/activityModel.js index fa90dd75f31c2d2f1892b761ae7d019ecbb805e9..bbf2a8750a205d1720cb235c00d035cfed9f2804 100644 --- a/src/resources/activity/activityModel.js +++ b/src/resources/activity/activityModel.js @@ -25,20 +25,16 @@ const ActivitySchema = new mongoose.Schema( ref: 'attendance', }, ], - comment: [ + comments: [ { type: mongoose.Schema.Types.ObjectId, ref: 'comment', - required: true, }, ], }, { timestamps: true } ) -// Careful with the docs, there are some deprecated ones -// https://mongoosejs.com/docs/guide.html - const Activity = mongoose.model('activity', ActivitySchema) exports.Activity = Activity diff --git a/src/resources/activity/activityRouter.js b/src/resources/activity/activityRouter.js index 6136da10fd53bf472ca8404afca75d9029e022b4..e8c18a460d100c1a41d50478d56ee3473a6046bd 100644 --- a/src/resources/activity/activityRouter.js +++ b/src/resources/activity/activityRouter.js @@ -1,19 +1,20 @@ const { Router } = require('express') const controllers = require('./activityControllers') +const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/item +// /api/v1/activity router .route('/') - .get(controllers.default.getMany) - .post(controllers.default.createOne) + .get(isLoggedIn, controllers.getMany) + .post(isLoggedIn, isMentor, controllers.createOne) -// /api/item/:id +// /api/v1/activity/:id router .route('/:id') - .get(controllers.default.getOne) - .put(controllers.default.updateOne) - .delete(controllers.default.removeOne) + .get(isLoggedIn, controllers.getOne) + .put(isLoggedIn, isMentor, controllers.updateOne) + .delete(isLoggedIn, isMentor, controllers.removeOne) exports.default = router diff --git a/src/resources/application/__tests__/applicationFuncTest.js b/src/resources/application/__tests__/applicationFuncTest.js index 35e5918eaaeeaec24d37c5e588e8bce1628720ec..a2b397947a51dbbeaa2d5041645be7fccbb5d62a 100644 --- a/src/resources/application/__tests__/applicationFuncTest.js +++ b/src/resources/application/__tests__/applicationFuncTest.js @@ -66,32 +66,36 @@ describe('/application "Mentor" Functionality', () => { }) }) // GET own - test(`GET own application`, async () => { + test(`GET own application by ID`, async () => { await createGroups() - const ownUser = await authSession.get('/api/v1/extra/me') + const ownUser = await authSession.get('/api/v1/user/me') const newApplication = await Application.create( Object.assign({}, fakeApplicationJson, { creator: ownUser.body.data.schacc, }) ) - let response = await authSession.get(`${endpointUrl}/${newApplication._id}`) + let response = await authSession.get( + `${endpointUrl}/id/${newApplication._id}` + ) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) - response = await authSession.get(`${endpointUrl}/own`) + response = await authSession.get(`${endpointUrl}/id/own`) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) }) // Get others - test(`GET others application`, async () => { + test(`GET others application by ID`, async () => { await createGroups() const newApplication = await Application.create(fakeApplicationJson) - let response = await authSession.get(`${endpointUrl}/${newApplication._id}`) + let response = await authSession.get( + `${endpointUrl}/id/${newApplication._id}` + ) expect(response.statusCode).toBe(200) }) test(`GET invalid ID`, async () => { - let response = await authSession.get(`${endpointUrl}/almafa`) + let response = await authSession.get(`${endpointUrl}/id/almafa`) expect(response.statusCode).toBe(422) }) diff --git a/src/resources/application/__tests__/applicationPermTest.js b/src/resources/application/__tests__/applicationPermTest.js index f00f695410daab998add6a882e15464f47593437..a5f94fd5d707835370e25ccacfbfe8689e1922d3 100644 --- a/src/resources/application/__tests__/applicationPermTest.js +++ b/src/resources/application/__tests__/applicationPermTest.js @@ -1,7 +1,10 @@ const { app } = require('../../../server') +const session = require('supertest-session') const { crudPermTest } = require('../../../utils/testHelpers') const { Application } = require('../applicationModel') +const { User } = require('../../user/userModel') +const { Groups } = require('../../groups/groupsModel') const endpointUrl = '/api/v1/application' @@ -9,17 +12,38 @@ let fakeApplicationJson = { motivation: 'what is motivation?', expectation: 'I expect a lot', solution: 'My awesome solution', - groups: ['5f089da3d11dae2a1ff07abc'], + groups: ['5f089da3d11dae2a1ff0aaaa', '5f089da3d11dae2a1f00aaab'], +} + +const fakeGroupsJsons = [ + { + _id: '5f089da3d11dae2a1ff0aaaa', + name: 'SuperGroup', + description: 'So awesome that you want to join in', + groupPath: 'future image', + }, + { + _id: '5f089da3d11dae2a1f00aaab', + name: 'AwesomeGroup', + description: 'So awesome that you want to join in', + groupPath: 'future image', + }, +] + +const createGroups = async () => { + await Groups.create(fakeGroupsJsons[0]) + await Groups.create(fakeGroupsJsons[1]) } describe('/application Permission tests', () => { + let authSession crudPermTest( app, endpointUrl, Application, 'application', fakeApplicationJson, - [true, true, true, false, false], + [true, true, false, false, false], [ // [role, create, readAll, readOne, update, delete] ['none', false, false, false, false, false], @@ -28,4 +52,73 @@ describe('/application Permission tests', () => { ['mentor', true, true, true, false, false], ] ) + crudPermTest( + app, + endpointUrl + '/id', + Application, + 'application', + fakeApplicationJson, + [false, false, true, false, false], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', true, false, false, false, false], + ['accepted', true, false, false, false, false], + ['mentor', true, true, true, false, false], + ] + ) + describe.each(['normal', 'accepted', 'mentor'])('role: %s', (role) => { + beforeEach(function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/${role}`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + // All roles + test(`Can Get own application on /id/ or on /schacc`, async () => { + await createGroups() + const ownUser = await authSession.get('/api/v1/user/me') + const newApplication = await Application.create( + Object.assign({}, fakeApplicationJson, { + creator: ownUser.body.data.schacc, + }) + ) + let response = await authSession.get( + `${endpointUrl}/id/${newApplication._id}` + ) + expect(response.statusCode).toBe(200) + response = await authSession.get( + `${endpointUrl}/schacc/${ownUser.body.data.schacc}` + ) + expect(response.statusCode).toBe(200) + + // get others + const newUser = await User.create({ + internal_id: 'fakeId', + schacc: 'fakeUser', + fullName: 'faker Janos', + secondaryEmail: 'faker@fake.com', + }) + const newApplication2 = await Application.create( + Object.assign({}, fakeApplicationJson, { + creator: newUser.schacc, + }) + ) + let responsebyId = await authSession.get( + `${endpointUrl}/id/${newApplication2._id}` + ) + let responsebySchacc = await authSession.get( + `${endpointUrl}/schacc/${newUser.schacc}` + ) + if (role == 'mentor') { + expect(responsebyId.statusCode).toBe(200) + expect(responsebySchacc.statusCode).toBe(200) + } else { + expect(responsebyId.statusCode).toBe(403) + expect(responsebySchacc.statusCode).toBe(403) + } + }) + }) }) diff --git a/src/resources/application/applicationControllers.js b/src/resources/application/applicationControllers.js index a449c8daca9d0140b8adc0bc196ee48e9a00c907..22be0bbbf6322f0eb5e7e734ed2b478faa4b121a 100644 --- a/src/resources/application/applicationControllers.js +++ b/src/resources/application/applicationControllers.js @@ -162,7 +162,7 @@ exports.default.getMany = async (req, res) => { } } -exports.default.getOne = async (req, res) => { +exports.default.getOneByID = async (req, res) => { try { let doc if (req.params.id === 'own') @@ -179,6 +179,47 @@ exports.default.getOne = async (req, res) => { return res.status(404).end() } + if (doc.creator != req.user.schacc && req.user.role != 'mentor') + return res.status(403).end() + + doc.creator = doc._creator + res.status(200).json({ + data: pick(doc, [ + '_id', + 'creator', + 'motivation', + 'expectation', + 'solution', + 'groups', + 'state', + ]), + }) + } catch (err) { + if (err.name == 'CastError') { + // Throwed by Mongoose + return res.status(422).json('Invalid ID provided') + } else { + console.error(err) + res.status(400).end() + } + } +} + +exports.default.getOneBySchacc = async (req, res) => { + try { + let doc = await Application.findOne({ creator: req.params.schacc }) + .populate('groups', ['name']) + .populate('_creator', ['schacc', 'fullName', 'secondaryEmail']) + .lean() + .exec() + + if (!doc) { + return res.status(404).end() + } + + if (doc.creator != req.user.schacc && req.user.role != 'mentor') + return res.status(403).end() + doc.creator = doc._creator res.status(200).json({ data: pick(doc, [ diff --git a/src/resources/application/applicationDocs.yml b/src/resources/application/applicationDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..1bab936fd6b6a75f651852d30e1da989012a0cfc --- /dev/null +++ b/src/resources/application/applicationDocs.yml @@ -0,0 +1,110 @@ +openapi: '3.0.2' +info: + title: 'Application Endpoint' + version: '1.0' + +paths: + /application: + get: + tags: + - 'Application' + summary: 'Get a List of applications' + description: 'This can only be done by a mentor.' + operationId: 'getAllApplication' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Application' + post: + tags: + - 'Application' + summary: 'Update an application or create it if doesnt exist' + description: 'Have to be logged in. + To update someones application as a mentor, have to pass in the creator. + After deadline only the mentor can create or update one. + Group names have to be passed instead of ids.' + operationId: 'createAnApplication' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + /application/id/{id}: + get: + tags: + - 'Application' + summary: 'Get Application by ID' + description: 'Can get own or have to be mentor. + {id} = own is a shorthand for getting own.' + operationId: 'getApplicationByID' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Application' + /application/schacc/{schacc}: + get: + tags: + - 'Application' + summary: 'Get Application by Schacc' + description: 'Can get own or have to be mentor.' + operationId: 'getApplicationBySchacc' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Application' + +components: + schemas: + Application: + type: object + properties: + creator: + type: string + description: Reference to a Users schacc + motivation: + type: string + expectation: + type: string + maxLength: 50 + solution: + type: string + groups: + type: array + items: + type: string + description: References to Group Ids + state: + type: string + enum: + - 'accepted' + - 'rejected' + - 'nonchecked' + default: 'nonchecked' + required: + - creator + - motivation + - expectation + - solution + - state diff --git a/src/resources/application/applicationRouter.js b/src/resources/application/applicationRouter.js index aba1e12ef6995872b5536af80f2a5fcd44eda2f5..093fbbf514638c179473b9aa58c1c57702c3ab94 100644 --- a/src/resources/application/applicationRouter.js +++ b/src/resources/application/applicationRouter.js @@ -4,13 +4,18 @@ const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/application +// /api/v1/application router .route('/') .get(isLoggedIn, isMentor, controllers.default.getMany) .post(isLoggedIn, controllers.default.createOne) -// /api/application/:id -router.route('/:id').get(isLoggedIn, controllers.default.getOne) +// /api/v1/application/id/:id +router.route('/id/:id').get(isLoggedIn, controllers.default.getOneByID) + +// /api/v1/application/schacc/:schacc +router + .route('/schacc/:schacc') + .get(isLoggedIn, controllers.default.getOneBySchacc) exports.default = router diff --git a/src/resources/attendance/__tests__/attendanceFuncTest.js b/src/resources/attendance/__tests__/attendanceFuncTest.js index c9836e0e65418149d3f83e1ea96dc9947fd97538..d0858f91e9954b362441ab15c7a38e4eed2f25fd 100644 --- a/src/resources/attendance/__tests__/attendanceFuncTest.js +++ b/src/resources/attendance/__tests__/attendanceFuncTest.js @@ -42,7 +42,9 @@ describe('/group "Mentor" Functionality', () => { // GET One test(`GET returns with allowed keys`, async () => { const newAttendance = await Attendance.create(fakeAttendanceJson) - let response = await authSession.get(`${endpointUrl}/${newAttendance._id}`) + let response = await authSession.get( + `${endpointUrl}/id/${newAttendance._id}` + ) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) }) @@ -58,7 +60,7 @@ describe('/group "Mentor" Functionality', () => { }) // Create test(`Create group returns with allowed keys`, async () => { - const ownUser = await authSession.get('/api/v1/extra/me') + const ownUser = await authSession.get('/api/v1/user/me') const newActivity = await Activity.create(fakeActivityJson) let response = await authSession.post(endpointUrl).send({ activity: newActivity._id, @@ -71,7 +73,7 @@ describe('/group "Mentor" Functionality', () => { test(`Delete group returns with allowed keys`, async () => { const newAttendance = await Attendance.create(fakeAttendanceJson) let response = await authSession.delete( - `${endpointUrl}/${newAttendance._id}` + `${endpointUrl}/id/${newAttendance._id}` ) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) @@ -80,7 +82,7 @@ describe('/group "Mentor" Functionality', () => { test(`Update group returns with allowed keys`, async () => { const newAttendance = await Attendance.create(fakeAttendanceJson) let response = await authSession - .put(`${endpointUrl}/${newAttendance._id}`) + .put(`${endpointUrl}/id/${newAttendance._id}`) .send({ state: 'present', }) diff --git a/src/resources/attendance/__tests__/attendancePermTest.js b/src/resources/attendance/__tests__/attendancePermTest.js index 4366a8dced12ccf3ab617e90e63bc3b3d7aa0453..9b9b81c222f35c0fc7a82021de22eb2498dce1cf 100644 --- a/src/resources/attendance/__tests__/attendancePermTest.js +++ b/src/resources/attendance/__tests__/attendancePermTest.js @@ -11,13 +11,28 @@ const fakeAttendanceJson = { } describe('/attendance Permission tests', () => { + crudPermTest( + app, + endpointUrl + '/id', + Attendance, + 'attendance', + fakeAttendanceJson, + [false, false, true, true, true], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, false, false, false, false], + ['accepted', false, false, false, false, false], + ['mentor', true, true, true, true, true], + ] + ) crudPermTest( app, endpointUrl, Attendance, 'attendance', fakeAttendanceJson, - [true, true, true, true, true], + [true, true, false, false, false], [ // [role, create, readAll, readOne, update, delete] ['none', false, false, false, false, false], diff --git a/src/resources/attendance/attendanceControllers.js b/src/resources/attendance/attendanceControllers.js index d2d9c94d1fa10c773d605f61c99ce81b81459a70..6561838748979edd9f0aaedeb246701a91c17df7 100644 --- a/src/resources/attendance/attendanceControllers.js +++ b/src/resources/attendance/attendanceControllers.js @@ -2,15 +2,17 @@ const { crudControllers } = require('../../utils/crud') const { Attendance } = require('./attendanceModel') const { pick } = require('lodash') -const pickedKeys = ['_id', 'activity', 'user', 'state', 'comment'] +const pickedKeys = ['_id', 'activity', 'user', 'state', 'comments'] -exports.default = crudControllers(Attendance, pickedKeys) +module.exports = crudControllers(Attendance, pickedKeys) -exports.default.getOne = async (req, res) => { +// On create update activity + +module.exports.getOne = async (req, res) => { try { const attendance = await Attendance.findOne({ _id: req.params.id }) .populate('_user', ['_id', 'fullName', 'schacc']) - .populate('comment', ['creator', 'date', 'text']) + .populate('comments', ['creator', 'date', 'text']) .lean() .exec() @@ -30,11 +32,11 @@ exports.default.getOne = async (req, res) => { } } -exports.default.getMany = async (req, res) => { +module.exports.getMany = async (req, res) => { try { const attendances = await Attendance.find() .populate('_user', ['_id', 'fullName', 'schacc']) - .populate('comment', ['creator', 'date', 'text']) + .populate('comments', ['creator', 'date', 'text']) .lean() .exec() @@ -50,7 +52,7 @@ exports.default.getMany = async (req, res) => { } } -exports.default.updateOne = async (req, res) => { +module.exports.updateOne = async (req, res) => { try { const updatedAttendance = await Attendance.findOneAndUpdate( { @@ -60,7 +62,7 @@ exports.default.updateOne = async (req, res) => { { new: true, runValidators: true } ) .populate('_user', ['_id', 'fullName', 'schacc']) - .populate('comment', ['creator', 'date', 'text']) + .populate('comments', ['creator', 'date', 'text']) .lean() .exec() diff --git a/src/resources/attendance/attendanceDocs.yml b/src/resources/attendance/attendanceDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..9dd6814942e54c9478b87f15c3b1fec44fc99353 --- /dev/null +++ b/src/resources/attendance/attendanceDocs.yml @@ -0,0 +1,113 @@ +openapi: '3.0.2' +info: + title: 'Attendance Endpoint' + version: '1.0' + +paths: + /attendance: + get: + tags: + - 'Attendance' + summary: 'Get a List of attendances' + description: 'Have to be mentor for this.' + operationId: 'getAllAttendance' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Attendance' + post: + tags: + - 'Attendance' + summary: 'Create an attendance' + description: 'Have to be mentor for this.' + operationId: 'createAttendance' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attendance' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attendance' + /attendance/id/{id}: + get: + tags: + - 'Attendance' + summary: 'Get an attendance by ID' + description: 'Have to be mentor for this.' + operationId: 'getAttendance' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attendance' + put: + tags: + - 'Attendance' + summary: 'Update an attendance by ID' + description: 'Only mentors can update an attendance.' + operationId: 'updateOneAttendance' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attendance' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attendance' + delete: + tags: + - 'Attendance' + summary: 'Delete an attendance by ID' + description: 'Only mentors can delete an attendance.' + operationId: 'deleteAttendance' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attendance' + +components: + schemas: + Attendance: + type: object + properties: + activity: + type: string + description: cuid of the activity + user: + type: string + description: schacc of the user + state: + type: string + enum: + - absent + - present + - cantcome + default: absent + comments: + type: array + items: + type: string + description: cuid of each comment + required: + - activity + - user + - state diff --git a/src/resources/attendance/attendanceModel.js b/src/resources/attendance/attendanceModel.js index 7cb4801958df1b12b59bd3d2184da26578c78350..ca827c82f3df9a07b526db1c28f4d42ee4593075 100644 --- a/src/resources/attendance/attendanceModel.js +++ b/src/resources/attendance/attendanceModel.js @@ -16,7 +16,7 @@ const AttendanceSchema = new mongoose.Schema( default: 'absent', required: true, }, - comment: [ + comments: [ { type: mongoose.Schema.Types.ObjectId, ref: 'comment', diff --git a/src/resources/attendance/attendanceRouter.js b/src/resources/attendance/attendanceRouter.js index 5046a3fe167cd4c2f92ef33b7502c1d94a84cb13..aa2468a81b646a4029ccb579b3b6cec73e932e07 100644 --- a/src/resources/attendance/attendanceRouter.js +++ b/src/resources/attendance/attendanceRouter.js @@ -4,17 +4,17 @@ const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/item +// /api/v1/attendance/ router .route('/') - .get(isLoggedIn, isMentor, controllers.default.getMany) - .post(isLoggedIn, isMentor, controllers.default.createOne) + .get(isLoggedIn, isMentor, controllers.getMany) + .post(isLoggedIn, isMentor, controllers.createOne) -// /api/item/:id +// /api/v1/attendance/id/:id router - .route('/:id') - .get(isLoggedIn, isMentor, controllers.default.getOne) - .put(isLoggedIn, isMentor, controllers.default.updateOne) - .delete(isLoggedIn, isMentor, controllers.default.removeOne) + .route('/id/:id') + .get(isLoggedIn, isMentor, controllers.getOne) + .put(isLoggedIn, isMentor, controllers.updateOne) + .delete(isLoggedIn, isMentor, controllers.removeOne) exports.default = router diff --git a/src/resources/comment/__tests__/commentFuncTest.js b/src/resources/comment/__tests__/commentFuncTest.js new file mode 100644 index 0000000000000000000000000000000000000000..8e3c54d6dac2bcf4a01f30a4d1c34f474f98df76 --- /dev/null +++ b/src/resources/comment/__tests__/commentFuncTest.js @@ -0,0 +1,247 @@ +const { app } = require('../../../server') +const session = require('supertest-session') +const { iteratee, merge, has } = require('lodash') + +const { validateKeys } = require('../../../utils/testHelpers') + +const { Comment } = require('../commentModel') +const { Solution } = require('../../solution/solutionModel') +const { Task } = require('../../task/taskModel') +const { Attendance } = require('../../attendance/attendanceModel') +const { Activity } = require('../../activity/activityModel') +const { User } = require('../../user/userModel') + +const endpointUrl = '/api/v1/comment' + +const fakeUserJson = { + internal_id: 'fakeId', + schacc: 'fakeUser', + fullName: 'faker Janos', + secondaryEmail: 'faker@fake.com', +} + +const fakeActivityJson = { + _id: '5f1351031185d36ad29687a3', + title: 'One title', + description: 'super desc', + date: '2020-01-01', + type: 'optional', +} + +const fakeAttendanceJson = { + _id: '5f1351031185d36ad29687a4', + activity: '5f1351031185d36ad29687a3', + user: 'accepted_test', +} + +const fakeTaskJson = { + _id: '5f1351031185d36ad29687a1', + title: 'One title', + description: 'super desc', + deadline: '2020-01-01', + bit: 1, + creator: 'mentor_test', + solutions: ['5f1351031185d36ad29687a2'], +} + +const fakeSolutionJson = { + _id: '5f1351031185d36ad29687a2', + task: '5f1351031185d36ad29687a1', + title: 'One title', + description: 'super desc', + file: 'superfile.txt', + creator: 'accepted_test', +} + +const fakeCommentOnSolutionJson = { + parentId: '5f1351031185d36ad29687a2', + parentType: 'solution', + text: 'alma', +} + +const fakeCommentOnTaskJson = { + parentId: '5f1351031185d36ad29687a1', + parentType: 'task', + text: 'alma', +} + +const fakeCommentOnAttendanceJson = { + parentId: '5f1351031185d36ad29687a4', + parentType: 'attendance', + text: 'alma', +} + +const fakeCommentOnActivityJson = { + parentId: '5f1351031185d36ad29687a3', + parentType: 'activity', + text: 'alma', +} + +describe('/comment "Mentor" Functionality', () => { + let authSession + // Login as mentor + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/mentor`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + // GET One + test(`GET returns with 404 if not found`, async () => { + let response = await authSession.get( + `${endpointUrl}/id/5f1351031185d36ad29687a1` + ) + expect(response.statusCode).toBe(404) + }) + test(`GET can get other users comment`, async () => { + await User.create(fakeUserJson) + await Activity.create(fakeActivityJson) + let comment = await Comment.create({ + ...fakeCommentOnActivityJson, + creator: 'fakeUser', + }) + let response = await authSession.get(`${endpointUrl}/id/${comment._id}`) + expect(response.statusCode).toBe(200) + expect(response.body.data.creator.schacc).toBe('fakeUser') + }) + test(`GET anonym comment`, async () => { + await User.create(fakeUserJson) + await Activity.create(fakeActivityJson) + let comment = await Comment.create({ + ...fakeCommentOnActivityJson, + creator: 'fakeUser', + isAnonim: true, + }) + let response = await authSession.get(`${endpointUrl}/id/${comment._id}`) + expect(response.statusCode).toBe(200) + expect(response.body.data.creator).toBe('') + }) + // Get Many + test(`GET many returns with all comments`, async () => { + await Activity.create(fakeActivityJson) + await Attendance.create(fakeAttendanceJson) + await Comment.create(fakeCommentOnActivityJson) + await Comment.create(fakeCommentOnAttendanceJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + expect(response.body.data.length).toBe(2) + }) + // Create + test(`CREATE adds to parent (activity)`, async () => { + let activity = await Activity.create(fakeActivityJson) + let response = await authSession + .post(endpointUrl) + .send({ ...fakeCommentOnActivityJson, _id: '5f2801d8d5b36f6b39562cbb' }) + + expect(response.statusCode).toBe(200) + expect(response.body.data.parentId).toBe(String(activity._id)) + let checkActivity = await Activity.findById(activity._id).lean().exec() + expect(String(checkActivity.comments[0])).toBe('5f2801d8d5b36f6b39562cbb') + }) + // Delete + test(`DELETE a comment`, async () => { + let activity = await Activity.create(fakeActivityJson) + let response = await authSession + .post(endpointUrl) + .send({ ...fakeCommentOnActivityJson, _id: '5f2801d8d5b36f6b39562cbb' }) + + response = await authSession.delete( + `${endpointUrl}/id/5f2801d8d5b36f6b39562cbb` + ) + + expect(response.statusCode).toBe(200) + }) + // Update + test(`UPDATE a comment`, async () => { + let activity = await Activity.create(fakeActivityJson) + let response = await authSession + .post(endpointUrl) + .send({ ...fakeCommentOnActivityJson, _id: '5f2801d8d5b36f6b39562cbb' }) + + response = await authSession + .put(`${endpointUrl}/id/5f2801d8d5b36f6b39562cbb`) + .send({ creator: 'almafa', text: 'kortefa' }) + + expect(response.statusCode).toBe(200) + expect(response.body.data.creator).not.toBe('almafa') + expect(response.body.data.text).toBe('kortefa') + }) +}) +describe('/comment "Accepted" Functionality', () => { + let authSession + // Login as accepted + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/accepted`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + // GET One + test(`GET cant get comment on Attendance or Activity`, async () => { + await User.create(fakeUserJson) + await Activity.create(fakeActivityJson) + await Attendance.create(fakeAttendanceJson) + let commentOnActivity = await Comment.create(fakeCommentOnActivityJson) + let commentonAttendance = await Comment.create(fakeCommentOnAttendanceJson) + let response = await authSession.get( + `${endpointUrl}/id/${commentOnActivity._id}` + ) + expect(response.statusCode).toBe(403) + response = await authSession.get( + `${endpointUrl}/id/${commentonAttendance._id}` + ) + expect(response.statusCode).toBe(403) + let ownCommentOnActivity = await Comment.create({ + ...fakeCommentOnActivityJson, + creator: 'accepted_test', + }) + response = await authSession.get( + `${endpointUrl}/id/${ownCommentOnActivity._id}` + ) + expect(response.statusCode).toBe(403) + }) + test(`GET comment on own Solution`, async () => { + await User.create(fakeUserJson) + await Task.create(fakeTaskJson) + await Solution.create({ ...fakeSolutionJson, creator: 'accepted_test' }) + let commentOnSolution = await Comment.create(fakeCommentOnSolutionJson) + let response = await authSession.get( + `${endpointUrl}/id/${commentOnSolution._id}` + ) + expect(response.statusCode).toBe(200) + let owncommentOnSolution = await Comment.create({ + ...fakeCommentOnSolutionJson, + creator: 'accepted_test', + }) + response = await authSession.get( + `${endpointUrl}/id/${owncommentOnSolution._id}` + ) + expect(response.statusCode).toBe(200) + }) + test(`GET comment on others Solution`, async () => { + await User.create(fakeUserJson) + await Task.create(fakeTaskJson) + await Solution.create({ ...fakeSolutionJson, creator: 'jani' }) + let commentOnSolution = await Comment.create({ + ...fakeCommentOnSolutionJson, + }) + let response = await authSession.get( + `${endpointUrl}/id/${commentOnSolution._id}` + ) + expect(response.statusCode).toBe(403) + let owncommentOnSolution = await Comment.create({ + ...fakeCommentOnSolutionJson, + creator: 'accepted_test', + }) + response = await authSession.get( + `${endpointUrl}/id/${owncommentOnSolution._id}` + ) + expect(response.statusCode).toBe(403) + }) + // Create + // TODO +}) diff --git a/src/resources/comment/__tests__/commentPermTest.js b/src/resources/comment/__tests__/commentPermTest.js new file mode 100644 index 0000000000000000000000000000000000000000..de634696ed272aa0455c66cb5c453f23b52abefa --- /dev/null +++ b/src/resources/comment/__tests__/commentPermTest.js @@ -0,0 +1,45 @@ +const { app } = require('../../../server') +const { crudPermTest } = require('../../../utils/testHelpers') + +const { Comment } = require('../commentModel') + +const endpointUrl = '/api/v1/comment' + +const fakeCommentJson = { + parentId: '5f1351031185d36ad29687a2', + parentType: 'solution', + text: 'alma', +} + +describe('/comment Permission tests', () => { + crudPermTest( + app, + endpointUrl + '/id', + Comment, + 'comment', + fakeCommentJson, + [false, false, true, true, true], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, false, false, false, false], + ['accepted', true, false, true, false, false], // can update or delete own + ['mentor', true, true, true, true, true], + ] + ) + crudPermTest( + app, + endpointUrl, + Comment, + 'comment', + fakeCommentJson, + [true, true, false, false, false], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, false, false, false, false], + ['accepted', true, false, true, false, false], + ['mentor', true, true, true, true, true], + ] + ) +}) diff --git a/src/resources/comment/commentControllers.js b/src/resources/comment/commentControllers.js index aec82b07861967c7a5a7d9a1fbff3178ddadf0aa..1b1d3b7555a5cccacddb6e40bf09d4c1b7e91b69 100644 --- a/src/resources/comment/commentControllers.js +++ b/src/resources/comment/commentControllers.js @@ -1,4 +1,259 @@ const { crudControllers } = require('../../utils/crud') +const { pick } = require('lodash') + const { Comment } = require('./commentModel') +const { Solution } = require('../solution/solutionModel') +const { Task } = require('../task/taskModel') +const { Attendance } = require('../attendance/attendanceModel') +const { Activity } = require('../activity/activityModel') + +const pickedKeys = [ + '_id', + 'parentId', + 'parentType', + 'creator', + 'text', + 'createdAt', + 'updatedAt', +] + +exports.default = crudControllers(Comment, pickedKeys) + +function getParentModel(comment) { + switch (comment.parentType) { + case 'solution': + return Solution + case 'task': + return Task + case 'attendance': + return Attendance + case 'activity': + return Activity + default: + throw { message: `Invalid Parent Type: ${comment.parentType}` } + } +} + +exports.default.getOne = async (req, res) => { + try { + const comment = await Comment.findOne({ + _id: req.params.id, + }) + .populate('_creator', ['fullName', 'nickName', 'schacc']) + .lean() + .exec() + + if (!comment) return res.status(404).end() + + if ( + (comment.parentType == 'attendance' || + comment.parentType == 'activity') && + req.user.role !== 'mentor' + ) + return res + .status(403) + .json({ messages: ['You cannot get other users comment.'] }) + .end() + + if (req.user.role !== 'mentor') { + // Check if own solution + if (comment.parentType == 'solution') { + const solution = await Solution.findById(comment.parentId).lean().exec() + if (!solution) + return res + .status(404) + .json({ messages: ["Parent Object doesn't exist!"] }) + + if (solution.creator != req.user.schacc) + return res + .status(403) + .json({ messages: ['You cannot get other users comment.'] }) + .end() + } + } + + comment.creator = comment._creator + if (comment.isAnonim) comment.creator = '' + + return res + .status(200) + .json({ data: pick(comment, pickedKeys) }) + .end() + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.getMany = async (req, res) => { + try { + const comments = await Comment.find() + .populate('_creator', 'fullName nickName schacc') + .select(['-__v']) + .lean() + .exec() + + return res + .status(200) + .json({ + data: comments.map((e) => { + if (e.isAnonim) e.creator = '' + else e.creator = e._creator + return pick(e, pickedKeys) + }), + }) + .end() + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.createOne = async (req, res) => { + try { + if (req.user.role != 'mentor' && !req.body.creator) + req.body.creator = req.user.schacc + + let comment = await Comment.create({ + ...req.body, + }) + + // Add to parent + let updateParent = await getParentModel(comment).findById(comment.parentId) + if (!updateParent) { + await Comment.findByIdAndRemove(comment._id).lean().exec() + return res + .status(404) + .json({ messages: ["Parent Object doesn't exist!"] }) + } + if (updateParent.comments.indexOf(comment._id) == -1) { + updateParent.comments.push(comment._id) + try { + await updateParent.save() + } catch (error) { + await Comment.findByIdAndRemove(comment._id) + } + } + + comment = await comment + .populate('_creator', 'fullName nickName schacc') + .execPopulate() + + let retComment = comment.toObject() + retComment.creator = retComment._creator + + if (retComment.isAnonim) retComment.creator = '' + + return res + .status(200) + .json({ + data: pick(retComment, pickedKeys), + }) + .end() + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.updateOne = async (req, res) => { + try { + const comment = await Comment.findById(req.params.id).lean().exec() + + if (!comment) return res.status(404).end() + + if (comment.creator !== req.user.schacc && req.user.role != 'mentor') + return res + .status(403) + .json({ messages: ['You cannot update other users comment.'] }) + .end() + + const updatedComment = await Comment.findOneAndUpdate( + { _id: req.params.id }, + { text: req.body.text }, + { new: true } + ) + .lean() + .exec() + + if (updatedComment.isAnonim) updatedComment.creator = '' + + return res.status(200).json({ data: pick(updatedComment, pickedKeys) }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.removeOne = async (req, res) => { + try { + const comment = await Comment.findById({ _id: req.params.id }).lean().exec() + + if (comment.creator !== req.user.schacc && req.user.role != 'mentor') + return res + .status(403) + .json({ messages: ['You cannot delete other users comment.'] }) + .end() + + const removed = await Comment.findByIdAndRemove({ _id: req.params.id }) + if (!removed) { + return res.status(404).end() + } + + // remove from parent model + await getParentModel(removed).updateMany( + {}, + { $pull: { comments: { $in: removed._id } } } + ) + + if (removed.isAnonim) removed.creator = '' -exports.default = crudControllers(Comment) + return res + .status(200) + .json({ data: pick(removed, pickedKeys) }) + .end() + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} diff --git a/src/resources/comment/commentDocs.yml b/src/resources/comment/commentDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..a9a3ea2b8ff5b9ec33517bbfdaf83c788f791912 --- /dev/null +++ b/src/resources/comment/commentDocs.yml @@ -0,0 +1,127 @@ +openapi: '3.0.2' +info: + title: 'Comment Endpoint' + version: '1.0' + +paths: + /comment: + get: + tags: + - 'Comment' + summary: 'Get a List of comments' + description: 'This can only be get by a mentor.' + operationId: 'getAllComments' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Comment' + post: + tags: + - 'Comment' + summary: 'Create a comment' + description: 'Only logged in users can create a comment. + The creator only can be set by Mentor. + By default the creator is own user. + Automatically adds itself to parent object.' + operationId: 'createComment' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + /comment/id/{id}: + get: + tags: + - 'Comment' + summary: 'Get a comment by ID' + description: 'If not mentor only own comment + or comment on own solution can be get.' + operationId: 'getComment' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + delete: + tags: + - 'Comment' + summary: 'Delete a comment by ID' + description: 'Only logged in users can delete a comment. + To delete has to be the owner or mentor.' + operationId: 'deleteComment' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + put: + tags: + - 'Comment' + summary: 'Update a comment by ID' + description: 'Only logged in users can update a comment. + To update has to be the owner or mentor.' + operationId: 'updateOneComment' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + +components: + schemas: + Comment: + type: object + properties: + parentId: + type: string + description: ObjectId of the object that has the comment + parentType: + type: string + enum: + - 'solution' + - 'task' + - 'attendance' + - 'activity' + description: Name of the object that has the comment + creator: + type: string + description: Schacc of the creator. Default is own user + text: + type: string + isAnonim: + type: boolean + default: false + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - parentId + - parentType + - text + - isAnonim diff --git a/src/resources/comment/commentModel.js b/src/resources/comment/commentModel.js index de782a9ad446e26e80493f5e21e8575fd220b51d..6bdcd7057ba98898b552c18fe39829528a530701 100644 --- a/src/resources/comment/commentModel.js +++ b/src/resources/comment/commentModel.js @@ -2,6 +2,16 @@ const mongoose = require('mongoose') const CommentSchema = new mongoose.Schema( { + parentId: { + // can be an objectId (no ref) + type: mongoose.Schema.Types.ObjectId, + required: true, + }, + parentType: { + type: String, + required: true, + enum: ['solution', 'task', 'attendance', 'activity'], + }, creator: { type: String, }, @@ -9,9 +19,10 @@ const CommentSchema = new mongoose.Schema( type: String, required: true, }, - date: { - type: Date, + isAnonim: { + type: Boolean, required: true, + default: false, }, }, { timestamps: true } diff --git a/src/resources/comment/commentRouter.js b/src/resources/comment/commentRouter.js index f5cb84a37c3648fb59b7d895200ca90c2e6b5eb8..f1711782e830e09505b64b451d00e73e82141cc5 100644 --- a/src/resources/comment/commentRouter.js +++ b/src/resources/comment/commentRouter.js @@ -1,19 +1,24 @@ const { Router } = require('express') const controllers = require('./commentControllers') +const { + isLoggedIn, + isMentor, + isAcceptedOrMentor, +} = require('../../middlewares/auth') const router = Router() -// /api/item +// /api/v1/comment router .route('/') - .get(controllers.default.getMany) - .post(controllers.default.createOne) + .get(isLoggedIn, isMentor, controllers.default.getMany) + .post(isLoggedIn, isAcceptedOrMentor, controllers.default.createOne) -// /api/item/:id +// /api/v1/comment/id/:id router - .route('/:id') - .get(controllers.default.getOne) - .put(controllers.default.updateOne) - .delete(controllers.default.removeOne) + .route('/id/:id') + .get(isLoggedIn, isAcceptedOrMentor, controllers.default.getOne) + .put(isLoggedIn, isAcceptedOrMentor, controllers.default.updateOne) + .delete(isLoggedIn, isAcceptedOrMentor, controllers.default.removeOne) exports.default = router diff --git a/src/resources/extra/extraRouter.js b/src/resources/extra/extraRouter.js deleted file mode 100644 index 7508feb41bc0fe9f8da7ef6e0805c4dc4fa4ffbe..0000000000000000000000000000000000000000 --- a/src/resources/extra/extraRouter.js +++ /dev/null @@ -1,19 +0,0 @@ -const { Router } = require('express') -const { isLoggedIn } = require('../../middlewares/auth') -const { getOne, updateOne } = require('../user/userControllers').default - -const router = Router() - -// /api/v1/extra/me -router - .route('/me') // Tested in user - .get(isLoggedIn, async (req, res) => { - req.params.id = req.user.schacc - await getOne(req, res) - }) - .put(isLoggedIn, async (req, res) => { - req.params.id = req.user.schacc - await updateOne(req, res) - }) - -exports.default = router diff --git a/src/resources/groups/__tests__/groupsFuncTest.js b/src/resources/groups/__tests__/groupsFuncTest.js index 7f652dd6c919eea22044f5baa04105716de9a8e2..89cf4f82156c096a56dc49739f573167fa9ee753 100644 --- a/src/resources/groups/__tests__/groupsFuncTest.js +++ b/src/resources/groups/__tests__/groupsFuncTest.js @@ -13,7 +13,7 @@ const fakeGroupsJson = { } const defaultKeys = { - _id: true, + _id: false, name: true, description: true, groupPath: true, @@ -33,7 +33,7 @@ describe('/group "Mentor" Functionality', () => { // GET One test(`GET one returns with allowed keys`, async () => { const newGroup = await Groups.create(fakeGroupsJson) - let response = await authSession.get(`${endpointUrl}/${newGroup._id}`) + let response = await authSession.get(`${endpointUrl}/${newGroup.name}`) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) }) @@ -56,7 +56,7 @@ describe('/group "Mentor" Functionality', () => { // Delete test(`Delete returns with allowed keys`, async () => { const newGroup = await Groups.create(fakeGroupsJson) - let response = await authSession.delete(`${endpointUrl}/${newGroup._id}`) + let response = await authSession.delete(`${endpointUrl}/${newGroup.name}`) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) }) @@ -64,7 +64,7 @@ describe('/group "Mentor" Functionality', () => { test(`Update returns with allowed keys`, async () => { const newGroup = await Groups.create(fakeGroupsJson) let response = await authSession - .put(`${endpointUrl}/${newGroup._id}`) + .put(`${endpointUrl}/${newGroup.name}`) .send({ name: 'almafa', }) diff --git a/src/resources/groups/groupsControllers.js b/src/resources/groups/groupsControllers.js index 74c23cf2e260bbc73fea57f31c330b309e1cafee..bf65110e3013786bb1f82c7cdff8d2f79a393fb8 100644 --- a/src/resources/groups/groupsControllers.js +++ b/src/resources/groups/groupsControllers.js @@ -1,19 +1,67 @@ -const { crudControllers } = require('../../utils/crud') +const { crudControllers, createOne } = require('../../utils/crud') const { Groups } = require('./groupsModel') const { Application } = require('../application/applicationModel') const { pick } = require('lodash') -exports.default = crudControllers(Groups, [ - '_id', - 'name', - 'description', - 'groupPath', -]) +const pickedKeys = ['name', 'description', 'groupPath'] -exports.default.removeOne = async (req, res) => { +module.exports = crudControllers(Groups, pickedKeys) + +module.exports.getOne = async (req, res) => { + try { + const group = await Groups.findOne({ name: req.params.groupName }) + .lean() + .exec() + + if (!group) { + return res.status(404).end() + } + res.status(200).json({ data: pick(group, pickedKeys) }) + } catch (err) { + if (err.name == 'CastError') { + return res.status(422).json('Invalid ID provided') + } else { + console.error(err) + res.status(400).end() + } + } +} + +module.exports.updateOne = async (req, res) => { + try { + const updatedGroup = await Groups.findOneAndUpdate( + { + name: req.params.groupName, + }, + req.body, + { new: true, runValidators: true } + ) + .lean() + .exec() + + if (!updatedGroup) { + return res.status(404).end() + } + + res.status(200).json({ data: pick(updatedGroup, pickedKeys) }) + } catch (err) { + if (err.name == 'ValidationError') { + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + res.status(422).json({ messages }) + } else { + console.error(err) + res.status(400).end() + } + } +} + +module.exports.removeOne = async (req, res) => { try { const removed = await Groups.findOneAndRemove({ - _id: req.params.id, + name: req.params.groupName, }) if (!removed) { @@ -22,11 +70,9 @@ exports.default.removeOne = async (req, res) => { Application.updateMany({}, { $pull: { groups: { $in: removed._id } } }) - return res - .status(200) - .json({ - data: pick(removed, ['_id', 'name', 'description', 'groupPath']), - }) + return res.status(200).json({ + data: pick(removed, pickedKeys), + }) } catch (e) { console.error(e) return res.status(400).end() diff --git a/src/resources/groups/groupsDocs.yml b/src/resources/groups/groupsDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..b7d468e19cd20832fe99390390fa2bd6966fe54f --- /dev/null +++ b/src/resources/groups/groupsDocs.yml @@ -0,0 +1,101 @@ +openapi: '3.0.2' +info: + title: 'Groups Endpoint' + version: '1.0' + +paths: + /groups: + get: + tags: + - 'Groups' + summary: 'Get a List of groups' + description: 'Avaliable to anyone.' + operationId: 'getAllGroups' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Groups' + post: + tags: + - 'Groups' + summary: 'Create a group' + description: 'This can only be done by a mentor.' + operationId: 'createGroup' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + /groups/{name}: + get: + tags: + - 'Groups' + summary: 'Get a group by name' + description: 'Avaliable to anyone.' + operationId: 'getOneGroup' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + put: + tags: + - 'Groups' + summary: 'Update a Group by name' + description: 'This can only be done by a mentor.' + operationId: 'updateOneGroup' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + delete: + tags: + - 'Groups' + summary: 'Delete Group by name' + description: 'This can only be done by a mentor.' + operationId: 'deleteOneGroup' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Groups' + +components: + schemas: + Groups: + type: object + properties: + name: + type: string + description: + type: string + groupPath: + type: string + required: + - name + - description + - groupPath diff --git a/src/resources/groups/groupsModel.js b/src/resources/groups/groupsModel.js index 4fa47345a87903dbff26f6c5b7c13787329e4eb9..c3dc1c442db5b0b1bdce38bd21a4b543a3dc02a3 100644 --- a/src/resources/groups/groupsModel.js +++ b/src/resources/groups/groupsModel.js @@ -4,6 +4,7 @@ const GroupsSchema = new mongoose.Schema({ name: { type: String, required: true, + index: { unique: true }, }, description: { type: String, @@ -15,9 +16,6 @@ const GroupsSchema = new mongoose.Schema({ }, }) -// Careful with the docs, there are some deprecated ones -// https://mongoosejs.com/docs/guide.html - const Groups = mongoose.model('groups', GroupsSchema) exports.Groups = Groups diff --git a/src/resources/groups/groupsRouter.js b/src/resources/groups/groupsRouter.js index 5d00212fc9fdc8454cd09a85415a5d468bc2cffd..16902eecdecdf14a2c260ad3f06278f03b8d5b52 100644 --- a/src/resources/groups/groupsRouter.js +++ b/src/resources/groups/groupsRouter.js @@ -5,17 +5,17 @@ const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/groups +// /api/v1/groups router .route('/') - .get(controllers.default.getMany) - .post(isLoggedIn, isMentor, controllers.default.createOne) + .get(controllers.getMany) + .post(isLoggedIn, isMentor, controllers.createOne) -// /api/groups/:id +// /api/v1/groups/:groupName router - .route('/:id') - .get(controllers.default.getOne) - .put(isLoggedIn, isMentor, controllers.default.updateOne) - .delete(isLoggedIn, isMentor, controllers.default.removeOne) + .route('/:groupName') + .get(controllers.getOne) + .put(isLoggedIn, isMentor, controllers.updateOne) + .delete(isLoggedIn, isMentor, controllers.removeOne) exports.default = router diff --git a/src/resources/mentor/__tests__/mentorFuncTest.js b/src/resources/mentor/__tests__/mentorFuncTest.js new file mode 100644 index 0000000000000000000000000000000000000000..ac116aea0e867783b39fbc4d1ac91817edf3982f --- /dev/null +++ b/src/resources/mentor/__tests__/mentorFuncTest.js @@ -0,0 +1,122 @@ +const { app } = require('../../../server') +const session = require('supertest-session') +const { merge, has } = require('lodash') +const { validateKeys } = require('../../../utils/testHelpers') + +const { User } = require('../../user/userModel') +const { Mentor } = require('../mentorModel') + +const endpointUrl = '/api/v1/mentor' +const fakeUserJson = { + internal_id: 'fakeId', + schacc: 'fakeUser', + fullName: 'faker Janos', + secondaryEmail: 'faker@fake.com', +} +let fakeMentorsJson = { + user: fakeUserJson.schacc, + description: 'Legjobb mentor ever', +} +const defaultKeys = { + _id: true, + user: true, + description: true, +} + +describe('/mentor "Mentor" Functionality', () => { + let authSession + // Login as mentor + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/mentor`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + //readone + test(`GET existing mentor`, async () => { + const newMentor = await Mentor.create(fakeMentorsJson) + let response = await authSession.get(`${endpointUrl}/id/${newMentor._id}`) + expect(response.statusCode).toBe(200) + }) + test('GET invalid mentor', async () => { + let response = await authSession.get(`${endpointUrl}/id/almafa`) + expect(response.statusCode).toBe(422) + }) + // Get Many + test(`GET many returns with allowed keys`, async () => { + await Mentor.create(fakeMentorsJson) + await Mentor.create(fakeMentorsJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + response.body.data.forEach((eachData) => { + validateKeys(eachData, defaultKeys) + }) + }) + // Create + test(`Create mentor`, async () => { + const newUser = await User.create(fakeUserJson) + const newMentor = await Mentor.create(fakeMentorsJson) + let response = await authSession.post(`${endpointUrl}/`).send({ + user: newUser.schacc, + description: 'Legjobb mentor ever', + }) + expect(response.statusCode).toBe(201) + validateKeys(response.body.data, defaultKeys) + }) + //Update + test(`Update mentor`, async () => { + const newMentor = await Mentor.create(fakeMentorsJson) + let response = await authSession + .put(`${endpointUrl}/id/${newMentor._id}`) + .send({ + description: 'updateddescription', + }) + expect(response.statusCode).toBe(200) + expect(response.body.data.description).toBe('updateddescription') + }) + //Delete + test(`Delete mentor`, async () => { + const newMentor = await Mentor.create(fakeMentorsJson) + let response = await authSession.delete( + `${endpointUrl}/id/${newMentor._id}` + ) + expect(response.statusCode).toBe(200) + }) +}) +//Nem számít az ,hogy melyikkel teszteled mert mind2nak ugyan olyannak kell lennie. +//normal=accepted +describe('/mentor "Accepted" Functionality', () => { + let authSession + // Login as accepted and create a group + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/accepted`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + //readone + test(`GET existing mentor`, async () => { + const newUser = await User.create(fakeUserJson) + const newMentor = await Mentor.create(fakeMentorsJson) + let response = await authSession.get(`${endpointUrl}/id/${newMentor._id}`) + expect(response.statusCode).toBe(200) + }) + test('GET invalid mentor', async () => { + let response = await authSession.get(`${endpointUrl}/id/almafa`) + expect(response.statusCode).toBe(422) + }) + // Get Many + test(`GET many returns with allowed keys`, async () => { + await Mentor.create(fakeMentorsJson) + await Mentor.create(fakeMentorsJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + response.body.data.forEach((eachData) => { + validateKeys(eachData, defaultKeys) + }) + }) +}) diff --git a/src/resources/mentor/__tests__/mentorPermTest.js b/src/resources/mentor/__tests__/mentorPermTest.js index e2edcc6933f692f6052eaf6a4e704dec87755673..9618c35560e0c69d05b74999133edce6e6d362e5 100644 --- a/src/resources/mentor/__tests__/mentorPermTest.js +++ b/src/resources/mentor/__tests__/mentorPermTest.js @@ -11,13 +11,28 @@ const fakeMentorJson = { } describe('/mentor Permission tests', () => { + crudPermTest( + app, + endpointUrl + '/id', + Mentor, + 'mentor', + fakeMentorJson, + [false, false, true, true, true], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, true, true, false, false], + ['accepted', false, true, true, false, false], + ['mentor', true, true, true, true, true], + ] + ) crudPermTest( app, endpointUrl, Mentor, 'mentor', fakeMentorJson, - [true, true, true, true, true], + [true, true, false, false, false], [ // [role, create, readAll, readOne, update, delete] ['none', false, false, false, false, false], diff --git a/src/resources/mentor/mentorControllers.js b/src/resources/mentor/mentorControllers.js index d20349e8593659b6745b9a67d222e9f5709311df..4da413f67440c53ede0c0503e471a75448991a1c 100644 --- a/src/resources/mentor/mentorControllers.js +++ b/src/resources/mentor/mentorControllers.js @@ -23,11 +23,10 @@ exports.default.getOne = async (req, res) => { } catch (err) { if (err.name === 'CastError') { // Throwed by Mongoose - console.error(err) return res.status(422).json({ message: 'Invalid ID provided' }) } console.error(err) - res.status(400).end() + res.status(500).end() } } exports.default.getMany = async (req, res) => { @@ -47,7 +46,7 @@ exports.default.getMany = async (req, res) => { }) } catch (err) { console.error(err) - return res.status(400).end() + return res.status(500).end() } } exports.default.createOne = async (req, res) => { @@ -69,6 +68,6 @@ exports.default.createOne = async (req, res) => { } console.error(err) - return res.status(400).end() + return res.status(500).end() } } diff --git a/src/resources/mentor/mentorDocs.yml b/src/resources/mentor/mentorDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..14669b5cb32f20c2f6230b6907594c5ea74f66a0 --- /dev/null +++ b/src/resources/mentor/mentorDocs.yml @@ -0,0 +1,99 @@ +openapi: '3.0.2' +info: + title: 'Mentor Endpoint' + version: '1.0' + +paths: + /mentor: + get: + tags: + - 'Mentor' + summary: 'Get a List of mentors' + description: 'Have to be logged in.' + operationId: 'getAllMentor' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Mentor' + post: + tags: + - 'Mentor' + summary: 'Create a mentor' + description: 'Have to be mentor for this.' + operationId: 'createMentor' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Mentor' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Mentor' + /mentor/id/{id}: + get: + tags: + - 'Mentor' + summary: 'Get a mentor by ID' + description: 'Have to be logged in for this.' + operationId: 'getMentor' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Mentor' + put: + tags: + - 'Mentor' + summary: 'Update a mentor by ID' + description: 'Only mentors can update a mentor.' + operationId: 'updateOneMentor' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Mentor' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Mentor' + delete: + tags: + - 'Mentor' + summary: 'Delete a mentor by ID' + description: 'Only mentors can delete a mentor.' + operationId: 'deleteMentor' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Mentor' + +components: + schemas: + Mentor: + type: object + properties: + user: + type: string + description: schacc of the user + description: + type: string + required: + - user + - description diff --git a/src/resources/mentor/mentorModel.js b/src/resources/mentor/mentorModel.js index 6e7bcc13fa99c18ff5cdef3bb92624010d657196..a442bb8f2f7c2af09b328c01d091b30a3d2cbe95 100644 --- a/src/resources/mentor/mentorModel.js +++ b/src/resources/mentor/mentorModel.js @@ -2,8 +2,7 @@ const mongoose = require('mongoose') const MentorSchema = new mongoose.Schema({ user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'user', + type: String, required: true, }, description: { diff --git a/src/resources/mentor/mentorRouter.js b/src/resources/mentor/mentorRouter.js index 860a015d3e787de86be115e0bbad955f98ca0eb5..a3f0c19d8abe5f4f73a33af0ab645776ebc5aa9f 100644 --- a/src/resources/mentor/mentorRouter.js +++ b/src/resources/mentor/mentorRouter.js @@ -5,15 +5,15 @@ const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/item +// /api/v1/mentor router .route('/') .get(isLoggedIn, controllers.default.getMany) .post(isLoggedIn, isMentor, controllers.default.createOne) -// /api/item/:id +// /api/v1/mentor/id/:id router - .route('/:id') + .route('/id/:id') .get(isLoggedIn, controllers.default.getOne) .put(isLoggedIn, isMentor, controllers.default.updateOne) .delete(isLoggedIn, isMentor, controllers.default.removeOne) diff --git a/src/resources/news/__tests__/newsFuncTest.js b/src/resources/news/__tests__/newsFuncTest.js new file mode 100644 index 0000000000000000000000000000000000000000..44b40341989f8b804b124e04e90110ce4f748d2e --- /dev/null +++ b/src/resources/news/__tests__/newsFuncTest.js @@ -0,0 +1,118 @@ +const { app } = require('../../../server') +const session = require('supertest-session') +const { merge, has } = require('lodash') +const { validateKeys } = require('../../../utils/testHelpers') + +const { News } = require('../newsModel') +const endpointUrl = '/api/v1/news' +const fakeUserJson = { + internal_id: 'fakeId', + schacc: 'fakeUser', + fullName: 'faker Janos', + secondaryEmail: 'faker@fake.com', +} +let fakeNewsJson = { + title: 'A legjobb hír', + body: 'A legjobb hír ever man!', + creator: 'fakeUSer', +} +const defaultKeys = { + _id: true, + creator: true, + title: true, + body: true, +} + +describe('/news "Mentor" Functionality', () => { + let authSession + // Login as mentor + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/mentor`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + //readone + test(`GET existing news`, async () => { + const newNews = await News.create(fakeNewsJson) + let response = await authSession.get(`${endpointUrl}/id/${newNews._id}`) + expect(response.statusCode).toBe(200) + }) + test('GET invalid news', async () => { + let response = await authSession.get(`${endpointUrl}/id/almafa`) + expect(response.statusCode).toBe(422) + }) + // Get Many + test(`GET many returns with allowed keys`, async () => { + await News.create(fakeNewsJson) + await News.create(fakeNewsJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + response.body.data.forEach((eachData) => { + validateKeys(eachData, defaultKeys) + }) + }) + // Create + test(`Create news`, async () => { + const newNews = await News.create(fakeNewsJson) + let response = await authSession.post(`${endpointUrl}/`).send({ + title: newNews.title, + body: newNews.body, + creator: newNews.creator, + }) + expect(response.statusCode).toBe(201) + validateKeys(response.body.data, defaultKeys) + }) + //Update + test(`Update news`, async () => { + const newNews = await News.create(fakeNewsJson) + let response = await authSession + .put(`${endpointUrl}/id/${newNews._id}`) + .send({ + title: 'updatedtitle', + }) + expect(response.statusCode).toBe(200) + expect(response.body.data.title).toBe('updatedtitle') + }) + //Delete + test(`Delete news`, async () => { + const newNews = await News.create(fakeNewsJson) + let response = await authSession.delete(`${endpointUrl}/id/${newNews._id}`) + expect(response.statusCode).toBe(200) + }) +}) +describe('/news "Accepted" Functionality', () => { + let authSession + // Login as accepted + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/accepted`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + //readone + test(`GET existing news`, async () => { + const newNews = await News.create(fakeNewsJson) + let response = await authSession.get(`${endpointUrl}/id/${newNews._id}`) + expect(response.statusCode).toBe(200) + }) + test('GET invalid news', async () => { + let response = await authSession.get(`${endpointUrl}/id/almafa`) + expect(response.statusCode).toBe(422) + }) + // Get Many + test(`GET many returns with allowed keys`, async () => { + await News.create(fakeNewsJson) + await News.create(fakeNewsJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + response.body.data.forEach((eachData) => { + validateKeys(eachData, defaultKeys) + }) + }) +}) +// diff --git a/src/resources/news/__tests__/newsPermTest.js b/src/resources/news/__tests__/newsPermTest.js index 2e3735c801d6819a2ac6bb2e211fb226827fc77d..5039915cea8d81c7632f09261941ca390af36637 100644 --- a/src/resources/news/__tests__/newsPermTest.js +++ b/src/resources/news/__tests__/newsPermTest.js @@ -12,13 +12,28 @@ const fakeNewsJson = { } describe('/news Permission tests', () => { + crudPermTest( + app, + endpointUrl + '/id', + News, + 'news', + fakeNewsJson, + [false, false, true, true, true], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, true, true, false, false], + ['accepted', false, true, true, false, false], + ['mentor', true, true, true, true, true], + ] + ) crudPermTest( app, endpointUrl, News, 'news', fakeNewsJson, - [true, true, true, true, true], + [true, true, false, false, false], [ // [role, create, readAll, readOne, update, delete] ['none', false, false, false, false, false], diff --git a/src/resources/news/newsControllers.js b/src/resources/news/newsControllers.js index ae07019a722cce094e056563f63d72d7b248c96d..23f6bb8ccd67cf71b4c5f50e2929f4e22fbab8c3 100644 --- a/src/resources/news/newsControllers.js +++ b/src/resources/news/newsControllers.js @@ -20,12 +20,11 @@ exports.default.getOne = async (req, res) => { } catch (err) { if (err.name === 'CastError') { // Throwed by Mongoose - console.error(err) return res.status(422).json({ message: 'Invalid ID provided' }) } console.error(err) - res.status(400).end() + res.status(500).end() } } exports.default.getMany = async (req, res) => { @@ -43,7 +42,7 @@ exports.default.getMany = async (req, res) => { }) } catch (err) { console.error(err) - res, status(400).end() + res, status(500).end() } } @@ -66,6 +65,6 @@ exports.default.createOne = async (req, res) => { } console.error(err) - return res.status(400).end() + return res.status(500).end() } } diff --git a/src/resources/news/newsDocs.yml b/src/resources/news/newsDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..28f54d3c49468350a8f3b09ade46bac4a5014565 --- /dev/null +++ b/src/resources/news/newsDocs.yml @@ -0,0 +1,102 @@ +openapi: '3.0.2' +info: + title: 'News Endpoint' + version: '1.0' + +paths: + /news: + get: + tags: + - 'News' + summary: 'Get a List of news' + description: 'Have to be logged in.' + operationId: 'getAllNews' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/News' + post: + tags: + - 'News' + summary: 'Create a news' + description: 'Have to be mentor for this.' + operationId: 'createNews' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/News' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/News' + /news/id/{id}: + get: + tags: + - 'News' + summary: 'Get a news by ID' + description: 'Have to be logged in for this.' + operationId: 'getNews' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/News' + put: + tags: + - 'News' + summary: 'Update a news by ID' + description: 'Only mentors can update a news.' + operationId: 'updateOneNews' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/News' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/News' + delete: + tags: + - 'News' + summary: 'Delete a news by ID' + description: 'Only mentors can delete a news.' + operationId: 'deleteNews' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/News' + +components: + schemas: + News: + type: object + properties: + title: + type: string + body: + type: string + creator: + type: string + description: schacc of the user + required: + - title + - body + - creator diff --git a/src/resources/news/newsRouter.js b/src/resources/news/newsRouter.js index c2e6b3d9027cec1d9519a0c014f80ebd17c022c3..47a4c888c2aa6448aa26984a46b1b0da9b14eb18 100644 --- a/src/resources/news/newsRouter.js +++ b/src/resources/news/newsRouter.js @@ -5,15 +5,15 @@ const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/item +// /api/v1/news router .route('/') .get(isLoggedIn, controllers.default.getMany) .post(isLoggedIn, isMentor, controllers.default.createOne) -// /api/item/:id +// /api/v1/news/id/:id router - .route('/:id') + .route('/id/:id') .get(isLoggedIn, controllers.default.getOne) .put(isLoggedIn, isMentor, controllers.default.updateOne) .delete(isLoggedIn, isMentor, controllers.default.removeOne) diff --git a/src/resources/settings/__tests__/setingsFuncTest.js b/src/resources/settings/__tests__/setingsFuncTest.js new file mode 100644 index 0000000000000000000000000000000000000000..c75827e257d74f53df991accf15bb5bd581d6896 --- /dev/null +++ b/src/resources/settings/__tests__/setingsFuncTest.js @@ -0,0 +1,41 @@ +const { app } = require('../../../server') +const session = require('supertest-session') + +const { Settings } = require('../settingsModel') + +const endpointUrl = '/api/v1/settings' + +const fakeSettingsJson = { + applicationEndDate: '2020-12-11', +} + +describe('/settings Functionality', () => { + let authSession + // Login as mentor + beforeEach(function (done) { + authSession = session(app) + authSession.get(`/api/v1/login/mock/mentor`).end(function (err) { + if (err) return done(err) + return done() + }) + }) + // Create + test(`Create settings`, async () => { + let response = await authSession + .put(`${endpointUrl}`) + .send(fakeSettingsJson) + expect(response.statusCode).toBe(201) + expect(response.body.data.applicationEndDate).toBe( + '2020-12-11T00:00:00.000Z' + ) + }) + // Get + test(`Get settings`, async () => { + await Settings.create(fakeSettingsJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + expect(response.body.data.applicationEndDate).toBe( + '2020-12-11T00:00:00.000Z' + ) + }) +}) diff --git a/src/resources/settings/__tests__/settingsPermTest.js b/src/resources/settings/__tests__/settingsPermTest.js new file mode 100644 index 0000000000000000000000000000000000000000..f0a32c26e2c5ba3e19b8521307e30f76b981b4e0 --- /dev/null +++ b/src/resources/settings/__tests__/settingsPermTest.js @@ -0,0 +1,29 @@ +const { app } = require('../../../server') +const { crudPermTest } = require('../../../utils/testHelpers') + +const { Settings } = require('../settingsModel') + +const endpointUrl = '/api/v1/settings' + +const fakeSettingsJson = { + applicationEndDate: '2020-12-11', +} + +// put isn't tested, but should be okay +describe('/settings Permission tests', () => { + crudPermTest( + app, + endpointUrl, + Settings, + 'settings', + fakeSettingsJson, + [false, true, false, false, false], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, true, false, false, false], + ['normal', false, true, false, false, false], + ['accepted', false, true, false, false, false], + ['mentor', false, true, false, true, false], + ] + ) +}) diff --git a/src/resources/settings/settingsControllers.js b/src/resources/settings/settingsControllers.js index 9ddba2ef7a711fe7198829f4df9d0124daa1c52e..54123924b5c8faa6e743e84dc78789c10d9f74c4 100644 --- a/src/resources/settings/settingsControllers.js +++ b/src/resources/settings/settingsControllers.js @@ -2,42 +2,34 @@ const { crudControllers } = require('../../utils/crud') const { Settings } = require('./settingsModel') const { pick } = require('lodash') -exports.default = crudControllers(Settings, ['applicationEndDate']) +module.exports = crudControllers(Settings, ['applicationEndDate']) -// Make sure that only one setting exists -exports.default.createOne = async (req, res) => { +module.exports.getOne = async (req, res) => { try { - if (await Settings.findOne().lean().exec()) - throw { - name: 'alreadyExists', - message: `Already exists a setting, update it!`, - } + const doc = await Settings.findOne().lean().exec() - const doc = await Settings.create({ ...req.body }) - return res.status(201).json({ data: pick(doc, ['applicationEndDate']) }) + if (!doc) { + return res.status(404).end() + } + res.status(200).json({ data: pick(doc, ['applicationEndDate']) }) } catch (err) { - if (err.name == 'ValidationError') { - // Mongoose validation errors - let messages = [] - for (field in err.errors) { - messages.push(err.errors[field].message) - } - return res.status(422).json({ messages }) + if (err.name == 'CastError') { + return res.status(422).json('Invalid ID provided') + } else { + console.error(err) + res.status(400).end() } - if (err.name == 'alreadyExists') - return res.status(409).json({ messages: err.message }) - console.error(err) - return res.status(400).end() } } -exports.default.updateOne = async (req, res) => { +// If there is no settings, then create it +module.exports.updateOne = async (req, res) => { try { const oldSettings = await Settings.findOne().lean().exec() - if (!oldSettings) - return res - .status(404) - .json({ messages: 'There is no settings to update' }) + if (!oldSettings) { + const doc = await Settings.create({ ...req.body }) + return res.status(201).json({ data: pick(doc, ['applicationEndDate']) }) + } const updatedSettings = await Settings.findOneAndUpdate( { _id: oldSettings._id, diff --git a/src/resources/settings/settingsDocs.yml b/src/resources/settings/settingsDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..91af7bff02c244ff8a3e688b88b87171dfdf7cce --- /dev/null +++ b/src/resources/settings/settingsDocs.yml @@ -0,0 +1,51 @@ +openapi: '3.0.2' +info: + title: 'Settings Endpoint' + version: '1.0' + +paths: + /settings: + get: + tags: + - 'Settings' + summary: 'Get the global settings' + description: 'Can be get by all user. Contains the global settings.' + operationId: 'getSettings' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Settings' + put: + tags: + - 'Settings' + summary: 'Update or create if doesnt exist the global settings' + description: 'Have to be mentor.' + operationId: 'updateSettings' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Settings' + responses: + '201': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Settings' + +components: + schemas: + Settings: + type: object + properties: + applicationEndDate: + type: string + format: date-time + required: + - applicationEndDate diff --git a/src/resources/settings/settingsRouter.js b/src/resources/settings/settingsRouter.js index ba1cdd985465a37f585442c3af1913a59786aca2..0daa1d32501c22bd0f45a01a2b483cd3fe14a75a 100644 --- a/src/resources/settings/settingsRouter.js +++ b/src/resources/settings/settingsRouter.js @@ -8,15 +8,7 @@ const router = Router() // /api/settings router .route('/') - .get(isLoggedIn, isMentor, controllers.default.getMany) - .post(isLoggedIn, isMentor, controllers.default.createOne) - .put(isLoggedIn, isMentor, controllers.default.updateOne) - -// /api/settings/:id -router - .route('/:id') - .get(isLoggedIn, isMentor, controllers.default.getOne) - .put(isLoggedIn, isMentor, controllers.default.updateOne) - .delete(isLoggedIn, isMentor, controllers.default.removeOne) + .get(controllers.getOne) + .put(isLoggedIn, isMentor, controllers.updateOne) exports.default = router diff --git a/src/resources/solution/solutionControllers.js b/src/resources/solution/solutionControllers.js index 524b0ebe157656e73a17df5322b43b8591d66058..50316b9373e5fb7456a94689670cf40b532b6a9d 100644 --- a/src/resources/solution/solutionControllers.js +++ b/src/resources/solution/solutionControllers.js @@ -1,4 +1,188 @@ const { crudControllers } = require('../../utils/crud') const { Solution } = require('./solutionModel') +const { Task } = require('../task/taskModel') +const { pick } = require('lodash') exports.default = crudControllers(Solution) + +exports.default.createOne = async (req, res) => { + try { + const task = await Task.findById(req.body.task) + let solution + if (req.user.role === 'mentor' && req.body.creator) { + // Mentor Creates/Updates a Solution to someone + solution = await Solution.findOneAndUpdate( + { creator: req.body.creator, task: req.body.task }, + { ...req.body }, + { + upsert: true, // Create new One if there is no match + returnOriginal: false, + setDefaultsOnInsert: true, + runValidators: true, + } + ) + .lean() + .exec() + } else { + // Someone creates a solution + if (req.user.role != 'mentor') { + delete req.body.isAccepted + delete req.body.comments + } + if (task.deadline < new Date()) + return res.status(400).json({ + message: `Can't create new Solution! End date was ${task.deadline.toDateString()}`, + }) + + solution = await Solution.findOneAndUpdate( + { creator: req.user.schacc, task: req.body.task }, + { ...req.body }, + { + upsert: true, // Create new One if there is no match + returnOriginal: false, + setDefaultsOnInsert: true, + runValidators: true, + } + ) + .lean() + .exec() + } + if (task.solutions.indexOf(solution._id) == -1) { + task.solutions.push(solution._id) + await task.save() + } + + let retSolution = await Solution.findById({ _id: solution._id }) + .populate('_creator', '-_id fullName nickName schacc') + .populate('comments', 'text creator createdAt') + .lean() + .exec() + + retSolution.creator = retSolution._creator + return res.status(200).json({ + data: pick(retSolution, [ + '_id', + 'creator', + 'title', + 'description', + 'file', + 'task', + 'comments', + 'createdAt', + 'updatedAt', + 'isAccepted', + ]), + }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.getMany = async (req, res) => { + try { + let solutions = undefined + + if (req.user.role === 'mentor') + solutions = await Solution.find() + .populate('_creator', '-_id fullName nickName schacc') + .populate('comments', 'text creator createdAt') + .select('-__v') + .lean() + .exec() + else + solutions = await Solution.find({ user: req.user._id }) + .populate('_creator', '-_id fullName nickName') + .populate('comments', '-_id text creator createdAt') + .select('-__v') + .lean() + .exec() + + return res.status(200).json({ + data: solutions.map((e) => { + e.creator = e._creator + return pick(e, [ + '_id', + 'creator', + 'title', + 'description', + 'file', + 'task', + 'comments', + 'createdAt', + 'updatedAt', + 'isAccepted', + ]) + }), + }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.getOne = async (req, res) => { + try { + let solution = await Solution.findById({ _id: req.params.id }) + .populate('_creator', '-_id fullName nickName') + .populate('comments', 'text creator createdAt') + .select('-__v') + .lean() + .exec() + + if (!solution) + return res.status(404).json({ messages: ['No such solution.'] }) + + if (req.user.schacc !== solution.creator && req.user.role !== 'mentor') { + return res + .status(403) + .json({ message: `You don't have permission for this solution.` }) + } + + return res.status(200).json({ + data: pick(solution, [ + '_id', + 'creator', + 'title', + 'description', + 'file', + 'task', + 'comments', + 'createdAt', + 'updatedAt', + 'isAccepted', + ]), + }) + } catch (err) { + if (err.name == 'ValidationError') { + // Throwed by Mongoose + let messages = [] + for (field in err.errors) { + messages.push(err.errors[field].message) + } + return res.status(422).json({ messages }) + } + console.error(err) + return res.status(500).json({ message: err.message }) + } +} + +exports.default.notSupported = async (req, res) => { + res.status(404).json({ data: { message: 'Not supported operation!' } }) +} diff --git a/src/resources/solution/solutionDocs.yml b/src/resources/solution/solutionDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..00b37c67515588bb6c3917adf627cd9ac5ffc5fa --- /dev/null +++ b/src/resources/solution/solutionDocs.yml @@ -0,0 +1,124 @@ +openapi: '3.0.2' +info: + title: 'Solution Endpoint' + version: '1.0' + +paths: + /solution: + get: + tags: + - 'Solution' + summary: 'Get a List of solution' + description: 'Have to be accepted or mentor. + As an accepted only own solutions can be seen.' + operationId: 'getAllSolution' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Solution' + post: + tags: + - 'Solution' + summary: 'Create a solution' + description: 'Have to be accepted or mentor. + As an accepted only own own solution can be made.' + operationId: 'createSolution' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Solution' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Solution' + /solution/id/{id}: + get: + tags: + - 'Solution' + summary: 'Get a solution by ID' + description: 'Have to be accepted or mentor. + As an accepted only own solutions can be get.' + operationId: 'getSolution' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Solution' + put: + tags: + - 'Solution' + summary: 'Update a solution by ID' + description: 'Have to be accepted or mentor. + As an accepted only own solutions can be get. + After deadline cant make new solution as accepted.' + operationId: 'updateOneSolution' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Solution' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Solution' + delete: + tags: + - 'Solution' + summary: 'Delete a solution by ID' + description: 'Have to be accepted or mentor. + As an accepted only own solutions can be delted. + After deadline cant make new solution as accepted.' + operationId: 'deleteSolution' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Solution' + +components: + schemas: + Solution: + type: object + properties: + task: + type: string + description: cuid of the task + title: + type: string + description: + type: string + file: + type: string + creator: + type: string + description: schacc of the user + comments: + type: array + items: + type: string + description: cuid of each comment + isAccepted: + type: boolean + default: false + required: + - task + - title + - description + - creator + - isAccepted diff --git a/src/resources/solution/solutionModel.js b/src/resources/solution/solutionModel.js index cc973a356d3aa438e5070efe4ffd8aafe4b181c5..11c1a52a47585bca4a70535428b389661343757c 100644 --- a/src/resources/solution/solutionModel.js +++ b/src/resources/solution/solutionModel.js @@ -2,6 +2,11 @@ const mongoose = require('mongoose') const SolutionSchema = new mongoose.Schema( { + task: { + type: mongoose.Schema.Types.ObjectId, + ref: 'task', + required: true, + }, title: { type: String, required: true, @@ -12,30 +17,32 @@ const SolutionSchema = new mongoose.Schema( }, file: { type: String, - required: true, }, - user: { - type: mongoose.Schema.Types.ObjectId, - ref: 'user', + creator: { + type: String, required: true, }, - comment: [ + comments: [ { type: mongoose.Schema.Types.ObjectId, ref: 'comment', - required: true, }, ], isAccepted: { type: Boolean, required: true, + default: false, }, }, { timestamps: true } ) -// Careful with the docs, there are some deprecated ones -// https://mongoosejs.com/docs/guide.html +SolutionSchema.virtual('_creator', { + ref: 'user', + localField: 'creator', + foreignField: 'schacc', + justOne: true, +}) const Solution = mongoose.model('solution', SolutionSchema) diff --git a/src/resources/solution/solutionRouter.js b/src/resources/solution/solutionRouter.js index f123b0dd11e2ebeecdc6e434d2db4395cbca999c..ec248934ba3847ecb204c0117f0e259d407a8e37 100644 --- a/src/resources/solution/solutionRouter.js +++ b/src/resources/solution/solutionRouter.js @@ -1,19 +1,20 @@ const { Router } = require('express') const controllers = require('./solutionControllers') +const { isLoggedIn, isAcceptedOrMentor } = require('../../middlewares/auth') const router = Router() -// /api/item +// /api/v1/solution router .route('/') - .get(controllers.default.getMany) - .post(controllers.default.createOne) + .get(isLoggedIn, isAcceptedOrMentor, controllers.default.getMany) + .post(isLoggedIn, isAcceptedOrMentor, controllers.default.createOne) -// /api/item/:id +// /api/v1/solution/id/:id router - .route('/:id') - .get(controllers.default.getOne) - .put(controllers.default.updateOne) - .delete(controllers.default.removeOne) + .route('/id/:id') + .get(isLoggedIn, isAcceptedOrMentor, controllers.default.getOne) + .put(isLoggedIn, isAcceptedOrMentor, controllers.default.notSupported) + .delete(isLoggedIn, isAcceptedOrMentor, controllers.default.notSupported) exports.default = router diff --git a/src/resources/task/__tests__/taskFuncTest.js b/src/resources/task/__tests__/taskFuncTest.js new file mode 100644 index 0000000000000000000000000000000000000000..8acc47f99c308b79470bdf5953ca2a4085d2cf63 --- /dev/null +++ b/src/resources/task/__tests__/taskFuncTest.js @@ -0,0 +1,125 @@ +const { app } = require('../../../server') +const session = require('supertest-session') +const { merge, has } = require('lodash') +const { validateKeys } = require('../../../utils/testHelpers') + +const { User } = require('../../user/userModel') +const { Task } = require('../taskModel') + +const endpointUrl = '/api/v1/task' +const fakeUserJson = { + internal_id: 'fakeId', + schacc: 'fakeUser', + fullName: 'faker Janos', + secondaryEmail: 'faker@fake.com', +} +let fakeTaskJson = { + title: 'The greatest task', + description: 'Ezt kell csinálni a greatest tasknál', + deadline: '2020.12.31', + bit: '10', +} +const defaultKeys = { + _id: true, + title: true, + description: true, + deadline: true, + bit: true, +} + +describe('/task "Mentor" Functionality', () => { + let authSession + // Login as mentor + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/mentor`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + //readone + test(`GET existing task`, async () => { + const newTask = await Task.create(fakeTaskJson) + let response = await authSession.get(`${endpointUrl}/id/${newTask._id}`) + expect(response.statusCode).toBe(200) + }) + test('GET invalid task', async () => { + let response = await authSession.get(`${endpointUrl}/id/almafa`) + expect(response.statusCode).toBe(422) + }) + // Get Many + test(`GET many returns with allowed keys`, async () => { + await Task.create(fakeTaskJson) + await Task.create(fakeTaskJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + response.body.data.forEach((eachData) => { + validateKeys(eachData, defaultKeys) + }) + }) + // Create + test(`Create task`, async () => { + const newTask = await Task.create(fakeTaskJson) + let response = await authSession.post(`${endpointUrl}/`).send({ + title: newTask.title, + description: newTask.description, + deadline: newTask.deadline, + createData: newTask.createData, + bit: newTask.bit, + }) + expect(response.statusCode).toBe(201) + validateKeys(response.body.data, defaultKeys) + }) + //Update + test(`Update task`, async () => { + const newTask = await Task.create(fakeTaskJson) + let response = await authSession + .put(`${endpointUrl}/id/${newTask._id}`) + .send({ + title: 'updatedtitle', + }) + expect(response.statusCode).toBe(200) + expect(response.body.data.title).toBe('updatedtitle') + }) + //Delete + test(`Delete task`, async () => { + const newTask = await Task.create(fakeTaskJson) + let response = await authSession.delete(`${endpointUrl}/id/${newTask._id}`) + expect(response.statusCode).toBe(200) + }) +}) +//Nem számít az ,hogy melyikkel teszteled mert mind2nak ugyan olyannak kell lennie. +//normal=accepted +describe('/task "Accepted" Functionality', () => { + let authSession + // Login as accepted and create a group + beforeEach(async function (done) { + let testSession = session(app) + testSession.get(`/api/v1/login/mock/accepted`).end(function (err) { + if (err) return done(err) + authSession = testSession + return done() + }) + }) + //readone + test(`GET existing task`, async () => { + const newTask = await Task.create(fakeTaskJson) + let response = await authSession.get(`${endpointUrl}/id/${newTask._id}`) + expect(response.statusCode).toBe(200) + }) + test('GET invalid task', async () => { + let response = await authSession.get(`${endpointUrl}/id/almafa`) + expect(response.statusCode).toBe(422) + }) + // Get Many + test(`GET many returns with allowed keys`, async () => { + await Task.create(fakeTaskJson) + await Task.create(fakeTaskJson) + let response = await authSession.get(endpointUrl) + expect(response.statusCode).toBe(200) + response.body.data.forEach((eachData) => { + validateKeys(eachData, defaultKeys) + }) + }) +}) diff --git a/src/resources/task/__tests__/taskPermTest.js b/src/resources/task/__tests__/taskPermTest.js index 478ba9b19bdba5a6d44c8839a4f867485fdfc2a7..9d68f713b9962d3148c980350e4822dbe3871a86 100644 --- a/src/resources/task/__tests__/taskPermTest.js +++ b/src/resources/task/__tests__/taskPermTest.js @@ -15,13 +15,28 @@ const fakeTaskJson = { } describe('/task Permission tests', () => { + crudPermTest( + app, + endpointUrl + '/id', + Task, + 'task', + fakeTaskJson, + [false, false, true, true, true], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, false, false, false, false], + ['accepted', false, true, true, false, false], + ['mentor', true, true, true, true, true], + ] + ) crudPermTest( app, endpointUrl, Task, 'task', fakeTaskJson, - [true, true, true, true, true], + [true, true, false, false, false], [ // [role, create, readAll, readOne, update, delete] ['none', false, false, false, false, false], diff --git a/src/resources/task/taskControllers.js b/src/resources/task/taskControllers.js index 129937434f8712460cd87c931eda5eb92373d386..865d652a5dbd5874a387cb13af24abdae65b2028 100644 --- a/src/resources/task/taskControllers.js +++ b/src/resources/task/taskControllers.js @@ -7,7 +7,6 @@ exports.default = crudControllers(Task, [ 'title', 'description', 'deadline', - 'createData', 'bit', 'creator', ]) @@ -18,6 +17,7 @@ exports.default.getOne = async (req, res) => { path: '_creator', select: '-_id schacc fullName secondaryEmail', }) + .populate('comments', 'text creator createdAt') .lean() .exec() @@ -32,6 +32,7 @@ exports.default.getOne = async (req, res) => { 'createData', 'bit', 'creator', + 'solutions', ]), }) } else { @@ -40,12 +41,11 @@ exports.default.getOne = async (req, res) => { } catch (err) { if (err.name === 'CastError') { // Throwed by Mongoose - console.error(err) return res.status(422).json({ message: 'Invalid ID provided' }) } console.error(err) - res.status(400).end() + res.status(500).end() } } exports.default.getMany = async (req, res) => { @@ -55,6 +55,7 @@ exports.default.getMany = async (req, res) => { path: '_creator', select: '-_id schacc fullName secondaryEmail', }) + .populate('comments', 'text creator createdAt') .lean() .exec() @@ -69,12 +70,13 @@ exports.default.getMany = async (req, res) => { 'createData', 'bit', 'creator', + 'solutions', ]) }), }) } catch (err) { console.error(err) - res, status(400).end() + res.status(500).end() } } exports.default.createOne = async (req, res) => { @@ -87,6 +89,7 @@ exports.default.createOne = async (req, res) => { 'createData', 'bit', 'creator', + 'solutions', ]) ) res.status(201).json({ @@ -98,6 +101,7 @@ exports.default.createOne = async (req, res) => { 'createData', 'bit', 'creator', + 'solutions', ]), }) } catch (err) { @@ -111,6 +115,6 @@ exports.default.createOne = async (req, res) => { } console.error(err) - return res.status(400).end() + return res.status(500).end() } } diff --git a/src/resources/task/taskDocs.yml b/src/resources/task/taskDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..3e73925a92a9966484bb743daab956207fd80eb1 --- /dev/null +++ b/src/resources/task/taskDocs.yml @@ -0,0 +1,119 @@ +openapi: '3.0.2' +info: + title: 'Task Endpoint' + version: '1.0' + +paths: + /task: + get: + tags: + - 'Task' + summary: 'Get a List of tasks' + description: 'Have to be accepted or mentor.' + operationId: 'getAllTask' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/Task' + post: + tags: + - 'Task' + summary: 'Create a task' + description: 'Have to be mentor.' + operationId: 'createTask' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + /task/id/{id}: + get: + tags: + - 'Task' + summary: 'Get a task by ID' + description: 'Have to be accepted or mentor.' + operationId: 'getTask' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + put: + tags: + - 'Task' + summary: 'Update a task by ID' + description: 'Have to be mentor.' + operationId: 'updateOneTask' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + delete: + tags: + - 'Task' + summary: 'Delete a task by ID' + description: 'Have to be mentor.' + operationId: 'deleteTask' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + +components: + schemas: + Task: + type: object + properties: + title: + type: string + description: + type: string + deadline: + type: string + format: date-time + bit: + type: number + default: 0 + creator: + type: string + description: schacc of the user. Default is the current user. + tasks: + type: array + items: + type: string + description: cuid of each task + comments: + type: array + items: + type: string + description: cuid of each comment + required: + - title + - description + - deadline + - bit diff --git a/src/resources/task/taskModel.js b/src/resources/task/taskModel.js index 1832ca9012baa87b6c0f31eaa607aa641d89f7b2..14a4660be390283a14eb53d4ae2094dc0ef10bd7 100644 --- a/src/resources/task/taskModel.js +++ b/src/resources/task/taskModel.js @@ -14,17 +14,26 @@ const TaskSchema = new mongoose.Schema( type: Date, required: true, }, - createData: { - type: String, - required: false, - }, bit: { type: Number, - require: true, + required: true, + default: 0, }, creator: { type: String, }, + solutions: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'solution', + }, + ], + comments: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'comment', + }, + ], }, { timestamps: true } ) diff --git a/src/resources/task/taskRouter.js b/src/resources/task/taskRouter.js index de80cac6bd5570d3a1a27e4bba99c884b76c2319..ecd9fbb1f48c1889fe878d627e0b8a4e7c69a2b4 100644 --- a/src/resources/task/taskRouter.js +++ b/src/resources/task/taskRouter.js @@ -9,15 +9,15 @@ const { const router = Router() -// /api/item +// /api/v1/task router .route('/') .get(isLoggedIn, isAcceptedOrMentor, controllers.default.getMany) .post(isLoggedIn, isMentor, controllers.default.createOne) -// /api/item/:id +// /api/v1/task/id/:id router - .route('/:id') + .route('/id/:id') .get(isLoggedIn, isAcceptedOrMentor, controllers.default.getOne) .put(isLoggedIn, isMentor, controllers.default.updateOne) .delete(isLoggedIn, isMentor, controllers.default.removeOne) diff --git a/src/resources/user/__tests__/userFuncTest.js b/src/resources/user/__tests__/userFuncTest.js index 78bc44144537bbd8a27cd36bcf50d20f66d4f86f..973d7671562048b880459fecb73d62e4fe4f854d 100644 --- a/src/resources/user/__tests__/userFuncTest.js +++ b/src/resources/user/__tests__/userFuncTest.js @@ -42,7 +42,9 @@ describe('/user "Mentor" Functionality', () => { // GET One test(`GET one returns with allowed keys`, async () => { let newUser = await User.create(fakeUserJson) - let response = await authSession.get(`${endpointUrl}/${newUser.schacc}`) + let response = await authSession.get( + `${endpointUrl}/schacc/${newUser.schacc}` + ) expect(response.statusCode).toBe(200) validateKeys( @@ -52,11 +54,11 @@ describe('/user "Mentor" Functionality', () => { newUser = await User.create( Object.assign({}, fakeUserJson, { imagePath: 'almafa', schacc: 'apple' }) ) - response = await authSession.get(`${endpointUrl}/${newUser.schacc}`) + response = await authSession.get(`${endpointUrl}/schacc/${newUser.schacc}`) expect(has(response.body.data, 'imagePath')).toBe(true) }) test(`GET own user returns with allowed keys`, async () => { - const response = await authSession.get(`/api/v1/extra/me`) + const response = await authSession.get(`${endpointUrl}/me`) expect(response.statusCode).toBe(200) validateKeys( @@ -79,7 +81,9 @@ describe('/user "Mentor" Functionality', () => { // SoftDelete test(`Delete user returns with allowed keys`, async () => { const newUser = await User.create(fakeUserJson) - let response = await authSession.delete(`${endpointUrl}/${newUser.schacc}`) + let response = await authSession.delete( + `${endpointUrl}/schacc/${newUser.schacc}` + ) expect(response.statusCode).toBe(200) validateKeys( response.body.data, @@ -92,7 +96,7 @@ describe('/user "Mentor" Functionality', () => { test(`Update user returns with allowed keys`, async () => { const newUser = await User.create(fakeUserJson) let response = await authSession - .put(`${endpointUrl}/${newUser.schacc}`) + .put(`${endpointUrl}/schacc/${newUser.schacc}`) .send({ fullName: 'Janos', secondaryEmail: 'jani@gmail.com', @@ -119,13 +123,22 @@ describe('/user "Mentor" Functionality', () => { test(`Update with invalid email`, async () => { const newUser = await User.create(fakeUserJson) let response = await authSession - .put(`${endpointUrl}/${newUser.schacc}`) + .put(`${endpointUrl}/schacc/${newUser.schacc}`) .send({ secondaryEmail: 'testemail', }) expect(response.statusCode).toBe(422) expect(response.body.messages).not.toBe(undefined) }) + test(`GET users by role`, async () => { + const user = await User.create(fakeUserJson) + let response = await authSession.get(`${endpointUrl}/role/normal`) + expect(response.body.data.length).toBe(1) + response = await authSession.get(`${endpointUrl}/role/accepted`) + expect(response.body.data.length).toBe(0) + response = await authSession.get(`${endpointUrl}/role/mentor`) + expect(response.body.data.length).toBe(1) + }) }) describe('/user "Accepted" Functionality', () => { @@ -140,13 +153,13 @@ describe('/user "Accepted" Functionality', () => { }) // GET One test(`GET own user returns with allowed keys`, async () => { - const response = await authSession.get(`/api/v1/extra/me`) + const response = await authSession.get(`${endpointUrl}/me`) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) }) // Update own user test(`Update own user returns with allowed keys`, async () => { - let response = await authSession.put(`/api/v1/extra/me`).send({ + let response = await authSession.put(`${endpointUrl}/me`).send({ fullName: 'Janos', secondaryEmail: 'jani@gmail.com', bit: 10, @@ -185,13 +198,13 @@ describe('/user "Normal" Functionality', () => { }) // GET One test(`GET own user returns with allowed keys`, async () => { - const response = await authSession.get(`/api/v1/extra/me`) + const response = await authSession.get(`${endpointUrl}/me`) expect(response.statusCode).toBe(200) validateKeys(response.body.data, defaultKeys) }) // Update own user test(`Update own user returns with allowed keys`, async () => { - let response = await authSession.put(`/api/v1/extra/me`).send({ + let response = await authSession.put(`${endpointUrl}/me`).send({ fullName: 'Janos', secondaryEmail: 'jani@gmail.com', bit: 10, diff --git a/src/resources/user/__tests__/userPermTest.js b/src/resources/user/__tests__/userPermTest.js index f51708520ff91a3259f36f4c2fcbf705bbe9d239..0536d29a945eceda0af91e6ad9cbe05b0602d3fd 100644 --- a/src/resources/user/__tests__/userPermTest.js +++ b/src/resources/user/__tests__/userPermTest.js @@ -17,11 +17,11 @@ describe('/user Permission tests', () => { let authSession crudPermTest( app, - endpointUrl, + endpointUrl + '/schacc', User, 'user', fakeUserJson, - [false, true, true, true, true], + [false, false, true, true, true], [ // [role, create, readAll, readOne, update, delete] ['none', false, false, false, false, false], @@ -41,14 +41,33 @@ describe('/user Permission tests', () => { }) }) // All roles - test(`Can Get own user on /:id or on /me`, async () => { + test(`Can Get own user on own /schacc/{schacc} or on /me`, async () => { user = await User.findOne().lean().exec() - let response = await authSession.get(`${endpointUrl}/${user.schacc}`) + let response = await authSession.get( + `${endpointUrl}/schacc/${user.schacc}` + ) expect(response.statusCode).toBe(200) expect(response.body.data.schacc).toBe(user.schacc) - response = await authSession.get(`/api/v1/extra/me`) + response = await authSession.get(`${endpointUrl}/me`) expect(response.statusCode).toBe(200) expect(response.body.data.schacc).toBe(user.schacc) }) }) + + crudPermTest( + app, + endpointUrl, + User, + 'user', + fakeUserJson, + [false, true, false, false, false], + [ + // [role, create, readAll, readOne, update, delete] + ['none', false, false, false, false, false], + ['normal', false, false, false, false, false], + ['accepted', false, false, false, false, false], + ['mentor', false, true, true, true, true], + ], + { schacc: 'alma' } + ) }) diff --git a/src/resources/user/userControllers.js b/src/resources/user/userControllers.js index 8543e6a9db1f7d1bce99d48cf111a2706fbf9994..875314b1a99dd76d79143b6950b1e87324ca017b 100644 --- a/src/resources/user/userControllers.js +++ b/src/resources/user/userControllers.js @@ -2,7 +2,7 @@ const { crudControllers, getMany } = require('../../utils/crud') const { User } = require('./userModel') const { pick } = require('lodash') -const defaultKeys = [ +const pickedKeys = [ 'schacc', 'fullName', 'secondaryEmail', @@ -29,18 +29,16 @@ const getUpdateKeysByRole = (role) => { return notMentorKeys } -exports.default = crudControllers(User, defaultKeys) - -exports.default.getOne = async (req, res) => { +module.exports.getOne = async (req, res) => { try { // Get Own User - if (req.params.id === req.user.schacc && req.user.role !== 'mentor') - return res.status(200).json({ data: pick(req.user, defaultKeys) }) + if (req.params.schacc === req.user.schacc && req.user.role !== 'mentor') + return res.status(200).json({ data: pick(req.user, pickedKeys) }) // Get Other User if (req.user.role !== 'mentor') return res.status(403).end() - const userSchacc = req.params.id + const userSchacc = req.params.schacc // Get user from db const user = await User.findOne({ schacc: userSchacc }).lean().exec() @@ -49,7 +47,7 @@ exports.default.getOne = async (req, res) => { return res .status(200) - .json({ data: pick(user, [...defaultKeys, 'bit', 'presence']) }) + .json({ data: pick(user, [...pickedKeys, 'bit', 'presence']) }) } catch (err) { if (err.name === 'CastError') // Throwed by Mongoose @@ -61,13 +59,24 @@ exports.default.getOne = async (req, res) => { } // Overwrite the returned keys -exports.default.getMany = getMany(User, [...defaultKeys, 'bit', 'presence']) +module.exports.getMany = getMany(User, [...pickedKeys, 'bit', 'presence']) + +module.exports.getManyByRole = async (req, res) => { + try { + const docs = await User.find({ role: req.params.role }).lean().exec() + + res.status(200).json({ data: docs.map((e) => pick(e, pickedKeys)) }) + } catch (e) { + console.error(e) + res.status(400).end() + } +} // Doesn't delete the user just disable it -exports.default.softRemove = async (req, res) => { +module.exports.softRemove = async (req, res) => { try { let user = await User.findOneAndUpdate( - { schacc: req.params.id }, + { schacc: req.params.schacc }, { valid: false, receiveMail: false, @@ -83,19 +92,19 @@ exports.default.softRemove = async (req, res) => { return res .status(200) - .json({ data: pick(user, [...defaultKeys, 'bit', 'presence']) }) + .json({ data: pick(user, [...pickedKeys, 'bit', 'presence']) }) } catch (e) { console.error(e) return res.status(400).end() } } -exports.default.updateOne = async (req, res) => { +module.exports.updateOne = async (req, res) => { try { - if (!(req.params.id === req.user.schacc || req.user.role === 'mentor')) + if (!(req.params.schacc === req.user.schacc || req.user.role === 'mentor')) return res.status(403).end() - const userSchacc = req.params.id + const userSchacc = req.params.schacc // Update with given keys const updatedUser = await User.findOneAndUpdate( @@ -114,8 +123,8 @@ exports.default.updateOne = async (req, res) => { // Pick keys let returnData if (req.user.role === 'mentor') - returnData = pick(updatedUser, [...defaultKeys, 'bit', 'presence']) - else returnData = pick(updatedUser, defaultKeys) + returnData = pick(updatedUser, [...pickedKeys, 'bit', 'presence']) + else returnData = pick(updatedUser, pickedKeys) return res.status(200).json({ data: returnData, diff --git a/src/resources/user/userDocs.yml b/src/resources/user/userDocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a836b3532505d043001f9dd2b3e18d1bf83136f --- /dev/null +++ b/src/resources/user/userDocs.yml @@ -0,0 +1,163 @@ +openapi: '3.0.2' +info: + title: 'User Endpoint' + version: '1.0' + +paths: + /user: + get: + tags: + - 'User' + summary: 'Get a List of users' + description: 'This can only be done by a mentor.' + operationId: 'getAllUser' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/User' + /user/me: + get: + tags: + - 'User' + summary: 'Get own user' + description: 'Have to be logged in for this.' + operationId: 'getOwnUser' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + put: + tags: + - 'User' + summary: 'Update own user' + description: 'Have to be logged in for this.' + operationId: 'updateOwnUser' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + /user/role/{role}: + get: + tags: + - 'User' + summary: 'Get a List of users by role' + description: 'This can only be done by a mentor.' + operationId: 'getAllUserByRole' + responses: + '200': + description: OK + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/User' + /user/schacc/{schacc}: + get: + tags: + - 'User' + summary: 'Get User by schacc' + description: 'This can only be done by a mentor or on own User.' + operationId: 'getOneUser' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + put: + tags: + - 'User' + summary: 'Update a User by schacc' + description: 'This can only be done by a mentor or on own User.' + operationId: 'updateOneUser' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + delete: + tags: + - 'User' + summary: 'Delete User by schacc' + description: 'This can only be done by a mentor. + It only sets the valid and receiveMail field to false' + operationId: 'deleteOneUser' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + +components: + schemas: + User: + type: object + properties: + internal_id: + type: string + schacc: + type: string + fullName: + type: string + maxLength: 50 + secondaryEmail: + type: string + format: email + receiveMail: + type: boolean + default: true + nickName: + type: string + maxLength: 50 + bit: + type: number + default: 0 + presence: + type: number + default: 0 + imagePath: + type: string + role: + type: string + enum: + - 'accepted' + - 'normal' + - 'mentor' + default: 'normal' + valid: + type: boolean + default: true + required: + - internal_id + - schacc + - fullName + - secondaryEmail + - role + - valid diff --git a/src/resources/user/userModel.js b/src/resources/user/userModel.js index 9c7ababac277b164d1195bd6d0c20943bde1d329..9321af1c6a800207c6366647313bdfd892cc5264 100644 --- a/src/resources/user/userModel.js +++ b/src/resources/user/userModel.js @@ -10,7 +10,7 @@ const UserSchema = new mongoose.Schema( schacc: { type: String, required: true, - unique: true, + index: { unique: true }, }, fullName: { type: String, @@ -47,7 +47,6 @@ const UserSchema = new mongoose.Schema( }, imagePath: { type: String, - required: false, }, role: { type: String, @@ -58,13 +57,12 @@ const UserSchema = new mongoose.Schema( valid: { type: Boolean, default: true, + required: true, }, }, { timestamps: true } ) -UserSchema.index({ schacc: 1 }) - const User = mongoose.model('user', UserSchema) exports.User = User diff --git a/src/resources/user/userRouter.js b/src/resources/user/userRouter.js index ca0638306c7ece2d9cb98fe5fd7e4f0cb4a794c7..583a0c920c50a3a8cbc16aa0c3d9e8726492d0cf 100644 --- a/src/resources/user/userRouter.js +++ b/src/resources/user/userRouter.js @@ -4,14 +4,29 @@ const { isLoggedIn, isMentor } = require('../../middlewares/auth') const router = Router() -// /api/user -router.route('/').get(isLoggedIn, isMentor, controllers.default.getMany) +// /api/v1/user +router.route('/').get(isLoggedIn, isMentor, controllers.getMany) -// /api/user/:id +// /api/v1/user/role/:role +router.route('/role/:role').get(isLoggedIn, isMentor, controllers.getManyByRole) + +// /api/v1/user/schacc/:schacc +router + .route('/schacc/:schacc') + .get(isLoggedIn, controllers.getOne) + .put(isLoggedIn, controllers.updateOne) + .delete(isLoggedIn, isMentor, controllers.softRemove) + +// /api/v1/user/me router - .route('/:id') - .get(isLoggedIn, controllers.default.getOne) - .put(isLoggedIn, controllers.default.updateOne) - .delete(isLoggedIn, isMentor, controllers.default.softRemove) + .route('/me') + .get(isLoggedIn, async (req, res) => { + req.params.schacc = req.user.schacc + await controllers.getOne(req, res) + }) + .put(isLoggedIn, async (req, res) => { + req.params.schacc = req.user.schacc + await controllers.updateOne(req, res) + }) exports.default = router diff --git a/src/routers.js b/src/routers.js index 3f2d01c8bd17d30d2f8c82959b28114069afa1e7..40d3d9832b1d74680481f04ab579874d2fa1387e 100644 --- a/src/routers.js +++ b/src/routers.js @@ -9,7 +9,6 @@ const commentRouter = require('./resources/comment/commentRouter') const attendanceRouter = require('./resources/attendance/attendanceRouter') const taskRouter = require('./resources/task/taskRouter') const settingsRouter = require('./resources/settings/settingsRouter') -const extraRouter = require('./resources/extra/extraRouter') exports.routers = { userRouter: userRouter.default, @@ -23,5 +22,4 @@ exports.routers = { attendanceRouter: attendanceRouter.default, taskRouter: taskRouter.default, settingsRouter: settingsRouter.default, - extraRouter: extraRouter.default, } diff --git a/src/server.js b/src/server.js index c315bc67bffecfb26edc4e2d534ae62da2e83787..37d8e4c37db5520faa7dec3db517d959cf0680eb 100644 --- a/src/server.js +++ b/src/server.js @@ -6,6 +6,8 @@ const cors = require('cors') const morgan = require('morgan') const passport = require('passport') const cookieSession = require('cookie-session') +const swaggerJsdoc = require('swagger-jsdoc') +const swaggerUi = require('swagger-ui-express') require('dotenv').config() @@ -44,7 +46,7 @@ app.use( app.use(passport.initialize()) app.use(passport.session()) -if (!config.default.isTest) { +if (!config.default.isTest || config.default.isDev) { require('./utils/oauthSetup') app.get( '/api/v1/login/authsch', @@ -96,6 +98,49 @@ app.get('/api/v1/logout', (req, res) => { res.redirect('/') }) +// Swagger +const options = { + swaggerDefinition: { + openapi: '3.0.0', + info: { + title: 'Kszképzés Express Backend', + version: '1.0.0', + description: 'Backendje a képzést elősegítő weboldalnak', + license: { + name: 'MIT', + url: 'https://choosealicense.com/licenses/mit/', + }, + contact: { + name: 'Swagger', + url: 'https://swagger.io', + email: 'Info@SmartBear.com', + }, + }, + servers: [ + { + url: 'http://127.0.0.1:3000/api/v1', + url: 'https://ujonc.dev.donald.sch.bme.hu/api/v1', + url: 'https://ujonc.donald.sch.bme.hu/api/v1', + }, + ], + }, + apis: ['./src/resources/**/*.yml'], + securityDefinitions: { + auth: { + type: 'basic', + }, + }, + security: [{ auth: [] }], +} +const specs = swaggerJsdoc(options) +app.use('/api/v1/swagger', swaggerUi.serve) +app.get( + '/api/v1/swagger', + swaggerUi.setup(specs, { + explorer: true, + }) +) + // API Routes app.use('/api/v1/user', routers.routers.userRouter) app.use('/api/v1/mentor', routers.routers.mentorRouter) @@ -108,6 +153,5 @@ app.use('/api/v1/attendance', routers.routers.attendanceRouter) app.use('/api/v1/task', routers.routers.taskRouter) app.use('/api/v1/activity', routers.routers.activityRouter) app.use('/api/v1/settings', routers.routers.settingsRouter) -app.use('/api/v1/extra', routers.routers.extraRouter) exports.app = app