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