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