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