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