├── app ├── README.md ├── .nvmrc ├── api │ ├── forest │ │ └── .gitkeep │ ├── status │ │ ├── rule.js │ │ ├── func.js │ │ ├── route.js │ │ └── spec.js │ ├── subjects │ │ ├── clear │ │ │ ├── func.js │ │ │ └── route.js │ │ ├── list │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── search │ │ │ ├── route.js │ │ │ └── func.js │ │ └── rest.js │ ├── teachers │ │ ├── clear │ │ │ ├── func.js │ │ │ └── route.js │ │ ├── list │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── search │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── create │ │ │ ├── route.js │ │ │ ├── func.js │ │ │ └── spec.js │ │ └── teacherId │ │ │ └── update │ │ │ ├── route.js │ │ │ └── func.js │ ├── facebook │ │ ├── route.js │ │ └── func.js │ ├── comment │ │ ├── create │ │ │ ├── route.js │ │ │ ├── func.js │ │ │ └── spec.js │ │ ├── missing │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── commentId │ │ │ ├── update │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ └── delete │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ ├── teacher │ │ │ └── teacherId │ │ │ │ └── view │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ └── Fields.js │ ├── graduations │ │ ├── list │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── stats │ │ │ └── route.js │ │ └── rest.js │ ├── stats │ │ ├── grades │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── usage │ │ │ ├── route.js │ │ │ └── func.js │ │ └── disciplinas │ │ │ ├── student │ │ │ ├── route.js │ │ │ └── func.js │ │ │ └── route.js │ ├── students │ │ ├── create │ │ │ ├── route.js │ │ │ └── spec.js │ │ └── view │ │ │ ├── route.js │ │ │ └── func.js │ ├── users │ │ ├── info │ │ │ ├── route.js │ │ │ ├── func.js │ │ │ └── spec.js │ │ ├── me │ │ │ ├── grades │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ ├── delete │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ ├── recover │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ ├── resend │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ ├── devices │ │ │ │ ├── create │ │ │ │ │ ├── route.js │ │ │ │ │ └── func.js │ │ │ │ └── delete │ │ │ │ │ ├── route.js │ │ │ │ │ └── func.js │ │ │ ├── relationships │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ └── confirm │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ ├── complete │ │ │ ├── route.js │ │ │ ├── rule.js │ │ │ └── func.js │ │ ├── fields.js │ │ └── rest.js │ ├── disciplinas │ │ ├── list │ │ │ ├── route.js │ │ │ ├── func.js │ │ │ └── spec.js │ │ ├── clear │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── sync │ │ │ ├── route.js │ │ │ ├── spec.js │ │ │ └── func.js │ │ ├── teachers │ │ │ ├── route.js │ │ │ ├── rule.js │ │ │ ├── func.js │ │ │ └── spec.js │ │ └── disciplinaId │ │ │ └── kicks │ │ │ ├── route.js │ │ │ ├── rule.js │ │ │ └── spec.js │ ├── histories │ │ ├── create │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── route.js │ │ ├── courses │ │ │ ├── list │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ │ └── clear │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ └── func.js │ ├── enrollments │ │ ├── student │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── sync │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── update │ │ │ ├── route.js │ │ │ └── func.js │ │ ├── enrollmentId │ │ │ └── view │ │ │ │ ├── route.js │ │ │ │ └── func.js │ │ └── create │ │ │ ├── route.js │ │ │ ├── rule.js │ │ │ ├── func.js │ │ │ └── spec.js │ ├── groups │ │ └── index │ │ │ ├── route.js │ │ │ └── func.js │ ├── reactions │ │ ├── create │ │ │ ├── route.js │ │ │ └── func.js │ │ └── reactionId │ │ │ └── delete │ │ │ ├── route.js │ │ │ └── func.js │ ├── matriculas │ │ └── sync │ │ │ ├── route.js │ │ │ ├── rule.js │ │ │ └── func.js │ ├── reviews │ │ ├── teachers │ │ │ └── route.js │ │ └── subjects │ │ │ └── route.js │ ├── subjectGraduations │ │ └── rest.js │ └── historiesGraduations │ │ └── rest.js ├── helpers │ ├── sleep.js │ ├── parse │ │ ├── toNumber.js │ │ ├── var2json.js │ │ ├── slugify.spec.js │ │ ├── mongoJson.js │ │ ├── toNumber.spec.js │ │ ├── pickles.js │ │ ├── error.js │ │ ├── slugify.js │ │ ├── error.spec.js │ │ ├── pickFields.js │ │ └── pickFields.spec.js │ ├── season │ │ ├── findSeasonKey.js │ │ ├── findLastSeason.js │ │ ├── findSeason.js │ │ ├── findSeasonKey.spec.js │ │ ├── findSeason.spec.js │ │ ├── findIdeais.js │ │ └── findIdeais.spec.js │ ├── transform │ │ ├── clearString.js │ │ ├── transformMatriculas.js │ │ ├── pdfDisciplinas.spec.js │ │ ├── identifier.js │ │ ├── resolveProfessor.js │ │ ├── disciplinas.spec.js │ │ └── pdfDisciplinas.js │ ├── test │ │ ├── getFixture.js │ │ ├── getMatriculas.js │ │ ├── getContagem.js │ │ ├── getDisciplinas.js │ │ └── sample.js │ ├── middlewares │ │ ├── notFound.js │ │ ├── private.js │ │ ├── cors.js │ │ ├── matricula.js │ │ ├── error.js │ │ └── auth.js │ ├── duration.js │ ├── validate │ │ ├── teachers.js │ │ ├── throwMissingParameter.js │ │ ├── subjects.js │ │ └── mongoError.js │ ├── agenda │ │ └── wrap.js │ ├── crypt │ │ ├── decrypt.js │ │ └── encrypt.js │ ├── courses │ │ ├── findIds.spec.js │ │ ├── findId.spec.js │ │ ├── findId.js │ │ └── findIds.js │ ├── rest │ │ ├── outputFn.js │ │ ├── paginate.js │ │ └── paginate.spec.js │ ├── routes │ │ ├── rule.js │ │ ├── order.js │ │ ├── func.js │ │ └── rule.spec.js │ ├── duration.spec.js │ ├── notification │ │ └── send.js │ ├── mailer │ │ ├── send.js │ │ └── send.spec.js │ └── mapLimit.js ├── populate │ └── data │ │ ├── disciplinas.js │ │ ├── users.js │ │ └── comments.js ├── redis │ └── handlers │ │ ├── exampleHandler.js │ │ ├── historySync.js │ │ └── enrollmentCreate.js ├── snapshot │ └── assets │ │ ├── image │ │ ├── audio.gif │ │ ├── help.gif │ │ ├── text.gif │ │ ├── refresh.gif │ │ └── reload ├── setup │ ├── package.js │ ├── router.js │ ├── helpers.js │ ├── lift.js │ ├── redirect.js │ ├── mongo.js │ ├── static.js │ ├── models.js │ ├── agenda.js │ ├── server.js │ ├── redis.js │ └── api.js ├── errors │ ├── BadRequest.js │ ├── Conflict.js │ ├── Forbidden.js │ ├── NotFound.js │ ├── Unauthorized.js │ ├── Unprocessable.js │ ├── BadRequest │ │ ├── InvalidToken.js │ │ ├── MissingSubject.js │ │ ├── InvalidParameter.js │ │ └── MissingParameter.js │ ├── Forbidden │ │ ├── CannotModify.js │ │ ├── CommunityMismatch.js │ │ └── MissingPermission.js │ ├── index.js │ ├── NotFound │ │ ├── PendingConfirmation.js │ │ └── PendingRegistration.js │ ├── Unauthorized │ │ ├── ExpiredToken.js │ │ └── InvalidToken.js │ ├── Unprocessable │ │ ├── LimitSizeExceeded.js │ │ └── FacebookLinkRequired.js │ └── BaseError.js ├── .env.example ├── agenda │ ├── jobs.js │ └── processors │ │ ├── enrollments │ │ ├── updateFromHistory.js │ │ ├── updateEnrollments.js │ │ └── updateStuff.js │ │ ├── matriculas │ │ └── syncMatriculas.js │ │ └── emails │ │ └── sendConfirmation.js ├── models │ ├── historiesGraduations.js │ ├── teachers.js │ ├── subjects.js │ ├── groups.js │ ├── graduation.js │ ├── subjectGraduations.js │ ├── alunos.js │ ├── histories.js │ ├── histories.spec.js │ ├── disciplinas.js │ ├── enrollments.js │ ├── users.js │ └── reactions.js ├── docker-compose.yaml ├── .gitignore ├── fixtures │ └── enrollments.json ├── app.js ├── test.js └── server.js ├── .nvmrc ├── Procfile ├── .travis.yml ├── captain-definition ├── docs └── pull_request_template.md ├── package.json ├── .eslintrc.json ├── README.md ├── .github └── workflows │ └── build.yml └── .gitignore /app/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.18.3 -------------------------------------------------------------------------------- /app/.nvmrc: -------------------------------------------------------------------------------- 1 | v14.18.3 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start -------------------------------------------------------------------------------- /app/api/forest/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/status/rule.js: -------------------------------------------------------------------------------- 1 | module.exports = async() => { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /app/helpers/sleep.js: -------------------------------------------------------------------------------- 1 | module.exports = ms => new Promise((res) => setTimeout(res, ms)) -------------------------------------------------------------------------------- /app/populate/data/disciplinas.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return [ 3 | 4 | ] 5 | } -------------------------------------------------------------------------------- /app/redis/handlers/exampleHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = async function () { 2 | // do some stuff 3 | } -------------------------------------------------------------------------------- /app/snapshot/assets/image: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufabc-next/ufabc-next-server/HEAD/app/snapshot/assets/image -------------------------------------------------------------------------------- /app/setup/package.js: -------------------------------------------------------------------------------- 1 | // Load package.json 2 | 3 | module.exports = async () => { 4 | return require('../package') 5 | } -------------------------------------------------------------------------------- /app/snapshot/assets/audio.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufabc-next/ufabc-next-server/HEAD/app/snapshot/assets/audio.gif -------------------------------------------------------------------------------- /app/snapshot/assets/help.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufabc-next/ufabc-next-server/HEAD/app/snapshot/assets/help.gif -------------------------------------------------------------------------------- /app/snapshot/assets/text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufabc-next/ufabc-next-server/HEAD/app/snapshot/assets/text.gif -------------------------------------------------------------------------------- /app/errors/BadRequest.js: -------------------------------------------------------------------------------- 1 | const BaseError = require('./BaseError') 2 | 3 | module.exports = class BadRequest extends BaseError {} -------------------------------------------------------------------------------- /app/errors/Conflict.js: -------------------------------------------------------------------------------- 1 | const BaseError = require('./BaseError') 2 | 3 | module.exports = class Conflict extends BaseError {} -------------------------------------------------------------------------------- /app/errors/Forbidden.js: -------------------------------------------------------------------------------- 1 | const BaseError = require('./BaseError') 2 | 3 | module.exports = class Forbidden extends BaseError {} -------------------------------------------------------------------------------- /app/errors/NotFound.js: -------------------------------------------------------------------------------- 1 | const BaseError = require('./BaseError') 2 | 3 | module.exports = class NotFound extends BaseError {} -------------------------------------------------------------------------------- /app/snapshot/assets/refresh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufabc-next/ufabc-next-server/HEAD/app/snapshot/assets/refresh.gif -------------------------------------------------------------------------------- /app/errors/Unauthorized.js: -------------------------------------------------------------------------------- 1 | const BaseError = require('./BaseError') 2 | 3 | module.exports = class Unauthorized extends BaseError {} -------------------------------------------------------------------------------- /app/setup/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | module.exports = async () => { 4 | return express.Router() 5 | } -------------------------------------------------------------------------------- /app/errors/Unprocessable.js: -------------------------------------------------------------------------------- 1 | const BaseError = require('./BaseError') 2 | 3 | module.exports = class Unprocessable extends BaseError {} -------------------------------------------------------------------------------- /app/errors/BadRequest/InvalidToken.js: -------------------------------------------------------------------------------- 1 | const BadRequest = require('../BadRequest') 2 | 3 | module.exports = class InvalidToken extends BadRequest {} -------------------------------------------------------------------------------- /app/errors/Forbidden/CannotModify.js: -------------------------------------------------------------------------------- 1 | const Forbidden = require('../Forbidden') 2 | 3 | module.exports = class CannotModify extends Forbidden {} 4 | -------------------------------------------------------------------------------- /app/errors/index.js: -------------------------------------------------------------------------------- 1 | const requireSmart = require('require-smart') 2 | 3 | module.exports = requireSmart('./', {skip: [/spec\.js$/, /index\.js/]}) -------------------------------------------------------------------------------- /app/errors/BadRequest/MissingSubject.js: -------------------------------------------------------------------------------- 1 | const BadRequest = require('../BadRequest') 2 | 3 | module.exports = class MissingSubject extends BadRequest {} -------------------------------------------------------------------------------- /app/errors/Forbidden/CommunityMismatch.js: -------------------------------------------------------------------------------- 1 | const Forbidden = require('../Forbidden') 2 | 3 | module.exports = class CommunityMismatch extends Forbidden {} -------------------------------------------------------------------------------- /app/errors/NotFound/PendingConfirmation.js: -------------------------------------------------------------------------------- 1 | const NotFound = require('../NotFound') 2 | 3 | module.exports = class PendingConfirmation extends NotFound {} -------------------------------------------------------------------------------- /app/errors/NotFound/PendingRegistration.js: -------------------------------------------------------------------------------- 1 | const NotFound = require('../NotFound') 2 | 3 | module.exports = class PendingRegistration extends NotFound {} -------------------------------------------------------------------------------- /app/api/subjects/clear/func.js: -------------------------------------------------------------------------------- 1 | const cachegoose = require('cachegoose') 2 | 3 | module.exports = function () { 4 | cachegoose.clearCache('subjects') 5 | } -------------------------------------------------------------------------------- /app/api/teachers/clear/func.js: -------------------------------------------------------------------------------- 1 | const cachegoose = require('cachegoose') 2 | 3 | module.exports = function () { 4 | cachegoose.clearCache('teachers') 5 | } -------------------------------------------------------------------------------- /app/errors/BadRequest/InvalidParameter.js: -------------------------------------------------------------------------------- 1 | const BadRequest = require('../BadRequest') 2 | 3 | module.exports = class InvalidParameter extends BadRequest {} -------------------------------------------------------------------------------- /app/errors/BadRequest/MissingParameter.js: -------------------------------------------------------------------------------- 1 | const BadRequest = require('../BadRequest') 2 | 3 | module.exports = class MissingParameter extends BadRequest {} -------------------------------------------------------------------------------- /app/errors/Forbidden/MissingPermission.js: -------------------------------------------------------------------------------- 1 | const Forbidden = require('../Forbidden') 2 | 3 | module.exports = class MissingPermission extends Forbidden {} 4 | -------------------------------------------------------------------------------- /app/errors/Unauthorized/ExpiredToken.js: -------------------------------------------------------------------------------- 1 | const Unauthorized = require('../Unauthorized') 2 | 3 | module.exports = class ExpiredToken extends Unauthorized {} -------------------------------------------------------------------------------- /app/errors/Unauthorized/InvalidToken.js: -------------------------------------------------------------------------------- 1 | const Unauthorized = require('../Unauthorized') 2 | 3 | module.exports = class InvalidToken extends Unauthorized {} -------------------------------------------------------------------------------- /app/api/status/func.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | return { 3 | status: 'alive', 4 | now: Date.now(), 5 | hash: Math.random() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/errors/Unprocessable/LimitSizeExceeded.js: -------------------------------------------------------------------------------- 1 | const Unprocessable = require('../Unprocessable') 2 | 3 | module.exports = class LimitSizeExceeded extends Unprocessable {} -------------------------------------------------------------------------------- /app/errors/Unprocessable/FacebookLinkRequired.js: -------------------------------------------------------------------------------- 1 | const Unprocessable = require('../Unprocessable') 2 | 3 | module.exports = class FacebookLinkRequired extends Unprocessable {} -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | MONGO_URL= 2 | GRANT_SECRET= 3 | PROTOCOL= 4 | OAUTH_FACEBOOK_SECRET= 5 | OAUTH_FACEBOOK_KEY= 6 | EMAIL_TEMPLATE_CONFIMATION= 7 | SENDGRID_KEY= 8 | TOKEN_DEVELOPMENT= -------------------------------------------------------------------------------- /app/api/facebook/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.post('/facebook/sync', app.helpers.routes.func(require('./func.js'))) 5 | } 6 | -------------------------------------------------------------------------------- /app/api/comment/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/comments', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/graduations/list/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.get('/graduations', app.helpers.routes.func(require('./func.js'))) 5 | } 6 | -------------------------------------------------------------------------------- /app/api/stats/grades/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/stats/grades', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/stats/usage/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/stats/usage', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/students/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/students', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/subjects/list/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/subjects', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/teachers/list/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/teachers', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/info/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.get('/users/info', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/disciplinas/list/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/disciplinas', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/histories/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/histories', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/students/view/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/students/aluno_id', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/subjects/search/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/subjects/search', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/teachers/search/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/teachers/search', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/grades/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/users/me/grades', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/enrollments/student/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/enrollments/', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/groups/index/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/private/groups/index', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/histories/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/private/enrollments/sync', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/teachers/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/private/teachers', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/delete/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.delete('/users/me/delete', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/recover/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/users/me/recover', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/resend/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/users/me/resend', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/helpers/parse/toNumber.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | module.exports = function toNumber(str) { 4 | if(_.isNumber(str)) return str 5 | return parseFloat((str || '').replace(',', '.')) 6 | } -------------------------------------------------------------------------------- /app/helpers/season/findSeasonKey.js: -------------------------------------------------------------------------------- 1 | const findSeason = require('./findSeason') 2 | 3 | module.exports = function findSeasonKey(date) { 4 | const d = findSeason(date) 5 | return d.year + ':' + d.quad 6 | } -------------------------------------------------------------------------------- /app/api/comment/missing/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/comments/:userId/missing', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/histories/courses/list/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/histories/courses', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/reactions/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/reactions/:commentId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/subjects/clear/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/private/subjects/clear', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/teachers/clear/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/private/teachers/clear', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/comment/commentId/update/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.put('/comments/:commentId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/disciplinas/clear/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/private/disciplinas/clear', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/disciplinas/sync/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/private/disciplinas/sync', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/enrollments/sync/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/private/enrollments/sync/pdf', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/enrollments/update/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.put('/enrollments/:identifier', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/devices/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.post('/users/me/devices', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/relationships/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/users/me/relationships', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/comment/commentId/delete/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.delete('/comments/:commentId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/enrollments/enrollmentId/view/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/enrollments/:enrollmentId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/histories/courses/clear/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/private/histories/courses/clear', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/stats/disciplinas/student/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/stats/disciplinas/students', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/subjects/list/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function () { 4 | const ONE_HOUR = 60 * 60 5 | return await app.models.subjects.find({}).lean(true).cache(ONE_HOUR, 'subjects') 6 | } -------------------------------------------------------------------------------- /app/api/teachers/list/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function () { 4 | const ONE_HOUR = 60 * 60 5 | return await app.models.teachers.find({}).lean(true).cache(ONE_HOUR, 'teachers') 6 | } -------------------------------------------------------------------------------- /app/api/teachers/teacherId/update/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.put('/private/teachers/:teacherId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/users/me/devices/delete/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.delete('/users/me/devices/:deviceId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/api/graduations/stats/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.get( 5 | '/graduations/stats/:id', 6 | app.helpers.routes.func(require('./func.js')) 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /app/api/reactions/reactionId/delete/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.delete('/reactions/:commentId/:kind', 5 | app.helpers.routes.func(require('./func.js'))) 6 | } -------------------------------------------------------------------------------- /app/helpers/transform/clearString.js: -------------------------------------------------------------------------------- 1 | const removeDiacritics = require('./removeDiacritics') 2 | 3 | module.exports = function cleanString(str) { 4 | return removeDiacritics((str || '').toLowerCase()).replace(/\s+/g, ' ') 5 | } 6 | -------------------------------------------------------------------------------- /app/api/histories/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function (context) { 4 | app.agenda.now('updateStuff', { json: context.body.json || {} }) 5 | 6 | return { 7 | status: 'published' 8 | } 9 | } -------------------------------------------------------------------------------- /app/api/status/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/status', 5 | app.helpers.routes.rule(require('./rule.js')), 6 | app.helpers.routes.func(require('./func.js'))) 7 | } -------------------------------------------------------------------------------- /app/helpers/test/getFixture.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = (name) => { 5 | let p = path.join(__dirname, `../../fixtures/${name}`) 6 | return { data : fs.readFileSync(p, 'utf8')} 7 | } -------------------------------------------------------------------------------- /app/setup/helpers.js: -------------------------------------------------------------------------------- 1 | const requireSmart = require('require-smart') 2 | 3 | // Load helpers into app.helper 4 | module.exports = async() => { 5 | return module.exports = requireSmart('../helpers', { 6 | skip: [/spec\.js$/], 7 | }) 8 | } -------------------------------------------------------------------------------- /app/helpers/middlewares/notFound.js: -------------------------------------------------------------------------------- 1 | const errors = require('../../errors') 2 | 3 | module.exports = (req, res, next) => { 4 | next(new errors.NotFound('Rota não encontrada')) 5 | // next(new errors.NotFound('This route was not found')) 6 | } -------------------------------------------------------------------------------- /app/helpers/test/getMatriculas.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = () => { 5 | let p = path.join(__dirname, '../../fixtures/matriculas.js') 6 | return { data : fs.readFileSync(p, 'utf8')} 7 | } -------------------------------------------------------------------------------- /app/setup/lift.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Start listening on specified port 3 | */ 4 | module.exports = async (app) => { 5 | // Bind server to port 6 | app.server.set('port', app.config.PORT) 7 | await app.server.listen(app.server.get('port')) 8 | } 9 | -------------------------------------------------------------------------------- /app/helpers/test/getContagem.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = () => { 5 | let p = path.join(__dirname, '../../fixtures/contagemMatriculas.js') 6 | return { data : fs.readFileSync(p, 'utf8')} 7 | } -------------------------------------------------------------------------------- /app/api/enrollments/create/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/enrollments', 5 | app.helpers.routes.rule(require('./rule.js')), 6 | app.helpers.routes.func(require('./func.js'))) 7 | } -------------------------------------------------------------------------------- /app/api/matriculas/sync/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/matriculas/sync', 5 | app.helpers.routes.rule(require('./rule.js')), 6 | app.helpers.routes.func(require('./func.js'))) 7 | } -------------------------------------------------------------------------------- /app/helpers/duration.js: -------------------------------------------------------------------------------- 1 | module.exports = ms => { 2 | if (ms < 1000) 3 | return ms + ' ms' 4 | 5 | ms = Math.round(ms / 1000) 6 | if (ms < 60) 7 | return ms + ' s' 8 | 9 | ms = Math.round(ms / 60) 10 | return ms + ' min' 11 | } -------------------------------------------------------------------------------- /app/helpers/parse/var2json.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | module.exports = (payload, max) => { 4 | let json = JSON.parse(_.get(new RegExp(/^\w*=(.*);/).exec(payload), '[1]', {})) 5 | 6 | if(max) return json.slice(0, max) 7 | return json 8 | } -------------------------------------------------------------------------------- /app/helpers/test/getDisciplinas.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = () => { 5 | let p = path.join(__dirname, '../../fixtures/todasDisciplinas.js') 6 | return { data : fs.readFileSync(p, 'utf8')} 7 | } -------------------------------------------------------------------------------- /app/api/users/complete/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async (router) => { 4 | router.put('/users/complete', 5 | // app.helpers.routes.rule(require('./rule.js')), 6 | app.helpers.routes.func(require('./func.js'))) 7 | } -------------------------------------------------------------------------------- /app/api/disciplinas/clear/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function () { 4 | const season = app.helpers.season.findSeasonKey() 5 | 6 | const cacheKey = `todasDisciplinas_${season}` 7 | await app.redis.cache.del(cacheKey) 8 | } -------------------------------------------------------------------------------- /app/api/disciplinas/teachers/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.put('/disciplinas/teachers', 5 | app.helpers.routes.rule(require('./rule.js')), 6 | app.helpers.routes.func(require('./func.js'))) 7 | } -------------------------------------------------------------------------------- /app/api/matriculas/sync/rule.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async(context) => { 5 | if(context.query.access_key != app.config.ACCESS_KEY) { 6 | throw new errors.Forbidden('ACCESS_KEY') 7 | } 8 | } -------------------------------------------------------------------------------- /app/api/enrollments/create/rule.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async(context) => { 5 | if(context.query.access_key != app.config.ACCESS_KEY) { 6 | throw new errors.Forbidden('ACCESS_KEY') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/api/disciplinas/teachers/rule.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async(context) => { 5 | if(context.query.access_key != app.config.ACCESS_KEY) { 6 | throw new errors.Forbidden('ACCESS_KEY') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/api/status/spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const func = require('./func') 3 | 4 | describe('GET /v1/status', function() { 5 | it('give back status', async function () { 6 | let resp = await func({}) 7 | assert.equal(resp.status, 'alive') 8 | }) 9 | }) -------------------------------------------------------------------------------- /app/helpers/validate/teachers.js: -------------------------------------------------------------------------------- 1 | module.exports = function (disciplinas) { 2 | return disciplinas.reduce((acc, d) => { 3 | if(d.teoria && d.teoria.error) acc.push(d.teoria.error) 4 | if(d.pratica && d.pratica.error) acc.push(d.pratica.error) 5 | return acc 6 | }, []) 7 | } -------------------------------------------------------------------------------- /app/api/disciplinas/disciplinaId/kicks/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/disciplinas/:disciplinaId/kicks', 5 | app.helpers.routes.rule(require('./rule.js')), 6 | app.helpers.routes.func(require('./func.js'))) 7 | } -------------------------------------------------------------------------------- /app/api/teachers/create/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const cachegoose = require('cachegoose') 3 | 4 | module.exports = async function (context) { 5 | const teacher = await app.models.teachers.create(context.body) 6 | cachegoose.clearCache('teachers') 7 | 8 | return teacher 9 | } -------------------------------------------------------------------------------- /app/agenda/jobs.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_OPTIONS = { 2 | timezone: 'America/Sao_Paulo', 3 | } 4 | 5 | module.exports = function (agenda) { 6 | agenda.on('ready', async function() { 7 | agenda.every('2 minutes', 'syncMatriculas', {}, DEFAULT_OPTIONS) 8 | agenda.start() 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /app/api/histories/courses/list/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function clearCacheKey(context) { 4 | let { season } = context.query 5 | 6 | if(!season) season = app.helpers.season.findSeasonKey() 7 | 8 | return await app.helpers.courses.findIds(season) 9 | } -------------------------------------------------------------------------------- /app/api/users/me/confirm/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.post('/users/me/confirm', 5 | app.helpers.routes.func(require('./func.js'))) 6 | 7 | router.post('/account/confirm', 8 | app.helpers.routes.func(require('./func.js'))) 9 | } -------------------------------------------------------------------------------- /app/api/stats/disciplinas/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/stats/disciplinas/:action', 5 | app.helpers.routes.func(require('./func.js'))) 6 | 7 | router.get('/stats/disciplinas', 8 | app.helpers.routes.func(require('./func.js'))) 9 | } -------------------------------------------------------------------------------- /app/api/reviews/teachers/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/help/teachers/:teacherId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | 7 | router.get('/reviews/teachers/:teacherId', 8 | app.helpers.routes.func(require('./func.js'))) 9 | } -------------------------------------------------------------------------------- /app/helpers/season/findLastSeason.js: -------------------------------------------------------------------------------- 1 | const findSeason = require('./findSeason') 2 | 3 | module.exports = function findLastSeason(date) { 4 | const d = findSeason(date) 5 | 6 | if(d.quad === 1) { 7 | return { year: (d.year - 1), quad: 3 } 8 | } 9 | return { year: d.year, quad: d.quad - 1 } 10 | } -------------------------------------------------------------------------------- /app/api/reviews/subjects/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/help/subjects/:subjectId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | 7 | router.get('/reviews/subjects/:subjectId', 8 | app.helpers.routes.func(require('./func.js'))) 9 | } -------------------------------------------------------------------------------- /app/api/users/complete/rule.js: -------------------------------------------------------------------------------- 1 | const errors = require('@/errors') 2 | 3 | module.exports = async (context) => { 4 | let permissions = context.user.permissions 5 | 6 | if (permissions.includes('user:write')) { 7 | return 8 | } 9 | 10 | throw new errors.Forbidden.MissingPermission('user:write') 11 | } -------------------------------------------------------------------------------- /app/api/comment/teacher/teacherId/view/route.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async(router) => { 4 | router.get('/comments/:teacherId', 5 | app.helpers.routes.func(require('./func.js'))) 6 | 7 | router.get('/comments/:teacherId/:subjectId', 8 | app.helpers.routes.func(require('./func.js'))) 9 | } -------------------------------------------------------------------------------- /app/helpers/agenda/wrap.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/node') 2 | 3 | module. exports = (fn) => { 4 | return async (job, done) => { 5 | try { 6 | job.attrs.result = await fn(job.attrs.data) 7 | done() 8 | } catch (e) { 9 | Sentry.captureException(e) 10 | done(e) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /app/snapshot/assets/reload: -------------------------------------------------------------------------------- 1 | Recaptcha.finish_reload('03AHJ_VusKx8iUI13Iz9SXG3-9H5fRljvq11MOThSvuikgmpCMPNe2w5cHds9-cOfGRoYG492ewupqmqhDQ80MqxiDL9Reys7-4qL-Uj1JGD_wZqKNmloZR2hPNK-4fgrE6tWEJOguvft1l-mSR89nthE6SKzqsPLcOlsj3ewbUNa5hst7FmiOB7BUXzrQ6SeZQdZbEMM3LHohqRhLHu0EDj-1vrCMOsmXzZBIDaVMHDX06RtVVT14tEHKJ85R-rI09gLRf93yg646', 'image', null); -------------------------------------------------------------------------------- /app/helpers/middlewares/private.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | const rule = require('@/helpers/routes/rule') 5 | 6 | module.exports = rule(async (context) => { 7 | if(context.query.access_key != app.config.ACCESS_KEY) { 8 | throw new errors.Forbidden('ACCESS_KEY') 9 | } 10 | }) -------------------------------------------------------------------------------- /app/models/historiesGraduations.js: -------------------------------------------------------------------------------- 1 | const Schema = require('mongoose').Schema 2 | 3 | module.exports = Schema({ 4 | ra: Number, 5 | coefficients: Object, 6 | 7 | disciplinas: Object, 8 | 9 | curso: String, 10 | grade: String, 11 | graduation: { 12 | type: Schema.Types.ObjectId, 13 | ref: 'graduation', 14 | } 15 | }) -------------------------------------------------------------------------------- /app/populate/data/users.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return [ 3 | { 4 | confirmed: true, 5 | ra: '11201822483', 6 | email: 'felipe.tiozo@aluno.ufabc.edu.br', 7 | }, 8 | { 9 | confirmed: true, 10 | ra: '999999', 11 | email: 'development@aluno.ufabc.edu.br', 12 | }, 13 | ] 14 | } -------------------------------------------------------------------------------- /app/setup/redirect.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Redirect requests in production to https. 3 | * Block requests without host. 4 | */ 5 | module.exports = async (app) => { 6 | // If not in production, skip redirection 7 | if (app.config.ENV != 'production') 8 | return 9 | 10 | if (app.config.HOST.startsWith('localhost')) 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /app/models/teachers.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const Schema = require('mongoose').Schema 3 | 4 | var Model = module.exports = Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | alias: [String] 10 | }) 11 | 12 | Model.pre('save', async function () { 13 | this.name = _.startCase(_.camelCase(this.name)) 14 | }) -------------------------------------------------------------------------------- /app/api/users/info/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const errors = require('@/errors') 3 | const Fields = require('../fields') 4 | 5 | module.exports = async(context) => { 6 | const { user } = context 7 | 8 | if (!user) { 9 | throw new errors.NotFound('Usuário não encontrado') 10 | } 11 | 12 | return _.pick(user, Fields.public) 13 | } -------------------------------------------------------------------------------- /app/api/users/me/resend/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async(context) => { 5 | const user = await app.models.users.findOne({ _id: context.user._id, active: true }) 6 | 7 | if(!user) { 8 | throw new errors.NotFound('user') 9 | } 10 | 11 | await user.sendConfirmation() 12 | } -------------------------------------------------------------------------------- /app/api/comment/Fields.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'comment', 3 | 'teacher', 4 | 'subject', 5 | '_id', 6 | 'myReactions', 7 | 'reactionsCount', 8 | 'enrollment.creditos', 9 | 'enrollment.conceito', 10 | 'enrollment._id', 11 | 'enrollment.year', 12 | 'enrollment.quad', 13 | 'enrollment.season', 14 | 'createdAt', 15 | 'updatedAt' 16 | ] -------------------------------------------------------------------------------- /app/helpers/crypt/decrypt.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const crypto = require('crypto') 3 | 4 | const ALGORITHM = 'aes-256-ctr' 5 | 6 | module.exports = function decrypt(text){ 7 | let decipher = crypto.createDecipher(ALGORITHM, app.config.JWT_SECRET) 8 | let dec = decipher.update(text,'hex','utf8') 9 | dec += decipher.final('utf8') 10 | return dec 11 | } -------------------------------------------------------------------------------- /app/models/subjects.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const Schema = require('mongoose').Schema 3 | 4 | var Model = module.exports = Schema({ 5 | name : { 6 | type: String, 7 | required: true 8 | }, 9 | search: String, 10 | creditos: Number 11 | }) 12 | 13 | Model.pre('save', function () { 14 | this.search = _.startCase(_.camelCase(this.name)) 15 | }) -------------------------------------------------------------------------------- /app/redis/handlers/historySync.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function () { 4 | // find all histories 5 | const histories = await app.models.histories.find({}) 6 | 7 | async function syncHistories(history){ 8 | await history.updateEnrollments() 9 | } 10 | 11 | await app.helpers.mapLimit(histories, syncHistories, 5) 12 | } -------------------------------------------------------------------------------- /app/helpers/crypt/encrypt.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const crypto = require('crypto') 3 | 4 | const ALGORITHM = 'aes-256-ctr' 5 | 6 | module.exports = function encrypt(text){ 7 | let cipher = crypto.createCipher(ALGORITHM, app.config.JWT_SECRET) 8 | let crypted = cipher.update(text,'utf8','hex') 9 | crypted += cipher.final('hex') 10 | return crypted 11 | } -------------------------------------------------------------------------------- /app/helpers/validate/throwMissingParameter.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const errors = require('@/errors') 3 | 4 | module.exports = function throwMissingParameter(fields, obj) { 5 | fields = _.castArray(fields) 6 | 7 | fields.forEach(field => { 8 | let exists = field in obj 9 | if(!exists) throw new errors.BadRequest.MissingParameter(field) 10 | }) 11 | } -------------------------------------------------------------------------------- /app/api/histories/courses/clear/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const cachegoose = require('cachegoose') 3 | 4 | module.exports = async function clearCacheKey(context) { 5 | let { season } = context.query 6 | 7 | if(!season) season = app.helpers.season.findSeasonKey() 8 | 9 | const cacheKey = `cursos_ids_${season}` 10 | 11 | cachegoose.clearCache(cacheKey) 12 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10.15.3" 5 | 6 | services: 7 | - redis-server 8 | cache: 9 | directories: 10 | - node_modules 11 | - $HOME/.npm 12 | 13 | jobs: 14 | include: 15 | - stage: deploy 16 | if: branch = master 17 | install: yarn install 18 | script: yarn deploy -h https://captain.sv.ufabcnext.com -a api -p $PASSWORD -b master -------------------------------------------------------------------------------- /app/api/users/fields.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | public: [ 3 | '_id', 4 | 'oauth', 5 | 'confirmed', 6 | 'email', 7 | 'ra', 8 | 'createdAt', 9 | 'devices' 10 | ], 11 | 12 | create: [ 13 | // Permissions 14 | 'oauth', 15 | 'confirmed', 16 | ], 17 | 18 | complete: [ 19 | 'ra', 20 | ], 21 | 22 | update: [ 23 | 'ra', 24 | 'email' 25 | ], 26 | } -------------------------------------------------------------------------------- /app/helpers/transform/transformMatriculas.js: -------------------------------------------------------------------------------- 1 | module.exports = function transformMatriculas(data) { 2 | var matriculas = {} 3 | 4 | for(var aluno_id in data) { 5 | const matriculasAluno = data[aluno_id] 6 | matriculasAluno.forEach(matricula => { 7 | matriculas[matricula] = (matriculas[matricula] || []).concat([parseInt(aluno_id)]) 8 | }) 9 | } 10 | 11 | return matriculas 12 | } -------------------------------------------------------------------------------- /app/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mongo: 5 | image: mongo:latest 6 | hostname: mongo 7 | ports: 8 | - "27017:27017" 9 | 10 | redis: 11 | image: redis 12 | ports: 13 | - "6379:6379" 14 | restart: always 15 | 16 | redisui: 17 | image: marian/rebrow 18 | ports: 19 | - "5001:5001" 20 | restart: always 21 | links: 22 | - redis -------------------------------------------------------------------------------- /app/helpers/parse/slugify.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const slugify = require('./slugify') 4 | 5 | describe('slugify', function () { 6 | it('should return slug correctly', function () { 7 | assert.equal('slug-teste', slugify('SLUG TESTE')) 8 | assert.equal('slug-teste', slugify(' SLUG TESTE ')) 9 | assert.equal('slug-g-u-teste', slugify(' SLUG [Ĝ] [Ự] TESTE ')) 10 | }) 11 | }) -------------------------------------------------------------------------------- /app/api/teachers/teacherId/update/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const cachegoose = require('cachegoose') 3 | 4 | module.exports = async function (context) { 5 | const { teacherId } = context.params 6 | const { alias } = context.body 7 | 8 | const teacher = await app.models.teachers.findOneAndUpdate({ 9 | _id: teacherId 10 | }, { 11 | alias 12 | }) 13 | cachegoose.clearCache('teachers') 14 | 15 | return teacher 16 | } -------------------------------------------------------------------------------- /app/helpers/test/sample.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | 4 | module.exports = function (data, number) { 5 | let parsed = app.helpers.parse.var2json(data) 6 | 7 | if(_.isArray(parsed)) { 8 | parsed = parsed.slice(0, number) 9 | } else if(_.isObject(parsed)) { 10 | let keys = Object.keys(parsed).slice(0, number) 11 | parsed = _.pick(parsed, keys) 12 | } 13 | 14 | return 'todas=' + JSON.stringify(parsed) + ';' 15 | } -------------------------------------------------------------------------------- /app/api/users/me/delete/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async function(context){ 5 | const User = app.models.users 6 | 7 | let user = await User.findOne({ _id: context.user._id, active: true }) 8 | 9 | if(!user) throw new errors.BadRequest(`This user dont exist: ${context.user._id}`) 10 | 11 | user.active = false 12 | 13 | await user.save() 14 | 15 | return { status: 'ok', message: 'Foi bom te ter aqui =)'} 16 | } -------------------------------------------------------------------------------- /app/api/disciplinas/disciplinaId/kicks/rule.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async function (context) { 5 | const { aluno_id } = context.query 6 | 7 | const season = app.helpers.season.findSeasonKey() 8 | const Alunos = app.models.alunos.bySeason(season) 9 | 10 | const aluno = await Alunos.findOne({ aluno_id }).lean(true) 11 | 12 | if(aluno) { 13 | return 14 | } 15 | 16 | throw new errors.Forbidden('aluno_id') 17 | } -------------------------------------------------------------------------------- /app/api/students/view/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | const _ = require('lodash') 4 | 5 | module.exports = async(context) => { 6 | const { ra } = context.user 7 | 8 | if(!ra) { 9 | throw new errors.BadRequest.MissingParameter('ra') 10 | } 11 | const season = app.helpers.season.findSeasonKey() 12 | const Alunos = app.models.alunos.bySeason(season) 13 | 14 | return _.pick(await Alunos.findOne({ 15 | ra: ra, 16 | }), ['aluno_id', 'login']) 17 | } -------------------------------------------------------------------------------- /app/models/groups.js: -------------------------------------------------------------------------------- 1 | const Schema = require('mongoose').Schema 2 | 3 | const Model = module.exports = Schema({ 4 | disciplina: { 5 | type: String 6 | }, 7 | 8 | season: { 9 | type: String 10 | }, 11 | 12 | mainTeacher: { 13 | type: Schema.Types.ObjectId, 14 | ref: 'teachers', 15 | required: true 16 | }, 17 | 18 | users: [{ 19 | type: Number, 20 | required: true 21 | }] 22 | }) 23 | 24 | Model.index({ users: -1 }) 25 | Model.index({ mainTeacher: -1, season: -1, disciplina: -1 }) -------------------------------------------------------------------------------- /app/helpers/transform/pdfDisciplinas.spec.js: -------------------------------------------------------------------------------- 1 | // const _ = require('lodash') 2 | // const app = require('@/app') 3 | // const assert = require('assert') 4 | // const fs = require('fs') 5 | // const path = require('path') 6 | // const func = require('./pdfDisciplinas') 7 | 8 | describe('helpers.transform.pdfDisciplinas', function () { 9 | // let sample 10 | // let pick = ['codigo', 'disciplina', 'turma', 'campus', 'turno'] 11 | 12 | beforeEach(function () { 13 | }) 14 | 15 | it('test if parses everything correctly') 16 | }) -------------------------------------------------------------------------------- /captain-definition: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion" : 2, 3 | "dockerfileLines" :[ 4 | "FROM node:14.18.3-alpine", 5 | "RUN apk --no-cache add bash git openssh", 6 | "RUN mkdir -p /usr/src/app", 7 | "WORKDIR /usr/src/app", 8 | "COPY ./package.json /usr/src/app/", 9 | "RUN npm install && npm cache clean --force", 10 | "COPY ./ /usr/src/app", 11 | "RUN npm run deepinstall", 12 | "ENV NODE_ENV production", 13 | "ENV PORT 80", 14 | "EXPOSE 80", 15 | "CMD [ \"npm\", \"start\" ]" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /app/api/enrollments/update/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async function (context) { 5 | const { identifier } = context.params 6 | 7 | if(!identifier) { 8 | return 9 | } 10 | 11 | const keys = ['ra', 'year', 'quad', 'disciplina'] 12 | const matchIdentifier = app.helpers.transform.identifier(context.body, keys) 13 | 14 | if(identifier != matchIdentifier) { 15 | throw new errors.Forbidden('enrollment') 16 | } 17 | 18 | // TODO 19 | } 20 | 21 | -------------------------------------------------------------------------------- /app/helpers/courses/findIds.spec.js: -------------------------------------------------------------------------------- 1 | // const _ = require('lodash') 2 | // const app = require('@/app') 3 | // const assert = require('assert') 4 | // const populate = require('@/populate') 5 | 6 | const func = require('./findIds') 7 | 8 | describe('helpers.courses.findIds', function () { 9 | // let models 10 | // let pick = ['disciplina', 'ideal_quad', 'turma', 'campus', 'turno'] 11 | 12 | xit('return a list of all disciplines and ids for a seson', async function () { 13 | await func('Bacharelado em Ciências da Computação') 14 | }) 15 | }) -------------------------------------------------------------------------------- /app/agenda/processors/enrollments/updateFromHistory.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = function (agenda) { 4 | agenda.define('updateFromHistory', app.helpers.agenda.wrap(updateFromHistory)) 5 | } 6 | 7 | async function updateFromHistory () { 8 | const histories = await app.models.histories.find({}).skip(1999).limit(5) 9 | 10 | for (var [key, history] of histories.entries()) { 11 | await history.save() 12 | console.log('saved history', key) 13 | } 14 | } 15 | 16 | module.exports.updateFromHistory = updateFromHistory -------------------------------------------------------------------------------- /app/models/graduation.js: -------------------------------------------------------------------------------- 1 | const Schema = require('mongoose').Schema 2 | 3 | const Model = module.exports = Schema({ 4 | locked: { 5 | type: Boolean, 6 | default: false 7 | }, 8 | 9 | curso: String, 10 | grade: String, 11 | 12 | mandatory_credits_number: Number, 13 | limited_credits_number: Number, 14 | free_credits_number: Number, 15 | credits_total: Number, 16 | 17 | creditsBreakdown: [{ 18 | year: Number, 19 | quad: Number, 20 | choosableCredits: Number 21 | }] 22 | }) 23 | 24 | Model.index({ curso: 1, grade: 1 }) -------------------------------------------------------------------------------- /app/api/graduations/rest.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const restify = require('express-restify-mongoose') 3 | 4 | restify.serve(app.router, app.models.graduation, { 5 | prefix: '', 6 | version: '', 7 | lean: { virtuals: true }, 8 | totalCountHeader: true, 9 | runValidators: true, 10 | preRead: [app.helpers.rest.paginate], 11 | // preCreate: guard.check('users:write'), 12 | // preUpdate: guard.check('users:write'), 13 | // preDelete: guard.check('users:write'), 14 | outputFn: app.helpers.rest.outputFn, 15 | only: ['list', 'get'] 16 | }) -------------------------------------------------------------------------------- /app/errors/BaseError.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | 3 | module.exports = class BaseError extends Error { 4 | constructor () { 5 | // Format message 6 | let message = util.format(...arguments) 7 | 8 | // Calling parent constructor of base Error class. 9 | super(message) 10 | 11 | // Saving class name in the property of our custom error as a shortcut. 12 | this.name = this.constructor.name 13 | 14 | // Capturing stack trace, excluding constructor call from it. 15 | Error.captureStackTrace(this, this.constructor) 16 | } 17 | } -------------------------------------------------------------------------------- /app/agenda/processors/matriculas/syncMatriculas.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const sync = require('@/api/matriculas/sync/func') 3 | 4 | module.exports = function (agenda) { 5 | agenda.define('syncMatriculas', app.helpers.agenda.wrap(() => { 6 | agenda.now('syncMatricula') 7 | })) 8 | 9 | agenda.define('syncMatricula', app.helpers.agenda.wrap(syncMatricula)) 10 | } 11 | 12 | async function syncMatricula () { 13 | try { 14 | await sync({ query: {} }) 15 | } catch(e) { 16 | console.log(e) 17 | } 18 | } 19 | 20 | module.exports.syncMatricula = syncMatricula -------------------------------------------------------------------------------- /app/api/users/complete/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | const Fields = require('../fields') 5 | 6 | module.exports = async (context) => { 7 | const { user } = context 8 | 9 | if (!user) { 10 | throw new errors.NotFound('Usuário não encontrado') 11 | } 12 | 13 | user.set(_.pick(context.body, Fields.update)) 14 | 15 | // Save 16 | try { 17 | await user.save(context.body.email) 18 | } catch (e) { 19 | app.helpers.validate.mongoError(e) 20 | } 21 | 22 | return _.pick(user, Fields.public) 23 | } -------------------------------------------------------------------------------- /app/helpers/transform/identifier.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const crypto = require('crypto') 3 | 4 | module.exports = function(disciplina, keys, silent = true) { 5 | keys = keys || ['disciplina', 'turno', 'campus', 'turma'] 6 | 7 | let d = _(disciplina || {}) 8 | .pick(keys) 9 | .mapValues(String) 10 | .mapValues(_.camelCase) 11 | .toPairs() 12 | .sortBy(0) 13 | .fromPairs() 14 | .values() 15 | .value() 16 | .join('') 17 | 18 | if(!silent) { 19 | console.log(d) 20 | } 21 | 22 | return crypto.createHash('md5').update(d).digest('hex') 23 | } -------------------------------------------------------------------------------- /app/helpers/courses/findId.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const populate = require('@/populate') 3 | 4 | const func = require('./findId') 5 | 6 | describe('helpers.courses.findId', function () { 7 | beforeEach(async function () { 8 | await populate({ operation: 'both', only: ['alunos'] }) 9 | }) 10 | 11 | it('return the id of a course for a season', async function () { 12 | let resp = await func('Bacharlado em Ciências da Compuação') 13 | assert.equal(resp, 17) 14 | 15 | resp = await func('Bacharlado em Ciênci e tecnolo') 16 | assert.equal(resp, 20) 17 | }) 18 | }) -------------------------------------------------------------------------------- /app/api/enrollments/student/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function (context) { 4 | const { ra } = context.user 5 | 6 | if(!ra) { 7 | return [] 8 | } 9 | 10 | return await app.models.enrollments.find({ 11 | ra, 12 | conceito: { $in: ['A', 'B', 'C', 'D', 'O', 'F'] }, 13 | }, { 14 | conceito: 1, 15 | subject: 1, 16 | disciplina: 1, 17 | pratica: 1, 18 | teoria: 1, 19 | year: 1, 20 | quad: 1, 21 | creditos: 1, 22 | updatedAt: 1, 23 | comments: 1, 24 | }).populate(['pratica', 'teoria', 'subject']).lean(true) 25 | } -------------------------------------------------------------------------------- /app/api/subjects/rest.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const restify = require('express-restify-mongoose') 3 | var guard = require('express-jwt-permissions')() 4 | 5 | restify.serve(app.router, app.models.subjects, { 6 | prefix: '', 7 | version: '', 8 | lean: { virtuals: true }, 9 | totalCountHeader: true, 10 | runValidators: true, 11 | preRead: [app.helpers.rest.paginate], 12 | preCreate: guard.check([['admin'],['subjects:write']]), 13 | // preUpdate: guard.check('users:write'), 14 | // preDelete: guard.check('users:write'), 15 | outputFn: app.helpers.rest.outputFn, 16 | only: ['get', 'create'] 17 | }) -------------------------------------------------------------------------------- /app/api/users/rest.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const restify = require('express-restify-mongoose') 3 | 4 | restify.serve(app.router, app.models.users, { 5 | prefix: '', 6 | version: '', 7 | access() { 8 | return 'protected' 9 | }, 10 | writeAccess() { 11 | return 'protected' 12 | }, 13 | lean: { virtuals: true }, 14 | totalCountHeader: true, 15 | runValidators: true, 16 | preRead: [app.helpers.rest.paginate], 17 | // preCreate: guard.check('users:write'), 18 | // preUpdate: guard.check('users:write'), 19 | // preDelete: guard.check('users:write'), 20 | outputFn: app.helpers.rest.outputFn 21 | }) -------------------------------------------------------------------------------- /app/helpers/season/findSeason.js: -------------------------------------------------------------------------------- 1 | function findQuadFromDate(month) { 2 | if([0, 1, 10, 11].includes(month)) return 1 3 | if([2, 3, 4].includes(month)) return 2 4 | if([5, 6, 7, 8, 9].includes(month)) return 3 5 | } 6 | 7 | module.exports = function findSeason(date = new Date()) { 8 | const month = date.getMonth() 9 | return { 10 | 1 : { 11 | quad: 1, 12 | year: date.getFullYear() + (month < 6 ? 0 : 1) 13 | }, 14 | 2 : { 15 | quad: 2, 16 | year: date.getFullYear() 17 | }, 18 | 3 : { 19 | quad: 3, 20 | year: date.getFullYear() 21 | }, 22 | }[findQuadFromDate(date.getMonth() || month)] 23 | } -------------------------------------------------------------------------------- /app/redis/handlers/enrollmentCreate.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function (payload) { 4 | const { season, enrollments } = payload 5 | 6 | const Enrollments = app.models.enrollments.bySeason(season) 7 | 8 | async function createEnrollments(enrollment){ 9 | return await Enrollments.findOneAndUpdate({ 10 | identifier: enrollment.identifier, 11 | quad: parseInt(enrollment.quad), 12 | year: parseInt(enrollment.year), 13 | ra: parseInt(enrollment.ra), 14 | }, enrollment, { 15 | upsert: true 16 | }) 17 | } 18 | 19 | await app.helpers.mapLimit(enrollments, createEnrollments, 10) 20 | } -------------------------------------------------------------------------------- /app/helpers/rest/outputFn.js: -------------------------------------------------------------------------------- 1 | module.exports = function outputFn (req, res) { 2 | // if we are in a LIST route, we should use out pagination pattern 3 | if('totalCount' in req.erm) { 4 | req.erm.result = { 5 | docs: req.erm.result, 6 | total: req.erm.totalCount, 7 | page: req.query.page, 8 | limit: req.query.limit, 9 | pages: Math.ceil(req.erm.totalCount / req.query.limit) || 1 10 | } 11 | } 12 | 13 | // this is the default outputFn as in express-restify-mongoose 14 | if (req.erm.result) { 15 | res.status(req.erm.statusCode).json(req.erm.result) 16 | } else { 17 | res.sendStatus(req.erm.statusCode) 18 | } 19 | } -------------------------------------------------------------------------------- /app/api/subjectGraduations/rest.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const restify = require('express-restify-mongoose') 3 | var guard = require('express-jwt-permissions')() 4 | 5 | restify.serve(app.router, app.models.subjectGraduations, { 6 | prefix: '', 7 | version: '', 8 | lean: { virtuals: true }, 9 | totalCountHeader: true, 10 | runValidators: true, 11 | preRead: [app.helpers.rest.paginate], 12 | preCreate: guard.check([['admin'],['subjectsGraduations:write']]), 13 | preUpdate: guard.check([['admin'],['subjectsGraduations:write']]), 14 | preDelete: guard.check([['admin'],['subjectsGraduations:write']]), 15 | outputFn: app.helpers.rest.outputFn 16 | }) -------------------------------------------------------------------------------- /app/helpers/routes/rule.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Converts an async function to a resolvable wrapper for express middlewares 3 | * 4 | * Example: 5 | * async function fn (context) { 6 | * if((await somePromisse).someKey) 7 | * return true 8 | * 9 | * return false 10 | * } 11 | * 12 | */ 13 | module.exports = (rule) => { 14 | return async (req, res, next) => { 15 | // Context that will be passed to rule 16 | let context = req 17 | 18 | try { 19 | // Compute rule and check the permission 20 | await rule(context) 21 | 22 | // Proceed with the request 23 | next() 24 | } catch(e) { 25 | next(e) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/api/reactions/reactionId/delete/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async (context) => { 5 | const Reaction = app.models.reactions 6 | 7 | const { user } = context 8 | const { commentId, kind } = context.params 9 | 10 | app.helpers.validate.throwMissingParameter(['kind', 'commentId'], context.params) 11 | 12 | let reaction = await Reaction.findOne({ 13 | comment: String(commentId), 14 | kind: kind, 15 | user: user._id, 16 | active: true 17 | }) 18 | 19 | if(!reaction) throw new errors.BadRequest(`Reação não encontrada no comentário: ${commentId}`) 20 | await reaction.remove() 21 | } -------------------------------------------------------------------------------- /app/helpers/duration.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const duration = require('./duration') 4 | 5 | describe('duration', function () { 6 | it('should return miliseconds duration', function () { 7 | const miliSecondsDuration = 999 8 | assert.equal(miliSecondsDuration + ' ms', duration(miliSecondsDuration)) 9 | }) 10 | 11 | it('should return seconds duration', function () { 12 | const secondsDuration = 59000 13 | assert.equal('59 s', duration(secondsDuration)) 14 | }) 15 | 16 | it('should return minutes duration', function () { 17 | const minutesDuration = 180000 18 | assert.equal('3 min', duration(minutesDuration)) 19 | }) 20 | }) -------------------------------------------------------------------------------- /app/api/users/me/devices/delete/func.js: -------------------------------------------------------------------------------- 1 | const errors = require('@/errors') 2 | 3 | module.exports = async function(context) { 4 | const { user } = context 5 | const { deviceId } = context.params 6 | 7 | if(!deviceId) { 8 | throw new errors.BadRequest.MissingParameter('deviceId') 9 | } 10 | 11 | if (!user) { 12 | throw new errors.NotFound('Usuário não encontrado') 13 | } 14 | 15 | const isValidDevice = user.devices.find(device => device.deviceId == deviceId) 16 | 17 | if(!isValidDevice) { 18 | throw new errors.BadRequest(`Invalid deviceId: ${deviceId}`) 19 | } 20 | 21 | user.removeDevice(deviceId) 22 | 23 | await user.save() 24 | 25 | return user.devices 26 | } -------------------------------------------------------------------------------- /app/helpers/courses/findId.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const difflib = require('difflib') 4 | 5 | module.exports = async function findId(course, season) { 6 | const clearString = app.helpers.transform.clearString 7 | let courses = await app.helpers.courses.findIds(season) 8 | 9 | course = clearString(course) 10 | let courseNames = _.map(courses, p => clearString(p.name)) 11 | let courseMap = new Map([...courses.map(h => [clearString(h.name), h])]) 12 | let closestMatch = _.get(difflib.getCloseMatches(course, courseNames), '[0]', null) 13 | 14 | if(!closestMatch) return null 15 | return _.get(courseMap.get(closestMatch), 'curso_id', null) 16 | } -------------------------------------------------------------------------------- /app/helpers/notification/send.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const Axios = require('axios') 3 | 4 | module.exports = async function(title, body, devicesTokens) { 5 | const ENDPOINT = 'https://fcm.googleapis.com/fcm/send' 6 | 7 | const headers = { 8 | 'Content-Type' : 'application/json', 9 | 'Authorization' : app.config.GOOGLE_FCM_KEY 10 | } 11 | 12 | const payload = { 13 | 'registration_ids': devicesTokens, 14 | 'priority' : 'high', 15 | 'notification':{ 16 | 'title': title, 17 | 'body': body, 18 | 'sound': 'default', 19 | 'click_action':'FCM_PLUGIN_ACTIVITY' 20 | } 21 | } 22 | 23 | return (await Axios.post(ENDPOINT, payload, { headers })).data 24 | } -------------------------------------------------------------------------------- /app/api/comment/commentId/delete/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | const Fields = require('@/api/comment/Fields') 4 | const pickFields = require('@/helpers/parse/pickFields') 5 | 6 | module.exports = async (context) => { 7 | const Comment = app.models.comments 8 | 9 | const { commentId } = context.params 10 | 11 | app.helpers.validate.throwMissingParameter(['commentId'], context.params) 12 | 13 | let comment = await Comment.findOne({ _id: String(commentId), active: true }) 14 | 15 | if(!comment) throw new errors.BadRequest(`Comentário não encontrado: ${commentId}`) 16 | 17 | comment.active = false 18 | 19 | return pickFields(await comment.save(), Fields) 20 | } -------------------------------------------------------------------------------- /docs/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Feature | Change Request | Bug 2 | 3 | ### Description 4 | Describe what your PR is doing 5 | If you did UI work, please consider including a screenshot 6 | 7 | ### How do I test this? 8 | Please describe the tests that you ran to verify your changes. 9 | Provide instructions so we can reproduce. 10 | Please also list any relevant details for your test configuration. 11 | 12 | 1. Test A 13 | 2. Test B 14 | 3. Test C 15 | 16 | ### Checklist 17 | 18 | - [ ] I have performed a self-review of my own code; 19 | - [ ] I have added tests that prove my fix is effective or that my feature works; 20 | - [ ] Add labels to distinguish the pull request. For example `bug`, `ready to review` etc. 21 | -------------------------------------------------------------------------------- /app/helpers/parse/mongoJson.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const mongoose = require('mongoose') 3 | 4 | module.exports = function mongoJson (models) { 5 | if(!_.isArray(models)) models = [models] 6 | 7 | models = models.map(model => { 8 | if(_.isObject(model) && !mongoose.Types.ObjectId.isValid(model)) { 9 | if(model.toJSON) model = model.toJSON() 10 | 11 | Object.keys(model).forEach(key => { 12 | if(mongoose.Types.ObjectId.isValid(model[key])) { 13 | model[key] = String(model[key]) 14 | } 15 | }) 16 | } 17 | else if(mongoose.Types.ObjectId.isValid(model)) { 18 | return model.toString() 19 | } 20 | 21 | return model 22 | }) 23 | 24 | return models 25 | } -------------------------------------------------------------------------------- /app/api/comment/commentId/update/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | const Fields = require('@/api/comment/Fields') 4 | const pickFields = require('@/helpers/parse/pickFields') 5 | 6 | module.exports = async (context) => { 7 | const Comment = app.models.comments 8 | 9 | const { commentId } = context.params 10 | 11 | app.helpers.validate.throwMissingParameter(['commentId'], context.params) 12 | 13 | let comment = await Comment.findOne({ _id: String(commentId), active: true }) 14 | 15 | if(!comment) throw new errors.BadRequest(`Comentário não encontrado: ${commentId}`) 16 | 17 | comment.comment = context.body.comment 18 | 19 | return pickFields(await comment.save(), Fields) 20 | } -------------------------------------------------------------------------------- /app/api/users/me/confirm/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async(context) => { 5 | let { token } = context.body 6 | 7 | 8 | if(!token) { 9 | throw new errors.BadRequest.MissingParameter('token') 10 | } 11 | 12 | let payload = null 13 | 14 | try { 15 | payload = JSON.parse(app.helpers.crypt.decrypt(token)) 16 | } catch(e) { 17 | throw new errors.BadRequest('Token inválido') 18 | } 19 | 20 | let user = await app.models.users.findOne({ email : payload.email, active: true }) 21 | 22 | if(!user) { 23 | throw new errors.NotFound('user') 24 | } 25 | 26 | user.confirmed = true 27 | await user.save() 28 | 29 | return { token: user.generateJWT() } 30 | } 31 | -------------------------------------------------------------------------------- /app/helpers/middlewares/cors.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | 3 | res.header( 4 | 'Access-Control-Allow-Origin', 5 | '*' 6 | ) 7 | 8 | res.header( 9 | 'Access-Control-Allow-Methods', 10 | 'GET,PUT,POST,DELETE,OPTIONS' 11 | ) 12 | 13 | res.header( 14 | 'Access-Control-Allow-Headers', 15 | 'Content-Type, Authorization, Content-Length, X-Requested-With, X-Community-Id, Community-Id' 16 | ) 17 | 18 | res.header( 19 | 'Access-Control-Allow-Credentials', 20 | 'true' 21 | ) 22 | 23 | res.header( 24 | 'Access-Control-Max-Age', 25 | '600' 26 | ) 27 | 28 | // Send headers just after OPTIONS request 29 | if (req.method === 'OPTIONS') { 30 | return res.sendStatus(200) 31 | } 32 | 33 | next() 34 | } -------------------------------------------------------------------------------- /app/helpers/season/findSeasonKey.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const func = require('./findSeasonKey') 3 | 4 | describe('HELPERS findSeasonKey', function() { 5 | it('works on the end of year', async function () { 6 | let resp = await func(new Date('2018-12-12')) 7 | assert.equal(resp, '2019:1') 8 | }) 9 | 10 | it('works on start of year', async function () { 11 | let resp = await func(new Date('2019-02-12')) 12 | assert.equal(resp, '2019:1') 13 | }) 14 | 15 | it('works on 2nd quad', async function () { 16 | let resp = await func(new Date('2018-04-12')) 17 | assert.equal(resp, '2018:2') 18 | }) 19 | 20 | it('works on 3rd quad', async function () { 21 | let resp = await func(new Date('2018-08-12')) 22 | assert.equal(resp, '2018:3') 23 | }) 24 | }) -------------------------------------------------------------------------------- /app/agenda/processors/emails/sendConfirmation.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = function (agenda) { 4 | agenda.define('sendConfirmation', app.helpers.agenda.wrap(sendConfirmation)) 5 | } 6 | 7 | async function sendConfirmation (user) { 8 | console.log('sendConfirmation', user) 9 | // create a token in order for the user to confirm 10 | const token = app.helpers.crypt.encrypt(JSON.stringify({ email: user.email })) 11 | 12 | // build email 13 | const email = { 14 | recipient: user.email, 15 | body: { 16 | url: `${app.config.WEB_URL}/confirm?token=${token}` 17 | } 18 | } 19 | 20 | // send email 21 | const TEMPLATE_ID = app.config.mailer.TEMPLATES.CONFIRMATION 22 | await app.helpers.mailer.send(email, TEMPLATE_ID) 23 | } 24 | 25 | module.exports.sendConfirmation = sendConfirmation -------------------------------------------------------------------------------- /app/api/facebook/func.js: -------------------------------------------------------------------------------- 1 | const app = require("@/app"); 2 | 3 | module.exports = async (context) => { 4 | const { ra, email } = context.body; 5 | 6 | // Find user by RA first 7 | const user = await app.models.users.findOne({ 8 | ra, 9 | $or: [{ "oauth.facebookEmail": email }, { "oauth.email": email }], 10 | 'oauth.facebook': { $exists: true } 11 | }); 12 | 13 | if (!user) { 14 | throw new Error('User does not exist'); 15 | } 16 | 17 | const userEmails = [ 18 | user.oauth.emailFacebook, 19 | user.oauth.email, 20 | ].filter(Boolean); 21 | 22 | if (!userEmails.includes(email)) { 23 | throw new Error('Email does not match the registered email for this RA'); 24 | } 25 | 26 | // Generate JWT token only if email matches 27 | return { 28 | token: user.generateJWT() 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ufabc-matricula-server", 3 | "version": "1.0.0", 4 | "description": "ufabc-matricula-server", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "install-app": "cd app && npm install --force && cd ../", 9 | "build-web": "cd web && npm install && npm run build && cd ../ rm -rf web", 10 | "start": "node app/server.js", 11 | "deepinstall": "npm run install-app", 12 | "deploy": "caprover deploy", 13 | "lint": "eslint app", 14 | "lint:fix": "eslint app --fix" 15 | }, 16 | "engines": { 17 | "node": ">= 8.0.0", 18 | "npm": ">= 3.0.0" 19 | }, 20 | "dependencies": { 21 | "caprover": "^1.2.0", 22 | "captainduckduck": "^1.0.15", 23 | "mongo-tenant": "1.5.0" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^6.8.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/models/subjectGraduations.js: -------------------------------------------------------------------------------- 1 | const Schema = require('mongoose').Schema 2 | 3 | const Model = module.exports = Schema({ 4 | /** One of: mandatory, limited, free */ 5 | category: String, 6 | /** How much confidence we have this is the right category */ 7 | confidence: String, 8 | /** One of: firstLevelMandatory, secondLevelMandatory, thirdLevelMandatory */ 9 | subcategory: String, 10 | 11 | creditos: Number, 12 | codigo: String, 13 | 14 | year: Number, 15 | quad: Number, 16 | 17 | /** Array of codes for equivalents */ 18 | equivalents: [{ 19 | type: String 20 | }], 21 | 22 | subject: { 23 | type: Schema.Types.ObjectId, 24 | ref: 'subjects' 25 | }, 26 | 27 | graduation: { 28 | type: Schema.Types.ObjectId, 29 | ref: 'graduation' 30 | } 31 | }) 32 | 33 | Model.index({ graduation: 1 }) -------------------------------------------------------------------------------- /app/populate/data/comments.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app, ids) { 2 | return [ 3 | { 4 | comment: 'Muito bom', 5 | teacher: ids.teachers[0]._id, 6 | type: 'teoria', 7 | enrollment: '000000000000000000000001', 8 | subject: ids.subjects[0]._id, 9 | ra: 11201822483, 10 | }, 11 | 12 | { 13 | comment: 'Morte à geometria analitica e vetores', 14 | type: 'teoria', 15 | enrollment: '000000000000000000000002', 16 | subject: ids.subjects[392]._id, 17 | teacher: ids.subjects[101]._id, 18 | ra: 11201822479, 19 | }, 20 | 21 | { 22 | comment: 'Só alegria', 23 | teacher: ids.teachers[0]._id, 24 | type: 'teoria', 25 | enrollment: '000000000000000000000007', 26 | subject: ids.subjects[0]._id, 27 | ra: 11201822481, 28 | }, 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /app/api/disciplinas/list/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function () { 4 | const season = app.helpers.season.findSeasonKey() 5 | const Disciplinas = app.models.disciplinas.bySeason(season) 6 | 7 | const cacheKey = `todasDisciplinas_${season}` 8 | 9 | let cached = await app.redis.cache.get(cacheKey) 10 | 11 | if(cached){ 12 | return cached 13 | } 14 | 15 | let disciplinas = await Disciplinas.find({}, { 16 | disciplina: 1, 17 | disciplina_id: 1, 18 | turno: 1, 19 | turma: 1, 20 | ideal_quad: 1, 21 | identifier: 1, 22 | subject: 1, 23 | 24 | vagas: 1, 25 | requisicoes: 1, 26 | teoria: 1, 27 | pratica: 1, 28 | }).populate(['teoria', 'pratica']).lean({ virtuals: true }) 29 | 30 | await app.redis.cache.set(cacheKey, disciplinas, '1d') 31 | return disciplinas 32 | } -------------------------------------------------------------------------------- /app/helpers/middlewares/matricula.js: -------------------------------------------------------------------------------- 1 | const unless = require('express-unless') 2 | 3 | const app = require('../../app') 4 | const errors = require('../../errors') 5 | 6 | 7 | // only studnts 8 | module.exports = async (req, res, next) => { 9 | // Find aluno-id header 10 | let aluno = null 11 | if ('aluno_id' in req.headers) { 12 | aluno = req.headers['aluno_id'] 13 | } else if ('x-aluno_id' in req.headers) { 14 | aluno = req.headers['x-aluno_id'] 15 | } else if (req.query && '_aluno_id' in req.query) { 16 | aluno = req.query['_aluno_id'] 17 | } else { 18 | // Bad request if no header was found 19 | let error = new errors.BadRequest('Você deve especificar o header `aluno-id`') 20 | return next(error) 21 | } 22 | 23 | req.aluno = await app.models.aluno.findOne({ aluno_id: aluno }) 24 | } 25 | 26 | 27 | module.exports.unless = unless -------------------------------------------------------------------------------- /app/api/reactions/create/func.js: -------------------------------------------------------------------------------- 1 | const errors = require('@/errors') 2 | const app = require('@/app') 3 | const _ = require('lodash') 4 | 5 | module.exports = async function(context){ 6 | const Reaction = app.models.reactions 7 | const Comment = app.models.comments 8 | 9 | const { commentId } = context.params 10 | const { user } = context 11 | 12 | app.helpers.validate.throwMissingParameter(['commentId'], context.params) 13 | app.helpers.validate.throwMissingParameter(['kind'], context.body) 14 | 15 | let comment = await Comment.findOne({ _id: String(commentId), active: true }).lean(true) 16 | if(!comment) throw new errors.BadRequest(`Comentário inválido: ${commentId}`) 17 | 18 | let reaction = await Reaction.create({ 19 | kind: context.body.kind, 20 | comment: comment, 21 | user: user 22 | }) 23 | 24 | return _.omit(reaction, 'user') 25 | } -------------------------------------------------------------------------------- /app/api/users/me/devices/create/func.js: -------------------------------------------------------------------------------- 1 | const errors = require('@/errors') 2 | 3 | const _ = require('lodash') 4 | const useragent = require('useragent') 5 | 6 | module.exports = async function(context) { 7 | const { user } = context 8 | const { deviceId, token } = context.body 9 | 10 | if(!deviceId) { 11 | throw new errors.BadRequest.MissingParameter('deviceId') 12 | } 13 | 14 | if(!token) { 15 | throw new errors.BadRequest.MissingParameter('token') 16 | } 17 | 18 | if (!user) { 19 | throw new errors.NotFound('Usuário não encontrado') 20 | } 21 | 22 | const agent = useragent.parse(_.get(context, 'headers.user-agent', '')) 23 | 24 | const newDevice = { 25 | deviceId, 26 | token, 27 | phone: agent.device.toString() 28 | } 29 | 30 | user.addDevice(newDevice) 31 | 32 | await user.save() 33 | 34 | return user.devices 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2018 10 | }, 11 | "globals": { 12 | "assertFuncThrows": true 13 | }, 14 | "rules": { 15 | "indent": [ 16 | "error", 17 | 2 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | 24 | "semi": [ 25 | "error", 26 | "never" 27 | ], 28 | 29 | "quotes": [2, "single", { "avoidEscape": true }] 30 | }, 31 | "overrides": [ 32 | { 33 | "files": [ 34 | "app.js", 35 | "test.js", 36 | "server.js" 37 | ], 38 | "rules": { 39 | "no-console": "off" 40 | } 41 | } 42 | ], 43 | "ignorePatterns": ["**/node_modules/**", "**/snapshot/**", "**/fixtures/**"] 44 | } 45 | -------------------------------------------------------------------------------- /app/models/alunos.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const Schema = require('mongoose').Schema 3 | 4 | const CursoSchema = Schema({ 5 | id_curso : Number, 6 | nome_curso : String, 7 | cp: Number, 8 | cr: Number, 9 | ind_afinidade : Number, 10 | turno : String 11 | }) 12 | 13 | const Model = module.exports = Schema({ 14 | ra: Number, 15 | login: String, 16 | aluno_id: Number, 17 | cursos: [CursoSchema], 18 | 19 | year: Number, 20 | quad: Number, 21 | 22 | quads: Number, 23 | }) 24 | 25 | function setSeason(doc) { 26 | const season = app.helpers.season.findSeason() 27 | doc.year = season.year 28 | doc.quad = season.quad 29 | } 30 | 31 | Model.pre('save', function () { 32 | if(!this.season) { 33 | setSeason(this) 34 | } 35 | }) 36 | 37 | Model.pre('findOneAndUpdate', function () { 38 | if(!this._update.season) { 39 | setSeason(this._update) 40 | } 41 | }) -------------------------------------------------------------------------------- /app/api/teachers/search/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | 4 | module.exports = async function (context) { 5 | const { q } = context.query 6 | 7 | const regex = new RegExp(escapeRegex(_.startCase(_.camelCase(q))), 'gi') 8 | 9 | const resp = await app.models.teachers.aggregate([ 10 | { $match: { name: regex } }, 11 | { $facet: 12 | { 13 | total: [ { $count: 'total' }], 14 | data: [ 15 | { $limit: 10 }, 16 | ] 17 | } 18 | }, 19 | { $addFields: 20 | { 21 | total: { $ifNull: [{ $arrayElemAt: [ '$total.total', 0 ] }, 0] }, 22 | } 23 | }, 24 | { 25 | $project: { 26 | total: 1, 27 | data: 1, 28 | } 29 | } 30 | ]) 31 | 32 | return resp[0] 33 | } 34 | 35 | function escapeRegex(text) { 36 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') 37 | } -------------------------------------------------------------------------------- /app/api/groups/index/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function() { 4 | const Groups = app.models.groups 5 | const Enrollments = app.models.enrollments 6 | 7 | const startedAt = new Date() 8 | 9 | const pipeline = [ 10 | { 11 | $match: { 12 | mainTeacher: { $ne: null }, 13 | } 14 | }, 15 | { 16 | $group: { 17 | _id: { 18 | disciplina: '$disciplina', 19 | season: '$season', 20 | mainTeacher: '$mainTeacher' 21 | }, 22 | users: { 23 | $push: '$ra' 24 | } 25 | } 26 | } 27 | ] 28 | 29 | let data = await Enrollments.aggregate(pipeline) 30 | 31 | await Groups.remove({}) 32 | 33 | await Groups.create(data.map(doc => { 34 | return { ...doc._id, users: doc.users } 35 | })) 36 | 37 | return { success: true, time: `${new Date() - startedAt} ms`} 38 | } -------------------------------------------------------------------------------- /app/api/subjects/search/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | 4 | module.exports = async function (context) { 5 | const { q } = context.query 6 | 7 | const regex = new RegExp(escapeRegex(_.startCase(_.camelCase(q))), 'gi') 8 | 9 | const resp = await app.models.subjects.aggregate([ 10 | { $match: { search: regex } }, 11 | { $facet: 12 | { 13 | total: [ { $count: 'total' }], 14 | data: [ 15 | { $limit: 10 }, 16 | ] 17 | } 18 | }, 19 | { $addFields: 20 | { 21 | total: { $ifNull: [{ $arrayElemAt: [ '$total.total', 0 ] }, 0] }, 22 | } 23 | }, 24 | { 25 | $project: { 26 | total: 1, 27 | data: 1, 28 | } 29 | } 30 | ]) 31 | 32 | return resp[0] 33 | } 34 | 35 | function escapeRegex(text) { 36 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') 37 | } -------------------------------------------------------------------------------- /app/helpers/rest/paginate.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const aqp = require('api-query-params') 3 | 4 | module.exports = function paginate(req, res, next) { 5 | // use aqp to transform query 6 | const omitOperators = ['page', 'populate', 'select', 'sort', 'skip'] 7 | req.erm.query.query = aqp(_.omit(req.query, omitOperators)).filter 8 | 9 | // enforce max limit on queries 10 | req.erm.query.limit = Math.min(req.erm.query.limit || 10, 100) 11 | req.query.limit = req.erm.query.limit 12 | 13 | // force page is not passed 14 | if(!('page' in req.query)) { 15 | req.query.page = 1 16 | } 17 | 18 | // normalize page 19 | req.query.page = parseInt(req.query.page) 20 | if(isNaN(req.query.page)){ 21 | req.query.page = 1 22 | } 23 | 24 | // calculate how much we should skip based on page and limit 25 | req.erm.query.skip = (req.query.page - 1) * req.erm.query.limit 26 | 27 | next() 28 | } -------------------------------------------------------------------------------- /app/api/stats/disciplinas/student/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function getDisciplinasByStudent(context) { 4 | let { season } = context.query 5 | 6 | if(!season) season = app.helpers.season.findSeasonKey() 7 | const Disciplinas = app.models.disciplinas.bySeason(season) 8 | 9 | // check if we are dealing with previous data or current 10 | const isPrevious = await Disciplinas.count({ before_kick: { $exists: true, $ne: [] }}) 11 | const dataKey = isPrevious ? '$before_kick' : '$alunos_matriculados' 12 | 13 | return Disciplinas.aggregate([ 14 | { $unwind: dataKey }, 15 | { $group : { _id : dataKey, count : { $sum : 1 } } }, 16 | { $group : { _id : '$count', students_number : { $sum : 1 } } }, 17 | { $sort: { _id: 1 } }, 18 | { $project: 19 | { 20 | students_number: 1, 21 | disciplines_number: '$_id' 22 | } 23 | } 24 | ]) 25 | } -------------------------------------------------------------------------------- /app/helpers/courses/findIds.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const math = require('mathjs') 4 | 5 | module.exports = async function findIds(season) { 6 | if(!season) season = app.helpers.season.findSeasonKey() 7 | 8 | const cacheKey = `cursos_ids_${season}` 9 | 10 | const Alunos = app.models.alunos.bySeason(season) 11 | 12 | let cursos = await Alunos 13 | .aggregate([ 14 | { $unwind: '$cursos' }, 15 | { $match: { 'cursos.id_curso': { $ne: null } }}, 16 | { $project: { 'cursos.id_curso': 1, 'cursos.nome_curso': { $trim: { input: '$cursos.nome_curso' } } } }, 17 | { $group: { _id: '$cursos.nome_curso', ids: { $push: '$cursos.id_curso' } } }, 18 | ]) 19 | .cache('10m', cacheKey) 20 | 21 | return cursos.map(curso => ({ 22 | name: curso._id, 23 | curso_id: _.compact(curso.ids).length ? math.mode(_.compact(curso.ids))[0] : undefined 24 | })) 25 | } -------------------------------------------------------------------------------- /app/helpers/routes/order.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const path = require('path') 3 | 4 | module.exports = (router) => { 5 | let routes = _.map(router._router.stack) 6 | 7 | routes = _.sortBy(routes, [function (r) { 8 | let path = _.get(r, 'route.path', '') 9 | return partsWeight(pathParts(path)) * -1 10 | }]) 11 | 12 | router._router.stack = routes 13 | 14 | // console.log(JSON.stringify(_.map(routes, 'route.path'), null, 2)) 15 | 16 | // router._router.stack = routes 17 | } 18 | 19 | function pathParts(str) { 20 | return path.normalize('/' + str + '/').split('/') 21 | } 22 | 23 | function partsWeight(sliced) { 24 | return sliced.reduce(function(acc, part, i) { 25 | // If is bound part 26 | if (!/^:.+$/.test(part)) { 27 | // Weight is positively correlated to indexes of bound parts 28 | acc += Math.pow(i + 1, sliced.length) 29 | } 30 | return acc 31 | }, 0) 32 | } 33 | -------------------------------------------------------------------------------- /app/api/enrollments/enrollmentId/view/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const _ = require('lodash') 3 | 4 | module.exports = async (context) => { 5 | 6 | const { enrollmentId } = context.params 7 | const { ra } = context.user 8 | 9 | if(!ra) { 10 | return {} 11 | } 12 | 13 | app.helpers.validate.throwMissingParameter(['enrollmentId'], context.params) 14 | 15 | let res = await app.models.enrollments.findOne({ _id: String(enrollmentId) }, { 16 | conceito: 1, 17 | subject: 1, 18 | disciplina: 1, 19 | pratica: 1, 20 | teoria: 1, 21 | year: 1, 22 | quad: 1, 23 | creditos: 1, 24 | updatedAt: 1, 25 | comments: 1, 26 | }).populate(['pratica', 'teoria', 'subject']).lean(true) 27 | 28 | let comment = await app.models.comments.find({ enrollment: String(enrollmentId) }) 29 | comment.map((c) => { 30 | res[c.type]['comment'] = c 31 | }) 32 | 33 | return _.omit(res, 'ra') 34 | } -------------------------------------------------------------------------------- /app/helpers/season/findSeason.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const func = require('./findSeason') 3 | 4 | describe('HELPERS findSeason', function() { 5 | it('works on the end of year', async function () { 6 | let resp = await func(new Date('2018-12-12')) 7 | assert.equal(resp.quad, 1) 8 | assert.equal(resp.year, 2019) 9 | }) 10 | 11 | it('works on start of year', async function () { 12 | let resp = await func(new Date('2019-02-12')) 13 | assert.equal(resp.quad, 1) 14 | assert.equal(resp.year, 2019) 15 | }) 16 | 17 | it('works on 2nd quad', async function () { 18 | let resp = await func(new Date('2018-04-12')) 19 | assert.equal(resp.quad, 2) 20 | assert.equal(resp.year, 2018) 21 | }) 22 | 23 | it('works on 3rd quad', async function () { 24 | let resp = await func(new Date('2018-08-12')) 25 | assert.equal(resp.quad, 3) 26 | assert.equal(resp.year, 2018) 27 | }) 28 | }) -------------------------------------------------------------------------------- /app/api/teachers/create/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const assert = require('assert') 3 | const func = require('./func') 4 | 5 | const populate = require('@/populate') 6 | const teacherList = require('@/api/teachers/list/func') 7 | 8 | describe('POST /v1/private/teachers', function() { 9 | let context 10 | 11 | beforeEach(async function () { 12 | await populate({ operation : 'remove', only: ['teachers'] }) 13 | context = { 14 | query: {}, 15 | body: { 16 | name: 'Some new teacher' 17 | } 18 | } 19 | }) 20 | 21 | it('creates a new teachers', async function () { 22 | const beforeTeachers = await teacherList() 23 | assert.equal(beforeTeachers.length, 0) 24 | 25 | await func(context) 26 | 27 | // wait to cache to be cleaned 28 | await app.helpers.sleep(50) 29 | 30 | const afterTeachers = await teacherList() 31 | assert.equal(afterTeachers.length, 1) 32 | }) 33 | }) -------------------------------------------------------------------------------- /app/helpers/transform/resolveProfessor.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const difflib = require('difflib') 3 | 4 | module.exports = function resolveProfessor(name, teachers, mappings = {}) { 5 | if (name in mappings) { 6 | return mappings[name] 7 | } 8 | 9 | name = _.startCase(_.camelCase(name)) 10 | 11 | const foundTeacher = 12 | _.find(teachers, { name: name }) || 13 | _.find(teachers, (teacher) => (teacher.alias || []).includes(name)) 14 | 15 | if(!name) return null 16 | else if(name == 'N D' || name == 'Falso') return null 17 | else if (foundTeacher) { 18 | return foundTeacher 19 | } 20 | else { 21 | let bestMatch = difflib.getCloseMatches(name, _.map(teachers, 'name'))[0] 22 | let s = new difflib.SequenceMatcher(null, bestMatch, name) 23 | if(s.ratio() > 0.8) return _.find(teachers, { name: bestMatch }) 24 | else { 25 | return { error : 'Missing Teacher: ' + name } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/api/historiesGraduations/rest.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const restify = require('express-restify-mongoose') 4 | 5 | async function addSelf(req, _res, next) { 6 | _.extend(req.query, { ra: req.user.ra }) 7 | 8 | if (Array.isArray(req.body)) { 9 | req.body.forEach(param => _.extend(param, { user: req.user.ra })) 10 | } else { 11 | _.extend(req.body, { ra: req.user.ra }) 12 | } 13 | 14 | next() 15 | } 16 | 17 | restify.serve(app.router, app.models.historiesGraduations, { 18 | prefix: '', 19 | version: '', 20 | lean: { virtuals: true }, 21 | totalCountHeader: true, 22 | runValidators: true, 23 | preRead: [app.helpers.rest.paginate], 24 | // preCreate: guard.check('users:write'), 25 | // preUpdate: guard.check('users:write'), 26 | // preDelete: guard.check('users:write'), 27 | preMiddleware: [addSelf], 28 | outputFn: app.helpers.rest.outputFn, 29 | only: ['list', 'get'] 30 | }) -------------------------------------------------------------------------------- /app/api/comment/missing/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async (context) => { 5 | const { userId } = context.params 6 | 7 | app.helpers.validate.throwMissingParameter(['userId'], context.params) 8 | 9 | const Comment = app.models.comments 10 | 11 | const Enrollment = app.models.enrollments 12 | 13 | const User = app.models.users 14 | 15 | let user = await User.findOne({ _id: userId, active: true }) 16 | 17 | if(!user) throw new errors.BadRequest(`Usuário inválido: ${userId}`) 18 | 19 | let enrollments = await Enrollment.find({ ra: user.ra }) 20 | 21 | let resp = [] 22 | 23 | let comments = (await Comment.find({ ra: user.ra }).lean(true)).map(comment => { 24 | return String(comment.enrollment) 25 | }) 26 | 27 | enrollments.map(enroll => { 28 | if(!comments.includes(enroll.id)) { 29 | resp.push(enroll) 30 | } 31 | }) 32 | 33 | return resp 34 | } -------------------------------------------------------------------------------- /app/api/comment/teacher/teacherId/view/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | const Fields = require('@/api/comment/Fields') 5 | const pickFields = require('@/helpers/parse/pickFields') 6 | 7 | module.exports = async (context) => { 8 | 9 | const { teacherId, subjectId } = context.params 10 | let { limit, page } = _.defaults(context.query, { 11 | limit: 10, 12 | page: 0, 13 | }) 14 | 15 | const userId = context.user._id 16 | 17 | const Comment = app.models.comments 18 | 19 | app.helpers.validate.throwMissingParameter(['teacherId'], context.params) 20 | 21 | if(!userId) throw new errors.BadRequest(`Missing userId: ${userId}`) 22 | 23 | let comment = await Comment.commentsByReactions({ 24 | teacher: teacherId, 25 | ...( subjectId && { subject: subjectId }) 26 | }, userId, ['enrollment', 'subject'], limit, page) 27 | 28 | return { data: pickFields(comment.data, Fields), total: comment.total } 29 | } -------------------------------------------------------------------------------- /app/setup/mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const cachegoose = require('cachegoose') 3 | const url = require('url') 4 | 5 | /* 6 | * Connects to MongoDB 7 | */ 8 | module.exports = async (app) => { 9 | // Set custom promisse library to use 10 | mongoose.Promise = global.Promise 11 | 12 | let driverOptions = { 13 | useNewUrlParser: true, 14 | } 15 | 16 | // open connection 17 | let conn = await mongoose.createConnection( 18 | app.config.MONGO_URL, 19 | driverOptions 20 | ) 21 | 22 | const parsedRedis = url.parse(app.config.REDIS_URL, false, true) 23 | 24 | const cacheOptions = { 25 | engine: 'redis', 26 | port: parsedRedis.port, 27 | host: parsedRedis.hostname, 28 | auth_pass: app.config.REDIS_PASSWORD, 29 | } 30 | 31 | if (app.config.REDIS_PASSWORD) { 32 | cacheOptions.auth_pass = app.config.REDIS_PASSWORD 33 | } 34 | 35 | // creates a cache layer 36 | cachegoose(mongoose, cacheOptions) 37 | 38 | return conn 39 | } 40 | -------------------------------------------------------------------------------- /app/helpers/validate/subjects.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | module.exports = function(payload, subjects, extraMappings = {}) { 4 | let mapping = {} 5 | 6 | _.extend(mapping, extraMappings) 7 | 8 | const mapSubjects = _.map(subjects, 'search') 9 | 10 | return _.castArray(payload).reduce((acc, d) => { 11 | const converted = _.startCase(_.camelCase(d.disciplina)) 12 | const convertedMapping = _.startCase(_.camelCase(mapping[d.disciplina])) 13 | 14 | if(!mapSubjects.includes(converted) && !mapSubjects.includes(convertedMapping) ) { 15 | acc.push(d.disciplina) 16 | } 17 | const subject = _.find(subjects, { search: converted }) 18 | const subjectMapping = _.find(subjects, { search: convertedMapping }) 19 | // set subject on discipline 20 | d.disciplina = subjectMapping ? mapping[d.disciplina] : d.disciplina 21 | d.subject = _.get(subject, '_id', null) || _.get(subjectMapping, '_id', null) 22 | return acc 23 | }, []).filter(d => d != '' && d != null) 24 | } -------------------------------------------------------------------------------- /app/api/users/me/recover/func.js: -------------------------------------------------------------------------------- 1 | const app = require("@/app"); 2 | const errors = require("@/errors"); 3 | 4 | module.exports = async function (context) { 5 | const { email } = context.body; 6 | if (!email) { 7 | throw new errors.BadRequest.MissingParameter("email"); 8 | } 9 | 10 | /* 11 | * Make sure email is a string to prevent NoSQL injections 12 | */ 13 | const Users = app.models.users; 14 | const user = await Users.findOne({ email: String(email) }).lean(true); 15 | if (!user) { 16 | throw new errors.BadRequest(`Invalid email: ${email}`); 17 | } 18 | 19 | const mailer = app.helpers.mailer; 20 | const TEMPLATE_ID = app.config.mailer.TEMPLATES.RECOVERY; 21 | const RECOVERY_URL = app.config.RECOVERY_URL; 22 | 23 | const payload = { 24 | recipient: user.email, 25 | body: { 26 | recovery_facebook: "https://api.ufabcnext.com/facebook/sync", 27 | recovery_google: `${RECOVERY_URL}/google?userId=${user._id}`, 28 | }, 29 | }; 30 | 31 | await mailer.send(payload, TEMPLATE_ID); 32 | }; 33 | -------------------------------------------------------------------------------- /app/models/histories.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const Schema = require('mongoose').Schema 4 | 5 | const Model = module.exports = Schema({ 6 | ra : Number, 7 | disciplinas: Object, 8 | coefficients: Object, 9 | 10 | curso: String, 11 | grade: String 12 | }) 13 | 14 | Model.index({ curso: 1, grade: 1 }) 15 | 16 | Model.method('updateEnrollments', async function () { 17 | app.agenda.now('updateUserEnrollments', this.toObject({ virtuals: true })) 18 | }) 19 | 20 | Model.pre('findOneAndUpdate', async function () { 21 | const update = _.pick(this._update, [ 22 | 'ra', 23 | 'disciplinas', 24 | 'curso', 25 | 'grade', 26 | 'mandatory_credits_number', 27 | 'limited_credits_number', 28 | 'free_credits_number', 29 | 'credits_total' 30 | ]) 31 | 32 | app.agenda.now('updateUserEnrollments', update) 33 | }) 34 | 35 | Model.post('save', async function () { 36 | app.config.isProduction && app.agenda.now('updateUserEnrollments', this.toObject({ virtuals: true })) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /app/helpers/parse/toNumber.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const toNumber = require('./toNumber') 4 | 5 | describe('toNumber', function () { 6 | describe('when value already is number', function () { 7 | it('should return miliseconds duration', function () { 8 | const number = 3 9 | assert.equal(number, toNumber(number)) 10 | }) 11 | }) 12 | 13 | describe('when the value is not a number', function () { 14 | it('should parse simple value correctly', function () { 15 | assert.equal(1, toNumber('1')) 16 | assert.equal(28, toNumber('28')) 17 | assert.equal(742, toNumber('742')) 18 | assert.equal(1000, toNumber('1000')) 19 | }) 20 | 21 | it('should parse comma value correctly', function () { 22 | assert.equal(1000, toNumber('1000,00')) 23 | assert.equal(6213.99, toNumber('6213,99')) 24 | }) 25 | 26 | it('should parse point value correctly', function () { 27 | assert.equal(10, toNumber('10.00')) 28 | assert.equal(767.35, toNumber('767.35')) 29 | }) 30 | }) 31 | }) -------------------------------------------------------------------------------- /app/models/histories.spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const populateData = require('@/populate/data/histories')() 3 | const populate = require('@/populate') 4 | 5 | describe('MODELS histories', function() { 6 | beforeEach(async function () { 7 | //await populate({ operation: 'remove' }) 8 | await populate({ operation: 'both', only: ['subjects'] }) 9 | }) 10 | 11 | describe('hooks', function () { 12 | describe('preSave', function () { 13 | 14 | xit('creates an enrollment or update one if does not exists', async function () { 15 | const prevEnrollments = await app.models.enrollments.count({}) 16 | const ra = populateData[0].ra 17 | 18 | await app.models.histories.findOneAndUpdate({ 19 | ra: ra 20 | }, populateData[0], { 21 | new: true, 22 | upsert: true 23 | }) 24 | 25 | const afterEnrollments = await app.models.enrollments.find({}) 26 | console.log(prevEnrollments, afterEnrollments.length, afterEnrollments[99]) 27 | }) 28 | }) 29 | }) 30 | }) -------------------------------------------------------------------------------- /app/api/comment/create/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | const Fields = require('@/api/comment/Fields') 4 | const pickFields = require('@/helpers/parse/pickFields') 5 | 6 | module.exports = async function (context) { 7 | const Comment = app.models.comments 8 | const Enrollment = app.models.enrollments 9 | 10 | app.helpers.validate.throwMissingParameter( 11 | ['enrollment', 'comment', 'type'], 12 | context.body 13 | ) 14 | 15 | let enrollment = await Enrollment.findById(String(context.body.enrollment)) 16 | 17 | if (!enrollment) 18 | throw new errors.BadRequest( 19 | `Este vínculo não existe: ${context.body.enrollment}` 20 | ) 21 | 22 | return pickFields( 23 | await Comment.create({ 24 | comment: String(context.body.comment), 25 | type: context.body.type, 26 | enrollment: enrollment.id, 27 | teacher: enrollment[context.body.type], 28 | disciplina: enrollment.disciplina, 29 | subject: enrollment.subject, 30 | ra: enrollment.ra, 31 | }), 32 | Fields 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/helpers/routes/func.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream') 2 | 3 | /* 4 | * Converts an async function to a resolvable wrapper for express middlewares 5 | * 6 | * Example: 7 | * async function fn (context) { 8 | * if((await somePromisse).someKey) 9 | * return true 10 | * 11 | * return false 12 | * } 13 | * 14 | */ 15 | module.exports = (func) => { 16 | return async (req, res, next) => { 17 | // Context that will be passed to func 18 | let context = req 19 | 20 | try { 21 | // Compute func and check the permission 22 | let result = await func(context, res) 23 | 24 | // Set status to 204 (No Content) if undefined 25 | if (result === undefined) { 26 | res.status(204) 27 | } 28 | 29 | // If response is a stream, pipe to output 30 | if (result instanceof stream.Readable) { 31 | return result.pipe(res) 32 | } 33 | 34 | // Redirect if _redirect key 35 | if (result && result._redirect) { 36 | return res.redirect(result._redirect) 37 | } 38 | 39 | // Respond the request 40 | res.send(result) 41 | } catch(e) { 42 | next(e) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/api/disciplinas/list/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const assert = require('assert') 3 | const populate = require('@/populate') 4 | const sinon = require('sinon') 5 | const Axios = require('axios') 6 | 7 | const func = require('./func') 8 | const sync = require('@/api/disciplinas/sync/func') 9 | 10 | describe('GET /v1/disciplinas', function () { 11 | beforeEach(async function () { 12 | await populate({ operation: 'both', only: ['disciplinas', 'subjects'] }) 13 | }) 14 | 15 | describe('func', function () { 16 | let axiosInstanceStub 17 | let axiosGetStub 18 | beforeEach(async function () { 19 | let file = app.helpers.test.getDisciplinas() 20 | file.data = app.helpers.test.sample(file.data, 100) 21 | axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) 22 | axiosGetStub = sinon.stub(Axios, 'get').returns(file) 23 | await sync() 24 | }) 25 | 26 | afterEach(function () { 27 | axiosInstanceStub.restore() 28 | axiosGetStub.restore() 29 | }) 30 | 31 | it('returns a complete list of disciplinas', async function () { 32 | let resp = await func() 33 | assert.equal(resp.length, 100) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /app/helpers/parse/pickles.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const mongoose = require('mongoose') 3 | 4 | // this function receives an object and the fields it should pick 5 | // it walks recursively the fields to filter 6 | module.exports = function pickles(obj, fields) { 7 | if (fields == null) { 8 | return obj 9 | } 10 | 11 | // if passing an array 12 | if (_.isArray(obj)) { 13 | return _.map(obj, (a) => pickles(a, fields)) 14 | } 15 | 16 | // mongoose.Types.ObjectId.isValid 17 | if (_.isObject(obj) && !mongoose.isObjectIdOrHexString(obj)) { 18 | // filter the object first 19 | if (obj.toObject instanceof Function) 20 | obj = obj.toObject({ 21 | getters: true, 22 | virtuals: true, 23 | }) 24 | obj = _.pick(obj, fields.public) 25 | 26 | // iterate on object keys 27 | Object.keys(obj).forEach((key) => { 28 | // if it's an array, filter each item of the array 29 | if (_.isArray(obj[key])) { 30 | obj[key] = obj[key].map((item) => pickles(item, fields[key])) 31 | // if not, only filter 32 | } else { 33 | obj[key] = pickles(obj[key], fields[key]) 34 | } 35 | }) 36 | } 37 | 38 | return obj 39 | } 40 | -------------------------------------------------------------------------------- /app/agenda/processors/enrollments/updateEnrollments.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | 4 | module.exports = function (agenda) { 5 | agenda.define('updateEnrollments', app.helpers.agenda.wrap(updateEnrollments)) 6 | } 7 | 8 | async function updateEnrollments (payload) { 9 | const data = payload.json 10 | 11 | let count = 1 12 | 13 | async function updateEnrollments(doc) { 14 | console.log('document', count++, doc.ra) 15 | const keys = ['ra', 'year', 'quad', 'disciplina'] 16 | 17 | const key = { 18 | ra: doc.ra, 19 | year: doc.year, 20 | quad: doc.quad, 21 | disciplina: doc.disciplina 22 | } 23 | 24 | const identifier = app.helpers.transform.identifier(key, keys) 25 | 26 | try { 27 | // check if a enrollment already exists for this 28 | await app.models.enrollments.findOneAndUpdate({ 29 | identifier: identifier 30 | }, _.omit(doc, ['identifier', 'id', '_id']), { 31 | new: true, 32 | upsert: true 33 | }) 34 | } catch(e) { 35 | // console.log(e) 36 | } 37 | } 38 | 39 | return app.helpers.mapLimit(data, updateEnrollments, 10) 40 | } 41 | 42 | module.exports.updateEnrollments = updateEnrollments -------------------------------------------------------------------------------- /app/api/stats/grades/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | function createGroup(totalPoints, inc){ 4 | const branches = [...Array(totalPoints).keys()].map(k => ({ 5 | case: { $lt: ['$value.cr_acumulado', inc * k ]}, then: inc * k 6 | })) 7 | 8 | return { 9 | $switch: { 10 | branches, 11 | default: inc * totalPoints 12 | } 13 | } 14 | } 15 | 16 | module.exports = async function getGradeStats() { 17 | const points = 40 18 | const interval = 4 / points 19 | 20 | const distribution = await app.models.histories.aggregate([{ 21 | $match: { 'coefficients.2018.3': { $exists: true }} 22 | }, { 23 | $project: { 24 | value: '$coefficients.2018.3' 25 | } 26 | }, 27 | { 28 | $group: { 29 | _id: createGroup(points, interval), 30 | total: { $sum: 1 }, 31 | point: { $avg: '$value.cr_acumulado' } 32 | } 33 | }, 34 | { $sort: { point : 1 } } 35 | ]) 36 | 37 | 38 | let normalizedDistribution = distribution.map((interval) => { 39 | interval._id = interval._id.toFixed(2) 40 | interval.point = interval.point .toFixed(2) 41 | return interval 42 | }) 43 | 44 | console.log('DIST', normalizedDistribution) 45 | 46 | return normalizedDistribution 47 | } -------------------------------------------------------------------------------- /app/helpers/mailer/send.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | const SES = require('aws-sdk/clients/ses') 5 | 6 | module.exports = async function send(emails, templateId) { 7 | const ses = new SES({ 8 | accessKeyId: app.config.AWS_ACCESS_KEY_ID, 9 | secretAccessKey: app.config.AWS_SECRET_ACCESS_KEY, 10 | region: app.config.AWS_REGION, 11 | }) 12 | 13 | let TemplateData 14 | 15 | if (templateId === 'Confirmation') { 16 | TemplateData = JSON.stringify({ url: emails.body.url }) 17 | } else { 18 | TemplateData = JSON.stringify({ 19 | recovery_facebook: emails.body.recovery_facebook, 20 | recovery_google: emails.body.recovery_google, 21 | }) 22 | } 23 | 24 | const personalizations = _.castArray(emails).map((e) => 25 | ses 26 | .sendTemplatedEmail({ 27 | Source: app.config.mailer.EMAIL, 28 | Destination: { 29 | ToAddresses: [e.recipient], 30 | }, 31 | Template: templateId, 32 | TemplateData, 33 | }) 34 | .promise() 35 | ) 36 | 37 | try { 38 | await Promise.all(personalizations) 39 | } catch (err) { 40 | throw new errors.Unprocessable(err.message) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ufabc-next-server 2 | 3 | [![Build Status](https://travis-ci.com/ufabc-next/ufabc-next-server.svg?branch=master)](https://travis-ci.com/ufabc-next/ufabc-next-server) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/4f19e74af1ca4b37ba4a069c70b7e5b5)](https://www.codacy.com/gh/ufabc-next/ufabc-next-server/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ufabc-next/ufabc-next-server&utm_campaign=Badge_Grade) 5 | [![codecov](https://codecov.io/gh/ufabc-next/ufabc-matricula-server/branch/master/graph/badge.svg)](https://codecov.io/gh/ufabc-next/ufabc-matricula-server) 6 | 7 | ### Para executar o server 8 | 9 | - Entrar em ufabc-next-server/app e executar o `yarn install`: 10 | - Instalar o **Docker** e o **Docker Compose** 11 | - Dentro de ufabc-next-server/app, como administrador, executar `docker-compose up -d` 12 | - Para iniciar o servidor, executar como `yarn start:watch` a fim de verificar quando um arquivo for atualizado. Dessa forma, o servidor reiniciará automaticamente 13 | 14 | ### Testes 15 | 16 | - Para popular o Banco de Dados com uma massa de dados padrão, executar o `yarn populate both` 17 | - `yarn test` para executar os testes unitários 18 | 19 | Back-end server in Node.js for UFABC Next services. 20 | -------------------------------------------------------------------------------- /app/helpers/middlewares/error.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const Sentry = require('@sentry/node') 4 | const parser = require('ua-parser-js') 5 | 6 | module.exports = (err, req, res, next) => { 7 | if (!err) { 8 | return next() 9 | } 10 | 11 | // Gatter error metadata 12 | let body = app.helpers.parse.error(err) 13 | 14 | // Add stack on debug mode 15 | if (!app.config.isProduction) { 16 | body.stack = err.stack 17 | } 18 | 19 | // Apply status to response 20 | res.status(body.status) 21 | 22 | // Cleanup body if in production 23 | if (app.config.isProduction && body.status == 500) { 24 | // Prepare response to user 25 | body.type = 'FatalError' 26 | body.error = 'Um erro inesperado aconteceu e foi enviado aos nossos desenvolvedores' 27 | 28 | // Get user agent 29 | const ua = _.pick(parser(req.headers['user-agent']), ['browser', 'os']) 30 | if(ua.os && ua.os.name) { 31 | ua.os.name = ua.os.name.replace('Mac OS', 'Mac OS X') 32 | } 33 | 34 | Sentry.captureException(err, { contexts: ua }) 35 | } 36 | 37 | if (!app.config.isProduction && body.status == 500) { 38 | console.log(err) 39 | } 40 | 41 | // Send back error 42 | return res.send(body) 43 | } 44 | -------------------------------------------------------------------------------- /app/helpers/parse/error.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | const errors = require('@/errors') 4 | 5 | module.exports = (err) => { 6 | const MappingErrors = [ 7 | { status: 400, name: 'BadRequest', class: errors.BadRequest }, 8 | { status: 401, name: 'Unauthorized', class: errors.Unauthorized }, 9 | { status: 403, name: 'Forbidden', class: errors.Forbidden }, 10 | { status: 404, name: 'NotFound', class: errors.NotFound }, 11 | { status: 409, name: 'Conflict', class: errors.Conflict }, 12 | { status: 422, name: 'Unprocessable', class: errors.Unprocessable }, 13 | ] 14 | 15 | // support for passing tendaEdu errors up 16 | if(_.find(MappingErrors, { name: err.type, status: err.status })) return err 17 | 18 | let errorClass = _.find(MappingErrors, maybe => err instanceof maybe.class) 19 | 20 | // Defaults to FatalError 21 | errorClass = errorClass || { status: 500, name: 'FatalError' } 22 | 23 | let parsed = { 24 | status: errorClass.status, 25 | name: errorClass.name, 26 | type: err.name, 27 | error: parseSafely(err.message), 28 | } 29 | 30 | return parsed 31 | } 32 | 33 | function parseSafely(message) { 34 | try { 35 | return JSON.parse(message) 36 | } catch(e) { 37 | return message 38 | } 39 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build/release 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - run: npm install 11 | - run: npm run lint 12 | 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Start MongoDB 19 | uses: supercharge/mongodb-github-action@1.7.0 20 | with: 21 | mongodb-version: 4.2 22 | 23 | - name: Start Redis 24 | uses: supercharge/redis-github-action@1.2.0 25 | with: 26 | redis-version: 5 27 | 28 | - run: cd app && yarn install 29 | - run: cd app && yarn test 30 | 31 | release: 32 | needs: test 33 | runs-on: ubuntu-latest 34 | if: github.ref == 'refs/heads/master' 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v2 39 | 40 | - name: Set up npm 41 | uses: actions/setup-node@v2 42 | with: 43 | node-version: '14' 44 | 45 | - name: Install caprover 46 | run: npm install -g caprover 47 | 48 | - name: Caprover Deploy 49 | run: caprover deploy -h 'https://captain.captain.sv.ufabcnext.com' -p '${{ secrets.CAPROVER_PASSWORD }}' -b 'master' -a 'api' 50 | -------------------------------------------------------------------------------- /app/setup/static.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const fs = require('fs') 3 | 4 | /* 5 | * Serve assets on app.config.DIST_FOLDER 6 | */ 7 | module.exports = async (app) => { 8 | let static = express.static(app.config.distFolder, { 9 | maxAge: app.config.maxAge, 10 | }) 11 | app.server.use ((req, res, next) => { 12 | let headers = req.headers 13 | 14 | //Get Headers 15 | let host = headers['host'] 16 | 17 | // In case host doesn't matches the HOST config, continue 18 | if (host != app.config.HOST) { 19 | return next() 20 | } 21 | 22 | 23 | next() 24 | }) 25 | 26 | app.server.use(static) 27 | app.server.get('/snapshot', function(req, res) { 28 | const { aluno_id } = req.query 29 | fs.readFile(app.config.snapshotFolder + '/index.html', 'utf8', function(err, data) { 30 | if (err) { 31 | console.log(err) 32 | res.send(500) 33 | return 34 | } 35 | let result = data 36 | result = result.replace('ALUNO_ID_HERE', aluno_id) 37 | res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'}) 38 | res.write(result) 39 | res.end() 40 | }) 41 | }) 42 | app.server.use('/snapshot', express.static(app.config.snapshotFolder, { 43 | maxAge: app.config.maxAge, 44 | })) 45 | } 46 | -------------------------------------------------------------------------------- /app/helpers/mapLimit.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (arr, func, limit = 2, ...args) { 2 | if (! (arr instanceof Array)) { 3 | throw new Error('Coleção de entrada deve ser um Array') 4 | // throw new Error('Input collection must be an Array') 5 | } 6 | 7 | let pending = [] 8 | let all = [] 9 | 10 | // Flag indicating something has thrown an error 11 | let thrown = null 12 | 13 | while (all.length < arr.length || pending.length > 0) { 14 | // Push to queue limited by total and the `limit` 15 | while (all.length < arr.length && pending.length < limit) { 16 | let element = arr[all.length] 17 | let promise = func(element, ...args) 18 | 19 | // Push to both lists 20 | pending.push(promise) 21 | all.push(promise) 22 | 23 | // Once this has fulfilled, remove it from pending or set throw error flag 24 | promise 25 | .then(() => pending.splice(pending.indexOf(promise), 1)) 26 | .catch(e => { thrown = e }) 27 | } 28 | 29 | // Wait something to finish, then remove it 30 | if (thrown || pending.length <= 0) 31 | break 32 | 33 | await Promise.race(pending) 34 | } 35 | 36 | // Error was thrown, break loop 37 | if (thrown) { 38 | throw thrown 39 | } 40 | 41 | // Map results back 42 | let res = [] 43 | for (let p of all) res.push(await p) 44 | return res 45 | } -------------------------------------------------------------------------------- /app/helpers/validate/mongoError.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This method is responsible for parsing an input and perhaps, 3 | * throwing errors to the application with the converted error 4 | * type. 5 | * 6 | * For instance, Mongo uses custom internal errors such as 7 | * 'ValidationError'. We then convert it to our own type of error 8 | * best for Requests and make them a 401 instead of 500 when 9 | * throwing them during request error handling 10 | * 11 | * See: helpers/middlewares/errors for details on handling errors 12 | * 13 | */ 14 | 15 | var _ = require('lodash') 16 | var errors = require('@/errors') 17 | 18 | module.exports = (maybeError) => { 19 | if (maybeError == null) 20 | return 21 | 22 | // Call self recursivelly if in an array 23 | if (_.isArray(maybeError)) { 24 | maybeError.map(module.exports) 25 | return 26 | } 27 | 28 | if (maybeError.name == 'ValidationError') { 29 | throw new errors.BadRequest.InvalidParameter(maybeError.message) 30 | } 31 | 32 | if(maybeError.code == 11000) { 33 | let firstSplit = _.get(maybeError.message.split('index: '), '1' , '') 34 | let secondSplit = _.get(firstSplit.split('dup key'), '0') 35 | let index = secondSplit.replace(/\s/g,'').split('_').filter(a => a != '1') 36 | let last = index[index.length - 1] 37 | throw new errors.Conflict(last) 38 | } 39 | 40 | throw maybeError 41 | } -------------------------------------------------------------------------------- /app/helpers/rest/paginate.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const func = require('./paginate') 3 | 4 | describe('helpers.rest.paginate', function() { 5 | it('apply defaults on limit and page', function () { 6 | let context = { 7 | erm: { query: {} }, 8 | query: {} 9 | } 10 | 11 | func(context, null, () => {} ) 12 | assert.equal(context.erm.query.limit, 10) 13 | assert.equal(context.erm.query.skip, 0) 14 | assert.equal(context.query.limit, 10) 15 | assert.equal(context.query.page, 1) 16 | }) 17 | 18 | it('corrects problem', function () { 19 | let context = { 20 | erm: { query: {} }, 21 | query: { 22 | page: 'dois' 23 | } 24 | } 25 | 26 | func(context, null, () => {} ) 27 | assert.equal(context.erm.query.limit, 10) 28 | assert.equal(context.erm.query.skip, 0) 29 | assert.equal(context.query.limit, 10) 30 | assert.equal(context.query.page, 1) 31 | }) 32 | 33 | it('paginates according to page and limit passed', function () { 34 | let context = { 35 | erm: { query: { limit: 5 } }, 36 | query: { 37 | page: '10', 38 | } 39 | } 40 | 41 | func(context, null, () => {} ) 42 | 43 | assert.equal(context.erm.query.limit, 5) 44 | assert.equal(context.erm.query.skip, 45) 45 | assert.equal(context.query.limit, 5) 46 | assert.equal(context.query.page, 10) 47 | }) 48 | }) -------------------------------------------------------------------------------- /app/setup/models.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Load models and register with Mongoose 3 | */ 4 | const mongoose = require('mongoose') 5 | //const mongoosastic = require('mongoosastic') 6 | const requireSmart = require('require-smart') 7 | const PluginMongoTenant = require('mongo-tenant') 8 | const PluginTimestamp = require('mongoose-timestamp') 9 | const mongooseLeanVirtuals = require('mongoose-lean-virtuals') 10 | 11 | module.exports = async (app) => { 12 | let schemas = requireSmart('../models', { 13 | skip: [/\..*\.js$/], 14 | }) 15 | 16 | // Return loaded models 17 | return await walkModels(app, schemas) 18 | } 19 | 20 | // recursively walk into models folders 21 | async function walkModels(app, schemas) { 22 | let models = {} 23 | 24 | for (let name in schemas) { 25 | let Schema = schemas[name] 26 | 27 | // check if we are dealing with a schema or a subfolder 28 | if (Schema instanceof mongoose.Schema) { 29 | // Applies basic plugins to all models 30 | Schema.plugin(PluginMongoTenant, app.config.mongoTenant) 31 | Schema.plugin(PluginTimestamp) 32 | Schema.plugin(mongooseLeanVirtuals) 33 | 34 | // Load model into mongo connection 35 | models[name] = app.mongo.model(name, Schema) 36 | } else { 37 | // Just append to the tree, but don't load it as a Mongoose model 38 | models[name] = Schema 39 | } 40 | } 41 | 42 | return models 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | .DS_Store 76 | node_modules/ 77 | dist/ -------------------------------------------------------------------------------- /app/api/graduations/list/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function func() { 4 | const Graduation = await app.models.graduation 5 | const graduations = await Graduation.find({}).lean() 6 | 7 | const Subject = await app.models.subjects 8 | const SubjectGraduations = await app.models.subjectGraduations 9 | 10 | const populatedGraduations = await Promise.all( 11 | graduations.map(async (graduation) => { 12 | const subjectsGraduations = await SubjectGraduations.find({ 13 | graduation: graduation._id, 14 | }).lean() 15 | 16 | const subjects = ( 17 | await Promise.all( 18 | subjectsGraduations.map(async (subjectGraduation) => { 19 | const subject = await Subject.findOne({ 20 | _id: subjectGraduation.subject, 21 | }).lean() 22 | return { 23 | ...subjectGraduation, 24 | subject, 25 | } 26 | }) 27 | ) 28 | ).reduce((acc, subject) => { 29 | const category = subject.category 30 | if (!category) { 31 | return acc 32 | } 33 | if (!acc[category]) { 34 | acc[category] = [] 35 | } 36 | acc[category].push(subject) 37 | return acc 38 | }, {}) 39 | 40 | return { 41 | ...graduation, 42 | subjects, 43 | } 44 | }) 45 | ) 46 | 47 | return populatedGraduations 48 | } 49 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | coverage.lcov 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless 75 | 76 | # Leave documentation outg 77 | doc/ 78 | 79 | alunos.json -------------------------------------------------------------------------------- /app/helpers/routes/rule.spec.js: -------------------------------------------------------------------------------- 1 | const rule = require('./rule') 2 | 3 | async function ruleAllowByContext(context) { 4 | if (!context.allow) 5 | throw 'Invalid' 6 | } 7 | 8 | async function ruleAllow() { 9 | return 10 | } 11 | 12 | async function ruleDeny() { 13 | throw 'Invalid credentials' 14 | } 15 | 16 | describe('helpers/routes.rule', function () { 17 | it('should proceed with callback', function(cb) { 18 | let wrapped = rule(ruleAllow) 19 | 20 | wrapped({}, {}, (err) => { 21 | if (err !== undefined) 22 | return cb('Should receive undefined as error') 23 | 24 | cb() 25 | }) 26 | }) 27 | 28 | it('should not proceed with callback', function(cb) { 29 | let wrapped = rule(ruleDeny) 30 | 31 | wrapped({}, {}, (err) => { 32 | if (err === undefined) 33 | return cb('Should receive one error') 34 | 35 | cb() 36 | }) 37 | }) 38 | 39 | it('should allow depending on context', function(cb) { 40 | let wrapped = rule(ruleAllowByContext) 41 | 42 | wrapped({allow: false}, {}, (err) => { 43 | if (err === undefined) 44 | return cb('Should receive one error') 45 | 46 | cb() 47 | }) 48 | }) 49 | 50 | it('should deny depending on context', function(cb) { 51 | let wrapped = rule(ruleAllowByContext) 52 | 53 | wrapped({allow: true}, {}, (err) => { 54 | if (err !== undefined) 55 | return cb('Should receive undefined as error') 56 | 57 | cb() 58 | }) 59 | }) 60 | }) -------------------------------------------------------------------------------- /app/helpers/parse/slugify.js: -------------------------------------------------------------------------------- 1 | module.exports = function slugify(text) { 2 | text = text.toString().toLowerCase().trim() 3 | 4 | const sets = [ 5 | {to: 'a', from: '[ÀÁÂÃÄÅÆĀĂĄẠẢẤẦẨẪẬẮẰẲẴẶ]'}, 6 | {to: 'c', from: '[ÇĆĈČ]'}, 7 | {to: 'd', from: '[ÐĎĐÞ]'}, 8 | {to: 'e', from: '[ÈÉÊËĒĔĖĘĚẸẺẼẾỀỂỄỆ]'}, 9 | {to: 'g', from: '[ĜĞĢǴ]'}, 10 | {to: 'h', from: '[ĤḦ]'}, 11 | {to: 'i', from: '[ÌÍÎÏĨĪĮİỈỊ]'}, 12 | {to: 'j', from: '[Ĵ]'}, 13 | {to: 'ij', from: '[IJ]'}, 14 | {to: 'k', from: '[Ķ]'}, 15 | {to: 'l', from: '[ĹĻĽŁ]'}, 16 | {to: 'm', from: '[Ḿ]'}, 17 | {to: 'n', from: '[ÑŃŅŇ]'}, 18 | {to: 'o', from: '[ÒÓÔÕÖØŌŎŐỌỎỐỒỔỖỘỚỜỞỠỢǪǬƠ]'}, 19 | {to: 'oe', from: '[Œ]'}, 20 | {to: 'p', from: '[ṕ]'}, 21 | {to: 'r', from: '[ŔŖŘ]'}, 22 | {to: 's', from: '[ߌŜŞŠ]'}, 23 | {to: 't', from: '[ŢŤ]'}, 24 | {to: 'u', from: '[ÙÚÛÜŨŪŬŮŰŲỤỦỨỪỬỮỰƯ]'}, 25 | {to: 'w', from: '[ẂŴẀẄ]'}, 26 | {to: 'x', from: '[ẍ]'}, 27 | {to: 'y', from: '[ÝŶŸỲỴỶỸ]'}, 28 | {to: 'z', from: '[ŹŻŽ]'}, 29 | {to: '-', from: '[·/_,:;\']'} 30 | ] 31 | 32 | sets.forEach(set => { 33 | text = text.replace(new RegExp(set.from,'gi'), set.to) 34 | }) 35 | 36 | return text 37 | .replace(/\s+/g, '-') // Replace spaces with - 38 | .replace(/[^\w-]+/g, '') // Remove all non-word chars 39 | .replace(/--+/g, '-') // Replace multiple - with single - 40 | .replace(/^-+/, '') // Trim - from start of text 41 | .replace(/-+$/, '') // Trim - from end of text 42 | } -------------------------------------------------------------------------------- /app/setup/agenda.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const Agenda = require('agenda') 3 | const requireSmart = require('require-smart') 4 | 5 | // Load helpers into app.helper 6 | module.exports = async(app) => { 7 | let files = requireSmart('../agenda', { 8 | skip: [/spec\.js$/], 9 | }) 10 | 11 | const FIVE_MINUTES = 1000 * 60 * 5 12 | 13 | const agenda = new Agenda({ 14 | db: { 15 | address: app.config.MONGO_URL, 16 | collection: 'agenda' 17 | }, 18 | defaultLockLifetime: FIVE_MINUTES, 19 | processEvery: '5 seconds' 20 | }) 21 | 22 | // only initialize jobs if we are not testing 23 | !app.config.isTest ? initialize(files, agenda) : null 24 | 25 | // wait agenda to be ready before returning 26 | return new Promise((resolve) => { 27 | agenda.once('ready', async function () { 28 | const collection = _.get(agenda, '_collection.collection', null) || agenda._collection 29 | 30 | // make sure indexes are created to optimized front-end 31 | await collection.createIndexes([ 32 | { key: { nextRunAt: -1, lastRunAt: -1, lastFinishedAt: -1 } }, 33 | { key: { name: 1, nextRunAt: -1, lastRunAt: -1, lastFinishedAt: -1 } } 34 | ]) 35 | 36 | resolve(agenda) 37 | }) 38 | }) 39 | } 40 | 41 | // initialize all jobs from agenda 42 | function initialize(files, agenda) { 43 | if(_.isObject(files) && !_.isFunction(files)) { 44 | Object.keys(files).map(key => initialize(files[key], agenda)) 45 | } 46 | if(_.isFunction(files)) { 47 | files(agenda) 48 | } 49 | } -------------------------------------------------------------------------------- /app/api/disciplinas/sync/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const assert = require('assert') 3 | const populate = require('@/populate') 4 | const sinon = require('sinon') 5 | const Axios = require('axios') 6 | 7 | const func = require('./func') 8 | 9 | describe('POST /v1/disciplinas/sync', function () { 10 | beforeEach(async function () { 11 | await populate({ operation: 'both', only: ['disciplinas'] }) 12 | }) 13 | 14 | describe('func', function () { 15 | it('sync disciplines', async function () { 16 | let file = app.helpers.test.getDisciplinas() 17 | file.data = app.helpers.test.sample(file.data, 200) 18 | 19 | let axiosInstanceStub 20 | let axiosGetStub 21 | try { 22 | axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) 23 | axiosGetStub = sinon.stub(Axios, 'get').returns(file) 24 | await func() 25 | 26 | // check if is scoped by season 27 | let Disciplinas = app.models.disciplinas.bySeason('2018:2') 28 | assert.equal(await Disciplinas.findOne({}), null) 29 | 30 | const season = app.helpers.season.findSeasonKey() 31 | Disciplinas = app.models.disciplinas.bySeason(season) 32 | 33 | assert.equal(await Disciplinas.countDocuments({}), 200) 34 | let disciplina = await Disciplinas.findOne({ 35 | disciplina_id: 2538, 36 | }).lean(true) 37 | assert.equal(disciplina.disciplina_id, 2538) 38 | } finally { 39 | axiosInstanceStub.restore() 40 | axiosGetStub.restore() 41 | } 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /app/api/users/info/spec.js: -------------------------------------------------------------------------------- 1 | const func = require('./func') 2 | const assert = require('assert') 3 | 4 | describe('GET /users/info', function() { 5 | describe('func', function() { 6 | describe('with valid params', function() { 7 | it('should return user public info', async function() { 8 | const user = { 9 | '_id': 'some id', 10 | 'oauth': 'google', 11 | 'confirmed': true, 12 | 'email' : 'someemail@ufabc.next.com', 13 | 'ra': 'some RA', 14 | 'createdAt': new Date(), 15 | 'devices': 'some device', 16 | 'private' : 'should not return' 17 | } 18 | 19 | const context = { 20 | user 21 | } 22 | 23 | const response = await func(context) 24 | 25 | assert.equal(user._id, response._id) 26 | assert.equal(user.oauth, response.oauth) 27 | assert.equal(user.confirmed, response.confirmed) 28 | assert.equal(user.email, response.email) 29 | assert.equal(user.ra, response.ra) 30 | assert.equal(user.createdAt, response.createdAt) 31 | assert.equal(user.devices, response.devices) 32 | assert.notEqual(user.private, response.private) 33 | 34 | 35 | }) 36 | }) 37 | describe('with invalid params', function() { 38 | it('should throw an error when user is null', async function() { 39 | const context = { 40 | 41 | } 42 | 43 | await assertFuncThrows('NotFound', func, context) 44 | 45 | }) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /app/models/disciplinas.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const Schema = require('mongoose').Schema 3 | 4 | var Model = module.exports = Schema({ 5 | disciplina_id: Number, 6 | disciplina: String, 7 | turno: String, 8 | turma: String, 9 | vagas: Number, 10 | obrigatorias: [Number], 11 | codigo: String, 12 | campus: String, 13 | ideal_quad: Boolean, 14 | 15 | subject: { 16 | type: Schema.Types.ObjectId, 17 | ref: 'subjects' 18 | }, 19 | 20 | identifier: { 21 | type: String, 22 | required: true 23 | }, 24 | 25 | // lista de alunos matriculados no momento 26 | alunos_matriculados: { 27 | type: [Number], 28 | default: [] 29 | }, 30 | 31 | // como estava o estado da matrícula antes do chute 32 | before_kick: { 33 | type: [Number], 34 | default: [] 35 | }, 36 | 37 | // como estava o estado da matrícula após o chute 38 | after_kick: { 39 | type: [Number], 40 | default: [] 41 | }, 42 | 43 | year: Number, 44 | quad: Number, 45 | 46 | teoria: { 47 | type: Schema.Types.ObjectId, 48 | ref: 'teachers' 49 | }, 50 | pratica: { 51 | type: Schema.Types.ObjectId, 52 | ref: 'teachers' 53 | }, 54 | }) 55 | 56 | Model.virtual('requisicoes').get(function () { 57 | return (this.alunos_matriculados || []).length 58 | }) 59 | 60 | Model.index({ identifier: 1 }) 61 | 62 | Model.pre('findOneAndUpdate', function () { 63 | if(!this._update.season) { 64 | const season = app.helpers.season.findSeason() 65 | this._update.year = season.year 66 | this._update.quad = season.quad 67 | } 68 | }) -------------------------------------------------------------------------------- /app/api/users/me/grades/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | 5 | module.exports = async(context) => { 6 | const { user } = context 7 | 8 | //This code is necessary for show data to performance page - get the coefficients from the last history 9 | //Example: users with BCT concluded and BCC in progress will have the BCC coefficients showed on the performance screen. 10 | const lastHistory = await app.models.historiesGraduations.findOne({ra: user.ra}).sort({createdAt: -1}) 11 | 12 | //Next step 13 | //Needs to add a querie to get the coefficients from the first historyGraduatiation and show that on the performance screen. 14 | 15 | 16 | if(!lastHistory) { 17 | throw new errors.NotFound('history') 18 | } 19 | 20 | let graduation = null 21 | if(lastHistory.curso && lastHistory.grade) { 22 | graduation = await app.models.graduation.findOne({ 23 | curso: lastHistory.curso, 24 | grade: lastHistory.grade, 25 | }).lean(true) 26 | } 27 | 28 | const coefficients = lastHistory.coefficients || app.helpers.calculate.coefficients(lastHistory.disciplinas || [], graduation) 29 | 30 | return normalizeHistory(coefficients) 31 | } 32 | 33 | function normalizeHistory(history) { 34 | var total = [] 35 | Object.keys(history).forEach(key => { 36 | const year = history[key] 37 | Object.keys(year).forEach(month => { 38 | total.push(_.extend(year[month], { 39 | season: `${key}:${month}`, 40 | quad: parseInt(month), 41 | year: parseInt(key) 42 | })) 43 | }) 44 | }) 45 | 46 | return total 47 | } -------------------------------------------------------------------------------- /app/fixtures/enrollments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "year": 2018, 4 | "quad": 1, 5 | "ra": "11028914", 6 | "codigo": "NA2ESTG017-17SB", 7 | "disciplina": "Introdução aos Processos de", 8 | "campus": "sao bernardo", 9 | "turno": "noturno", 10 | "turma": "A2", 11 | "teoria": "Guilherme Canuto Da Silva", 12 | "pratica": "Guilherme Canto Da Silva", 13 | "identifier": "79788a4ea84a724f1c6106fd8b33ecda", 14 | "mainTeacher": "5cb659db0b0a3917c56f7961", 15 | "subject": "5cb659db0b0a3917c56f753c" 16 | }, 17 | { 18 | "year": 2018, 19 | "quad": 1, 20 | "ra": "11028914", 21 | "codigo": "NAESTG009-17SB", 22 | "disciplina": "Gestão de Operações", 23 | "campus": "sao bernardo", 24 | "turno": "noturno", 25 | "turma": "A", 26 | "teoria": "Ricardo Reolon Jorge", 27 | "pratica": null, 28 | "identifier": "b2046666ce07017174cf138316167be7" 29 | }, 30 | { 31 | "year": 2018, 32 | "quad": 1, 33 | "ra": "11028914", 34 | "codigo": "NAESTG011-17SB", 35 | "disciplina": "Estatística Aplicada a Sistemas de Gestão", 36 | "campus": "sao bernardo", 37 | "turno": "noturno", 38 | "turma": "A", 39 | "teoria": "Patricia Belfiore Favero", 40 | "pratica": "Patricia Teixeira Leite Asano", 41 | "identifier": "fbe1fc70127f1073e8e4d981744edecb" 42 | }, 43 | { 44 | "year": 2018, 45 | "quad": 1, 46 | "ra": "11086610", 47 | "codigo": "NAESTS003-17SB", 48 | "disciplina": "Introdução à Astronáutica", 49 | "campus": "sao bernardo", 50 | "turno": "noturno", 51 | "turma": "A", 52 | "teoria": "Carlos Renato Huaura Solorzano", 53 | "pratica": null, 54 | "identifier": "48186a9d78c7994fc8120c1113d9c4aa" 55 | } 56 | ] -------------------------------------------------------------------------------- /app/helpers/season/findIdeais.js: -------------------------------------------------------------------------------- 1 | function findQuadFromDate(month) { 2 | if([0, 1, 2, 10, 11].includes(month)) return 3 3 | if([3, 4, 5].includes(month)) return 1 4 | if([6, 7, 8, 9].includes(month)) return 2 5 | } 6 | 7 | module.exports = function findIdeais(date = new Date()) { 8 | const month = date.getMonth() 9 | return { 10 | 1 : 11 | [ 12 | 'BCM0506-15', // COMUNICACAO E REDES 13 | 'BCJ0203-15', // ELETROMAG 14 | 'BIN0406-15', // IPE 15 | 'BCN0405-15', // IEDO 16 | 'BIR0004-15', // EPISTEMOLOGICAS 17 | 'BHO0102-15', // DESENVOL. E SUSTE. 18 | 'BHO0002-15', // PENSA. ECONOMICO 19 | 'BHP0201-15', // TEMAS E PROBLEMAS 20 | 'BHO0101-15', // ESTADO E RELA 21 | 'BIR0603-15', // CTS 22 | 'BHQ0003-15', // INTEPRE. BRASIL 23 | 'BHQ0001-15', // IDENT.E CULTURA 24 | ], 25 | 2 : [ 26 | 'BCM0504-15', // NI 27 | 'BCN0404-15', // GA 28 | 'BCN0402-15', // FUV 29 | 'BCJ0204-15', // FEMEC 30 | 'BCL0306-15', // BIODIVERSIDADE 31 | 'BCK0103-15', // QUANTICA 32 | 'BCL0308-15', // BIOQUIMICA 33 | 'BIQ0602-15', // EDS 34 | 'BHO1335-15', // FORMACAO SISTEMA INTERNACIONAL 35 | 'BHO1101-15', // INTRODUCAO A ECONOMIA 36 | 'BHO0001-15', // INTRODUCAO AS HUMANIDADES 37 | 'BHP0202-15', // PENSAMENTO CRITICO 38 | ], 39 | 3 : [ 40 | 'BCJ0205-15', // FETERM 41 | 'BCM0505-15', // PI 42 | 'BCN0407-15', // FVV 43 | 'BCL0307-15', // TQ 44 | 'BCK0104-15', // IAM 45 | 'BIR0603-15', // CTS 46 | 'BHP0001-15', // ETICA E JUSTICA 47 | 'BHQ0301-15', // TERRITORIO E SOCIEDADE 48 | // ESTUDO ÉTNICOS RACIAIS 49 | ], 50 | }[findQuadFromDate(month)] 51 | } -------------------------------------------------------------------------------- /app/api/enrollments/create/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const crypto = require('crypto') 4 | const errors = require('@/errors') 5 | 6 | module.exports = async function (context) { 7 | const { season, hash, teacherMappings, subjectMappings } = context.body 8 | let { enrollments } = context.body 9 | 10 | if(!season) { 11 | throw new errors.BadRequest.MissingParameter('season') 12 | } 13 | 14 | // get all teachers 15 | const ONE_HOUR = 60 * 60 16 | const teachers = await app.models.teachers.find({}).lean(true).cache(ONE_HOUR, 'teachers') 17 | const subjects = await app.models.subjects.find({}).lean(true).cache(ONE_HOUR, 'subjects') 18 | 19 | enrollments = enrollments 20 | .filter(d => d && Number.isInteger(parseInt(d.ra))) 21 | .map(app.helpers.transform.disciplinas) 22 | .map(d => _.merge(d, { 23 | teoria: app.helpers.transform.resolveProfessor(d.teoria, teachers, teacherMappings), 24 | pratica: app.helpers.transform.resolveProfessor(d.pratica, teachers, teacherMappings), 25 | })) 26 | 27 | const teacherErrors = app.helpers.validate.teachers(enrollments) 28 | const subjectErrors = app.helpers.validate.subjects(enrollments, subjects, subjectMappings) 29 | 30 | const enrollmentsHash = crypto.createHash('md5').update(JSON.stringify(enrollments)).digest('hex') 31 | if(enrollmentsHash != hash) { 32 | return { 33 | hash: enrollmentsHash, 34 | teacherErrors: _.uniq(teacherErrors), 35 | subjectErrors: _.uniq(subjectErrors), 36 | documents: enrollments.length 37 | } 38 | } 39 | 40 | app.redis.publish('enrollmentCreate', { 41 | season: season, 42 | enrollments: enrollments 43 | }) 44 | 45 | return { 46 | status: 'published', 47 | } 48 | } -------------------------------------------------------------------------------- /app/agenda/processors/enrollments/updateStuff.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = function (agenda) { 4 | agenda.define('updateStuff', app.helpers.agenda.wrap(updateStuff)) 5 | } 6 | 7 | async function updateStuff (payload) { 8 | const data = payload.json 9 | 10 | const ONE_HOUR = 60 * 60 11 | const teachers = await app.models.teachers.find({}).lean(true).cache(ONE_HOUR, 'teachers') 12 | 13 | let count = 1 14 | 15 | async function updateEnrollments(doc) { 16 | console.log('document', count++, doc.ra) 17 | const keys = ['ra', 'year', 'quad', 'disciplina'] 18 | 19 | const key = { 20 | ra: doc.ra, 21 | year: doc.year, 22 | quad: doc.quad, 23 | disciplina: doc.disciplina 24 | } 25 | 26 | const identifier = app.helpers.transform.identifier(key, keys) 27 | 28 | try { 29 | // check if a enrollment already exists for this 30 | await app.models.enrollments.findOneAndUpdate({ 31 | identifier: identifier 32 | }, { 33 | teoria: app.helpers.transform.resolveProfessor(doc.teoria, teachers), 34 | pratica: app.helpers.transform.resolveProfessor(doc.pratica, teachers) 35 | }, { 36 | new: true, 37 | upsert: true 38 | }) 39 | } catch(e) { 40 | // console.log(e) 41 | } 42 | 43 | 44 | 45 | // if(enrollment && enrollment.mainTeacher) { 46 | // const cacheKey = `reviews_${enrollment.mainTeacher}` 47 | // await app.redis.cache.del(cacheKey) 48 | // } 49 | 50 | // if(enrollment && enrollment.subject) { 51 | // const cacheKey = `reviews_${enrollment.subject}` 52 | // await app.redis.cache.del(cacheKey) 53 | // } 54 | 55 | // return enrollment 56 | } 57 | 58 | return app.helpers.mapLimit(data, updateEnrollments, 10) 59 | } 60 | 61 | module.exports.updateStuff = updateStuff -------------------------------------------------------------------------------- /app/helpers/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const unless = require('express-unless') 2 | const jwt = require('jsonwebtoken') 3 | const app = require('@/app') 4 | const errors = require('@/errors') 5 | 6 | module.exports = async (req, res, next) => { 7 | try { 8 | /* 9 | * Check for Bearer token (JWT format) 10 | */ 11 | let authorization = req.headers.authorization 12 | if (authorization && authorization.startsWith('Bearer ')) { 13 | let tokenString = req.headers.authorization.replace('Bearer ', '') 14 | 15 | // bypass authentication development enviroment 16 | if (tokenString == app.config.TOKEN_DEVELOPMENT && app.config.isDev){ 17 | req.user = await app.models.users.findOne({ ra : '999999' }) 18 | if(!req.user || !req.user.active) { 19 | throw new errors.BadRequest('Você precisa executar o comando populate.') 20 | } 21 | 22 | return next() 23 | } 24 | 25 | // verify user 26 | await jwt.verify(tokenString, app.config.JWT_SECRET) 27 | let user = jwt.decode(tokenString, { complete: true }) || {} 28 | 29 | req.user = await app.models.users.findOne({ _id: user.payload._id }) 30 | 31 | if(!req.user.active) { 32 | throw new errors.BadRequest('Essa conta foi desativada') 33 | } 34 | 35 | 36 | // if(!req.user.confirmed) { 37 | // throw new errors.Unauthorized('Usuário ainda não foi confirmado') 38 | // } 39 | 40 | if(!req.user) { 41 | throw new errors.Unauthorized('Usuário não existe ou foi desativado') 42 | } 43 | 44 | } else { 45 | // Default is to throw... 46 | throw new errors.BadRequest('Header de autenticação inválido ou não fornecido') 47 | } 48 | 49 | next() 50 | } catch (e) { 51 | next(e) 52 | } 53 | } 54 | 55 | 56 | module.exports.unless = unless 57 | -------------------------------------------------------------------------------- /app/helpers/transform/disciplinas.spec.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const assert = require('assert') 4 | 5 | const func = require('./disciplinas') 6 | 7 | describe('helpers.transform.disciplinas', function () { 8 | let sample 9 | let pick = ['disciplina', 'ideal_quad', 'turma', 'campus', 'turno'] 10 | 11 | beforeEach(function () { 12 | sample = app.helpers.test.getDisciplinas().data 13 | sample = app.helpers.parse.var2json(sample) 14 | }) 15 | 16 | it('test if parses everything correctly', function () { 17 | let resp = sample.map(m => _.pick(func(m), pick)) 18 | 19 | assert(resp.every(r => ['diurno', 'noturno', 'tarde'].includes(r.turno))) 20 | assert(resp.every(r => ['sao bernardo', 'santo andre' ].includes(r.campus))) 21 | assert(resp.every(r => r.turma.length > 0 && r.turma.length <= 3)) 22 | }) 23 | 24 | it('multiple parenthesis', function () { 25 | let resp = func({ nome: 'Aeronáutica I-A (quantas coisas e ---) A3 - São Bernardo Noturno'}) 26 | assert.equal(resp.disciplina, 'Aeronáutica I-A (quantas coisas e ---)') 27 | assert.equal(resp.turma, 'A3') 28 | assert.equal(resp.campus, 'sao bernardo') 29 | assert.equal(resp.turno, 'noturno') 30 | }) 31 | 32 | it('who fails', function () { 33 | let resp = func({ nome: 'Dinâmica de Fluidos Computacional A-diurno (São Bernardo d\rCampo) - MINISTRADA EM INGLÊS' }) 34 | assert.equal(resp.disciplina, 'Dinâmica de Fluidos Computacional') 35 | }) 36 | 37 | xit('without -', function () { 38 | let resp = func({ nome: 'Introdução às Humanidades e Ciências Sociais A\rdiurno (São Bernardo do Campo)'}) 39 | assert.equal(resp.disciplina, 'Introdução às Humanidades e Ciências Sociais') 40 | assert.equal(resp.turma, 'A') 41 | assert.equal(resp.campus, 'sao bernardo') 42 | assert.equal(resp.turno, 'diurno') 43 | }) 44 | }) -------------------------------------------------------------------------------- /app/setup/server.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/node') 2 | const morgan = require('morgan') 3 | const express = require('express') 4 | const bodyParser = require('body-parser') 5 | const path = require('path') 6 | const compression = require('compression') 7 | // const { createAgent } = require('@forestadmin/agent') 8 | // const { createMongooseDataSource } = require('@forestadmin/datasource-mongoose') 9 | const methodOverride = require('method-override') 10 | 11 | const logFormat = '[server] [:date[iso]] :status :res[content-length] :response-time ms :method :url :remote-addr' 12 | 13 | /* 14 | * Instantiate and configure express server 15 | */ 16 | module.exports = async (app) => { 17 | let server = express() 18 | 19 | // Config Sentry 20 | if (app.config.isProduction) { 21 | Sentry.init({ dsn: app.config.SENTRY, tracesSampleRate: 1.0 }) 22 | } 23 | 24 | // Configure compression 25 | server.use(compression()) 26 | 27 | // Configure body-parsing 28 | // server.use('^(?!forest).+$', bodyParser.json({ limit: '30mb', extended: true })) 29 | server.use(bodyParser.urlencoded({ limit: '30mb', extended: true })) 30 | server.use('/v1/', bodyParser.json()) 31 | 32 | // For browsers that don't make advanced HTTP Method calls 33 | server.use(methodOverride('_method')) 34 | 35 | // HTTP request Logging 36 | server.use(morgan(logFormat)) 37 | 38 | // Disable x-powered-by, add custom one 39 | server.disable('x-powered-by') 40 | server.use((req, res, next) => { 41 | // Apply custom header to response 42 | res.setHeader('X-Powered-By', 'UFABC Next') 43 | 44 | // Expose remoteAddress to other middlewares 45 | let ip = ( 46 | req.headers['x-forwarded-for'] || req.connection.remoteAddress || '' 47 | ).split(',')[0].trim() 48 | 49 | req.remoteAddress = ip 50 | 51 | next() 52 | }) 53 | 54 | server.use('*', app.helpers.middlewares.cors) 55 | 56 | 57 | return server 58 | } -------------------------------------------------------------------------------- /app/helpers/mailer/send.spec.js: -------------------------------------------------------------------------------- 1 | // const app = require('@/app') 2 | // const Axios = require('axios') 3 | // const sinon = require('sinon') 4 | // const assert = require('assert') 5 | // const populate = require('@/populate') 6 | 7 | // const send = require('./send') 8 | 9 | // describe('HELPER mailer/send', async function(){ 10 | // let stub 11 | 12 | // beforeEach(async function () { 13 | // await populate({ operation: 'both' }) 14 | // stub = sinon.stub(Axios, 'post') 15 | // }) 16 | 17 | // afterEach(async function () { 18 | // stub.restore() 19 | // }) 20 | 21 | // it('send a email to a single recipient', async function(){ 22 | // const email = { 23 | // recipient: 'email@test.com', 24 | // body: { 25 | // name: 'My name' 26 | // } 27 | // } 28 | 29 | // const sender = { name: 'test', email: 'sender@email.com' } 30 | 31 | // await send(email, sender, 'templateId') 32 | // assert.equal(stub.callCount, 1) 33 | // assert.equal(stub.firstCall.args[0], 'https://api.sendgrid.com/v3/mail/send') 34 | // const params = stub.firstCall.args[1] 35 | // assert.equal(params.personalizations.length, 1) 36 | // assert.equal(params.from.name, sender.name) 37 | // assert.equal(params.from.email, app.config.mailer.EMAIL) 38 | // assert.equal(params.reply_to.email, sender.email) 39 | // }) 40 | 41 | // it('send a email for multiple single recipient', async function(){ 42 | // const email = [{ 43 | // recipient: 'email@test.com', 44 | // body: { name: 'My name' } 45 | // }, { 46 | // recipient: 'email2@test.com' 47 | // }] 48 | 49 | // await send(email, {}, 'templateId') 50 | // assert.equal(stub.callCount, 1) 51 | // assert.equal(stub.firstCall.args[0], 'https://api.sendgrid.com/v3/mail/send') 52 | // const params = stub.firstCall.args[1] 53 | // assert.equal(params.personalizations.length, 2) 54 | // }) 55 | // }) -------------------------------------------------------------------------------- /app/api/disciplinas/sync/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | const Axios = require('axios') 5 | const https = require('https') 6 | 7 | module.exports = async (context = {}) => { 8 | const { mappings } = context.body || {} 9 | const season = app.helpers.season.findSeasonKey() 10 | const Disciplinas = app.models.disciplinas.bySeason(season) 11 | 12 | const instance = Axios.create({ 13 | httpsAgent: new https.Agent({ 14 | rejectUnauthorized: false, 15 | }), 16 | }) 17 | 18 | const disciplinas = await instance.get(app.config.DISCIPLINAS_URL) 19 | const payload = app.helpers.parse 20 | .var2json(disciplinas.data) 21 | .map(app.helpers.transform.disciplinas) 22 | 23 | // get all subjects 24 | const ONE_HOUR = 60 * 60 25 | const subjects = await app.models.subjects 26 | .find({}) 27 | .lean(true) 28 | .cache(ONE_HOUR, 'subjects') 29 | 30 | // check if subjects actually exists before creating the relation 31 | const err = app.helpers.validate.subjects(payload, subjects, mappings) 32 | 33 | if (err.length) { 34 | throw new errors.BadRequest.MissingSubject(_.uniq(err)) 35 | } 36 | 37 | async function updateDisciplinas(disciplina) { 38 | // find and update disciplina 39 | return await Disciplinas.findOneAndUpdate( 40 | { 41 | disciplina_id: disciplina.disciplina_id, 42 | identifier: app.helpers.transform.identifier(disciplina), 43 | }, 44 | disciplina, 45 | { 46 | upsert: true, 47 | new: true, 48 | } 49 | ) 50 | } 51 | 52 | const start = Date.now() 53 | await app.helpers.mapLimit(payload, updateDisciplinas, 15) 54 | 55 | // clear cache for this season 56 | const cacheKey = `todasDisciplinas_${season}` 57 | await app.redis.cache.del(cacheKey) 58 | 59 | return { 60 | status: 'ok', 61 | time: Date.now() - start, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/api/enrollments/sync/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const crypto = require('crypto') 4 | 5 | module.exports = async function (context) { 6 | let { hash, year, quad } = context.body 7 | 8 | app.helpers.validate.throwMissingParameter(['year', 'quad'], context.body) 9 | const season = `${year}:${quad}` 10 | 11 | const Disciplinas = app.models.disciplinas.bySeason(season) 12 | const disciplinas = await Disciplinas.find({}, { 13 | identifier: 1, 14 | subject: 1, 15 | teoria: 1, 16 | pratica: 1, 17 | }).lean({ virtuals: true }) 18 | 19 | const disciplinasMap = new Map([...disciplinas.map(d => [d.identifier, d])]) 20 | 21 | const keys = ['ra', 'year', 'quad', 'disciplina'] 22 | 23 | // parse disciplinas 24 | const enrollments = (await app.helpers.parse.pdf(context.body)) 25 | .map(app.helpers.transform.disciplinas) 26 | .filter(enrollment => enrollment.ra && enrollment.disciplina) 27 | .map(e => _.extend(e, { year, quad })) 28 | .map(e => _.extend(e, { 29 | ...(_.omit(disciplinasMap.get(app.helpers.transform.identifier(e)) || {}, ['id', '_id'])), 30 | identifier: app.helpers.transform.identifier(e, keys), 31 | disciplina_identifier: app.helpers.transform.identifier(e), 32 | })) 33 | 34 | const enrollmentsHash = crypto.createHash('md5').update(JSON.stringify(enrollments)).digest('hex') 35 | if(enrollmentsHash != hash) { 36 | return { 37 | hash: enrollmentsHash, 38 | size: enrollments.length, 39 | sample: _.take(enrollments, 500) 40 | } 41 | } 42 | 43 | const chunks = _.chunk(enrollments, Math.ceil(enrollments.length / 3)) 44 | 45 | app.agenda.now('updateEnrollments', { json: chunks[0] }) 46 | app.agenda.schedule('in 2 minutes', 'updateEnrollments', { json: chunks[1] }) 47 | app.agenda.schedule('in 4 minutes', 'updateEnrollments', { json: chunks[2] }) 48 | 49 | return { published: true } 50 | } 51 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const path = require('path') 3 | 4 | const TAG = '[app]' 5 | const duration = require('./helpers/duration') 6 | 7 | const app = module.exports = {} 8 | 9 | // Loads a single script and saves it's results to app 10 | async function load(app, step, silently) { 11 | // Debug step 12 | let status = `${TAG} load:${chalk.yellow(step)}` 13 | 14 | // Fill with ………… 15 | let fill = chalk.dim('…'.repeat(12 - step.length)) 16 | 17 | // Save result of step 18 | try { 19 | let time = Date.now() 20 | let result = await require('./setup/' + step)(app) 21 | 22 | if (!silently) 23 | console.info(status, fill, chalk.dim(duration(Date.now() - time))) 24 | 25 | // Save output into the name 26 | if (result !== undefined) { 27 | app[step] = result 28 | } 29 | } catch (e) { 30 | if (!silently) 31 | console.error(status, fill, chalk.red('failed')) 32 | console.error(e) 33 | process.exit(1) 34 | } 35 | } 36 | 37 | app.bootstrap = async function bootstrap(pipeline, silently = false) { 38 | let start = Date.now() 39 | 40 | for (var i in pipeline) { 41 | let step = pipeline[i] 42 | 43 | await load(app, step, silently) 44 | } 45 | 46 | let liftDuration = duration(Date.now() - start) 47 | 48 | // Fill with ………… 49 | let step = 'lifted' 50 | let fill = chalk.dim('…'.repeat(17 - step.length)) 51 | 52 | if (!silently) 53 | console.info(TAG, `${chalk.green(step)} ${fill} ${chalk.dim(liftDuration)}`) 54 | } 55 | 56 | /* 57 | * Allow @ to point to root directory in require 58 | */ 59 | var Module = require('module') 60 | var originalRequire = Module.prototype.require 61 | 62 | Module.prototype.require = function (name, ...args) { 63 | if (name.startsWith('@/')) { 64 | let absolute = path.join(__dirname, name.substring(1)) 65 | return originalRequire.apply(this, [absolute, ...args]) 66 | } 67 | 68 | return originalRequire.apply(this, arguments) 69 | } 70 | -------------------------------------------------------------------------------- /app/setup/redis.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis') 2 | const bluebird = require('bluebird') 3 | const Cacheman = require('cacheman') 4 | const requireSmart = require('require-smart') 5 | const Sentry = require('@sentry/node') 6 | const url = require('url') 7 | 8 | module.exports = async (app) => { 9 | const parsedRedis = url.parse(app.config.REDIS_URL, false, true) 10 | 11 | const OPTIONS = { 12 | port: parsedRedis.port, 13 | host: parsedRedis.hostname, 14 | no_ready_check: true, 15 | } 16 | 17 | if (app.config.REDIS_PASSWORD) { 18 | OPTIONS.auth_pass = app.config.REDIS_PASSWORD 19 | } 20 | 21 | // load handlers 22 | const HANDLERS = requireSmart('../redis/handlers', { 23 | skip: [/\..*\.js$/], 24 | }) 25 | 26 | bluebird.promisifyAll(redis.RedisClient.prototype) 27 | bluebird.promisifyAll(redis.Multi.prototype) 28 | 29 | // create publisher connection 30 | const pub = redis.createClient(OPTIONS) 31 | 32 | // create subscribe connection 33 | const sub = redis.createClient(OPTIONS) 34 | 35 | // create handler ? 36 | sub.on('message', async function (channel, message) { 37 | try { 38 | // find handler for this channel and pass message to him 39 | await HANDLERS[channel](JSON.parse(message)) 40 | } catch (e) { 41 | console.log(e) 42 | Sentry.captureException(e) 43 | } 44 | }) 45 | 46 | // subscrive for every handler declared 47 | Object.keys(HANDLERS).forEach((handler) => { 48 | sub.subscribe(handler) 49 | }) 50 | 51 | const cacheOptions = { 52 | engine: 'redis', 53 | port: parsedRedis.port, 54 | host: parsedRedis.hostname, 55 | } 56 | 57 | if (app.config.REDIS_PASSWORD) { 58 | cacheOptions.password = app.config.REDIS_PASSWORD 59 | } 60 | 61 | // return pub sub 62 | return { 63 | publish(handler, payload) { 64 | pub.publish(handler, JSON.stringify(payload)) 65 | }, 66 | sub: sub, 67 | cache: new Cacheman(app.config.CACHE_NAME, cacheOptions), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/helpers/transform/pdfDisciplinas.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | 5 | module.exports = function parsePdfData(payload, mappings) { 6 | if(!mappings) { 7 | mappings = [ 8 | 'codigo', 9 | 'nome', 10 | 'horarios', 11 | 'teoria', 12 | 'pratica' 13 | ] 14 | } 15 | 16 | // group by region 17 | const columns = _(payload) 18 | .map(s => _.extend(s, { pos: parseInt((s.top + s.left) / 10) })) 19 | .groupBy((i) => i.pos) 20 | .value() 21 | 22 | const result = [] 23 | 24 | if(Object.keys(columns).length != mappings.length) { 25 | throw new errors.BadRequest('Mapping does not reflect PDF structure') 26 | } 27 | 28 | var pagesLenghts = [] 29 | 30 | Object.keys(columns).forEach((key) => { 31 | let propertyName = mappings.shift() 32 | let propertyColumn = columns[key] 33 | // keep track of array position, for 34 | // let pageNumber = 0 35 | // let pageLenght = null 36 | let pagePositions = {} 37 | 38 | propertyColumn.forEach((page, i) => { 39 | if(!Object.keys(pagePositions).length) { 40 | page.data.forEach((r, j) => { 41 | pagePositions[`${i}_${resolvePosition(r[0].top)}`] = j * (i + 1) 42 | }) 43 | } 44 | 45 | if(i == 0) { 46 | pagesLenghts.push(page.data.length) 47 | } 48 | 49 | page.data.forEach(row => { 50 | const prop = { [propertyName]: _.map(row, 'text').join(' ') } 51 | if(pagePositions[resolvePosition(row[0].top)] == null) return 52 | var pos = pagePositions[`${i}_${resolvePosition(row[0].top)}`] 53 | result[pos] = result[pos] ? _.extend(result[pos], prop) : prop 54 | }) 55 | }) 56 | 57 | console.log(pagePositions) 58 | }) 59 | 60 | 61 | return result.map(app.helpers.transform.disciplinas).filter(r => { 62 | return r.codigo && r.turma && r.campus && r.turno 63 | }) 64 | } 65 | 66 | function resolvePosition (top) { 67 | return parseInt((top) / 10) 68 | } -------------------------------------------------------------------------------- /app/api/histories/create/func.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | const app = require('@/app') 3 | const errors = require('@/errors') 4 | const moment = require('moment') 5 | 6 | module.exports = async function (context) { 7 | const { 8 | ra, 9 | grade, 10 | mandatory_credits_number, 11 | limited_credits_number, 12 | free_credits_number, 13 | credits_total 14 | } = context.body 15 | let { curso } = context.body 16 | 17 | if(!ra) { 18 | throw new errors.BadRequest.MissingParameter('ra') 19 | } 20 | 21 | if(curso == 'Bacharelado em CIências e Humanidades') { 22 | curso = 'Bacharelado em Ciências e Humanidades' 23 | context.body.curso = 'Bacharelado em Ciências e Humanidades' 24 | } 25 | 26 | if(curso && grade) { 27 | const graduation = { 28 | locked: false, 29 | name: curso, 30 | grade: grade, 31 | } 32 | 33 | if(mandatory_credits_number > 0) { 34 | graduation.mandatory_credits_number = mandatory_credits_number 35 | } 36 | 37 | if(limited_credits_number > 0) { 38 | graduation.limited_credits_number = limited_credits_number 39 | } 40 | 41 | if(free_credits_number > 0){ 42 | graduation.free_credits_number = free_credits_number 43 | } 44 | 45 | if(credits_total > 0){ 46 | graduation.credits_total = credits_total 47 | } 48 | 49 | const doc = await app.models.graduation.findOne({ curso: curso, grade: grade }).lean(true) 50 | if(!doc || !doc.locked){ 51 | await app.models.graduation.findOneAndUpdate({ 52 | curso: curso, 53 | grade: grade, 54 | }, graduation, { 55 | upsert: true, 56 | new: true 57 | }) 58 | } 59 | } 60 | 61 | await app.models.histories.findOneAndUpdate({ 62 | ra: ra, 63 | // only let update history once per hour 64 | // since this creates too much propagation on enrollments 65 | updatedAt: { $lte: moment().subtract(1, 'hour').toDate() } 66 | }, context.body, { 67 | upsert: true, 68 | new: true 69 | }) 70 | 71 | return { 72 | ok: Date.now() 73 | } 74 | } -------------------------------------------------------------------------------- /app/api/disciplinas/teachers/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const crypto = require('crypto') 4 | 5 | module.exports = async function (context) { 6 | let { mappings, hash } = context.body 7 | 8 | const season = context.body.season || app.helpers.season.findSeasonKey() 9 | const Disciplinas = app.models.disciplinas.bySeason(season) 10 | 11 | // get all teachers 12 | const ONE_HOUR = 60 * 60 13 | const teachers = await app.models.teachers.find({}).lean(true).cache(ONE_HOUR, 'teachers') 14 | 15 | // parse disciplinas 16 | let disciplinas = (await app.helpers.parse.pdf(context.body)) 17 | .map(app.helpers.transform.disciplinas) 18 | .map(d => _.merge(d, { 19 | teoria: app.helpers.transform.resolveProfessor(d.teoria, teachers, mappings), 20 | pratica: app.helpers.transform.resolveProfessor(d.pratica, teachers, mappings), 21 | })) 22 | 23 | // check which teachers from pdf or xls are missing 24 | const errors = app.helpers.validate.teachers(disciplinas) 25 | 26 | // create hash 27 | const disciplinaHash = crypto.createHash('md5').update(JSON.stringify(disciplinas)).digest('hex') 28 | if(disciplinaHash != hash) { 29 | return { 30 | hash: disciplinaHash, 31 | payload: disciplinas, 32 | errors: _.uniq(errors) 33 | } 34 | } 35 | 36 | const identifierKeys = ['disciplina', 'turno', 'campus', 'turma'] 37 | 38 | async function updateDisciplinas(disciplina){ 39 | // find and update disciplina 40 | return await Disciplinas.findOneAndUpdate({ 41 | identifier: app.helpers.transform.identifier(disciplina, identifierKeys) 42 | }, { 43 | teoria: _.get(disciplina.teoria, '_id', null), 44 | pratica: _.get(disciplina.pratica, '_id', null) 45 | }, { 46 | new: true, 47 | }) 48 | } 49 | 50 | const start = Date.now() 51 | disciplinas = await app.helpers.mapLimit(disciplinas, updateDisciplinas, 15) 52 | 53 | // clear cache for this season 54 | const cacheKey = `todasDisciplinas_${season}` 55 | await app.redis.cache.del(cacheKey) 56 | 57 | return { 58 | status: 'ok', 59 | time: Date.now() - start, 60 | } 61 | } -------------------------------------------------------------------------------- /app/models/enrollments.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const Schema = require('mongoose').Schema 4 | 5 | const Model = module.exports = Schema({ 6 | year: { 7 | type: Number, 8 | required: true, 9 | }, 10 | quad: { 11 | type: Number, 12 | required: true 13 | }, 14 | identifier: String, 15 | ra: Number, 16 | disciplina: String, 17 | subject: { 18 | type: Schema.Types.ObjectId, 19 | ref: 'subjects' 20 | }, 21 | campus: String, 22 | turno: String, 23 | turma: String, 24 | teoria: { 25 | type: Schema.Types.ObjectId, 26 | ref: 'teachers' 27 | }, 28 | pratica: { 29 | type: Schema.Types.ObjectId, 30 | ref: 'teachers' 31 | }, 32 | mainTeacher: { 33 | type: Schema.Types.ObjectId, 34 | ref: 'teachers' 35 | }, 36 | comments: [{ 37 | type: String, 38 | enum: ['teoria', 'pratica'], 39 | }], 40 | // vem do portal 41 | conceito: String, 42 | creditos: Number, 43 | ca_acumulado: Number, 44 | cr_acumulado: Number, 45 | cp_acumulado: Number, 46 | }) 47 | 48 | Model.index({ identifier: 1, ra: 1 }) 49 | Model.index({ ra: 1 }) 50 | Model.index({ conceito: 1 }) 51 | Model.index({ mainTeacher: 1, subject: 1, cr_acumulado: 1, conceito: 1 }) 52 | 53 | function pre(doc) { 54 | if('teoria' in doc || 'pratica' in doc ) { 55 | 56 | doc.mainTeacher = _.get(doc, 'teoria._id', doc.teoria) || _.get(doc, 'pratica._id', doc.pratica) 57 | } 58 | } 59 | 60 | Model.pre('save', async function () { 61 | pre(this) 62 | 63 | await addEnrollmentToGroup(this) 64 | }) 65 | 66 | Model.pre('findOneAndUpdate', function () { 67 | pre(this._update) 68 | }) 69 | 70 | async function addEnrollmentToGroup(doc) { 71 | /* 72 | * If is a new enrollment, must create a new 73 | * group or insert doc.ra in group.users 74 | */ 75 | const Groups = app.models.groups 76 | 77 | if (doc.mainTeacher && doc.isNew) { 78 | await Groups.update( 79 | { 80 | disciplina: doc.disciplina, 81 | season: doc.season, 82 | mainTeacher: doc.mainTeacher 83 | }, 84 | { 85 | $push: { users: doc.ra } 86 | }, 87 | { 88 | upsert: true 89 | } 90 | ) 91 | } 92 | } -------------------------------------------------------------------------------- /app/api/stats/usage/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | 3 | module.exports = async function getUsageStats(context) { 4 | let { season } = context.query 5 | if(!season) season = app.helpers.season.findSeasonKey() 6 | 7 | const Alunos = app.models.alunos.bySeason(season) 8 | const Disciplinas = app.models.disciplinas.bySeason(season) 9 | const teachersCount = [ 10 | { $group: { _id: null, teoria: { $addToSet: '$teoria'}, pratica: { $addToSet: '$pratica'} }}, 11 | { $project: { teachers: { $setUnion: [ '$teoria', '$pratica' ] } } }, 12 | { $unwind: { path: '$teachers', preserveNullAndEmptyArrays: true } }, 13 | { $group: { _id: null, total: { $sum:1 } } }, 14 | { $project: { _id: 0 } } 15 | ] 16 | 17 | const subjectsCount = [ 18 | { $group: { _id: null, total: { $sum: 1 } } }, 19 | { $project: { _id: 0 } } 20 | ] 21 | 22 | // check if we are dealing with previous data or current 23 | const isPrevious = await Disciplinas.count({ before_kick: { $exists: true, $ne: [] }}) 24 | const dataKey = isPrevious ? '$before_kick' : '$alunos_matriculados' 25 | const alunosCount = [ 26 | { $unwind: dataKey }, 27 | { $group: { _id: null, alunos: { $addToSet: dataKey} }}, 28 | { $unwind:'$alunos' }, 29 | { $group: { _id: null, total: { $sum:1 } }}, 30 | ] 31 | 32 | const disciplinaStats = await Disciplinas.aggregate([{ 33 | $facet: { 34 | teachers: teachersCount, 35 | totalAlunos: alunosCount, 36 | subjects: subjectsCount 37 | }, 38 | }, { 39 | $addFields: { 40 | teachers: { $ifNull: [{ $arrayElemAt: [ '$teachers.total', 0 ] }, 0] }, 41 | totalAlunos: { $ifNull: [{ $arrayElemAt: [ '$totalAlunos.total', 0 ] }, 0] }, 42 | subjects: { $ifNull: [{ $arrayElemAt: [ '$subjects.total', 0 ] }, 0] }, 43 | } 44 | }]) 45 | 46 | const otherStats = { 47 | users: await app.models.users.count({}), 48 | currentAlunos: await Alunos.count({}), 49 | comments: await app.models.comments.count({}), 50 | enrollments: await app.models.enrollments.count({ conceito: { $in: ['A', 'B', 'C', 'D', 'O', 'F'] }}) 51 | } 52 | 53 | return Object.assign({}, disciplinaStats[0], otherStats) 54 | } -------------------------------------------------------------------------------- /app/api/disciplinas/teachers/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const assert = require('assert') 3 | const populate = require('@/populate') 4 | const sinon = require('sinon') 5 | const Axios = require('axios') 6 | 7 | const func = require('./func') 8 | const rule = require('./rule') 9 | 10 | const sync = require('@/api/disciplinas/sync/func') 11 | 12 | describe('PUT /v1/disciplinas/teachers', function() { 13 | var context 14 | 15 | beforeEach(async function () { 16 | context = { 17 | query: {}, 18 | body: { 19 | // disciplinas: pdfData 20 | }, 21 | } 22 | }) 23 | 24 | describe('func', function () { 25 | beforeEach(async function () { 26 | await populate({ operation : 'both' }) 27 | 28 | let file = app.helpers.test.getDisciplinas() 29 | file.data = app.helpers.test.sample(file.data) 30 | let stub = sinon.stub(Axios, 'get').returns(file) 31 | await sync() 32 | stub.restore() 33 | }) 34 | 35 | xit('sync disciplines teachers', async function () { 36 | let resp = await func(context) 37 | 38 | context.body.hash = resp.hash 39 | 40 | resp = await func(context) 41 | 42 | // check if is scoped by season 43 | let Disciplinas = app.models.disciplinas.bySeason('2018:2') 44 | assert.equal(await Disciplinas.findOne({}), null) 45 | 46 | const season = app.helpers.season.findSeasonKey() 47 | Disciplinas = app.models.disciplinas.bySeason(season) 48 | 49 | // assert.equal(await Disciplinas.countDocuments({}), 200) 50 | let disciplina = await Disciplinas.find({ teoria : { $ne: null } }).lean(true) 51 | assert.equal(disciplina.length, 2) 52 | }) 53 | }) 54 | 55 | describe('rule', function () { 56 | it('works if has permissions', async function () { 57 | context.query.access_key = app.config.ACCESS_KEY 58 | await rule(context) 59 | }) 60 | 61 | it('throws if permission is wrong', async function () { 62 | context.query.access_key = 'wrong permissions' 63 | await assertFuncThrows('Forbidden', rule, context) 64 | }) 65 | 66 | it('throws if permissions is missing', async function () { 67 | await assertFuncThrows('Forbidden', rule, context) 68 | }) 69 | }) 70 | }) -------------------------------------------------------------------------------- /app/api/matriculas/sync/func.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const app = require('@/app') 3 | const Axios = require('axios') 4 | const https = require('https') 5 | 6 | module.exports = async(context) => { 7 | const season = app.helpers.season.findSeasonKey() 8 | const Disciplinas = app.models.disciplinas.bySeason(season) 9 | 10 | const { operation } = context.query 11 | 12 | // map possible operations 13 | const operationField = { 14 | 'before_kick': 'before_kick', 15 | 'after_kick' : 'after_kick', 16 | 'sync': 'alunos_matriculados' 17 | }[operation] || 'alunos_matriculados' 18 | 19 | // check if we are doing a sync operation 20 | // update current enrolled students 21 | const isSync = operationField == 'alunos_matriculados' 22 | 23 | const instance = Axios.create({ 24 | httpsAgent: new https.Agent({ 25 | rejectUnauthorized: false 26 | }) 27 | }) 28 | 29 | // fetch matriculas and parse into an undestandable way 30 | const matriculas = await instance.get(app.config.MATRICULAS_URL) 31 | let payload = app.helpers.parse.var2json(matriculas.data) 32 | payload = app.helpers.transform.transformMatriculas(payload) 33 | 34 | async function updateAlunosMatriculados(id, payload) { 35 | const cacheKey = `disciplina_${season}_${id}` 36 | // only get cache result if we are doing a sync operation 37 | const cachedMatriculas = isSync 38 | ? await app.redis.cache.get(cacheKey) 39 | : {} 40 | 41 | // only update disciplinas that matriculas has changed 42 | if(_.isEqual(cachedMatriculas, payload[id])){ 43 | return cachedMatriculas 44 | } 45 | 46 | // find and update disciplina 47 | const saved = await Disciplinas.findOneAndUpdate({ 48 | disciplina_id: id 49 | }, { [operationField]: payload[id] }, { 50 | upsert: true, 51 | new: true 52 | }) 53 | 54 | // save matriculas for this disciplina on cache if is sync operation 55 | isSync ? await app.redis.cache.set(cacheKey, payload[id]) : null 56 | return saved 57 | } 58 | 59 | const start = Date.now() 60 | await app.helpers.mapLimit(Object.keys(payload), updateAlunosMatriculados, 15, payload) 61 | 62 | return { 63 | status: 'ok', 64 | time: Date.now() - start, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/api/students/create/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const assert = require('assert') 3 | const populate = require('@/populate') 4 | const sinon = require('sinon') 5 | const Axios = require('axios') 6 | 7 | const func = require('./func') 8 | const sync = require('@/api/disciplinas/sync/func') 9 | 10 | describe('POST /v1/students', function () { 11 | let context 12 | let axiosGetStub 13 | let axiosInstanceStub 14 | 15 | const season = app.helpers.season.findSeasonKey() 16 | 17 | beforeEach(async function () { 18 | await populate({ 19 | operation: 'both', 20 | only: ['disciplinas', 'subjects', 'alunos'], 21 | }) 22 | 23 | context = { 24 | query: {}, 25 | body: { 26 | aluno_id: 3000, 27 | cursos: [ 28 | { 29 | cr: 2, 30 | cp: 0.5, 31 | quads: 3, 32 | curso: 'Bacharelado em Ciência e Tecnologia', 33 | }, 34 | { 35 | cr: 2, 36 | cp: 0.6, 37 | quads: 3, 38 | curso: 'Bacharelado e Ciências da Computação', 39 | }, 40 | ], 41 | }, 42 | } 43 | 44 | let file = app.helpers.test.getDisciplinas() 45 | file.data = app.helpers.test.sample(file.data, 1) 46 | axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) 47 | axiosGetStub = sinon.stub(Axios, 'get').returns(file) 48 | await sync() 49 | }) 50 | 51 | afterEach(function () { 52 | axiosInstanceStub.restore() 53 | axiosGetStub.restore() 54 | }) 55 | 56 | describe('func', function () { 57 | xit('returns a complete list of disciplinas', async function () { 58 | let resp = await func(context) 59 | 60 | assert.equal(resp.aluno_id, context.body.aluno_id) 61 | assert.equal(resp.season, season) 62 | }) 63 | 64 | it('returns if kicks are already in place', async function () { 65 | const disciplina = await app.models.disciplinas.findOne({}) 66 | disciplina.before_kick = [1, 2, 3] 67 | await disciplina.save() 68 | 69 | let resp = await func(context) 70 | assert(!resp) 71 | }) 72 | 73 | it('throws if missing aluno_id', async function () { 74 | delete context.body.aluno_id 75 | await assertFuncThrows('MissingParameter', func, context) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /app/helpers/season/findIdeais.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const findIdeais = require('./findIdeais') 3 | 4 | describe('helpers.season.findIdeais', function() { 5 | 6 | it('Should return first quad courses', function () { 7 | const expectedCourses = [ 8 | 'BCM0506-15', // COMUNICACAO E REDES 9 | 'BCJ0203-15', // ELETROMAG 10 | 'BIN0406-15', // IPE 11 | 'BCN0405-15', // IEDO 12 | 'BIR0004-15', // EPISTEMOLOGICAS 13 | 'BHO0102-15', // DESENVOL. E SUSTE. 14 | 'BHO0002-15', // PENSA. ECONOMICO 15 | 'BHP0201-15', // TEMAS E PROBLEMAS 16 | 'BHO0101-15', // ESTADO E RELA 17 | 'BIR0603-15', // CTS 18 | 'BHQ0003-15', // INTEPRE. BRASIL 19 | 'BHQ0001-15', // IDENT.E CULTURA 20 | ] 21 | const date = new Date('2021-04-10') 22 | const func = findIdeais(date) 23 | 24 | expectedCourses.forEach(function (course,index) { 25 | assert.equal(course, func[index]) 26 | }) 27 | }) 28 | 29 | it('Should return second quad courses', function () { 30 | const expectedCourses = [ 31 | 'BCM0504-15', // NI 32 | 'BCN0404-15', // GA 33 | 'BCN0402-15', // FUV 34 | 'BCJ0204-15', // FEMEC 35 | 'BCL0306-15', // BIODIVERSIDADE 36 | 'BCK0103-15', // QUANTICA 37 | 'BCL0308-15', // BIOQUIMICA 38 | 'BIQ0602-15', // EDS 39 | 'BHO1335-15', // FORMACAO SISTEMA INTERNACIONAL 40 | 'BHO1101-15', // INTRODUCAO A ECONOMIA 41 | 'BHO0001-15', // INTRODUCAO AS HUMANIDADES 42 | 'BHP0202-15', // PENSAMENTO CRITICO 43 | ] 44 | const date = new Date('2021-07-10') 45 | const func = findIdeais(date) 46 | 47 | expectedCourses.forEach(function (course,index) { 48 | assert.equal(course, func[index]) 49 | }) 50 | }) 51 | 52 | it('Should return third quad courses', function () { 53 | const expectedCourses = [ 54 | 'BCJ0205-15', // FETERM 55 | 'BCM0505-15', // PI 56 | 'BCN0407-15', // FVV 57 | 'BCL0307-15', // TQ 58 | 'BCK0104-15', // IAM 59 | 'BIR0603-15', // CTS 60 | 'BHP0001-15', // ETICA E JUSTICA 61 | 'BHQ0301-15', // TERRITORIO E SOCIEDADE 62 | // ESTUDO ÉTNICOS RACIAIS 63 | ] 64 | const date = new Date('2021-11-21') 65 | const func = findIdeais(date) 66 | 67 | expectedCourses.forEach(function (course,index) { 68 | assert.equal(course, func[index]) 69 | }) 70 | }) 71 | }) -------------------------------------------------------------------------------- /app/helpers/parse/error.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const errors = require('@/errors') 3 | const parseError = require('./error') 4 | 5 | describe('helpers/parse/error', async function () { 6 | const defaultError = 'Some error' 7 | const expectedErrors = [ 8 | { 9 | expectedName: 'BadRequest', 10 | expectedType: 'BadRequest', 11 | expectedStatus: 400, 12 | }, 13 | { 14 | expectedName: 'Unauthorized', 15 | expectedType: 'Unauthorized', 16 | expectedStatus: 401, 17 | }, 18 | { 19 | expectedName: 'Forbidden', 20 | expectedType: 'Forbidden', 21 | expectedStatus: 403, 22 | }, 23 | { 24 | expectedName: 'NotFound', 25 | expectedType: 'NotFound', 26 | expectedStatus: 404, 27 | }, 28 | { 29 | expectedName: 'Conflict', 30 | expectedType: 'Conflict', 31 | expectedStatus: 409, 32 | }, 33 | { 34 | expectedName: 'Unprocessable', 35 | expectedType: 'Unprocessable', 36 | expectedStatus: 422, 37 | }, 38 | ] 39 | describe('with an mapped error', async function () { 40 | for (const expectedError of expectedErrors) { 41 | it(`parse an ${expectedError.expectedName}`, async function () { 42 | const error = new errors[expectedError.expectedName](defaultError) 43 | const parsedError = parseError(error) 44 | 45 | assert.equal(defaultError, parsedError.error) 46 | assert.equal(expectedError.expectedName, parsedError.name) 47 | assert.equal(expectedError.expectedType, parsedError.type) 48 | assert.equal(expectedError.expectedStatus, parsedError.status) 49 | }) 50 | } 51 | }) 52 | it('with an different class error', async function () { 53 | const error = { 54 | type: 'Unprocessable', 55 | status: 422, 56 | } 57 | 58 | const parsedError = parseError(error) 59 | 60 | assert.equal(error.type, parsedError.type) 61 | assert.equal(error.status, parsedError.status) 62 | }) 63 | it('with an unexpected error', async function () { 64 | const expectedError = 'Some error' 65 | const error = new Error(expectedError) 66 | 67 | const parsedError = parseError(error) 68 | 69 | assert.equal(parsedError.status, 500) 70 | assert.equal(parsedError.name, 'FatalError') 71 | assert.equal(parsedError.type, 'Error') 72 | assert.equal(parsedError.error, expectedError) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /app/api/disciplinas/disciplinaId/kicks/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const assert = require('assert') 3 | const populate = require('@/populate') 4 | const func = require('./func') 5 | const rule = require('./rule') 6 | 7 | describe('GET /v1/disciplinas/:disciplina-id/kicks', function () { 8 | var context 9 | 10 | const season = app.helpers.season.findSeasonKey() 11 | const Disciplinas = app.models.disciplinas.bySeason(season) 12 | 13 | beforeEach(async function () { 14 | await populate({ operation: 'both', only: ['disciplinas', 'alunos'] }) 15 | 16 | context = { 17 | query: {}, 18 | params: {}, 19 | } 20 | }) 21 | 22 | describe('func', function () { 23 | it('returns a complete list of disciplinas', async function () { 24 | let disciplina = await Disciplinas.create({ 25 | disciplina_id: 100, 26 | identifier: 'some identifier', 27 | alunos_matriculados: [12263], 28 | turno: 'diurno', 29 | before_kick: [10523, 1504], 30 | obrigatorias: [6, 20], 31 | after_kick: [1504], 32 | }) 33 | 34 | context.params.disciplinaId = disciplina.disciplina_id 35 | let resp = await func(context) 36 | 37 | assert.equal(resp.length, 2) 38 | assert(resp.every((s) => 'kicked' in s)) 39 | }) 40 | 41 | it('allows custom query method', async function () { 42 | context.query.sort = ['cr', 'cp'] 43 | 44 | let disciplina = await Disciplinas.create({ 45 | disciplina_id: 100, 46 | identifier: 'some identifier', 47 | alunos_matriculados: [12263, 10523], 48 | turno: 'noturno', 49 | obrigatorias: [6, 20], 50 | }) 51 | 52 | context.params.disciplinaId = disciplina.disciplina_id 53 | let resp = await func(context) 54 | 55 | assert.equal(resp.length, 2) 56 | assert(resp.every((s) => !('kicked' in s))) 57 | }) 58 | }) 59 | 60 | describe('rule', function () { 61 | it('throws if aluno_id is not passed', async function () { 62 | await assertFuncThrows('Forbidden', rule, context) 63 | }) 64 | 65 | it('throws if aluno_id is not on database', async function () { 66 | context.query.aluno_id = 1 67 | await assertFuncThrows('Forbidden', rule, context) 68 | }) 69 | 70 | it('throws if aluno_id is not on database', async function () { 71 | context.query.aluno_id = 12263 72 | await rule(context) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /app/api/enrollments/create/spec.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const populate = require('@/populate') 3 | 4 | const func = require('./func') 5 | const rule = require('./rule') 6 | 7 | describe('POST /v1/enrollments', function() { 8 | var context, enrollments 9 | 10 | beforeEach(async function () { 11 | enrollments = JSON.parse(app.helpers.test.getFixture('enrollments.json').data) 12 | 13 | context = { 14 | query: {}, 15 | body: { 16 | season: '2018:1', 17 | enrollments, 18 | subjectMappings: { 19 | 'Introdução aos Processos de' : 'Introdução aos Processos de Fabricação Metal - Mecânico' 20 | } 21 | }, 22 | } 23 | }) 24 | 25 | describe('func', function () { 26 | 27 | beforeEach(async function () { 28 | await populate({ operation : 'both', only: ['teachers', 'subjects', 'enrollments'] }) 29 | }) 30 | 31 | xit('create enrollments', async function () { 32 | let resp = await func(context) 33 | 34 | context.body.hash = resp.hash 35 | 36 | resp = await func(context) 37 | 38 | //await populate({ operation: 'both', only: ['histories']}) 39 | 40 | // let file = app.helpers.test.getDisciplinas() 41 | // file.data = app.helpers.test.sample(file.data, 200) 42 | // let stub = sinon.stub(Axios, 'get').returns(file) 43 | 44 | // let resp = await func() 45 | 46 | // // check if is scoped by season 47 | // let Disciplinas = app.models.disciplinas.bySeason('2018:2') 48 | // assert.equal(await Disciplinas.findOne({}), null) 49 | 50 | // const season = app.helpers.season.findSeasonKey() 51 | // Disciplinas = app.models.disciplinas.bySeason(season) 52 | 53 | // assert.equal(await Disciplinas.countDocuments({}), 200) 54 | // let disciplina = await Disciplinas.findOne({ disciplina_id : 2538 }).lean(true) 55 | // assert.equal(disciplina.disciplina_id, 2538) 56 | 57 | // stub.restore() 58 | }) 59 | }) 60 | 61 | describe('rule', function () { 62 | it('works if has permissions', async function () { 63 | context.query.access_key = app.config.ACCESS_KEY 64 | await rule(context) 65 | }) 66 | 67 | it('throws if permission is wrong', async function () { 68 | context.query.access_key = 'wrong permissions' 69 | await assertFuncThrows('Forbidden', rule, context) 70 | }) 71 | 72 | it('throws if permissions is missing', async function () { 73 | await assertFuncThrows('Forbidden', rule, context) 74 | }) 75 | }) 76 | }) -------------------------------------------------------------------------------- /app/helpers/parse/pickFields.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const mongoose = require('mongoose') 3 | 4 | function pickIt(obj, fields) { 5 | if (_.isNil(obj)) { 6 | return {} 7 | } 8 | 9 | if (!_.isArray(fields)) { 10 | fields = [String(fields)] 11 | } 12 | 13 | if (_.isArray(obj)) { 14 | return obj.map(el => pickIt(el, fields)) 15 | } 16 | 17 | let fieldGroups = _.groupBy((fields || []).sort().map(fieldPath), path => path[0]) 18 | 19 | let out = {} 20 | 21 | for (let base in fieldGroups) { 22 | let fields = fieldGroups[base] 23 | 24 | let value = obj[base] 25 | 26 | if (_.isUndefined(value)) { 27 | // Skip 28 | } else if (fields.some(el => el.length == 1)) { 29 | out[base] = value 30 | } else if (_.isObject(value) || _.isArray(value) ) { 31 | out[base] = pickIt(value, fields.map(path => path.slice(1))) 32 | } else if (!_.isUndefined(value)){ 33 | out[base] = value 34 | } 35 | } 36 | return out 37 | } 38 | 39 | function fieldPath(field) { 40 | return _.isArray(field) ? field : field.split('.') 41 | } 42 | 43 | // this function receives an object and the fields it should pick 44 | // it walks recursively the fields to filter 45 | module.exports = function pickFields(obj, fields, payload) { 46 | payload = payload || {} 47 | 48 | // make sure we are not converting a cyclic structure 49 | JSON.stringify(obj) 50 | 51 | // parse object Id to string 52 | if (_.isObject(obj) && mongoose.isObjectIdOrHexString(obj)) { 53 | return obj.toString() 54 | } 55 | 56 | // if passing an array 57 | if(_.isArray(obj)) { 58 | return _.map(obj, a => pickFields(a, fields, payload)) 59 | } 60 | 61 | if(_.isObject(obj) && Object.keys(obj).length) { 62 | // filter the object first 63 | if(obj.toObject instanceof Function) obj = obj.toObject({ virtuals: true }) 64 | 65 | const pickedFields = _.get(fields, 'public', null) || fields 66 | let resolvedFields = _.isFunction(pickedFields) ? pickedFields(payload) : pickedFields 67 | 68 | // get all keys 69 | if((resolvedFields || []).includes('*') || resolvedFields == null) { 70 | resolvedFields = Object.keys(obj) 71 | } 72 | 73 | // filter fields 74 | obj = pickIt(obj, resolvedFields || []) 75 | 76 | // iterate on object keys 77 | Object.keys(obj).forEach(key => { 78 | const nextFields = _.get(fields, key, null) 79 | 80 | // if it's an array, filter each item of the array 81 | if(_.isArray(obj[key])) { 82 | obj[key] = obj[key].map(item => pickFields(item, nextFields, payload)) 83 | // if not, only filter 84 | } else { 85 | obj[key] = pickFields(obj[key], nextFields, payload) 86 | } 87 | }) 88 | } 89 | 90 | return obj 91 | } -------------------------------------------------------------------------------- /app/api/comment/create/spec.js: -------------------------------------------------------------------------------- 1 | // const app = require('@/app') 2 | // const func = require('./func') 3 | // const assert = require('assert') 4 | // const populate = require('@/populate') 5 | 6 | // describe('POST /v1/comments', async function () { 7 | // let models 8 | // let context 9 | // let enrollment 10 | // beforeEach(async function () { 11 | // models = await populate({ 12 | // operation: 'both', 13 | // only: ['enrollments', 'teachers', 'subjects', 'comments'], 14 | // }) 15 | 16 | // enrollment = models.enrollments[0] 17 | // context = { 18 | // body: { 19 | // enrollment: enrollment._id, 20 | // comment: 'Some comment', 21 | // type: 'pratica', 22 | // }, 23 | // } 24 | // }) 25 | // describe('func', async function () { 26 | // describe('with valid params', async function () { 27 | // it('create and return a filtered comment', async function () { 28 | // const resp = await func(context) 29 | 30 | // assert(resp._id) 31 | // assert.equal(resp.comment, context.body.comment) 32 | // assert(resp.createdAt) 33 | // assert(new Date() > resp.createdAt) 34 | // assert.equal(resp.enrollment._id, context.body.enrollment) 35 | // assert.equal(resp.subject, enrollment.subject) 36 | // assert.equal(resp.teacher, enrollment.mainTeacher) 37 | // assert.notEqual(resp.ra, enrollment.ra) 38 | 39 | // const Comment = app.models.comments 40 | // const comment = await Comment.findOne({ _id: resp._id }) 41 | // assert(comment) 42 | // }) 43 | // }) 44 | // describe('with invalid params', async function () { 45 | // it('should throw if enrollment is missing', async function () { 46 | // delete context.body.enrollment 47 | // await assertFuncThrows('MissingParameter', func, context) 48 | // }) 49 | // it('should throw if comment is missing', async function () { 50 | // delete context.body.comment 51 | // await assertFuncThrows('MissingParameter', func, context) 52 | // }) 53 | // it('should throw if type is missing', async function () { 54 | // delete context.body.type 55 | // await assertFuncThrows('MissingParameter', func, context) 56 | // }) 57 | // it('should throw if enrollment is invalid', async function () { 58 | // // Invalid enrollment id 59 | // context.body.enrollment = models.comments[0]._id 60 | // await assertFuncThrows('BadRequest', func, context) 61 | // }) 62 | // it('should throw if is a duplicated comment', async function () { 63 | // await func(context) 64 | // await assertFuncThrows('BadRequest', func, context) 65 | // }) 66 | // }) 67 | // }) 68 | // }) 69 | -------------------------------------------------------------------------------- /app/models/users.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const Schema = require('mongoose').Schema 3 | const jwt = require('jsonwebtoken') 4 | const app = require('@/app') 5 | const errors = require('@/errors') 6 | 7 | const Model = module.exports = Schema({ 8 | oauth: { 9 | facebook: String, 10 | emailFacebook: String, 11 | google: String, 12 | emailGoogle: String, 13 | email: String, 14 | picture: String 15 | }, 16 | ra: { 17 | type: Number, 18 | unique: true, 19 | partialFilterExpression: { ra: { $exists: true } } 20 | }, 21 | email: { 22 | type: String, 23 | validate: { 24 | validator: v => v.indexOf('ufabc.edu.br') != -1, 25 | message: props => `${props.value} não é um e-mail válido` 26 | }, 27 | unique: true, 28 | partialFilterExpression: { email: { $exists: true } } 29 | }, 30 | confirmed: { 31 | type: Boolean, 32 | default: false, 33 | }, 34 | active: { 35 | type: Boolean, 36 | default: true 37 | }, 38 | permissions: [String], 39 | 40 | devices: [{ 41 | phone: String, 42 | token: { 43 | type: String, 44 | required: true 45 | }, 46 | deviceId: { 47 | type: String, 48 | required: true 49 | } 50 | }] 51 | }) 52 | 53 | Model.virtual('isFilled').get(function () { 54 | return this.ra && this.email 55 | }) 56 | 57 | Model.method('addDevice', function (device) { 58 | this.devices.unshift(device) 59 | this.devices = _.uniqBy(this.devices, 'deviceId') 60 | }) 61 | 62 | Model.method('removeDevice', function (deviceId) { 63 | this.devices = _.remove(this.devices, { deviceId }) 64 | }) 65 | 66 | Model.method('sendNotification', async function (title, body) { 67 | const sendNotification = app.helpers.notification.sendNotification 68 | 69 | const devicesTokens = this.devices.map(device => device.token) 70 | 71 | await sendNotification(title, body, devicesTokens) 72 | }) 73 | 74 | Model.method('generateJWT', function () { 75 | return jwt.sign(_.pick(this, [ 76 | '_id', 77 | 'ra', 78 | 'confirmed', 79 | 'email', 80 | 'permissions' 81 | ]), app.config.JWT_SECRET) 82 | }) 83 | 84 | Model.method('sendConfirmation', async function () { 85 | // !app.config.isTest && app.agenda.now('sendConfirmation', this.toObject({ virtuals: true })) 86 | app.agenda.now('sendConfirmation', this.toObject({ virtuals: true })) 87 | }) 88 | 89 | Model.pre('save', async function (email) { 90 | if (this.email === email) { 91 | throw new errors.Conflict('E-mail já cadastrado, realize seu login via Facebook ou entre em contato com nosso suporte') 92 | } 93 | 94 | 95 | if ((this.isFilled && !this.confirmed)) { 96 | this.sendConfirmation() 97 | } 98 | }) 99 | 100 | Model.index({ ra: -1 }) -------------------------------------------------------------------------------- /app/test.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob') 2 | const chalk = require('chalk') 3 | const Mocha = require('mocha') 4 | 5 | const app = require('./app') 6 | 7 | // this function receives another function and test it to see if it's throws the expected error 8 | global.assertFuncThrows = async (expectedError, fun, ...context) => { 9 | try { 10 | await fun(...context) 11 | throw new Error(`Should have thrown ${expectedError}, but succeded`) 12 | } catch (e) { 13 | if (e.name != expectedError) { 14 | throw e 15 | } 16 | } 17 | } 18 | 19 | async function test() { 20 | console.info() 21 | console.info('Bootstrapping basic components...') 22 | console.info() 23 | 24 | await app.bootstrap([ 25 | 'package', 26 | 'config', 27 | 'helpers', 28 | 'mongo', 29 | 'models', 30 | 'redis', 31 | 'agenda', 32 | 'redirect', 33 | ]) 34 | 35 | // Security Checks before starting tests 36 | if ( 37 | app.config.MONGO_URL && 38 | !app.config.MONGO_URL.includes('localhost') && 39 | app.config.MONGO_URL.length > 45 40 | ) { 41 | throw new Error( 42 | chalk.red( 43 | 'You cannot test on a non local MongoDB.\n' + 44 | 'It would have cleaned ALL THE DATA and fucked up everything.\n' + 45 | 'BE FUCKING CAREFULL WITH PRODUCTION!!!!\n' + 46 | 'Change MONGO_URI to default localhost before testing' 47 | ) 48 | ) 49 | } 50 | 51 | let mocha = new Mocha({ timeout: 12000 }) 52 | 53 | // Find files for testing 54 | let testFiles = glob.sync('**/*spec.js', { 55 | ignore: ['node_modules/**'], 56 | }) 57 | 58 | // Add files to mocha 59 | testFiles.forEach(mocha.addFile.bind(mocha)) 60 | 61 | // Default passes to 0, in case there is only syncronous tests running 62 | let stats = { passes: 0 } 63 | 64 | // Run tests and save stats 65 | stats = mocha.run(function (failures) { 66 | // Exits if less then 10 tests are being run (in case .only is used) 67 | if (!failures && stats.passes < 3) { 68 | console.error(chalk.red('Did not performed full testing:')) 69 | console.error(chalk.red(' - Remove `describe.only` on tests')) 70 | console.log() 71 | return process.exit(1) 72 | } 73 | 74 | process.exit(!!failures) 75 | }).stats 76 | 77 | // creating some helpers 78 | Object.defineProperty(Array.prototype, 'elToString', { 79 | enumerable: false, 80 | value: function () { 81 | return this.sort().map((s) => s.toString()) 82 | }, 83 | }) 84 | } 85 | 86 | process.on('unhandledRejection', (e) => { 87 | console.error('Unhandled Rejection') 88 | console.error(e.stack) 89 | process.exit(1) 90 | }); 91 | 92 | (async function () { 93 | try { 94 | await test() 95 | } catch (e) { 96 | console.error(e) 97 | process.exit(1) 98 | } 99 | })() 100 | -------------------------------------------------------------------------------- /app/helpers/parse/pickFields.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const pickFields = require('./pickFields') 3 | 4 | describe('helpers/parse/pickFields', async function () { 5 | describe('should return valid fields', async function () { 6 | it('with plain object', async function () { 7 | const payload = { 8 | valid: 'valid', 9 | invalid: 'invalid', 10 | } 11 | 12 | const fields = ['valid'] 13 | 14 | const afterPickPayload = pickFields(payload, fields) 15 | 16 | assert(afterPickPayload.valid) 17 | assert(!afterPickPayload.invalid) 18 | }) 19 | it('with nested object', async function () { 20 | const payload = { 21 | valid: 'valid', 22 | nested: { 23 | valid: 'valid', 24 | invalid: 'invalid', 25 | }, 26 | invalid: 'invalid', 27 | } 28 | 29 | const fields = ['valid', 'nested.valid'] 30 | 31 | const afterPickPayload = pickFields(payload, fields) 32 | 33 | assert(afterPickPayload.valid) 34 | assert(!afterPickPayload.invalid) 35 | assert(afterPickPayload.nested.valid) 36 | assert(!afterPickPayload.nested.invalid) 37 | }) 38 | it('with nested array object', async function () { 39 | const payload = { 40 | valid: 'valid', 41 | nested: { 42 | valid: 'valid', 43 | invalid: 'invalid', 44 | array: [ 45 | { 46 | valid: 'valid', 47 | invalid: 'invalid', 48 | }, 49 | ], 50 | }, 51 | invalid: 'invalid', 52 | } 53 | 54 | const fields = ['valid', 'nested.valid', 'nested.array.valid'] 55 | 56 | const afterPickPayload = pickFields(payload, fields) 57 | 58 | assert(afterPickPayload.valid) 59 | assert(!afterPickPayload.invalid) 60 | assert(afterPickPayload.nested.valid) 61 | assert(!afterPickPayload.nested.invalid) 62 | assert(afterPickPayload.nested.array[0].valid) 63 | assert(!afterPickPayload.nested.array[0].invalid) 64 | }) 65 | it('with an array', async function () { 66 | const payload = [ 67 | { 68 | valid: 'valid', 69 | nested: { 70 | valid: 'valid', 71 | invalid: 'invalid', 72 | array: [ 73 | { 74 | valid: 'valid', 75 | invalid: 'invalid', 76 | }, 77 | ], 78 | }, 79 | invalid: 'invalid', 80 | }, 81 | ] 82 | 83 | const fields = ['valid', 'nested.valid', 'nested.array.valid'] 84 | 85 | const afterPickPayload = pickFields(payload, fields) 86 | 87 | assert(afterPickPayload[0].valid) 88 | assert(!afterPickPayload[0].invalid) 89 | assert(afterPickPayload[0].nested.valid) 90 | assert(!afterPickPayload[0].nested.invalid) 91 | assert(afterPickPayload[0].nested.array[0].valid) 92 | assert(!afterPickPayload[0].nested.array[0].invalid) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /app/server.js: -------------------------------------------------------------------------------- 1 | // Install tracer (must be the first thing in order to properly work) 2 | require('dotenv').config() 3 | if (process.env.GCLOUD_PROJECT && process.env.GCLOUD_CREDENTIALS) { 4 | console.log('trace-agent enabled') 5 | require('@google-cloud/trace-agent').start({ 6 | projectId: process.env.GCLOUD_PROJECT, 7 | credentials: JSON.parse(process.env.GCLOUD_CREDENTIALS), 8 | // Enable Mongo Reporting 9 | enhancedDatabaseReporting: true, 10 | // Don't trace status requests 11 | ignoreUrls: ['/v1/status'], 12 | }) 13 | } 14 | 15 | const chalk = require('chalk') 16 | 17 | const app = require('./app') 18 | 19 | const TAG = '[server]' 20 | 21 | // Order of execution for setup steps 22 | const pipeline = [ 23 | // package.json information 24 | 'package', 25 | // Global configurations 26 | 'config', 27 | // Load app.helpers 28 | 'helpers', 29 | // Load mongo 30 | 'mongo', 31 | // Load models 32 | 'models', 33 | // Load Redis, 34 | 'redis', 35 | // Load Agenda 36 | 'agenda', 37 | // Create base express server 38 | 'server', 39 | // Add redirection behavior 40 | 'redirect', 41 | // Create web app 42 | 'static', 43 | // Generater Router for Restify 44 | 'router', 45 | // Create api (/v1) routes and middlewares 46 | 'api', 47 | // Create oauth helpers 48 | 'oauth', 49 | // Bind to port and lift http app 50 | 'lift', 51 | ] 52 | 53 | async function serve() { 54 | console.info(TAG, chalk.dim('lifting...')) 55 | 56 | await app.bootstrap(pipeline) 57 | 58 | console.info(TAG, ' version:', chalk.white(app.package.version)) 59 | console.info(TAG, ' port:', chalk.white(app.config.PORT)) 60 | console.info(TAG, ' env:', chalk.white(app.config.ENV)) 61 | 62 | try { 63 | let mongoUrl = new (require('url').URL)(app.config.MONGO_URL) 64 | console.info(TAG, ' mongo host:', chalk.white(mongoUrl.hostname)) 65 | console.info(TAG, ' mongo db:', chalk.white(mongoUrl.pathname)) 66 | } catch (e) { 67 | console.error(e) 68 | } 69 | 70 | if (process.env.SHUTDOWN_ON_LIFT) process.exit(0) 71 | } 72 | 73 | process.stdin.resume() 74 | // Listen for Application wide errors 75 | process.on('exit', shutdown) 76 | process.on('SIGTERM', shutdown) 77 | process.on('SIGINT', shutdown) 78 | process.on('SIGUSR1', shutdown) 79 | process.on('SIGUSR2', shutdown) 80 | process.on('unhandledRejection', handleError) 81 | process.on('uncaughtException', handleError) 82 | 83 | function shutdown() { 84 | app.agenda.stop() 85 | process.exit() 86 | } 87 | 88 | function handleError(e) { 89 | console.error('Fatal Error') 90 | console.error(e.stack) 91 | 92 | if (app.reporter) { 93 | console.error('Reporting...') 94 | app.reporter.report(e, () => { 95 | console.error('Reported. Exiting.') 96 | process.exit(1) 97 | }) 98 | return 99 | } 100 | 101 | console.error('Exiting.') 102 | process.exit(1) 103 | } 104 | 105 | serve() 106 | -------------------------------------------------------------------------------- /app/setup/api.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const glob = require('glob') 3 | const path = require('path') 4 | const express = require('express') 5 | const fallback = require('express-history-api-fallback') 6 | 7 | /* 8 | * Load routes from api 9 | */ 10 | module.exports = async (app) => { 11 | // Create secondary router for '/api/*' 12 | let api = express() 13 | 14 | // If delay mode is enabled, inject middleware to slowdown things 15 | if (app.config.DEBUG_DELAY) { 16 | api.use((req, res, next) => { 17 | setTimeout(next, app.config.DEBUG_DELAY * 1) 18 | }) 19 | } 20 | 21 | // Authenticate user 22 | api.use( 23 | [ 24 | '/users/info', 25 | '/users/complete', 26 | '/users/me/resend', 27 | '/users/me/grades', 28 | '/users/me/delete', 29 | '/users/me/devices', 30 | '/enrollments', 31 | '/comments', 32 | '/reactions', 33 | '/histories/courses', 34 | '/users/me/relationships', 35 | '/graduation', 36 | '/graduations/stats', 37 | '/subjectGraduations', 38 | '/historiesGraduations', 39 | '/students/aluno_id', 40 | '/subjects', 41 | ], 42 | app.helpers.middlewares.auth 43 | ) 44 | 45 | // Protect Private routes 46 | api.use('/private', app.helpers.middlewares.private) 47 | 48 | // Locate route files from api folder 49 | let cwd = path.join(__dirname, '../api') 50 | let routerPaths = glob.sync('**/*route.js', { cwd }) 51 | 52 | // Require route files 53 | let routers = routerPaths.map((file) => require(path.join(cwd, file))) 54 | 55 | // user a temporary router to order 56 | let tmpRoute = express() 57 | 58 | // Install routes 59 | for (let route of routers) { 60 | await route(tmpRoute) 61 | } 62 | 63 | // Order routes by path priority 64 | app.helpers.routes.order(tmpRoute) 65 | 66 | // get ordered router and apply on api 67 | tmpRoute._router.stack.forEach(function (currentRoute) { 68 | let path = _.get(currentRoute, 'route.path') 69 | let stack = _.get(currentRoute, 'route.stack', []) 70 | let method = _.get(currentRoute, 'route.stack[0].method') 71 | let functions = stack.map((s) => s.handle) 72 | 73 | if (method) { 74 | api[method](path, ...functions) 75 | } 76 | }) 77 | 78 | tmpRoute = null 79 | 80 | // Locate express-restify-mongoose files 81 | let restPaths = glob.sync('**/*rest.js', { cwd }) 82 | restPaths.map((file) => require(path.join(cwd, file))) 83 | 84 | // Add rest to api 85 | api.use(app.router) 86 | 87 | // Server errors and Not Found 88 | api.use('*', app.helpers.middlewares.notFound) 89 | api.use('*', app.helpers.middlewares.error) 90 | 91 | // Install into `/v1` route 92 | app.server.use('/v1', api) 93 | 94 | // Default errors 95 | // app.server.use('*', app.helpers.middlewares.notFound) 96 | // app.server.use('*', app.helpers.middlewares.error) 97 | app.server.use(fallback('index.html', { root: app.config.distFolder })) 98 | 99 | // Return api router 100 | return api 101 | } 102 | -------------------------------------------------------------------------------- /app/api/users/me/relationships/func.js: -------------------------------------------------------------------------------- 1 | const app = require('@/app') 2 | const errors = require('@/errors') 3 | 4 | module.exports = async function(context) { 5 | const RA = String(context.user.ra) 6 | 7 | var MAX_BREADTH = Math.min(Math.max(context.query.breadth, 1), 5) || 5 8 | var MAX_DEPTH = Math.min(Math.max(context.query.depth, 1), 3) || 3 9 | 10 | let nodes = [ { data: { id: RA, label: RA, 'color': '#f9b928' }} ] 11 | let edges = [] 12 | 13 | let usedRAs = [] 14 | 15 | let DEPTH_COUNT = 0 16 | 17 | async function searchRelationships(mainStudent, DEPTH) { 18 | if(DEPTH > MAX_DEPTH) return 19 | 20 | let students = await enrollmentsByRA(mainStudent, MAX_BREADTH) 21 | 22 | addStudents(students, mainStudent, DEPTH) 23 | 24 | DEPTH_COUNT++ 25 | 26 | await Promise.all(students.map(async student => { 27 | await searchRelationships(student[0], DEPTH_COUNT) 28 | })) 29 | } 30 | 31 | function addStudents(students, mainStudent, DEPTH) { 32 | students.map((student) => { 33 | if(usedRAs.includes(student[0])) return 34 | 35 | nodes.push({ 36 | data: { 37 | id: student[0], 38 | label: student[0], 39 | color: '#007bff', 40 | depth: DEPTH 41 | } 42 | }) 43 | 44 | if(mainStudent == student[0]) return 45 | 46 | edges.push({ 47 | data: { 48 | source: mainStudent, 49 | target: student[0], 50 | recurrence: student[1], 51 | width: (Math.pow(student[1], 1.3)) 52 | } 53 | }) 54 | }) 55 | } 56 | 57 | await searchRelationships(RA, DEPTH_COUNT) 58 | 59 | return { nodes: replaceNodesColor(nodes), edges } 60 | 61 | } 62 | 63 | function replaceNodesColor(nodes) { 64 | const colors = ['#ea3453', '#e85971', '#e47184', '#e48998', '#e8b1ba'] 65 | 66 | nodes.forEach(node => { 67 | node.data.color = colors[node.data.depth] || '#007bff' 68 | }) 69 | 70 | return nodes 71 | } 72 | 73 | async function enrollmentsByRA(RA, MAX_BREADTH) { 74 | const Groups = app.models.groups 75 | 76 | var usersGroups = await Groups.find({ users: RA }) 77 | 78 | if(!usersGroups) throw new errors.BadRequest('Invalid user RA') 79 | 80 | const allRelatedUsers = [] 81 | 82 | usersGroups.map(student => { 83 | allRelatedUsers.push(...student.users) 84 | }) 85 | 86 | var recurrentStudents = {} 87 | 88 | allRelatedUsers.map(student => { 89 | recurrentStudents[student] = recurrentStudents[student] === undefined ? 1 : recurrentStudents[student] + 1 90 | }) 91 | 92 | var orderedRecurrentStudents = Object.keys(recurrentStudents) 93 | .sort(function(a, b){ return recurrentStudents[a] - recurrentStudents[b] }) 94 | .filter(student => student != RA) 95 | .reverse() 96 | 97 | var orderedUsersRelatedWithRecurrence = [] 98 | 99 | orderedRecurrentStudents.map(student => { 100 | orderedUsersRelatedWithRecurrence.push([student, recurrentStudents[student]]) 101 | }) 102 | 103 | return orderedUsersRelatedWithRecurrence.slice(0, MAX_BREADTH) 104 | } -------------------------------------------------------------------------------- /app/models/reactions.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const Schema = require('mongoose').Schema 3 | const errors = require('@/errors') 4 | 5 | const app = require('@/app') 6 | 7 | var Model = module.exports = Schema({ 8 | kind: { 9 | type: String, 10 | required: true, 11 | enum: ['like', 'recommendation', 'star'] 12 | }, 13 | 14 | comment: { 15 | type: Schema.Types.ObjectId, 16 | required: true, 17 | ref: 'comments' 18 | }, 19 | 20 | user: { 21 | type: Schema.Types.ObjectId, 22 | required: true, 23 | ref: 'users' 24 | }, 25 | 26 | active: { 27 | type: Boolean, 28 | default: true 29 | }, 30 | 31 | slug: { 32 | type: String, 33 | } 34 | }) 35 | 36 | Model.pre('save', async function(){ 37 | // Validate if reaction is equal 38 | let slug = `${this.kind}:${this.comment._id}:${this.user._id}` 39 | if(this.isNew) { 40 | let equalReaction = await this.constructor.findOne({ slug: slug }) 41 | if(equalReaction) throw new errors.BadRequest('Você não pode reagir duas vezes iguais ao mesmo comentário') 42 | this.slug = slug 43 | } 44 | await validateRules(this) 45 | }) 46 | 47 | async function validateRules(reaction){ 48 | const Enrollment = app.models.enrollments 49 | const User = app.models.users 50 | const Comment = app.models.comments 51 | 52 | if(reaction.kind == 'recommendation') { 53 | const isValidId = mongoose.isObjectIdOrHexString 54 | 55 | const user = isValidId(reaction.user) ? await User.findById(reaction.user) : reaction.user 56 | 57 | const comment = isValidId(reaction.comment) ? await Comment.findById(reaction.comment) : reaction.comment 58 | 59 | let isValid = await Enrollment.findOne({ 60 | ra: user.ra, 61 | $or: [{ teoria: comment.teacher }, { pratica: comment.teacher }] 62 | }) 63 | if(!isValid) throw new errors.BadRequest('Você não pode recomendar este comentário, pois não fez nenhuma matéria com este professor') 64 | } 65 | } 66 | 67 | Model.post('save', async function(){ 68 | await computeReactions(this) 69 | }) 70 | 71 | Model.post('remove', async function(){ 72 | await computeReactions(this) 73 | }) 74 | 75 | async function computeReactions(doc) { 76 | const Comment = app.models.comments 77 | 78 | // let commentId = doc.comment._id ? doc.comment._id : doc.comment 79 | 80 | // let comments = await Comment.find({ _id: commentId }) 81 | 82 | // await Promise.all(comments.map(async function(a) { 83 | // a.reactionsCount = { 84 | // like: await doc.constructor.count({ comment: a.id, kind: 'like' }), 85 | // recommendation: await doc.constructor.count({ comment: a.id, kind: 'recommendation' }), 86 | // star: await doc.constructor.count({ comment: a.id, kind: 'star' }) 87 | // } 88 | // await a.save() 89 | // })) 90 | 91 | const commentId = doc.comment._id || doc.comment 92 | 93 | await Comment.findOneAndUpdate( 94 | { _id: commentId }, 95 | { [`reactionsCount.${doc.kind}`]: await doc.constructor.count({ comment: commentId, kind: doc.kind })} 96 | ) 97 | } 98 | 99 | Model.index({ comment: 1, kind: 1 }) 100 | --------------------------------------------------------------------------------