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