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 // Sets the routers middleware to load after everything set
159 // before it, but before static files.
160 app.use( app.router );
161 // Static files are loaded when no dynamic views match.
162 app.use( express.static( __dirname + '/public' ) );
164 // This is the errorHandler set in configuration earlier
165 // being set to a variable to be used after all other
166 // middleware is loaded. Error handling should always
167 // come last or near the bottom.
168 var errorHandler = app.set( 'errorHandler' );
170 app.use( errorHandler );
174 // Mailer functions and helpers
175 // These are helper functions that make for cleaner code.
177 // sendUserActivation is for when a user registers and
178 // first needs to activate their account to use it.
179 function sendUserActivation( user ) {
183 'subject' : 'Activate your FinalsClub.org Account',
185 // Templates are in the email folder and use ejs
186 'template' : 'userActivation',
187 // Locals are used inside ejs so dynamic information
188 // can be rendered properly.
191 'serverHost' : serverHost
195 // Email is sent here
196 mailer.send( message, function( err, result ) {
198 // XXX: Add route to resend this email
199 console.log( 'Error sending user activation email\nError Message: '+err.Message );
201 console.log( 'Successfully sent user activation email.' );
206 // sendUserWelcome is for when a user registers and
207 // a welcome email is sent.
208 function sendUserWelcome( user, school ) {
209 // If a user is not apart of a supported school, they are
210 // sent a different template than if they are apart of a
212 var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
216 'subject' : 'Welcome to FinalsClub',
218 'template' : template,
221 'serverHost' : serverHost
225 mailer.send( message, function( err, result ) {
227 // XXX: Add route to resend this email
228 console.log( 'Error sending user welcome email\nError Message: '+err.Message );
230 console.log( 'Successfully sent user welcome email.' );
236 // These functions are used later in the routes to help
237 // load information and variables, as well as handle
238 // various instances like checking if a user is logged in
240 function loggedIn( req, res, next ) {
241 // If req.user is set, then pass on to the next function
242 // or else alert the user with an error message.
246 req.flash( 'error', 'You must be logged in to access that feature!' );
251 // This loads the user if logged in
252 function loadUser( req, res, next ) {
253 var sid = req.sessionID;
255 console.log( 'got request from session ID: %s', sid );
257 // Find a user based on their stored session id
258 User.findOne( { session : sid }, function( err, user ) {
263 // If a user is found then set req.user the contents of user
264 // and make sure req.user.loggedIn is true.
268 req.user.loggedIn = true;
270 log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
272 // Check if a user is activated. If not, then redirec
273 // to the homepage and tell them to check their email
274 // for the activation email.
275 if( req.user.activated ) {
276 // Is the user's profile complete? If not, redirect to their profile
277 if( ! req.user.isComplete ) {
278 if( url.parse( req.url ).pathname != '/profile' ) {
279 req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
281 res.redirect( '/profile' );
289 req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
294 // If no user record was found, then we store the requested
295 // path they intended to view and redirect them after they
296 // login if it is requred.
297 var path = url.parse( req.url ).pathname;
298 req.session.redirect = path;
300 // Set req.user to an empty object so it doesn't throw errors
301 // later on that it isn't defined.
309 // loadSchool is used to load a school by it's id
310 function loadSchool( req, res, next ) {
312 var schoolId = req.params.id;
314 School.findById( schoolId, function( err, school ) {
318 // If a school is found, the user is checked to see if they are
319 // authorized to see or interact with anything related to that
321 school.authorize( user, function( authorized ){
322 req.school.authorized = authorized;
326 // If no school is found, display an appropriate error.
327 req.flash( 'error', 'Invalid school specified!' );
334 // loadSchool is used to load a course by it's id
335 function loadCourse( req, res, next ) {
337 var courseId = req.params.id;
339 Course.findById( courseId, function( err, course ) {
340 if( course && !course.deleted ) {
343 // If a course is found, the user is checked to see if they are
344 // authorized to see or interact with anything related to that
346 course.authorize( user, function( authorized ) {
347 req.course.authorized = authorized;
352 // If no course is found, display an appropriate error.
353 req.flash( 'error', 'Invalid course specified!' );
360 // loadLecture is used to load a lecture by it's id
361 function loadLecture( req, res, next ) {
363 var lectureId = req.params.id;
365 Lecture.findById( lectureId, function( err, lecture ) {
366 if( lecture && !lecture.deleted ) {
367 req.lecture = lecture;
369 // If a lecture is found, the user is checked to see if they are
370 // authorized to see or interact with anything related to that
372 lecture.authorize( user, function( authorized ) {
373 req.lecture.authorized = authorized;
378 // If no lecture is found, display an appropriate error.
379 req.flash( 'error', 'Invalid lecture specified!' );
386 // loadNote is used to load a note by it's id
387 // This is a lot more complicated than the above
388 // due to public/private handling of notes.
389 function loadNote( req, res, next ) {
390 var user = req.user ? req.user : false;
391 var noteId = req.params.id;
393 Note.findById( noteId, function( err, note ) {
394 // If a note is found, and user is set, check if
395 // user is authorized to interact with that note.
396 if( note && user && !note.deleted ) {
397 note.authorize( user, function( auth ) {
399 // If authorzied, then set req.note to be used later
403 } else if ( note.public ) {
404 // If not authorized, but the note is public, then
405 // designate the note read only (RO) and store req.note
411 // If the user is not authorized and the note is private
412 // then display and error.
413 req.flash( 'error', 'You do not have permission to access that note.' );
418 } else if ( note && note.public && !note.deleted ) {
419 // If note is found, but user is not set because they are not
420 // logged in, and the note is public, set the note to read only
421 // and store the note for later.
426 } else if ( note && !note.public && !note.deleted ) {
427 // If the note is found, but user is not logged in and the note is
428 // not public, then ask them to login to view the note. Once logged
429 // in they will be redirected to the note, at which time authorization
430 // handling will be put in effect above.
431 req.session.redirect = '/note/' + note._id;
432 req.flash( 'error', 'You must be logged in to view that note.' );
433 res.redirect( '/login' );
436 req.flash( 'error', 'Invalid note specified!' );
438 res.redirect( '/schools' );
443 function checkAjax( req, res, next ) {
451 // Dynamic Helpers are loaded automatically into views
452 app.dynamicHelpers( {
453 // express-messages is for flash messages for easy
454 // errors and information display
455 'messages' : require( 'express-messages' ),
457 // By default the req object isn't sen't to views
458 // during rendering, this allows you to use the
459 // user object if available in views.
460 'user' : function( req, res ) {
464 // Same, this allows session to be available in views.
465 'session' : function( req, res ) {
471 // The following are the main CRUD routes that are used
472 // to make up this web app.
476 app.get( '/', loadUser, function( req, res ) {
479 res.render( 'index' );
483 // Used to display all available schools and any courses
485 // Public with some private information
486 app.get( '/schools', checkAjax, loadUser, function( req, res ) {
490 // Find all schools and sort by name
491 // XXX mongoose's documentation on sort is extremely poor, tread carefully
492 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
494 // If schools are found, loop through them gathering any courses that are
495 // associated with them and then render the page with that information.
496 res.json({ 'schools' : schools.map(function(school) {
497 return school.sanitized;
500 // If no schools have been found, display none
501 //res.render( 'schools', { 'schools' : [] } );
502 res.json({ 'schools' : [] });
507 app.get( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
508 var school = req.school;
511 school.authorize( user, function( authorized ) {
512 // This is used to display interface elements for those users
513 // that are are allowed to see th)m, for instance a 'New Course' button.
514 var sanitizedSchool = school.sanitized;
515 sanitizedSchool.authorized = authorized;
516 // Find all courses for school by it's id and sort by name
517 Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
518 // If any courses are found, set them to the appropriate school, otherwise
520 if( courses.length > 0 ) {
521 sanitizedSchool.courses = courses.filter(function(course) {
522 if (!course.deleted) return course;
523 }).map(function(course) {
524 return course.sanitized;
527 sanitizedSchool.courses = [];
529 // This tells async (the module) that each iteration of forEach is
530 // done and will continue to call the rest until they have all been
531 // completed, at which time the last function below will be called.
532 res.json({ 'school': sanitizedSchool })
538 // Displays form to create new course
539 // Private, requires user to be authorized
540 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
541 // Load school from middleware
542 var school = req.school;
544 // If school was not loaded for whatever reason, or the user is not authorized
545 // then redirect to the main schools page.
546 if( ( ! school ) || ( ! school.authorized ) ) {
547 return res.redirect( '/schools' );
550 // If they are authorized and the school exists, then render the page
551 res.render( 'course/new', { 'school': school } );
554 // Recieves new course form
555 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
556 var school = req.school;
557 // Creates new course from Course Schema
558 var course = new Course;
559 // Gathers instructor information from form
560 var instructorEmail = req.body.email.toLowerCase();
561 var instructorName = req.body.instructorName;
563 // If school doesn't exist or user is not authorized redirect to main schools page
564 if( ( ! school ) || ( ! school.authorized ) ) {
565 res.redirect( '/schools' );
568 // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
569 if ( !instructorEmail || !instructorName ) {
570 req.flash( 'error', 'Invalid parameters!' )
571 return res.render( 'course/new' );
574 // Fill out the course with information from the form
575 course.number = req.body.number;
576 course.name = req.body.name;
577 course.description = req.body.description;
578 course.school = school._id;
579 course.creator = req.user._id;
580 course.subject = req.body.subject;
581 course.department = req.body.department;
583 // Check if a user exists with the instructorEmail, if not then create
584 // a new user and send them an instructor welcome email.
585 User.findOne( { 'email' : instructorEmail }, function( err, user ) {
589 user.name = instructorName
590 user.email = instructorEmail;
591 user.affil = 'Instructor';
592 user.school = school.name;
594 user.activated = false;
596 // Validate instructorEmail
597 // XXX Probably could be done before checking db
598 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
599 req.flash( 'error', 'Please enter a valid email' );
600 // XXX This needs to be fixed, this is not the proper flow
601 return res.redirect( '/register' );
603 // Once the new user information has been completed, save the user
604 // to the database then email them the instructor welcome email.
605 user.save(function( err ) {
606 // If there was an error saving the instructor, prompt the user to fill out
607 // the information again.
609 req.flash( 'error', 'Invalid parameters!' )
610 return res.render( 'course/new' );
615 'subject' : 'A non-profit open education initiative',
617 'template' : 'instructorInvite',
622 'serverHost' : serverHost
626 mailer.send( message, function( err, result ) {
628 console.log( 'Error inviting instructor to course!' );
630 console.log( 'Successfully invited instructor to course.' );
634 // After emails are sent, set the courses instructor to the
635 // new users id and then save the course to the database.
636 course.instructor = user._id;
637 course.save( function( err ) {
639 // XXX better validation
640 req.flash( 'error', 'Invalid parameters!' );
642 return res.render( 'course/new' );
644 // Once the course has been completed email the admin with information
645 // on the course and new instructor
649 'subject' : school.name+' has a new course: '+course.name,
651 'template' : 'newCourse',
656 'serverHost' : serverHost
660 mailer.send( message, function( err, result ) {
662 console.log( 'Error sending new course email to info@finalsclub.org' )
664 console.log( 'Successfully invited instructor to course')
667 // Redirect the user to the schools page where they can see
669 // XXX Redirect to the new course instead
670 res.redirect( '/schools' );
676 // If the user exists, then check if they are already and instructor
677 if (user.affil === 'Instructor') {
678 // If they are an instructor, then save the course with the appropriate
679 // information and email the admin.
680 course.instructor = user._id;
681 course.save( function( err ) {
683 // XXX better validation
684 req.flash( 'error', 'Invalid parameters!' );
686 return res.render( 'course/new' );
691 'subject' : school.name+' has a new course: '+course.name,
693 'template' : 'newCourse',
698 'serverHost' : serverHost
702 mailer.send( message, function( err, result ) {
704 console.log( 'Error sending new course email to info@finalsclub.org' )
706 console.log( 'Successfully invited instructor to course')
709 // XXX Redirect to the new course instead
710 res.redirect( '/schools' );
714 // The existing user isn't an instructor, so the user is notified of the error
715 // and the course isn't created.
716 req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
717 res.render( 'course/new' );
723 // Individual Course Listing
724 // Public with private information
725 app.get( '/course/:id', loadUser, loadCourse, function( req, res ) {
726 var userId = req.user._id;
727 var course = req.course;
729 // Check if the user is subscribed to the course
730 // XXX Not currently used for anything
731 var subscribed = course.subscribed( userId );
733 // Find lectures associated with this course and sort by name
734 Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
735 // Get course instructor information using their id
736 User.findById( course.instructor, function( err, instructor ) {
737 // Render course and lectures
738 res.render( 'course/index', { 'course' : course, 'instructor': instructor, 'subscribed' : subscribed, 'lectures' : lectures } );
744 app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
745 var course = req.course;
749 res.render( 'course/new', {course: course} )
751 req.flash( 'error', 'You don\'t have permission to do that' )
752 res.redirect( '/schools' );
756 // Recieve Course Edit Form
757 app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
758 var course = req.course;
762 var courseChanges = req.body;
763 course.number = courseChanges.number;
764 course.name = courseChanges.name;
765 course.description = courseChanges.description;
766 course.department = courseChanges.department;
768 course.save(function(err) {
770 req.flash( 'error', 'There was an error saving the course' );
772 res.redirect( '/course/'+ course._id.toString());
775 req.flash( 'error', 'You don\'t have permission to do that' )
776 res.redirect( '/schools' );
781 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
782 var course = req.course;
786 course.delete(function( err ) {
787 if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
788 else req.flash( 'info', 'Successfully removed course' )
789 res.redirect( '/schools' );
792 req.flash( 'error', 'You don\'t have permission to do that' )
793 res.redirect( '/schools' );
797 // Subscribe to course
798 // XXX Not currently used for anything
799 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
800 var course = req.course;
801 var userId = req.user._id;
803 course.subscribe( userId, function( err ) {
805 req.flash( 'error', 'Error subscribing to course!' );
808 res.redirect( '/course/' + course._id );
812 // Unsubscribe from course
813 // XXX Not currently used for anything
814 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
815 var course = req.course;
816 var userId = req.user._id;
818 course.unsubscribe( userId, function( err ) {
820 req.flash( 'error', 'Error unsubscribing from course!' );
823 res.redirect( '/course/' + course._id );
827 // Create new lecture
828 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
829 var courseId = req.params.id;
830 var course = req.course;
833 // If course isn't valid or user isn't authorized for course, redirect
834 if( ( ! course ) || ( ! course.authorized ) ) {
835 return res.redirect( '/course/' + courseId );
838 // Render new lecture form
839 res.render( 'lecture/new', { 'lecture' : lecture } );
842 // Recieve New Lecture Form
843 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
844 var courseId = req.params.id;
845 var course = req.course;
846 // Create new lecture from Lecture schema
847 var lecture = new Lecture;
849 if( ( ! course ) || ( ! course.authorized ) ) {
850 res.redirect( '/course/' + courseId );
855 // Populate lecture with form data
856 lecture.name = req.body.name;
857 lecture.date = req.body.date;
858 lecture.course = course._id;
859 lecture.creator = req.user._id;
861 // Save lecture to database
862 lecture.save( function( err ) {
864 // XXX better validation
865 req.flash( 'error', 'Invalid parameters!' );
867 res.render( 'lecture/new', { 'lecture' : lecture } );
869 // XXX Redirect to new lecture instead
870 res.redirect( '/course/' + course._id );
876 // Display individual lecture and related notes
877 app.get( '/lecture/:id', loadUser, loadLecture, function( req, res ) {
878 var lecture = req.lecture;
880 // Grab the associated course
881 // XXX this should be done with DBRefs eventually
882 Course.findById( lecture.course, function( err, course ) {
884 // If course is found, find instructor information to be displayed on page
885 User.findById( course.instructor, function( err, instructor ) {
886 // Pull out our notes
887 Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
888 if ( !req.user.loggedIn || !req.lecture.authorized ) {
889 // Loop through notes and only return those that are public if the
890 // user is not logged in or not authorized for that lecture
891 notes = notes.filter(function( note ) {
892 if ( note.public ) return note;
895 // Render lecture and notes
896 res.render( 'lecture/index', {
899 'instructor' : instructor,
903 'javascripts' : [ 'counts.js' ]
908 // XXX with DBRefs we will be able to reassign orphaned courses/lecture/pads
910 req.flash( 'error', 'That lecture is orphaned!' );
917 // Display new note form
918 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
919 var lectureId = req.params.id;
920 var lecture = req.lecture;
923 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
924 res.redirect( '/lecture/' + lectureId );
929 res.render( 'notes/new', { 'note' : note } );
932 // Recieve new note form
933 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
934 var lectureId = req.params.id;
935 var lecture = req.lecture;
937 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
938 res.redirect( '/lecture/' + lectureId );
943 // Create note from Note schema
946 // Populate note from form data
947 note.name = req.body.name;
948 note.date = req.body.date;
949 note.lecture = lecture._id;
950 note.public = req.body.private ? false : true;
951 note.creator = req.user._id;
953 // Save note to database
954 note.save( function( err ) {
956 // XXX better validation
957 req.flash( 'error', 'Invalid parameters!' );
959 res.render( 'notes/new', { 'note' : note } );
961 // XXX Redirect to new note instead
962 res.redirect( '/lecture/' + lecture._id );
968 // Display individual note page
969 app.get( '/note/:id', loadUser, loadNote, function( req, res ) {
971 // Set read only id for etherpad-lite or false for later check
972 var roID = note.roID || false;
974 var lectureId = note.lecture;
976 // Count the amount of visits, but only once per session
977 if ( req.session.visited ) {
978 if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
979 req.session.visited.push( note._id );
983 req.session.visited = [];
984 req.session.visited.push( note._id );
988 // If a read only id exists process note
992 // If read only id doesn't, then fetch the read only id from the database and then
994 // XXX Soon to be depracated due to a new API in etherpad that makes for a
995 // much cleaner solution.
996 db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
997 epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
999 roID = record.value.replace(/"/g, '');
1008 function processReq() {
1010 Lecture.findById( lectureId, function( err, lecture ) {
1012 req.flash( 'error', 'That notes page is orphaned!' );
1014 res.redirect( '/' );
1016 // Find notes based on lecture id, which will be displayed in a dropdown
1018 Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1020 // User is logged in and sees full notepad
1022 res.render( 'notes/index', {
1023 'layout' : 'noteLayout',
1024 'host' : serverHost,
1026 'lecture' : lecture,
1027 'otherNotes' : otherNotes,
1030 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1031 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1034 // User is not logged in and sees notepad that is public
1035 res.render( 'notes/public', {
1036 'layout' : 'noteLayout',
1037 'host' : serverHost,
1039 'otherNotes' : otherNotes,
1041 'lecture' : lecture,
1042 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1043 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1051 // Static pages and redirects
1052 app.get( '/about', loadUser, function( req, res ) {
1053 res.redirect( 'http://blog.finalsclub.org/about.html' );
1056 app.get( '/press', loadUser, function( req, res ) {
1057 res.render( 'static/press' );
1060 app.get( '/conduct', loadUser, function( req, res ) {
1061 res.render( 'static/conduct' );
1064 app.get( '/legal', loadUser, function( req, res ) {
1065 res.redirect( 'http://blog.finalsclub.org/legal.html' );
1068 app.get( '/contact', loadUser, function( req, res ) {
1069 res.redirect( 'http://blog.finalsclub.org/contact.html' );
1072 app.get( '/privacy', loadUser, function( req, res ) {
1073 res.render( 'static/privacy' );
1077 // Authentication routes
1078 // These are used for logging in, logging out, registering
1079 // and other user authentication purposes
1081 // Render login page
1082 app.get( '/login', function( req, res ) {
1083 log3("get login page")
1085 res.render( 'login' );
1088 // Recieve login form
1089 app.post( '/login', function( req, res ) {
1090 var email = req.body.email;
1091 var password = req.body.password;
1092 log3("post login ...")
1094 // Find user from email
1095 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1099 // If user exists, check if activated, if not notify them and send them to
1102 if( ! user.activated ) {
1103 // (undocumented) markdown-esque link functionality in req.flash
1104 req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1106 req.session.activateCode = user._id;
1108 res.render( 'login' );
1110 // If user is activated, check if their password is correct
1111 if( user.authenticate( password ) ) {
1114 var sid = req.sessionID;
1118 // Set the session then save the user to the database
1119 user.save( function() {
1120 var redirect = req.session.redirect;
1122 // login complete, remember the user's email for next time
1123 req.session.email = email;
1125 // alert the successful login
1126 req.flash( 'info', 'Successfully logged in!' );
1128 // redirect to profile if we don't have a stashed request
1129 res.redirect( redirect || '/profile' );
1132 // Notify user of bad login
1133 req.flash( 'error', 'Invalid login!' );
1135 res.render( 'login' );
1139 // Notify user of bad login
1141 req.flash( 'error', 'Invalid login!' );
1143 res.render( 'login' );
1148 // Request reset password
1149 app.get( '/resetpw', function( req, res ) {
1150 log3("get resetpw page");
1151 res.render( 'resetpw' );
1154 // Display reset password from requested email
1155 app.get( '/resetpw/:id', function( req, res ) {
1156 var resetPassCode = req.params.id
1157 res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1160 // Recieve reset password request form
1161 app.post( '/resetpw', function( req, res ) {
1162 log3("post resetpw");
1163 var email = req.body.email
1167 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1170 // If user exists, create reset code
1171 var resetPassCode = hat(64);
1172 user.setResetPassCode(resetPassCode);
1174 // Construct url that the user can then click to reset password
1175 var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1177 // Save user to database
1178 user.save( function( err ) {
1179 log3('save '+user.email);
1181 // Construct email and send it to the user
1185 'subject' : 'Your FinalsClub.org Password has been Reset!',
1187 'template' : 'userPasswordReset',
1189 'resetPassCode' : resetPassCode,
1190 'resetPassUrl' : resetPassUrl
1194 mailer.send( message, function( err, result ) {
1196 // XXX: Add route to resend this email
1198 console.log( 'Error sending user password reset email!' );
1200 console.log( 'Successfully sent user password reset email.' );
1205 // Render request success page
1206 res.render( 'resetpw-success', { 'email' : email } );
1210 res.render( 'resetpw-error', { 'email' : email } );
1215 // Recieve reset password form
1216 app.post( '/resetpw/:id', function( req, res ) {
1217 log3("post resetpw.code");
1218 var resetPassCode = req.params.id
1219 var email = req.body.email
1220 var pass1 = req.body.pass1
1221 var pass2 = req.body.pass2
1223 // Find user by email
1224 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1226 // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1227 // save user with new password and display success message.
1229 var valid = user.resetPassword(resetPassCode, pass1, pass2);
1231 user.save( function( err ) {
1232 res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );
1237 // If there was a problem, notify user
1239 res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1244 // Display registration page
1245 app.get( '/register', function( req, res ) {
1246 log3("get reg page");
1248 // Populate school dropdown list
1249 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1250 res.render( 'register', { 'schools' : schools } );
1254 // Recieve registration form
1255 app.post( '/register', function( req, res ) {
1256 var sid = req.sessionId;
1258 // Create new user from User schema
1259 var user = new User;
1261 // Populate user from form
1262 user.email = req.body.email.toLowerCase();
1263 user.password = req.body.password;
1265 // If school is set to other, then fill in school as what the
1267 user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1268 user.name = req.body.name;
1269 user.affil = req.body.affil;
1270 user.activated = false;
1273 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1274 req.flash( 'error', 'Please enter a valid email' );
1275 return res.redirect( '/register' );
1278 // Check if password is greater than 6 characters, otherwise notify user
1279 if ( req.body.password.length < 6 ) {
1280 req.flash( 'error', 'Please enter a password longer than eight characters' );
1281 return res.redirect( '/register' );
1284 // Pull out hostname from email
1285 var hostname = user.email.split( '@' ).pop();
1287 // Check if email is from one of the special domains
1288 if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1292 // Save user to database
1293 user.save( function( err ) {
1294 // If error, check if it is because the user already exists, if so
1295 // get the user information and let them know
1297 if( /dup key/.test( err.message ) ) {
1298 // attempting to register an existing address
1299 User.findOne({ 'email' : user.email }, function(err, result ) {
1300 if (result.activated) {
1301 // If activated, make sure they know how to contact the admin
1302 req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1303 return res.redirect( '/register' )
1305 // If not activated, direct them to the resendActivation page
1306 req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1307 return res.redirect( '/resendActivation' )
1311 // If any other type of error, prompt them to enter the registration again
1312 req.flash( 'error', 'An error occurred during registration.' );
1314 return res.redirect( '/register' );
1317 // send user activation email
1318 sendUserActivation( user );
1320 // Check if the hostname matches any in the approved schools
1321 School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1323 // If there is a match, send associated welcome message
1324 sendUserWelcome( user, true );
1325 log3('school recognized '+school.name);
1326 // If no users exist for the school, create empty array
1327 if (!school.users) school.users = [];
1328 // Add user to the school
1329 school.users.push( user._id );
1331 // Save school to the database
1332 school.save( function( err ) {
1333 log3('school.save() done');
1334 // Notify user that they have been added to the school
1335 req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1336 res.redirect( '/' );
1338 // Construct admin email about user registration
1342 'subject' : 'FC User Registration : User added to ' + school.name,
1344 'template' : 'userSchool',
1350 // If there isn't a match, send associated welcome message
1351 sendUserWelcome( user, false );
1352 // Tell user to check for activation link
1353 req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1354 res.redirect( '/' );
1355 // Construct admin email about user registration
1359 'subject' : 'FC User Registration : Email did not match any schools',
1361 'template' : 'userNoSchool',
1367 // Send email to admin
1368 mailer.send( message, function( err, result ) {
1371 console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1373 console.log( 'Successfully sent user has no school email to admin.' );
1383 // Display resendActivation request page
1384 app.get( '/resendActivation', function( req, res ) {
1385 var activateCode = req.session.activateCode;
1387 // Check if user exists by activateCode set in their session
1388 User.findById( activateCode, function( err, user ) {
1389 if( ( ! user ) || ( user.activated ) ) {
1390 res.redirect( '/' );
1392 // Send activation and redirect to login
1393 sendUserActivation( user );
1395 req.flash( 'info', 'Your activation code has been resent.' );
1397 res.redirect( '/login' );
1402 // Display activation page
1403 app.get( '/activate/:code', function( req, res ) {
1404 var code = req.params.code;
1406 // XXX could break this out into a middleware
1408 res.redirect( '/' );
1411 // Find user by activation code
1412 User.findById( code, function( err, user ) {
1413 if( err || ! user ) {
1414 // If not found, notify user of invalid code
1415 req.flash( 'error', 'Invalid activation code!' );
1417 res.redirect( '/' );
1419 // If valid, then activate user
1420 user.activated = true;
1422 // Regenerate our session and log in as the new user
1423 req.session.regenerate( function() {
1424 user.session = req.sessionID;
1426 // Save user to database
1427 user.save( function( err ) {
1429 req.flash( 'error', 'Unable to activate account.' );
1431 res.redirect( '/' );
1433 req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1435 res.redirect( '/profile' );
1444 app.get( '/logout', function( req, res ) {
1445 var sid = req.sessionID;
1447 // Find user by session id
1448 User.findOne( { 'session' : sid }, function( err, user ) {
1450 // Empty out session id
1453 // Save user to database
1454 user.save( function( err ) {
1455 res.redirect( '/' );
1458 res.redirect( '/' );
1463 // Display users profile page
1464 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1465 var user = req.user;
1467 res.render( 'profile/index', { 'user' : user } );
1470 // Recieve profile edit page form
1471 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1472 var user = req.user;
1473 var fields = req.body;
1476 var wasComplete = user.isComplete;
1478 if( ! fields.name ) {
1479 req.flash( 'error', 'Please enter a valid name!' );
1483 user.name = fields.name;
1486 if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1487 req.flash( 'error', 'Please select a valid affiliation!' );
1491 user.affil = fields.affiliation;
1494 if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1495 // changing password
1496 if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1497 if( fields.newPassword === fields.newPasswordConfirm ) {
1498 // test password strength?
1500 user.password = fields.newPassword;
1502 req.flash( 'error', 'Mismatch in new password!' );
1507 req.flash( 'error', 'Please supply your existing password.' );
1513 user.major = fields.major;
1514 user.bio = fields.bio;
1516 user.showName = ( fields.showName ? true : false );
1519 user.save( function( err ) {
1521 req.flash( 'error', 'Unable to save user profile!' );
1523 if( ( user.isComplete ) && ( ! wasComplete ) ) {
1524 req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1526 res.redirect( '/' );
1528 res.render( 'info', 'Your profile was successfully updated!' );
1530 res.render( 'profile/index', { 'user' : user } );
1535 res.render( 'profile/index', { 'user' : user } );
1542 function loadSubject( req, res, next ) {
1543 if( url.parse( req.url ).pathname.match(/subject/) ) {
1544 ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1546 req.flash( 'error', 'Subject with this ID does not exist' )
1547 res.redirect( '/archive' );
1549 req.subject = subject;
1558 function loadOldCourse( req, res, next ) {
1559 if( url.parse( req.url ).pathname.match(/course/) ) {
1560 ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1562 req.flash( 'error', 'Course with this ID does not exist' )
1563 res.redirect( '/archive' );
1565 req.course = course;
1574 var featuredCourses = [
1575 {name: 'The Human Mind', 'id': 1563},
1576 {name: 'Justice', 'id': 797},
1577 {name: 'Protest Literature', 'id': 1681},
1578 {name: 'Animal Cognition', 'id': 681},
1579 {name: 'Positive Psychology', 'id': 1793},
1580 {name: 'Social Psychology', 'id': 660},
1581 {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1582 {name: 'Cyberspace in Court', 'id': 1446},
1583 {name: 'Nazi Cinema', 'id': 2586},
1584 {name: 'Media and the American Mind', 'id': 2583},
1585 {name: 'Social Thought in Modern America', 'id': 2585},
1586 {name: 'Major British Writers II', 'id': 869},
1587 {name: 'Civil Procedure', 'id': 2589},
1588 {name: 'Evidence', 'id': 2590},
1589 {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1592 app.get( '/learn', loadUser, function( req, res ) {
1593 res.render( 'archive/learn', { 'courses' : featuredCourses } );
1596 app.get( '/learn/random', loadUser, function( req, res ) {
1597 res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1600 app.get( '/archive', loadUser, function( req, res ) {
1601 ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1603 req.flash( 'error', 'There was a problem gathering the archived courses, please try again later.' );
1604 res.redirect( '/' );
1606 res.render( 'archive/index', { 'subjects' : subjects } );
1611 app.get( '/archive/subject/:id', loadUser, loadSubject, function( req, res ) {
1612 ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1614 req.flash( 'error', 'There are no archived courses' );
1615 res.redirect( '/' );
1617 res.render( 'archive/courses', { 'courses' : courses, 'subject': req.subject } );
1622 app.get( '/archive/course/:id', loadUser, loadOldCourse, function( req, res ) {
1623 ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1625 req.flash( 'error', 'There are no notes in this course' );
1626 res.redirect( '/archive' );
1628 res.render( 'archive/notes', { 'notes' : notes, 'course' : req.course } );
1633 app.get( '/archive/note/:id', loadUser, function( req, res ) {
1634 ArchivedNote.findById(req.params.id, function(err, note) {
1636 req.flash( 'error', 'This is not a valid id for a note' );
1637 res.redirect( '/archive' );
1639 ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1641 req.flash( 'error', 'There is no course for this note' )
1642 res.redirect( '/archive' )
1644 res.render( 'archive/note', { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
1653 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1654 // the browser which facilitates near realtime interaction. This allows the user to post questions
1655 // and comments and other users to get those almost immediately after they are posted, without
1656 // reloading the page or pressing a button to refresh.
1658 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1659 // saving the data to the database, and then sending it out to everyone else connected.
1662 // Posts - Posts are the main items in backchannel, useful for questions or discussion points
1663 // [[ example object needed with explanation E.G:
1665 Post: { postID: '999-1',
1667 userName: 'Bob Jones',
1668 userAffil: 'Instructor',
1669 body: 'This is the text content of the post.',
1670 comments: { {<commentObj>, <commentObj>, ...},
1672 votes: [ <userID>, <userID>, ...],
1673 reports: [ <userID>, <userID>, ...]
1675 Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
1677 if anonymous: userName => 'Anonymous', userAffil => 'N/A'
1682 // Comments - Comments are replies to posts, for clarification or answering questions
1683 // [[ example object needed]]
1684 // Votes - Votes signifyg a users approval of a post
1685 // [[ example object needed]]
1686 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1687 // [[ example object needed]]
1691 // body - Main content of the post
1692 // userId - Not currently used, but would contain the users id that made the post
1693 // userName - Users name that made post
1694 // userAffil - Users affiliation to their school
1695 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1696 // date - Date post was made, updates when any comments are made for the post
1697 // comments - An array of comments which contain a body, userName, and userAffil
1698 // votes - An array of user ids which are the users that voted
1699 // [[ example needed ]]
1700 // reports - An array of user ids which are the users that reported the post
1701 // [[ reports would be "this post is flagged as inappropriate"? ]]
1702 // [[ bruml: consistent terminology needed ]]
1704 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1705 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1706 // users the ability to make posts or comments that they might not otherwise due to not wanting
1707 // the content of the post/comment to be attributed to them.
1709 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1710 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1711 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1712 // user is logged in, and if so display information that otherwise might not be visible to them if they
1713 // aren't apart of a particular school.
1715 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1716 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1717 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1718 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1719 // rendered in the browser.
1721 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1722 // events are as follows.
1725 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1726 // The server recieves the data, assembles a new post object for the database and then fills it with
1727 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1728 // replaced. If the user chose for the post to be private, then public will be set to false and it
1729 // will be filtered from being sent to users not logged into and not having access to the school. Once
1730 // the post has been created and saved into the database, it is sent to all connected users to that
1731 // particular lecture, unless it is private, than only logged in users will get it.
1734 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1735 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1736 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1737 // users unless the post is private, which then it will be only sent to logged in users.
1740 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1741 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1742 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1743 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1744 // bottom of the interface.
1747 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1748 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1749 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1750 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1751 // to the top of the interface. The comment can be anonymous, which then will have the user
1752 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1753 // connected users unless the post is private, then only logged in users will recieve the comment.
1755 var io = require( 'socket.io' ).listen( app );
1757 var Post = mongoose.model( 'Post' );
1759 io.set('authorization', function ( handshake, next ) {
1760 var rawCookie = handshake.headers.cookie;
1762 handshake.cookie = parseCookie(rawCookie);
1763 handshake.sid = handshake.cookie['connect.sid'];
1765 if ( handshake.sid ) {
1766 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1768 handshake.user = false;
1769 return next(null, true);
1771 // bake a new session object for full r/w
1772 handshake.session = new Session( handshake, session );
1774 User.findOne( { session : handshake.sid }, function( err, user ) {
1776 handshake.user = user;
1777 return next(null, true);
1779 handshake.user = false;
1780 return next(null, true);
1788 return next(null, true);
1792 var backchannel = new Backchannel(app, io.of('/backchannel'), {
1793 subscribe: function(lecture, send) {
1794 Post.find({'lecture': lecture}, function(err, posts) {
1798 post: function(fillPost) {
1799 var post = new Post;
1800 fillPost(post, function(send) {
1801 post.save(function(err) {
1806 items: function(postId, addItem) {
1807 Post.findById(postId, function( err, post ) {
1808 addItem(post, function(send) {
1809 post.save(function(err) {
1824 .on( 'connection', function( socket ) {
1825 // pull out user/session information etc.
1826 var handshake = socket.handshake;
1827 var userID = handshake.user._id;
1834 socket.on( 'join', function( note ) {
1835 if (handshake.user === false) {
1837 // XXX: replace by addToSet (once it's implemented in mongoose)
1838 Note.findById( noteID, function( err, note ) {
1840 if( note.collaborators.indexOf( userID ) == -1 ) {
1841 note.collaborators.push( userID );
1849 socket.on( 'watch', function( l ) {
1850 var sendCounts = function() {
1853 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1856 function( note, callback ) {
1858 var count = note.collaborators.length;
1864 socket.emit( 'counts', send );
1866 timer = setTimeout( sendCounts, 5000 );
1872 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1873 notes.forEach( function( note ) {
1874 watched.push( note._id );
1881 socket.on( 'disconnect', function() {
1882 clearTimeout( timer );
1884 if (handshake.user === false) {
1885 // XXX: replace with $pull once it's available
1887 Note.findById( noteID, function( err, note ) {
1889 var index = note.collaborators.indexOf( userID );
1892 note.collaborators.splice( index, 1 );
1903 // Exception Catch-All
1905 process.on('uncaughtException', function (e) {
1906 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1912 mongoose.connect( app.set( 'dbUri' ) );
1913 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1915 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1917 app.listen( serverPort, function() {
1918 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1920 // if run as root, downgrade to the owner of this file
1921 if (process.getuid() === 0) {
1922 require('fs').stat(__filename, function(err, stats) {
1923 if (err) { return console.log(err); }
1924 process.setuid(stats.uid);
1929 function isValidEmail(email) {
1930 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])?/;
1931 return email.match(re);