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