app.js 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837
  1. // FinalsClub Server
  2. //
  3. // This file consists of the main webserver for FinalsClub.org
  4. // and is split between a standard CRUD style webserver and
  5. // a websocket based realtime webserver.
  6. //
  7. // A note on house keeping: Anything with XXX is marked
  8. // as such because it should be looked at and possibly
  9. // revamped or removed depending on circumstances.
  10. // Module loading
  11. var sys = require( 'sys' );
  12. var os = require( 'os' );
  13. var url = require( 'url' );
  14. var express = require( 'express' );
  15. var mongoStore = require( 'connect-mongo' );
  16. var async = require( 'async' );
  17. var db = require( './db.js' );
  18. var mongoose = require( './models.js' ).mongoose;
  19. var Mailer = require( './mailer.js' );
  20. var hat = require('hat');
  21. var connect = require( 'connect' );
  22. var Session = connect.middleware.session.Session;
  23. var parseCookie = connect.utils.parseCookie;
  24. var Backchannel = require('./bc/backchannel');
  25. // Depracated
  26. // Used for initial testing
  27. var log3 = function() {}
  28. // Create webserver
  29. var app = module.exports = express.createServer();
  30. // Load Mongoose Schemas
  31. // The actual schemas are located in models.j
  32. var User = mongoose.model( 'User' );
  33. var School = mongoose.model( 'School' );
  34. var Course = mongoose.model( 'Course' );
  35. var Lecture = mongoose.model( 'Lecture' );
  36. var Note = mongoose.model( 'Note' );
  37. // More schemas used for legacy data
  38. var ArchivedCourse = mongoose.model( 'ArchivedCourse' );
  39. var ArchivedNote = mongoose.model( 'ArchivedNote' );
  40. var ArchivedSubject = mongoose.model( 'ArchivedSubject' );
  41. // XXX Not sure if necessary
  42. var ObjectId = mongoose.SchemaTypes.ObjectId;
  43. // Configuration
  44. // Use the environment variable DEV_EMAIL for testing
  45. var ADMIN_EMAIL = process.env.DEV_EMAIL || 'info@finalsclub.org';
  46. // Set server hostname and port from environment variables,
  47. // then check if set.
  48. // XXX Can be cleaned up
  49. var serverHost = process.env.SERVER_HOST;
  50. var serverPort = process.env.SERVER_PORT;
  51. if( serverHost ) {
  52. console.log( 'Using server hostname defined in environment: %s', serverHost );
  53. } else {
  54. serverHost = os.hostname();
  55. console.log( 'No hostname defined, defaulting to os.hostname(): %s', serverHost );
  56. }
  57. // Express configuration depending on environment
  58. // development is intended for developing locally or
  59. // when not in production, otherwise production is used
  60. // when the site will be run live for regular usage.
  61. app.configure( 'development', function() {
  62. // In development mode, all errors and stack traces will be
  63. // dumped to the console and on page for easier troubleshooting
  64. // and debugging.
  65. app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
  66. // Set database connection information from environment
  67. // variables otherwise use localhost.
  68. app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
  69. app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
  70. // Set Amazon access and secret keys from environment
  71. // variables. These keys are intended to be secret, so
  72. // are not included in the source code, but set on the server
  73. // manually.
  74. app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
  75. app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
  76. // If a port wasn't set earlier, set to 3000
  77. if ( !serverPort ) {
  78. serverPort = 3000;
  79. }
  80. });
  81. // Production configuration settings
  82. app.configure( 'production', function() {
  83. // At the moment we have errors outputting everything
  84. // so if there are any issues it is easier to track down.
  85. // Once the site is more stable it will be prudent to
  86. // use less error tracing.
  87. app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
  88. // Disable view cache due to stale views.
  89. // XXX Disable view caching temp
  90. app.disable( 'view cache' )
  91. // Against setting the database connection information
  92. // XXX Can be cleaned up or combined
  93. app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
  94. app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
  95. // XXX Can be cleaned up or combined
  96. app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
  97. app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
  98. // Set to port 80 if not set through environment variables
  99. if ( !serverPort ) {
  100. serverPort = 80;
  101. }
  102. });
  103. // General Express configuration settings
  104. app.configure(function(){
  105. // Views are housed in the views folder
  106. app.set( 'views', __dirname + '/views' );
  107. // All templates use jade for rendering
  108. app.set( 'view engine', 'jade' );
  109. // Bodyparser is required to handle form submissions
  110. // without manually parsing them.
  111. app.use( express.bodyParser() );
  112. app.use( express.cookieParser() );
  113. // Sessions are stored in mongodb which allows them
  114. // to be persisted even between server restarts.
  115. app.set( 'sessionStore', new mongoStore( {
  116. 'db' : 'fc',
  117. 'url' : app.set( 'dbUri' )
  118. }));
  119. // This is where the actual Express session handler
  120. // is defined, with a mongoStore being set as the
  121. // session storage versus in memory storage that is
  122. // used by default.
  123. app.use( express.session( {
  124. // A secret 'password' for encrypting and decrypting
  125. // cookies.
  126. // XXX Should be handled differently
  127. 'secret' : 'finalsclub',
  128. // The max age of the cookies that is allowed
  129. // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) * 1000 (milliseconds)
  130. 'maxAge' : new Date(Date.now() + (60 * 60 * 24 * 30 * 1000)),
  131. 'store' : app.set( 'sessionStore' )
  132. }));
  133. // methodOverride is used to handle PUT and DELETE HTTP
  134. // requests that otherwise aren't handled by default.
  135. app.use( express.methodOverride() );
  136. // Static files are loaded when no dynamic views match.
  137. app.use( express.static( __dirname + '/public' ) );
  138. // Sets the routers middleware to load after everything set
  139. // before it, but before static files.
  140. app.use( app.router );
  141. app.use(express.logger({ format: ':method :url' }));
  142. // This is the command to use the default express error logger/handler
  143. app.use(express.errorHandler({ dumpExceptions: true }));
  144. });
  145. // Mailer functions and helpers
  146. // These are helper functions that make for cleaner code.
  147. // sendUserActivation is for when a user registers and
  148. // first needs to activate their account to use it.
  149. function sendUserActivation( user ) {
  150. var message = {
  151. 'to' : user.email,
  152. 'subject' : 'Activate your FinalsClub.org Account',
  153. // Templates are in the email folder and use ejs
  154. 'template' : 'userActivation',
  155. // Locals are used inside ejs so dynamic information
  156. // can be rendered properly.
  157. 'locals' : {
  158. 'user' : user,
  159. 'serverHost' : serverHost
  160. }
  161. };
  162. // Email is sent here
  163. mailer.send( message, function( err, result ) {
  164. if( err ) {
  165. // XXX: Add route to resend this email
  166. console.log( 'Error sending user activation email\nError Message: '+err.Message );
  167. } else {
  168. console.log( 'Successfully sent user activation email.' );
  169. }
  170. });
  171. }
  172. // sendUserWelcome is for when a user registers and
  173. // a welcome email is sent.
  174. function sendUserWelcome( user, school ) {
  175. // If a user is not apart of a supported school, they are
  176. // sent a different template than if they are apart of a
  177. // supported school.
  178. var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
  179. var message = {
  180. 'to' : user.email,
  181. 'subject' : 'Welcome to FinalsClub',
  182. 'template' : template,
  183. 'locals' : {
  184. 'user' : user,
  185. 'serverHost' : serverHost
  186. }
  187. };
  188. mailer.send( message, function( err, result ) {
  189. if( err ) {
  190. // XXX: Add route to resend this email
  191. console.log( 'Error sending user welcome email\nError Message: '+err.Message );
  192. } else {
  193. console.log( 'Successfully sent user welcome email.' );
  194. }
  195. });
  196. }
  197. // Helper middleware
  198. // These functions are used later in the routes to help
  199. // load information and variables, as well as handle
  200. // various instances like checking if a user is logged in
  201. // or not.
  202. function loggedIn( req, res, next ) {
  203. // If req.user is set, then pass on to the next function
  204. // or else alert the user with an error message.
  205. if( req.user ) {
  206. next();
  207. } else {
  208. req.flash( 'error', 'You must be logged in to access that feature!' );
  209. res.redirect( '/' );
  210. }
  211. }
  212. // This loads the user if logged in
  213. function loadUser( req, res, next ) {
  214. var sid = req.sessionID;
  215. console.log( 'got request from session ID: %s', sid );
  216. // Find a user based on their stored session id
  217. User.findOne( { session : sid }, function( err, user ) {
  218. log3(err);
  219. log3(user);
  220. // If a user is found then set req.user the contents of user
  221. // and make sure req.user.loggedIn is true.
  222. if( user ) {
  223. req.user = user;
  224. req.user.loggedIn = true;
  225. log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
  226. // Check if a user is activated. If not, then redirec
  227. // to the homepage and tell them to check their email
  228. // for the activation email.
  229. if( req.user.activated ) {
  230. // Is the user's profile complete? If not, redirect to their profile
  231. if( ! req.user.isComplete ) {
  232. if( url.parse( req.url ).pathname != '/profile' ) {
  233. req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
  234. res.redirect( '/profile' );
  235. } else {
  236. next();
  237. }
  238. } else {
  239. next();
  240. }
  241. } else {
  242. req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
  243. res.redirect( '/' );
  244. }
  245. } else {
  246. // If no user record was found, then we store the requested
  247. // path they intended to view and redirect them after they
  248. // login if it is requred.
  249. var path = url.parse( req.url ).pathname;
  250. req.session.redirect = path;
  251. // Set req.user to an empty object so it doesn't throw errors
  252. // later on that it isn't defined.
  253. req.user = {
  254. sanitized: {}
  255. };
  256. next();
  257. }
  258. });
  259. }
  260. // loadSchool is used to load a school by it's id
  261. function loadSchool( req, res, next ) {
  262. var user = req.user;
  263. var schoolId = req.params.id;
  264. School.findById( schoolId, function( err, school ) {
  265. if( school ) {
  266. req.school = school;
  267. // If a school is found, the user is checked to see if they are
  268. // authorized to see or interact with anything related to that
  269. // school.
  270. school.authorize( user, function( authorized ){
  271. req.school.authorized = authorized;
  272. next();
  273. });
  274. } else {
  275. // If no school is found, display an appropriate error.
  276. sendJson(res, {status: 'not_found', message: 'Invalid school specified!'} );
  277. }
  278. });
  279. }
  280. // loadSchool is used to load a course by it's id
  281. function loadCourse( req, res, next ) {
  282. var user = req.user;
  283. var courseId = req.params.id;
  284. Course.findById( courseId, function( err, course ) {
  285. if( course && !course.deleted ) {
  286. req.course = course;
  287. // If a course is found, the user is checked to see if they are
  288. // authorized to see or interact with anything related to that
  289. // school.
  290. course.authorize( user, function( authorized ) {
  291. req.course.authorized = authorized;
  292. next();
  293. });
  294. } else {
  295. // If no course is found, display an appropriate error.
  296. sendJson(res, {status: 'not_found', message: 'Invalid course specified!'} );
  297. }
  298. });
  299. }
  300. // loadLecture is used to load a lecture by it's id
  301. function loadLecture( req, res, next ) {
  302. var user = req.user;
  303. var lectureId = req.params.id;
  304. Lecture.findById( lectureId, function( err, lecture ) {
  305. if( lecture && !lecture.deleted ) {
  306. req.lecture = lecture;
  307. // If a lecture is found, the user is checked to see if they are
  308. // authorized to see or interact with anything related to that
  309. // school.
  310. lecture.authorize( user, function( authorized ) {
  311. req.lecture.authorized = authorized;
  312. next();
  313. });
  314. } else {
  315. // If no lecture is found, display an appropriate error.
  316. sendJson(res, {status: 'not_found', message: 'Invalid lecture specified!'} );
  317. }
  318. });
  319. }
  320. // loadNote is used to load a note by it's id
  321. // This is a lot more complicated than the above
  322. // due to public/private handling of notes.
  323. function loadNote( req, res, next ) {
  324. var user = req.user ? req.user : false;
  325. var noteId = req.params.id;
  326. Note.findById( noteId, function( err, note ) {
  327. // If a note is found, and user is set, check if
  328. // user is authorized to interact with that note.
  329. if( note && user && !note.deleted ) {
  330. note.authorize( user, function( auth ) {
  331. if( auth ) {
  332. // If authorzied, then set req.note to be used later
  333. req.note = note;
  334. next();
  335. } else if ( note.public ) {
  336. // If not authorized, but the note is public, then
  337. // designate the note read only (RO) and store req.note
  338. req.RO = true;
  339. req.note = note;
  340. next();
  341. } else {
  342. // If the user is not authorized and the note is private
  343. // then display and error.
  344. sendJson(res, {status: 'error', message: 'You do not have permission to access that note.'} );
  345. }
  346. })
  347. } else if ( note && note.public && !note.deleted ) {
  348. // If note is found, but user is not set because they are not
  349. // logged in, and the note is public, set the note to read only
  350. // and store the note for later.
  351. req.note = note;
  352. req.RO = true;
  353. next();
  354. } else if ( note && !note.public && !note.deleted ) {
  355. // If the note is found, but user is not logged in and the note is
  356. // not public, then ask them to login to view the note. Once logged
  357. // in they will be redirected to the note, at which time authorization
  358. // handling will be put in effect above.
  359. //req.session.redirect = '/note/' + note._id;
  360. sendJson(res, {status: 'error', message: 'You must be logged in to view that note.'} );
  361. } else {
  362. // No note was found
  363. sendJson(res, {status: 'error', message: 'Invalid note specified!'} );
  364. }
  365. });
  366. }
  367. function checkAjax( req, res, next ) {
  368. if ( req.xhr ) {
  369. next();
  370. } else {
  371. res.sendfile( 'public/index.html' );
  372. }
  373. }
  374. // Dynamic Helpers are loaded automatically into views
  375. app.dynamicHelpers( {
  376. // express-messages is for flash messages for easy
  377. // errors and information display
  378. 'messages' : require( 'express-messages' ),
  379. // By default the req object isn't sen't to views
  380. // during rendering, this allows you to use the
  381. // user object if available in views.
  382. 'user' : function( req, res ) {
  383. return req.user;
  384. },
  385. // Same, this allows session to be available in views.
  386. 'session' : function( req, res ) {
  387. return req.session;
  388. }
  389. });
  390. function sendJson( res, obj ) {
  391. res.header('Cache-Control', 'no-cache, no-store');
  392. res.json(obj);
  393. }
  394. // Routes
  395. // The following are the main CRUD routes that are used
  396. // to make up this web app.
  397. // Homepage
  398. // Public
  399. /*
  400. app.get( '/', loadUser, function( req, res ) {
  401. log3("get / page");
  402. res.render( 'index' );
  403. });
  404. */
  405. // Schools list
  406. // Used to display all available schools and any courses
  407. // in those schools.
  408. // Public with some private information
  409. app.get( '/schools', checkAjax, loadUser, function( req, res ) {
  410. var user = req.user;
  411. var schoolList = [];
  412. // Find all schools and sort by name
  413. // XXX mongoose's documentation on sort is extremely poor, tread carefully
  414. School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
  415. if( schools ) {
  416. // If schools are found, loop through them gathering any courses that are
  417. // associated with them and then render the page with that information.
  418. sendJson(res, { 'user': user.sanitized, 'schools' : schools.map(function(school) {
  419. return school.sanitized;
  420. })})
  421. } else {
  422. // If no schools have been found, display none
  423. //res.render( 'schools', { 'schools' : [] } );
  424. sendJson(res, { 'schools' : [] , 'user': user.sanitized });
  425. }
  426. });
  427. });
  428. app.get( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
  429. var school = req.school;
  430. var user = req.user;
  431. school.authorize( user, function( authorized ) {
  432. // This is used to display interface elements for those users
  433. // that are are allowed to see th)m, for instance a 'New Course' button.
  434. var sanitizedSchool = school.sanitized;
  435. sanitizedSchool.authorized = authorized;
  436. // Find all courses for school by it's id and sort by name
  437. Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
  438. // If any courses are found, set them to the appropriate school, otherwise
  439. // leave empty.
  440. if( courses.length > 0 ) {
  441. sanitizedSchool.courses = courses.filter(function(course) {
  442. if (!course.deleted) return course;
  443. }).map(function(course) {
  444. return course.sanitized;
  445. });
  446. } else {
  447. sanitizedSchool.courses = [];
  448. }
  449. // This tells async (the module) that each iteration of forEach is
  450. // done and will continue to call the rest until they have all been
  451. // completed, at which time the last function below will be called.
  452. sendJson(res, { 'school': sanitizedSchool, 'user': user.sanitized })
  453. });
  454. });
  455. });
  456. // Recieves new course form
  457. app.post( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
  458. var school = req.school;
  459. // Creates new course from Course Schema
  460. var course = new Course;
  461. // Gathers instructor information from form
  462. var instructorEmail = req.body.email.toLowerCase();
  463. var instructorName = req.body.instructorName;
  464. if( ( ! school ) || ( ! school.authorized ) ) {
  465. return sendJson(res, {status: 'error', message: 'There was a problem trying to create a course'})
  466. }
  467. if ( !instructorName ) {
  468. return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
  469. }
  470. if ( ( instructorEmail === '' ) || ( !isValidEmail( instructorEmail ) ) ) {
  471. return sendJson(res, {status: 'error', message:'Please enter a valid email'} );
  472. }
  473. // Fill out the course with information from the form
  474. course.number = req.body.number;
  475. course.name = req.body.name;
  476. course.description = req.body.description;
  477. course.school = school._id;
  478. course.creator = req.user._id;
  479. course.subject = req.body.subject;
  480. course.department = req.body.department;
  481. // Check if a user exists with the instructorEmail, if not then create
  482. // a new user and send them an instructor welcome email.
  483. User.findOne( { 'email' : instructorEmail }, function( err, user ) {
  484. if ( !user ) {
  485. var user = new User;
  486. user.name = instructorName
  487. user.email = instructorEmail;
  488. user.affil = 'Instructor';
  489. user.school = school.name;
  490. user.activated = false;
  491. // Once the new user information has been completed, save the user
  492. // to the database then email them the instructor welcome email.
  493. user.save(function( err ) {
  494. // If there was an error saving the instructor, prompt the user to fill out
  495. // the information again.
  496. if ( err ) {
  497. return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
  498. } else {
  499. var message = {
  500. to : user.email,
  501. 'subject' : 'A non-profit open education initiative',
  502. 'template' : 'instructorInvite',
  503. 'locals' : {
  504. 'course' : course,
  505. 'school' : school,
  506. 'user' : user,
  507. 'serverHost' : serverHost
  508. }
  509. };
  510. mailer.send( message, function( err, result ) {
  511. if( err ) {
  512. console.log( 'Error inviting instructor to course!' );
  513. } else {
  514. console.log( 'Successfully invited instructor to course.' );
  515. }
  516. });
  517. // After emails are sent, set the courses instructor to the
  518. // new users id and then save the course to the database.
  519. course.instructor = user._id;
  520. course.save( function( err ) {
  521. if( err ) {
  522. return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
  523. } else {
  524. // Once the course has been completed email the admin with information
  525. // on the course and new instructor
  526. var message = {
  527. to : ADMIN_EMAIL,
  528. 'subject' : school.name+' has a new course: '+course.name,
  529. 'template' : 'newCourse',
  530. 'locals' : {
  531. 'course' : course,
  532. 'instructor' : user,
  533. 'user' : req.user,
  534. 'serverHost' : serverHost
  535. }
  536. };
  537. mailer.send( message, function( err, result ) {
  538. if ( err ) {
  539. console.log( 'Error sending new course email to info@finalsclub.org' )
  540. } else {
  541. console.log( 'Successfully invited instructor to course')
  542. }
  543. })
  544. // Redirect the user to the schools page where they can see
  545. // their new course.
  546. // XXX Redirect to the new course instead
  547. return sendJson(res, {status: 'ok', message: 'Course created'} )
  548. }
  549. });
  550. }
  551. })
  552. } else {
  553. // If the user exists, then check if they are already and instructor
  554. if (user.affil === 'Instructor') {
  555. // If they are an instructor, then save the course with the appropriate
  556. // information and email the admin.
  557. course.instructor = user._id;
  558. course.save( function( err ) {
  559. if( err ) {
  560. // XXX better validation
  561. return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
  562. } else {
  563. var message = {
  564. to : ADMIN_EMAIL,
  565. 'subject' : school.name+' has a new course: '+course.name,
  566. 'template' : 'newCourse',
  567. 'locals' : {
  568. 'course' : course,
  569. 'instructor' : user,
  570. 'user' : req.user,
  571. 'serverHost' : serverHost
  572. }
  573. };
  574. mailer.send( message, function( err, result ) {
  575. if ( err ) {
  576. console.log( 'Error sending new course email to info@finalsclub.org' )
  577. } else {
  578. console.log( 'Successfully invited instructor to course')
  579. }
  580. })
  581. // XXX Redirect to the new course instead
  582. return sendJson(res, {status: 'ok', message: 'Course created'} )
  583. }
  584. });
  585. } else {
  586. // The existing user isn't an instructor, so the user is notified of the error
  587. // and the course isn't created.
  588. sendJson(res, {status: 'error', message: 'The existing user\'s email you entered is not an instructor'} )
  589. }
  590. }
  591. })
  592. });
  593. // Individual Course Listing
  594. // Public with private information
  595. app.get( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
  596. var userId = req.user._id;
  597. var course = req.course;
  598. // Check if the user is subscribed to the course
  599. // XXX Not currently used for anything
  600. //var subscribed = course.subscribed( userId );
  601. // Find lectures associated with this course and sort by name
  602. Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
  603. // Get course instructor information using their id
  604. User.findById( course.instructor, function( err, instructor ) {
  605. // Render course and lectures
  606. var sanitizedInstructor = instructor.sanitized;
  607. var sanitizedCourse = course.sanitized;
  608. if (!course.authorized) {
  609. delete sanitizedInstructor.email;
  610. } else {
  611. sanitizedCourse.authorized = course.authorized;
  612. }
  613. sendJson(res, { 'course' : sanitizedCourse, 'instructor': sanitizedInstructor, 'lectures' : lectures.map(function(lecture) { return lecture.sanitized })} );
  614. })
  615. });
  616. });
  617. // Recieve New Lecture Form
  618. app.post( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
  619. var course = req.course;
  620. // Create new lecture from Lecture schema
  621. var lecture = new Lecture;
  622. if( ( ! course ) || ( ! course.authorized ) ) {
  623. return sendJson(res, {status: 'error', message: 'There was a problem trying to create a lecture'})
  624. }
  625. // Populate lecture with form data
  626. lecture.name = req.body.name;
  627. lecture.date = req.body.date;
  628. lecture.course = course._id;
  629. lecture.creator = req.user._id;
  630. // Save lecture to database
  631. lecture.save( function( err ) {
  632. if( err ) {
  633. // XXX better validation
  634. sendJson(res, {status: 'error', message: 'Invalid parameters!'} );
  635. } else {
  636. sendJson(res, {status: 'ok', message: 'Created new lecture'} );
  637. }
  638. });
  639. });
  640. // Edit Course
  641. app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
  642. var course = req.course;
  643. var user = req.user;
  644. if ( user.admin ) {
  645. res.render( 'course/new', {course: course} )
  646. } else {
  647. req.flash( 'error', 'You don\'t have permission to do that' )
  648. res.redirect( '/schools' );
  649. }
  650. })
  651. // Recieve Course Edit Form
  652. app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
  653. var course = req.course;
  654. var user = req.user;
  655. if (user.admin) {
  656. var courseChanges = req.body;
  657. course.number = courseChanges.number;
  658. course.name = courseChanges.name;
  659. course.description = courseChanges.description;
  660. course.department = courseChanges.department;
  661. course.save(function(err) {
  662. if (err) {
  663. req.flash( 'error', 'There was an error saving the course' );
  664. }
  665. res.redirect( '/course/'+ course._id.toString());
  666. })
  667. } else {
  668. req.flash( 'error', 'You don\'t have permission to do that' )
  669. res.redirect( '/schools' );
  670. }
  671. });
  672. // Delete Course
  673. app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
  674. var course = req.course;
  675. var user = req.user;
  676. if ( user.admin ) {
  677. course.delete(function( err ) {
  678. if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
  679. else req.flash( 'info', 'Successfully removed course' )
  680. res.redirect( '/schools' );
  681. });
  682. } else {
  683. req.flash( 'error', 'You don\'t have permission to do that' )
  684. res.redirect( '/schools' );
  685. }
  686. })
  687. // Subscribe to course
  688. // XXX Not currently used for anything
  689. app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
  690. var course = req.course;
  691. var userId = req.user._id;
  692. course.subscribe( userId, function( err ) {
  693. if( err ) {
  694. req.flash( 'error', 'Error subscribing to course!' );
  695. }
  696. res.redirect( '/course/' + course._id );
  697. });
  698. });
  699. // Unsubscribe from course
  700. // XXX Not currently used for anything
  701. app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
  702. var course = req.course;
  703. var userId = req.user._id;
  704. course.unsubscribe( userId, function( err ) {
  705. if( err ) {
  706. req.flash( 'error', 'Error unsubscribing from course!' );
  707. }
  708. res.redirect( '/course/' + course._id );
  709. });
  710. });
  711. // Display individual lecture and related notes
  712. app.get( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
  713. var lecture = req.lecture;
  714. // Grab the associated course
  715. // XXX this should be done with DBRefs eventually
  716. Course.findById( lecture.course, function( err, course ) {
  717. if( course ) {
  718. // If course is found, find instructor information to be displayed on page
  719. User.findById( course.instructor, function( err, instructor ) {
  720. // Pull out our notes
  721. Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
  722. if ( !req.user.loggedIn || !req.lecture.authorized ) {
  723. // Loop through notes and only return those that are public if the
  724. // user is not logged in or not authorized for that lecture
  725. notes = notes.filter(function( note ) {
  726. if ( note.public ) return note;
  727. })
  728. }
  729. var sanitizedInstructor = instructor.sanitized;
  730. var sanitizedLecture = lecture.sanitized;
  731. if (!lecture.authorized) {
  732. delete sanitizedInstructor.email;
  733. } else {
  734. sanitizedLecture.authorized = lecture.authorized;
  735. }
  736. sendJson(res, {
  737. 'lecture' : sanitizedLecture,
  738. 'course' : course.sanitized,
  739. 'instructor' : sanitizedInstructor,
  740. 'notes' : notes.map(function(note) {
  741. return note.sanitized;
  742. })
  743. });
  744. });
  745. })
  746. } else {
  747. sendJson(res, { status: 'not_found', msg: 'This course is orphaned' })
  748. }
  749. });
  750. });
  751. // Recieve new note form
  752. app.post( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
  753. var lecture = req.lecture;
  754. if( ( ! lecture ) || ( ! lecture.authorized ) ) {
  755. return sendJson(res, {status: 'error', message: 'There was a problem trying to create a note pad'})
  756. }
  757. // Create note from Note schema
  758. var note = new Note;
  759. // Populate note from form data
  760. note.name = req.body.name;
  761. note.date = req.body.date;
  762. note.lecture = lecture._id;
  763. note.public = req.body.private ? false : true;
  764. note.creator = req.user._id;
  765. // Save note to database
  766. note.save( function( err ) {
  767. if( err ) {
  768. // XXX better validation
  769. sendJson(res, {status: 'error', message: 'There was a problem trying to create a note pad'})
  770. } else {
  771. sendJson(res, {status: 'ok', message: 'Successfully created a new note pad'})
  772. }
  773. });
  774. });
  775. // Display individual note page
  776. app.get( '/note/:id', /*checkAjax,*/ loadUser, loadNote, function( req, res ) {
  777. var note = req.note;
  778. // Set read only id for etherpad-lite or false for later check
  779. var roID = note.roID || false;
  780. var lectureId = note.lecture;
  781. // Count the amount of visits, but only once per session
  782. if ( req.session.visited ) {
  783. if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
  784. req.session.visited.push( note._id );
  785. note.addVisit();
  786. }
  787. } else {
  788. req.session.visited = [];
  789. req.session.visited.push( note._id );
  790. note.addVisit();
  791. }
  792. // If a read only id exists process note
  793. if (roID) {
  794. processReq();
  795. } else {
  796. // If read only id doesn't, then fetch the read only id from the database and then
  797. // process note.
  798. // XXX Soon to be depracated due to a new API in etherpad that makes for a
  799. // much cleaner solution.
  800. db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
  801. epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
  802. if ( record ) {
  803. roID = record.value.replace(/"/g, '');
  804. } else {
  805. roID = false;
  806. }
  807. processReq();
  808. })
  809. })
  810. }
  811. function processReq() {
  812. // Find lecture
  813. Lecture.findById( lectureId, function( err, lecture ) {
  814. if( ! lecture ) {
  815. req.flash( 'error', 'That notes page is orphaned!' );
  816. res.redirect( '/' );
  817. }
  818. // Find notes based on lecture id, which will be displayed in a dropdown
  819. // on the page
  820. Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
  821. /*
  822. sendJson(res, {
  823. 'host' : serverHost,
  824. 'note' : note.sanitized,
  825. 'lecture' : lecture.sanitized,
  826. 'otherNotes' : otherNotes.map(function(note) {
  827. return note.sanitized;
  828. }),
  829. 'RO' : req.RO,
  830. 'roID' : roID,
  831. });
  832. */
  833. if( !req.RO ) {
  834. // User is logged in and sees full notepad
  835. res.render( 'notes/index', {
  836. 'layout' : 'noteLayout',
  837. 'host' : serverHost,
  838. 'note' : note,
  839. 'lecture' : lecture,
  840. 'otherNotes' : otherNotes,
  841. 'RO' : false,
  842. 'roID' : roID,
  843. 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
  844. 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
  845. });
  846. } else {
  847. // User is not logged in and sees notepad that is public
  848. res.render( 'notes/public', {
  849. 'layout' : 'noteLayout',
  850. 'host' : serverHost,
  851. 'note' : note,
  852. 'otherNotes' : otherNotes,
  853. 'roID' : roID,
  854. 'lecture' : lecture,
  855. 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
  856. 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
  857. });
  858. }
  859. });
  860. });
  861. }
  862. });
  863. // Static pages and redirects
  864. /*
  865. app.get( '/about', loadUser, function( req, res ) {
  866. res.redirect( 'http://blog.finalsclub.org/about.html' );
  867. });
  868. app.get( '/press', loadUser, function( req, res ) {
  869. res.render( 'static/press' );
  870. });
  871. app.get( '/conduct', loadUser, function( req, res ) {
  872. res.render( 'static/conduct' );
  873. });
  874. app.get( '/legal', loadUser, function( req, res ) {
  875. res.redirect( 'http://blog.finalsclub.org/legal.html' );
  876. });
  877. app.get( '/contact', loadUser, function( req, res ) {
  878. res.redirect( 'http://blog.finalsclub.org/contact.html' );
  879. });
  880. app.get( '/privacy', loadUser, function( req, res ) {
  881. res.render( 'static/privacy' );
  882. });
  883. */
  884. // Authentication routes
  885. // These are used for logging in, logging out, registering
  886. // and other user authentication purposes
  887. // Render login page
  888. /*
  889. app.get( '/login', function( req, res ) {
  890. log3("get login page")
  891. res.render( 'login' );
  892. });
  893. */
  894. app.get( '/checkuser', checkAjax, loadUser, function( req, res ) {
  895. sendJson(res, {user: req.user.sanitized});
  896. });
  897. // Recieve login form
  898. app.post( '/login', checkAjax, function( req, res ) {
  899. var email = req.body.email;
  900. var password = req.body.password;
  901. log3("post login ...")
  902. // Find user from email
  903. User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
  904. log3(err)
  905. log3(user)
  906. // If user exists, check if activated, if not notify them and send them to
  907. // the login form
  908. if( user ) {
  909. if( ! user.activated ) {
  910. // (undocumented) markdown-esque link functionality in req.flash
  911. req.session.activateCode = user._id;
  912. sendJson(res, {status: 'error', message: 'This account isn\'t activated.'} );
  913. } else {
  914. // If user is activated, check if their password is correct
  915. if( user.authenticate( password ) ) {
  916. log3("pass ok")
  917. var sid = req.sessionID;
  918. user.session = sid;
  919. // Set the session then save the user to the database
  920. user.save( function() {
  921. var redirect = req.session.redirect;
  922. // login complete, remember the user's email for next time
  923. req.session.email = email;
  924. // alert the successful login
  925. sendJson(res, {status: 'ok', message:'Successfully logged in!'} );
  926. // redirect to profile if we don't have a stashed request
  927. //res.redirect( redirect || '/profile' );
  928. });
  929. } else {
  930. // Notify user of bad login
  931. sendJson(res, {status: 'error', message: 'Invalid login!'} );
  932. //res.render( 'login' );
  933. }
  934. }
  935. } else {
  936. // Notify user of bad login
  937. log3("bad login")
  938. sendJson(res, {status: 'error', message: 'Invalid login!'} );
  939. //res.render( 'login' );
  940. }
  941. });
  942. });
  943. // Recieve reset password request form
  944. app.post( '/resetpass', checkAjax, function( req, res ) {
  945. log3("post resetpw");
  946. var email = req.body.email
  947. // Search for user
  948. User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
  949. if( user ) {
  950. // If user exists, create reset code
  951. var resetPassCode = hat(64);
  952. user.setResetPassCode(resetPassCode);
  953. // Construct url that the user can then click to reset password
  954. var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
  955. // Save user to database
  956. user.save( function( err ) {
  957. log3('save '+user.email);
  958. // Construct email and send it to the user
  959. var message = {
  960. 'to' : user.email,
  961. 'subject' : 'Your FinalsClub.org Password has been Reset!',
  962. 'template' : 'userPasswordReset',
  963. 'locals' : {
  964. 'resetPassCode' : resetPassCode,
  965. 'resetPassUrl' : resetPassUrl
  966. }
  967. };
  968. mailer.send( message, function( err, result ) {
  969. if( err ) {
  970. // XXX: Add route to resend this email
  971. console.log( 'Error sending user password reset email!' );
  972. } else {
  973. console.log( 'Successfully sent user password reset email.' );
  974. }
  975. });
  976. // Render request success page
  977. sendJson(res, {status: 'ok', message: 'Your password has been reset. An email has been sent to ' + email })
  978. });
  979. } else {
  980. // Notify of error
  981. sendJson(res, {status: 'error', message: 'We were unable to reset the password using that email address. Please try again.' })
  982. }
  983. });
  984. });
  985. // Recieve reset password form
  986. app.post( '/resetpw/:id', checkAjax, function( req, res ) {
  987. log3("post resetpw.code");
  988. var resetPassCode = req.params.id
  989. var email = req.body.email
  990. var pass1 = req.body.pass1
  991. var pass2 = req.body.pass2
  992. // Find user by email
  993. User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
  994. var valid = false;
  995. // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
  996. // save user with new password and display success message.
  997. if( user ) {
  998. var valid = user.resetPassword(resetPassCode, pass1, pass2);
  999. if (valid) {
  1000. user.save( function( err ) {
  1001. sendJson(res, {status: 'ok', message: 'Your password has been reset. You can now login with your the new password you just created.'})
  1002. });
  1003. }
  1004. }
  1005. // If there was a problem, notify user
  1006. if (!valid) {
  1007. sendJson(res, {status: 'error', message: 'We were unable to reset the password. Please try again.' })
  1008. }
  1009. });
  1010. });
  1011. // Display registration page
  1012. /*
  1013. app.get( '/register', function( req, res ) {
  1014. log3("get reg page");
  1015. // Populate school dropdown list
  1016. School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
  1017. res.render( 'register', { 'schools' : schools } );
  1018. })
  1019. });
  1020. */
  1021. // Recieve registration form
  1022. app.post( '/register', checkAjax, function( req, res ) {
  1023. var sid = req.sessionId;
  1024. // Create new user from User schema
  1025. var user = new User;
  1026. // Populate user from form
  1027. user.email = req.body.email.toLowerCase();
  1028. user.password = req.body.password;
  1029. user.session = sid;
  1030. // If school is set to other, then fill in school as what the
  1031. // user entered
  1032. user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
  1033. user.name = req.body.name;
  1034. user.affil = req.body.affil;
  1035. user.activated = false;
  1036. // Validate email
  1037. if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
  1038. return sendJson(res, {status: 'error', message: 'Please enter a valid email'} );
  1039. }
  1040. // Check if password is greater than 6 characters, otherwise notify user
  1041. if ( req.body.password.length < 6 ) {
  1042. return sendJson(res, {status: 'error', message: 'Please enter a password longer than eight characters'} );
  1043. }
  1044. // Pull out hostname from email
  1045. var hostname = user.email.split( '@' ).pop();
  1046. // Check if email is from one of the special domains
  1047. if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
  1048. user.admin = true;
  1049. }
  1050. // Save user to database
  1051. user.save( function( err ) {
  1052. // If error, check if it is because the user already exists, if so
  1053. // get the user information and let them know
  1054. if ( err ) {
  1055. if( /dup key/.test( err.message ) ) {
  1056. // attempting to register an existing address
  1057. User.findOne({ 'email' : user.email }, function(err, result ) {
  1058. if (result.activated) {
  1059. // If activated, make sure they know how to contact the admin
  1060. return sendJson(res, {status: 'error', message: 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help'} );
  1061. } else {
  1062. // If not activated, direct them to the resendActivation page
  1063. return sendJson(res, {status: 'error', message: 'There is already someone registered with this email, if this is you, please check your email for the activation code'} );
  1064. }
  1065. });
  1066. } else {
  1067. // If any other type of error, prompt them to enter the registration again
  1068. return sendJson(res, {status: 'error', message: 'An error occurred during registration.'} );
  1069. }
  1070. } else {
  1071. // send user activation email
  1072. sendUserActivation( user );
  1073. // Check if the hostname matches any in the approved schools
  1074. School.findOne( { 'hostnames' : hostname }, function( err, school ) {
  1075. if( school ) {
  1076. // If there is a match, send associated welcome message
  1077. sendUserWelcome( user, true );
  1078. log3('school recognized '+school.name);
  1079. // If no users exist for the school, create empty array
  1080. if (!school.users) school.users = [];
  1081. // Add user to the school
  1082. school.users.push( user._id );
  1083. // Save school to the database
  1084. school.save( function( err ) {
  1085. log3('school.save() done');
  1086. // Notify user that they have been added to the school
  1087. sendJson(res, {status: 'ok', message: 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link'} );
  1088. });
  1089. // Construct admin email about user registration
  1090. var message = {
  1091. 'to' : ADMIN_EMAIL,
  1092. 'subject' : 'FC User Registration : User added to ' + school.name,
  1093. 'template' : 'userSchool',
  1094. 'locals' : {
  1095. 'user' : user
  1096. }
  1097. }
  1098. } else {
  1099. // If there isn't a match, send associated welcome message
  1100. sendUserWelcome( user, false );
  1101. // Tell user to check for activation link
  1102. sendJson(res, {status: 'ok', message: 'Your account has been created, please check your email for the activation link'} );
  1103. // Construct admin email about user registration
  1104. var message = {
  1105. 'to' : ADMIN_EMAIL,
  1106. 'subject' : 'FC User Registration : Email did not match any schools',
  1107. 'template' : 'userNoSchool',
  1108. 'locals' : {
  1109. 'user' : user
  1110. }
  1111. }
  1112. }
  1113. // Send email to admin
  1114. mailer.send( message, function( err, result ) {
  1115. if ( err ) {
  1116. console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
  1117. } else {
  1118. console.log( 'Successfully sent user has no school email to admin.' );
  1119. }
  1120. })
  1121. });
  1122. }
  1123. });
  1124. });
  1125. // Display resendActivation request page
  1126. app.get( '/resendActivation', function( req, res ) {
  1127. var activateCode = req.session.activateCode;
  1128. // Check if user exists by activateCode set in their session
  1129. User.findById( activateCode, function( err, user ) {
  1130. if( ( ! user ) || ( user.activated ) ) {
  1131. res.redirect( '/' );
  1132. } else {
  1133. // Send activation and redirect to login
  1134. sendUserActivation( user );
  1135. req.flash( 'info', 'Your activation code has been resent.' );
  1136. res.redirect( '/login' );
  1137. }
  1138. });
  1139. });
  1140. // Display activation page
  1141. app.get( '/activate/:code', checkAjax, function( req, res ) {
  1142. var code = req.params.code;
  1143. // XXX could break this out into a middleware
  1144. if( ! code ) {
  1145. return sendJson(res, {status:'error', message: 'Invalid activation code!'} );
  1146. }
  1147. // Find user by activation code
  1148. User.findById( code, function( err, user ) {
  1149. if( err || ! user ) {
  1150. // If not found, notify user of invalid code
  1151. sendJson(res, {status:'error', message:'Invalid activation code!'} );
  1152. } else {
  1153. // If valid, then activate user
  1154. user.activated = true;
  1155. // Regenerate our session and log in as the new user
  1156. req.session.regenerate( function() {
  1157. user.session = req.sessionID;
  1158. // Save user to database
  1159. user.save( function( err ) {
  1160. if( err ) {
  1161. sendJson(res, {status: 'error', message: 'Unable to activate account.'} );
  1162. } else {
  1163. sendJson(res, {status: 'info', message: 'Account successfully activated. Please complete your profile.'} );
  1164. }
  1165. });
  1166. });
  1167. }
  1168. });
  1169. });
  1170. // Logut user
  1171. app.get( '/logout', checkAjax, function( req, res ) {
  1172. var sid = req.sessionID;
  1173. // Find user by session id
  1174. User.findOne( { 'session' : sid }, function( err, user ) {
  1175. if( user ) {
  1176. // Empty out session id
  1177. user.session = '';
  1178. // Save user to database
  1179. user.save( function( err ) {
  1180. sendJson(res, {status: 'ok', message: 'Successfully logged out'});
  1181. });
  1182. } else {
  1183. sendJson(res, {status: 'ok', message: ''});
  1184. }
  1185. });
  1186. });
  1187. // Recieve profile edit page form
  1188. app.post( '/profile', checkAjax, loadUser, loggedIn, function( req, res ) {
  1189. var user = req.user;
  1190. var fields = req.body;
  1191. var error = false;
  1192. var wasComplete = user.isComplete;
  1193. if( ! fields.name ) {
  1194. return sendJson(res, {status: 'error', message: 'Please enter a valid name!'} );
  1195. } else {
  1196. user.name = fields.name;
  1197. }
  1198. if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
  1199. return sendJson(res, {status: 'error', message: 'Please select a valid affiliation!'} );
  1200. } else {
  1201. user.affil = fields.affiliation;
  1202. }
  1203. if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
  1204. // changing password
  1205. if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
  1206. if( fields.newPassword === fields.newPasswordConfirm ) {
  1207. // test password strength?
  1208. user.password = fields.newPassword;
  1209. } else {
  1210. return sendJson(res, {status: 'error', message: 'Mismatch in new password!'} );
  1211. }
  1212. } else {
  1213. return sendJson(res, {status: 'error', message: 'Please supply your existing password.'} );
  1214. }
  1215. }
  1216. user.major = fields.major;
  1217. user.bio = fields.bio;
  1218. user.showName = ( fields.showName ? true : false );
  1219. user.save( function( err ) {
  1220. if( err ) {
  1221. sendJson(res, {status: 'error', message: 'Unable to save user profile!'} );
  1222. } else {
  1223. if( ( user.isComplete ) && ( ! wasComplete ) ) {
  1224. sendJson(res, {status: 'ok', message: 'Your account is now fully activated. Thank you for joining FinalsClub!'} );
  1225. } else {
  1226. sendJson(res, {status:'ok', message:'Your profile was successfully updated!'} );
  1227. }
  1228. }
  1229. });
  1230. });
  1231. // Old Notes
  1232. function loadSubject( req, res, next ) {
  1233. if( url.parse( req.url ).pathname.match(/subject/) ) {
  1234. ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
  1235. if ( err || !subject) {
  1236. sendJson(res, {status: 'not_found', message: 'Subject with this ID does not exist'} )
  1237. } else {
  1238. req.subject = subject;
  1239. next()
  1240. }
  1241. })
  1242. } else {
  1243. next()
  1244. }
  1245. }
  1246. function loadOldCourse( req, res, next ) {
  1247. if( url.parse( req.url ).pathname.match(/course/) ) {
  1248. ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
  1249. if ( err || !course ) {
  1250. sendJson(res, {status: 'not_found', message: 'Course with this ID does not exist'} )
  1251. } else {
  1252. req.course = course;
  1253. next()
  1254. }
  1255. })
  1256. } else {
  1257. next()
  1258. }
  1259. }
  1260. var featuredCourses = [
  1261. {name: 'The Human Mind', 'id': 1563},
  1262. {name: 'Justice', 'id': 797},
  1263. {name: 'Protest Literature', 'id': 1681},
  1264. {name: 'Animal Cognition', 'id': 681},
  1265. {name: 'Positive Psychology', 'id': 1793},
  1266. {name: 'Social Psychology', 'id': 660},
  1267. {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
  1268. {name: 'Cyberspace in Court', 'id': 1446},
  1269. {name: 'Nazi Cinema', 'id': 2586},
  1270. {name: 'Media and the American Mind', 'id': 2583},
  1271. {name: 'Social Thought in Modern America', 'id': 2585},
  1272. {name: 'Major British Writers II', 'id': 869},
  1273. {name: 'Civil Procedure', 'id': 2589},
  1274. {name: 'Evidence', 'id': 2590},
  1275. {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
  1276. ];
  1277. app.get( '/learn', loadUser, function( req, res ) {
  1278. res.render( 'archive/learn', { 'courses' : featuredCourses } );
  1279. })
  1280. app.get( '/learn/random', checkAjax, function( req, res ) {
  1281. sendJson(res, {status: 'ok', data: '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id });
  1282. })
  1283. app.get( '/archive', checkAjax, loadUser, function( req, res ) {
  1284. ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
  1285. if ( err || subjects.length === 0) {
  1286. sendJson(res, {status: 'error', message: 'There was a problem gathering the archived courses, please try again later.'} );
  1287. } else {
  1288. sendJson(res, { 'subjects' : subjects, 'user': req.user.sanitized } );
  1289. }
  1290. })
  1291. })
  1292. app.get( '/archive/subject/:id', checkAjax, loadUser, loadSubject, function( req, res ) {
  1293. ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
  1294. if ( err || courses.length === 0 ) {
  1295. sendJson(res, {status: 'not_found', message: 'There are no archived courses'} );
  1296. } else {
  1297. sendJson(res, { 'courses' : courses, 'subject': req.subject, 'user': req.user.sanitized } );
  1298. }
  1299. })
  1300. })
  1301. app.get( '/archive/course/:id', checkAjax, loadUser, loadOldCourse, function( req, res ) {
  1302. ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
  1303. if ( err || notes.length === 0) {
  1304. sendJson(res, {status: 'not_found', message: 'There are no notes in this course'} );
  1305. } else {
  1306. notes = notes.map(function(note) { return note.sanitized });
  1307. sendJson(res, { 'notes': notes, 'course' : req.course, 'user': req.user.sanitized } );
  1308. }
  1309. })
  1310. })
  1311. app.get( '/archive/note/:id', checkAjax, loadUser, function( req, res ) {
  1312. console.log( "id="+req.params.id)
  1313. ArchivedNote.findById(req.params.id, function(err, note) {
  1314. if ( err || !note ) {
  1315. sendJson(res, {status: 'not_found', message: 'This is not a valid id for a note'} );
  1316. } else {
  1317. ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
  1318. if ( err || !course ) {
  1319. sendJson(res, {status: 'not_found', message: 'There is no course for this note'} )
  1320. } else {
  1321. sendJson(res, { 'layout' : 'notesLayout', 'note' : note, 'course': course, 'user': req.user.sanitized } );
  1322. }
  1323. })
  1324. }
  1325. })
  1326. })
  1327. app.get( '*', function(req, res) {
  1328. res.sendfile('public/index.html');
  1329. });
  1330. // socket.io server
  1331. // The finalsclub backchannel server uses socket.io to handle communication between the server and
  1332. // the browser which facilitates near realtime interaction. This allows the user to post questions
  1333. // and comments and other users to get those almost immediately after they are posted, without
  1334. // reloading the page or pressing a button to refresh.
  1335. //
  1336. // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
  1337. // saving the data to the database, and then sending it out to everyone else connected.
  1338. //
  1339. // Data types:
  1340. // Posts - Posts are the main items in backchannel, useful for questions or discussion points
  1341. // [[ example object needed with explanation E.G:
  1342. /*
  1343. Post: { postID: '999-1',
  1344. userID: '1234',
  1345. userName: 'Bob Jones',
  1346. userAffil: 'Instructor',
  1347. body: 'This is the text content of the post.',
  1348. comments: { {<commentObj>, <commentObj>, ...},
  1349. public: true,
  1350. votes: [ <userID>, <userID>, ...],
  1351. reports: [ <userID>, <userID>, ...]
  1352. }
  1353. Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
  1354. if anonymous: userName => 'Anonymous', userAffil => 'N/A'
  1355. */
  1356. //
  1357. //
  1358. //
  1359. // Comments - Comments are replies to posts, for clarification or answering questions
  1360. // [[ example object needed]]
  1361. // Votes - Votes signifyg a users approval of a post
  1362. // [[ example object needed]]
  1363. // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
  1364. // [[ example object needed]]
  1365. //
  1366. //
  1367. // Post Schema
  1368. // body - Main content of the post
  1369. // userId - Not currently used, but would contain the users id that made the post
  1370. // userName - Users name that made post
  1371. // userAffil - Users affiliation to their school
  1372. // public - Boolean which denotes if the post is public to everyone, or private to school users only
  1373. // date - Date post was made, updates when any comments are made for the post
  1374. // comments - An array of comments which contain a body, userName, and userAffil
  1375. // votes - An array of user ids which are the users that voted
  1376. // [[ example needed ]]
  1377. // reports - An array of user ids which are the users that reported the post
  1378. // [[ reports would be "this post is flagged as inappropriate"? ]]
  1379. // [[ bruml: consistent terminology needed ]]
  1380. //
  1381. // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
  1382. // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
  1383. // users the ability to make posts or comments that they might not otherwise due to not wanting
  1384. // the content of the post/comment to be attributed to them.
  1385. //
  1386. // Each time a user connects to the server, it passes through authorization which checks for a cookie
  1387. // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
  1388. // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
  1389. // user is logged in, and if so display information that otherwise might not be visible to them if they
  1390. // aren't apart of a particular school.
  1391. //
  1392. // After the authorization step, the client browser sends the lecture id which is rendered into the html
  1393. // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
  1394. // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
  1395. // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
  1396. // rendered in the browser.
  1397. //
  1398. // Everything else from this point on is handled in an event form and requires a user initiating it. The
  1399. // events are as follows.
  1400. //
  1401. // Post event
  1402. // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
  1403. // The server recieves the data, assembles a new post object for the database and then fills it with
  1404. // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
  1405. // replaced. If the user chose for the post to be private, then public will be set to false and it
  1406. // will be filtered from being sent to users not logged into and not having access to the school. Once
  1407. // the post has been created and saved into the database, it is sent to all connected users to that
  1408. // particular lecture, unless it is private, than only logged in users will get it.
  1409. //
  1410. // Vote event
  1411. // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
  1412. // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
  1413. // votes array, and then the post is subsequently saved back to the database and sent to all connected
  1414. // users unless the post is private, which then it will be only sent to logged in users.
  1415. //
  1416. // Report event
  1417. // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
  1418. // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
  1419. // Then the report is sent out to all connected users unless it is a private post, which will be only sent
  1420. // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
  1421. // bottom of the interface.
  1422. //
  1423. // Comment event
  1424. // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
  1425. // user name, and user affiliation are sent to the server, which are then used to find the parent post
  1426. // and then a new comment object is assembled. When new comments are made, it updates the posts date
  1427. // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
  1428. // to the top of the interface. The comment can be anonymous, which then will have the user
  1429. // name and affiliation stripped before saving to the database. The comment then will be sent out to all
  1430. // connected users unless the post is private, then only logged in users will recieve the comment.
  1431. var io = require( 'socket.io' ).listen( app );
  1432. var Post = mongoose.model( 'Post' );
  1433. io.set('authorization', function ( handshake, next ) {
  1434. var rawCookie = handshake.headers.cookie;
  1435. if (rawCookie) {
  1436. handshake.cookie = parseCookie(rawCookie);
  1437. handshake.sid = handshake.cookie['connect.sid'];
  1438. if ( handshake.sid ) {
  1439. app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
  1440. if( err ) {
  1441. handshake.user = false;
  1442. return next(null, true);
  1443. } else {
  1444. // bake a new session object for full r/w
  1445. handshake.session = new Session( handshake, session );
  1446. User.findOne( { session : handshake.sid }, function( err, user ) {
  1447. if( user ) {
  1448. handshake.user = user;
  1449. return next(null, true);
  1450. } else {
  1451. handshake.user = false;
  1452. return next(null, true);
  1453. }
  1454. });
  1455. }
  1456. })
  1457. }
  1458. } else {
  1459. data.user = false;
  1460. return next(null, true);
  1461. }
  1462. });
  1463. var backchannel = new Backchannel(app, io.of('/backchannel'), {
  1464. subscribe: function(lecture, send) {
  1465. Post.find({'lecture': lecture}, function(err, posts) {
  1466. send(posts);
  1467. });
  1468. },
  1469. post: function(fillPost) {
  1470. var post = new Post;
  1471. fillPost(post, function(send) {
  1472. post.save(function(err) {
  1473. send();
  1474. });
  1475. });
  1476. },
  1477. items: function(postId, addItem) {
  1478. Post.findById(postId, function( err, post ) {
  1479. addItem(post, function(send) {
  1480. post.save(function(err) {
  1481. send();
  1482. });
  1483. })
  1484. })
  1485. }
  1486. });
  1487. var counters = {};
  1488. var counts = io
  1489. .of( '/counts' )
  1490. .on( 'connection', function( socket ) {
  1491. // pull out user/session information etc.
  1492. var handshake = socket.handshake;
  1493. var userID = handshake.user._id;
  1494. var watched = [];
  1495. var noteID = null;
  1496. var timer = null;
  1497. socket.on( 'join', function( note ) {
  1498. if (handshake.user === false) {
  1499. noteID = note;
  1500. // XXX: replace by addToSet (once it's implemented in mongoose)
  1501. Note.findById( noteID, function( err, note ) {
  1502. if( note ) {
  1503. if( note.collaborators.indexOf( userID ) == -1 ) {
  1504. note.collaborators.push( userID );
  1505. note.save();
  1506. }
  1507. }
  1508. });
  1509. }
  1510. });
  1511. socket.on( 'watch', function( l ) {
  1512. var sendCounts = function() {
  1513. var send = {};
  1514. Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
  1515. async.forEach(
  1516. notes,
  1517. function( note, callback ) {
  1518. var id = note._id;
  1519. var count = note.collaborators.length;
  1520. send[ id ] = count;
  1521. callback();
  1522. }, function() {
  1523. socket.emit( 'counts', send );
  1524. timer = setTimeout( sendCounts, 5000 );
  1525. }
  1526. );
  1527. });
  1528. }
  1529. Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
  1530. notes.forEach( function( note ) {
  1531. watched.push( note._id );
  1532. });
  1533. });
  1534. sendCounts();
  1535. });
  1536. socket.on( 'disconnect', function() {
  1537. clearTimeout( timer );
  1538. if (handshake.user === false) {
  1539. // XXX: replace with $pull once it's available
  1540. if( noteID ) {
  1541. Note.findById( noteID, function( err, note ) {
  1542. if( note ) {
  1543. var index = note.collaborators.indexOf( userID );
  1544. if( index != -1 ) {
  1545. note.collaborators.splice( index, 1 );
  1546. }
  1547. note.save();
  1548. }
  1549. });
  1550. }
  1551. }
  1552. });
  1553. });
  1554. // Exception Catch-All
  1555. process.on('uncaughtException', function (e) {
  1556. console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
  1557. });
  1558. // Launch
  1559. // mongoose now exepects a mongo url
  1560. mongoose.connect( 'mongodb://localhost/fc' ); // FIXME: make relative to hostname
  1561. var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
  1562. app.listen( serverPort, function() {
  1563. console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
  1564. // if run as root, downgrade to the owner of this file
  1565. if (process.getuid() === 0) {
  1566. require('fs').stat(__filename, function(err, stats) {
  1567. if (err) { return console.log(err); }
  1568. process.setuid(stats.uid);
  1569. });
  1570. }
  1571. });
  1572. function isValidEmail(email) {
  1573. var re = /[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
  1574. return email.match(re);
  1575. }