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.
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.
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');
28 // Used for initial testing
29 var log3 = function() {}
32 var app = module.exports = express.createServer();
34 // Load Mongoose Schemas
35 // The actual schemas are located in models.j
36 var User = mongoose.model( 'User' );
37 var School = mongoose.model( 'School' );
38 var Course = mongoose.model( 'Course' );
39 var Lecture = mongoose.model( 'Lecture' );
40 var Note = mongoose.model( 'Note' );
42 // More schemas used for legacy data
43 var ArchivedCourse = mongoose.model( 'ArchivedCourse' );
44 var ArchivedNote = mongoose.model( 'ArchivedNote' );
45 var ArchivedSubject = mongoose.model( 'ArchivedSubject' );
47 // XXX Not sure if necessary
48 var ObjectId = mongoose.SchemaTypes.ObjectId;
51 // Use the environment variable DEV_EMAIL for testing
52 var ADMIN_EMAIL = process.env.DEV_EMAIL || 'info@finalsclub.org';
54 // Set server hostname and port from environment variables,
56 // XXX Can be cleaned up
57 var serverHost = process.env.SERVER_HOST;
58 var serverPort = process.env.SERVER_PORT;
61 console.log( 'Using server hostname defined in environment: %s', serverHost );
63 serverHost = os.hostname();
64 console.log( 'No hostname defined, defaulting to os.hostname(): %s', serverHost );
67 // Express configuration depending on environment
68 // development is intended for developing locally or
69 // when not in production, otherwise production is used
70 // when the site will be run live for regular usage.
71 app.configure( 'development', function() {
72 // In development mode, all errors and stack traces will be
73 // dumped to the console and on page for easier troubleshooting
75 app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
77 // Set database connection information from environment
78 // variables otherwise use localhost.
79 app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
80 app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
82 // Set Amazon access and secret keys from environment
83 // variables. These keys are intended to be secret, so
84 // are not included in the source code, but set on the server
86 app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
87 app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
89 // If a port wasn't set earlier, set to 3000
95 // Production configuration settings
96 app.configure( 'production', function() {
97 // At the moment we have errors outputting everything
98 // so if there are any issues it is easier to track down.
99 // Once the site is more stable it will be prudent to
100 // use less error tracing.
101 app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
103 // Disable view cache due to stale views.
104 // XXX Disable view caching temp
105 app.disable( 'view cache' )
107 // Against setting the database connection information
108 // XXX Can be cleaned up or combined
109 app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
110 app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
112 // XXX Can be cleaned up or combined
113 app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
114 app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
116 // Set to port 80 if not set through environment variables
122 // General Express configuration settings
123 app.configure(function(){
124 // Views are housed in the views folder
125 app.set( 'views', __dirname + '/views' );
126 // All templates use jade for rendering
127 app.set( 'view engine', 'jade' );
128 // Bodyparser is required to handle form submissions
129 // without manually parsing them.
130 app.use( express.bodyParser() );
132 app.use( express.cookieParser() );
134 // Sessions are stored in mongodb which allows them
135 // to be persisted even between server restarts.
136 app.set( 'sessionStore', new mongoStore( {
137 'url' : app.set( 'dbUri' )
140 // This is where the actual Express session handler
141 // is defined, with a mongoStore being set as the
142 // session storage versus in memory storage that is
144 app.use( express.session( {
145 // A secret 'password' for encrypting and decrypting
147 // XXX Should be handled differently
148 'secret' : 'finalsclub',
149 // The max age of the cookies that is allowed
150 // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) * 1000 (milliseconds)
151 'maxAge' : new Date(Date.now() + (60 * 60 * 24 * 30 * 1000)),
152 'store' : app.set( 'sessionStore' )
155 // methodOverride is used to handle PUT and DELETE HTTP
156 // requests that otherwise aren't handled by default.
157 app.use( express.methodOverride() );
158 // Static files are loaded when no dynamic views match.
159 app.use( express.static( __dirname + '/public' ) );
160 // Sets the routers middleware to load after everything set
161 // before it, but before static files.
162 app.use( app.router );
164 app.use(express.logger({ format: ':method :url' }));
165 // This is the errorHandler set in configuration earlier
166 // being set to a variable to be used after all other
167 // middleware is loaded. Error handling should always
168 // come last or near the bottom.
169 var errorHandler = app.set( 'errorHandler' );
171 app.use( errorHandler );
175 // Mailer functions and helpers
176 // These are helper functions that make for cleaner code.
178 // sendUserActivation is for when a user registers and
179 // first needs to activate their account to use it.
180 function sendUserActivation( user ) {
184 'subject' : 'Activate your FinalsClub.org Account',
186 // Templates are in the email folder and use ejs
187 'template' : 'userActivation',
188 // Locals are used inside ejs so dynamic information
189 // can be rendered properly.
192 'serverHost' : serverHost
196 // Email is sent here
197 mailer.send( message, function( err, result ) {
199 // XXX: Add route to resend this email
200 console.log( 'Error sending user activation email\nError Message: '+err.Message );
202 console.log( 'Successfully sent user activation email.' );
207 // sendUserWelcome is for when a user registers and
208 // a welcome email is sent.
209 function sendUserWelcome( user, school ) {
210 // If a user is not apart of a supported school, they are
211 // sent a different template than if they are apart of a
213 var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
217 'subject' : 'Welcome to FinalsClub',
219 'template' : template,
222 'serverHost' : serverHost
226 mailer.send( message, function( err, result ) {
228 // XXX: Add route to resend this email
229 console.log( 'Error sending user welcome email\nError Message: '+err.Message );
231 console.log( 'Successfully sent user welcome email.' );
237 // These functions are used later in the routes to help
238 // load information and variables, as well as handle
239 // various instances like checking if a user is logged in
241 function loggedIn( req, res, next ) {
242 // If req.user is set, then pass on to the next function
243 // or else alert the user with an error message.
247 req.flash( 'error', 'You must be logged in to access that feature!' );
252 // This loads the user if logged in
253 function loadUser( req, res, next ) {
254 var sid = req.sessionID;
256 console.log( 'got request from session ID: %s', sid );
258 // Find a user based on their stored session id
259 User.findOne( { session : sid }, function( err, user ) {
264 // If a user is found then set req.user the contents of user
265 // and make sure req.user.loggedIn is true.
269 req.user.loggedIn = true;
271 log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
273 // Check if a user is activated. If not, then redirec
274 // to the homepage and tell them to check their email
275 // for the activation email.
276 if( req.user.activated ) {
277 // Is the user's profile complete? If not, redirect to their profile
278 if( ! req.user.isComplete ) {
279 if( url.parse( req.url ).pathname != '/profile' ) {
280 req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
282 res.redirect( '/profile' );
290 req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
295 // If no user record was found, then we store the requested
296 // path they intended to view and redirect them after they
297 // login if it is requred.
298 var path = url.parse( req.url ).pathname;
299 req.session.redirect = path;
301 // Set req.user to an empty object so it doesn't throw errors
302 // later on that it isn't defined.
310 // loadSchool is used to load a school by it's id
311 function loadSchool( req, res, next ) {
313 var schoolId = req.params.id;
315 School.findById( schoolId, function( err, school ) {
319 // If a school is found, the user is checked to see if they are
320 // authorized to see or interact with anything related to that
322 school.authorize( user, function( authorized ){
323 req.school.authorized = authorized;
327 // If no school is found, display an appropriate error.
328 res.json( {status: 'error', message: 'Invalid school specified!'} );
333 // loadSchool is used to load a course by it's id
334 function loadCourse( req, res, next ) {
336 var courseId = req.params.id;
338 Course.findById( courseId, function( err, course ) {
339 if( course && !course.deleted ) {
342 // If a course is found, the user is checked to see if they are
343 // authorized to see or interact with anything related to that
345 course.authorize( user, function( authorized ) {
346 req.course.authorized = authorized;
351 // If no course is found, display an appropriate error.
352 res.json( {status: 'error', message: 'Invalid course specified!'} );
357 // loadLecture is used to load a lecture by it's id
358 function loadLecture( req, res, next ) {
360 var lectureId = req.params.id;
362 Lecture.findById( lectureId, function( err, lecture ) {
363 if( lecture && !lecture.deleted ) {
364 req.lecture = lecture;
366 // If a lecture is found, the user is checked to see if they are
367 // authorized to see or interact with anything related to that
369 lecture.authorize( user, function( authorized ) {
370 req.lecture.authorized = authorized;
375 // If no lecture is found, display an appropriate error.
376 res.json( {status: 'error', message: 'Invalid lecture specified!'} );
381 // loadNote is used to load a note by it's id
382 // This is a lot more complicated than the above
383 // due to public/private handling of notes.
384 function loadNote( req, res, next ) {
385 var user = req.user ? req.user : false;
386 var noteId = req.params.id;
388 Note.findById( noteId, function( err, note ) {
389 // If a note is found, and user is set, check if
390 // user is authorized to interact with that note.
391 if( note && user && !note.deleted ) {
392 note.authorize( user, function( auth ) {
394 // If authorzied, then set req.note to be used later
398 } else if ( note.public ) {
399 // If not authorized, but the note is public, then
400 // designate the note read only (RO) and store req.note
406 // If the user is not authorized and the note is private
407 // then display and error.
408 res.json( {status: 'error', message: 'You do not have permission to access that note.'} );
411 } else if ( note && note.public && !note.deleted ) {
412 // If note is found, but user is not set because they are not
413 // logged in, and the note is public, set the note to read only
414 // and store the note for later.
419 } else if ( note && !note.public && !note.deleted ) {
420 // If the note is found, but user is not logged in and the note is
421 // not public, then ask them to login to view the note. Once logged
422 // in they will be redirected to the note, at which time authorization
423 // handling will be put in effect above.
424 //req.session.redirect = '/note/' + note._id;
425 res.json( {status: 'error', message: 'You must be logged in to view that note.'} );
428 res.json( {status: 'error', message: 'Invalid note specified!'} );
433 function checkAjax( req, res, next ) {
437 res.sendfile( 'public/index.html' );
441 // Dynamic Helpers are loaded automatically into views
442 app.dynamicHelpers( {
443 // express-messages is for flash messages for easy
444 // errors and information display
445 'messages' : require( 'express-messages' ),
447 // By default the req object isn't sen't to views
448 // during rendering, this allows you to use the
449 // user object if available in views.
450 'user' : function( req, res ) {
454 // Same, this allows session to be available in views.
455 'session' : function( req, res ) {
461 // The following are the main CRUD routes that are used
462 // to make up this web app.
467 app.get( '/', loadUser, function( req, res ) {
470 res.render( 'index' );
475 // Used to display all available schools and any courses
477 // Public with some private information
478 app.get( '/schools', checkAjax, loadUser, function( req, res ) {
482 // Find all schools and sort by name
483 // XXX mongoose's documentation on sort is extremely poor, tread carefully
484 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
486 // If schools are found, loop through them gathering any courses that are
487 // associated with them and then render the page with that information.
488 res.json({ 'schools' : schools.map(function(school) {
489 return school.sanitized;
492 // If no schools have been found, display none
493 //res.render( 'schools', { 'schools' : [] } );
494 res.json({ 'schools' : [] });
499 app.get( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
500 var school = req.school;
503 school.authorize( user, function( authorized ) {
504 // This is used to display interface elements for those users
505 // that are are allowed to see th)m, for instance a 'New Course' button.
506 var sanitizedSchool = school.sanitized;
507 sanitizedSchool.authorized = authorized;
508 // Find all courses for school by it's id and sort by name
509 Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
510 // If any courses are found, set them to the appropriate school, otherwise
512 if( courses.length > 0 ) {
513 sanitizedSchool.courses = courses.filter(function(course) {
514 if (!course.deleted) return course;
515 }).map(function(course) {
516 return course.sanitized;
519 sanitizedSchool.courses = [];
521 // This tells async (the module) that each iteration of forEach is
522 // done and will continue to call the rest until they have all been
523 // completed, at which time the last function below will be called.
524 res.json({ 'school': sanitizedSchool })
530 // Displays form to create new course
531 // Private, requires user to be authorized
532 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
533 // Load school from middleware
534 var school = req.school;
536 // If school was not loaded for whatever reason, or the user is not authorized
537 // then redirect to the main schools page.
538 if( ( ! school ) || ( ! school.authorized ) ) {
539 return res.redirect( '/schools' );
542 // If they are authorized and the school exists, then render the page
543 res.render( 'course/new', { 'school': school } );
546 // Recieves new course form
547 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
548 var school = req.school;
549 // Creates new course from Course Schema
550 var course = new Course;
551 // Gathers instructor information from form
552 var instructorEmail = req.body.email.toLowerCase();
553 var instructorName = req.body.instructorName;
555 // If school doesn't exist or user is not authorized redirect to main schools page
556 if( ( ! school ) || ( ! school.authorized ) ) {
557 res.redirect( '/schools' );
560 // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
561 if ( !instructorEmail || !instructorName ) {
562 req.flash( 'error', 'Invalid parameters!' )
563 return res.render( 'course/new' );
566 // Fill out the course with information from the form
567 course.number = req.body.number;
568 course.name = req.body.name;
569 course.description = req.body.description;
570 course.school = school._id;
571 course.creator = req.user._id;
572 course.subject = req.body.subject;
573 course.department = req.body.department;
575 // Check if a user exists with the instructorEmail, if not then create
576 // a new user and send them an instructor welcome email.
577 User.findOne( { 'email' : instructorEmail }, function( err, user ) {
581 user.name = instructorName
582 user.email = instructorEmail;
583 user.affil = 'Instructor';
584 user.school = school.name;
586 user.activated = false;
588 // Validate instructorEmail
589 // XXX Probably could be done before checking db
590 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
591 req.flash( 'error', 'Please enter a valid email' );
592 // XXX This needs to be fixed, this is not the proper flow
593 return res.redirect( '/register' );
595 // Once the new user information has been completed, save the user
596 // to the database then email them the instructor welcome email.
597 user.save(function( err ) {
598 // If there was an error saving the instructor, prompt the user to fill out
599 // the information again.
601 req.flash( 'error', 'Invalid parameters!' )
602 return res.render( 'course/new' );
607 'subject' : 'A non-profit open education initiative',
609 'template' : 'instructorInvite',
614 'serverHost' : serverHost
618 mailer.send( message, function( err, result ) {
620 console.log( 'Error inviting instructor to course!' );
622 console.log( 'Successfully invited instructor to course.' );
626 // After emails are sent, set the courses instructor to the
627 // new users id and then save the course to the database.
628 course.instructor = user._id;
629 course.save( function( err ) {
631 // XXX better validation
632 req.flash( 'error', 'Invalid parameters!' );
634 return res.render( 'course/new' );
636 // Once the course has been completed email the admin with information
637 // on the course and new instructor
641 'subject' : school.name+' has a new course: '+course.name,
643 'template' : 'newCourse',
648 'serverHost' : serverHost
652 mailer.send( message, function( err, result ) {
654 console.log( 'Error sending new course email to info@finalsclub.org' )
656 console.log( 'Successfully invited instructor to course')
659 // Redirect the user to the schools page where they can see
661 // XXX Redirect to the new course instead
662 res.redirect( '/schools' );
668 // If the user exists, then check if they are already and instructor
669 if (user.affil === 'Instructor') {
670 // If they are an instructor, then save the course with the appropriate
671 // information and email the admin.
672 course.instructor = user._id;
673 course.save( function( err ) {
675 // XXX better validation
676 req.flash( 'error', 'Invalid parameters!' );
678 return res.render( 'course/new' );
683 'subject' : school.name+' has a new course: '+course.name,
685 'template' : 'newCourse',
690 'serverHost' : serverHost
694 mailer.send( message, function( err, result ) {
696 console.log( 'Error sending new course email to info@finalsclub.org' )
698 console.log( 'Successfully invited instructor to course')
701 // XXX Redirect to the new course instead
702 res.redirect( '/schools' );
706 // The existing user isn't an instructor, so the user is notified of the error
707 // and the course isn't created.
708 req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
709 res.render( 'course/new' );
715 // Individual Course Listing
716 // Public with private information
717 app.get( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
718 var userId = req.user._id;
719 var course = req.course;
721 // Check if the user is subscribed to the course
722 // XXX Not currently used for anything
723 var subscribed = course.subscribed( userId );
725 // Find lectures associated with this course and sort by name
726 Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
727 // Get course instructor information using their id
728 User.findById( course.instructor, function( err, instructor ) {
729 // Render course and lectures
730 res.json( { 'course' : course.sanitized, 'instructor': instructor.sanitized, 'subscribed' : subscribed, 'lectures' : lectures.map(function(lecture) { return lecture.sanitized })} );
736 app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
737 var course = req.course;
741 res.render( 'course/new', {course: course} )
743 req.flash( 'error', 'You don\'t have permission to do that' )
744 res.redirect( '/schools' );
748 // Recieve Course Edit Form
749 app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
750 var course = req.course;
754 var courseChanges = req.body;
755 course.number = courseChanges.number;
756 course.name = courseChanges.name;
757 course.description = courseChanges.description;
758 course.department = courseChanges.department;
760 course.save(function(err) {
762 req.flash( 'error', 'There was an error saving the course' );
764 res.redirect( '/course/'+ course._id.toString());
767 req.flash( 'error', 'You don\'t have permission to do that' )
768 res.redirect( '/schools' );
773 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
774 var course = req.course;
778 course.delete(function( err ) {
779 if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
780 else req.flash( 'info', 'Successfully removed course' )
781 res.redirect( '/schools' );
784 req.flash( 'error', 'You don\'t have permission to do that' )
785 res.redirect( '/schools' );
789 // Subscribe to course
790 // XXX Not currently used for anything
791 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
792 var course = req.course;
793 var userId = req.user._id;
795 course.subscribe( userId, function( err ) {
797 req.flash( 'error', 'Error subscribing to course!' );
800 res.redirect( '/course/' + course._id );
804 // Unsubscribe from course
805 // XXX Not currently used for anything
806 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
807 var course = req.course;
808 var userId = req.user._id;
810 course.unsubscribe( userId, function( err ) {
812 req.flash( 'error', 'Error unsubscribing from course!' );
815 res.redirect( '/course/' + course._id );
819 // Create new lecture
820 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
821 var courseId = req.params.id;
822 var course = req.course;
825 // If course isn't valid or user isn't authorized for course, redirect
826 if( ( ! course ) || ( ! course.authorized ) ) {
827 return res.redirect( '/course/' + courseId );
830 // Render new lecture form
831 res.render( 'lecture/new', { 'lecture' : lecture } );
834 // Recieve New Lecture Form
835 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
836 var courseId = req.params.id;
837 var course = req.course;
838 // Create new lecture from Lecture schema
839 var lecture = new Lecture;
841 if( ( ! course ) || ( ! course.authorized ) ) {
842 res.redirect( '/course/' + courseId );
847 // Populate lecture with form data
848 lecture.name = req.body.name;
849 lecture.date = req.body.date;
850 lecture.course = course._id;
851 lecture.creator = req.user._id;
853 // Save lecture to database
854 lecture.save( function( err ) {
856 // XXX better validation
857 req.flash( 'error', 'Invalid parameters!' );
859 res.render( 'lecture/new', { 'lecture' : lecture } );
861 // XXX Redirect to new lecture instead
862 res.redirect( '/course/' + course._id );
868 // Display individual lecture and related notes
869 app.get( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
870 var lecture = req.lecture;
872 // Grab the associated course
873 // XXX this should be done with DBRefs eventually
874 Course.findById( lecture.course, function( err, course ) {
876 // If course is found, find instructor information to be displayed on page
877 User.findById( course.instructor, function( err, instructor ) {
878 // Pull out our notes
879 Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
880 if ( !req.user.loggedIn || !req.lecture.authorized ) {
881 // Loop through notes and only return those that are public if the
882 // user is not logged in or not authorized for that lecture
883 notes = notes.filter(function( note ) {
884 if ( note.public ) return note;
888 'lecture' : lecture.sanitized,
889 'course' : course.sanitized,
890 'instructor' : instructor.sanitized,
891 'notes' : notes.map(function(note) {
892 return note.sanitized;
898 res.json( { status: 'error', msg: 'This course is orphaned' })
903 // Display new note form
904 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
905 var lectureId = req.params.id;
906 var lecture = req.lecture;
909 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
910 res.redirect( '/lecture/' + lectureId );
915 res.render( 'notes/new', { 'note' : note } );
918 // Recieve new note form
919 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
920 var lectureId = req.params.id;
921 var lecture = req.lecture;
923 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
924 res.redirect( '/lecture/' + lectureId );
929 // Create note from Note schema
932 // Populate note from form data
933 note.name = req.body.name;
934 note.date = req.body.date;
935 note.lecture = lecture._id;
936 note.public = req.body.private ? false : true;
937 note.creator = req.user._id;
939 // Save note to database
940 note.save( function( err ) {
942 // XXX better validation
943 req.flash( 'error', 'Invalid parameters!' );
945 res.render( 'notes/new', { 'note' : note } );
947 // XXX Redirect to new note instead
948 res.redirect( '/lecture/' + lecture._id );
954 // Display individual note page
955 app.get( '/note/:id', /*checkAjax,*/ loadUser, loadNote, function( req, res ) {
957 // Set read only id for etherpad-lite or false for later check
958 var roID = note.roID || false;
960 var lectureId = note.lecture;
962 // Count the amount of visits, but only once per session
963 if ( req.session.visited ) {
964 if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
965 req.session.visited.push( note._id );
969 req.session.visited = [];
970 req.session.visited.push( note._id );
974 // If a read only id exists process note
978 // If read only id doesn't, then fetch the read only id from the database and then
980 // XXX Soon to be depracated due to a new API in etherpad that makes for a
981 // much cleaner solution.
982 db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
983 epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
985 roID = record.value.replace(/"/g, '');
994 function processReq() {
996 Lecture.findById( lectureId, function( err, lecture ) {
998 req.flash( 'error', 'That notes page is orphaned!' );
1000 res.redirect( '/' );
1002 // Find notes based on lecture id, which will be displayed in a dropdown
1004 Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1007 'host' : serverHost,
1008 'note' : note.sanitized,
1009 'lecture' : lecture.sanitized,
1010 'otherNotes' : otherNotes.map(function(note) {
1011 return note.sanitized;
1018 // User is logged in and sees full notepad
1020 res.render( 'notes/index', {
1021 'layout' : 'noteLayout',
1022 'host' : serverHost,
1024 'lecture' : lecture,
1025 'otherNotes' : otherNotes,
1028 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1029 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1032 // User is not logged in and sees notepad that is public
1033 res.render( 'notes/public', {
1034 'layout' : 'noteLayout',
1035 'host' : serverHost,
1037 'otherNotes' : otherNotes,
1039 'lecture' : lecture,
1040 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1041 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1049 // Static pages and redirects
1050 app.get( '/about', loadUser, function( req, res ) {
1051 res.redirect( 'http://blog.finalsclub.org/about.html' );
1054 app.get( '/press', loadUser, function( req, res ) {
1055 res.render( 'static/press' );
1058 app.get( '/conduct', loadUser, function( req, res ) {
1059 res.render( 'static/conduct' );
1062 app.get( '/legal', loadUser, function( req, res ) {
1063 res.redirect( 'http://blog.finalsclub.org/legal.html' );
1066 app.get( '/contact', loadUser, function( req, res ) {
1067 res.redirect( 'http://blog.finalsclub.org/contact.html' );
1070 app.get( '/privacy', loadUser, function( req, res ) {
1071 res.render( 'static/privacy' );
1075 // Authentication routes
1076 // These are used for logging in, logging out, registering
1077 // and other user authentication purposes
1079 // Render login page
1080 app.get( '/login', function( req, res ) {
1081 log3("get login page")
1083 res.render( 'login' );
1086 // Recieve login form
1087 app.post( '/login', function( req, res ) {
1088 var email = req.body.email;
1089 var password = req.body.password;
1090 log3("post login ...")
1092 // Find user from email
1093 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1097 // If user exists, check if activated, if not notify them and send them to
1100 if( ! user.activated ) {
1101 // (undocumented) markdown-esque link functionality in req.flash
1102 req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1104 req.session.activateCode = user._id;
1106 res.render( 'login' );
1108 // If user is activated, check if their password is correct
1109 if( user.authenticate( password ) ) {
1112 var sid = req.sessionID;
1116 // Set the session then save the user to the database
1117 user.save( function() {
1118 var redirect = req.session.redirect;
1120 // login complete, remember the user's email for next time
1121 req.session.email = email;
1123 // alert the successful login
1124 req.flash( 'info', 'Successfully logged in!' );
1126 // redirect to profile if we don't have a stashed request
1127 res.redirect( redirect || '/profile' );
1130 // Notify user of bad login
1131 req.flash( 'error', 'Invalid login!' );
1133 res.render( 'login' );
1137 // Notify user of bad login
1139 req.flash( 'error', 'Invalid login!' );
1141 res.render( 'login' );
1146 // Request reset password
1147 app.get( '/resetpw', function( req, res ) {
1148 log3("get resetpw page");
1149 res.render( 'resetpw' );
1152 // Display reset password from requested email
1153 app.get( '/resetpw/:id', function( req, res ) {
1154 var resetPassCode = req.params.id
1155 res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1158 // Recieve reset password request form
1159 app.post( '/resetpw', function( req, res ) {
1160 log3("post resetpw");
1161 var email = req.body.email
1165 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1168 // If user exists, create reset code
1169 var resetPassCode = hat(64);
1170 user.setResetPassCode(resetPassCode);
1172 // Construct url that the user can then click to reset password
1173 var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1175 // Save user to database
1176 user.save( function( err ) {
1177 log3('save '+user.email);
1179 // Construct email and send it to the user
1183 'subject' : 'Your FinalsClub.org Password has been Reset!',
1185 'template' : 'userPasswordReset',
1187 'resetPassCode' : resetPassCode,
1188 'resetPassUrl' : resetPassUrl
1192 mailer.send( message, function( err, result ) {
1194 // XXX: Add route to resend this email
1196 console.log( 'Error sending user password reset email!' );
1198 console.log( 'Successfully sent user password reset email.' );
1203 // Render request success page
1204 res.render( 'resetpw-success', { 'email' : email } );
1208 res.render( 'resetpw-error', { 'email' : email } );
1213 // Recieve reset password form
1214 app.post( '/resetpw/:id', function( req, res ) {
1215 log3("post resetpw.code");
1216 var resetPassCode = req.params.id
1217 var email = req.body.email
1218 var pass1 = req.body.pass1
1219 var pass2 = req.body.pass2
1221 // Find user by email
1222 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1224 // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1225 // save user with new password and display success message.
1227 var valid = user.resetPassword(resetPassCode, pass1, pass2);
1229 user.save( function( err ) {
1230 res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );
1235 // If there was a problem, notify user
1237 res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1242 // Display registration page
1243 app.get( '/register', function( req, res ) {
1244 log3("get reg page");
1246 // Populate school dropdown list
1247 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1248 res.render( 'register', { 'schools' : schools } );
1252 // Recieve registration form
1253 app.post( '/register', function( req, res ) {
1254 var sid = req.sessionId;
1256 // Create new user from User schema
1257 var user = new User;
1259 // Populate user from form
1260 user.email = req.body.email.toLowerCase();
1261 user.password = req.body.password;
1263 // If school is set to other, then fill in school as what the
1265 user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1266 user.name = req.body.name;
1267 user.affil = req.body.affil;
1268 user.activated = false;
1271 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1272 req.flash( 'error', 'Please enter a valid email' );
1273 return res.redirect( '/register' );
1276 // Check if password is greater than 6 characters, otherwise notify user
1277 if ( req.body.password.length < 6 ) {
1278 req.flash( 'error', 'Please enter a password longer than eight characters' );
1279 return res.redirect( '/register' );
1282 // Pull out hostname from email
1283 var hostname = user.email.split( '@' ).pop();
1285 // Check if email is from one of the special domains
1286 if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1290 // Save user to database
1291 user.save( function( err ) {
1292 // If error, check if it is because the user already exists, if so
1293 // get the user information and let them know
1295 if( /dup key/.test( err.message ) ) {
1296 // attempting to register an existing address
1297 User.findOne({ 'email' : user.email }, function(err, result ) {
1298 if (result.activated) {
1299 // If activated, make sure they know how to contact the admin
1300 req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1301 return res.redirect( '/register' )
1303 // If not activated, direct them to the resendActivation page
1304 req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1305 return res.redirect( '/resendActivation' )
1309 // If any other type of error, prompt them to enter the registration again
1310 req.flash( 'error', 'An error occurred during registration.' );
1312 return res.redirect( '/register' );
1315 // send user activation email
1316 sendUserActivation( user );
1318 // Check if the hostname matches any in the approved schools
1319 School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1321 // If there is a match, send associated welcome message
1322 sendUserWelcome( user, true );
1323 log3('school recognized '+school.name);
1324 // If no users exist for the school, create empty array
1325 if (!school.users) school.users = [];
1326 // Add user to the school
1327 school.users.push( user._id );
1329 // Save school to the database
1330 school.save( function( err ) {
1331 log3('school.save() done');
1332 // Notify user that they have been added to the school
1333 req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1334 res.redirect( '/' );
1336 // Construct admin email about user registration
1340 'subject' : 'FC User Registration : User added to ' + school.name,
1342 'template' : 'userSchool',
1348 // If there isn't a match, send associated welcome message
1349 sendUserWelcome( user, false );
1350 // Tell user to check for activation link
1351 req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1352 res.redirect( '/' );
1353 // Construct admin email about user registration
1357 'subject' : 'FC User Registration : Email did not match any schools',
1359 'template' : 'userNoSchool',
1365 // Send email to admin
1366 mailer.send( message, function( err, result ) {
1369 console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1371 console.log( 'Successfully sent user has no school email to admin.' );
1381 // Display resendActivation request page
1382 app.get( '/resendActivation', function( req, res ) {
1383 var activateCode = req.session.activateCode;
1385 // Check if user exists by activateCode set in their session
1386 User.findById( activateCode, function( err, user ) {
1387 if( ( ! user ) || ( user.activated ) ) {
1388 res.redirect( '/' );
1390 // Send activation and redirect to login
1391 sendUserActivation( user );
1393 req.flash( 'info', 'Your activation code has been resent.' );
1395 res.redirect( '/login' );
1400 // Display activation page
1401 app.get( '/activate/:code', function( req, res ) {
1402 var code = req.params.code;
1404 // XXX could break this out into a middleware
1406 res.redirect( '/' );
1409 // Find user by activation code
1410 User.findById( code, function( err, user ) {
1411 if( err || ! user ) {
1412 // If not found, notify user of invalid code
1413 req.flash( 'error', 'Invalid activation code!' );
1415 res.redirect( '/' );
1417 // If valid, then activate user
1418 user.activated = true;
1420 // Regenerate our session and log in as the new user
1421 req.session.regenerate( function() {
1422 user.session = req.sessionID;
1424 // Save user to database
1425 user.save( function( err ) {
1427 req.flash( 'error', 'Unable to activate account.' );
1429 res.redirect( '/' );
1431 req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1433 res.redirect( '/profile' );
1442 app.get( '/logout', function( req, res ) {
1443 var sid = req.sessionID;
1445 // Find user by session id
1446 User.findOne( { 'session' : sid }, function( err, user ) {
1448 // Empty out session id
1451 // Save user to database
1452 user.save( function( err ) {
1453 res.redirect( '/' );
1456 res.redirect( '/' );
1461 // Display users profile page
1462 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1463 var user = req.user;
1465 res.render( 'profile/index', { 'user' : user } );
1468 // Recieve profile edit page form
1469 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1470 var user = req.user;
1471 var fields = req.body;
1474 var wasComplete = user.isComplete;
1476 if( ! fields.name ) {
1477 req.flash( 'error', 'Please enter a valid name!' );
1481 user.name = fields.name;
1484 if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1485 req.flash( 'error', 'Please select a valid affiliation!' );
1489 user.affil = fields.affiliation;
1492 if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1493 // changing password
1494 if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1495 if( fields.newPassword === fields.newPasswordConfirm ) {
1496 // test password strength?
1498 user.password = fields.newPassword;
1500 req.flash( 'error', 'Mismatch in new password!' );
1505 req.flash( 'error', 'Please supply your existing password.' );
1511 user.major = fields.major;
1512 user.bio = fields.bio;
1514 user.showName = ( fields.showName ? true : false );
1517 user.save( function( err ) {
1519 req.flash( 'error', 'Unable to save user profile!' );
1521 if( ( user.isComplete ) && ( ! wasComplete ) ) {
1522 req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1524 res.redirect( '/' );
1526 res.render( 'info', 'Your profile was successfully updated!' );
1528 res.render( 'profile/index', { 'user' : user } );
1533 res.render( 'profile/index', { 'user' : user } );
1540 function loadSubject( req, res, next ) {
1541 if( url.parse( req.url ).pathname.match(/subject/) ) {
1542 ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1544 res.json( {status: 'error', message: 'Subject with this ID does not exist'} )
1546 req.subject = subject;
1555 function loadOldCourse( req, res, next ) {
1556 if( url.parse( req.url ).pathname.match(/course/) ) {
1557 ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1559 res.json( {status: 'error', message: 'Course with this ID does not exist'} )
1561 req.course = course;
1570 var featuredCourses = [
1571 {name: 'The Human Mind', 'id': 1563},
1572 {name: 'Justice', 'id': 797},
1573 {name: 'Protest Literature', 'id': 1681},
1574 {name: 'Animal Cognition', 'id': 681},
1575 {name: 'Positive Psychology', 'id': 1793},
1576 {name: 'Social Psychology', 'id': 660},
1577 {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1578 {name: 'Cyberspace in Court', 'id': 1446},
1579 {name: 'Nazi Cinema', 'id': 2586},
1580 {name: 'Media and the American Mind', 'id': 2583},
1581 {name: 'Social Thought in Modern America', 'id': 2585},
1582 {name: 'Major British Writers II', 'id': 869},
1583 {name: 'Civil Procedure', 'id': 2589},
1584 {name: 'Evidence', 'id': 2590},
1585 {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1588 app.get( '/learn', loadUser, function( req, res ) {
1589 res.render( 'archive/learn', { 'courses' : featuredCourses } );
1592 app.get( '/learn/random', loadUser, function( req, res ) {
1593 res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1596 app.get( '/archive', checkAjax, loadUser, function( req, res ) {
1597 ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1599 res.json( {status: 'error', message: 'There was a problem gathering the archived courses, please try again later.'} );
1601 res.json( { 'subjects' : subjects } );
1606 app.get( '/archive/subject/:id', checkAjax, loadUser, loadSubject, function( req, res ) {
1607 ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1609 res.json( {status: 'error', message: 'There are no archived courses'} );
1611 res.json( { 'courses' : courses, 'subject': req.subject } );
1616 app.get( '/archive/course/:id', checkAjax, loadUser, loadOldCourse, function( req, res ) {
1617 ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1619 res.json( {status: 'error', message: 'There are no notes in this course'} );
1621 res.json( { 'notes' : notes, 'course' : req.course } );
1626 app.get( '/archive/note/:id', checkAjax, loadUser, function( req, res ) {
1627 ArchivedNote.findById(req.params.id, function(err, note) {
1629 res.json( {status: 'error', message: 'This is not a valid id for a note'} );
1631 ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1633 res.json( {status: 'error', message: 'There is no course for this note'} )
1635 res.json( { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
1643 app.get( '*', function(req, res) {
1644 res.sendfile('public/index.html');
1649 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1650 // the browser which facilitates near realtime interaction. This allows the user to post questions
1651 // and comments and other users to get those almost immediately after they are posted, without
1652 // reloading the page or pressing a button to refresh.
1654 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1655 // saving the data to the database, and then sending it out to everyone else connected.
1658 // Posts - Posts are the main items in backchannel, useful for questions or discussion points
1659 // [[ example object needed with explanation E.G:
1661 Post: { postID: '999-1',
1663 userName: 'Bob Jones',
1664 userAffil: 'Instructor',
1665 body: 'This is the text content of the post.',
1666 comments: { {<commentObj>, <commentObj>, ...},
1668 votes: [ <userID>, <userID>, ...],
1669 reports: [ <userID>, <userID>, ...]
1671 Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
1673 if anonymous: userName => 'Anonymous', userAffil => 'N/A'
1678 // Comments - Comments are replies to posts, for clarification or answering questions
1679 // [[ example object needed]]
1680 // Votes - Votes signifyg a users approval of a post
1681 // [[ example object needed]]
1682 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1683 // [[ example object needed]]
1687 // body - Main content of the post
1688 // userId - Not currently used, but would contain the users id that made the post
1689 // userName - Users name that made post
1690 // userAffil - Users affiliation to their school
1691 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1692 // date - Date post was made, updates when any comments are made for the post
1693 // comments - An array of comments which contain a body, userName, and userAffil
1694 // votes - An array of user ids which are the users that voted
1695 // [[ example needed ]]
1696 // reports - An array of user ids which are the users that reported the post
1697 // [[ reports would be "this post is flagged as inappropriate"? ]]
1698 // [[ bruml: consistent terminology needed ]]
1700 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1701 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1702 // users the ability to make posts or comments that they might not otherwise due to not wanting
1703 // the content of the post/comment to be attributed to them.
1705 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1706 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1707 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1708 // user is logged in, and if so display information that otherwise might not be visible to them if they
1709 // aren't apart of a particular school.
1711 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1712 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1713 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1714 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1715 // rendered in the browser.
1717 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1718 // events are as follows.
1721 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1722 // The server recieves the data, assembles a new post object for the database and then fills it with
1723 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1724 // replaced. If the user chose for the post to be private, then public will be set to false and it
1725 // will be filtered from being sent to users not logged into and not having access to the school. Once
1726 // the post has been created and saved into the database, it is sent to all connected users to that
1727 // particular lecture, unless it is private, than only logged in users will get it.
1730 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1731 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1732 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1733 // users unless the post is private, which then it will be only sent to logged in users.
1736 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1737 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1738 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1739 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1740 // bottom of the interface.
1743 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1744 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1745 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1746 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1747 // to the top of the interface. The comment can be anonymous, which then will have the user
1748 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1749 // connected users unless the post is private, then only logged in users will recieve the comment.
1751 var io = require( 'socket.io' ).listen( app );
1753 var Post = mongoose.model( 'Post' );
1755 io.set('authorization', function ( handshake, next ) {
1756 var rawCookie = handshake.headers.cookie;
1758 handshake.cookie = parseCookie(rawCookie);
1759 handshake.sid = handshake.cookie['connect.sid'];
1761 if ( handshake.sid ) {
1762 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1764 handshake.user = false;
1765 return next(null, true);
1767 // bake a new session object for full r/w
1768 handshake.session = new Session( handshake, session );
1770 User.findOne( { session : handshake.sid }, function( err, user ) {
1772 handshake.user = user;
1773 return next(null, true);
1775 handshake.user = false;
1776 return next(null, true);
1784 return next(null, true);
1788 var backchannel = new Backchannel(app, io.of('/backchannel'), {
1789 subscribe: function(lecture, send) {
1790 Post.find({'lecture': lecture}, function(err, posts) {
1794 post: function(fillPost) {
1795 var post = new Post;
1796 fillPost(post, function(send) {
1797 post.save(function(err) {
1802 items: function(postId, addItem) {
1803 Post.findById(postId, function( err, post ) {
1804 addItem(post, function(send) {
1805 post.save(function(err) {
1820 .on( 'connection', function( socket ) {
1821 // pull out user/session information etc.
1822 var handshake = socket.handshake;
1823 var userID = handshake.user._id;
1830 socket.on( 'join', function( note ) {
1831 if (handshake.user === false) {
1833 // XXX: replace by addToSet (once it's implemented in mongoose)
1834 Note.findById( noteID, function( err, note ) {
1836 if( note.collaborators.indexOf( userID ) == -1 ) {
1837 note.collaborators.push( userID );
1845 socket.on( 'watch', function( l ) {
1846 var sendCounts = function() {
1849 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1852 function( note, callback ) {
1854 var count = note.collaborators.length;
1860 socket.emit( 'counts', send );
1862 timer = setTimeout( sendCounts, 5000 );
1868 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1869 notes.forEach( function( note ) {
1870 watched.push( note._id );
1877 socket.on( 'disconnect', function() {
1878 clearTimeout( timer );
1880 if (handshake.user === false) {
1881 // XXX: replace with $pull once it's available
1883 Note.findById( noteID, function( err, note ) {
1885 var index = note.collaborators.indexOf( userID );
1888 note.collaborators.splice( index, 1 );
1899 // Exception Catch-All
1901 process.on('uncaughtException', function (e) {
1902 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1908 mongoose.connect( app.set( 'dbUri' ) );
1909 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1911 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1913 app.listen( serverPort, function() {
1914 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1916 // if run as root, downgrade to the owner of this file
1917 if (process.getuid() === 0) {
1918 require('fs').stat(__filename, function(err, stats) {
1919 if (err) { return console.log(err); }
1920 process.setuid(stats.uid);
1925 function isValidEmail(email) {
1926 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])?/;
1927 return email.match(re);