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