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