3 // This file consists of the main webserver for FinalsClub.org
4 // and is split between a standard CRUD style webserver and
5 // a websocket based realtime webserver.
7 // A note on house keeping: Anything with XXX is marked
8 // as such because it should be looked at and possibly
9 // revamped or removed depending on circumstances.
12 var sys = require( 'sys' );
13 var os = require( 'os' );
14 var url = require( 'url' );
15 var express = require( 'express' );
16 var mongoStore = require( 'connect-mongo' );
17 var async = require( 'async' );
18 var db = require( './db.js' );
19 var mongoose = require( './models.js' ).mongoose;
20 var Mailer = require( './mailer.js' );
21 var hat = require('hat');
22 var connect = require( 'connect' );
23 var Session = connect.middleware.session.Session;
24 var parseCookie = connect.utils.parseCookie;
25 var Backchannel = require('./bc/backchannel');
27 // ********************************
28 // For facebook oauth and connect
29 // ********************************
30 var everyauth = require('everyauth');
31 var FacebookClient = require('facebook-client').FacebookClient;
32 var facebook = new FacebookClient();
34 everyauth.debug = true;
35 everyauth.everymodule.logoutPath('/bye');
37 // configure facebook authentication
39 .appId('foobieblechreplacethisXXX')
40 .appSecret('foobiederpreplacethisXXX')
41 .myHostname('http://localhost:8000')
44 .redirectPath('/schools')
45 .findOrCreateUser(function(session, accessToken, accessTokExtra, fbUserMetadata, req) {
46 console.log('req.session');
47 console.log(req.session);
48 var userPromise = this.Promise();
49 User.findOne( {'email': fbUserMetadata.email }, function( err, euser ) {
50 console.log("Found a fc user for this fb email");
51 if (err) return userPromise.fail(err);
52 // if a user exists with that email, call them logged in
53 // FIXME: change this to different query on 'fbid'
55 //hsession = new Session( handshake, session );
56 // save thhat this cookie/session-id is right for this user
57 req.session.regenerate( function() {
58 euser.session = req.sessionID;
60 console.log( req.sessionID );
64 if (euser) return userPromise.fulfill(euser);
68 //.callbackPath('/fbsucc')
74 // Used for initial testing
75 var log3 = function() {}
78 var app = module.exports = express.createServer();
80 // Load Mongoose Schemas
81 // The actual schemas are located in models.j
82 var User = mongoose.model( 'User' );
83 var School = mongoose.model( 'School' );
84 var Course = mongoose.model( 'Course' );
85 var Lecture = mongoose.model( 'Lecture' );
86 var Note = mongoose.model( 'Note' );
88 // More schemas used for legacy data
89 var ArchivedCourse = mongoose.model( 'ArchivedCourse' );
90 var ArchivedNote = mongoose.model( 'ArchivedNote' );
91 var ArchivedSubject = mongoose.model( 'ArchivedSubject' );
93 // XXX Not sure if necessary
94 var ObjectId = mongoose.SchemaTypes.ObjectId;
97 // Use the environment variable DEV_EMAIL for testing
98 var ADMIN_EMAIL = process.env.DEV_EMAIL || 'info@finalsclub.org';
100 // Set server hostname and port from environment variables,
101 // then check if set.
102 // XXX Can be cleaned up
103 var serverHost = process.env.SERVER_HOST;
104 var serverPort = process.env.SERVER_PORT;
107 console.log( 'Using server hostname defined in environment: %s', serverHost );
109 serverHost = os.hostname();
110 console.log( 'No hostname defined, defaulting to os.hostname(): %s', serverHost );
113 // Express configuration depending on environment
114 // development is intended for developing locally or
115 // when not in production, otherwise production is used
116 // when the site will be run live for regular usage.
117 app.configure( 'development', function() {
118 // In development mode, all errors and stack traces will be
119 // dumped to the console and on page for easier troubleshooting
121 app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
123 // Set database connection information from environment
124 // variables otherwise use localhost.
125 app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
126 app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
128 // Set Amazon access and secret keys from environment
129 // variables. These keys are intended to be secret, so
130 // are not included in the source code, but set on the server
132 app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
133 app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
135 // If a port wasn't set earlier, set to 3000
141 // Production configuration settings
142 app.configure( 'production', function() {
143 // At the moment we have errors outputting everything
144 // so if there are any issues it is easier to track down.
145 // Once the site is more stable it will be prudent to
146 // use less error tracing.
147 app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
149 // Disable view cache due to stale views.
150 // XXX Disable view caching temp
151 app.disable( 'view cache' )
153 // Against setting the database connection information
154 // XXX Can be cleaned up or combined
155 app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
156 app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
158 // XXX Can be cleaned up or combined
159 app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
160 app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
162 // Set to port 80 if not set through environment variables
168 // General Express configuration settings
169 app.configure(function(){
170 // Views are rendered from public/index.html and main.js except for the pad that surrounds EPL and BC
171 // FIXME: make all views exist inside of public/index.html
172 // Views are housed in the views folder
173 app.set( 'views', __dirname + '/views' );
174 // All templates use jade for rendering
175 app.set( 'view engine', 'jade' );
177 // Bodyparser is required to handle form submissions
178 // without manually parsing them.
179 app.use( express.bodyParser() );
181 app.use( express.cookieParser() );
183 // Sessions are stored in mongodb which allows them
184 // to be persisted even between server restarts.
185 app.set( 'sessionStore', new mongoStore( {
187 'url' : app.set( 'dbUri' )
190 // This is where the actual Express session handler
191 // is defined, with a mongoStore being set as the
192 // session storage versus in memory storage that is
194 app.use( express.session( {
195 // A secret 'password' for encrypting and decrypting
197 // XXX Should be handled differently
198 'secret' : 'finalsclub',
199 // The max age of the cookies that is allowed
200 // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) * 1000 (milliseconds)
201 'maxAge' : new Date(Date.now() + (60 * 60 * 24 * 30 * 1000)),
202 'store' : app.set( 'sessionStore' )
205 // methodOverride is used to handle PUT and DELETE HTTP
206 // requests that otherwise aren't handled by default.
207 app.use( express.methodOverride() );
208 // Static files are loaded when no dynamic views match.
209 app.use( express.static( __dirname + '/public' ) );
211 // EveryAuth fb connect
212 app.use( everyauth.middleware() );
214 // Sets the routers middleware to load after everything set
215 // before it, but before static files.
216 app.use( app.router );
218 app.use(express.logger({ format: ':method :url' }));
219 // This is the command to use the default express error logger/handler
220 app.use(express.errorHandler({ dumpExceptions: true }));
224 // Mailer functions and helpers
225 // These are helper functions that make for cleaner code.
227 // sendUserActivation is for when a user registers and
228 // first needs to activate their account to use it.
229 function sendUserActivation( user ) {
233 'subject' : 'Activate your FinalsClub.org Account',
235 // Templates are in the email folder and use ejs
236 'template' : 'userActivation',
237 // Locals are used inside ejs so dynamic information
238 // can be rendered properly.
241 'serverHost' : serverHost
245 // Email is sent here
246 mailer.send( message, function( err, result ) {
248 // XXX: Add route to resend this email
249 console.log( 'Error sending user activation email\nError Message: '+err.Message );
251 console.log( 'Successfully sent user activation email.' );
256 // sendUserWelcome is for when a user registers and
257 // a welcome email is sent.
258 function sendUserWelcome( user, school ) {
259 // If a user is not apart of a supported school, they are
260 // sent a different template than if they are apart of a
262 var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
266 'subject' : 'Welcome to FinalsClub',
268 'template' : template,
271 'serverHost' : serverHost
275 mailer.send( message, function( err, result ) {
277 // XXX: Add route to resend this email
278 console.log( 'Error sending user welcome email\nError Message: '+err.Message );
280 console.log( 'Successfully sent user welcome email.' );
286 // These functions are used later in the routes to help
287 // load information and variables, as well as handle
288 // various instances like checking if a user is logged in
290 function loggedIn( req, res, next ) {
291 // If req.user is set, then pass on to the next function
292 // or else alert the user with an error message.
296 req.flash( 'error', 'You must be logged in to access that feature!' );
301 // This loads the user if logged in
302 function loadUser( req, res, next ) {
303 var sid = req.sessionID;
305 console.log( 'got request from session ID: %s', sid );
307 // Find a user based on their stored session id
308 User.findOne( { session : sid }, function( err, user ) {
313 // If a user is found then set req.user the contents of user
314 // and make sure req.user.loggedIn is true.
318 req.user.loggedIn = true;
320 log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
322 // Check if a user is activated. If not, then redirec
323 // to the homepage and tell them to check their email
324 // for the activation email.
325 if( req.user.activated ) {
326 // Is the user's profile complete? If not, redirect to their profile
327 if( ! req.user.isComplete ) {
328 if( url.parse( req.url ).pathname != '/profile' ) {
329 req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
331 res.redirect( '/profile' );
339 req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
343 } else if('a'==='b'){
344 console.log('never. in. behrlin.');
346 // If no user record was found, then we store the requested
347 // path they intended to view and redirect them after they
348 // login if it is requred.
349 var path = url.parse( req.url ).pathname;
350 req.session.redirect = path;
352 // Set req.user to an empty object so it doesn't throw errors
353 // later on that it isn't defined.
363 // loadSchool is used to load a school by it's id
364 function loadSchool( req, res, next ) {
366 var schoolName = req.params.name;
367 console.log( 'loading a school by id' );
369 School.findOne({'name': schoolName}).run( function( err, school ) {
374 // If a school is found, the user is checked to see if they are
375 // authorized to see or interact with anything related to that
377 school.authorize( user, function( authorized ){
378 req.school.authorized = authorized;
382 // If no school is found, display an appropriate error.
383 sendJson(res, {status: 'not_found', message: 'Invalid school specified!'} );
388 function loadSchoolSlug( req, res, next ) {
390 var schoolSlug = req.params.slug;
392 console.log("loading a school by slug");
393 //console.log(schoolSlug);
395 School.findOne({ 'slug': schoolSlug }, function( err, school ) {
396 console.log( school );
400 // If a school is found, the user is checked to see if they are
401 // authorized to see or interact with anything related to that
404 //school.authorize( user, function( authorized ){
405 //req.school.authorized = authorized;
409 // If no school is found, display an appropriate error.
410 sendJson(res, {status: 'not_found', message: 'Invalid school specified!'} );
415 // loadSchool is used to load a course by it's id
416 function loadCourse( req, res, next ) {
418 var courseId = req.params.id;
420 Course.findById( courseId, function( err, course ) {
421 if( course && !course.deleted ) {
424 // If a course is found, the user is checked to see if they are
425 // authorized to see or interact with anything related to that
427 course.authorize( user, function( authorized ) {
428 req.course.authorized = authorized;
433 // If no course is found, display an appropriate error.
434 sendJson(res, {status: 'not_found', message: 'Invalid course specified!'} );
439 // loadLecture is used to load a lecture by it's id
440 function loadLecture( req, res, next ) {
442 var lectureId = req.params.id;
444 Lecture.findById( lectureId, function( err, lecture ) {
445 if( lecture && !lecture.deleted ) {
446 req.lecture = lecture;
448 // If a lecture is found, the user is checked to see if they are
449 // authorized to see or interact with anything related to that
451 lecture.authorize( user, function( authorized ) {
452 req.lecture.authorized = authorized;
457 // If no lecture is found, display an appropriate error.
458 sendJson(res, {status: 'not_found', message: 'Invalid lecture specified!'} );
463 // loadNote is used to load a note by it's id
464 // This is a lot more complicated than the above
465 // due to public/private handling of notes.
466 function loadNote( req, res, next ) {
467 var user = req.user ? req.user : false;
468 var noteId = req.params.id;
470 Note.findById( noteId, function( err, note ) {
471 // If a note is found, and user is set, check if
472 // user is authorized to interact with that note.
473 if( note && user && !note.deleted ) {
474 note.authorize( user, function( auth ) {
476 // If authorzied, then set req.note to be used later
480 } else if ( note.public ) {
481 // If not authorized, but the note is public, then
482 // designate the note read only (RO) and store req.note
483 // FIXME: this should be req.RO = true, disabled due to auth issues
484 // TODO: ^^^ Important
491 // If the user is not authorized and the note is private
492 // then display and error.
493 sendJson(res, {status: 'error', message: 'You do not have permission to access that note.'} );
496 } else if ( note && note.public && !note.deleted ) {
497 // If note is found, but user is not set because they are not
498 // logged in, and the note is public, set the note to read only
499 // and store the note for later.
504 } else if ( note && !note.public && !note.deleted ) {
505 // If the note is found, but user is not logged in and the note is
506 // not public, then ask them to login to view the note. Once logged
507 // in they will be redirected to the note, at which time authorization
508 // handling will be put in effect above.
509 //req.session.redirect = '/note/' + note._id;
510 sendJson(res, {status: 'error', message: 'You must be logged in to view that note.'} );
513 sendJson(res, {status: 'error', message: 'Invalid note specified!'} );
518 function checkAjax( req, res, next ) {
522 res.sendfile( 'public/index.html' );
526 // Dynamic Helpers are loaded automatically into views
527 app.dynamicHelpers( {
528 // express-messages is for flash messages for easy
529 // errors and information display
530 'messages' : require( 'express-messages' ),
532 // By default the req object isn't sen't to views
533 // during rendering, this allows you to use the
534 // user object if available in views.
535 'user' : function( req, res ) {
539 // Same, this allows session to be available in views.
540 'session' : function( req, res ) {
545 function sendJson( res, obj ) {
546 res.header('Cache-Control', 'no-cache, no-store');
551 // The following are the main CRUD routes that are used
552 // to make up this web app.
557 app.get( '/', loadUser, function( req, res ) {
560 res.render( 'index' );
565 // Used to display all available schools and any courses
567 // Public with some private information
568 app.get( '/schools', checkAjax, loadUser, function( req, res ) {
569 sys.puts('loading schools');
570 console.log(req.user);
574 // Find all schools and sort by name
575 // XXX mongoose's documentation on sort is extremely poor, tread carefully
576 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
578 // If schools are found, loop through them gathering any courses that are
579 // associated with them and then render the page with that information.
580 var schools_todo = schools.length;
581 schools.map(function (school) {
582 Course.find( { 'school': school.id } ).run(function (err, courses) {
583 school.courses_length = courses.length
585 if (schools_todo <= 0) {
586 sendJson(res, { 'user': user.sanitized, 'schools': schools.map( function(s) {
590 description: s.description,
593 courses: s.courses_length
602 // If no schools have been found, display none
603 //res.render( 'schools', { 'schools' : [] } );
604 sendJson(res, { 'schools' : [] , 'user': user.sanitized });
609 app.get( '/school/:name', checkAjax, loadUser, loadSchool, function( req, res ) {
610 var school = req.school;
613 console.log( 'loading a school by school/:id now name' );
615 //school.authorize( user, function( authorized ) {
616 // This is used to display interface elements for those users
617 // that are are allowed to see th)m, for instance a 'New Course' button.
618 //var sanitizedSchool = school.sanitized;
619 var sanitizedSchool = {
622 description: school.description,
625 //sanitizedSchool.authorized = authorized;
626 // Find all courses for school by it's id and sort by name
627 Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
628 // If any courses are found, set them to the appropriate school, otherwise
631 if( courses.length > 0 ) {
632 courses = courses.filter(function(course) {
633 if (!course.deleted) return course;
634 }).map(function(course) {
635 return course.sanitized;
640 sanitizedSchool.courses = courses;
643 // This tells async (the module) that each iteration of forEach is
644 // done and will continue to call the rest until they have all been
645 // completed, at which time the last function below will be called.
646 sendJson(res, { 'school': sanitizedSchool, 'user': user.sanitized })
651 // FIXME: version of the same using school slugs instead of ids
652 // TODO: merge this with the :id funciton or depricate it
653 app.get( '/schoolslug/:slug', checkAjax, loadUser, loadSchoolSlug, function( req, res ) {
654 var school = req.school;
656 console.log( 'loading a schoolslug/:slug' );
658 school.authorize( user, function( authorized ) {
659 // This is used to display interface elements for those users
660 // that are are allowed to see th)m, for instance a 'New Course' button.
661 var sanitizedSchool = school.sanitized;
662 sanitizedSchool.authorized = authorized;
663 // Find all courses for school by it's id and sort by name
664 Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
665 // If any courses are found, set them to the appropriate school, otherwise
667 if( courses.length > 0 ) {
668 sanitizedSchool.courses = courses.filter(function(course) {
669 if (!course.deleted) return course;
670 }).map(function(course) {
671 return course.sanitized;
674 sanitizedSchool.courses = [];
676 // This tells async (the module) that each iteration of forEach is
677 // done and will continue to call the rest until they have all been
678 // completed, at which time the last function below will be called.
679 sendJson(res, { 'school': sanitizedSchool, 'user': user.sanitized })
685 // Recieves new course form
686 app.post( '/school/:id', checkAjax, loadUser, loadSchool, function( req, res ) {
687 var school = req.school;
688 // Creates new course from Course Schema
689 var course = new Course;
690 // Gathers instructor information from form
691 var instructorEmail = req.body.email.toLowerCase();
692 var instructorName = req.body.instructorName;
694 if( ( ! school ) || ( ! school.authorized ) ) {
695 return sendJson(res, {status: 'error', message: 'There was a problem trying to create a course'})
698 if ( !instructorName ) {
699 return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
702 if ( ( instructorEmail === '' ) || ( !isValidEmail( instructorEmail ) ) ) {
703 return sendJson(res, {status: 'error', message:'Please enter a valid email'} );
706 // Fill out the course with information from the form
707 course.number = req.body.number;
708 course.name = req.body.name;
709 course.description = req.body.description;
710 course.school = school._id;
711 course.creator = req.user._id;
712 course.subject = req.body.subject;
713 course.department = req.body.department;
715 // Check if a user exists with the instructorEmail, if not then create
716 // a new user and send them an instructor welcome email.
717 User.findOne( { 'email' : instructorEmail }, function( err, user ) {
721 user.name = instructorName
722 user.email = instructorEmail;
723 user.affil = 'Instructor';
724 user.school = school.name;
726 user.activated = false;
728 // Once the new user information has been completed, save the user
729 // to the database then email them the instructor welcome email.
730 user.save(function( err ) {
731 // If there was an error saving the instructor, prompt the user to fill out
732 // the information again.
734 return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
739 'subject' : 'A non-profit open education initiative',
741 'template' : 'instructorInvite',
746 'serverHost' : serverHost
750 mailer.send( message, function( err, result ) {
752 console.log( 'Error inviting instructor to course!' );
754 console.log( 'Successfully invited instructor to course.' );
758 // After emails are sent, set the courses instructor to the
759 // new users id and then save the course to the database.
760 course.instructor = user._id;
761 course.save( function( err ) {
763 return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
765 // Once the course has been completed email the admin with information
766 // on the course and new instructor
770 'subject' : school.name+' has a new course: '+course.name,
772 'template' : 'newCourse',
777 'serverHost' : serverHost
781 mailer.send( message, function( err, result ) {
783 console.log( 'Error sending new course email to info@finalsclub.org' )
785 console.log( 'Successfully invited instructor to course')
788 // Redirect the user to the schools page where they can see
790 // XXX Redirect to the new course instead
791 return sendJson(res, {status: 'ok', message: 'Course created'} )
797 // If the user exists, then check if they are already and instructor
798 if (user.affil === 'Instructor') {
799 // If they are an instructor, then save the course with the appropriate
800 // information and email the admin.
801 course.instructor = user._id;
802 course.save( function( err ) {
804 // XXX better validation
805 return sendJson(res, {status: 'error', message: 'Invalid parameters!'} )
810 'subject' : school.name+' has a new course: '+course.name,
812 'template' : 'newCourse',
817 'serverHost' : serverHost
821 mailer.send( message, function( err, result ) {
823 console.log( 'Error sending new course email to info@finalsclub.org' )
825 console.log( 'Successfully invited instructor to course')
828 // XXX Redirect to the new course instead
829 return sendJson(res, {status: 'ok', message: 'Course created'} )
833 // The existing user isn't an instructor, so the user is notified of the error
834 // and the course isn't created.
835 sendJson(res, {status: 'error', message: 'The existing user\'s email you entered is not an instructor'} )
841 // Individual Course Listing
842 // Public with private information
843 app.get( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
844 var userId = req.user._id;
845 var course = req.course;
847 // Check if the user is subscribed to the course
848 // XXX Not currently used for anything
849 //var subscribed = course.subscribed( userId );
851 // Find lectures associated with this course and sort by name
852 Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
853 // Get course instructor information using their id
854 User.findById( course.instructor, function( err, instructor ) {
855 // Render course and lectures
856 var sanitizedInstructor = instructor.sanitized;
857 var sanitizedCourse = course.sanitized;
858 if (!course.authorized) {
859 delete sanitizedInstructor.email;
861 sanitizedCourse.authorized = course.authorized;
863 sendJson(res, { 'course' : sanitizedCourse, 'instructor': sanitizedInstructor, 'lectures' : lectures.map(function(lecture) { return lecture.sanitized })} );
868 // Recieve New Lecture Form
869 app.post( '/course/:id', checkAjax, loadUser, loadCourse, function( req, res ) {
870 var course = req.course;
871 // Create new lecture from Lecture schema
872 var lecture = new Lecture;
874 if( ( ! course ) || ( ! course.authorized ) ) {
875 return sendJson(res, {status: 'error', message: 'There was a problem trying to create a lecture'})
878 // Populate lecture with form data
879 lecture.name = req.body.name;
880 lecture.date = req.body.date;
881 lecture.course = course._id;
882 lecture.creator = req.user._id;
884 // Save lecture to database
885 lecture.save( function( err ) {
887 // XXX better validation
888 sendJson(res, {status: 'error', message: 'Invalid parameters!'} );
890 sendJson(res, {status: 'ok', message: 'Created new lecture'} );
896 app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
897 var course = req.course;
901 res.render( 'course/new', {course: course} )
903 req.flash( 'error', 'You don\'t have permission to do that' )
904 res.redirect( '/schools' );
908 // Recieve Course Edit Form
909 app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
910 var course = req.course;
914 var courseChanges = req.body;
915 course.number = courseChanges.number;
916 course.name = courseChanges.name;
917 course.description = courseChanges.description;
918 course.department = courseChanges.department;
920 course.save(function(err) {
922 req.flash( 'error', 'There was an error saving the course' );
924 res.redirect( '/course/'+ course._id.toString());
927 req.flash( 'error', 'You don\'t have permission to do that' )
928 res.redirect( '/schools' );
933 app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
934 var course = req.course;
938 course.delete(function( err ) {
939 if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
940 else req.flash( 'info', 'Successfully removed course' )
941 res.redirect( '/schools' );
944 req.flash( 'error', 'You don\'t have permission to do that' )
945 res.redirect( '/schools' );
949 // Subscribe to course
950 // XXX Not currently used for anything
951 app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
952 var course = req.course;
953 var userId = req.user._id;
955 course.subscribe( userId, function( err ) {
957 req.flash( 'error', 'Error subscribing to course!' );
960 res.redirect( '/course/' + course._id );
964 // Unsubscribe from course
965 // XXX Not currently used for anything
966 app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
967 var course = req.course;
968 var userId = req.user._id;
970 course.unsubscribe( userId, function( err ) {
972 req.flash( 'error', 'Error unsubscribing from course!' );
975 res.redirect( '/course/' + course._id );
982 // Display individual lecture and related notes
983 app.get( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
984 var lecture = req.lecture;
986 // Grab the associated course
987 // XXX this should be done with DBRefs eventually
988 Course.findById( lecture.course, function( err, course ) {
990 // If course is found, find instructor information to be displayed on page
991 User.findById( course.instructor, function( err, instructor ) {
992 // Pull out our notes
993 Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
994 if ( !req.user.loggedIn || !req.lecture.authorized ) {
995 // Loop through notes and only return those that are public if the
996 // user is not logged in or not authorized for that lecture
997 notes = notes.filter(function( note ) {
998 if ( note.public ) return note;
1001 var sanitizedInstructor = instructor.sanitized;
1002 var sanitizedLecture = lecture.sanitized;
1003 if (!lecture.authorized) {
1004 delete sanitizedInstructor.email;
1006 sanitizedLecture.authorized = lecture.authorized;
1009 'lecture' : sanitizedLecture,
1010 'course' : course.sanitized,
1011 'instructor' : sanitizedInstructor,
1012 'notes' : notes.map(function(note) {
1013 return note.sanitized;
1019 sendJson(res, { status: 'not_found', msg: 'This course is orphaned' })
1025 // Recieve new note form
1026 app.post( '/lecture/:id', checkAjax, loadUser, loadLecture, function( req, res ) {
1027 var lecture = req.lecture;
1029 if( ( ! lecture ) || ( ! lecture.authorized ) ) {
1030 return sendJson(res, {status: 'error', message: 'There was a problem trying to create a note pad'})
1033 // Create note from Note schema
1034 var note = new Note;
1036 // Populate note from form data
1037 note.name = req.body.name;
1038 note.date = req.body.date;
1039 note.lecture = lecture._id;
1040 note.public = req.body.private ? false : true;
1041 note.creator = req.user._id;
1043 // Save note to database
1044 note.save( function( err ) {
1046 // XXX better validation
1047 sendJson(res, {status: 'error', message: 'There was a problem trying to create a note pad'})
1049 sendJson(res, {status: 'ok', message: 'Successfully created a new note pad'})
1055 // Display individual note page
1056 app.get( '/note/:id', /*checkAjax,*/ loadUser, loadNote, function( req, res ) {
1057 var note = req.note;
1058 // Set read only id for etherpad-lite or false for later check
1059 var roID = note.roID || false;
1061 var lectureId = note.lecture;
1063 // Count the amount of visits, but only once per session
1064 if ( req.session.visited ) {
1065 if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
1066 req.session.visited.push( note._id );
1070 req.session.visited = [];
1071 req.session.visited.push( note._id );
1075 // If a read only id exists process note
1079 // If read only id doesn't, then fetch the read only id from the database and then
1081 // XXX Soon to be depracated due to a new API in etherpad that makes for a
1082 // much cleaner solution.
1083 db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
1084 epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
1086 roID = record.value.replace(/"/g, '');
1095 function processReq() {
1097 Lecture.findById( lectureId, function( err, lecture ) {
1099 req.flash( 'error', 'That notes page is orphaned!' );
1101 res.redirect( '/' );
1103 // Find notes based on lecture id, which will be displayed in a dropdown
1105 Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
1108 'host' : serverHost,
1109 'note' : note.sanitized,
1110 'lecture' : lecture.sanitized,
1111 'otherNotes' : otherNotes.map(function(note) {
1112 return note.sanitized;
1119 // User is logged in and sees full notepad
1121 res.render( 'notes/index', {
1122 'layout' : 'noteLayout',
1123 'host' : serverHost,
1125 'lecture' : lecture,
1126 'otherNotes' : otherNotes,
1129 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1130 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1133 // User is not logged in and sees notepad that is public
1134 res.render( 'notes/public', {
1135 'layout' : 'noteLayout',
1136 'host' : serverHost,
1138 'otherNotes' : otherNotes,
1140 'lecture' : lecture,
1141 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
1142 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
1150 // Static pages and redirects
1152 app.get( '/about', loadUser, function( req, res ) {
1153 res.redirect( 'http://blog.finalsclub.org/about.html' );
1156 app.get( '/press', loadUser, function( req, res ) {
1157 res.render( 'static/press' );
1160 app.get( '/conduct', loadUser, function( req, res ) {
1161 res.render( 'static/conduct' );
1164 app.get( '/legal', loadUser, function( req, res ) {
1165 res.redirect( 'http://blog.finalsclub.org/legal.html' );
1168 app.get( '/contact', loadUser, function( req, res ) {
1169 res.redirect( 'http://blog.finalsclub.org/contact.html' );
1172 app.get( '/privacy', loadUser, function( req, res ) {
1173 res.render( 'static/privacy' );
1178 // Authentication routes
1179 // These are used for logging in, logging out, registering
1180 // and other user authentication purposes
1182 // Render login page
1184 app.get( '/login', function( req, res ) {
1185 log3("get login page")
1187 res.render( 'login' );
1191 app.get( '/checkuser', checkAjax, loadUser, function( req, res ) {
1192 sendJson(res, {user: req.user.sanitized});
1195 // Recieve login form
1196 app.post( '/login', checkAjax, function( req, res ) {
1197 var email = req.body.email;
1198 var password = req.body.password;
1199 log3("post login ...")
1201 // Find user from email
1202 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1206 // If user exists, check if activated, if not notify them and send them to
1209 if( ! user.activated ) {
1210 // (undocumented) markdown-esque link functionality in req.flash
1211 req.session.activateCode = user._id;
1212 sendJson(res, {status: 'error', message: 'This account isn\'t activated.'} );
1215 // If user is activated, check if their password is correct
1216 if( user.authenticate( password ) ) {
1219 var sid = req.sessionID;
1223 // Set the session then save the user to the database
1224 user.save( function() {
1225 var redirect = req.session.redirect;
1227 // login complete, remember the user's email for next time
1228 req.session.email = email;
1230 // alert the successful login
1231 sendJson(res, {status: 'ok', message:'Successfully logged in!'} );
1233 // redirect to profile if we don't have a stashed request
1234 //res.redirect( redirect || '/profile' );
1237 // Notify user of bad login
1238 sendJson(res, {status: 'error', message: 'Invalid login!'} );
1240 //res.render( 'login' );
1244 // Notify user of bad login
1246 sendJson(res, {status: 'error', message: 'Invalid login!'} );
1248 //res.render( 'login' );
1253 // Recieve reset password request form
1254 app.post( '/resetpass', checkAjax, function( req, res ) {
1255 log3("post resetpw");
1256 var email = req.body.email
1260 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1263 // If user exists, create reset code
1264 var resetPassCode = hat(64);
1265 user.setResetPassCode(resetPassCode);
1267 // Construct url that the user can then click to reset password
1268 var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
1270 // Save user to database
1271 user.save( function( err ) {
1272 log3('save '+user.email);
1274 // Construct email and send it to the user
1278 'subject' : 'Your FinalsClub.org Password has been Reset!',
1280 'template' : 'userPasswordReset',
1282 'resetPassCode' : resetPassCode,
1283 'resetPassUrl' : resetPassUrl
1287 mailer.send( message, function( err, result ) {
1289 // XXX: Add route to resend this email
1291 console.log( 'Error sending user password reset email!' );
1293 console.log( 'Successfully sent user password reset email.' );
1298 // Render request success page
1299 sendJson(res, {status: 'ok', message: 'Your password has been reset. An email has been sent to ' + email })
1303 sendJson(res, {status: 'error', message: 'We were unable to reset the password using that email address. Please try again.' })
1308 // Recieve reset password form
1309 app.post( '/resetpw/:id', checkAjax, function( req, res ) {
1310 log3("post resetpw.code");
1311 var resetPassCode = req.params.id
1312 var email = req.body.email
1313 var pass1 = req.body.pass1
1314 var pass2 = req.body.pass2
1316 // Find user by email
1317 User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
1319 // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
1320 // save user with new password and display success message.
1322 var valid = user.resetPassword(resetPassCode, pass1, pass2);
1324 user.save( function( err ) {
1325 sendJson(res, {status: 'ok', message: 'Your password has been reset. You can now login with your the new password you just created.'})
1329 // If there was a problem, notify user
1331 sendJson(res, {status: 'error', message: 'We were unable to reset the password. Please try again.' })
1336 // Display registration page
1338 app.get( '/register', function( req, res ) {
1339 log3("get reg page");
1341 // Populate school dropdown list
1342 School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
1343 res.render( 'register', { 'schools' : schools } );
1348 // Recieve registration form
1349 app.post( '/register', checkAjax, function( req, res ) {
1350 var sid = req.sessionId;
1352 // Create new user from User schema
1353 var user = new User;
1355 // Populate user from form
1356 user.email = req.body.email.toLowerCase();
1357 user.password = req.body.password;
1359 // If school is set to other, then fill in school as what the user entered
1360 user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
1361 user.name = req.body.name;
1362 user.affil = req.body.affil;
1363 user.activated = false;
1366 if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
1367 return sendJson(res, {status: 'error', message: 'Please enter a valid email'} );
1370 // Check if password is greater than 6 characters, otherwise notify user
1371 if ( req.body.password.length < 6 ) {
1372 return sendJson(res, {status: 'error', message: 'Please enter a password longer than eight characters'} );
1375 // Pull out hostname from email
1376 var hostname = user.email.split( '@' ).pop();
1378 // Check if email is from one of the special domains
1379 if( /^(finalsclub.org)$/.test( hostname ) ) {
1383 // Save user to database
1384 user.save( function( err ) {
1385 // If error, check if it is because the user already exists, if so
1386 // get the user information and let them know
1388 if( /dup key/.test( err.message ) ) {
1389 // attempting to register an existing address
1390 User.findOne({ 'email' : user.email }, function(err, result ) {
1391 if (result.activated) {
1392 // If activated, make sure they know how to contact the admin
1393 return sendJson(res, {status: 'error', message: 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help'} );
1395 // If not activated, direct them to the resendActivation page
1396 return sendJson(res, {status: 'error', message: 'There is already someone registered with this email, if this is you, please check your email for the activation code'} );
1400 // If any other type of error, prompt them to enter the registration again
1401 return sendJson(res, {status: 'error', message: 'An error occurred during registration.'} );
1404 // send user activation email
1405 sendUserActivation( user );
1407 // Check if the hostname matches any in the approved schools
1408 School.findOne( { 'hostnames' : hostname }, function( err, school ) {
1410 // If there is a match, send associated welcome message
1411 sendUserWelcome( user, true );
1412 log3('school recognized '+school.name);
1413 // If no users exist for the school, create empty array
1414 if (!school.users) school.users = [];
1415 // Add user to the school
1416 school.users.push( user._id );
1418 // Save school to the database
1419 school.save( function( err ) {
1420 log3('school.save() done');
1421 // Notify user that they have been added to the school
1422 sendJson(res, {status: 'ok', message: 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link'} );
1424 // Construct admin email about user registration
1428 'subject' : 'FC User Registration : User added to ' + school.name,
1430 'template' : 'userSchool',
1436 // If there isn't a match, send associated welcome message
1437 sendUserWelcome( user, false );
1438 // Tell user to check for activation link
1439 sendJson(res, {status: 'ok', message: 'Your account has been created, please check your email for the activation link'} );
1440 // Construct admin email about user registration
1444 'subject' : 'FC User Registration : Email did not match any schools',
1446 'template' : 'userNoSchool',
1452 // Send email to admin
1453 mailer.send( message, function( err, result ) {
1456 console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
1458 console.log( 'Successfully sent user has no school email to admin.' );
1468 // Display resendActivation request page
1469 app.get( '/resendActivation', function( req, res ) {
1470 var activateCode = req.session.activateCode;
1472 // Check if user exists by activateCode set in their session
1473 User.findById( activateCode, function( err, user ) {
1474 if( ( ! user ) || ( user.activated ) ) {
1475 res.redirect( '/' );
1477 // Send activation and redirect to login
1478 sendUserActivation( user );
1480 req.flash( 'info', 'Your activation code has been resent.' );
1482 res.redirect( '/login' );
1487 // Display activation page
1488 app.get( '/activate/:code', checkAjax, function( req, res ) {
1489 var code = req.params.code;
1491 // XXX could break this out into a middleware
1493 return sendJson(res, {status:'error', message: 'Invalid activation code!'} );
1496 // Find user by activation code
1497 User.findById( code, function( err, user ) {
1498 if( err || ! user ) {
1499 // If not found, notify user of invalid code
1500 sendJson(res, {status:'error', message:'Invalid activation code!'} );
1502 // If valid, then activate user
1503 user.activated = true;
1505 // Regenerate our session and log in as the new user
1506 req.session.regenerate( function() {
1507 user.session = req.sessionID;
1509 // Save user to database
1510 user.save( function( err ) {
1512 sendJson(res, {status: 'error', message: 'Unable to activate account.'} );
1514 sendJson(res, {status: 'info', message: 'Account successfully activated. Please complete your profile.'} );
1523 app.get( '/logout', checkAjax, function( req, res ) {
1524 sys.puts("logging out");
1525 var sid = req.sessionID;
1527 // Find user by session id
1528 User.findOne( { 'session' : sid }, function( err, user ) {
1530 // Empty out session id
1533 // Save user to database
1534 user.save( function( err ) {
1535 sendJson(res, {status: 'ok', message: 'Successfully logged out'});
1538 sendJson(res, {status: 'ok', message: ''});
1543 // Recieve profile edit page form
1544 app.post( '/profile', checkAjax, loadUser, loggedIn, function( req, res ) {
1545 var user = req.user;
1546 var fields = req.body;
1549 var wasComplete = user.isComplete;
1551 if( ! fields.name ) {
1552 return sendJson(res, {status: 'error', message: 'Please enter a valid name!'} );
1554 user.name = fields.name;
1557 if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
1558 return sendJson(res, {status: 'error', message: 'Please select a valid affiliation!'} );
1560 user.affil = fields.affiliation;
1563 if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
1564 // changing password
1565 if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
1566 if( fields.newPassword === fields.newPasswordConfirm ) {
1567 // test password strength?
1569 user.password = fields.newPassword;
1571 return sendJson(res, {status: 'error', message: 'Mismatch in new password!'} );
1574 return sendJson(res, {status: 'error', message: 'Please supply your existing password.'} );
1578 user.major = fields.major;
1579 user.bio = fields.bio;
1581 user.showName = ( fields.showName ? true : false );
1583 user.save( function( err ) {
1585 sendJson(res, {status: 'error', message: 'Unable to save user profile!'} );
1587 if( ( user.isComplete ) && ( ! wasComplete ) ) {
1588 sendJson(res, {status: 'ok', message: 'Your account is now fully activated. Thank you for joining FinalsClub!'} );
1590 sendJson(res, {status:'ok', message:'Your profile was successfully updated!'} );
1599 function loadSubject( req, res, next ) {
1600 if( url.parse( req.url ).pathname.match(/subject/) ) {
1601 ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
1602 if ( err || !subject) {
1603 sendJson(res, {status: 'not_found', message: 'Subject with this ID does not exist'} )
1605 req.subject = subject;
1614 function loadOldCourse( req, res, next ) {
1615 if( url.parse( req.url ).pathname.match(/course/) ) {
1616 ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
1617 if ( err || !course ) {
1618 sendJson(res, {status: 'not_found', message: 'Course with this ID does not exist'} )
1620 req.course = course;
1629 var featuredCourses = [
1630 {name: 'The Human Mind', 'id': 1563},
1631 {name: 'Justice', 'id': 797},
1632 {name: 'Protest Literature', 'id': 1681},
1633 {name: 'Animal Cognition', 'id': 681},
1634 {name: 'Positive Psychology', 'id': 1793},
1635 {name: 'Social Psychology', 'id': 660},
1636 {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
1637 {name: 'Cyberspace in Court', 'id': 1446},
1638 {name: 'Nazi Cinema', 'id': 2586},
1639 {name: 'Media and the American Mind', 'id': 2583},
1640 {name: 'Social Thought in Modern America', 'id': 2585},
1641 {name: 'Major British Writers II', 'id': 869},
1642 {name: 'Civil Procedure', 'id': 2589},
1643 {name: 'Evidence', 'id': 2590},
1644 {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
1647 app.get( '/learn', loadUser, function( req, res ) {
1648 res.render( 'archive/learn', { 'courses' : featuredCourses } );
1651 app.get( '/learn/random', checkAjax, function( req, res ) {
1652 sendJson(res, {status: 'ok', data: '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id });
1655 app.get( '/archive', checkAjax, loadUser, function( req, res ) {
1656 ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
1657 if ( err || subjects.length === 0) {
1658 sendJson(res, {status: 'error', message: 'There was a problem gathering the archived courses, please try again later.'} );
1660 sendJson(res, { 'subjects' : subjects, 'user': req.user.sanitized } );
1665 app.get( '/archive/subject/:id', checkAjax, loadUser, loadSubject, function( req, res ) {
1666 ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
1667 if ( err || courses.length === 0 ) {
1668 sendJson(res, {status: 'not_found', message: 'There are no archived courses'} );
1670 sendJson(res, { 'courses' : courses, 'subject': req.subject, 'user': req.user.sanitized } );
1675 app.get( '/archive/course/:id', checkAjax, loadUser, loadOldCourse, function( req, res ) {
1676 ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
1677 if ( err || notes.length === 0) {
1678 sendJson(res, {status: 'not_found', message: 'There are no notes in this course'} );
1680 notes = notes.map(function(note) { return note.sanitized });
1681 sendJson(res, { 'notes': notes, 'course' : req.course, 'user': req.user.sanitized } );
1686 app.get( '/archive/note/:id', checkAjax, loadUser, function( req, res ) {
1687 console.log( "id="+req.params.id)
1688 ArchivedNote.findById(req.params.id, function(err, note) {
1689 if ( err || !note ) {
1690 sendJson(res, {status: 'not_found', message: 'This is not a valid id for a note'} );
1692 ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
1693 if ( err || !course ) {
1694 sendJson(res, {status: 'not_found', message: 'There is no course for this note'} )
1696 sendJson(res, { 'layout' : 'notesLayout', 'note' : note, 'course': course, 'user': req.user.sanitized } );
1704 app.get( '*', function(req, res) {
1705 res.sendfile('public/index.html');
1710 // The finalsclub backchannel server uses socket.io to handle communication between the server and
1711 // the browser which facilitates near realtime interaction. This allows the user to post questions
1712 // and comments and other users to get those almost immediately after they are posted, without
1713 // reloading the page or pressing a button to refresh.
1715 // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
1716 // saving the data to the database, and then sending it out to everyone else connected.
1719 // Posts - Posts are the main items in backchannel, useful for questions or discussion points
1720 // [[ example object needed with explanation E.G:
1722 Post: { postID: '999-1',
1724 userName: 'Bob Jones',
1725 userAffil: 'Instructor',
1726 body: 'This is the text content of the post.',
1727 comments: { {<commentObj>, <commentObj>, ...},
1729 votes: [ <userID>, <userID>, ...],
1730 reports: [ <userID>, <userID>, ...]
1732 Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
1734 if anonymous: userName => 'Anonymous', userAffil => 'N/A'
1739 // Comments - Comments are replies to posts, for clarification or answering questions
1740 // [[ example object needed]]
1741 // Votes - Votes signifyg a users approval of a post
1742 // [[ example object needed]]
1743 // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
1744 // [[ example object needed]]
1748 // body - Main content of the post
1749 // userId - Not currently used, but would contain the users id that made the post
1750 // userName - Users name that made post
1751 // userAffil - Users affiliation to their school
1752 // public - Boolean which denotes if the post is public to everyone, or private to school users only
1753 // date - Date post was made, updates when any comments are made for the post
1754 // comments - An array of comments which contain a body, userName, and userAffil
1755 // votes - An array of user ids which are the users that voted
1756 // [[ example needed ]]
1757 // reports - An array of user ids which are the users that reported the post
1758 // [[ reports would be "this post is flagged as inappropriate"? ]]
1759 // [[ bruml: consistent terminology needed ]]
1761 // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
1762 // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
1763 // users the ability to make posts or comments that they might not otherwise due to not wanting
1764 // the content of the post/comment to be attributed to them.
1766 // Each time a user connects to the server, it passes through authorization which checks for a cookie
1767 // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
1768 // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
1769 // user is logged in, and if so display information that otherwise might not be visible to them if they
1770 // aren't apart of a particular school.
1772 // After the authorization step, the client browser sends the lecture id which is rendered into the html
1773 // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
1774 // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
1775 // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
1776 // rendered in the browser.
1778 // Everything else from this point on is handled in an event form and requires a user initiating it. The
1779 // events are as follows.
1782 // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
1783 // The server recieves the data, assembles a new post object for the database and then fills it with
1784 // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
1785 // replaced. If the user chose for the post to be private, then public will be set to false and it
1786 // will be filtered from being sent to users not logged into and not having access to the school. Once
1787 // the post has been created and saved into the database, it is sent to all connected users to that
1788 // particular lecture, unless it is private, than only logged in users will get it.
1791 // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
1792 // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
1793 // votes array, and then the post is subsequently saved back to the database and sent to all connected
1794 // users unless the post is private, which then it will be only sent to logged in users.
1797 // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
1798 // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
1799 // Then the report is sent out to all connected users unless it is a private post, which will be only sent
1800 // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
1801 // bottom of the interface.
1804 // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
1805 // user name, and user affiliation are sent to the server, which are then used to find the parent post
1806 // and then a new comment object is assembled. When new comments are made, it updates the posts date
1807 // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
1808 // to the top of the interface. The comment can be anonymous, which then will have the user
1809 // name and affiliation stripped before saving to the database. The comment then will be sent out to all
1810 // connected users unless the post is private, then only logged in users will recieve the comment.
1812 var io = require( 'socket.io' ).listen( app );
1814 var Post = mongoose.model( 'Post' );
1816 io.set('authorization', function ( handshake, next ) {
1817 var rawCookie = handshake.headers.cookie;
1819 handshake.cookie = parseCookie(rawCookie);
1820 handshake.sid = handshake.cookie['connect.sid'];
1822 if ( handshake.sid ) {
1823 app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
1825 handshake.user = false;
1826 return next(null, true);
1828 // bake a new session object for full r/w
1829 handshake.session = new Session( handshake, session );
1831 User.findOne( { session : handshake.sid }, function( err, user ) {
1833 handshake.user = user;
1834 return next(null, true);
1836 handshake.user = false;
1837 return next(null, true);
1845 return next(null, true);
1849 var backchannel = new Backchannel(app, io.of('/backchannel'), {
1850 // TODO: if lecture belongs to course (find pinker's courseId) pass a 'no-answers' true/false
1851 subscribe: function(lecture, send) {
1852 Post.find({'lecture': lecture}, function(err, posts) {
1856 post: function(fillPost) {
1857 var post = new Post;
1858 fillPost(post, function(send) {
1859 post.save(function(err) {
1864 items: function(postId, addItem) {
1865 Post.findById(postId, function( err, post ) {
1866 addItem(post, function(send) {
1867 post.save(function(err) {
1882 .on( 'connection', function( socket ) {
1883 // pull out user/session information etc.
1884 var handshake = socket.handshake;
1885 var userID = handshake.user._id;
1892 socket.on( 'join', function( note ) {
1893 if (handshake.user === false) {
1895 // XXX: replace by addToSet (once it's implemented in mongoose)
1896 Note.findById( noteID, function( err, note ) {
1898 if( note.collaborators.indexOf( userID ) == -1 ) {
1899 note.collaborators.push( userID );
1907 socket.on( 'watch', function( l ) {
1908 var sendCounts = function() {
1911 Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
1914 function( note, callback ) {
1916 var count = note.collaborators.length;
1922 socket.emit( 'counts', send );
1924 timer = setTimeout( sendCounts, 5000 );
1930 Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
1931 notes.forEach( function( note ) {
1932 watched.push( note._id );
1939 socket.on( 'disconnect', function() {
1940 clearTimeout( timer );
1942 if (handshake.user === false) {
1943 // XXX: replace with $pull once it's available
1945 Note.findById( noteID, function( err, note ) {
1947 var index = note.collaborators.indexOf( userID );
1950 note.collaborators.splice( index, 1 );
1961 // Exception Catch-All
1963 process.on('uncaughtException', function (e) {
1964 console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
1970 // mongoose now exepects a mongo url
1971 mongoose.connect( 'mongodb://localhost/fc' ); // FIXME: make relative to hostname
1973 var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
1975 everyauth.helpExpress(app);
1977 app.listen( serverPort, function() {
1978 console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
1980 // if run as root, downgrade to the owner of this file
1981 if (process.getuid() === 0) {
1982 require('fs').stat(__filename, function(err, stats) {
1983 if (err) { return console.log(err); }
1984 process.setuid(stats.uid);
1989 function isValidEmail(email) {
1990 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])?/;
1991 return email.match(re);