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 ) {
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 ) {
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.
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 ) {
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 ) {
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( '/login' );
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;
504 // This tells async (the module) that each iteration of forEach is
505 // done and will continue to call the rest until they have all been
506 // completed, at which time the last function below will be called.
511 // After all schools and courses have been found, render them
513 res.render( 'schools', { 'schools' : schools } );
517 // If no schools have been found, display none
518 res.render( 'schools', { 'schools' : [] } );
524 // Displays form to create new course
525 // Private, requires user to be authorized
526 app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
527 // Load school from middleware
528 var school = req.school;
530 // If school was not loaded for whatever reason, or the user is not authorized
531 // then redirect to the main schools page.
532 if( ( ! school ) || ( ! school.authorized ) ) {
533 return res.redirect( '/schools' );
536 // If they are authorized and the school exists, then render the page
537 res.render( 'course/new', { 'school': school } );
540 // Recieves new course form
541 app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
542 var school = req.school;
543 // Creates new course from Course Schema
544 var course = new Course;
545 // Gathers instructor information from form
546 var instructorEmail = req.body.email.toLowerCase();
547 var instructorName = req.body.instructorName;
549 // If school doesn't exist or user is not authorized redirect to main schools page
550 if( ( ! school ) || ( ! school.authorized ) ) {
551 res.redirect( '/schools' );
554 // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
555 if ( !instructorEmail || !instructorName ) {
556 req.flash( 'error', 'Invalid parameters!' )
557 return res.render( 'course/new' );
560 // Fill out the course with information from the form
561 course.number = req.body.number;
562 course.name = req.body.name;
563 course.description = req.body.description;
564 course.school = school._id;
565 course.creator = req.user._id;
566 course.subject = req.body.subject;
567 course.department = req.body.department;
569 // Check if a user exists with the instructorEmail, if not then create
570 // a new user and send them an instructor welcome email.
571 User.findOne( { 'email' : instructorEmail }, function( err, user ) {
575 user.name = instructorName
576 user.email = instructorEmail;
577 user.affil = 'Instructor';
578 user.school = school.name;
580 user.activated = false;
582 // Validate instructorEmail
583 // XXX Probably could be done before checking db
584 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
585 req.flash( 'error', 'Please enter a valid email' );
586 // XXX This needs to be fixed, this is not the proper flow
587 return res.redirect( '/register' );
589 // Once the new user information has been completed, save the user
590 // to the database then email them the instructor welcome email.
591 user.save(function( err ) {
592 // If there was an error saving the instructor, prompt the user to fill out
593 // the information again.
595 req.flash( 'error', 'Invalid parameters!' )
596 return res.render( 'course/new' );
601 'subject' : 'A non-profit open education initiative',
603 'template' : 'instructorInvite',
608 'serverHost' : serverHost
612 mailer.send( message, function( err, result ) {
614 console.log( 'Error inviting instructor to course!' );
616 console.log( 'Successfully invited instructor to course.' );
620 // After emails are sent, set the courses instructor to the
621 // new users id and then save the course to the database.
622 course.instructor = user._id;
623 course.save( function( err ) {
625 // XXX better validation
626 req.flash( 'error', 'Invalid parameters!' );
628 return res.render( 'course/new' );
630 // Once the course has been completed email the admin with information
631 // on the course and new instructor
635 'subject' : school.name+' has a new course: '+course.name,
637 'template' : 'newCourse',
642 'serverHost' : serverHost
646 mailer.send( message, function( err, result ) {
648 console.log( 'Error sending new course email to info@finalsclub.org' )
650 console.log( 'Successfully invited instructor to course')
653 // Redirect the user to the schools page where they can see
655 // XXX Redirect to the new course instead
656 res.redirect( '/schools' );
662 // If the user exists, then check if they are already and instructor
663 if (user.affil === 'Instructor') {
664 // If they are an instructor, then save the course with the appropriate
665 // information and email the admin.
666 course.instructor = user._id;
667 course.save( function( err ) {
669 // XXX better validation
670 req.flash( 'error', 'Invalid parameters!' );
672 return res.render( 'course/new' );
677 'subject' : school.name+' has a new course: '+course.name,
679 'template' : 'newCourse',
684 'serverHost' : serverHost
688 mailer.send( message, function( err, result ) {
690 console.log( 'Error sending new course email to info@finalsclub.org' )
692 console.log( 'Successfully invited instructor to course')
695 // XXX Redirect to the new course instead
696 res.redirect( '/schools' );
700 // The existing user isn't an instructor, so the user is notified of the error
701 // and the course isn't created.
702 req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
703 res.render( 'course/new' );
709 // Individual Course Listing
710 // Public with private information
711 app.get( '/course/:id', loadUser, loadCourse, function( req, res ) {
712 var userId = req.user._id;
713 var course = req.course;
715 // Check if the user is subscribed to the course
716 // XXX Not currently used for anything
717 var subscribed = course.subscribed( userId );
719 // Find lectures associated with this course and sort by name
720 Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
721 // Get course instructor information using their id
722 User.findById( course.instructor, function( err, instructor ) {
723 // Render course and lectures
724 res.render( 'course/index', { 'course' : course, 'instructor': instructor, 'subscribed' : subscribed, 'lectures' : lectures } );
730 // XXX Non functioning
731 // Will be used as apart of a larger admin interface for managing courses and
732 // lectures on the site.
733 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
734 var course = req.course;
738 course.delete(function( err ) {
739 if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
740 else req.flash( 'info', 'Successfully removed course' )
741 res.redirect( '/schools' );
744 req.flash( 'error', 'You don\'t have permission to do that' )
745 res.redirect( '/schools' );
749 // Subscribe to course
750 // XXX Not currently used for anything
751 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
752 var course = req.course;
753 var userId = req.user._id;
755 course.subscribe( userId, function( err ) {
757 req.flash( 'error', 'Error subscribing to course!' );
760 res.redirect( '/course/' + course._id );
764 // Unsubscribe from course
765 // XXX Not currently used for anything
766 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
767 var course = req.course;
768 var userId = req.user._id;
770 course.unsubscribe( userId, function( err ) {
772 req.flash( 'error', 'Error unsubscribing from course!' );
775 res.redirect( '/course/' + course._id );
779 // Create new lecture
780 app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
781 var courseId = req.params.id;
782 var course = req.course;
785 // If course isn't valid or user isn't authorized for course, redirect
786 if( ( ! course ) || ( ! course.authorized ) ) {
787 return res.redirect( '/course/' + courseId );
790 // Render new lecture form
791 res.render( 'lecture/new', { 'lecture' : lecture } );
794 // Recieve New Lecture Form
795 app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
796 var courseId = req.params.id;
797 var course = req.course;
798 // Create new lecture from Lecture schema
799 var lecture = new Lecture;
801 if( ( ! course ) || ( ! course.authorized ) ) {
802 res.redirect( '/course/' + courseId );
807 // Populate lecture with form data
808 lecture.name = req.body.name;
809 lecture.date = req.body.date;
810 lecture.course = course._id;
811 lecture.creator = req.user._id;
813 // Save lecture to database
814 lecture.save( function( err ) {
816 // XXX better validation
817 req.flash( 'error', 'Invalid parameters!' );
819 res.render( 'lecture/new', { 'lecture' : lecture } );
821 // XXX Redirect to new lecture instead
822 res.redirect( '/course/' + course._id );
828 // Display individual lecture and related notes
829 app.get( '/lecture/:id', loadUser, loadLecture, function( req, res ) {
830 var lecture = req.lecture;
832 // Grab the associated course
833 // XXX this should be done with DBRefs eventually
834 Course.findById( lecture.course, function( err, course ) {
836 // If course is found, find instructor information to be displayed on page
837 User.findById( course.instructor, function( err, instructor ) {
838 // Pull out our notes
839 Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
840 if ( !req.user.loggedIn || !req.lecture.authorized ) {
841 // Loop through notes and only return those that are public if the
842 // user is not logged in or not authorized for that lecture
843 notes = notes.filter(function( note ) {
844 if ( note.public ) return note;
847 // Render lecture and notes
848 res.render( 'lecture/index', {
851 'instructor' : instructor,
855 'javascripts' : [ 'counts.js' ]
860 // XXX with DBRefs we will be able to reassign orphaned courses/lecture/pads
862 req.flash( 'error', 'That lecture is orphaned!' );
869 // Display new note form
870 app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
871 var lectureId = req.params.id;
872 var lecture = req.lecture;
875 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
876 res.redirect( '/lecture/' + lectureId );
881 res.render( 'notes/new', { 'note' : note } );
884 // Recieve new note form
885 app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
886 var lectureId = req.params.id;
887 var lecture = req.lecture;
889 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
890 res.redirect( '/lecture/' + lectureId );
895 // Create note from Note schema
898 // Populate note from form data
899 note.name = req.body.name;
900 note.date = req.body.date;
901 note.lecture = lecture._id;
902 note.public = req.body.private ? false : true;
903 note.creator = req.user._id;
905 // Save note to database
906 note.save( function( err ) {
908 // XXX better validation
909 req.flash( 'error', 'Invalid parameters!' );
911 res.render( 'notes/new', { 'note' : note } );
913 // XXX Redirect to new note instead
914 res.redirect( '/lecture/' + lecture._id );
920 // Display individual note page
921 app.get( '/note/:id', loadUser, loadNote, function( req, res ) {
923 // Set read only id for etherpad-lite or false for later check
924 var roID = note.roID || false;
926 var lectureId = note.lecture;
928 // Count the amount of visits, but only once per session
929 if ( req.session.visited ) {
930 if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
931 req.session.visited.push( note._id );
935 req.session.visited = [];
936 req.session.visited.push( note._id );
940 // If a read only id exists process note
944 // If read only id doesn't, then fetch the read only id from the database and then
946 // XXX Soon to be depracated due to a new API in etherpad that makes for a
947 // much cleaner solution.
948 db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
949 epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
951 roID = record.value.replace(/"/g, '');
960 function processReq() {
962 Lecture.findById( lectureId, function( err, lecture ) {
964 req.flash( 'error', 'That notes page is orphaned!' );
968 // Find notes based on lecture id, which will be displayed in a dropdown
970 Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
972 // User is logged in and sees full notepad
974 res.render( 'notes/index', {
975 'layout' : 'noteLayout',
979 'otherNotes' : otherNotes,
982 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
983 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
986 // User is not logged in and sees notepad that is public
987 res.render( 'notes/public', {
988 'layout' : 'noteLayout',
991 'otherNotes' : otherNotes,
994 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
995 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1003 // Static pages and redirects
1004 app.get( '/about', loadUser, function( req, res ) {
1005 res.redirect( 'http://blog.finalsclub.org/about.html' );
1008 app.get( '/press', loadUser, function( req, res ) {
1009 res.render( 'static/press' );
1012 app.get( '/conduct', loadUser, function( req, res ) {
1013 res.render( 'static/conduct' );
1016 app.get( '/legal', loadUser, function( req, res ) {
1017 res.redirect( 'http://blog.finalsclub.org/legal.html' );
1020 app.get( '/contact', loadUser, function( req, res ) {
1021 res.redirect( 'http://blog.finalsclub.org/contact.html' );
1024 app.get( '/privacy', loadUser, function( req, res ) {
1025 res.render( 'static/privacy' );
1029 // Authentication routes
1030 // These are used for logging in, logging out, registering
1031 // and other user authentication purposes
1033 // Render login page
1034 app.get( '/login', function( req, res ) {
1035 log3("get login page")
1037 res.render( 'login' );
1040 // Recieve login form
1041 app.post( '/login', function( req, res ) {
1042 var email = req.body.email;
1043 var password = req.body.password;
1044 log3("post login ...")
1046 // Find user from email
1047 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1051 // If user exists, check if activated, if not notify them and send them to
1054 if( ! user.activated ) {
1055 // (undocumented) markdown-esque link functionality in req.flash
1056 req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
1058 req.session.activateCode = user._id;
1060 res.render( 'login' );
1062 // If user is activated, check if their password is correct
1063 if( user.authenticate( password ) ) {
1066 var sid = req.sessionID;
1070 // Set the session then save the user to the database
1071 user.save( function() {
1072 var redirect = req.session.redirect;
1074 // login complete, remember the user's email for next time
1075 req.session.email = email;
1077 // alert the successful login
1078 req.flash( 'info', 'Successfully logged in!' );
1080 // redirect to profile if we don't have a stashed request
1081 res.redirect( redirect || '/profile' );
1084 // Notify user of bad login
1085 req.flash( 'error', 'Invalid login!' );
1087 res.render( 'login' );
1091 // Notify user of bad login
1093 req.flash( 'error', 'Invalid login!' );
1095 res.render( 'login' );
1100 // Request reset password
1101 app.get( '/resetpw', function( req, res ) {
1102 log3("get resetpw page");
1103 res.render( 'resetpw' );
1106 // Display reset password from requested email
1107 app.get( '/resetpw/:id', function( req, res ) {
1108 var resetPassCode = req.params.id
1109 res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
1112 // Recieve reset password request form
1113 app.post( '/resetpw', function( req, res ) {
1114 log3("post resetpw");
1115 var email = req.body.email
1119 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1122 // If user exists, create reset code
1123 var resetPassCode = hat(64);
1124 user.setResetPassCode(resetPassCode);
1126 // Construct url that the user can then click to reset password
1127 var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1129 // Save user to database
1130 user.save( function( err ) {
1131 log3('save '+user.email);
1133 // Construct email and send it to the user
1137 'subject' : 'Your FinalsClub.org Password has been Reset!',
1139 'template' : 'userPasswordReset',
1141 'resetPassCode' : resetPassCode,
1142 'resetPassUrl' : resetPassUrl
1146 mailer.send( message, function( err, result ) {
1148 // XXX: Add route to resend this email
1150 console.log( 'Error sending user password reset email!' );
1152 console.log( 'Successfully sent user password reset email.' );
1157 // Render request success page
1158 res.render( 'resetpw-success', { 'email' : email } );
1162 res.render( 'resetpw-error', { 'email' : email } );
1167 // Recieve reset password form
1168 app.post( '/resetpw/:id', function( req, res ) {
1169 log3("post resetpw.code");
1170 var resetPassCode = req.params.id
1171 var email = req.body.email
1172 var pass1 = req.body.pass1
1173 var pass2 = req.body.pass2
1175 // Find user by email
1176 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1178 // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1179 // save user with new password and display success message.
1181 var valid = user.resetPassword(resetPassCode, pass1, pass2);
1183 user.save( function( err ) {
1184 res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );
1189 // If there was a problem, notify user
1191 res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
1196 // Display registration page
1197 app.get( '/register', function( req, res ) {
1198 log3("get reg page");
1200 // Populate school dropdown list
1201 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1202 res.render( 'register', { 'schools' : schools } );
1206 // Recieve registration form
1207 app.post( '/register', function( req, res ) {
1208 var sid = req.sessionId;
1210 // Create new user from User schema
1211 var user = new User;
1213 // Populate user from form
1214 user.email = req.body.email.toLowerCase();
1215 user.password = req.body.password;
1217 // If school is set to other, then fill in school as what the
1219 user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1220 user.name = req.body.name;
1221 user.affil = req.body.affil;
1222 user.activated = false;
1225 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1226 req.flash( 'error', 'Please enter a valid email' );
1227 return res.redirect( '/register' );
1230 // Check if password is greater than 6 characters, otherwise notify user
1231 if ( req.body.password.length < 6 ) {
1232 req.flash( 'error', 'Please enter a password longer than eight characters' );
1233 return res.redirect( '/register' );
1236 // Pull out hostname from email
1237 var hostname = user.email.split( '@' ).pop();
1239 // Check if email is from one of the special domains
1240 if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
1244 // Save user to database
1245 user.save( function( err ) {
1246 // If error, check if it is because the user already exists, if so
1247 // get the user information and let them know
1249 if( /dup key/.test( err.message ) ) {
1250 // attempting to register an existing address
1251 User.findOne({ 'email' : user.email }, function(err, result ) {
1252 if (result.activated) {
1253 // If activated, make sure they know how to contact the admin
1254 req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
1255 return res.redirect( '/register' )
1257 // If not activated, direct them to the resendActivation page
1258 req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
1259 return res.redirect( '/resendActivation' )
1263 // If any other type of error, prompt them to enter the registration again
1264 req.flash( 'error', 'An error occurred during registration.' );
1266 return res.redirect( '/register' );
1269 // send user activation email
1270 sendUserActivation( user );
1272 // Check if the hostname matches any in the approved schools
1273 School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1275 // If there is a match, send associated welcome message
1276 sendUserWelcome( user, true );
1277 log3('school recognized '+school.name);
1278 // If no users exist for the school, create empty array
1279 if (!school.users) school.users = [];
1280 // Add user to the school
1281 school.users.push( user._id );
1283 // Save school to the database
1284 school.save( function( err ) {
1285 log3('school.save() done');
1286 // Notify user that they have been added to the school
1287 req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
1288 res.redirect( '/' );
1290 // Construct admin email about user registration
1294 'subject' : 'FC User Registration : User added to ' + school.name,
1296 'template' : 'userSchool',
1302 // If there isn't a match, send associated welcome message
1303 sendUserWelcome( user, false );
1304 // Tell user to check for activation link
1305 req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
1306 res.redirect( '/' );
1307 // Construct admin email about user registration
1311 'subject' : 'FC User Registration : Email did not match any schools',
1313 'template' : 'userNoSchool',
1319 // Send email to admin
1320 mailer.send( message, function( err, result ) {
1323 console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1325 console.log( 'Successfully sent user has no school email to admin.' );
1335 // Display resendActivation request page
1336 app.get( '/resendActivation', function( req, res ) {
1337 var activateCode = req.session.activateCode;
1339 // Check if user exists by activateCode set in their session
1340 User.findById( activateCode, function( err, user ) {
1341 if( ( ! user ) || ( user.activated ) ) {
1342 res.redirect( '/' );
1344 // Send activation and redirect to login
1345 sendUserActivation( user );
1347 req.flash( 'info', 'Your activation code has been resent.' );
1349 res.redirect( '/login' );
1354 // Display activation page
1355 app.get( '/activate/:code', function( req, res ) {
1356 var code = req.params.code;
1358 // XXX could break this out into a middleware
1360 res.redirect( '/' );
1363 // Find user by activation code
1364 User.findById( code, function( err, user ) {
1365 if( err || ! user ) {
1366 // If not found, notify user of invalid code
1367 req.flash( 'error', 'Invalid activation code!' );
1369 res.redirect( '/' );
1371 // If valid, then activate user
1372 user.activated = true;
1374 // Regenerate our session and log in as the new user
1375 req.session.regenerate( function() {
1376 user.session = req.sessionID;
1378 // Save user to database
1379 user.save( function( err ) {
1381 req.flash( 'error', 'Unable to activate account.' );
1383 res.redirect( '/' );
1385 req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
1387 res.redirect( '/profile' );
1396 app.get( '/logout', function( req, res ) {
1397 var sid = req.sessionID;
1399 // Find user by session id
1400 User.findOne( { 'session' : sid }, function( err, user ) {
1402 // Empty out session id
1405 // Save user to database
1406 user.save( function( err ) {
1407 res.redirect( '/' );
1410 res.redirect( '/' );
1415 // Display users profile page
1416 app.get( '/profile', loadUser, loggedIn, function( req, res ) {
1417 var user = req.user;
1419 res.render( 'profile/index', { 'user' : user } );
1422 // Recieve profile edit page form
1423 app.post( '/profile', loadUser, loggedIn, function( req, res ) {
1424 var user = req.user;
1425 var fields = req.body;
1428 var wasComplete = user.isComplete;
1430 if( ! fields.name ) {
1431 req.flash( 'error', 'Please enter a valid name!' );
1435 user.name = fields.name;
1438 if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1439 req.flash( 'error', 'Please select a valid affiliation!' );
1443 user.affil = fields.affiliation;
1446 if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1447 // changing password
1448 if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1449 if( fields.newPassword === fields.newPasswordConfirm ) {
1450 // test password strength?
1452 user.password = fields.newPassword;
1454 req.flash( 'error', 'Mismatch in new password!' );
1459 req.flash( 'error', 'Please supply your existing password.' );
1465 user.major = fields.major;
1466 user.bio = fields.bio;
1468 user.showName = ( fields.showName ? true : false );
1471 user.save( function( err ) {
1473 req.flash( 'error', 'Unable to save user profile!' );
1475 if( ( user.isComplete ) && ( ! wasComplete ) ) {
1476 req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
1478 res.redirect( '/' );
1480 res.render( 'info', 'Your profile was successfully updated!' );
1482 res.render( 'profile/index', { 'user' : user } );
1487 res.render( 'profile/index', { 'user' : user } );
1494 function loadSubject( req, res, next ) {
1495 if( url.parse( req.url ).pathname.match(/subject/) ) {
1496 ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1498 req.flash( 'error', 'Subject with this ID does not exist' )
1499 res.redirect( '/archive' );
1501 req.subject = subject;
1510 function loadOldCourse( req, res, next ) {
1511 if( url.parse( req.url ).pathname.match(/course/) ) {
1512 ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1514 req.flash( 'error', 'Course with this ID does not exist' )
1515 res.redirect( '/archive' );
1517 req.course = course;
1526 var featuredCourses = [
1527 {name: 'The Human Mind', 'id': 1563},
1528 {name: 'Justice', 'id': 797},
1529 {name: 'Protest Literature', 'id': 1681},
1530 {name: 'Animal Cognition', 'id': 681},
1531 {name: 'Positive Psychology', 'id': 1793},
1532 {name: 'Social Psychology', 'id': 660},
1533 {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1534 {name: 'Cyberspace in Court', 'id': 1446},
1535 {name: 'Nazi Cinema', 'id': 2586},
1536 {name: 'Media and the American Mind', 'id': 2583},
1537 {name: 'Social Thought in Modern America', 'id': 2585},
1538 {name: 'Major British Writers II', 'id': 869},
1539 {name: 'Civil Procedure', 'id': 2589},
1540 {name: 'Evidence', 'id': 2590},
1541 {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1544 app.get( '/learn', loadUser, function( req, res ) {
1545 res.render( 'archive/learn', { 'courses' : featuredCourses } );
1548 app.get( '/learn/random', loadUser, function( req, res ) {
1549 res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
1552 app.get( '/archive', loadUser, function( req, res ) {
1553 ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1555 req.flash( 'error', 'There was a problem gathering the archived courses, please try again later.' );
1556 res.redirect( '/' );
1558 res.render( 'archive/index', { 'subjects' : subjects } );
1563 app.get( '/archive/subject/:id', loadUser, loadSubject, function( req, res ) {
1564 ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1566 req.flash( 'error', 'There are no archived courses' );
1567 res.redirect( '/' );
1569 res.render( 'archive/courses', { 'courses' : courses, 'subject': req.subject } );
1574 app.get( '/archive/course/:id', loadUser, loadOldCourse, function( req, res ) {
1575 ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1577 req.flash( 'error', 'There are no notes in this course' );
1578 res.redirect( '/archive' );
1580 res.render( 'archive/notes', { 'notes' : notes, 'course' : req.course } );
1585 app.get( '/archive/note/:id', loadUser, function( req, res ) {
1586 ArchivedNote.findById(req.params.id, function(err, note) {
1588 req.flash( 'error', 'This is not a valid id for a note' );
1589 res.redirect( '/archive' );
1591 ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1593 req.flash( 'error', 'There is no course for this note' )
1594 res.redirect( '/archive' )
1596 res.render( 'archive/note', { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
1605 var io = require( 'socket.io' ).listen( app );
1607 var Post = mongoose.model( 'Post' );
1609 io.set('authorization', function ( handshake, next ) {
1610 var rawCookie = handshake.headers.cookie;
1612 handshake.cookie = parseCookie(rawCookie);
1613 handshake.sid = handshake.cookie['connect.sid'];
1615 if ( handshake.sid ) {
1616 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1618 handshake.user = false;
1619 return next(null, true);
1621 // bake a new session object for full r/w
1622 handshake.session = new Session( handshake, session );
1624 User.findOne( { session : handshake.sid }, function( err, user ) {
1626 handshake.user = user;
1627 return next(null, true);
1629 handshake.user = false;
1630 return next(null, true);
1638 return next(null, true);
1643 var backchannel = io
1644 .of( '/backchannel' )
1645 .on( 'connection', function( socket ) {
1647 socket.on('subscribe', function(lecture, cb) {
1648 socket.join(lecture);
1649 Post.find({'lecture': lecture}, function(err, posts) {
1650 if (socket.handshake.user) {
1653 var posts = posts.filter(
1664 socket.on('post', function(res) {
1665 var post = new Post;
1666 var _post = res.post;
1667 var lecture = res.lecture;
1668 post.lecture = lecture;
1669 if ( _post.anonymous ) {
1671 post.userName = 'Anonymous';
1672 post.userAffil = 'N/A';
1674 post.userName = _post.userName;
1675 post.userAffil = _post.userAffil;
1678 post.public = _post.public;
1679 post.date = new Date();
1680 post.body = _post.body;
1683 post.save(function(err) {
1685 // XXX some error handling
1689 backchannel.in(lecture).emit('post', post);
1691 privateEmit(lecture, 'post', post);
1697 socket.on('vote', function(res) {
1698 var vote = res.vote;
1699 var lecture = res.lecture;
1700 Post.findById(vote.parentid, function( err, post ) {
1702 if (post.votes.indexOf(vote.userid) == -1) {
1703 post.votes.push(vote.userid);
1704 post.save(function(err) {
1706 // XXX error handling
1709 backchannel.in(lecture).emit('vote', vote);
1711 privteEmit(lecture, 'vote', vote);
1720 socket.on('report', function(res) {
1721 var report = res.report;
1722 var lecture = res.lecture;
1723 Post.findById(report.parentid, function( err, post ){
1725 if (post.reports.indexOf(report.userid) == -1) {
1726 post.reports.push(report.userid);
1727 post.save(function(err) {
1729 // XXX error handling
1732 backchannel.in(lecture).emit('report', report);
1734 privateEmit(lecture, 'report', report);
1743 socket.on('comment', function(res) {
1744 var comment = res.comment;
1745 var lecture = res.lecture;
1746 console.log('anon', comment.anonymous);
1747 if ( comment.anonymous ) {
1749 comment.userName = 'Anonymous';
1750 comment.userAffil = 'N/A';
1752 Post.findById(comment.parentid, function( err, post ) {
1754 post.comments.push(comment);
1755 post.date = new Date();
1756 post.save(function(err) {
1761 backchannel.in(lecture).emit('comment', comment);
1763 privateEmit(lecture, 'comment', comment);
1771 function privateEmit(lecture, event, data) {
1772 backchannel.clients(lecture).forEach(function(socket) {
1773 if (socket.handshake.user)
1774 socket.emit(event, data);
1778 socket.on('disconnect', function() {
1779 //delete clients[socket.id];
1788 .on( 'connection', function( socket ) {
1789 // pull out user/session information etc.
1790 var handshake = socket.handshake;
1791 var userID = handshake.user._id;
1798 socket.on( 'join', function( note ) {
1799 if (handshake.user === false) {
1801 // XXX: replace by addToSet (once it's implemented in mongoose)
1802 Note.findById( noteID, function( err, note ) {
1804 if( note.collaborators.indexOf( userID ) == -1 ) {
1805 note.collaborators.push( userID );
1813 socket.on( 'watch', function( l ) {
1814 var sendCounts = function() {
1817 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1820 function( note, callback ) {
1822 var count = note.collaborators.length;
1828 socket.emit( 'counts', send );
1830 timer = setTimeout( sendCounts, 5000 );
1836 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1837 notes.forEach( function( note ) {
1838 watched.push( note._id );
1845 socket.on( 'disconnect', function() {
1846 clearTimeout( timer );
1848 if (handshake.user === false) {
1849 // XXX: replace with $pull once it's available
1851 Note.findById( noteID, function( err, note ) {
1853 var index = note.collaborators.indexOf( userID );
1856 note.collaborators.splice( index, 1 );
1867 // Exception Catch-All
1869 process.on('uncaughtException', function (e) {
1870 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1876 mongoose.connect( app.set( 'dbUri' ) );
1877 mongoose.connection.db.serverConfig.connection.autoReconnect = true
1879 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1881 app.listen( serverPort, function() {
1882 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1884 // if run as root, downgrade to the owner of this file
1885 if (process.getuid() === 0) {
1886 require('fs').stat(__filename, function(err, stats) {
1887 if (err) { return console.log(err); }
1888 process.setuid(stats.uid);
1893 function isValidEmail(email) {
1894 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])?/;
1895 return email.match(re);