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