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