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