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.
312 // loadSchool is used to load a school by it's id
313 function loadSchool( req, res, next ) {
315 var schoolId = req.params.id;
317 School.findById( schoolId, function( err, school ) {
321 // If a school is found, the user is checked to see if they are
322 // authorized to see or interact with anything related to that
324 school.authorize( user, function( authorized ){
325 req.school.authorized = authorized;
329 // If no school is found, display an appropriate error.
330 sendJson(res, {status: 'error', message: 'Invalid school specified!'} );
335 // loadSchool is used to load a course by it's id
336 function loadCourse( req, res, next ) {
338 var courseId = req.params.id;
340 Course.findById( courseId, function( err, course ) {
341 if( course && !course.deleted ) {
344 // If a course is found, the user is checked to see if they are
345 // authorized to see or interact with anything related to that
347 course.authorize( user, function( authorized ) {
348 req.course.authorized = authorized;
353 // If no course is found, display an appropriate error.
354 sendJson(res, {status: 'error', message: 'Invalid course specified!'} );
359 // loadLecture is used to load a lecture by it's id
360 function loadLecture( req, res, next ) {
362 var lectureId = req.params.id;
364 Lecture.findById( lectureId, function( err, lecture ) {
365 if( lecture && !lecture.deleted ) {
366 req.lecture = lecture;
368 // If a lecture is found, the user is checked to see if they are
369 // authorized to see or interact with anything related to that
371 lecture.authorize( user, function( authorized ) {
372 req.lecture.authorized = authorized;
377 // If no lecture is found, display an appropriate error.
378 sendJson(res, {status: 'error', message: 'Invalid lecture specified!'} );
383 // loadNote is used to load a note by it's id
384 // This is a lot more complicated than the above
385 // due to public/private handling of notes.
386 function loadNote( req, res, next ) {
387 var user = req.user ? req.user : false;
388 var noteId = req.params.id;
390 Note.findById( noteId, function( err, note ) {
391 // If a note is found, and user is set, check if
392 // user is authorized to interact with that note.
393 if( note && user && !note.deleted ) {
394 note.authorize( user, function( auth ) {
396 // If authorzied, then set req.note to be used later
400 } else if ( note.public ) {
401 // If not authorized, but the note is public, then
402 // designate the note read only (RO) and store req.note
408 // If the user is not authorized and the note is private
409 // then display and error.
410 sendJson(res, {status: 'error', message: 'You do not have permission to access that note.'} );
413 } else if ( note && note.public && !note.deleted ) {
414 // If note is found, but user is not set because they are not
415 // logged in, and the note is public, set the note to read only
416 // and store the note for later.
421 } else if ( note && !note.public && !note.deleted ) {
422 // If the note is found, but user is not logged in and the note is
423 // not public, then ask them to login to view the note. Once logged
424 // in they will be redirected to the note, at which time authorization
425 // handling will be put in effect above.
426 //req.session.redirect = '/note/' + note._id;
427 sendJson(res, {status: 'error', message: 'You must be logged in to view that note.'} );
430 sendJson(res, {status: 'error', message: 'Invalid note specified!'} );
435 function checkAjax( req, res, next ) {
439 res.sendfile( 'public/index.html' );
443 // Dynamic Helpers are loaded automatically into views
444 app.dynamicHelpers( {
445 // express-messages is for flash messages for easy
446 // errors and information display
447 'messages' : require( 'express-messages' ),
449 // By default the req object isn't sen't to views
450 // during rendering, this allows you to use the
451 // user object if available in views.
452 'user' : function( req, res ) {
456 // Same, this allows session to be available in views.
457 'session' : function( req, res ) {
462 function sendJson( res, obj ) {
463 res.header('Cache-Control', 'no-cache');
468 // The following are the main CRUD routes that are used
469 // to make up this web app.
474 app.get( '/', loadUser, function( req, res ) {
477 res.render( 'index' );
482 // Used to display all available schools and any courses
484 // Public with some private information
485 app.get( '/schools', checkAjax, loadUser, function( req, res ) {
489 // Find all schools and sort by name
490 // XXX mongoose's documentation on sort is extremely poor, tread carefully
491 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
493 // If schools are found, loop through them gathering any courses that are
494 // associated with them and then render the page with that information.
495 sendJson(res, { 'user': user.sanitized, 'schools' : schools.map(function(school) {
496 return school.sanitized;
499 // If no schools have been found, display none
500 //res.render( 'schools', { 'schools' : [] } );
501 sendJson(res, { 'schools' : [] , 'user': user.sanitized });
506 app.get( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
507 var school = req.school;
510 school.authorize( user, function( authorized ) {
511 // This is used to display interface elements for those users
512 // that are are allowed to see th)m, for instance a 'New Course' button.
513 var sanitizedSchool = school.sanitized;
514 sanitizedSchool.authorized = authorized;
515 // Find all courses for school by it's id and sort by name
516 Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
517 // If any courses are found, set them to the appropriate school, otherwise
519 if( courses.length > 0 ) {
520 sanitizedSchool.courses = courses.filter(function(course) {
521 if (!course.deleted) return course;
522 }).map(function(course) {
523 return course.sanitized;
526 sanitizedSchool.courses = [];
528 // This tells async (the module) that each iteration of forEach is
529 // done and will continue to call the rest until they have all been
530 // completed, at which time the last function below will be called.
531 sendJson(res, { 'school': sanitizedSchool, 'user': user.sanitized })
537 // Displays form to create new course
538 // Private, requires user to be authorized
539 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
540 // Load school from middleware
541 var school = req.school;
543 // If school was not loaded for whatever reason, or the user is not authorized
544 // then redirect to the main schools page.
545 if( ( ! school ) || ( ! school.authorized ) ) {
546 return res.redirect( '/schools' );
549 // If they are authorized and the school exists, then render the page
550 res.render( 'course/new', { 'school': school } );
553 // Recieves new course form
554 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
555 var school = req.school;
556 // Creates new course from Course Schema
557 var course = new Course;
558 // Gathers instructor information from form
559 var instructorEmail = req.body.email.toLowerCase();
560 var instructorName = req.body.instructorName;
562 // If school doesn't exist or user is not authorized redirect to main schools page
563 if( ( ! school ) || ( ! school.authorized ) ) {
564 res.redirect( '/schools' );
567 // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
568 if ( !instructorEmail || !instructorName ) {
569 req.flash( 'error', 'Invalid parameters!' )
570 return res.render( 'course/new' );
573 // Fill out the course with information from the form
574 course.number = req.body.number;
575 course.name = req.body.name;
576 course.description = req.body.description;
577 course.school = school._id;
578 course.creator = req.user._id;
579 course.subject = req.body.subject;
580 course.department = req.body.department;
582 // Check if a user exists with the instructorEmail, if not then create
583 // a new user and send them an instructor welcome email.
584 User.findOne( { 'email' : instructorEmail }, function( err, user ) {
588 user.name = instructorName
589 user.email = instructorEmail;
590 user.affil = 'Instructor';
591 user.school = school.name;
593 user.activated = false;
595 // Validate instructorEmail
596 // XXX Probably could be done before checking db
597 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
598 req.flash( 'error', 'Please enter a valid email' );
599 // XXX This needs to be fixed, this is not the proper flow
600 return res.redirect( '/register' );
602 // Once the new user information has been completed, save the user
603 // to the database then email them the instructor welcome email.
604 user.save(function( err ) {
605 // If there was an error saving the instructor, prompt the user to fill out
606 // the information again.
608 req.flash( 'error', 'Invalid parameters!' )
609 return res.render( 'course/new' );
614 'subject' : 'A non-profit open education initiative',
616 'template' : 'instructorInvite',
621 'serverHost' : serverHost
625 mailer.send( message, function( err, result ) {
627 console.log( 'Error inviting instructor to course!' );
629 console.log( 'Successfully invited instructor to course.' );
633 // After emails are sent, set the courses instructor to the
634 // new users id and then save the course to the database.
635 course.instructor = user._id;
636 course.save( function( err ) {
638 // XXX better validation
639 req.flash( 'error', 'Invalid parameters!' );
641 return res.render( 'course/new' );
643 // Once the course has been completed email the admin with information
644 // on the course and new instructor
648 'subject' : school.name+' has a new course: '+course.name,
650 'template' : 'newCourse',
655 'serverHost' : serverHost
659 mailer.send( message, function( err, result ) {
661 console.log( 'Error sending new course email to info@finalsclub.org' )
663 console.log( 'Successfully invited instructor to course')
666 // Redirect the user to the schools page where they can see
668 // XXX Redirect to the new course instead
669 res.redirect( '/schools' );
675 // If the user exists, then check if they are already and instructor
676 if (user.affil === 'Instructor') {
677 // If they are an instructor, then save the course with the appropriate
678 // information and email the admin.
679 course.instructor = user._id;
680 course.save( function( err ) {
682 // XXX better validation
683 req.flash( 'error', 'Invalid parameters!' );
685 return res.render( 'course/new' );
690 'subject' : school.name+' has a new course: '+course.name,
692 'template' : 'newCourse',
697 'serverHost' : serverHost
701 mailer.send( message, function( err, result ) {
703 console.log( 'Error sending new course email to info@finalsclub.org' )
705 console.log( 'Successfully invited instructor to course')
708 // XXX Redirect to the new course instead
709 res.redirect( '/schools' );
713 // The existing user isn't an instructor, so the user is notified of the error
714 // and the course isn't created.
715 req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
716 res.render( 'course/new' );
722 // Individual Course Listing
723 // Public with private information
724 app.get( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
725 var userId = req.user._id;
726 var course = req.course;
728 // Check if the user is subscribed to the course
729 // XXX Not currently used for anything
730 var subscribed = course.subscribed( userId );
732 // Find lectures associated with this course and sort by name
733 Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
734 // Get course instructor information using their id
735 User.findById( course.instructor, function( err, instructor ) {
736 // Render course and lectures
737 sendJson(res, { 'user': req.user.sanitized, 'course' : course.sanitized, 'instructor': instructor.sanitized, 'subscribed' : subscribed, 'lectures' : lectures.map(function(lecture) { return lecture.sanitized })} );
743 app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
744 var course = req.course;
748 res.render( 'course/new', {course: course} )
750 req.flash( 'error', 'You don\'t have permission to do that' )
751 res.redirect( '/schools' );
755 // Recieve Course Edit Form
756 app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
757 var course = req.course;
761 var courseChanges = req.body;
762 course.number = courseChanges.number;
763 course.name = courseChanges.name;
764 course.description = courseChanges.description;
765 course.department = courseChanges.department;
767 course.save(function(err) {
769 req.flash( 'error', 'There was an error saving the course' );
771 res.redirect( '/course/'+ course._id.toString());
774 req.flash( 'error', 'You don\'t have permission to do that' )
775 res.redirect( '/schools' );
780 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
781 var course = req.course;
785 course.delete(function( err ) {
786 if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
787 else req.flash( 'info', 'Successfully removed course' )
788 res.redirect( '/schools' );
791 req.flash( 'error', 'You don\'t have permission to do that' )
792 res.redirect( '/schools' );
796 // Subscribe to course
797 // XXX Not currently used for anything
798 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
799 var course = req.course;
800 var userId = req.user._id;
802 course.subscribe( userId, function( err ) {
804 req.flash( 'error', 'Error subscribing to course!' );
807 res.redirect( '/course/' + course._id );
811 // Unsubscribe from course
812 // XXX Not currently used for anything
813 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
814 var course = req.course;
815 var userId = req.user._id;
817 course.unsubscribe( userId, function( err ) {
819 req.flash( 'error', 'Error unsubscribing from course!' );
822 res.redirect( '/course/' + course._id );
826 // Create new lecture
827 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
828 var courseId = req.params.id;
829 var course = req.course;
832 // If course isn't valid or user isn't authorized for course, redirect
833 if( ( ! course ) || ( ! course.authorized ) ) {
834 return res.redirect( '/course/' + courseId );
837 // Render new lecture form
838 res.render( 'lecture/new', { 'lecture' : lecture } );
841 // Recieve New Lecture Form
842 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
843 var courseId = req.params.id;
844 var course = req.course;
845 // Create new lecture from Lecture schema
846 var lecture = new Lecture;
848 if( ( ! course ) || ( ! course.authorized ) ) {
849 res.redirect( '/course/' + courseId );
854 // Populate lecture with form data
855 lecture.name = req.body.name;
856 lecture.date = req.body.date;
857 lecture.course = course._id;
858 lecture.creator = req.user._id;
860 // Save lecture to database
861 lecture.save( function( err ) {
863 // XXX better validation
864 req.flash( 'error', 'Invalid parameters!' );
866 res.render( 'lecture/new', { 'lecture' : lecture } );
868 // XXX Redirect to new lecture instead
869 res.redirect( '/course/' + course._id );
875 // Display individual lecture and related notes
876 app.get( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
877 var lecture = req.lecture;
879 // Grab the associated course
880 // XXX this should be done with DBRefs eventually
881 Course.findById( lecture.course, function( err, course ) {
883 // If course is found, find instructor information to be displayed on page
884 User.findById( course.instructor, function( err, instructor ) {
885 // Pull out our notes
886 Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
887 if ( !req.user.loggedIn || !req.lecture.authorized ) {
888 // Loop through notes and only return those that are public if the
889 // user is not logged in or not authorized for that lecture
890 notes = notes.filter(function( note ) {
891 if ( note.public ) return note;
895 'lecture' : lecture.sanitized,
896 'course' : course.sanitized,
897 'instructor' : instructor.sanitized,
898 'user' : req.user.sanitized,
899 'notes' : notes.map(function(note) {
900 return note.sanitized;
906 sendJson(res, { status: 'error', msg: 'This course is orphaned' })
911 // Display new note form
912 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
913 var lectureId = req.params.id;
914 var lecture = req.lecture;
917 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
918 res.redirect( '/lecture/' + lectureId );
923 res.render( 'notes/new', { 'note' : note } );
926 // Recieve new note form
927 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
928 var lectureId = req.params.id;
929 var lecture = req.lecture;
931 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
932 res.redirect( '/lecture/' + lectureId );
937 // Create note from Note schema
940 // Populate note from form data
941 note.name = req.body.name;
942 note.date = req.body.date;
943 note.lecture = lecture._id;
944 note.public = req.body.private ? false : true;
945 note.creator = req.user._id;
947 // Save note to database
948 note.save( function( err ) {
950 // XXX better validation
951 req.flash( 'error', 'Invalid parameters!' );
953 res.render( 'notes/new', { 'note' : note } );
955 // XXX Redirect to new note instead
956 res.redirect( '/lecture/' + lecture._id );
962 // Display individual note page
963 app.get( '/note/:id', /*checkAjax,*/ loadUser, loadNote, function( req, res ) {
965 // Set read only id for etherpad-lite or false for later check
966 var roID = note.roID || false;
968 var lectureId = note.lecture;
970 // Count the amount of visits, but only once per session
971 if ( req.session.visited ) {
972 if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
973 req.session.visited.push( note._id );
977 req.session.visited = [];
978 req.session.visited.push( note._id );
982 // If a read only id exists process note
986 // If read only id doesn't, then fetch the read only id from the database and then
988 // XXX Soon to be depracated due to a new API in etherpad that makes for a
989 // much cleaner solution.
990 db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
991 epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
993 roID = record.value.replace(/"/g, '');
1002 function processReq() {
1004 Lecture.findById( lectureId, function( err, lecture ) {
1006 req.flash( 'error', 'That notes page is orphaned!' );
1008 res.redirect( '/' );
1010 // Find notes based on lecture id, which will be displayed in a dropdown
1012 Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1015 'host' : serverHost,
1016 'note' : note.sanitized,
1017 'lecture' : lecture.sanitized,
1018 'otherNotes' : otherNotes.map(function(note) {
1019 return note.sanitized;
1026 // User is logged in and sees full notepad
1028 res.render( 'notes/index', {
1029 'layout' : 'noteLayout',
1030 'host' : serverHost,
1032 'lecture' : lecture,
1033 'otherNotes' : otherNotes,
1036 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1037 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1040 // User is not logged in and sees notepad that is public
1041 res.render( 'notes/public', {
1042 'layout' : 'noteLayout',
1043 'host' : serverHost,
1045 'otherNotes' : otherNotes,
1047 'lecture' : lecture,
1048 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1049 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1057 // Static pages and redirects
1058 app.get( '/about', loadUser, function( req, res ) {
1059 res.redirect( 'http://blog.finalsclub.org/about.html' );
1062 app.get( '/press', loadUser, function( req, res ) {
1063 res.render( 'static/press' );
1066 app.get( '/conduct', loadUser, function( req, res ) {
1067 res.render( 'static/conduct' );
1070 app.get( '/legal', loadUser, function( req, res ) {
1071 res.redirect( 'http://blog.finalsclub.org/legal.html' );
1074 app.get( '/contact', loadUser, function( req, res ) {
1075 res.redirect( 'http://blog.finalsclub.org/contact.html' );
1078 app.get( '/privacy', loadUser, function( req, res ) {
1079 res.render( 'static/privacy' );
1083 // Authentication routes
1084 // These are used for logging in, logging out, registering
1085 // and other user authentication purposes
1087 // Render login page
1088 app.get( '/login', function( req, res ) {
1089 log3("get login page")
1091 res.render( 'login' );
1094 // Recieve login form
1095 app.post( '/login', function( req, res ) {
1096 var email = req.body.email;
1097 var password = req.body.password;
1098 log3("post login ...")
1100 // Find user from email
1101 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1105 // If user exists, check if activated, if not notify them and send them to
1108 if( ! user.activated ) {
1109 // (undocumented) markdown-esque link functionality in req.flash
1110 req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1112 req.session.activateCode = user._id;
1114 res.render( 'login' );
1116 // If user is activated, check if their password is correct
1117 if( user.authenticate( password ) ) {
1120 var sid = req.sessionID;
1124 // Set the session then save the user to the database
1125 user.save( function() {
1126 var redirect = req.session.redirect;
1128 // login complete, remember the user's email for next time
1129 req.session.email = email;
1131 // alert the successful login
1132 req.flash( 'info', 'Successfully logged in!' );
1134 // redirect to profile if we don't have a stashed request
1135 res.redirect( redirect || '/profile' );
1138 // Notify user of bad login
1139 req.flash( 'error', 'Invalid login!' );
1141 res.render( 'login' );
1145 // Notify user of bad login
1147 req.flash( 'error', 'Invalid login!' );
1149 res.render( 'login' );
1154 // Request reset password
1155 app.get( '/resetpw', function( req, res ) {
1156 log3("get resetpw page");
1157 res.render( 'resetpw' );
1160 // Display reset password from requested email
1161 app.get( '/resetpw/:id', function( req, res ) {
1162 var resetPassCode = req.params.id
1163 res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1166 // Recieve reset password request form
1167 app.post( '/resetpw', function( req, res ) {
1168 log3("post resetpw");
1169 var email = req.body.email
1173 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1176 // If user exists, create reset code
1177 var resetPassCode = hat(64);
1178 user.setResetPassCode(resetPassCode);
1180 // Construct url that the user can then click to reset password
1181 var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1183 // Save user to database
1184 user.save( function( err ) {
1185 log3('save '+user.email);
1187 // Construct email and send it to the user
1191 'subject' : 'Your FinalsClub.org Password has been Reset!',
1193 'template' : 'userPasswordReset',
1195 'resetPassCode' : resetPassCode,
1196 'resetPassUrl' : resetPassUrl
1200 mailer.send( message, function( err, result ) {
1202 // XXX: Add route to resend this email
1204 console.log( 'Error sending user password reset email!' );
1206 console.log( 'Successfully sent user password reset email.' );
1211 // Render request success page
1212 res.render( 'resetpw-success', { 'email' : email } );
1216 res.render( 'resetpw-error', { 'email' : email } );
1221 // Recieve reset password form
1222 app.post( '/resetpw/:id', function( req, res ) {
1223 log3("post resetpw.code");
1224 var resetPassCode = req.params.id
1225 var email = req.body.email
1226 var pass1 = req.body.pass1
1227 var pass2 = req.body.pass2
1229 // Find user by email
1230 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1232 // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1233 // save user with new password and display success message.
1235 var valid = user.resetPassword(resetPassCode, pass1, pass2);
1237 user.save( function( err ) {
1238 res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );
1243 // If there was a problem, notify user
1245 res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1250 // Display registration page
1251 app.get( '/register', function( req, res ) {
1252 log3("get reg page");
1254 // Populate school dropdown list
1255 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1256 res.render( 'register', { 'schools' : schools } );
1260 // Recieve registration form
1261 app.post( '/register', function( req, res ) {
1262 var sid = req.sessionId;
1264 // Create new user from User schema
1265 var user = new User;
1267 // Populate user from form
1268 user.email = req.body.email.toLowerCase();
1269 user.password = req.body.password;
1271 // If school is set to other, then fill in school as what the
1273 user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1274 user.name = req.body.name;
1275 user.affil = req.body.affil;
1276 user.activated = false;
1279 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1280 req.flash( 'error', 'Please enter a valid email' );
1281 return res.redirect( '/register' );
1284 // Check if password is greater than 6 characters, otherwise notify user
1285 if ( req.body.password.length < 6 ) {
1286 req.flash( 'error', 'Please enter a password longer than eight characters' );
1287 return res.redirect( '/register' );
1290 // Pull out hostname from email
1291 var hostname = user.email.split( '@' ).pop();
1293 // Check if email is from one of the special domains
1294 if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1298 // Save user to database
1299 user.save( function( err ) {
1300 // If error, check if it is because the user already exists, if so
1301 // get the user information and let them know
1303 if( /dup key/.test( err.message ) ) {
1304 // attempting to register an existing address
1305 User.findOne({ 'email' : user.email }, function(err, result ) {
1306 if (result.activated) {
1307 // If activated, make sure they know how to contact the admin
1308 req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1309 return res.redirect( '/register' )
1311 // If not activated, direct them to the resendActivation page
1312 req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1313 return res.redirect( '/resendActivation' )
1317 // If any other type of error, prompt them to enter the registration again
1318 req.flash( 'error', 'An error occurred during registration.' );
1320 return res.redirect( '/register' );
1323 // send user activation email
1324 sendUserActivation( user );
1326 // Check if the hostname matches any in the approved schools
1327 School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1329 // If there is a match, send associated welcome message
1330 sendUserWelcome( user, true );
1331 log3('school recognized '+school.name);
1332 // If no users exist for the school, create empty array
1333 if (!school.users) school.users = [];
1334 // Add user to the school
1335 school.users.push( user._id );
1337 // Save school to the database
1338 school.save( function( err ) {
1339 log3('school.save() done');
1340 // Notify user that they have been added to the school
1341 req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1342 res.redirect( '/' );
1344 // Construct admin email about user registration
1348 'subject' : 'FC User Registration : User added to ' + school.name,
1350 'template' : 'userSchool',
1356 // If there isn't a match, send associated welcome message
1357 sendUserWelcome( user, false );
1358 // Tell user to check for activation link
1359 req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1360 res.redirect( '/' );
1361 // Construct admin email about user registration
1365 'subject' : 'FC User Registration : Email did not match any schools',
1367 'template' : 'userNoSchool',
1373 // Send email to admin
1374 mailer.send( message, function( err, result ) {
1377 console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1379 console.log( 'Successfully sent user has no school email to admin.' );
1389 // Display resendActivation request page
1390 app.get( '/resendActivation', function( req, res ) {
1391 var activateCode = req.session.activateCode;
1393 // Check if user exists by activateCode set in their session
1394 User.findById( activateCode, function( err, user ) {
1395 if( ( ! user ) || ( user.activated ) ) {
1396 res.redirect( '/' );
1398 // Send activation and redirect to login
1399 sendUserActivation( user );
1401 req.flash( 'info', 'Your activation code has been resent.' );
1403 res.redirect( '/login' );
1408 // Display activation page
1409 app.get( '/activate/:code', function( req, res ) {
1410 var code = req.params.code;
1412 // XXX could break this out into a middleware
1414 res.redirect( '/' );
1417 // Find user by activation code
1418 User.findById( code, function( err, user ) {
1419 if( err || ! user ) {
1420 // If not found, notify user of invalid code
1421 req.flash( 'error', 'Invalid activation code!' );
1423 res.redirect( '/' );
1425 // If valid, then activate user
1426 user.activated = true;
1428 // Regenerate our session and log in as the new user
1429 req.session.regenerate( function() {
1430 user.session = req.sessionID;
1432 // Save user to database
1433 user.save( function( err ) {
1435 req.flash( 'error', 'Unable to activate account.' );
1437 res.redirect( '/' );
1439 req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1441 res.redirect( '/profile' );
1450 app.get( '/logout', function( req, res ) {
1451 var sid = req.sessionID;
1453 // Find user by session id
1454 User.findOne( { 'session' : sid }, function( err, user ) {
1456 // Empty out session id
1459 // Save user to database
1460 user.save( function( err ) {
1461 res.redirect( '/' );
1464 res.redirect( '/' );
1469 // Display users profile page
1470 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1471 var user = req.user;
1473 res.render( 'profile/index', { 'user' : user } );
1476 // Recieve profile edit page form
1477 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1478 var user = req.user;
1479 var fields = req.body;
1482 var wasComplete = user.isComplete;
1484 if( ! fields.name ) {
1485 req.flash( 'error', 'Please enter a valid name!' );
1489 user.name = fields.name;
1492 if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1493 req.flash( 'error', 'Please select a valid affiliation!' );
1497 user.affil = fields.affiliation;
1500 if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1501 // changing password
1502 if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1503 if( fields.newPassword === fields.newPasswordConfirm ) {
1504 // test password strength?
1506 user.password = fields.newPassword;
1508 req.flash( 'error', 'Mismatch in new password!' );
1513 req.flash( 'error', 'Please supply your existing password.' );
1519 user.major = fields.major;
1520 user.bio = fields.bio;
1522 user.showName = ( fields.showName ? true : false );
1525 user.save( function( err ) {
1527 req.flash( 'error', 'Unable to save user profile!' );
1529 if( ( user.isComplete ) && ( ! wasComplete ) ) {
1530 req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1532 res.redirect( '/' );
1534 res.render( 'info', 'Your profile was successfully updated!' );
1536 res.render( 'profile/index', { 'user' : user } );
1541 res.render( 'profile/index', { 'user' : user } );
1548 function loadSubject( req, res, next ) {
1549 if( url.parse( req.url ).pathname.match(/subject/) ) {
1550 ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1552 sendJson(res, {status: 'error', message: 'Subject with this ID does not exist'} )
1554 req.subject = subject;
1563 function loadOldCourse( req, res, next ) {
1564 if( url.parse( req.url ).pathname.match(/course/) ) {
1565 ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1567 sendJson(res, {status: 'error', message: 'Course with this ID does not exist'} )
1569 req.course = course;
1578 var featuredCourses = [
1579 {name: 'The Human Mind', 'id': 1563},
1580 {name: 'Justice', 'id': 797},
1581 {name: 'Protest Literature', 'id': 1681},
1582 {name: 'Animal Cognition', 'id': 681},
1583 {name: 'Positive Psychology', 'id': 1793},
1584 {name: 'Social Psychology', 'id': 660},
1585 {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1586 {name: 'Cyberspace in Court', 'id': 1446},
1587 {name: 'Nazi Cinema', 'id': 2586},
1588 {name: 'Media and the American Mind', 'id': 2583},
1589 {name: 'Social Thought in Modern America', 'id': 2585},
1590 {name: 'Major British Writers II', 'id': 869},
1591 {name: 'Civil Procedure', 'id': 2589},
1592 {name: 'Evidence', 'id': 2590},
1593 {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1596 app.get( '/learn', loadUser, function( req, res ) {
1597 res.render( 'archive/learn', { 'courses' : featuredCourses } );
1600 app.get( '/learn/random', loadUser, function( req, res ) {
1601 res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1604 app.get( '/archive', checkAjax, loadUser, function( req, res ) {
1605 ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1607 sendJson(res, {status: 'error', message: 'There was a problem gathering the archived courses, please try again later.'} );
1609 sendJson(res, { 'subjects' : subjects, 'user': req.user.sanitized } );
1614 app.get( '/archive/subject/:id', checkAjax, loadUser, loadSubject, function( req, res ) {
1615 ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1617 sendJson(res, {status: 'error', message: 'There are no archived courses'} );
1619 sendJson(res, { 'courses' : courses, 'subject': req.subject, 'user': req.user.sanitized } );
1624 app.get( '/archive/course/:id', checkAjax, loadUser, loadOldCourse, function( req, res ) {
1625 ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1627 sendJson(res, {status: 'error', message: 'There are no notes in this course'} );
1629 sendJson(res, { 'notes' : notes, 'course' : req.course, 'user': req.user.sanitized } );
1634 app.get( '/archive/note/:id', checkAjax, loadUser, function( req, res ) {
1635 ArchivedNote.findById(req.params.id, function(err, note) {
1637 sendJson(res, {status: 'error', message: 'This is not a valid id for a note'} );
1639 ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1641 sendJson(res, {status: 'error', message: 'There is no course for this note'} )
1643 sendJson(res, { 'layout' : 'notesLayout', 'note' : note, 'course': course, 'user': req.user.sanitized } );
1651 app.get( '*', function(req, res) {
1652 res.sendfile('public/index.html');
1657 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1658 // the browser which facilitates near realtime interaction. This allows the user to post questions
1659 // and comments and other users to get those almost immediately after they are posted, without
1660 // reloading the page or pressing a button to refresh.
1662 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1663 // saving the data to the database, and then sending it out to everyone else connected.
1666 // Posts - Posts are the main items in backchannel, useful for questions or discussion points
1667 // [[ example object needed with explanation E.G:
1669 Post: { postID: '999-1',
1671 userName: 'Bob Jones',
1672 userAffil: 'Instructor',
1673 body: 'This is the text content of the post.',
1674 comments: { {<commentObj>, <commentObj>, ...},
1676 votes: [ <userID>, <userID>, ...],
1677 reports: [ <userID>, <userID>, ...]
1679 Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
1681 if anonymous: userName => 'Anonymous', userAffil => 'N/A'
1686 // Comments - Comments are replies to posts, for clarification or answering questions
1687 // [[ example object needed]]
1688 // Votes - Votes signifyg a users approval of a post
1689 // [[ example object needed]]
1690 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1691 // [[ example object needed]]
1695 // body - Main content of the post
1696 // userId - Not currently used, but would contain the users id that made the post
1697 // userName - Users name that made post
1698 // userAffil - Users affiliation to their school
1699 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1700 // date - Date post was made, updates when any comments are made for the post
1701 // comments - An array of comments which contain a body, userName, and userAffil
1702 // votes - An array of user ids which are the users that voted
1703 // [[ example needed ]]
1704 // reports - An array of user ids which are the users that reported the post
1705 // [[ reports would be "this post is flagged as inappropriate"? ]]
1706 // [[ bruml: consistent terminology needed ]]
1708 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1709 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1710 // users the ability to make posts or comments that they might not otherwise due to not wanting
1711 // the content of the post/comment to be attributed to them.
1713 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1714 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1715 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1716 // user is logged in, and if so display information that otherwise might not be visible to them if they
1717 // aren't apart of a particular school.
1719 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1720 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1721 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1722 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1723 // rendered in the browser.
1725 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1726 // events are as follows.
1729 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1730 // The server recieves the data, assembles a new post object for the database and then fills it with
1731 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1732 // replaced. If the user chose for the post to be private, then public will be set to false and it
1733 // will be filtered from being sent to users not logged into and not having access to the school. Once
1734 // the post has been created and saved into the database, it is sent to all connected users to that
1735 // particular lecture, unless it is private, than only logged in users will get it.
1738 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1739 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1740 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1741 // users unless the post is private, which then it will be only sent to logged in users.
1744 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1745 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1746 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1747 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1748 // bottom of the interface.
1751 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1752 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1753 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1754 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1755 // to the top of the interface. The comment can be anonymous, which then will have the user
1756 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1757 // connected users unless the post is private, then only logged in users will recieve the comment.
1759 var io = require( 'socket.io' ).listen( app );
1761 var Post = mongoose.model( 'Post' );
1763 io.set('authorization', function ( handshake, next ) {
1764 var rawCookie = handshake.headers.cookie;
1766 handshake.cookie = parseCookie(rawCookie);
1767 handshake.sid = handshake.cookie['connect.sid'];
1769 if ( handshake.sid ) {
1770 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1772 handshake.user = false;
1773 return next(null, true);
1775 // bake a new session object for full r/w
1776 handshake.session = new Session( handshake, session );
1778 User.findOne( { session : handshake.sid }, function( err, user ) {
1780 handshake.user = user;
1781 return next(null, true);
1783 handshake.user = false;
1784 return next(null, true);
1792 return next(null, true);
1796 var backchannel = new Backchannel(app, io.of('/backchannel'), {
1797 subscribe: function(lecture, send) {
1798 Post.find({'lecture': lecture}, function(err, posts) {
1802 post: function(fillPost) {
1803 var post = new Post;
1804 fillPost(post, function(send) {
1805 post.save(function(err) {
1810 items: function(postId, addItem) {
1811 Post.findById(postId, function( err, post ) {
1812 addItem(post, function(send) {
1813 post.save(function(err) {
1828 .on( 'connection', function( socket ) {
1829 // pull out user/session information etc.
1830 var handshake = socket.handshake;
1831 var userID = handshake.user._id;
1838 socket.on( 'join', function( note ) {
1839 if (handshake.user === false) {
1841 // XXX: replace by addToSet (once it's implemented in mongoose)
1842 Note.findById( noteID, function( err, note ) {
1844 if( note.collaborators.indexOf( userID ) == -1 ) {
1845 note.collaborators.push( userID );
1853 socket.on( 'watch', function( l ) {
1854 var sendCounts = function() {
1857 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1860 function( note, callback ) {
1862 var count = note.collaborators.length;
1868 socket.emit( 'counts', send );
1870 timer = setTimeout( sendCounts, 5000 );
1876 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1877 notes.forEach( function( note ) {
1878 watched.push( note._id );
1885 socket.on( 'disconnect', function() {
1886 clearTimeout( timer );
1888 if (handshake.user === false) {
1889 // XXX: replace with $pull once it's available
1891 Note.findById( noteID, function( err, note ) {
1893 var index = note.collaborators.indexOf( userID );
1896 note.collaborators.splice( index, 1 );
1907 // Exception Catch-All
1909 process.on('uncaughtException', function (e) {
1910 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1916 mongoose.connect( app.set( 'dbUri' ) );
1917 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1919 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1921 app.listen( serverPort, function() {
1922 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1924 // if run as root, downgrade to the owner of this file
1925 if (process.getuid() === 0) {
1926 require('fs').stat(__filename, function(err, stats) {
1927 if (err) { return console.log(err); }
1928 process.setuid(stats.uid);
1933 function isValidEmail(email) {
1934 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])?/;
1935 return email.match(re);