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