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