Add initial documentation to app.js
[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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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( '/login' );
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;
501               } else {
502                 school.courses = [];
503               }
504               // This tells async (the module) that each iteration of forEach is
505               // done and will continue to call the rest until they have all been
506               // completed, at which time the last function below will be called.
507               callback();
508             });
509           });
510         },
511         // After all schools and courses have been found, render them
512         function( err ) {
513           res.render( 'schools', { 'schools' : schools } );
514         }
515       );
516     } else {
517       // If no schools have been found, display none
518       res.render( 'schools', { 'schools' : [] } );
519     }
520   });
521 });
522
523 // New course page
524 // Displays form to create new course
525 // Private, requires user to be authorized
526 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
527   // Load school from middleware
528   var school = req.school;
529
530   // If school was not loaded for whatever reason, or the user is not authorized
531   // then redirect to the main schools page.
532   if( ( ! school ) || ( ! school.authorized ) ) {
533     return res.redirect( '/schools' );
534   }
535
536   // If they are authorized and the school exists, then render the page
537   res.render( 'course/new', { 'school': school } );
538 });
539
540 // Recieves new course form
541 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
542   var school = req.school;
543   // Creates new course from Course Schema
544   var course = new Course;
545   // Gathers instructor information from form
546   var instructorEmail = req.body.email.toLowerCase();
547   var instructorName = req.body.instructorName;
548
549   // If school doesn't exist or user is not authorized redirect to main schools page
550   if( ( ! school ) || ( ! school.authorized ) ) {
551     res.redirect( '/schools' );
552   }
553
554   // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
555   if ( !instructorEmail || !instructorName ) {
556     req.flash( 'error', 'Invalid parameters!' )
557     return res.render( 'course/new' );
558   }
559
560   // Fill out the course with information from the form
561   course.number                         = req.body.number;
562   course.name                                   = req.body.name;
563   course.description    = req.body.description;
564   course.school                         = school._id;
565   course.creator      = req.user._id;
566   course.subject      = req.body.subject;
567   course.department   = req.body.department;
568
569   // Check if a user exists with the instructorEmail, if not then create
570   // a new user and send them an instructor welcome email.
571   User.findOne( { 'email' : instructorEmail }, function( err, user ) {
572     if ( !user ) {
573       var user          = new User;
574
575       user.name                                 = instructorName
576       user.email        = instructorEmail;
577       user.affil        = 'Instructor';
578       user.school       = school.name;
579
580       user.activated    = false;
581
582       // Validate instructorEmail
583       // XXX Probably could be done before checking db
584       if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
585         req.flash( 'error', 'Please enter a valid email' );
586         // XXX This needs to be fixed, this is not the proper flow
587         return res.redirect( '/register' );
588       }
589       // Once the new user information has been completed, save the user
590       // to the database then email them the instructor welcome email.
591       user.save(function( err ) {
592         // If there was an error saving the instructor, prompt the user to fill out
593         // the information again.
594         if ( err ) {
595           req.flash( 'error', 'Invalid parameters!' )
596           return res.render( 'course/new' );
597         } else {
598           var message = {
599             to                                  : user.email,
600
601             'subject'           : 'A non-profit open education initiative',
602
603             'template'  : 'instructorInvite',
604             'locals'            : {
605               'course'                  : course,
606               'school'                  : school,
607               'user'                            : user,
608               'serverHost'      : serverHost
609             }
610           };
611
612           mailer.send( message, function( err, result ) {
613             if( err ) {
614               console.log( 'Error inviting instructor to course!' );
615             } else {
616               console.log( 'Successfully invited instructor to course.' );
617             }
618           });
619
620           // After emails are sent, set the courses instructor to the
621           // new users id and then save the course to the database.
622           course.instructor = user._id;
623           course.save( function( err ) {
624             if( err ) {
625               // XXX better validation
626               req.flash( 'error', 'Invalid parameters!' );
627
628               return res.render( 'course/new' );
629             } else {
630               // Once the course has been completed email the admin with information
631               // on the course and new instructor
632               var message = {
633                 to                                      : ADMIN_EMAIL,
634
635                 'subject'               : school.name+' has a new course: '+course.name,
636
637                 'template'      : 'newCourse',
638                 'locals'                : {
639                   'course'                      : course,
640                   'instructor'  : user,
641                   'user'                                : req.user,
642                   'serverHost'  : serverHost
643                 }
644               };
645
646               mailer.send( message, function( err, result ) {
647                 if ( err ) {
648                   console.log( 'Error sending new course email to info@finalsclub.org' )
649                 } else {
650                   console.log( 'Successfully invited instructor to course')
651                 }
652               })
653               // Redirect the user to the schools page where they can see
654               // their new course.
655               // XXX Redirect to the new course instead
656               res.redirect( '/schools' );
657             }
658           });
659         }
660       })
661     } else {
662       // If the user exists, then check if they are already and instructor
663       if (user.affil === 'Instructor') {
664         // If they are an instructor, then save the course with the appropriate
665         // information and email the admin.
666         course.instructor = user._id;
667         course.save( function( err ) {
668           if( err ) {
669             // XXX better validation
670             req.flash( 'error', 'Invalid parameters!' );
671
672             return res.render( 'course/new' );
673           } else {
674             var message = {
675               to                                        : ADMIN_EMAIL,
676
677               'subject'         : school.name+' has a new course: '+course.name,
678
679               'template'        : 'newCourse',
680               'locals'          : {
681                 'course'                        : course,
682                 'instructor'  : user,
683                 'user'                          : req.user,
684                 'serverHost'    : serverHost
685               }
686             };
687
688             mailer.send( message, function( err, result ) {
689               if ( err ) {
690                 console.log( 'Error sending new course email to info@finalsclub.org' )
691               } else {
692                 console.log( 'Successfully invited instructor to course')
693               }
694             })
695             // XXX Redirect to the new course instead
696             res.redirect( '/schools' );
697           }
698         });
699       } else {
700         // The existing user isn't an instructor, so the user is notified of the error
701         // and the course isn't created.
702         req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
703         res.render( 'course/new' );
704       }
705     }
706   })
707 });
708
709 // Individual Course Listing
710 // Public with private information
711 app.get( '/course/:id', loadUser, loadCourse, function( req, res ) {
712   var userId = req.user._id;
713   var course = req.course;
714
715   // Check if the user is subscribed to the course
716   // XXX Not currently used for anything
717   var subscribed = course.subscribed( userId );
718
719   // Find lectures associated with this course and sort by name
720   Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
721     // Get course instructor information using their id
722     User.findById( course.instructor, function( err, instructor ) {
723       // Render course and lectures
724       res.render( 'course/index', { 'course' : course, 'instructor': instructor, 'subscribed' : subscribed, 'lectures' : lectures } );
725     })
726   });
727 });
728
729 // Delete Course
730 // XXX Non functioning 
731 // Will be used as apart of a larger admin interface for managing courses and
732 // lectures on the site.
733 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
734   var course = req.course;
735   var user = req.user;
736
737   if ( user.admin ) {
738     course.delete(function( err ) {
739       if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
740         else req.flash( 'info', 'Successfully removed course' )
741           res.redirect( '/schools' );
742     });
743   } else {
744     req.flash( 'error', 'You don\'t have permission to do that' )
745     res.redirect( '/schools' );
746   }
747 })
748
749 // Subscribe to course
750 // XXX Not currently used for anything
751 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
752   var course = req.course;
753   var userId = req.user._id;
754
755   course.subscribe( userId, function( err ) {
756     if( err ) {
757       req.flash( 'error', 'Error subscribing to course!' );
758     }
759
760     res.redirect( '/course/' + course._id );
761   });
762 });
763
764 // Unsubscribe from course
765 // XXX Not currently used for anything
766 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
767   var course = req.course;
768   var userId = req.user._id;
769
770   course.unsubscribe( userId, function( err ) {
771     if( err ) {
772       req.flash( 'error', 'Error unsubscribing from course!' );
773     }
774
775     res.redirect( '/course/' + course._id );
776   });
777 });
778
779 // Create new lecture
780 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
781   var courseId  = req.params.id;
782   var course            = req.course;
783   var lecture           = {};
784
785   // If course isn't valid or user isn't authorized for course, redirect
786   if( ( ! course ) || ( ! course.authorized ) ) {
787     return res.redirect( '/course/' + courseId );
788   }
789
790   // Render new lecture form
791   res.render( 'lecture/new', { 'lecture' : lecture } );
792 });
793
794 // Recieve New Lecture Form
795 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
796   var courseId  = req.params.id;
797   var course            = req.course;
798   // Create new lecture from Lecture schema
799   var lecture           = new Lecture;
800
801   if( ( ! course ) || ( ! course.authorized ) ) {
802     res.redirect( '/course/' + courseId );
803
804     return;
805   }
806
807   // Populate lecture with form data
808   lecture.name          = req.body.name;
809   lecture.date          = req.body.date;
810   lecture.course        = course._id;
811   lecture.creator = req.user._id;
812
813   // Save lecture to database
814   lecture.save( function( err ) {
815     if( err ) {
816       // XXX better validation
817       req.flash( 'error', 'Invalid parameters!' );
818
819       res.render( 'lecture/new', { 'lecture' : lecture } );
820     } else {
821       // XXX Redirect to new lecture instead
822       res.redirect( '/course/' + course._id );
823     }
824   });
825 });
826
827
828 // Display individual lecture and related notes
829 app.get( '/lecture/:id', loadUser, loadLecture, function( req, res ) {
830   var lecture   = req.lecture;
831
832   // Grab the associated course
833   // XXX this should be done with DBRefs eventually
834   Course.findById( lecture.course, function( err, course ) {
835     if( course ) {
836       // If course is found, find instructor information to be displayed on page
837       User.findById( course.instructor, function( err, instructor ) {
838         // Pull out our notes
839         Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
840           if ( !req.user.loggedIn || !req.lecture.authorized ) {
841             // Loop through notes and only return those that are public if the
842             // user is not logged in or not authorized for that lecture
843             notes = notes.filter(function( note ) {
844               if ( note.public ) return note;
845             })
846           }
847           // Render lecture and notes
848           res.render( 'lecture/index', {
849             'lecture'                   : lecture,
850             'course'                    : course,
851             'instructor'  : instructor,
852             'notes'                             : notes,
853             'counts'                    : counts,
854
855             'javascripts'       : [ 'counts.js' ]
856           });
857         });
858       })
859     } else {
860       // XXX with DBRefs we will be able to reassign orphaned courses/lecture/pads
861
862       req.flash( 'error', 'That lecture is orphaned!' );
863
864       res.redirect( '/' );
865     }
866   });
867 });
868
869 // Display new note form
870 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
871   var lectureId = req.params.id;
872   var lecture           = req.lecture;
873   var note                      = {};
874
875   if( ( ! lecture ) || ( ! lecture.authorized ) ) {
876     res.redirect( '/lecture/' + lectureId );
877
878     return;
879   }
880
881   res.render( 'notes/new', { 'note' : note } );
882 });
883
884 // Recieve new note form
885 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
886   var lectureId = req.params.id;
887   var lecture           = req.lecture;
888
889   if( ( ! lecture ) || ( ! lecture.authorized ) ) {
890     res.redirect( '/lecture/' + lectureId );
891
892     return;
893   }
894
895   // Create note from Note schema
896   var note              = new Note;
897
898   // Populate note from form data
899   note.name                     = req.body.name;
900   note.date                     = req.body.date;
901   note.lecture  = lecture._id;
902   note.public           = req.body.private ? false : true;
903   note.creator  = req.user._id;
904
905   // Save note to database
906   note.save( function( err ) {
907     if( err ) {
908       // XXX better validation
909       req.flash( 'error', 'Invalid parameters!' );
910
911       res.render( 'notes/new', { 'note' : note } );
912     } else {
913       // XXX Redirect to new note instead
914       res.redirect( '/lecture/' + lecture._id );
915     }
916   });
917 });
918
919
920 // Display individual note page
921 app.get( '/note/:id', loadUser, loadNote, function( req, res ) {
922   var note = req.note;
923   // Set read only id for etherpad-lite or false for later check
924   var roID = note.roID || false;
925
926   var lectureId = note.lecture;
927
928   // Count the amount of visits, but only once per session
929   if ( req.session.visited ) {
930     if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
931       req.session.visited.push( note._id );
932       note.addVisit();
933     }
934   } else {
935     req.session.visited = [];
936     req.session.visited.push( note._id );
937     note.addVisit();
938   }
939
940   // If a read only id exists process note
941   if (roID) {
942     processReq();
943   } else {
944     // If read only id doesn't, then fetch the read only id from the database and then
945     // process note.
946     // XXX Soon to be depracated due to a new API in etherpad that makes for a
947     // much cleaner solution.
948     db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
949       epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
950         if ( record ) {
951           roID = record.value.replace(/"/g, '');
952         } else {
953           roID = false;
954         }
955         processReq();
956       })
957     })
958   }
959
960   function processReq() {
961     // Find lecture
962     Lecture.findById( lectureId, function( err, lecture ) {
963       if( ! lecture ) {
964         req.flash( 'error', 'That notes page is orphaned!' );
965
966         res.redirect( '/' );
967       }
968       // Find notes based on lecture id, which will be displayed in a dropdown
969       // on the page
970       Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
971         if( !req.RO ) {
972           // User is logged in and sees full notepad
973
974           res.render( 'notes/index', {
975             'layout'                    : 'noteLayout',
976             'host'                              : serverHost,
977             'note'                              : note,
978             'lecture'                   : lecture,
979             'otherNotes'        : otherNotes,
980             'RO'                                        : false,
981             'roID'                              : roID,
982             'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
983             'javascripts'       : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
984           });
985         } else {
986           // User is not logged in and sees notepad that is public
987           res.render( 'notes/public', {
988             'layout'                    : 'noteLayout',
989             'host'                              : serverHost,
990             'note'                              : note,
991             'otherNotes'        : otherNotes,
992             'roID'                              : roID,
993             'lecture'                   : lecture,
994             'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
995             'javascripts'       : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
996           });
997         }
998       });
999     });
1000   }
1001 });
1002
1003 // Static pages and redirects
1004 app.get( '/about', loadUser, function( req, res ) {
1005   res.redirect( 'http://blog.finalsclub.org/about.html' );
1006 });
1007
1008 app.get( '/press', loadUser, function( req, res ) {
1009   res.render( 'static/press' );
1010 });
1011
1012 app.get( '/conduct', loadUser, function( req, res ) {
1013   res.render( 'static/conduct' );
1014 });
1015
1016 app.get( '/legal', loadUser, function( req, res ) {
1017   res.redirect( 'http://blog.finalsclub.org/legal.html' );
1018 });
1019
1020 app.get( '/contact', loadUser, function( req, res ) {
1021   res.redirect( 'http://blog.finalsclub.org/contact.html' );
1022 });
1023
1024 app.get( '/privacy', loadUser, function( req, res ) {
1025   res.render( 'static/privacy' );
1026 });
1027
1028
1029 // Authentication routes
1030 // These are used for logging in, logging out, registering
1031 // and other user authentication purposes
1032
1033 // Render login page
1034 app.get( '/login', function( req, res ) {
1035   log3("get login page")
1036
1037   res.render( 'login' );        
1038 });
1039
1040 // Recieve login form
1041 app.post( '/login', function( req, res ) {
1042   var email              = req.body.email;
1043   var password = req.body.password;
1044   log3("post login ...")
1045
1046   // Find user from email
1047   User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1048     log3(err) 
1049     log3(user) 
1050
1051     // If user exists, check if activated, if not notify them and send them to
1052     // the login form
1053     if( user ) {
1054       if( ! user.activated ) {
1055         // (undocumented) markdown-esque link functionality in req.flash
1056         req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1057
1058         req.session.activateCode = user._id;
1059
1060         res.render( 'login' );
1061       } else {
1062         // If user is activated, check if their password is correct
1063         if( user.authenticate( password ) ) {
1064           log3("pass ok") 
1065
1066           var sid = req.sessionID;
1067
1068           user.session = sid;
1069
1070           // Set the session then save the user to the database
1071           user.save( function() {
1072             var redirect = req.session.redirect;
1073
1074             // login complete, remember the user's email for next time
1075             req.session.email = email;
1076
1077             // alert the successful login
1078             req.flash( 'info', 'Successfully logged in!' );
1079
1080             // redirect to profile if we don't have a stashed request
1081             res.redirect( redirect || '/profile' );
1082           });
1083         } else {
1084           // Notify user of bad login
1085           req.flash( 'error', 'Invalid login!' );
1086
1087           res.render( 'login' );
1088         }
1089       }
1090     } else {
1091       // Notify user of bad login
1092       log3("bad login")
1093       req.flash( 'error', 'Invalid login!' );
1094
1095       res.render( 'login' );
1096     }
1097   });
1098 });
1099
1100 // Request reset password
1101 app.get( '/resetpw', function( req, res ) {
1102   log3("get resetpw page");
1103   res.render( 'resetpw' );
1104 });
1105
1106 // Display reset password from requested email
1107 app.get( '/resetpw/:id', function( req, res ) {
1108   var resetPassCode = req.params.id
1109   res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1110 });
1111
1112 // Recieve reset password request form
1113 app.post( '/resetpw', function( req, res ) {
1114   log3("post resetpw");
1115   var email = req.body.email
1116
1117
1118   // Search for user
1119   User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1120     if( user ) {
1121
1122       // If user exists, create reset code
1123       var resetPassCode = hat(64);
1124       user.setResetPassCode(resetPassCode);
1125
1126       // Construct url that the user can then click to reset password
1127       var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1128
1129       // Save user to database
1130       user.save( function( err ) {
1131         log3('save '+user.email);
1132
1133         // Construct email and send it to the user
1134         var message = {
1135           'to'                          : user.email,
1136
1137           'subject'             : 'Your FinalsClub.org Password has been Reset!',
1138
1139           'template'    : 'userPasswordReset',
1140           'locals'              : {
1141             'resetPassCode'             : resetPassCode,
1142             'resetPassUrl'              : resetPassUrl
1143           }
1144         };
1145
1146         mailer.send( message, function( err, result ) {
1147           if( err ) {
1148             // XXX: Add route to resend this email
1149
1150             console.log( 'Error sending user password reset email!' );
1151           } else {
1152             console.log( 'Successfully sent user password reset email.' );
1153           }
1154
1155         }); 
1156
1157         // Render request success page
1158         res.render( 'resetpw-success', { 'email' : email } );
1159       });                       
1160     } else {
1161       // Notify of error
1162       res.render( 'resetpw-error', { 'email' : email } );
1163     }
1164   });
1165 });
1166
1167 // Recieve reset password form
1168 app.post( '/resetpw/:id', function( req, res ) {
1169   log3("post resetpw.code");
1170   var resetPassCode = req.params.id
1171   var email = req.body.email
1172   var pass1 = req.body.pass1
1173   var pass2 = req.body.pass2
1174
1175   // Find user by email
1176   User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1177     var valid = false;
1178     // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1179     // save user with new password and display success message.
1180     if( user ) {
1181       var valid = user.resetPassword(resetPassCode, pass1, pass2);
1182       if (valid) {
1183         user.save( function( err ) {
1184           res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );               
1185         });                     
1186       }
1187     } 
1188
1189     // If there was a problem, notify user
1190     if (!valid) {
1191       res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1192     }
1193   });
1194 });
1195
1196 // Display registration page
1197 app.get( '/register', function( req, res ) {
1198   log3("get reg page");
1199
1200   // Populate school dropdown list
1201   School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1202     res.render( 'register', { 'schools' : schools } );
1203   })
1204 });
1205
1206 // Recieve registration form
1207 app.post( '/register', function( req, res ) {
1208   var sid = req.sessionId;
1209
1210   // Create new user from User schema
1211   var user = new User;
1212
1213   // Populate user from form
1214   user.email        = req.body.email.toLowerCase();
1215   user.password     = req.body.password;
1216   user.session      = sid;
1217   // If school is set to other, then fill in school as what the
1218   // user entered
1219   user.school                           = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1220   user.name         = req.body.name;
1221   user.affil        = req.body.affil;
1222   user.activated    = false;
1223
1224   // Validate email
1225   if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1226     req.flash( 'error', 'Please enter a valid email' );
1227     return res.redirect( '/register' );
1228   }
1229
1230   // Check if password is greater than 6 characters, otherwise notify user
1231   if ( req.body.password.length < 6 ) {
1232     req.flash( 'error', 'Please enter a password longer than eight characters' );
1233     return res.redirect( '/register' );
1234   }
1235
1236   // Pull out hostname from email
1237   var hostname = user.email.split( '@' ).pop();
1238
1239   // Check if email is from one of the special domains
1240   if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1241     user.admin = true;
1242   }
1243
1244   // Save user to database
1245   user.save( function( err ) {
1246     // If error, check if it is because the user already exists, if so
1247     // get the user information and let them know
1248     if ( err ) {
1249       if( /dup key/.test( err.message ) ) {
1250         // attempting to register an existing address
1251         User.findOne({ 'email' : user.email }, function(err, result ) {
1252           if (result.activated) {
1253             // If activated, make sure they know how to contact the admin
1254             req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1255             return res.redirect( '/register' )
1256           } else {
1257             // If not activated, direct them to the resendActivation page
1258             req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1259             return res.redirect( '/resendActivation' )
1260           }
1261         });
1262       } else {
1263         // If any other type of error, prompt them to enter the registration again
1264         req.flash( 'error', 'An error occurred during registration.' );
1265
1266         return res.redirect( '/register' );
1267       }
1268     } else {
1269       // send user activation email
1270       sendUserActivation( user );
1271
1272       // Check if the hostname matches any in the approved schools
1273       School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1274         if( school ) {
1275           // If there is a match, send associated welcome message
1276           sendUserWelcome( user, true );
1277           log3('school recognized '+school.name);
1278           // If no users exist for the school, create empty array
1279           if (!school.users) school.users = [];
1280           // Add user to the school
1281           school.users.push( user._id );
1282
1283           // Save school to the database
1284           school.save( function( err ) {
1285             log3('school.save() done');
1286             // Notify user that they have been added to the school
1287             req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1288             res.redirect( '/' );
1289           });
1290           // Construct admin email about user registration
1291           var message = {
1292             'to'       : ADMIN_EMAIL,
1293
1294             'subject'  : 'FC User Registration : User added to ' + school.name,
1295
1296             'template' : 'userSchool',
1297             'locals'   : {
1298               'user'   : user
1299             }
1300           }
1301         } else {
1302           // If there isn't a match, send associated welcome message
1303           sendUserWelcome( user, false );
1304           // Tell user to check for activation link
1305           req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1306           res.redirect( '/' );
1307           // Construct admin email about user registration
1308           var message = {
1309             'to'       : ADMIN_EMAIL,
1310
1311             'subject'  : 'FC User Registration : Email did not match any schools',
1312
1313             'template' : 'userNoSchool',
1314             'locals'   : {
1315               'user'   : user
1316             }
1317           }
1318         }
1319         // Send email to admin
1320         mailer.send( message, function( err, result ) {
1321           if ( err ) {
1322
1323             console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1324           } else {
1325             console.log( 'Successfully sent user has no school email to admin.' );
1326           }
1327         })
1328
1329       });
1330     }
1331
1332   });
1333 });
1334
1335 // Display resendActivation request page
1336 app.get( '/resendActivation', function( req, res ) {
1337   var activateCode = req.session.activateCode;
1338
1339   // Check if user exists by activateCode set in their session
1340   User.findById( activateCode, function( err, user ) {
1341     if( ( ! user ) || ( user.activated ) ) {
1342       res.redirect( '/' );
1343     } else {
1344       // Send activation and redirect to login
1345       sendUserActivation( user );
1346
1347       req.flash( 'info', 'Your activation code has been resent.' );
1348
1349       res.redirect( '/login' );
1350     }
1351   });
1352 });
1353
1354 // Display activation page
1355 app.get( '/activate/:code', function( req, res ) {
1356   var code = req.params.code;
1357
1358   // XXX could break this out into a middleware
1359   if( ! code ) {
1360     res.redirect( '/' );
1361   }
1362
1363   // Find user by activation code
1364   User.findById( code, function( err, user ) {
1365     if( err || ! user ) {
1366       // If not found, notify user of invalid code
1367       req.flash( 'error', 'Invalid activation code!' );
1368
1369       res.redirect( '/' );
1370     } else {
1371       // If valid, then activate user
1372       user.activated = true;
1373
1374       // Regenerate our session and log in as the new user
1375       req.session.regenerate( function() {
1376         user.session = req.sessionID;
1377
1378         // Save user to database
1379         user.save( function( err ) {
1380           if( err ) {
1381             req.flash( 'error', 'Unable to activate account.' );
1382
1383             res.redirect( '/' );
1384           } else {
1385             req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1386
1387             res.redirect( '/profile' );
1388           }
1389         });
1390       });
1391     }
1392   });
1393 });
1394
1395 // Logut user
1396 app.get( '/logout', function( req, res ) {
1397   var sid = req.sessionID;
1398
1399   // Find user by session id
1400   User.findOne( { 'session' : sid }, function( err, user ) {
1401     if( user ) {
1402       // Empty out session id
1403       user.session = '';
1404
1405       // Save user to database
1406       user.save( function( err ) {
1407         res.redirect( '/' );
1408       });
1409     } else {
1410       res.redirect( '/' );
1411     }
1412   });
1413 });
1414
1415 // Display users profile page
1416 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1417   var user = req.user;
1418
1419   res.render( 'profile/index', { 'user' : user } );
1420 });
1421
1422 // Recieve profile edit page form
1423 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1424   var user              = req.user;
1425   var fields    = req.body;
1426
1427   var error                             = false;
1428   var wasComplete       = user.isComplete;
1429
1430   if( ! fields.name ) {
1431     req.flash( 'error', 'Please enter a valid name!' );
1432
1433     error = true;
1434   } else {
1435     user.name = fields.name;
1436   }
1437
1438   if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1439     req.flash( 'error', 'Please select a valid affiliation!' );
1440
1441     error = true;
1442   } else {
1443     user.affil = fields.affiliation;
1444   }
1445
1446   if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1447     // changing password
1448     if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1449       if( fields.newPassword === fields.newPasswordConfirm ) {
1450         // test password strength?
1451
1452         user.password = fields.newPassword;
1453       } else {
1454         req.flash( 'error', 'Mismatch in new password!' );
1455
1456         error = true;
1457       }
1458     } else {
1459       req.flash( 'error', 'Please supply your existing password.' );
1460
1461       error = true;
1462     }
1463   }
1464
1465   user.major            = fields.major;
1466   user.bio                      = fields.bio;
1467
1468   user.showName = ( fields.showName ? true : false );
1469
1470   if( ! error ) {
1471     user.save( function( err ) {
1472       if( err ) {
1473         req.flash( 'error', 'Unable to save user profile!' );
1474       } else {
1475         if( ( user.isComplete ) && ( ! wasComplete ) ) {
1476           req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1477
1478           res.redirect( '/' );
1479         } else {
1480           res.render( 'info', 'Your profile was successfully updated!' );
1481
1482           res.render( 'profile/index', { 'user' : user } );
1483         }
1484       }
1485     });
1486   } else {
1487     res.render( 'profile/index', { 'user' : user } );
1488   }
1489 });
1490
1491
1492 // Old Notes
1493
1494 function loadSubject( req, res, next ) {
1495   if( url.parse( req.url ).pathname.match(/subject/) ) {
1496     ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1497       if ( err ) {
1498         req.flash( 'error', 'Subject with this ID does not exist' )
1499         res.redirect( '/archive' );
1500       } else {
1501         req.subject = subject;
1502         next()
1503       }
1504     })
1505   } else {
1506     next()
1507   } 
1508 }
1509
1510 function loadOldCourse( req, res, next ) {
1511   if( url.parse( req.url ).pathname.match(/course/) ) {
1512     ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1513       if ( err ) {
1514         req.flash( 'error', 'Course with this ID does not exist' )
1515         res.redirect( '/archive' );
1516       } else {
1517         req.course = course;
1518         next()
1519       }
1520     })
1521   } else {
1522     next()
1523   } 
1524 }
1525
1526 var featuredCourses = [
1527   {name: 'The Human Mind', 'id': 1563},
1528   {name: 'Justice', 'id': 797},
1529   {name: 'Protest Literature', 'id': 1681},
1530   {name: 'Animal Cognition', 'id': 681},
1531   {name: 'Positive Psychology', 'id': 1793},
1532   {name: 'Social Psychology', 'id': 660},
1533   {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1534   {name: 'Cyberspace in Court', 'id': 1446},
1535   {name: 'Nazi Cinema', 'id': 2586},
1536   {name: 'Media and the American Mind', 'id': 2583},
1537   {name: 'Social Thought in Modern America', 'id': 2585},
1538   {name: 'Major British Writers II', 'id': 869},
1539   {name: 'Civil Procedure', 'id': 2589},
1540   {name: 'Evidence', 'id': 2590},
1541   {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1542 ];
1543
1544 app.get( '/learn', loadUser, function( req, res ) {
1545   res.render( 'archive/learn', { 'courses' : featuredCourses } );
1546 })
1547
1548 app.get( '/learn/random', loadUser, function( req, res ) {
1549   res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1550 })
1551
1552 app.get( '/archive', loadUser, function( req, res ) {
1553   ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1554     if ( err ) {
1555       req.flash( 'error', 'There was a problem gathering the archived courses, please try again later.' );
1556       res.redirect( '/' );
1557     } else {
1558       res.render( 'archive/index', { 'subjects' : subjects } );
1559     }
1560   })
1561 })
1562
1563 app.get( '/archive/subject/:id', loadUser, loadSubject, function( req, res ) {
1564   ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1565     if ( err ) {
1566       req.flash( 'error', 'There are no archived courses' );
1567       res.redirect( '/' );
1568     } else {
1569       res.render( 'archive/courses', { 'courses' : courses, 'subject': req.subject } );
1570     }
1571   })
1572 })
1573
1574 app.get( '/archive/course/:id', loadUser, loadOldCourse, function( req, res ) {
1575   ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1576     if ( err ) {
1577       req.flash( 'error', 'There are no notes in this course' );
1578       res.redirect( '/archive' );
1579     } else {
1580       res.render( 'archive/notes', { 'notes' : notes, 'course' : req.course } );
1581     }
1582   })
1583 })
1584
1585 app.get( '/archive/note/:id', loadUser, function( req, res ) {
1586   ArchivedNote.findById(req.params.id, function(err, note) {
1587     if ( err ) {
1588       req.flash( 'error', 'This is not a valid id for a note' );
1589       res.redirect( '/archive' );
1590     } else {
1591       ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1592         if ( err ) {
1593           req.flash( 'error', 'There is no course for this note' )
1594           res.redirect( '/archive' )
1595         } else {
1596           res.render( 'archive/note', { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
1597         }
1598       })
1599     }
1600   })
1601 })
1602
1603 // socket.io server
1604
1605 var io = require( 'socket.io' ).listen( app );
1606
1607 var Post = mongoose.model( 'Post' );
1608
1609 io.set('authorization', function ( handshake, next ) {
1610   var rawCookie = handshake.headers.cookie;
1611   if (rawCookie) {
1612     handshake.cookie = parseCookie(rawCookie);
1613     handshake.sid = handshake.cookie['connect.sid'];
1614
1615     if ( handshake.sid ) {
1616       app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1617         if( err ) {
1618           handshake.user = false;
1619           return next(null, true);
1620         } else {
1621           // bake a new session object for full r/w
1622           handshake.session = new Session( handshake, session );
1623
1624           User.findOne( { session : handshake.sid }, function( err, user ) {
1625             if( user ) {
1626               handshake.user = user;
1627               return next(null, true);
1628             } else {
1629               handshake.user = false;
1630               return next(null, true);
1631             }
1632           });
1633         }
1634       })
1635     }
1636   } else {
1637     data.user = false;
1638     return next(null, true);
1639   }
1640 });
1641
1642
1643 var backchannel = io
1644 .of( '/backchannel' )
1645 .on( 'connection', function( socket ) {
1646
1647   socket.on('subscribe', function(lecture, cb) {
1648     socket.join(lecture);
1649     Post.find({'lecture': lecture}, function(err, posts) {
1650       if (socket.handshake.user) {
1651         cb(posts);
1652       } else {
1653         var posts = posts.filter(
1654           function(post) {
1655           if (post.public)
1656             return post;
1657         }
1658         )
1659         cb(posts)
1660       }
1661     });
1662   });
1663
1664   socket.on('post', function(res) {
1665     var post = new Post;
1666     var _post = res.post;
1667     var lecture = res.lecture;
1668     post.lecture = lecture;
1669     if ( _post.anonymous ) {
1670       post.userid               = 0;
1671       post.userName     = 'Anonymous';
1672       post.userAffil = 'N/A';
1673     } else {
1674       post.userName = _post.userName;
1675       post.userAffil = _post.userAffil;
1676     }
1677
1678     post.public = _post.public;
1679     post.date = new Date();
1680     post.body = _post.body;
1681     post.votes = [];
1682     post.reports = [];
1683     post.save(function(err) {
1684       if (err) {
1685         // XXX some error handling
1686         console.log(err);
1687       } else {
1688         if (post.public) {
1689           backchannel.in(lecture).emit('post', post);
1690         } else {
1691           privateEmit(lecture, 'post', post);
1692         }
1693       }
1694     });
1695   });
1696
1697   socket.on('vote', function(res) {
1698     var vote = res.vote;
1699     var lecture = res.lecture;
1700     Post.findById(vote.parentid, function( err, post ) {
1701       if (!err) {
1702         if (post.votes.indexOf(vote.userid) == -1) {
1703           post.votes.push(vote.userid);
1704           post.save(function(err) {
1705             if (err) {
1706               // XXX error handling
1707             } else {
1708               if (post.public) {
1709                 backchannel.in(lecture).emit('vote', vote);
1710               } else {
1711                 privteEmit(lecture, 'vote', vote);
1712               }
1713             }
1714           });
1715         }
1716       }
1717     })
1718   });
1719
1720   socket.on('report', function(res) {
1721     var report = res.report;
1722     var lecture = res.lecture;
1723     Post.findById(report.parentid, function( err, post ){
1724       if (!err) {
1725         if (post.reports.indexOf(report.userid) == -1) {
1726           post.reports.push(report.userid);
1727           post.save(function(err) {
1728             if (err) {
1729               // XXX error handling
1730             } else {
1731               if (post.public) {
1732                 backchannel.in(lecture).emit('report', report);
1733               } else {
1734                 privateEmit(lecture, 'report', report);
1735               }
1736             }
1737           });
1738         }
1739       }
1740     })
1741   });
1742
1743   socket.on('comment', function(res) {
1744     var comment = res.comment;
1745     var lecture = res.lecture;
1746     console.log('anon', comment.anonymous);
1747     if ( comment.anonymous ) {
1748       comment.userid            = 0;
1749       comment.userName  = 'Anonymous';
1750       comment.userAffil = 'N/A';
1751     }
1752     Post.findById(comment.parentid, function( err, post ) {
1753       if (!err) {
1754         post.comments.push(comment);
1755         post.date = new Date();
1756         post.save(function(err) {
1757           if (err) {
1758             console.log(err);
1759           } else {
1760             if (post.public) {
1761               backchannel.in(lecture).emit('comment', comment);
1762             } else {
1763               privateEmit(lecture, 'comment', comment);
1764             }
1765           }
1766         })
1767       }
1768     })
1769   });
1770
1771   function privateEmit(lecture, event, data) {
1772     backchannel.clients(lecture).forEach(function(socket) {
1773       if (socket.handshake.user)
1774         socket.emit(event, data);
1775     })
1776   }
1777
1778   socket.on('disconnect', function() {
1779     //delete clients[socket.id];
1780   });
1781 });
1782
1783
1784 var counters = {};
1785
1786 var counts = io
1787 .of( '/counts' )
1788 .on( 'connection', function( socket ) {
1789   // pull out user/session information etc.
1790   var handshake = socket.handshake;
1791   var userID            = handshake.user._id;
1792
1793   var watched           = [];
1794   var noteID            = null;
1795
1796   var timer                     = null;
1797
1798   socket.on( 'join', function( note ) {
1799     if (handshake.user === false) {
1800       noteID                    = note;
1801       // XXX: replace by addToSet (once it's implemented in mongoose)
1802       Note.findById( noteID, function( err, note ) {
1803         if( note ) {
1804           if( note.collaborators.indexOf( userID ) == -1 ) {
1805             note.collaborators.push( userID );
1806             note.save();
1807           }
1808         }
1809       });
1810     }
1811   });
1812
1813   socket.on( 'watch', function( l ) {
1814     var sendCounts = function() {
1815       var send = {};
1816
1817       Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1818         async.forEach(
1819           notes,
1820           function( note, callback ) {
1821             var id              = note._id;
1822             var count   = note.collaborators.length;
1823
1824             send[ id ] = count;
1825
1826             callback();
1827           }, function() {
1828             socket.emit( 'counts', send );
1829
1830             timer = setTimeout( sendCounts, 5000 );
1831           }
1832         );
1833       });
1834     }
1835
1836     Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1837       notes.forEach( function( note ) {
1838         watched.push( note._id );
1839       });
1840     });
1841
1842     sendCounts();
1843   });
1844
1845   socket.on( 'disconnect', function() {
1846     clearTimeout( timer );
1847
1848     if (handshake.user === false) {
1849       // XXX: replace with $pull once it's available
1850       if( noteID ) {
1851         Note.findById( noteID, function( err, note ) {
1852           if( note ) {
1853             var index = note.collaborators.indexOf( userID );
1854
1855             if( index != -1 ) {
1856               note.collaborators.splice( index, 1 );
1857             }
1858
1859             note.save();
1860           }
1861         });
1862       }
1863     }
1864   });
1865 });
1866
1867 // Exception Catch-All
1868
1869 process.on('uncaughtException', function (e) {
1870   console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1871 });
1872
1873
1874 // Launch
1875
1876 mongoose.connect( app.set( 'dbUri' ) );
1877 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1878
1879 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1880
1881 app.listen( serverPort, function() {
1882   console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1883
1884   // if run as root, downgrade to the owner of this file
1885   if (process.getuid() === 0) {
1886     require('fs').stat(__filename, function(err, stats) {
1887       if (err) { return console.log(err); }
1888       process.setuid(stats.uid);
1889     });
1890   }
1891 });
1892
1893 function isValidEmail(email) {
1894   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])?/;
1895   return email.match(re);
1896 }