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   // Static files are loaded when no dynamic views match.
159   app.use( express.static( __dirname + '/public' ) );
160   // Sets the routers middleware to load after everything set
161   // before it, but before static files.
162   app.use( app.router );
163
164   app.use(express.logger({ format: ':method :url' }));
165   // This is the errorHandler set in configuration earlier
166   // being set to a variable to be used after all other
167   // middleware is loaded. Error handling should always
168   // come last or near the bottom.
169   var errorHandler = app.set( 'errorHandler' );
170
171   app.use( errorHandler );
172 });
173
174
175 // Mailer functions and helpers
176 // These are helper functions that make for cleaner code.
177
178 // sendUserActivation is for when a user registers and
179 // first needs to activate their account to use it.
180 function sendUserActivation( user ) {
181   var message = {
182     'to'                                : user.email,
183
184     'subject'           : 'Activate your FinalsClub.org Account',
185
186     // Templates are in the email folder and use ejs
187     'template'  : 'userActivation',
188     // Locals are used inside ejs so dynamic information
189     // can be rendered properly.
190     'locals'            : {
191       'user'                            : user,
192       'serverHost'      : serverHost
193     }
194   };
195
196   // Email is sent here
197   mailer.send( message, function( err, result ) {
198     if( err ) {
199       // XXX: Add route to resend this email
200       console.log( 'Error sending user activation email\nError Message: '+err.Message );
201     } else {
202       console.log( 'Successfully sent user activation email.' );
203     }
204   });
205 }
206
207 // sendUserWelcome is for when a user registers and
208 // a welcome email is sent.
209 function sendUserWelcome( user, school ) {
210   // If a user is not apart of a supported school, they are
211   // sent a different template than if they are apart of a
212   // supported school.
213   var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
214   var message = {
215     'to'                                : user.email,
216
217     'subject'           : 'Welcome to FinalsClub',
218
219     'template'  : template,
220     'locals'            : {
221       'user'                            : user,
222       'serverHost'      : serverHost
223     }
224   };
225
226   mailer.send( message, function( err, result ) {
227     if( err ) {
228       // XXX: Add route to resend this email
229       console.log( 'Error sending user welcome email\nError Message: '+err.Message );
230     } else {
231       console.log( 'Successfully sent user welcome email.' );
232     }
233   });
234 }
235
236 // Helper middleware
237 // These functions are used later in the routes to help
238 // load information and variables, as well as handle
239 // various instances like checking if a user is logged in
240 // or not.
241 function loggedIn( req, res, next ) {
242   // If req.user is set, then pass on to the next function
243   // or else alert the user with an error message.
244   if( req.user ) {
245     next();
246   } else {
247     req.flash( 'error', 'You must be logged in to access that feature!' );
248     res.redirect( '/' );
249   }
250 }
251
252 // This loads the user if logged in
253 function loadUser( req, res, next ) {
254   var sid = req.sessionID;
255
256   console.log( 'got request from session ID: %s', sid );
257
258   // Find a user based on their stored session id
259   User.findOne( { session : sid }, function( err, user ) {
260
261     log3(err);
262     log3(user);
263
264     // If a user is found then set req.user the contents of user
265     // and make sure req.user.loggedIn is true.
266     if( user ) {
267       req.user = user;
268
269       req.user.loggedIn = true;
270
271       log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
272
273       // Check if a user is activated. If not, then redirec
274       // to the homepage and tell them to check their email
275       // for the activation email.
276       if( req.user.activated ) {
277         // Is the user's profile complete? If not, redirect to their profile
278         if( ! req.user.isComplete ) {
279           if( url.parse( req.url ).pathname != '/profile' ) {
280             req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
281
282             res.redirect( '/profile' );
283           } else {
284             next();
285           }
286         } else {
287           next();
288         }
289       } else {
290         req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
291
292         res.redirect( '/' );
293       }
294     } else {
295       // If no user record was found, then we store the requested
296       // path they intended to view and redirect them after they
297       // login if it is requred.
298       var path = url.parse( req.url ).pathname;
299       req.session.redirect = path;
300
301       // Set req.user to an empty object so it doesn't throw errors
302       // later on that it isn't defined.
303       req.user = {
304         sanitized: {}
305       };
306
307       next();
308     }
309   });
310 }
311
312 // loadSchool is used to load a school by it's id
313 function loadSchool( req, res, next ) {
314   var user                      = req.user;
315   var schoolId  = req.params.id;
316
317   School.findById( schoolId, function( err, school ) {
318     if( school ) {
319       req.school = school;
320
321       // If a school is found, the user is checked to see if they are
322       // authorized to see or interact with anything related to that
323       // school.
324       school.authorize( user, function( authorized ){
325         req.school.authorized = authorized;
326         next();
327       });
328     } else {
329       // If no school is found, display an appropriate error.
330       res.json( {status: 'error', message: 'Invalid school specified!'} );
331     }
332   });
333 }
334
335 // loadSchool is used to load a course by it's id
336 function loadCourse( req, res, next ) {
337   var user                      = req.user;
338   var courseId  = req.params.id;
339
340   Course.findById( courseId, function( err, course ) {
341     if( course && !course.deleted ) {
342       req.course = course;
343
344       // If a course is found, the user is checked to see if they are
345       // authorized to see or interact with anything related to that
346       // school.
347       course.authorize( user, function( authorized )  {
348         req.course.authorized = authorized;
349
350         next();
351       });
352     } else {
353       // If no course is found, display an appropriate error.
354       res.json( {status: 'error', message: 'Invalid course specified!'} );
355     }
356   });
357 }
358
359 // loadLecture is used to load a lecture by it's id
360 function loadLecture( req, res, next ) {
361   var user                      = req.user;
362   var lectureId = req.params.id;
363
364   Lecture.findById( lectureId, function( err, lecture ) {
365     if( lecture && !lecture.deleted ) {
366       req.lecture = lecture;
367
368       // If a lecture is found, the user is checked to see if they are
369       // authorized to see or interact with anything related to that
370       // school.
371       lecture.authorize( user, function( authorized ) {
372         req.lecture.authorized = authorized;
373
374         next();
375       });
376     } else {
377       // If no lecture is found, display an appropriate error.
378       res.json( {status: 'error', message: 'Invalid lecture specified!'} );
379     }
380   });
381 }
382
383 // loadNote is used to load a note by it's id
384 // This is a lot more complicated than the above
385 // due to public/private handling of notes.
386 function loadNote( req, res, next ) {
387   var user       = req.user ? req.user : false;
388   var noteId = req.params.id;
389
390   Note.findById( noteId, function( err, note ) {
391     // If a note is found, and user is set, check if
392     // user is authorized to interact with that note.
393     if( note && user && !note.deleted ) {
394       note.authorize( user, function( auth ) {
395         if( auth ) {
396           // If authorzied, then set req.note to be used later
397           req.note = note;
398
399           next();
400         } else if ( note.public ) {
401           // If not authorized, but the note is public, then
402           // designate the note read only (RO) and store req.note
403           req.RO = true;
404           req.note = note;
405
406           next();
407         } else {
408           // If the user is not authorized and the note is private
409           // then display and error.
410           res.json( {status: 'error', message: 'You do not have permission to access that note.'} );
411         }
412       })
413     } else if ( note && note.public && !note.deleted ) {
414       // If note is found, but user is not set because they are not
415       // logged in, and the note is public, set the note to read only
416       // and store the note for later.
417       req.note = note;
418       req.RO = true;
419
420       next();
421     } else if ( note && !note.public && !note.deleted ) {
422       // If the note is found, but user is not logged in and the note is
423       // not public, then ask them to login to view the note. Once logged
424       // in they will be redirected to the note, at which time authorization
425       // handling will be put in effect above.
426       //req.session.redirect = '/note/' + note._id;
427       res.json( {status: 'error', message: 'You must be logged in to view that note.'} );
428     } else {
429       // No note was found
430       res.json( {status: 'error', message: 'Invalid note specified!'} );
431     }
432   });
433 }
434
435 function checkAjax( req, res, next ) {
436   if ( req.xhr ) {
437     next();
438   } else {
439     res.sendfile( 'public/index.html' );
440   }
441 }
442
443 // Dynamic Helpers are loaded automatically into views
444 app.dynamicHelpers( {
445   // express-messages is for flash messages for easy
446   // errors and information display
447   'messages' : require( 'express-messages' ),
448
449   // By default the req object isn't sen't to views
450   // during rendering, this allows you to use the
451   // user object if available in views.
452   'user' : function( req, res ) {
453     return req.user;
454   },
455
456   // Same, this allows session to be available in views.
457   'session' : function( req, res ) {
458     return req.session;
459   }
460 });
461
462 // Routes
463 // The following are the main CRUD routes that are used
464 // to make up this web app.
465
466 // Homepage
467 // Public
468 /*
469 app.get( '/', loadUser, function( req, res ) {
470   log3("get / page");
471
472   res.render( 'index' );
473 });
474 */
475
476 // Schools list
477 // Used to display all available schools and any courses
478 // in those schools.
479 // Public with some private information
480 app.get( '/schools', checkAjax, loadUser, function( req, res ) {
481   var user = req.user;
482
483   var schoolList = [];
484   // Find all schools and sort by name
485   // XXX mongoose's documentation on sort is extremely poor, tread carefully
486   School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
487     if( schools ) {
488       // If schools are found, loop through them gathering any courses that are
489       // associated with them and then render the page with that information.
490       res.json({ 'user': user.sanitized, 'schools' : schools.map(function(school) {
491         return school.sanitized;
492       })})
493     } else {
494       // If no schools have been found, display none
495       //res.render( 'schools', { 'schools' : [] } );
496       res.json({ 'schools' : [] , 'user': user.sanitized });
497     }
498   });
499 });
500
501 app.get( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
502   var school = req.school;
503   var user = req.user;
504
505   school.authorize( user, function( authorized ) {
506     // This is used to display interface elements for those users
507     // that are are allowed to see th)m, for instance a 'New Course' button.
508     var sanitizedSchool = school.sanitized;
509     sanitizedSchool.authorized = authorized;
510     // Find all courses for school by it's id and sort by name
511     Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
512       // If any courses are found, set them to the appropriate school, otherwise
513       // leave empty.
514       if( courses.length > 0 ) {
515         sanitizedSchool.courses = courses.filter(function(course) {
516           if (!course.deleted) return course;
517         }).map(function(course) {
518           return course.sanitized;
519         });
520       } else {
521         sanitizedSchool.courses = [];
522       }
523       // This tells async (the module) that each iteration of forEach is
524       // done and will continue to call the rest until they have all been
525       // completed, at which time the last function below will be called.
526       res.json({ 'school': sanitizedSchool, 'user': user.sanitized })
527     });
528   });
529 });
530
531 // New course page
532 // Displays form to create new course
533 // Private, requires user to be authorized
534 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
535   // Load school from middleware
536   var school = req.school;
537
538   // If school was not loaded for whatever reason, or the user is not authorized
539   // then redirect to the main schools page.
540   if( ( ! school ) || ( ! school.authorized ) ) {
541     return res.redirect( '/schools' );
542   }
543
544   // If they are authorized and the school exists, then render the page
545   res.render( 'course/new', { 'school': school } );
546 });
547
548 // Recieves new course form
549 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
550   var school = req.school;
551   // Creates new course from Course Schema
552   var course = new Course;
553   // Gathers instructor information from form
554   var instructorEmail = req.body.email.toLowerCase();
555   var instructorName = req.body.instructorName;
556
557   // If school doesn't exist or user is not authorized redirect to main schools page
558   if( ( ! school ) || ( ! school.authorized ) ) {
559     res.redirect( '/schools' );
560   }
561
562   // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
563   if ( !instructorEmail || !instructorName ) {
564     req.flash( 'error', 'Invalid parameters!' )
565     return res.render( 'course/new' );
566   }
567
568   // Fill out the course with information from the form
569   course.number                         = req.body.number;
570   course.name                                   = req.body.name;
571   course.description    = req.body.description;
572   course.school                         = school._id;
573   course.creator      = req.user._id;
574   course.subject      = req.body.subject;
575   course.department   = req.body.department;
576
577   // Check if a user exists with the instructorEmail, if not then create
578   // a new user and send them an instructor welcome email.
579   User.findOne( { 'email' : instructorEmail }, function( err, user ) {
580     if ( !user ) {
581       var user          = new User;
582
583       user.name                                 = instructorName
584       user.email        = instructorEmail;
585       user.affil        = 'Instructor';
586       user.school       = school.name;
587
588       user.activated    = false;
589
590       // Validate instructorEmail
591       // XXX Probably could be done before checking db
592       if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
593         req.flash( 'error', 'Please enter a valid email' );
594         // XXX This needs to be fixed, this is not the proper flow
595         return res.redirect( '/register' );
596       }
597       // Once the new user information has been completed, save the user
598       // to the database then email them the instructor welcome email.
599       user.save(function( err ) {
600         // If there was an error saving the instructor, prompt the user to fill out
601         // the information again.
602         if ( err ) {
603           req.flash( 'error', 'Invalid parameters!' )
604           return res.render( 'course/new' );
605         } else {
606           var message = {
607             to                                  : user.email,
608
609             'subject'           : 'A non-profit open education initiative',
610
611             'template'  : 'instructorInvite',
612             'locals'            : {
613               'course'                  : course,
614               'school'                  : school,
615               'user'                            : user,
616               'serverHost'      : serverHost
617             }
618           };
619
620           mailer.send( message, function( err, result ) {
621             if( err ) {
622               console.log( 'Error inviting instructor to course!' );
623             } else {
624               console.log( 'Successfully invited instructor to course.' );
625             }
626           });
627
628           // After emails are sent, set the courses instructor to the
629           // new users id and then save the course to the database.
630           course.instructor = user._id;
631           course.save( function( err ) {
632             if( err ) {
633               // XXX better validation
634               req.flash( 'error', 'Invalid parameters!' );
635
636               return res.render( 'course/new' );
637             } else {
638               // Once the course has been completed email the admin with information
639               // on the course and new instructor
640               var message = {
641                 to                                      : ADMIN_EMAIL,
642
643                 'subject'               : school.name+' has a new course: '+course.name,
644
645                 'template'      : 'newCourse',
646                 'locals'                : {
647                   'course'                      : course,
648                   'instructor'  : user,
649                   'user'                                : req.user,
650                   'serverHost'  : serverHost
651                 }
652               };
653
654               mailer.send( message, function( err, result ) {
655                 if ( err ) {
656                   console.log( 'Error sending new course email to info@finalsclub.org' )
657                 } else {
658                   console.log( 'Successfully invited instructor to course')
659                 }
660               })
661               // Redirect the user to the schools page where they can see
662               // their new course.
663               // XXX Redirect to the new course instead
664               res.redirect( '/schools' );
665             }
666           });
667         }
668       })
669     } else {
670       // If the user exists, then check if they are already and instructor
671       if (user.affil === 'Instructor') {
672         // If they are an instructor, then save the course with the appropriate
673         // information and email the admin.
674         course.instructor = user._id;
675         course.save( function( err ) {
676           if( err ) {
677             // XXX better validation
678             req.flash( 'error', 'Invalid parameters!' );
679
680             return res.render( 'course/new' );
681           } else {
682             var message = {
683               to                                        : ADMIN_EMAIL,
684
685               'subject'         : school.name+' has a new course: '+course.name,
686
687               'template'        : 'newCourse',
688               'locals'          : {
689                 'course'                        : course,
690                 'instructor'  : user,
691                 'user'                          : req.user,
692                 'serverHost'    : serverHost
693               }
694             };
695
696             mailer.send( message, function( err, result ) {
697               if ( err ) {
698                 console.log( 'Error sending new course email to info@finalsclub.org' )
699               } else {
700                 console.log( 'Successfully invited instructor to course')
701               }
702             })
703             // XXX Redirect to the new course instead
704             res.redirect( '/schools' );
705           }
706         });
707       } else {
708         // The existing user isn't an instructor, so the user is notified of the error
709         // and the course isn't created.
710         req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
711         res.render( 'course/new' );
712       }
713     }
714   })
715 });
716
717 // Individual Course Listing
718 // Public with private information
719 app.get( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
720   var userId = req.user._id;
721   var course = req.course;
722
723   // Check if the user is subscribed to the course
724   // XXX Not currently used for anything
725   var subscribed = course.subscribed( userId );
726
727   // Find lectures associated with this course and sort by name
728   Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
729     // Get course instructor information using their id
730     User.findById( course.instructor, function( err, instructor ) {
731       // Render course and lectures
732       res.json( { 'user': req.user.sanitized, 'course' : course.sanitized, 'instructor': instructor.sanitized, 'subscribed' : subscribed, 'lectures' : lectures.map(function(lecture) { return lecture.sanitized })} );
733     })
734   });
735 });
736
737 // Edit Course
738 app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
739   var course = req.course;
740   var user = req.user;
741
742   if ( user.admin ) {
743     res.render( 'course/new', {course: course} )
744   } else {
745     req.flash( 'error', 'You don\'t have permission to do that' )
746     res.redirect( '/schools' );
747   }
748 })
749
750 // Recieve Course Edit Form
751 app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
752   var course = req.course;
753   var user = req.user;
754
755   if (user.admin) {
756     var courseChanges = req.body;
757     course.number = courseChanges.number;
758     course.name = courseChanges.name;
759     course.description = courseChanges.description;
760     course.department = courseChanges.department;
761
762     course.save(function(err) {
763       if (err) {
764         req.flash( 'error', 'There was an error saving the course' );
765       }
766       res.redirect( '/course/'+ course._id.toString());
767     })
768   } else {
769     req.flash( 'error', 'You don\'t have permission to do that' )
770     res.redirect( '/schools' );
771   }
772 });
773
774 // Delete Course
775 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
776   var course = req.course;
777   var user = req.user;
778
779   if ( user.admin ) {
780     course.delete(function( err ) {
781       if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
782       else req.flash( 'info', 'Successfully removed course' )
783       res.redirect( '/schools' );
784     });
785   } else {
786     req.flash( 'error', 'You don\'t have permission to do that' )
787     res.redirect( '/schools' );
788   }
789 })
790
791 // Subscribe to course
792 // XXX Not currently used for anything
793 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
794   var course = req.course;
795   var userId = req.user._id;
796
797   course.subscribe( userId, function( err ) {
798     if( err ) {
799       req.flash( 'error', 'Error subscribing to course!' );
800     }
801
802     res.redirect( '/course/' + course._id );
803   });
804 });
805
806 // Unsubscribe from course
807 // XXX Not currently used for anything
808 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
809   var course = req.course;
810   var userId = req.user._id;
811
812   course.unsubscribe( userId, function( err ) {
813     if( err ) {
814       req.flash( 'error', 'Error unsubscribing from course!' );
815     }
816
817     res.redirect( '/course/' + course._id );
818   });
819 });
820
821 // Create new lecture
822 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
823   var courseId  = req.params.id;
824   var course            = req.course;
825   var lecture           = {};
826
827   // If course isn't valid or user isn't authorized for course, redirect
828   if( ( ! course ) || ( ! course.authorized ) ) {
829     return res.redirect( '/course/' + courseId );
830   }
831
832   // Render new lecture form
833   res.render( 'lecture/new', { 'lecture' : lecture } );
834 });
835
836 // Recieve New Lecture Form
837 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
838   var courseId  = req.params.id;
839   var course            = req.course;
840   // Create new lecture from Lecture schema
841   var lecture           = new Lecture;
842
843   if( ( ! course ) || ( ! course.authorized ) ) {
844     res.redirect( '/course/' + courseId );
845
846     return;
847   }
848
849   // Populate lecture with form data
850   lecture.name          = req.body.name;
851   lecture.date          = req.body.date;
852   lecture.course        = course._id;
853   lecture.creator = req.user._id;
854
855   // Save lecture to database
856   lecture.save( function( err ) {
857     if( err ) {
858       // XXX better validation
859       req.flash( 'error', 'Invalid parameters!' );
860
861       res.render( 'lecture/new', { 'lecture' : lecture } );
862     } else {
863       // XXX Redirect to new lecture instead
864       res.redirect( '/course/' + course._id );
865     }
866   });
867 });
868
869
870 // Display individual lecture and related notes
871 app.get( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
872   var lecture   = req.lecture;
873
874   // Grab the associated course
875   // XXX this should be done with DBRefs eventually
876   Course.findById( lecture.course, function( err, course ) {
877     if( course ) {
878       // If course is found, find instructor information to be displayed on page
879       User.findById( course.instructor, function( err, instructor ) {
880         // Pull out our notes
881         Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
882           if ( !req.user.loggedIn || !req.lecture.authorized ) {
883             // Loop through notes and only return those that are public if the
884             // user is not logged in or not authorized for that lecture
885             notes = notes.filter(function( note ) {
886               if ( note.public ) return note;
887             })
888           }
889           res.json( {
890             'lecture'                   : lecture.sanitized,
891             'course'                    : course.sanitized,
892             'instructor'  : instructor.sanitized,
893             'user'        : req.user.sanitized,
894             'notes'                             : notes.map(function(note) {
895               return note.sanitized;
896             })
897           });
898         });
899       })
900     } else {
901       res.json( { status: 'error', msg: 'This course is orphaned' })
902     }
903   });
904 });
905
906 // Display new note form
907 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
908   var lectureId = req.params.id;
909   var lecture           = req.lecture;
910   var note                      = {};
911
912   if( ( ! lecture ) || ( ! lecture.authorized ) ) {
913     res.redirect( '/lecture/' + lectureId );
914
915     return;
916   }
917
918   res.render( 'notes/new', { 'note' : note } );
919 });
920
921 // Recieve new note form
922 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
923   var lectureId = req.params.id;
924   var lecture           = req.lecture;
925
926   if( ( ! lecture ) || ( ! lecture.authorized ) ) {
927     res.redirect( '/lecture/' + lectureId );
928
929     return;
930   }
931
932   // Create note from Note schema
933   var note              = new Note;
934
935   // Populate note from form data
936   note.name                     = req.body.name;
937   note.date                     = req.body.date;
938   note.lecture  = lecture._id;
939   note.public           = req.body.private ? false : true;
940   note.creator  = req.user._id;
941
942   // Save note to database
943   note.save( function( err ) {
944     if( err ) {
945       // XXX better validation
946       req.flash( 'error', 'Invalid parameters!' );
947
948       res.render( 'notes/new', { 'note' : note } );
949     } else {
950       // XXX Redirect to new note instead
951       res.redirect( '/lecture/' + lecture._id );
952     }
953   });
954 });
955
956
957 // Display individual note page
958 app.get( '/note/:id', /*checkAjax,*/ loadUser, loadNote, function( req, res ) {
959   var note = req.note;
960   // Set read only id for etherpad-lite or false for later check
961   var roID = note.roID || false;
962
963   var lectureId = note.lecture;
964
965   // Count the amount of visits, but only once per session
966   if ( req.session.visited ) {
967     if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
968       req.session.visited.push( note._id );
969       note.addVisit();
970     }
971   } else {
972     req.session.visited = [];
973     req.session.visited.push( note._id );
974     note.addVisit();
975   }
976
977   // If a read only id exists process note
978   if (roID) {
979     processReq();
980   } else {
981     // If read only id doesn't, then fetch the read only id from the database and then
982     // process note.
983     // XXX Soon to be depracated due to a new API in etherpad that makes for a
984     // much cleaner solution.
985     db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
986       epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
987         if ( record ) {
988           roID = record.value.replace(/"/g, '');
989         } else {
990           roID = false;
991         }
992         processReq();
993       })
994     })
995   }
996
997   function processReq() {
998     // Find lecture
999     Lecture.findById( lectureId, function( err, lecture ) {
1000       if( ! lecture ) {
1001         req.flash( 'error', 'That notes page is orphaned!' );
1002
1003         res.redirect( '/' );
1004       }
1005       // Find notes based on lecture id, which will be displayed in a dropdown
1006       // on the page
1007       Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1008         /*
1009         res.json({
1010           'host'                                : serverHost,
1011           'note'                                : note.sanitized,
1012           'lecture'                     : lecture.sanitized,
1013           'otherNotes'  : otherNotes.map(function(note) {
1014             return note.sanitized;
1015           }),
1016           'RO'                                  : req.RO,
1017           'roID'                                : roID,
1018         });
1019         */
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         res.json( {status: 'error', message: 'Subject with this ID does not exist'} )
1548       } else {
1549         req.subject = subject;
1550         next()
1551       }
1552     })
1553   } else {
1554     next()
1555   } 
1556 }
1557
1558 function loadOldCourse( req, res, next ) {
1559   if( url.parse( req.url ).pathname.match(/course/) ) {
1560     ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1561       if ( err ) {
1562         res.json( {status: 'error', message: 'Course with this ID does not exist'} )
1563       } else {
1564         req.course = course;
1565         next()
1566       }
1567     })
1568   } else {
1569     next()
1570   } 
1571 }
1572
1573 var featuredCourses = [
1574   {name: 'The Human Mind', 'id': 1563},
1575   {name: 'Justice', 'id': 797},
1576   {name: 'Protest Literature', 'id': 1681},
1577   {name: 'Animal Cognition', 'id': 681},
1578   {name: 'Positive Psychology', 'id': 1793},
1579   {name: 'Social Psychology', 'id': 660},
1580   {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1581   {name: 'Cyberspace in Court', 'id': 1446},
1582   {name: 'Nazi Cinema', 'id': 2586},
1583   {name: 'Media and the American Mind', 'id': 2583},
1584   {name: 'Social Thought in Modern America', 'id': 2585},
1585   {name: 'Major British Writers II', 'id': 869},
1586   {name: 'Civil Procedure', 'id': 2589},
1587   {name: 'Evidence', 'id': 2590},
1588   {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1589 ];
1590
1591 app.get( '/learn', loadUser, function( req, res ) {
1592   res.render( 'archive/learn', { 'courses' : featuredCourses } );
1593 })
1594
1595 app.get( '/learn/random', loadUser, function( req, res ) {
1596   res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1597 })
1598
1599 app.get( '/archive', checkAjax, loadUser, function( req, res ) {
1600   ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1601     if ( err ) {
1602       res.json( {status: 'error', message: 'There was a problem gathering the archived courses, please try again later.'} );
1603     } else {
1604       res.json( { 'subjects' : subjects, 'user': req.user.sanitized } );
1605     }
1606   })
1607 })
1608
1609 app.get( '/archive/subject/:id', checkAjax, loadUser, loadSubject, function( req, res ) {
1610   ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1611     if ( err ) {
1612       res.json( {status: 'error', message: 'There are no archived courses'} );
1613     } else {
1614       res.json( { 'courses' : courses, 'subject': req.subject, 'user': req.user.sanitized } );
1615     }
1616   })
1617 })
1618
1619 app.get( '/archive/course/:id', checkAjax, loadUser, loadOldCourse, function( req, res ) {
1620   ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1621     if ( err ) {
1622       res.json( {status: 'error', message: 'There are no notes in this course'} );
1623     } else {
1624       res.json( { 'notes' : notes, 'course' : req.course, 'user': req.user.sanitized } );
1625     }
1626   })
1627 })
1628
1629 app.get( '/archive/note/:id', checkAjax, loadUser, function( req, res ) {
1630   ArchivedNote.findById(req.params.id, function(err, note) {
1631     if ( err ) {
1632       res.json( {status: 'error', message: 'This is not a valid id for a note'} );
1633     } else {
1634       ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1635         if ( err ) {
1636           res.json( {status: 'error', message: 'There is no course for this note'} )
1637         } else {
1638           res.json( { 'layout' : 'notesLayout', 'note' : note, 'course': course, 'user': req.user.sanitized } );
1639         }
1640       })
1641     }
1642   })
1643 })
1644
1645
1646 app.get( '*', function(req, res) {
1647   res.sendfile('public/index.html');
1648 });
1649
1650 // socket.io server
1651
1652 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1653 // the browser which facilitates near realtime interaction. This allows the user to post questions
1654 // and comments and other users to get those almost immediately after they are posted, without
1655 // reloading the page or pressing a button to refresh.
1656 //
1657 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1658 // saving the data to the database, and then sending it out to everyone else connected. 
1659 //
1660 // Data types:
1661 // Posts -  Posts are the main items in backchannel, useful for questions or discussion points
1662 //              [[ example object needed with explanation E.G: 
1663 /*
1664                 Post: { postID: '999-1',
1665                                   userID: '1234',
1666                                   userName: 'Bob Jones',
1667                                   userAffil: 'Instructor',
1668                                   body: 'This is the text content of the post.',
1669                                   comments: { {<commentObj>, <commentObj>, ...},
1670                                   public: true,
1671                                   votes:   [ <userID>, <userID>, ...],
1672                                   reports: [ <userID>, <userID>, ...]
1673                                 }
1674                   Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
1675                 
1676                   if anonymous: userName => 'Anonymous', userAffil => 'N/A'
1677 */
1678 //
1679 //
1680 //
1681 // Comments - Comments are replies to posts, for clarification or answering questions
1682 //              [[ example object needed]]
1683 // Votes - Votes signifyg a users approval of a post
1684 //              [[ example object needed]]
1685 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1686 //              [[ example object needed]]
1687 //
1688 //
1689 // Post Schema
1690 // body - Main content of the post
1691 // userId - Not currently used, but would contain the users id that made the post
1692 // userName - Users name that made post
1693 // userAffil - Users affiliation to their school
1694 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1695 // date - Date post was made, updates when any comments are made for the post
1696 // comments - An array of comments which contain a body, userName, and userAffil
1697 // votes - An array of user ids which are the users that voted
1698 //              [[ example needed ]]
1699 // reports - An array of user ids which are the users that reported the post
1700 //              [[ reports would be "this post is flagged as inappropriate"? ]]
1701 //              [[ bruml: consistent terminology needed ]]
1702 //
1703 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1704 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1705 // users the ability to make posts or comments that they might not otherwise due to not wanting
1706 // the content of the post/comment to be attributed to them.
1707 //
1708 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1709 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1710 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1711 // user is logged in, and if so display information that otherwise might not be visible to them if they
1712 // aren't apart of a particular school.
1713 //
1714 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1715 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1716 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1717 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1718 // rendered in the browser.
1719 //
1720 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1721 // events are as follows.
1722 //
1723 // Post event
1724 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1725 // The server recieves the data, assembles a new post object for the database and then fills it with
1726 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1727 // replaced. If the user chose for the post to be private, then public will be set to false and it
1728 // will be filtered from being sent to users not logged into and not having access to the school. Once
1729 // the post has been created and saved into the database, it is sent to all connected users to that
1730 // particular lecture, unless it is private, than only logged in users will get it.
1731 //
1732 // Vote event
1733 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1734 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1735 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1736 // users unless the post is private, which then it will be only sent to logged in users.
1737 //
1738 // Report event
1739 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1740 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1741 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1742 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1743 // bottom of the interface.
1744 //
1745 // Comment event
1746 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1747 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1748 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1749 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1750 // to the top of the interface. The comment can be anonymous, which then will have the user
1751 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1752 // connected users unless the post is private, then only logged in users will recieve the comment.
1753
1754 var io = require( 'socket.io' ).listen( app );
1755
1756 var Post = mongoose.model( 'Post' );
1757
1758 io.set('authorization', function ( handshake, next ) {
1759   var rawCookie = handshake.headers.cookie;
1760   if (rawCookie) {
1761     handshake.cookie = parseCookie(rawCookie);
1762     handshake.sid = handshake.cookie['connect.sid'];
1763
1764     if ( handshake.sid ) {
1765       app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1766         if( err ) {
1767           handshake.user = false;
1768           return next(null, true);
1769         } else {
1770           // bake a new session object for full r/w
1771           handshake.session = new Session( handshake, session );
1772
1773           User.findOne( { session : handshake.sid }, function( err, user ) {
1774             if( user ) {
1775               handshake.user = user;
1776               return next(null, true);
1777             } else {
1778               handshake.user = false;
1779               return next(null, true);
1780             }
1781           });
1782         }
1783       })
1784     }
1785   } else {
1786     data.user = false;
1787     return next(null, true);
1788   }
1789 });
1790
1791 var backchannel = new Backchannel(app, io.of('/backchannel'), {
1792   subscribe: function(lecture, send) {
1793     Post.find({'lecture': lecture}, function(err, posts) {
1794       send(posts);
1795     });
1796   },
1797   post: function(fillPost) {
1798     var post = new Post;
1799     fillPost(post, function(send) {
1800       post.save(function(err) {
1801         send();
1802       });
1803     });
1804   },
1805   items: function(postId, addItem) {
1806     Post.findById(postId, function( err, post ) {
1807       addItem(post, function(send) {
1808         post.save(function(err) {
1809           send();
1810         });
1811       })
1812     })
1813   }
1814 });
1815
1816
1817
1818
1819 var counters = {};
1820
1821 var counts = io
1822 .of( '/counts' )
1823 .on( 'connection', function( socket ) {
1824   // pull out user/session information etc.
1825   var handshake = socket.handshake;
1826   var userID            = handshake.user._id;
1827
1828   var watched           = [];
1829   var noteID            = null;
1830
1831   var timer                     = null;
1832
1833   socket.on( 'join', function( note ) {
1834     if (handshake.user === false) {
1835       noteID                    = note;
1836       // XXX: replace by addToSet (once it's implemented in mongoose)
1837       Note.findById( noteID, function( err, note ) {
1838         if( note ) {
1839           if( note.collaborators.indexOf( userID ) == -1 ) {
1840             note.collaborators.push( userID );
1841             note.save();
1842           }
1843         }
1844       });
1845     }
1846   });
1847
1848   socket.on( 'watch', function( l ) {
1849     var sendCounts = function() {
1850       var send = {};
1851
1852       Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1853         async.forEach(
1854           notes,
1855           function( note, callback ) {
1856             var id              = note._id;
1857             var count   = note.collaborators.length;
1858
1859             send[ id ] = count;
1860
1861             callback();
1862           }, function() {
1863             socket.emit( 'counts', send );
1864
1865             timer = setTimeout( sendCounts, 5000 );
1866           }
1867         );
1868       });
1869     }
1870
1871     Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1872       notes.forEach( function( note ) {
1873         watched.push( note._id );
1874       });
1875     });
1876
1877     sendCounts();
1878   });
1879
1880   socket.on( 'disconnect', function() {
1881     clearTimeout( timer );
1882
1883     if (handshake.user === false) {
1884       // XXX: replace with $pull once it's available
1885       if( noteID ) {
1886         Note.findById( noteID, function( err, note ) {
1887           if( note ) {
1888             var index = note.collaborators.indexOf( userID );
1889
1890             if( index != -1 ) {
1891               note.collaborators.splice( index, 1 );
1892             }
1893
1894             note.save();
1895           }
1896         });
1897       }
1898     }
1899   });
1900 });
1901
1902 // Exception Catch-All
1903
1904 process.on('uncaughtException', function (e) {
1905   console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1906 });
1907
1908
1909 // Launch
1910
1911 mongoose.connect( app.set( 'dbUri' ) );
1912 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1913
1914 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1915
1916 app.listen( serverPort, function() {
1917   console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1918
1919   // if run as root, downgrade to the owner of this file
1920   if (process.getuid() === 0) {
1921     require('fs').stat(__filename, function(err, stats) {
1922       if (err) { return console.log(err); }
1923       process.setuid(stats.uid);
1924     });
1925   }
1926 });
1927
1928 function isValidEmail(email) {
1929   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])?/;
1930   return email.match(re);
1931 }