updated start/stop scripts to run form /etc/rc.local (without sudo)
[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 // Dynamic Helpers are loaded automatically into views
444 app.dynamicHelpers( {
445   // express-messages is for flash messages for easy
446   // errors and information display
447   'messages' : require( 'express-messages' ),
448
449   // By default the req object isn't sen't to views
450   // during rendering, this allows you to use the
451   // user object if available in views.
452   'user' : function( req, res ) {
453     return req.user;
454   },
455
456   // Same, this allows session to be available in views.
457   'session' : function( req, res ) {
458     return req.session;
459   }
460 });
461
462 // Routes
463 // The following are the main CRUD routes that are used
464 // to make up this web app.
465
466 // Homepage
467 // Public
468 app.get( '/', loadUser, function( req, res ) {
469   log3("get / page");
470
471   res.render( 'index' );
472 });
473
474 // Schools list
475 // Used to display all available schools and any courses
476 // in those schools.
477 // Public with some private information
478 app.get( '/schools', loadUser, function( req, res ) {
479   var user = req.user;
480
481   // Find all schools and sort by name
482   // XXX mongoose's documentation on sort is extremely poor, tread carefully
483   School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
484     if( schools ) {
485       // If schools are found, loop through them gathering any courses that are
486       // associated with them and then render the page with that information.
487       async.forEach(
488         schools,
489         function( school, callback ) {
490           // Check if user is authorized with each school
491           school.authorize( user, function( authorized ) {
492             // This is used to display interface elements for those users
493             // that are are allowed to see them, for instance a 'New Course' button.
494             school.authorized = authorized;
495
496             // Find all courses for school by it's id and sort by name
497             Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
498               // If any courses are found, set them to the appropriate school, otherwise
499               // leave empty.
500               if( courses.length > 0 ) {
501                 school.courses = courses.filter(function(course) {
502                   if (!course.deleted) return course;
503                 });
504               } else {
505                 school.courses = [];
506               }
507               // This tells async (the module) that each iteration of forEach is
508               // done and will continue to call the rest until they have all been
509               // completed, at which time the last function below will be called.
510               callback();
511             });
512           });
513         },
514         // After all schools and courses have been found, render them
515         function( err ) {
516           res.render( 'schools', { 'schools' : schools } );
517         }
518       );
519     } else {
520       // If no schools have been found, display none
521       res.render( 'schools', { 'schools' : [] } );
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', 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.render( 'course/index', { 'course' : course, 'instructor': instructor, 'subscribed' : subscribed, 'lectures' : lectures } );
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', 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           // Render lecture and notes
885           res.render( 'lecture/index', {
886             'lecture'                   : lecture,
887             'course'                    : course,
888             'instructor'  : instructor,
889             'notes'                             : notes,
890             'counts'                    : counts,
891
892             'javascripts'       : [ 'counts.js' ]
893           });
894         });
895       })
896     } else {
897       // XXX with DBRefs we will be able to reassign orphaned courses/lecture/pads
898
899       req.flash( 'error', 'That lecture is orphaned!' );
900
901       res.redirect( '/' );
902     }
903   });
904 });
905
906 // Display new note form
907 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
908   var lectureId = req.params.id;
909   var lecture           = req.lecture;
910   var note                      = {};
911
912   if( ( ! lecture ) || ( ! lecture.authorized ) ) {
913     res.redirect( '/lecture/' + lectureId );
914
915     return;
916   }
917
918   res.render( 'notes/new', { 'note' : note } );
919 });
920
921 // Recieve new note form
922 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
923   var lectureId = req.params.id;
924   var lecture           = req.lecture;
925
926   if( ( ! lecture ) || ( ! lecture.authorized ) ) {
927     res.redirect( '/lecture/' + lectureId );
928
929     return;
930   }
931
932   // Create note from Note schema
933   var note              = new Note;
934
935   // Populate note from form data
936   note.name                     = req.body.name;
937   note.date                     = req.body.date;
938   note.lecture  = lecture._id;
939   note.public           = req.body.private ? false : true;
940   note.creator  = req.user._id;
941
942   // Save note to database
943   note.save( function( err ) {
944     if( err ) {
945       // XXX better validation
946       req.flash( 'error', 'Invalid parameters!' );
947
948       res.render( 'notes/new', { 'note' : note } );
949     } else {
950       // XXX Redirect to new note instead
951       res.redirect( '/lecture/' + lecture._id );
952     }
953   });
954 });
955
956
957 // Display individual note page
958 app.get( '/note/:id', loadUser, loadNote, function( req, res ) {
959   var note = req.note;
960   // Set read only id for etherpad-lite or false for later check
961   var roID = note.roID || false;
962
963   var lectureId = note.lecture;
964
965   // Count the amount of visits, but only once per session
966   if ( req.session.visited ) {
967     if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
968       req.session.visited.push( note._id );
969       note.addVisit();
970     }
971   } else {
972     req.session.visited = [];
973     req.session.visited.push( note._id );
974     note.addVisit();
975   }
976
977   // If a read only id exists process note
978   if (roID) {
979     processReq();
980   } else {
981     // If read only id doesn't, then fetch the read only id from the database and then
982     // process note.
983     // XXX Soon to be depracated due to a new API in etherpad that makes for a
984     // much cleaner solution.
985     db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
986       epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
987         if ( record ) {
988           roID = record.value.replace(/"/g, '');
989         } else {
990           roID = false;
991         }
992         processReq();
993       })
994     })
995   }
996
997   function processReq() {
998     // Find lecture
999     Lecture.findById( lectureId, function( err, lecture ) {
1000       if( ! lecture ) {
1001         req.flash( 'error', 'That notes page is orphaned!' );
1002
1003         res.redirect( '/' );
1004       }
1005       // Find notes based on lecture id, which will be displayed in a dropdown
1006       // on the page
1007       Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1008         if( !req.RO ) {
1009           // User is logged in and sees full notepad
1010
1011           res.render( 'notes/index', {
1012             'layout'                    : 'noteLayout',
1013             'host'                              : serverHost,
1014             'note'                              : note,
1015             'lecture'                   : lecture,
1016             'otherNotes'        : otherNotes,
1017             'RO'                                        : false,
1018             'roID'                              : roID,
1019             'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1020             'javascripts'       : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1021           });
1022         } else {
1023           // User is not logged in and sees notepad that is public
1024           res.render( 'notes/public', {
1025             'layout'                    : 'noteLayout',
1026             'host'                              : serverHost,
1027             'note'                              : note,
1028             'otherNotes'        : otherNotes,
1029             'roID'                              : roID,
1030             'lecture'                   : lecture,
1031             'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1032             'javascripts'       : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1033           });
1034         }
1035       });
1036     });
1037   }
1038 });
1039
1040 // Static pages and redirects
1041 app.get( '/about', loadUser, function( req, res ) {
1042   res.redirect( 'http://blog.finalsclub.org/about.html' );
1043 });
1044
1045 app.get( '/press', loadUser, function( req, res ) {
1046   res.render( 'static/press' );
1047 });
1048
1049 app.get( '/conduct', loadUser, function( req, res ) {
1050   res.render( 'static/conduct' );
1051 });
1052
1053 app.get( '/legal', loadUser, function( req, res ) {
1054   res.redirect( 'http://blog.finalsclub.org/legal.html' );
1055 });
1056
1057 app.get( '/contact', loadUser, function( req, res ) {
1058   res.redirect( 'http://blog.finalsclub.org/contact.html' );
1059 });
1060
1061 app.get( '/privacy', loadUser, function( req, res ) {
1062   res.render( 'static/privacy' );
1063 });
1064
1065
1066 // Authentication routes
1067 // These are used for logging in, logging out, registering
1068 // and other user authentication purposes
1069
1070 // Render login page
1071 app.get( '/login', function( req, res ) {
1072   log3("get login page")
1073
1074   res.render( 'login' );        
1075 });
1076
1077 // Recieve login form
1078 app.post( '/login', function( req, res ) {
1079   var email              = req.body.email;
1080   var password = req.body.password;
1081   log3("post login ...")
1082
1083   // Find user from email
1084   User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1085     log3(err) 
1086     log3(user) 
1087
1088     // If user exists, check if activated, if not notify them and send them to
1089     // the login form
1090     if( user ) {
1091       if( ! user.activated ) {
1092         // (undocumented) markdown-esque link functionality in req.flash
1093         req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1094
1095         req.session.activateCode = user._id;
1096
1097         res.render( 'login' );
1098       } else {
1099         // If user is activated, check if their password is correct
1100         if( user.authenticate( password ) ) {
1101           log3("pass ok") 
1102
1103           var sid = req.sessionID;
1104
1105           user.session = sid;
1106
1107           // Set the session then save the user to the database
1108           user.save( function() {
1109             var redirect = req.session.redirect;
1110
1111             // login complete, remember the user's email for next time
1112             req.session.email = email;
1113
1114             // alert the successful login
1115             req.flash( 'info', 'Successfully logged in!' );
1116
1117             // redirect to profile if we don't have a stashed request
1118             res.redirect( redirect || '/profile' );
1119           });
1120         } else {
1121           // Notify user of bad login
1122           req.flash( 'error', 'Invalid login!' );
1123
1124           res.render( 'login' );
1125         }
1126       }
1127     } else {
1128       // Notify user of bad login
1129       log3("bad login")
1130       req.flash( 'error', 'Invalid login!' );
1131
1132       res.render( 'login' );
1133     }
1134   });
1135 });
1136
1137 // Request reset password
1138 app.get( '/resetpw', function( req, res ) {
1139   log3("get resetpw page");
1140   res.render( 'resetpw' );
1141 });
1142
1143 // Display reset password from requested email
1144 app.get( '/resetpw/:id', function( req, res ) {
1145   var resetPassCode = req.params.id
1146   res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1147 });
1148
1149 // Recieve reset password request form
1150 app.post( '/resetpw', function( req, res ) {
1151   log3("post resetpw");
1152   var email = req.body.email
1153
1154
1155   // Search for user
1156   User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1157     if( user ) {
1158
1159       // If user exists, create reset code
1160       var resetPassCode = hat(64);
1161       user.setResetPassCode(resetPassCode);
1162
1163       // Construct url that the user can then click to reset password
1164       var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1165
1166       // Save user to database
1167       user.save( function( err ) {
1168         log3('save '+user.email);
1169
1170         // Construct email and send it to the user
1171         var message = {
1172           'to'                          : user.email,
1173
1174           'subject'             : 'Your FinalsClub.org Password has been Reset!',
1175
1176           'template'    : 'userPasswordReset',
1177           'locals'              : {
1178             'resetPassCode'             : resetPassCode,
1179             'resetPassUrl'              : resetPassUrl
1180           }
1181         };
1182
1183         mailer.send( message, function( err, result ) {
1184           if( err ) {
1185             // XXX: Add route to resend this email
1186
1187             console.log( 'Error sending user password reset email!' );
1188           } else {
1189             console.log( 'Successfully sent user password reset email.' );
1190           }
1191
1192         }); 
1193
1194         // Render request success page
1195         res.render( 'resetpw-success', { 'email' : email } );
1196       });                       
1197     } else {
1198       // Notify of error
1199       res.render( 'resetpw-error', { 'email' : email } );
1200     }
1201   });
1202 });
1203
1204 // Recieve reset password form
1205 app.post( '/resetpw/:id', function( req, res ) {
1206   log3("post resetpw.code");
1207   var resetPassCode = req.params.id
1208   var email = req.body.email
1209   var pass1 = req.body.pass1
1210   var pass2 = req.body.pass2
1211
1212   // Find user by email
1213   User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1214     var valid = false;
1215     // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1216     // save user with new password and display success message.
1217     if( user ) {
1218       var valid = user.resetPassword(resetPassCode, pass1, pass2);
1219       if (valid) {
1220         user.save( function( err ) {
1221           res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );               
1222         });                     
1223       }
1224     } 
1225
1226     // If there was a problem, notify user
1227     if (!valid) {
1228       res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1229     }
1230   });
1231 });
1232
1233 // Display registration page
1234 app.get( '/register', function( req, res ) {
1235   log3("get reg page");
1236
1237   // Populate school dropdown list
1238   School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1239     res.render( 'register', { 'schools' : schools } );
1240   })
1241 });
1242
1243 // Recieve registration form
1244 app.post( '/register', function( req, res ) {
1245   var sid = req.sessionId;
1246
1247   // Create new user from User schema
1248   var user = new User;
1249
1250   // Populate user from form
1251   user.email        = req.body.email.toLowerCase();
1252   user.password     = req.body.password;
1253   user.session      = sid;
1254   // If school is set to other, then fill in school as what the
1255   // user entered
1256   user.school                           = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1257   user.name         = req.body.name;
1258   user.affil        = req.body.affil;
1259   user.activated    = false;
1260
1261   // Validate email
1262   if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1263     req.flash( 'error', 'Please enter a valid email' );
1264     return res.redirect( '/register' );
1265   }
1266
1267   // Check if password is greater than 6 characters, otherwise notify user
1268   if ( req.body.password.length < 6 ) {
1269     req.flash( 'error', 'Please enter a password longer than eight characters' );
1270     return res.redirect( '/register' );
1271   }
1272
1273   // Pull out hostname from email
1274   var hostname = user.email.split( '@' ).pop();
1275
1276   // Check if email is from one of the special domains
1277   if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1278     user.admin = true;
1279   }
1280
1281   // Save user to database
1282   user.save( function( err ) {
1283     // If error, check if it is because the user already exists, if so
1284     // get the user information and let them know
1285     if ( err ) {
1286       if( /dup key/.test( err.message ) ) {
1287         // attempting to register an existing address
1288         User.findOne({ 'email' : user.email }, function(err, result ) {
1289           if (result.activated) {
1290             // If activated, make sure they know how to contact the admin
1291             req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1292             return res.redirect( '/register' )
1293           } else {
1294             // If not activated, direct them to the resendActivation page
1295             req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1296             return res.redirect( '/resendActivation' )
1297           }
1298         });
1299       } else {
1300         // If any other type of error, prompt them to enter the registration again
1301         req.flash( 'error', 'An error occurred during registration.' );
1302
1303         return res.redirect( '/register' );
1304       }
1305     } else {
1306       // send user activation email
1307       sendUserActivation( user );
1308
1309       // Check if the hostname matches any in the approved schools
1310       School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1311         if( school ) {
1312           // If there is a match, send associated welcome message
1313           sendUserWelcome( user, true );
1314           log3('school recognized '+school.name);
1315           // If no users exist for the school, create empty array
1316           if (!school.users) school.users = [];
1317           // Add user to the school
1318           school.users.push( user._id );
1319
1320           // Save school to the database
1321           school.save( function( err ) {
1322             log3('school.save() done');
1323             // Notify user that they have been added to the school
1324             req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1325             res.redirect( '/' );
1326           });
1327           // Construct admin email about user registration
1328           var message = {
1329             'to'       : ADMIN_EMAIL,
1330
1331             'subject'  : 'FC User Registration : User added to ' + school.name,
1332
1333             'template' : 'userSchool',
1334             'locals'   : {
1335               'user'   : user
1336             }
1337           }
1338         } else {
1339           // If there isn't a match, send associated welcome message
1340           sendUserWelcome( user, false );
1341           // Tell user to check for activation link
1342           req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1343           res.redirect( '/' );
1344           // Construct admin email about user registration
1345           var message = {
1346             'to'       : ADMIN_EMAIL,
1347
1348             'subject'  : 'FC User Registration : Email did not match any schools',
1349
1350             'template' : 'userNoSchool',
1351             'locals'   : {
1352               'user'   : user
1353             }
1354           }
1355         }
1356         // Send email to admin
1357         mailer.send( message, function( err, result ) {
1358           if ( err ) {
1359
1360             console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1361           } else {
1362             console.log( 'Successfully sent user has no school email to admin.' );
1363           }
1364         })
1365
1366       });
1367     }
1368
1369   });
1370 });
1371
1372 // Display resendActivation request page
1373 app.get( '/resendActivation', function( req, res ) {
1374   var activateCode = req.session.activateCode;
1375
1376   // Check if user exists by activateCode set in their session
1377   User.findById( activateCode, function( err, user ) {
1378     if( ( ! user ) || ( user.activated ) ) {
1379       res.redirect( '/' );
1380     } else {
1381       // Send activation and redirect to login
1382       sendUserActivation( user );
1383
1384       req.flash( 'info', 'Your activation code has been resent.' );
1385
1386       res.redirect( '/login' );
1387     }
1388   });
1389 });
1390
1391 // Display activation page
1392 app.get( '/activate/:code', function( req, res ) {
1393   var code = req.params.code;
1394
1395   // XXX could break this out into a middleware
1396   if( ! code ) {
1397     res.redirect( '/' );
1398   }
1399
1400   // Find user by activation code
1401   User.findById( code, function( err, user ) {
1402     if( err || ! user ) {
1403       // If not found, notify user of invalid code
1404       req.flash( 'error', 'Invalid activation code!' );
1405
1406       res.redirect( '/' );
1407     } else {
1408       // If valid, then activate user
1409       user.activated = true;
1410
1411       // Regenerate our session and log in as the new user
1412       req.session.regenerate( function() {
1413         user.session = req.sessionID;
1414
1415         // Save user to database
1416         user.save( function( err ) {
1417           if( err ) {
1418             req.flash( 'error', 'Unable to activate account.' );
1419
1420             res.redirect( '/' );
1421           } else {
1422             req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1423
1424             res.redirect( '/profile' );
1425           }
1426         });
1427       });
1428     }
1429   });
1430 });
1431
1432 // Logut user
1433 app.get( '/logout', function( req, res ) {
1434   var sid = req.sessionID;
1435
1436   // Find user by session id
1437   User.findOne( { 'session' : sid }, function( err, user ) {
1438     if( user ) {
1439       // Empty out session id
1440       user.session = '';
1441
1442       // Save user to database
1443       user.save( function( err ) {
1444         res.redirect( '/' );
1445       });
1446     } else {
1447       res.redirect( '/' );
1448     }
1449   });
1450 });
1451
1452 // Display users profile page
1453 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1454   var user = req.user;
1455
1456   res.render( 'profile/index', { 'user' : user } );
1457 });
1458
1459 // Recieve profile edit page form
1460 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1461   var user              = req.user;
1462   var fields    = req.body;
1463
1464   var error                             = false;
1465   var wasComplete       = user.isComplete;
1466
1467   if( ! fields.name ) {
1468     req.flash( 'error', 'Please enter a valid name!' );
1469
1470     error = true;
1471   } else {
1472     user.name = fields.name;
1473   }
1474
1475   if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1476     req.flash( 'error', 'Please select a valid affiliation!' );
1477
1478     error = true;
1479   } else {
1480     user.affil = fields.affiliation;
1481   }
1482
1483   if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1484     // changing password
1485     if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1486       if( fields.newPassword === fields.newPasswordConfirm ) {
1487         // test password strength?
1488
1489         user.password = fields.newPassword;
1490       } else {
1491         req.flash( 'error', 'Mismatch in new password!' );
1492
1493         error = true;
1494       }
1495     } else {
1496       req.flash( 'error', 'Please supply your existing password.' );
1497
1498       error = true;
1499     }
1500   }
1501
1502   user.major            = fields.major;
1503   user.bio                      = fields.bio;
1504
1505   user.showName = ( fields.showName ? true : false );
1506
1507   if( ! error ) {
1508     user.save( function( err ) {
1509       if( err ) {
1510         req.flash( 'error', 'Unable to save user profile!' );
1511       } else {
1512         if( ( user.isComplete ) && ( ! wasComplete ) ) {
1513           req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1514
1515           res.redirect( '/' );
1516         } else {
1517           res.render( 'info', 'Your profile was successfully updated!' );
1518
1519           res.render( 'profile/index', { 'user' : user } );
1520         }
1521       }
1522     });
1523   } else {
1524     res.render( 'profile/index', { 'user' : user } );
1525   }
1526 });
1527
1528
1529 // Old Notes
1530
1531 function loadSubject( req, res, next ) {
1532   if( url.parse( req.url ).pathname.match(/subject/) ) {
1533     ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1534       if ( err ) {
1535         req.flash( 'error', 'Subject with this ID does not exist' )
1536         res.redirect( '/archive' );
1537       } else {
1538         req.subject = subject;
1539         next()
1540       }
1541     })
1542   } else {
1543     next()
1544   } 
1545 }
1546
1547 function loadOldCourse( req, res, next ) {
1548   if( url.parse( req.url ).pathname.match(/course/) ) {
1549     ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1550       if ( err ) {
1551         req.flash( 'error', 'Course with this ID does not exist' )
1552         res.redirect( '/archive' );
1553       } else {
1554         req.course = course;
1555         next()
1556       }
1557     })
1558   } else {
1559     next()
1560   } 
1561 }
1562
1563 var featuredCourses = [
1564   {name: 'The Human Mind', 'id': 1563},
1565   {name: 'Justice', 'id': 797},
1566   {name: 'Protest Literature', 'id': 1681},
1567   {name: 'Animal Cognition', 'id': 681},
1568   {name: 'Positive Psychology', 'id': 1793},
1569   {name: 'Social Psychology', 'id': 660},
1570   {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1571   {name: 'Cyberspace in Court', 'id': 1446},
1572   {name: 'Nazi Cinema', 'id': 2586},
1573   {name: 'Media and the American Mind', 'id': 2583},
1574   {name: 'Social Thought in Modern America', 'id': 2585},
1575   {name: 'Major British Writers II', 'id': 869},
1576   {name: 'Civil Procedure', 'id': 2589},
1577   {name: 'Evidence', 'id': 2590},
1578   {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1579 ];
1580
1581 app.get( '/learn', loadUser, function( req, res ) {
1582   res.render( 'archive/learn', { 'courses' : featuredCourses } );
1583 })
1584
1585 app.get( '/learn/random', loadUser, function( req, res ) {
1586   res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1587 })
1588
1589 app.get( '/archive', loadUser, function( req, res ) {
1590   ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1591     if ( err ) {
1592       req.flash( 'error', 'There was a problem gathering the archived courses, please try again later.' );
1593       res.redirect( '/' );
1594     } else {
1595       res.render( 'archive/index', { 'subjects' : subjects } );
1596     }
1597   })
1598 })
1599
1600 app.get( '/archive/subject/:id', loadUser, loadSubject, function( req, res ) {
1601   ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1602     if ( err ) {
1603       req.flash( 'error', 'There are no archived courses' );
1604       res.redirect( '/' );
1605     } else {
1606       res.render( 'archive/courses', { 'courses' : courses, 'subject': req.subject } );
1607     }
1608   })
1609 })
1610
1611 app.get( '/archive/course/:id', loadUser, loadOldCourse, function( req, res ) {
1612   ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1613     if ( err ) {
1614       req.flash( 'error', 'There are no notes in this course' );
1615       res.redirect( '/archive' );
1616     } else {
1617       res.render( 'archive/notes', { 'notes' : notes, 'course' : req.course } );
1618     }
1619   })
1620 })
1621
1622 app.get( '/archive/note/:id', loadUser, function( req, res ) {
1623   ArchivedNote.findById(req.params.id, function(err, note) {
1624     if ( err ) {
1625       req.flash( 'error', 'This is not a valid id for a note' );
1626       res.redirect( '/archive' );
1627     } else {
1628       ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1629         if ( err ) {
1630           req.flash( 'error', 'There is no course for this note' )
1631           res.redirect( '/archive' )
1632         } else {
1633           res.render( 'archive/note', { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
1634         }
1635       })
1636     }
1637   })
1638 })
1639
1640 // socket.io server
1641
1642 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1643 // the browser which facilitates near realtime interaction. This allows the user to post questions
1644 // and comments and other users to get those almost immediately after they are posted, without
1645 // reloading the page or pressing a button to refresh.
1646 //
1647 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1648 // saving the data to the database, and then sending it out to everyone else connected. 
1649 //
1650 // Data types:
1651 // Posts -  Posts are the main items in backchannel, useful for questions or discussion points
1652 //              [[ example object needed with explanation E.G: 
1653 /*
1654                 Post: { postID: '999-1',
1655                                   userID: '1234',
1656                                   userName: 'Bob Jones',
1657                                   userAffil: 'Instructor',
1658                                   body: 'This is the text content of the post.',
1659                                   comments: { {<commentObj>, <commentObj>, ...},
1660                                   public: true,
1661                                   votes:   [ <userID>, <userID>, ...],
1662                                   reports: [ <userID>, <userID>, ...]
1663                                 }
1664                   Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
1665                 
1666                   if anonymous: userName => 'Anonymous', userAffil => 'N/A'
1667 */
1668 //
1669 //
1670 //
1671 // Comments - Comments are replies to posts, for clarification or answering questions
1672 //              [[ example object needed]]
1673 // Votes - Votes signifyg a users approval of a post
1674 //              [[ example object needed]]
1675 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1676 //              [[ example object needed]]
1677 //
1678 //
1679 // Post Schema
1680 // body - Main content of the post
1681 // userId - Not currently used, but would contain the users id that made the post
1682 // userName - Users name that made post
1683 // userAffil - Users affiliation to their school
1684 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1685 // date - Date post was made, updates when any comments are made for the post
1686 // comments - An array of comments which contain a body, userName, and userAffil
1687 // votes - An array of user ids which are the users that voted
1688 //              [[ example needed ]]
1689 // reports - An array of user ids which are the users that reported the post
1690 //              [[ reports would be "this post is flagged as inappropriate"? ]]
1691 //              [[ bruml: consistent terminology needed ]]
1692 //
1693 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1694 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1695 // users the ability to make posts or comments that they might not otherwise due to not wanting
1696 // the content of the post/comment to be attributed to them.
1697 //
1698 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1699 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1700 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1701 // user is logged in, and if so display information that otherwise might not be visible to them if they
1702 // aren't apart of a particular school.
1703 //
1704 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1705 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1706 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1707 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1708 // rendered in the browser.
1709 //
1710 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1711 // events are as follows.
1712 //
1713 // Post event
1714 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1715 // The server recieves the data, assembles a new post object for the database and then fills it with
1716 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1717 // replaced. If the user chose for the post to be private, then public will be set to false and it
1718 // will be filtered from being sent to users not logged into and not having access to the school. Once
1719 // the post has been created and saved into the database, it is sent to all connected users to that
1720 // particular lecture, unless it is private, than only logged in users will get it.
1721 //
1722 // Vote event
1723 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1724 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1725 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1726 // users unless the post is private, which then it will be only sent to logged in users.
1727 //
1728 // Report event
1729 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1730 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1731 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1732 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1733 // bottom of the interface.
1734 //
1735 // Comment event
1736 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1737 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1738 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1739 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1740 // to the top of the interface. The comment can be anonymous, which then will have the user
1741 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1742 // connected users unless the post is private, then only logged in users will recieve the comment.
1743
1744 var io = require( 'socket.io' ).listen( app );
1745
1746 var Post = mongoose.model( 'Post' );
1747
1748 io.set('authorization', function ( handshake, next ) {
1749   var rawCookie = handshake.headers.cookie;
1750   if (rawCookie) {
1751     handshake.cookie = parseCookie(rawCookie);
1752     handshake.sid = handshake.cookie['connect.sid'];
1753
1754     if ( handshake.sid ) {
1755       app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1756         if( err ) {
1757           handshake.user = false;
1758           return next(null, true);
1759         } else {
1760           // bake a new session object for full r/w
1761           handshake.session = new Session( handshake, session );
1762
1763           User.findOne( { session : handshake.sid }, function( err, user ) {
1764             if( user ) {
1765               handshake.user = user;
1766               return next(null, true);
1767             } else {
1768               handshake.user = false;
1769               return next(null, true);
1770             }
1771           });
1772         }
1773       })
1774     }
1775   } else {
1776     data.user = false;
1777     return next(null, true);
1778   }
1779 });
1780
1781 var backchannel = new Backchannel(app, io.of('/backchannel'), {
1782   subscribe: function(lecture, send) {
1783     Post.find({'lecture': lecture}, function(err, posts) {
1784       send(posts);
1785     });
1786   },
1787   post: function(fillPost) {
1788     var post = new Post;
1789     fillPost(post, function(send) {
1790       post.save(function(err) {
1791         send();
1792       });
1793     });
1794   },
1795   items: function(postId, addItem) {
1796     Post.findById(postId, function( err, post ) {
1797       addItem(post, function(send) {
1798         post.save(function(err) {
1799           send();
1800         });
1801       })
1802     })
1803   }
1804 });
1805
1806
1807
1808
1809 var counters = {};
1810
1811 var counts = io
1812 .of( '/counts' )
1813 .on( 'connection', function( socket ) {
1814   // pull out user/session information etc.
1815   var handshake = socket.handshake;
1816   var userID            = handshake.user._id;
1817
1818   var watched           = [];
1819   var noteID            = null;
1820
1821   var timer                     = null;
1822
1823   socket.on( 'join', function( note ) {
1824     if (handshake.user === false) {
1825       noteID                    = note;
1826       // XXX: replace by addToSet (once it's implemented in mongoose)
1827       Note.findById( noteID, function( err, note ) {
1828         if( note ) {
1829           if( note.collaborators.indexOf( userID ) == -1 ) {
1830             note.collaborators.push( userID );
1831             note.save();
1832           }
1833         }
1834       });
1835     }
1836   });
1837
1838   socket.on( 'watch', function( l ) {
1839     var sendCounts = function() {
1840       var send = {};
1841
1842       Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1843         async.forEach(
1844           notes,
1845           function( note, callback ) {
1846             var id              = note._id;
1847             var count   = note.collaborators.length;
1848
1849             send[ id ] = count;
1850
1851             callback();
1852           }, function() {
1853             socket.emit( 'counts', send );
1854
1855             timer = setTimeout( sendCounts, 5000 );
1856           }
1857         );
1858       });
1859     }
1860
1861     Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1862       notes.forEach( function( note ) {
1863         watched.push( note._id );
1864       });
1865     });
1866
1867     sendCounts();
1868   });
1869
1870   socket.on( 'disconnect', function() {
1871     clearTimeout( timer );
1872
1873     if (handshake.user === false) {
1874       // XXX: replace with $pull once it's available
1875       if( noteID ) {
1876         Note.findById( noteID, function( err, note ) {
1877           if( note ) {
1878             var index = note.collaborators.indexOf( userID );
1879
1880             if( index != -1 ) {
1881               note.collaborators.splice( index, 1 );
1882             }
1883
1884             note.save();
1885           }
1886         });
1887       }
1888     }
1889   });
1890 });
1891
1892 // Exception Catch-All
1893
1894 process.on('uncaughtException', function (e) {
1895   console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1896 });
1897
1898
1899 // Launch
1900
1901 mongoose.connect( app.set( 'dbUri' ) );
1902 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1903
1904 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1905
1906 app.listen( serverPort, function() {
1907   console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1908
1909   // if run as root, downgrade to the owner of this file
1910   if (process.getuid() === 0) {
1911     require('fs').stat(__filename, function(err, stats) {
1912       if (err) { return console.log(err); }
1913       process.setuid(stats.uid);
1914     });
1915   }
1916 });
1917
1918 function isValidEmail(email) {
1919   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])?/;
1920   return email.match(re);
1921 }