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