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;
27 // Used for initial testing
28 var log3 = function() {}
31 var app = module.exports = express.createServer();
33 // Load Mongoose Schemas
34 // The actual schemas are located in models.j
35 var User = mongoose.model( 'User' );
36 var School = mongoose.model( 'School' );
37 var Course = mongoose.model( 'Course' );
38 var Lecture = mongoose.model( 'Lecture' );
39 var Note = mongoose.model( 'Note' );
41 // More schemas used for legacy data
42 var ArchivedCourse = mongoose.model( 'ArchivedCourse' );
43 var ArchivedNote = mongoose.model( 'ArchivedNote' );
44 var ArchivedSubject = mongoose.model( 'ArchivedSubject' );
46 // XXX Not sure if necessary
47 var ObjectId = mongoose.SchemaTypes.ObjectId;
50 // Use the environment variable DEV_EMAIL for testing
51 var ADMIN_EMAIL = process.env.DEV_EMAIL || 'info@finalsclub.org';
53 // Set server hostname and port from environment variables,
55 // XXX Can be cleaned up
56 var serverHost = process.env.SERVER_HOST;
57 var serverPort = process.env.SERVER_PORT;
60 console.log( 'Using server hostname defined in environment: %s', serverHost );
62 serverHost = os.hostname();
63 console.log( 'No hostname defined, defaulting to os.hostname(): %s', serverHost );
66 // Express configuration depending on environment
67 // development is intended for developing locally or
68 // when not in production, otherwise production is used
69 // when the site will be run live for regular usage.
70 app.configure( 'development', function() {
71 // In development mode, all errors and stack traces will be
72 // dumped to the console and on page for easier troubleshooting
74 app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
76 // Set database connection information from environment
77 // variables otherwise use localhost.
78 app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
79 app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
81 // Set Amazon access and secret keys from environment
82 // variables. These keys are intended to be secret, so
83 // are not included in the source code, but set on the server
85 app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
86 app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
88 // If a port wasn't set earlier, set to 3000
94 // Production configuration settings
95 app.configure( 'production', function() {
96 // At the moment we have errors outputting everything
97 // so if there are any issues it is easier to track down.
98 // Once the site is more stable it will be prudent to
99 // use less error tracing.
100 app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
102 // Disable view cache due to stale views.
103 // XXX Disable view caching temp
104 app.disable( 'view cache' )
106 // Against setting the database connection information
107 // XXX Can be cleaned up or combined
108 app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
109 app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
111 // XXX Can be cleaned up or combined
112 app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
113 app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
115 // Set to port 80 if not set through environment variables
121 // General Express configuration settings
122 app.configure(function(){
123 // Views are housed in the views folder
124 app.set( 'views', __dirname + '/views' );
125 // All templates use jade for rendering
126 app.set( 'view engine', 'jade' );
127 // Bodyparser is required to handle form submissions
128 // without manually parsing them.
129 app.use( express.bodyParser() );
131 app.use( express.cookieParser() );
133 // Sessions are stored in mongodb which allows them
134 // to be persisted even between server restarts.
135 app.set( 'sessionStore', new mongoStore( {
136 'url' : app.set( 'dbUri' )
139 // This is where the actual Express session handler
140 // is defined, with a mongoStore being set as the
141 // session storage versus in memory storage that is
143 app.use( express.session( {
144 // A secret 'password' for encrypting and decrypting
146 // XXX Should be handled differently
147 'secret' : 'finalsclub',
148 // The max age of the cookies that is allowed
149 // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) * 1000 (milliseconds)
150 'maxAge' : new Date(Date.now() + (60 * 60 * 24 * 30 * 1000)),
151 'store' : app.set( 'sessionStore' )
154 // methodOverride is used to handle PUT and DELETE HTTP
155 // requests that otherwise aren't handled by default.
156 app.use( express.methodOverride() );
157 // Sets the routers middleware to load after everything set
158 // before it, but before static files.
159 app.use( app.router );
160 // Static files are loaded when no dynamic views match.
161 app.use( express.static( __dirname + '/public' ) );
163 // This is the errorHandler set in configuration earlier
164 // being set to a variable to be used after all other
165 // middleware is loaded. Error handling should always
166 // come last or near the bottom.
167 var errorHandler = app.set( 'errorHandler' );
169 app.use( errorHandler );
173 // Mailer functions and helpers
174 // These are helper functions that make for cleaner code.
176 // sendUserActivation is for when a user registers and
177 // first needs to activate their account to use it.
178 function sendUserActivation( user ) {
182 'subject' : 'Activate your FinalsClub.org Account',
184 // Templates are in the email folder and use ejs
185 'template' : 'userActivation',
186 // Locals are used inside ejs so dynamic information
187 // can be rendered properly.
190 'serverHost' : serverHost
194 // Email is sent here
195 mailer.send( message, function( err, result ) {
197 // XXX: Add route to resend this email
198 console.log( 'Error sending user activation email\nError Message: '+err.Message );
200 console.log( 'Successfully sent user activation email.' );
205 // sendUserWelcome is for when a user registers and
206 // a welcome email is sent.
207 function sendUserWelcome( user, school ) {
208 // If a user is not apart of a supported school, they are
209 // sent a different template than if they are apart of a
211 var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
215 'subject' : 'Welcome to FinalsClub',
217 'template' : template,
220 'serverHost' : serverHost
224 mailer.send( message, function( err, result ) {
226 // XXX: Add route to resend this email
227 console.log( 'Error sending user welcome email\nError Message: '+err.Message );
229 console.log( 'Successfully sent user welcome email.' );
235 // These functions are used later in the routes to help
236 // load information and variables, as well as handle
237 // various instances like checking if a user is logged in
239 function loggedIn( req, res, next ) {
240 // If req.user is set, then pass on to the next function
241 // or else alert the user with an error message.
245 req.flash( 'error', 'You must be logged in to access that feature!' );
250 // This loads the user if logged in
251 function loadUser( req, res, next ) {
252 var sid = req.sessionID;
254 console.log( 'got request from session ID: %s', sid );
256 // Find a user based on their stored session id
257 User.findOne( { session : sid }, function( err, user ) {
262 // If a user is found then set req.user the contents of user
263 // and make sure req.user.loggedIn is true.
267 req.user.loggedIn = true;
269 log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
271 // Check if a user is activated. If not, then redirec
272 // to the homepage and tell them to check their email
273 // for the activation email.
274 if( req.user.activated ) {
275 // Is the user's profile complete? If not, redirect to their profile
276 if( ! req.user.isComplete ) {
277 if( url.parse( req.url ).pathname != '/profile' ) {
278 req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
280 res.redirect( '/profile' );
288 req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
293 // If no user record was found, then we store the requested
294 // path they intended to view and redirect them after they
295 // login if it is requred.
296 var path = url.parse( req.url ).pathname;
297 req.session.redirect = path;
299 // Set req.user to an empty object so it doesn't throw errors
300 // later on that it isn't defined.
308 // loadSchool is used to load a school by it's id
309 function loadSchool( req, res, next ) {
311 var schoolId = req.params.id;
313 School.findById( schoolId, function( err, school ) {
317 // If a school is found, the user is checked to see if they are
318 // authorized to see or interact with anything related to that
320 school.authorize( user, function( authorized ){
321 req.school.authorized = authorized;
325 // If no school is found, display an appropriate error.
326 req.flash( 'error', 'Invalid school specified!' );
333 // loadSchool is used to load a course by it's id
334 function loadCourse( req, res, next ) {
336 var courseId = req.params.id;
338 Course.findById( courseId, function( err, course ) {
339 if( course && !course.deleted ) {
342 // If a course is found, the user is checked to see if they are
343 // authorized to see or interact with anything related to that
345 course.authorize( user, function( authorized ) {
346 req.course.authorized = authorized;
351 // If no course is found, display an appropriate error.
352 req.flash( 'error', '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 req.flash( 'error', 'Invalid lecture specified!' );
385 // loadNote is used to load a note by it's id
386 // This is a lot more complicated than the above
387 // due to public/private handling of notes.
388 function loadNote( req, res, next ) {
389 var user = req.user ? req.user : false;
390 var noteId = req.params.id;
392 Note.findById( noteId, function( err, note ) {
393 // If a note is found, and user is set, check if
394 // user is authorized to interact with that note.
395 if( note && user && !note.deleted ) {
396 note.authorize( user, function( auth ) {
398 // If authorzied, then set req.note to be used later
402 } else if ( note.public ) {
403 // If not authorized, but the note is public, then
404 // designate the note read only (RO) and store req.note
410 // If the user is not authorized and the note is private
411 // then display and error.
412 req.flash( 'error', 'You do not have permission to access that note.' );
417 } else if ( note && note.public && !note.deleted ) {
418 // If note is found, but user is not set because they are not
419 // logged in, and the note is public, set the note to read only
420 // and store the note for later.
425 } else if ( note && !note.public && !note.deleted ) {
426 // If the note is found, but user is not logged in and the note is
427 // not public, then ask them to login to view the note. Once logged
428 // in they will be redirected to the note, at which time authorization
429 // handling will be put in effect above.
430 req.session.redirect = '/note/' + note._id;
431 req.flash( 'error', 'You must be logged in to view that note.' );
432 res.redirect( '/login' );
435 req.flash( 'error', 'Invalid note specified!' );
437 res.redirect( '/schools' );
442 // Dynamic Helpers are loaded automatically into views
443 app.dynamicHelpers( {
444 // express-messages is for flash messages for easy
445 // errors and information display
446 'messages' : require( 'express-messages' ),
448 // By default the req object isn't sen't to views
449 // during rendering, this allows you to use the
450 // user object if available in views.
451 'user' : function( req, res ) {
455 // Same, this allows session to be available in views.
456 'session' : function( req, res ) {
462 // The following are the main CRUD routes that are used
463 // to make up this web app.
467 app.get( '/', loadUser, function( req, res ) {
470 res.render( 'index' );
474 // Used to display all available schools and any courses
476 // Public with some private information
477 app.get( '/schools', loadUser, function( req, res ) {
480 // Find all schools and sort by name
481 // XXX mongoose's documentation on sort is extremely poor, tread carefully
482 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
484 // If schools are found, loop through them gathering any courses that are
485 // associated with them and then render the page with that information.
488 function( school, callback ) {
489 // Check if user is authorized with each school
490 school.authorize( user, function( authorized ) {
491 // This is used to display interface elements for those users
492 // that are are allowed to see them, for instance a 'New Course' button.
493 school.authorized = authorized;
495 // Find all courses for school by it's id and sort by name
496 Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
497 // If any courses are found, set them to the appropriate school, otherwise
499 if( courses.length > 0 ) {
500 school.courses = courses.filter(function(course) {
501 if (!course.deleted) return course;
506 // This tells async (the module) that each iteration of forEach is
507 // done and will continue to call the rest until they have all been
508 // completed, at which time the last function below will be called.
513 // After all schools and courses have been found, render them
515 res.render( 'schools', { 'schools' : schools } );
519 // If no schools have been found, display none
520 res.render( 'schools', { 'schools' : [] } );
526 // Displays form to create new course
527 // Private, requires user to be authorized
528 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
529 // Load school from middleware
530 var school = req.school;
532 // If school was not loaded for whatever reason, or the user is not authorized
533 // then redirect to the main schools page.
534 if( ( ! school ) || ( ! school.authorized ) ) {
535 return res.redirect( '/schools' );
538 // If they are authorized and the school exists, then render the page
539 res.render( 'course/new', { 'school': school } );
542 // Recieves new course form
543 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
544 var school = req.school;
545 // Creates new course from Course Schema
546 var course = new Course;
547 // Gathers instructor information from form
548 var instructorEmail = req.body.email.toLowerCase();
549 var instructorName = req.body.instructorName;
551 // If school doesn't exist or user is not authorized redirect to main schools page
552 if( ( ! school ) || ( ! school.authorized ) ) {
553 res.redirect( '/schools' );
556 // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
557 if ( !instructorEmail || !instructorName ) {
558 req.flash( 'error', 'Invalid parameters!' )
559 return res.render( 'course/new' );
562 // Fill out the course with information from the form
563 course.number = req.body.number;
564 course.name = req.body.name;
565 course.description = req.body.description;
566 course.school = school._id;
567 course.creator = req.user._id;
568 course.subject = req.body.subject;
569 course.department = req.body.department;
571 // Check if a user exists with the instructorEmail, if not then create
572 // a new user and send them an instructor welcome email.
573 User.findOne( { 'email' : instructorEmail }, function( err, user ) {
577 user.name = instructorName
578 user.email = instructorEmail;
579 user.affil = 'Instructor';
580 user.school = school.name;
582 user.activated = false;
584 // Validate instructorEmail
585 // XXX Probably could be done before checking db
586 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
587 req.flash( 'error', 'Please enter a valid email' );
588 // XXX This needs to be fixed, this is not the proper flow
589 return res.redirect( '/register' );
591 // Once the new user information has been completed, save the user
592 // to the database then email them the instructor welcome email.
593 user.save(function( err ) {
594 // If there was an error saving the instructor, prompt the user to fill out
595 // the information again.
597 req.flash( 'error', 'Invalid parameters!' )
598 return res.render( 'course/new' );
603 'subject' : 'A non-profit open education initiative',
605 'template' : 'instructorInvite',
610 'serverHost' : serverHost
614 mailer.send( message, function( err, result ) {
616 console.log( 'Error inviting instructor to course!' );
618 console.log( 'Successfully invited instructor to course.' );
622 // After emails are sent, set the courses instructor to the
623 // new users id and then save the course to the database.
624 course.instructor = user._id;
625 course.save( function( err ) {
627 // XXX better validation
628 req.flash( 'error', 'Invalid parameters!' );
630 return res.render( 'course/new' );
632 // Once the course has been completed email the admin with information
633 // on the course and new instructor
637 'subject' : school.name+' has a new course: '+course.name,
639 'template' : 'newCourse',
644 'serverHost' : serverHost
648 mailer.send( message, function( err, result ) {
650 console.log( 'Error sending new course email to info@finalsclub.org' )
652 console.log( 'Successfully invited instructor to course')
655 // Redirect the user to the schools page where they can see
657 // XXX Redirect to the new course instead
658 res.redirect( '/schools' );
664 // If the user exists, then check if they are already and instructor
665 if (user.affil === 'Instructor') {
666 // If they are an instructor, then save the course with the appropriate
667 // information and email the admin.
668 course.instructor = user._id;
669 course.save( function( err ) {
671 // XXX better validation
672 req.flash( 'error', 'Invalid parameters!' );
674 return res.render( 'course/new' );
679 'subject' : school.name+' has a new course: '+course.name,
681 'template' : 'newCourse',
686 'serverHost' : serverHost
690 mailer.send( message, function( err, result ) {
692 console.log( 'Error sending new course email to info@finalsclub.org' )
694 console.log( 'Successfully invited instructor to course')
697 // XXX Redirect to the new course instead
698 res.redirect( '/schools' );
702 // The existing user isn't an instructor, so the user is notified of the error
703 // and the course isn't created.
704 req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
705 res.render( 'course/new' );
711 // Individual Course Listing
712 // Public with private information
713 app.get( '/course/:id', loadUser, loadCourse, function( req, res ) {
714 var userId = req.user._id;
715 var course = req.course;
717 // Check if the user is subscribed to the course
718 // XXX Not currently used for anything
719 var subscribed = course.subscribed( userId );
721 // Find lectures associated with this course and sort by name
722 Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
723 // Get course instructor information using their id
724 User.findById( course.instructor, function( err, instructor ) {
725 // Render course and lectures
726 res.render( 'course/index', { 'course' : course, 'instructor': instructor, 'subscribed' : subscribed, 'lectures' : lectures } );
732 app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
733 var course = req.course;
737 res.render( 'course/new', {course: course} )
739 req.flash( 'error', 'You don\'t have permission to do that' )
740 res.redirect( '/schools' );
744 // Recieve Course Edit Form
745 app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
746 var course = req.course;
750 var courseChanges = req.body;
751 course.number = courseChanges.number;
752 course.name = courseChanges.name;
753 course.description = courseChanges.description;
754 course.department = courseChanges.department;
756 course.save(function(err) {
758 req.flash( 'error', 'There was an error saving the course' );
760 res.redirect( '/course/'+ course._id.toString());
763 req.flash( 'error', 'You don\'t have permission to do that' )
764 res.redirect( '/schools' );
769 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
770 var course = req.course;
774 course.delete(function( err ) {
775 if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
776 else req.flash( 'info', 'Successfully removed course' )
777 res.redirect( '/schools' );
780 req.flash( 'error', 'You don\'t have permission to do that' )
781 res.redirect( '/schools' );
785 // Subscribe to course
786 // XXX Not currently used for anything
787 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
788 var course = req.course;
789 var userId = req.user._id;
791 course.subscribe( userId, function( err ) {
793 req.flash( 'error', 'Error subscribing to course!' );
796 res.redirect( '/course/' + course._id );
800 // Unsubscribe from course
801 // XXX Not currently used for anything
802 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
803 var course = req.course;
804 var userId = req.user._id;
806 course.unsubscribe( userId, function( err ) {
808 req.flash( 'error', 'Error unsubscribing from course!' );
811 res.redirect( '/course/' + course._id );
815 // Create new lecture
816 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
817 var courseId = req.params.id;
818 var course = req.course;
821 // If course isn't valid or user isn't authorized for course, redirect
822 if( ( ! course ) || ( ! course.authorized ) ) {
823 return res.redirect( '/course/' + courseId );
826 // Render new lecture form
827 res.render( 'lecture/new', { 'lecture' : lecture } );
830 // Recieve New Lecture Form
831 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
832 var courseId = req.params.id;
833 var course = req.course;
834 // Create new lecture from Lecture schema
835 var lecture = new Lecture;
837 if( ( ! course ) || ( ! course.authorized ) ) {
838 res.redirect( '/course/' + courseId );
843 // Populate lecture with form data
844 lecture.name = req.body.name;
845 lecture.date = req.body.date;
846 lecture.course = course._id;
847 lecture.creator = req.user._id;
849 // Save lecture to database
850 lecture.save( function( err ) {
852 // XXX better validation
853 req.flash( 'error', 'Invalid parameters!' );
855 res.render( 'lecture/new', { 'lecture' : lecture } );
857 // XXX Redirect to new lecture instead
858 res.redirect( '/course/' + course._id );
864 // Display individual lecture and related notes
865 app.get( '/lecture/:id', loadUser, loadLecture, function( req, res ) {
866 var lecture = req.lecture;
868 // Grab the associated course
869 // XXX this should be done with DBRefs eventually
870 Course.findById( lecture.course, function( err, course ) {
872 // If course is found, find instructor information to be displayed on page
873 User.findById( course.instructor, function( err, instructor ) {
874 // Pull out our notes
875 Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
876 if ( !req.user.loggedIn || !req.lecture.authorized ) {
877 // Loop through notes and only return those that are public if the
878 // user is not logged in or not authorized for that lecture
879 notes = notes.filter(function( note ) {
880 if ( note.public ) return note;
883 // Render lecture and notes
884 res.render( 'lecture/index', {
887 'instructor' : instructor,
891 'javascripts' : [ 'counts.js' ]
896 // XXX with DBRefs we will be able to reassign orphaned courses/lecture/pads
898 req.flash( 'error', 'That lecture is orphaned!' );
905 // Display new note form
906 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
907 var lectureId = req.params.id;
908 var lecture = req.lecture;
911 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
912 res.redirect( '/lecture/' + lectureId );
917 res.render( 'notes/new', { 'note' : note } );
920 // Recieve new note form
921 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
922 var lectureId = req.params.id;
923 var lecture = req.lecture;
925 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
926 res.redirect( '/lecture/' + lectureId );
931 // Create note from Note schema
934 // Populate note from form data
935 note.name = req.body.name;
936 note.date = req.body.date;
937 note.lecture = lecture._id;
938 note.public = req.body.private ? false : true;
939 note.creator = req.user._id;
941 // Save note to database
942 note.save( function( err ) {
944 // XXX better validation
945 req.flash( 'error', 'Invalid parameters!' );
947 res.render( 'notes/new', { 'note' : note } );
949 // XXX Redirect to new note instead
950 res.redirect( '/lecture/' + lecture._id );
956 // Display individual note page
957 app.get( '/note/:id', loadUser, loadNote, function( req, res ) {
959 // Set read only id for etherpad-lite or false for later check
960 var roID = note.roID || false;
962 var lectureId = note.lecture;
964 // Count the amount of visits, but only once per session
965 if ( req.session.visited ) {
966 if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
967 req.session.visited.push( note._id );
971 req.session.visited = [];
972 req.session.visited.push( note._id );
976 // If a read only id exists process note
980 // If read only id doesn't, then fetch the read only id from the database and then
982 // XXX Soon to be depracated due to a new API in etherpad that makes for a
983 // much cleaner solution.
984 db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
985 epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
987 roID = record.value.replace(/"/g, '');
996 function processReq() {
998 Lecture.findById( lectureId, function( err, lecture ) {
1000 req.flash( 'error', 'That notes page is orphaned!' );
1002 res.redirect( '/' );
1004 // Find notes based on lecture id, which will be displayed in a dropdown
1006 Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1008 // User is logged in and sees full notepad
1010 res.render( 'notes/index', {
1011 'layout' : 'noteLayout',
1012 'host' : serverHost,
1014 'lecture' : lecture,
1015 'otherNotes' : otherNotes,
1018 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1019 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1022 // User is not logged in and sees notepad that is public
1023 res.render( 'notes/public', {
1024 'layout' : 'noteLayout',
1025 'host' : serverHost,
1027 'otherNotes' : otherNotes,
1029 'lecture' : lecture,
1030 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1031 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1039 // Static pages and redirects
1040 app.get( '/about', loadUser, function( req, res ) {
1041 res.redirect( 'http://blog.finalsclub.org/about.html' );
1044 app.get( '/press', loadUser, function( req, res ) {
1045 res.render( 'static/press' );
1048 app.get( '/conduct', loadUser, function( req, res ) {
1049 res.render( 'static/conduct' );
1052 app.get( '/legal', loadUser, function( req, res ) {
1053 res.redirect( 'http://blog.finalsclub.org/legal.html' );
1056 app.get( '/contact', loadUser, function( req, res ) {
1057 res.redirect( 'http://blog.finalsclub.org/contact.html' );
1060 app.get( '/privacy', loadUser, function( req, res ) {
1061 res.render( 'static/privacy' );
1065 // Authentication routes
1066 // These are used for logging in, logging out, registering
1067 // and other user authentication purposes
1069 // Render login page
1070 app.get( '/login', function( req, res ) {
1071 log3("get login page")
1073 res.render( 'login' );
1076 // Recieve login form
1077 app.post( '/login', function( req, res ) {
1078 var email = req.body.email;
1079 var password = req.body.password;
1080 log3("post login ...")
1082 // Find user from email
1083 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1087 // If user exists, check if activated, if not notify them and send them to
1090 if( ! user.activated ) {
1091 // (undocumented) markdown-esque link functionality in req.flash
1092 req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1094 req.session.activateCode = user._id;
1096 res.render( 'login' );
1098 // If user is activated, check if their password is correct
1099 if( user.authenticate( password ) ) {
1102 var sid = req.sessionID;
1106 // Set the session then save the user to the database
1107 user.save( function() {
1108 var redirect = req.session.redirect;
1110 // login complete, remember the user's email for next time
1111 req.session.email = email;
1113 // alert the successful login
1114 req.flash( 'info', 'Successfully logged in!' );
1116 // redirect to profile if we don't have a stashed request
1117 res.redirect( redirect || '/profile' );
1120 // Notify user of bad login
1121 req.flash( 'error', 'Invalid login!' );
1123 res.render( 'login' );
1127 // Notify user of bad login
1129 req.flash( 'error', 'Invalid login!' );
1131 res.render( 'login' );
1136 // Request reset password
1137 app.get( '/resetpw', function( req, res ) {
1138 log3("get resetpw page");
1139 res.render( 'resetpw' );
1142 // Display reset password from requested email
1143 app.get( '/resetpw/:id', function( req, res ) {
1144 var resetPassCode = req.params.id
1145 res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1148 // Recieve reset password request form
1149 app.post( '/resetpw', function( req, res ) {
1150 log3("post resetpw");
1151 var email = req.body.email
1155 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1158 // If user exists, create reset code
1159 var resetPassCode = hat(64);
1160 user.setResetPassCode(resetPassCode);
1162 // Construct url that the user can then click to reset password
1163 var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1165 // Save user to database
1166 user.save( function( err ) {
1167 log3('save '+user.email);
1169 // Construct email and send it to the user
1173 'subject' : 'Your FinalsClub.org Password has been Reset!',
1175 'template' : 'userPasswordReset',
1177 'resetPassCode' : resetPassCode,
1178 'resetPassUrl' : resetPassUrl
1182 mailer.send( message, function( err, result ) {
1184 // XXX: Add route to resend this email
1186 console.log( 'Error sending user password reset email!' );
1188 console.log( 'Successfully sent user password reset email.' );
1193 // Render request success page
1194 res.render( 'resetpw-success', { 'email' : email } );
1198 res.render( 'resetpw-error', { 'email' : email } );
1203 // Recieve reset password form
1204 app.post( '/resetpw/:id', function( req, res ) {
1205 log3("post resetpw.code");
1206 var resetPassCode = req.params.id
1207 var email = req.body.email
1208 var pass1 = req.body.pass1
1209 var pass2 = req.body.pass2
1211 // Find user by email
1212 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1214 // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1215 // save user with new password and display success message.
1217 var valid = user.resetPassword(resetPassCode, pass1, pass2);
1219 user.save( function( err ) {
1220 res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );
1225 // If there was a problem, notify user
1227 res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1232 // Display registration page
1233 app.get( '/register', function( req, res ) {
1234 log3("get reg page");
1236 // Populate school dropdown list
1237 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1238 res.render( 'register', { 'schools' : schools } );
1242 // Recieve registration form
1243 app.post( '/register', function( req, res ) {
1244 var sid = req.sessionId;
1246 // Create new user from User schema
1247 var user = new User;
1249 // Populate user from form
1250 user.email = req.body.email.toLowerCase();
1251 user.password = req.body.password;
1253 // If school is set to other, then fill in school as what the
1255 user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1256 user.name = req.body.name;
1257 user.affil = req.body.affil;
1258 user.activated = false;
1261 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1262 req.flash( 'error', 'Please enter a valid email' );
1263 return res.redirect( '/register' );
1266 // Check if password is greater than 6 characters, otherwise notify user
1267 if ( req.body.password.length < 6 ) {
1268 req.flash( 'error', 'Please enter a password longer than eight characters' );
1269 return res.redirect( '/register' );
1272 // Pull out hostname from email
1273 var hostname = user.email.split( '@' ).pop();
1275 // Check if email is from one of the special domains
1276 if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1280 // Save user to database
1281 user.save( function( err ) {
1282 // If error, check if it is because the user already exists, if so
1283 // get the user information and let them know
1285 if( /dup key/.test( err.message ) ) {
1286 // attempting to register an existing address
1287 User.findOne({ 'email' : user.email }, function(err, result ) {
1288 if (result.activated) {
1289 // If activated, make sure they know how to contact the admin
1290 req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1291 return res.redirect( '/register' )
1293 // If not activated, direct them to the resendActivation page
1294 req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1295 return res.redirect( '/resendActivation' )
1299 // If any other type of error, prompt them to enter the registration again
1300 req.flash( 'error', 'An error occurred during registration.' );
1302 return res.redirect( '/register' );
1305 // send user activation email
1306 sendUserActivation( user );
1308 // Check if the hostname matches any in the approved schools
1309 School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1311 // If there is a match, send associated welcome message
1312 sendUserWelcome( user, true );
1313 log3('school recognized '+school.name);
1314 // If no users exist for the school, create empty array
1315 if (!school.users) school.users = [];
1316 // Add user to the school
1317 school.users.push( user._id );
1319 // Save school to the database
1320 school.save( function( err ) {
1321 log3('school.save() done');
1322 // Notify user that they have been added to the school
1323 req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1324 res.redirect( '/' );
1326 // Construct admin email about user registration
1330 'subject' : 'FC User Registration : User added to ' + school.name,
1332 'template' : 'userSchool',
1338 // If there isn't a match, send associated welcome message
1339 sendUserWelcome( user, false );
1340 // Tell user to check for activation link
1341 req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1342 res.redirect( '/' );
1343 // Construct admin email about user registration
1347 'subject' : 'FC User Registration : Email did not match any schools',
1349 'template' : 'userNoSchool',
1355 // Send email to admin
1356 mailer.send( message, function( err, result ) {
1359 console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1361 console.log( 'Successfully sent user has no school email to admin.' );
1371 // Display resendActivation request page
1372 app.get( '/resendActivation', function( req, res ) {
1373 var activateCode = req.session.activateCode;
1375 // Check if user exists by activateCode set in their session
1376 User.findById( activateCode, function( err, user ) {
1377 if( ( ! user ) || ( user.activated ) ) {
1378 res.redirect( '/' );
1380 // Send activation and redirect to login
1381 sendUserActivation( user );
1383 req.flash( 'info', 'Your activation code has been resent.' );
1385 res.redirect( '/login' );
1390 // Display activation page
1391 app.get( '/activate/:code', function( req, res ) {
1392 var code = req.params.code;
1394 // XXX could break this out into a middleware
1396 res.redirect( '/' );
1399 // Find user by activation code
1400 User.findById( code, function( err, user ) {
1401 if( err || ! user ) {
1402 // If not found, notify user of invalid code
1403 req.flash( 'error', 'Invalid activation code!' );
1405 res.redirect( '/' );
1407 // If valid, then activate user
1408 user.activated = true;
1410 // Regenerate our session and log in as the new user
1411 req.session.regenerate( function() {
1412 user.session = req.sessionID;
1414 // Save user to database
1415 user.save( function( err ) {
1417 req.flash( 'error', 'Unable to activate account.' );
1419 res.redirect( '/' );
1421 req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1423 res.redirect( '/profile' );
1432 app.get( '/logout', function( req, res ) {
1433 var sid = req.sessionID;
1435 // Find user by session id
1436 User.findOne( { 'session' : sid }, function( err, user ) {
1438 // Empty out session id
1441 // Save user to database
1442 user.save( function( err ) {
1443 res.redirect( '/' );
1446 res.redirect( '/' );
1451 // Display users profile page
1452 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1453 var user = req.user;
1455 res.render( 'profile/index', { 'user' : user } );
1458 // Recieve profile edit page form
1459 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1460 var user = req.user;
1461 var fields = req.body;
1464 var wasComplete = user.isComplete;
1466 if( ! fields.name ) {
1467 req.flash( 'error', 'Please enter a valid name!' );
1471 user.name = fields.name;
1474 if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1475 req.flash( 'error', 'Please select a valid affiliation!' );
1479 user.affil = fields.affiliation;
1482 if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1483 // changing password
1484 if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1485 if( fields.newPassword === fields.newPasswordConfirm ) {
1486 // test password strength?
1488 user.password = fields.newPassword;
1490 req.flash( 'error', 'Mismatch in new password!' );
1495 req.flash( 'error', 'Please supply your existing password.' );
1501 user.major = fields.major;
1502 user.bio = fields.bio;
1504 user.showName = ( fields.showName ? true : false );
1507 user.save( function( err ) {
1509 req.flash( 'error', 'Unable to save user profile!' );
1511 if( ( user.isComplete ) && ( ! wasComplete ) ) {
1512 req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1514 res.redirect( '/' );
1516 res.render( 'info', 'Your profile was successfully updated!' );
1518 res.render( 'profile/index', { 'user' : user } );
1523 res.render( 'profile/index', { 'user' : user } );
1530 function loadSubject( req, res, next ) {
1531 if( url.parse( req.url ).pathname.match(/subject/) ) {
1532 ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1534 req.flash( 'error', 'Subject with this ID does not exist' )
1535 res.redirect( '/archive' );
1537 req.subject = subject;
1546 function loadOldCourse( req, res, next ) {
1547 if( url.parse( req.url ).pathname.match(/course/) ) {
1548 ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1550 req.flash( 'error', 'Course with this ID does not exist' )
1551 res.redirect( '/archive' );
1553 req.course = course;
1562 var featuredCourses = [
1563 {name: 'The Human Mind', 'id': 1563},
1564 {name: 'Justice', 'id': 797},
1565 {name: 'Protest Literature', 'id': 1681},
1566 {name: 'Animal Cognition', 'id': 681},
1567 {name: 'Positive Psychology', 'id': 1793},
1568 {name: 'Social Psychology', 'id': 660},
1569 {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1570 {name: 'Cyberspace in Court', 'id': 1446},
1571 {name: 'Nazi Cinema', 'id': 2586},
1572 {name: 'Media and the American Mind', 'id': 2583},
1573 {name: 'Social Thought in Modern America', 'id': 2585},
1574 {name: 'Major British Writers II', 'id': 869},
1575 {name: 'Civil Procedure', 'id': 2589},
1576 {name: 'Evidence', 'id': 2590},
1577 {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1580 app.get( '/learn', loadUser, function( req, res ) {
1581 res.render( 'archive/learn', { 'courses' : featuredCourses } );
1584 app.get( '/learn/random', loadUser, function( req, res ) {
1585 res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1588 app.get( '/archive', loadUser, function( req, res ) {
1589 ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1591 req.flash( 'error', 'There was a problem gathering the archived courses, please try again later.' );
1592 res.redirect( '/' );
1594 res.render( 'archive/index', { 'subjects' : subjects } );
1599 app.get( '/archive/subject/:id', loadUser, loadSubject, function( req, res ) {
1600 ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1602 req.flash( 'error', 'There are no archived courses' );
1603 res.redirect( '/' );
1605 res.render( 'archive/courses', { 'courses' : courses, 'subject': req.subject } );
1610 app.get( '/archive/course/:id', loadUser, loadOldCourse, function( req, res ) {
1611 ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1613 req.flash( 'error', 'There are no notes in this course' );
1614 res.redirect( '/archive' );
1616 res.render( 'archive/notes', { 'notes' : notes, 'course' : req.course } );
1621 app.get( '/archive/note/:id', loadUser, function( req, res ) {
1622 ArchivedNote.findById(req.params.id, function(err, note) {
1624 req.flash( 'error', 'This is not a valid id for a note' );
1625 res.redirect( '/archive' );
1627 ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1629 req.flash( 'error', 'There is no course for this note' )
1630 res.redirect( '/archive' )
1632 res.render( 'archive/note', { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
1641 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1642 // the browser which facilitates near realtime interaction. This allows the user to post questions
1643 // and comments and other users to get those almost immediately after they are posted, without
1644 // reloading the page or pressing a button to refresh.
1646 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1647 // saving the data to the database, and then sending it out to everyone else connected.
1650 // Posts - Posts are the main items in backchannel, useful for questions or discussion points
1651 // [[ example object needed]]
1652 // Comments - Comments are replies to posts, for clarification or answering questions
1653 // [[ example object needed]]
1654 // Votes - Votes signifyg a users approval of a post
1655 // [[ example object needed]]
1656 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1657 // [[ example object needed]]
1661 // body - Main content of the post
1662 // userId - Not currently used, but would contain the users id that made the post
1663 // userName - Users name that made post
1664 // userAffil - Users affiliation to their school
1665 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1666 // date - Date post was made, updates when any comments are made for the post
1667 // comments - An array of comments which contain a body, userName, and userAffil
1668 // votes - An array of user ids which are the users that voted
1669 // [[ example needed ]]
1670 // reports - An array of user ids which are the users that reported the post
1671 // [[ reports would be "this post is flagged as inappropriate"? ]]
1673 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1674 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1675 // users the ability to make posts or comments that they might not otherwise due to not wanting
1676 // the content of the post/comment to be attributed to them.
1678 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1679 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1680 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1681 // user is logged in, and if so display information that otherwise might not be visible to them if they
1682 // aren't apart of a particular school.
1684 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1685 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1686 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1687 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1688 // rendered in the browser.
1690 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1691 // events are as follows.
1694 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1695 // The server recieves the data, assembles a new post object for the database and then fills it with
1696 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1697 // replaced. If the user chose for the post to be private, then public will be set to false and it
1698 // will be filtered from being sent to users not logged into and not having access to the school. Once
1699 // the post has been created and saved into the database, it is sent to all connected users to that
1700 // particular lecture, unless it is private, than only logged in users will get it.
1703 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1704 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1705 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1706 // users unless the post is private, which then it will be only sent to logged in users.
1709 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1710 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1711 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1712 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1713 // bottom of the interface.
1716 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1717 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1718 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1719 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1720 // to the top of the interface. The comment can be anonymous, which then will have the user
1721 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1722 // connected users unless the post is private, then only logged in users will recieve the comment.
1724 var io = require( 'socket.io' ).listen( app );
1726 var Post = mongoose.model( 'Post' );
1728 io.set('authorization', function ( handshake, next ) {
1729 var rawCookie = handshake.headers.cookie;
1731 handshake.cookie = parseCookie(rawCookie);
1732 handshake.sid = handshake.cookie['connect.sid'];
1734 if ( handshake.sid ) {
1735 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1737 handshake.user = false;
1738 return next(null, true);
1740 // bake a new session object for full r/w
1741 handshake.session = new Session( handshake, session );
1743 User.findOne( { session : handshake.sid }, function( err, user ) {
1745 handshake.user = user;
1746 return next(null, true);
1748 handshake.user = false;
1749 return next(null, true);
1757 return next(null, true);
1762 var backchannel = io
1763 .of( '/backchannel' )
1764 .on( 'connection', function( socket ) {
1766 socket.on('subscribe', function(lecture, cb) {
1767 socket.join(lecture);
1768 Post.find({'lecture': lecture}, function(err, posts) {
1769 if (socket.handshake.user) {
1772 var posts = posts.filter(
1783 socket.on('post', function(res) {
1784 var post = new Post;
1785 var _post = res.post;
1786 var lecture = res.lecture;
1787 post.lecture = lecture;
1788 if ( _post.anonymous ) {
1790 post.userName = 'Anonymous';
1791 post.userAffil = 'N/A';
1793 post.userName = _post.userName;
1794 post.userAffil = _post.userAffil;
1797 post.public = _post.public;
1798 post.date = new Date();
1799 post.body = _post.body;
1802 post.save(function(err) {
1804 // XXX some error handling
1808 backchannel.in(lecture).emit('post', post);
1810 privateEmit(lecture, 'post', post);
1816 socket.on('vote', function(res) {
1817 var vote = res.vote;
1818 var lecture = res.lecture;
1819 Post.findById(vote.parentid, function( err, post ) {
1821 if (post.votes.indexOf(vote.userid) == -1) {
1822 post.votes.push(vote.userid);
1823 post.save(function(err) {
1825 // XXX error handling
1828 backchannel.in(lecture).emit('vote', vote);
1830 privteEmit(lecture, 'vote', vote);
1839 socket.on('report', function(res) {
1840 var report = res.report;
1841 var lecture = res.lecture;
1842 Post.findById(report.parentid, function( err, post ){
1844 if (post.reports.indexOf(report.userid) == -1) {
1845 post.reports.push(report.userid);
1846 post.save(function(err) {
1848 // XXX error handling
1851 backchannel.in(lecture).emit('report', report);
1853 privateEmit(lecture, 'report', report);
1862 socket.on('comment', function(res) {
1863 var comment = res.comment;
1864 var lecture = res.lecture;
1865 console.log('anon', comment.anonymous);
1866 if ( comment.anonymous ) {
1868 comment.userName = 'Anonymous';
1869 comment.userAffil = 'N/A';
1871 Post.findById(comment.parentid, function( err, post ) {
1873 post.comments.push(comment);
1874 post.date = new Date();
1875 post.save(function(err) {
1880 backchannel.in(lecture).emit('comment', comment);
1882 privateEmit(lecture, 'comment', comment);
1890 function privateEmit(lecture, event, data) {
1891 backchannel.clients(lecture).forEach(function(socket) {
1892 if (socket.handshake.user)
1893 socket.emit(event, data);
1897 socket.on('disconnect', function() {
1898 //delete clients[socket.id];
1907 .on( 'connection', function( socket ) {
1908 // pull out user/session information etc.
1909 var handshake = socket.handshake;
1910 var userID = handshake.user._id;
1917 socket.on( 'join', function( note ) {
1918 if (handshake.user === false) {
1920 // XXX: replace by addToSet (once it's implemented in mongoose)
1921 Note.findById( noteID, function( err, note ) {
1923 if( note.collaborators.indexOf( userID ) == -1 ) {
1924 note.collaborators.push( userID );
1932 socket.on( 'watch', function( l ) {
1933 var sendCounts = function() {
1936 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1939 function( note, callback ) {
1941 var count = note.collaborators.length;
1947 socket.emit( 'counts', send );
1949 timer = setTimeout( sendCounts, 5000 );
1955 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1956 notes.forEach( function( note ) {
1957 watched.push( note._id );
1964 socket.on( 'disconnect', function() {
1965 clearTimeout( timer );
1967 if (handshake.user === false) {
1968 // XXX: replace with $pull once it's available
1970 Note.findById( noteID, function( err, note ) {
1972 var index = note.collaborators.indexOf( userID );
1975 note.collaborators.splice( index, 1 );
1986 // Exception Catch-All
1988 process.on('uncaughtException', function (e) {
1989 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1995 mongoose.connect( app.set( 'dbUri' ) );
1996 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1998 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
2000 app.listen( serverPort, function() {
2001 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
2003 // if run as root, downgrade to the owner of this file
2004 if (process.getuid() === 0) {
2005 require('fs').stat(__filename, function(err, stats) {
2006 if (err) { return console.log(err); }
2007 process.setuid(stats.uid);
2012 function isValidEmail(email) {
2013 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])?/;
2014 return email.match(re);