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