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