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 var io = require( 'socket.io' ).listen( app );
1643 var Post = mongoose.model( 'Post' );
1645 io.set('authorization', function ( handshake, next ) {
1646 var rawCookie = handshake.headers.cookie;
1648 handshake.cookie = parseCookie(rawCookie);
1649 handshake.sid = handshake.cookie['connect.sid'];
1651 if ( handshake.sid ) {
1652 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1654 handshake.user = false;
1655 return next(null, true);
1657 // bake a new session object for full r/w
1658 handshake.session = new Session( handshake, session );
1660 User.findOne( { session : handshake.sid }, function( err, user ) {
1662 handshake.user = user;
1663 return next(null, true);
1665 handshake.user = false;
1666 return next(null, true);
1674 return next(null, true);
1679 var backchannel = io
1680 .of( '/backchannel' )
1681 .on( 'connection', function( socket ) {
1683 socket.on('subscribe', function(lecture, cb) {
1684 socket.join(lecture);
1685 Post.find({'lecture': lecture}, function(err, posts) {
1686 if (socket.handshake.user) {
1689 var posts = posts.filter(
1700 socket.on('post', function(res) {
1701 var post = new Post;
1702 var _post = res.post;
1703 var lecture = res.lecture;
1704 post.lecture = lecture;
1705 if ( _post.anonymous ) {
1707 post.userName = 'Anonymous';
1708 post.userAffil = 'N/A';
1710 post.userName = _post.userName;
1711 post.userAffil = _post.userAffil;
1714 post.public = _post.public;
1715 post.date = new Date();
1716 post.body = _post.body;
1719 post.save(function(err) {
1721 // XXX some error handling
1725 backchannel.in(lecture).emit('post', post);
1727 privateEmit(lecture, 'post', post);
1733 socket.on('vote', function(res) {
1734 var vote = res.vote;
1735 var lecture = res.lecture;
1736 Post.findById(vote.parentid, function( err, post ) {
1738 if (post.votes.indexOf(vote.userid) == -1) {
1739 post.votes.push(vote.userid);
1740 post.save(function(err) {
1742 // XXX error handling
1745 backchannel.in(lecture).emit('vote', vote);
1747 privteEmit(lecture, 'vote', vote);
1756 socket.on('report', function(res) {
1757 var report = res.report;
1758 var lecture = res.lecture;
1759 Post.findById(report.parentid, function( err, post ){
1761 if (post.reports.indexOf(report.userid) == -1) {
1762 post.reports.push(report.userid);
1763 post.save(function(err) {
1765 // XXX error handling
1768 backchannel.in(lecture).emit('report', report);
1770 privateEmit(lecture, 'report', report);
1779 socket.on('comment', function(res) {
1780 var comment = res.comment;
1781 var lecture = res.lecture;
1782 console.log('anon', comment.anonymous);
1783 if ( comment.anonymous ) {
1785 comment.userName = 'Anonymous';
1786 comment.userAffil = 'N/A';
1788 Post.findById(comment.parentid, function( err, post ) {
1790 post.comments.push(comment);
1791 post.date = new Date();
1792 post.save(function(err) {
1797 backchannel.in(lecture).emit('comment', comment);
1799 privateEmit(lecture, 'comment', comment);
1807 function privateEmit(lecture, event, data) {
1808 backchannel.clients(lecture).forEach(function(socket) {
1809 if (socket.handshake.user)
1810 socket.emit(event, data);
1814 socket.on('disconnect', function() {
1815 //delete clients[socket.id];
1824 .on( 'connection', function( socket ) {
1825 // pull out user/session information etc.
1826 var handshake = socket.handshake;
1827 var userID = handshake.user._id;
1834 socket.on( 'join', function( note ) {
1835 if (handshake.user === false) {
1837 // XXX: replace by addToSet (once it's implemented in mongoose)
1838 Note.findById( noteID, function( err, note ) {
1840 if( note.collaborators.indexOf( userID ) == -1 ) {
1841 note.collaborators.push( userID );
1849 socket.on( 'watch', function( l ) {
1850 var sendCounts = function() {
1853 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1856 function( note, callback ) {
1858 var count = note.collaborators.length;
1864 socket.emit( 'counts', send );
1866 timer = setTimeout( sendCounts, 5000 );
1872 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1873 notes.forEach( function( note ) {
1874 watched.push( note._id );
1881 socket.on( 'disconnect', function() {
1882 clearTimeout( timer );
1884 if (handshake.user === false) {
1885 // XXX: replace with $pull once it's available
1887 Note.findById( noteID, function( err, note ) {
1889 var index = note.collaborators.indexOf( userID );
1892 note.collaborators.splice( index, 1 );
1903 // Exception Catch-All
1905 process.on('uncaughtException', function (e) {
1906 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1912 mongoose.connect( app.set( 'dbUri' ) );
1913 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1915 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1917 app.listen( serverPort, function() {
1918 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1920 // if run as root, downgrade to the owner of this file
1921 if (process.getuid() === 0) {
1922 require('fs').stat(__filename, function(err, stats) {
1923 if (err) { return console.log(err); }
1924 process.setuid(stats.uid);
1929 function isValidEmail(email) {
1930 var re = /[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
1931 return email.match(re);