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