app.js 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933
  1. // FinalsClub Server
  2. //
  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.
  6. //
  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.
  10. // Module loading
  11. var sys = require( 'sys' );
  12. var os = require( 'os' );
  13. var url = require( 'url' );
  14. var express = require( 'express' );
  15. var mongoStore = require( 'connect-mongo' );
  16. var async = require( 'async' );
  17. var db = require( './db.js' );
  18. var mongoose = require( './models.js' ).mongoose;
  19. var Mailer = require( './mailer.js' );
  20. var hat = require('hat');
  21. var connect = require( 'connect' );
  22. var Session = connect.middleware.session.Session;
  23. var parseCookie = connect.utils.parseCookie;
  24. var Backchannel = require('../bc/backchannel');
  25. // Depracated
  26. // Used for initial testing
  27. var log3 = function() {}
  28. // Create webserver
  29. var app = module.exports = express.createServer();
  30. // Load Mongoose Schemas
  31. // The actual schemas are located in models.j
  32. var User = mongoose.model( 'User' );
  33. var School = mongoose.model( 'School' );
  34. var Course = mongoose.model( 'Course' );
  35. var Lecture = mongoose.model( 'Lecture' );
  36. var Note = mongoose.model( 'Note' );
  37. // More schemas used for legacy data
  38. var ArchivedCourse = mongoose.model( 'ArchivedCourse' );
  39. var ArchivedNote = mongoose.model( 'ArchivedNote' );
  40. var ArchivedSubject = mongoose.model( 'ArchivedSubject' );
  41. // XXX Not sure if necessary
  42. var ObjectId = mongoose.SchemaTypes.ObjectId;
  43. // Configuration
  44. // Use the environment variable DEV_EMAIL for testing
  45. var ADMIN_EMAIL = process.env.DEV_EMAIL || 'info@finalsclub.org';
  46. // Set server hostname and port from environment variables,
  47. // then check if set.
  48. // XXX Can be cleaned up
  49. var serverHost = process.env.SERVER_HOST;
  50. var serverPort = process.env.SERVER_PORT;
  51. if( serverHost ) {
  52. console.log( 'Using server hostname defined in environment: %s', serverHost );
  53. } else {
  54. serverHost = os.hostname();
  55. console.log( 'No hostname defined, defaulting to os.hostname(): %s', serverHost );
  56. }
  57. // Express configuration depending on environment
  58. // development is intended for developing locally or
  59. // when not in production, otherwise production is used
  60. // when the site will be run live for regular usage.
  61. app.configure( 'development', function() {
  62. // In development mode, all errors and stack traces will be
  63. // dumped to the console and on page for easier troubleshooting
  64. // and debugging.
  65. app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
  66. // Set database connection information from environment
  67. // variables otherwise use localhost.
  68. app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
  69. app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
  70. // Set Amazon access and secret keys from environment
  71. // variables. These keys are intended to be secret, so
  72. // are not included in the source code, but set on the server
  73. // manually.
  74. app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
  75. app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
  76. // If a port wasn't set earlier, set to 3000
  77. if ( !serverPort ) {
  78. serverPort = 3000;
  79. }
  80. });
  81. // Production configuration settings
  82. app.configure( 'production', function() {
  83. // At the moment we have errors outputting everything
  84. // so if there are any issues it is easier to track down.
  85. // Once the site is more stable it will be prudent to
  86. // use less error tracing.
  87. app.set( 'errorHandler', express.errorHandler( { dumpExceptions: true, showStack: true } ) );
  88. // Disable view cache due to stale views.
  89. // XXX Disable view caching temp
  90. app.disable( 'view cache' )
  91. // Against setting the database connection information
  92. // XXX Can be cleaned up or combined
  93. app.set( 'dbHost', process.env.MONGO_HOST || 'localhost' );
  94. app.set( 'dbUri', 'mongodb://' + app.set( 'dbHost' ) + '/fc' );
  95. // XXX Can be cleaned up or combined
  96. app.set( 'awsAccessKey', process.env.AWS_ACCESS_KEY_ID );
  97. app.set( 'awsSecretKey', process.env.AWS_SECRET_ACCESS_KEY );
  98. // Set to port 80 if not set through environment variables
  99. if ( !serverPort ) {
  100. serverPort = 80;
  101. }
  102. });
  103. // General Express configuration settings
  104. app.configure(function(){
  105. // Views are housed in the views folder
  106. app.set( 'views', __dirname + '/views' );
  107. // All templates use jade for rendering
  108. app.set( 'view engine', 'jade' );
  109. // Bodyparser is required to handle form submissions
  110. // without manually parsing them.
  111. app.use( express.bodyParser() );
  112. app.use( express.cookieParser() );
  113. // Sessions are stored in mongodb which allows them
  114. // to be persisted even between server restarts.
  115. app.set( 'sessionStore', new mongoStore( {
  116. 'url' : app.set( 'dbUri' )
  117. }));
  118. // This is where the actual Express session handler
  119. // is defined, with a mongoStore being set as the
  120. // session storage versus in memory storage that is
  121. // used by default.
  122. app.use( express.session( {
  123. // A secret 'password' for encrypting and decrypting
  124. // cookies.
  125. // XXX Should be handled differently
  126. 'secret' : 'finalsclub',
  127. // The max age of the cookies that is allowed
  128. // 60 (seconds) * 60 (minutes) * 24 (hours) * 30 (days) * 1000 (milliseconds)
  129. 'maxAge' : new Date(Date.now() + (60 * 60 * 24 * 30 * 1000)),
  130. 'store' : app.set( 'sessionStore' )
  131. }));
  132. // methodOverride is used to handle PUT and DELETE HTTP
  133. // requests that otherwise aren't handled by default.
  134. app.use( express.methodOverride() );
  135. // Sets the routers middleware to load after everything set
  136. // before it, but before static files.
  137. app.use( app.router );
  138. // Static files are loaded when no dynamic views match.
  139. app.use( express.static( __dirname + '/public' ) );
  140. // This is the errorHandler set in configuration earlier
  141. // being set to a variable to be used after all other
  142. // middleware is loaded. Error handling should always
  143. // come last or near the bottom.
  144. var errorHandler = app.set( 'errorHandler' );
  145. app.use( errorHandler );
  146. });
  147. // Mailer functions and helpers
  148. // These are helper functions that make for cleaner code.
  149. // sendUserActivation is for when a user registers and
  150. // first needs to activate their account to use it.
  151. function sendUserActivation( user ) {
  152. var message = {
  153. 'to' : user.email,
  154. 'subject' : 'Activate your FinalsClub.org Account',
  155. // Templates are in the email folder and use ejs
  156. 'template' : 'userActivation',
  157. // Locals are used inside ejs so dynamic information
  158. // can be rendered properly.
  159. 'locals' : {
  160. 'user' : user,
  161. 'serverHost' : serverHost
  162. }
  163. };
  164. // Email is sent here
  165. mailer.send( message, function( err, result ) {
  166. if( err ) {
  167. // XXX: Add route to resend this email
  168. console.log( 'Error sending user activation email\nError Message: '+err.Message );
  169. } else {
  170. console.log( 'Successfully sent user activation email.' );
  171. }
  172. });
  173. }
  174. // sendUserWelcome is for when a user registers and
  175. // a welcome email is sent.
  176. function sendUserWelcome( user, school ) {
  177. // If a user is not apart of a supported school, they are
  178. // sent a different template than if they are apart of a
  179. // supported school.
  180. var template = school ? 'userWelcome' : 'userWelcomeNoSchool';
  181. var message = {
  182. 'to' : user.email,
  183. 'subject' : 'Welcome to FinalsClub',
  184. 'template' : template,
  185. 'locals' : {
  186. 'user' : user,
  187. 'serverHost' : serverHost
  188. }
  189. };
  190. mailer.send( message, function( err, result ) {
  191. if( err ) {
  192. // XXX: Add route to resend this email
  193. console.log( 'Error sending user welcome email\nError Message: '+err.Message );
  194. } else {
  195. console.log( 'Successfully sent user welcome email.' );
  196. }
  197. });
  198. }
  199. // Helper middleware
  200. // These functions are used later in the routes to help
  201. // load information and variables, as well as handle
  202. // various instances like checking if a user is logged in
  203. // or not.
  204. function loggedIn( req, res, next ) {
  205. // If req.user is set, then pass on to the next function
  206. // or else alert the user with an error message.
  207. if( req.user ) {
  208. next();
  209. } else {
  210. req.flash( 'error', 'You must be logged in to access that feature!' );
  211. res.redirect( '/' );
  212. }
  213. }
  214. // This loads the user if logged in
  215. function loadUser( req, res, next ) {
  216. var sid = req.sessionID;
  217. console.log( 'got request from session ID: %s', sid );
  218. // Find a user based on their stored session id
  219. User.findOne( { session : sid }, function( err, user ) {
  220. log3(err);
  221. log3(user);
  222. // If a user is found then set req.user the contents of user
  223. // and make sure req.user.loggedIn is true.
  224. if( user ) {
  225. req.user = user;
  226. req.user.loggedIn = true;
  227. log3( 'authenticated user: '+req.user._id+' / '+req.user.email+'');
  228. // Check if a user is activated. If not, then redirec
  229. // to the homepage and tell them to check their email
  230. // for the activation email.
  231. if( req.user.activated ) {
  232. // Is the user's profile complete? If not, redirect to their profile
  233. if( ! req.user.isComplete ) {
  234. if( url.parse( req.url ).pathname != '/profile' ) {
  235. req.flash( 'info', 'Your profile is incomplete. Please complete your profile to fully activate your account.' );
  236. res.redirect( '/profile' );
  237. } else {
  238. next();
  239. }
  240. } else {
  241. next();
  242. }
  243. } else {
  244. req.flash( 'info', 'This account has not been activated. Check your email for the activation URL.' );
  245. res.redirect( '/' );
  246. }
  247. } else {
  248. // If no user record was found, then we store the requested
  249. // path they intended to view and redirect them after they
  250. // login if it is requred.
  251. var path = url.parse( req.url ).pathname;
  252. req.session.redirect = path;
  253. // Set req.user to an empty object so it doesn't throw errors
  254. // later on that it isn't defined.
  255. req.user = {};
  256. next();
  257. }
  258. });
  259. }
  260. // loadSchool is used to load a school by it's id
  261. function loadSchool( req, res, next ) {
  262. var user = req.user;
  263. var schoolId = req.params.id;
  264. School.findById( schoolId, function( err, school ) {
  265. if( school ) {
  266. req.school = school;
  267. // If a school is found, the user is checked to see if they are
  268. // authorized to see or interact with anything related to that
  269. // school.
  270. school.authorize( user, function( authorized ){
  271. req.school.authorized = authorized;
  272. next();
  273. });
  274. } else {
  275. // If no school is found, display an appropriate error.
  276. req.flash( 'error', 'Invalid school specified!' );
  277. res.redirect( '/' );
  278. }
  279. });
  280. }
  281. // loadSchool is used to load a course by it's id
  282. function loadCourse( req, res, next ) {
  283. var user = req.user;
  284. var courseId = req.params.id;
  285. Course.findById( courseId, function( err, course ) {
  286. if( course && !course.deleted ) {
  287. req.course = course;
  288. // If a course is found, the user is checked to see if they are
  289. // authorized to see or interact with anything related to that
  290. // school.
  291. course.authorize( user, function( authorized ) {
  292. req.course.authorized = authorized;
  293. next();
  294. });
  295. } else {
  296. // If no course is found, display an appropriate error.
  297. req.flash( 'error', 'Invalid course specified!' );
  298. res.redirect( '/' );
  299. }
  300. });
  301. }
  302. // loadLecture is used to load a lecture by it's id
  303. function loadLecture( req, res, next ) {
  304. var user = req.user;
  305. var lectureId = req.params.id;
  306. Lecture.findById( lectureId, function( err, lecture ) {
  307. if( lecture && !lecture.deleted ) {
  308. req.lecture = lecture;
  309. // If a lecture is found, the user is checked to see if they are
  310. // authorized to see or interact with anything related to that
  311. // school.
  312. lecture.authorize( user, function( authorized ) {
  313. req.lecture.authorized = authorized;
  314. next();
  315. });
  316. } else {
  317. // If no lecture is found, display an appropriate error.
  318. req.flash( 'error', 'Invalid lecture specified!' );
  319. res.redirect( '/' );
  320. }
  321. });
  322. }
  323. // loadNote is used to load a note by it's id
  324. // This is a lot more complicated than the above
  325. // due to public/private handling of notes.
  326. function loadNote( req, res, next ) {
  327. var user = req.user ? req.user : false;
  328. var noteId = req.params.id;
  329. Note.findById( noteId, function( err, note ) {
  330. // If a note is found, and user is set, check if
  331. // user is authorized to interact with that note.
  332. if( note && user && !note.deleted ) {
  333. note.authorize( user, function( auth ) {
  334. if( auth ) {
  335. // If authorzied, then set req.note to be used later
  336. req.note = note;
  337. next();
  338. } else if ( note.public ) {
  339. // If not authorized, but the note is public, then
  340. // designate the note read only (RO) and store req.note
  341. req.RO = true;
  342. req.note = note;
  343. next();
  344. } else {
  345. // If the user is not authorized and the note is private
  346. // then display and error.
  347. req.flash( 'error', 'You do not have permission to access that note.' );
  348. res.redirect( '/' );
  349. }
  350. })
  351. } else if ( note && note.public && !note.deleted ) {
  352. // If note is found, but user is not set because they are not
  353. // logged in, and the note is public, set the note to read only
  354. // and store the note for later.
  355. req.note = note;
  356. req.RO = true;
  357. next();
  358. } else if ( note && !note.public && !note.deleted ) {
  359. // If the note is found, but user is not logged in and the note is
  360. // not public, then ask them to login to view the note. Once logged
  361. // in they will be redirected to the note, at which time authorization
  362. // handling will be put in effect above.
  363. req.session.redirect = '/note/' + note._id;
  364. req.flash( 'error', 'You must be logged in to view that note.' );
  365. res.redirect( '/login' );
  366. } else {
  367. // No note was found
  368. req.flash( 'error', 'Invalid note specified!' );
  369. res.redirect( '/schools' );
  370. }
  371. });
  372. }
  373. function checkAjax( req, res, next ) {
  374. if ( req.xhr ) {
  375. next();
  376. } else {
  377. res.redirect( '/' );
  378. }
  379. }
  380. // Dynamic Helpers are loaded automatically into views
  381. app.dynamicHelpers( {
  382. // express-messages is for flash messages for easy
  383. // errors and information display
  384. 'messages' : require( 'express-messages' ),
  385. // By default the req object isn't sen't to views
  386. // during rendering, this allows you to use the
  387. // user object if available in views.
  388. 'user' : function( req, res ) {
  389. return req.user;
  390. },
  391. // Same, this allows session to be available in views.
  392. 'session' : function( req, res ) {
  393. return req.session;
  394. }
  395. });
  396. // Routes
  397. // The following are the main CRUD routes that are used
  398. // to make up this web app.
  399. // Homepage
  400. // Public
  401. app.get( '/', loadUser, function( req, res ) {
  402. log3("get / page");
  403. res.render( 'index' );
  404. });
  405. // Schools list
  406. // Used to display all available schools and any courses
  407. // in those schools.
  408. // Public with some private information
  409. app.get( '/schools', checkAjax, loadUser, function( req, res ) {
  410. var user = req.user;
  411. var schoolList = [];
  412. // Find all schools and sort by name
  413. // XXX mongoose's documentation on sort is extremely poor, tread carefully
  414. School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
  415. if( schools ) {
  416. // If schools are found, loop through them gathering any courses that are
  417. // associated with them and then render the page with that information.
  418. async.forEach(
  419. schools,
  420. function( school, callback ) {
  421. // Check if user is authorized with each school
  422. school.authorize( user, function( authorized ) {
  423. // This is used to display interface elements for those users
  424. // that are are allowed to see them, for instance a 'New Course' button.
  425. var sanitizedSchool = school.sanitized;
  426. sanitizedSchool.authorized = authorized;
  427. // Find all courses for school by it's id and sort by name
  428. Course.find( { 'school' : school._id } ).sort( 'name', '1' ).run( function( err, courses ) {
  429. // If any courses are found, set them to the appropriate school, otherwise
  430. // leave empty.
  431. if( courses.length > 0 ) {
  432. sanitizedSchool.courses = courses.filter(function(course) {
  433. if (!course.deleted) return course;
  434. });
  435. } else {
  436. sanitizedSchool.courses = [];
  437. }
  438. schoolList.push(sanitizedSchool);
  439. // This tells async (the module) that each iteration of forEach is
  440. // done and will continue to call the rest until they have all been
  441. // completed, at which time the last function below will be called.
  442. callback();
  443. });
  444. });
  445. },
  446. // After all schools and courses have been found, render them
  447. function( err ) {
  448. //res.render( 'schools', { 'schools' : schools } );
  449. res.json({ 'schools' : schoolList });
  450. }
  451. );
  452. } else {
  453. // If no schools have been found, display none
  454. //res.render( 'schools', { 'schools' : [] } );
  455. res.json({ 'schools' : [] });
  456. }
  457. });
  458. });
  459. // New course page
  460. // Displays form to create new course
  461. // Private, requires user to be authorized
  462. app.get( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
  463. // Load school from middleware
  464. var school = req.school;
  465. // If school was not loaded for whatever reason, or the user is not authorized
  466. // then redirect to the main schools page.
  467. if( ( ! school ) || ( ! school.authorized ) ) {
  468. return res.redirect( '/schools' );
  469. }
  470. // If they are authorized and the school exists, then render the page
  471. res.render( 'course/new', { 'school': school } );
  472. });
  473. // Recieves new course form
  474. app.post( '/:id/course/new', loadUser, loadSchool, function( req, res ) {
  475. var school = req.school;
  476. // Creates new course from Course Schema
  477. var course = new Course;
  478. // Gathers instructor information from form
  479. var instructorEmail = req.body.email.toLowerCase();
  480. var instructorName = req.body.instructorName;
  481. // If school doesn't exist or user is not authorized redirect to main schools page
  482. if( ( ! school ) || ( ! school.authorized ) ) {
  483. res.redirect( '/schools' );
  484. }
  485. // If instructorEmail isn't set, or name isn't set, display error and re-render the page.
  486. if ( !instructorEmail || !instructorName ) {
  487. req.flash( 'error', 'Invalid parameters!' )
  488. return res.render( 'course/new' );
  489. }
  490. // Fill out the course with information from the form
  491. course.number = req.body.number;
  492. course.name = req.body.name;
  493. course.description = req.body.description;
  494. course.school = school._id;
  495. course.creator = req.user._id;
  496. course.subject = req.body.subject;
  497. course.department = req.body.department;
  498. // Check if a user exists with the instructorEmail, if not then create
  499. // a new user and send them an instructor welcome email.
  500. User.findOne( { 'email' : instructorEmail }, function( err, user ) {
  501. if ( !user ) {
  502. var user = new User;
  503. user.name = instructorName
  504. user.email = instructorEmail;
  505. user.affil = 'Instructor';
  506. user.school = school.name;
  507. user.activated = false;
  508. // Validate instructorEmail
  509. // XXX Probably could be done before checking db
  510. if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
  511. req.flash( 'error', 'Please enter a valid email' );
  512. // XXX This needs to be fixed, this is not the proper flow
  513. return res.redirect( '/register' );
  514. }
  515. // Once the new user information has been completed, save the user
  516. // to the database then email them the instructor welcome email.
  517. user.save(function( err ) {
  518. // If there was an error saving the instructor, prompt the user to fill out
  519. // the information again.
  520. if ( err ) {
  521. req.flash( 'error', 'Invalid parameters!' )
  522. return res.render( 'course/new' );
  523. } else {
  524. var message = {
  525. to : user.email,
  526. 'subject' : 'A non-profit open education initiative',
  527. 'template' : 'instructorInvite',
  528. 'locals' : {
  529. 'course' : course,
  530. 'school' : school,
  531. 'user' : user,
  532. 'serverHost' : serverHost
  533. }
  534. };
  535. mailer.send( message, function( err, result ) {
  536. if( err ) {
  537. console.log( 'Error inviting instructor to course!' );
  538. } else {
  539. console.log( 'Successfully invited instructor to course.' );
  540. }
  541. });
  542. // After emails are sent, set the courses instructor to the
  543. // new users id and then save the course to the database.
  544. course.instructor = user._id;
  545. course.save( function( err ) {
  546. if( err ) {
  547. // XXX better validation
  548. req.flash( 'error', 'Invalid parameters!' );
  549. return res.render( 'course/new' );
  550. } else {
  551. // Once the course has been completed email the admin with information
  552. // on the course and new instructor
  553. var message = {
  554. to : ADMIN_EMAIL,
  555. 'subject' : school.name+' has a new course: '+course.name,
  556. 'template' : 'newCourse',
  557. 'locals' : {
  558. 'course' : course,
  559. 'instructor' : user,
  560. 'user' : req.user,
  561. 'serverHost' : serverHost
  562. }
  563. };
  564. mailer.send( message, function( err, result ) {
  565. if ( err ) {
  566. console.log( 'Error sending new course email to info@finalsclub.org' )
  567. } else {
  568. console.log( 'Successfully invited instructor to course')
  569. }
  570. })
  571. // Redirect the user to the schools page where they can see
  572. // their new course.
  573. // XXX Redirect to the new course instead
  574. res.redirect( '/schools' );
  575. }
  576. });
  577. }
  578. })
  579. } else {
  580. // If the user exists, then check if they are already and instructor
  581. if (user.affil === 'Instructor') {
  582. // If they are an instructor, then save the course with the appropriate
  583. // information and email the admin.
  584. course.instructor = user._id;
  585. course.save( function( err ) {
  586. if( err ) {
  587. // XXX better validation
  588. req.flash( 'error', 'Invalid parameters!' );
  589. return res.render( 'course/new' );
  590. } else {
  591. var message = {
  592. to : ADMIN_EMAIL,
  593. 'subject' : school.name+' has a new course: '+course.name,
  594. 'template' : 'newCourse',
  595. 'locals' : {
  596. 'course' : course,
  597. 'instructor' : user,
  598. 'user' : req.user,
  599. 'serverHost' : serverHost
  600. }
  601. };
  602. mailer.send( message, function( err, result ) {
  603. if ( err ) {
  604. console.log( 'Error sending new course email to info@finalsclub.org' )
  605. } else {
  606. console.log( 'Successfully invited instructor to course')
  607. }
  608. })
  609. // XXX Redirect to the new course instead
  610. res.redirect( '/schools' );
  611. }
  612. });
  613. } else {
  614. // The existing user isn't an instructor, so the user is notified of the error
  615. // and the course isn't created.
  616. req.flash( 'error', 'The existing user\'s email you entered is not an instructor' );
  617. res.render( 'course/new' );
  618. }
  619. }
  620. })
  621. });
  622. // Individual Course Listing
  623. // Public with private information
  624. app.get( '/course/:id', loadUser, loadCourse, function( req, res ) {
  625. var userId = req.user._id;
  626. var course = req.course;
  627. // Check if the user is subscribed to the course
  628. // XXX Not currently used for anything
  629. var subscribed = course.subscribed( userId );
  630. // Find lectures associated with this course and sort by name
  631. Lecture.find( { 'course' : course._id } ).sort( 'name', '1' ).run( function( err, lectures ) {
  632. // Get course instructor information using their id
  633. User.findById( course.instructor, function( err, instructor ) {
  634. // Render course and lectures
  635. res.render( 'course/index', { 'course' : course, 'instructor': instructor, 'subscribed' : subscribed, 'lectures' : lectures } );
  636. })
  637. });
  638. });
  639. // Edit Course
  640. app.get( '/course/:id/edit', loadUser, loadCourse, function( req, res) {
  641. var course = req.course;
  642. var user = req.user;
  643. if ( user.admin ) {
  644. res.render( 'course/new', {course: course} )
  645. } else {
  646. req.flash( 'error', 'You don\'t have permission to do that' )
  647. res.redirect( '/schools' );
  648. }
  649. })
  650. // Recieve Course Edit Form
  651. app.post( '/course/:id/edit', loadUser, loadCourse, function( req, res ) {
  652. var course = req.course;
  653. var user = req.user;
  654. if (user.admin) {
  655. var courseChanges = req.body;
  656. course.number = courseChanges.number;
  657. course.name = courseChanges.name;
  658. course.description = courseChanges.description;
  659. course.department = courseChanges.department;
  660. course.save(function(err) {
  661. if (err) {
  662. req.flash( 'error', 'There was an error saving the course' );
  663. }
  664. res.redirect( '/course/'+ course._id.toString());
  665. })
  666. } else {
  667. req.flash( 'error', 'You don\'t have permission to do that' )
  668. res.redirect( '/schools' );
  669. }
  670. });
  671. // Delete Course
  672. app.get( '/course/:id/delete', loadUser, loadCourse, function( req, res) {
  673. var course = req.course;
  674. var user = req.user;
  675. if ( user.admin ) {
  676. course.delete(function( err ) {
  677. if ( err ) req.flash( 'info', 'There was a problem removing course: ' + err )
  678. else req.flash( 'info', 'Successfully removed course' )
  679. res.redirect( '/schools' );
  680. });
  681. } else {
  682. req.flash( 'error', 'You don\'t have permission to do that' )
  683. res.redirect( '/schools' );
  684. }
  685. })
  686. // Subscribe to course
  687. // XXX Not currently used for anything
  688. app.get( '/course/:id/subscribe', loadUser, loadCourse, function( req, res ) {
  689. var course = req.course;
  690. var userId = req.user._id;
  691. course.subscribe( userId, function( err ) {
  692. if( err ) {
  693. req.flash( 'error', 'Error subscribing to course!' );
  694. }
  695. res.redirect( '/course/' + course._id );
  696. });
  697. });
  698. // Unsubscribe from course
  699. // XXX Not currently used for anything
  700. app.get( '/course/:id/unsubscribe', loadUser, loadCourse, function( req, res ) {
  701. var course = req.course;
  702. var userId = req.user._id;
  703. course.unsubscribe( userId, function( err ) {
  704. if( err ) {
  705. req.flash( 'error', 'Error unsubscribing from course!' );
  706. }
  707. res.redirect( '/course/' + course._id );
  708. });
  709. });
  710. // Create new lecture
  711. app.get( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
  712. var courseId = req.params.id;
  713. var course = req.course;
  714. var lecture = {};
  715. // If course isn't valid or user isn't authorized for course, redirect
  716. if( ( ! course ) || ( ! course.authorized ) ) {
  717. return res.redirect( '/course/' + courseId );
  718. }
  719. // Render new lecture form
  720. res.render( 'lecture/new', { 'lecture' : lecture } );
  721. });
  722. // Recieve New Lecture Form
  723. app.post( '/course/:id/lecture/new', loadUser, loadCourse, function( req, res ) {
  724. var courseId = req.params.id;
  725. var course = req.course;
  726. // Create new lecture from Lecture schema
  727. var lecture = new Lecture;
  728. if( ( ! course ) || ( ! course.authorized ) ) {
  729. res.redirect( '/course/' + courseId );
  730. return;
  731. }
  732. // Populate lecture with form data
  733. lecture.name = req.body.name;
  734. lecture.date = req.body.date;
  735. lecture.course = course._id;
  736. lecture.creator = req.user._id;
  737. // Save lecture to database
  738. lecture.save( function( err ) {
  739. if( err ) {
  740. // XXX better validation
  741. req.flash( 'error', 'Invalid parameters!' );
  742. res.render( 'lecture/new', { 'lecture' : lecture } );
  743. } else {
  744. // XXX Redirect to new lecture instead
  745. res.redirect( '/course/' + course._id );
  746. }
  747. });
  748. });
  749. // Display individual lecture and related notes
  750. app.get( '/lecture/:id', loadUser, loadLecture, function( req, res ) {
  751. var lecture = req.lecture;
  752. // Grab the associated course
  753. // XXX this should be done with DBRefs eventually
  754. Course.findById( lecture.course, function( err, course ) {
  755. if( course ) {
  756. // If course is found, find instructor information to be displayed on page
  757. User.findById( course.instructor, function( err, instructor ) {
  758. // Pull out our notes
  759. Note.find( { 'lecture' : lecture._id } ).sort( 'name', '1' ).run( function( err, notes ) {
  760. if ( !req.user.loggedIn || !req.lecture.authorized ) {
  761. // Loop through notes and only return those that are public if the
  762. // user is not logged in or not authorized for that lecture
  763. notes = notes.filter(function( note ) {
  764. if ( note.public ) return note;
  765. })
  766. }
  767. // Render lecture and notes
  768. res.render( 'lecture/index', {
  769. 'lecture' : lecture,
  770. 'course' : course,
  771. 'instructor' : instructor,
  772. 'notes' : notes,
  773. 'counts' : counts,
  774. 'javascripts' : [ 'counts.js' ]
  775. });
  776. });
  777. })
  778. } else {
  779. // XXX with DBRefs we will be able to reassign orphaned courses/lecture/pads
  780. req.flash( 'error', 'That lecture is orphaned!' );
  781. res.redirect( '/' );
  782. }
  783. });
  784. });
  785. // Display new note form
  786. app.get( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
  787. var lectureId = req.params.id;
  788. var lecture = req.lecture;
  789. var note = {};
  790. if( ( ! lecture ) || ( ! lecture.authorized ) ) {
  791. res.redirect( '/lecture/' + lectureId );
  792. return;
  793. }
  794. res.render( 'notes/new', { 'note' : note } );
  795. });
  796. // Recieve new note form
  797. app.post( '/lecture/:id/notes/new', loadUser, loadLecture, function( req, res ) {
  798. var lectureId = req.params.id;
  799. var lecture = req.lecture;
  800. if( ( ! lecture ) || ( ! lecture.authorized ) ) {
  801. res.redirect( '/lecture/' + lectureId );
  802. return;
  803. }
  804. // Create note from Note schema
  805. var note = new Note;
  806. // Populate note from form data
  807. note.name = req.body.name;
  808. note.date = req.body.date;
  809. note.lecture = lecture._id;
  810. note.public = req.body.private ? false : true;
  811. note.creator = req.user._id;
  812. // Save note to database
  813. note.save( function( err ) {
  814. if( err ) {
  815. // XXX better validation
  816. req.flash( 'error', 'Invalid parameters!' );
  817. res.render( 'notes/new', { 'note' : note } );
  818. } else {
  819. // XXX Redirect to new note instead
  820. res.redirect( '/lecture/' + lecture._id );
  821. }
  822. });
  823. });
  824. // Display individual note page
  825. app.get( '/note/:id', loadUser, loadNote, function( req, res ) {
  826. var note = req.note;
  827. // Set read only id for etherpad-lite or false for later check
  828. var roID = note.roID || false;
  829. var lectureId = note.lecture;
  830. // Count the amount of visits, but only once per session
  831. if ( req.session.visited ) {
  832. if ( req.session.visited.indexOf( note._id.toString() ) == -1 ) {
  833. req.session.visited.push( note._id );
  834. note.addVisit();
  835. }
  836. } else {
  837. req.session.visited = [];
  838. req.session.visited.push( note._id );
  839. note.addVisit();
  840. }
  841. // If a read only id exists process note
  842. if (roID) {
  843. processReq();
  844. } else {
  845. // If read only id doesn't, then fetch the read only id from the database and then
  846. // process note.
  847. // XXX Soon to be depracated due to a new API in etherpad that makes for a
  848. // much cleaner solution.
  849. db.open('mongodb://' + app.set( 'dbHost' ) + '/etherpad/etherpad', function( err, epl ) {
  850. epl.findOne( { key: 'pad2readonly:' + note._id }, function(err, record) {
  851. if ( record ) {
  852. roID = record.value.replace(/"/g, '');
  853. } else {
  854. roID = false;
  855. }
  856. processReq();
  857. })
  858. })
  859. }
  860. function processReq() {
  861. // Find lecture
  862. Lecture.findById( lectureId, function( err, lecture ) {
  863. if( ! lecture ) {
  864. req.flash( 'error', 'That notes page is orphaned!' );
  865. res.redirect( '/' );
  866. }
  867. // Find notes based on lecture id, which will be displayed in a dropdown
  868. // on the page
  869. Note.find( { 'lecture' : lecture._id }, function( err, otherNotes ) {
  870. if( !req.RO ) {
  871. // User is logged in and sees full notepad
  872. res.render( 'notes/index', {
  873. 'layout' : 'noteLayout',
  874. 'host' : serverHost,
  875. 'note' : note,
  876. 'lecture' : lecture,
  877. 'otherNotes' : otherNotes,
  878. 'RO' : false,
  879. 'roID' : roID,
  880. 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
  881. 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
  882. });
  883. } else {
  884. // User is not logged in and sees notepad that is public
  885. res.render( 'notes/public', {
  886. 'layout' : 'noteLayout',
  887. 'host' : serverHost,
  888. 'note' : note,
  889. 'otherNotes' : otherNotes,
  890. 'roID' : roID,
  891. 'lecture' : lecture,
  892. 'stylesheets' : [ 'dropdown.css', 'fc2.css' ],
  893. 'javascripts' : [ 'dropdown.js', 'counts.js', 'backchannel.js', 'jquery.tmpl.min.js' ]
  894. });
  895. }
  896. });
  897. });
  898. }
  899. });
  900. // Static pages and redirects
  901. app.get( '/about', loadUser, function( req, res ) {
  902. res.redirect( 'http://blog.finalsclub.org/about.html' );
  903. });
  904. app.get( '/press', loadUser, function( req, res ) {
  905. res.render( 'static/press' );
  906. });
  907. app.get( '/conduct', loadUser, function( req, res ) {
  908. res.render( 'static/conduct' );
  909. });
  910. app.get( '/legal', loadUser, function( req, res ) {
  911. res.redirect( 'http://blog.finalsclub.org/legal.html' );
  912. });
  913. app.get( '/contact', loadUser, function( req, res ) {
  914. res.redirect( 'http://blog.finalsclub.org/contact.html' );
  915. });
  916. app.get( '/privacy', loadUser, function( req, res ) {
  917. res.render( 'static/privacy' );
  918. });
  919. // Authentication routes
  920. // These are used for logging in, logging out, registering
  921. // and other user authentication purposes
  922. // Render login page
  923. app.get( '/login', function( req, res ) {
  924. log3("get login page")
  925. res.render( 'login' );
  926. });
  927. // Recieve login form
  928. app.post( '/login', function( req, res ) {
  929. var email = req.body.email;
  930. var password = req.body.password;
  931. log3("post login ...")
  932. // Find user from email
  933. User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
  934. log3(err)
  935. log3(user)
  936. // If user exists, check if activated, if not notify them and send them to
  937. // the login form
  938. if( user ) {
  939. if( ! user.activated ) {
  940. // (undocumented) markdown-esque link functionality in req.flash
  941. req.flash( 'error', 'This account isn\'t activated. Check your inbox or [click here](/resendActivation) to resend the activation email.' );
  942. req.session.activateCode = user._id;
  943. res.render( 'login' );
  944. } else {
  945. // If user is activated, check if their password is correct
  946. if( user.authenticate( password ) ) {
  947. log3("pass ok")
  948. var sid = req.sessionID;
  949. user.session = sid;
  950. // Set the session then save the user to the database
  951. user.save( function() {
  952. var redirect = req.session.redirect;
  953. // login complete, remember the user's email for next time
  954. req.session.email = email;
  955. // alert the successful login
  956. req.flash( 'info', 'Successfully logged in!' );
  957. // redirect to profile if we don't have a stashed request
  958. res.redirect( redirect || '/profile' );
  959. });
  960. } else {
  961. // Notify user of bad login
  962. req.flash( 'error', 'Invalid login!' );
  963. res.render( 'login' );
  964. }
  965. }
  966. } else {
  967. // Notify user of bad login
  968. log3("bad login")
  969. req.flash( 'error', 'Invalid login!' );
  970. res.render( 'login' );
  971. }
  972. });
  973. });
  974. // Request reset password
  975. app.get( '/resetpw', function( req, res ) {
  976. log3("get resetpw page");
  977. res.render( 'resetpw' );
  978. });
  979. // Display reset password from requested email
  980. app.get( '/resetpw/:id', function( req, res ) {
  981. var resetPassCode = req.params.id
  982. res.render( 'resetpw', { 'verify': true, 'resetPassCode' : resetPassCode } );
  983. });
  984. // Recieve reset password request form
  985. app.post( '/resetpw', function( req, res ) {
  986. log3("post resetpw");
  987. var email = req.body.email
  988. // Search for user
  989. User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
  990. if( user ) {
  991. // If user exists, create reset code
  992. var resetPassCode = hat(64);
  993. user.setResetPassCode(resetPassCode);
  994. // Construct url that the user can then click to reset password
  995. var resetPassUrl = 'http://' + serverHost + ((app.address().port != 80)? ':'+app.address().port: '') + '/resetpw/' + resetPassCode;
  996. // Save user to database
  997. user.save( function( err ) {
  998. log3('save '+user.email);
  999. // Construct email and send it to the user
  1000. var message = {
  1001. 'to' : user.email,
  1002. 'subject' : 'Your FinalsClub.org Password has been Reset!',
  1003. 'template' : 'userPasswordReset',
  1004. 'locals' : {
  1005. 'resetPassCode' : resetPassCode,
  1006. 'resetPassUrl' : resetPassUrl
  1007. }
  1008. };
  1009. mailer.send( message, function( err, result ) {
  1010. if( err ) {
  1011. // XXX: Add route to resend this email
  1012. console.log( 'Error sending user password reset email!' );
  1013. } else {
  1014. console.log( 'Successfully sent user password reset email.' );
  1015. }
  1016. });
  1017. // Render request success page
  1018. res.render( 'resetpw-success', { 'email' : email } );
  1019. });
  1020. } else {
  1021. // Notify of error
  1022. res.render( 'resetpw-error', { 'email' : email } );
  1023. }
  1024. });
  1025. });
  1026. // Recieve reset password form
  1027. app.post( '/resetpw/:id', function( req, res ) {
  1028. log3("post resetpw.code");
  1029. var resetPassCode = req.params.id
  1030. var email = req.body.email
  1031. var pass1 = req.body.pass1
  1032. var pass2 = req.body.pass2
  1033. // Find user by email
  1034. User.findOne( { 'email' : email.toLowerCase() }, function( err, user ) {
  1035. var valid = false;
  1036. // If user exists, and the resetPassCode is valid, pass1 and pass2 match, then
  1037. // save user with new password and display success message.
  1038. if( user ) {
  1039. var valid = user.resetPassword(resetPassCode, pass1, pass2);
  1040. if (valid) {
  1041. user.save( function( err ) {
  1042. res.render( 'resetpw-success', { 'verify' : true, 'email' : email, 'resetPassCode' : resetPassCode } );
  1043. });
  1044. }
  1045. }
  1046. // If there was a problem, notify user
  1047. if (!valid) {
  1048. res.render( 'resetpw-error', { 'verify' : true, 'email' : email } );
  1049. }
  1050. });
  1051. });
  1052. // Display registration page
  1053. app.get( '/register', function( req, res ) {
  1054. log3("get reg page");
  1055. // Populate school dropdown list
  1056. School.find( {} ).sort( 'name', '1' ).run( function( err, schools ) {
  1057. res.render( 'register', { 'schools' : schools } );
  1058. })
  1059. });
  1060. // Recieve registration form
  1061. app.post( '/register', function( req, res ) {
  1062. var sid = req.sessionId;
  1063. // Create new user from User schema
  1064. var user = new User;
  1065. // Populate user from form
  1066. user.email = req.body.email.toLowerCase();
  1067. user.password = req.body.password;
  1068. user.session = sid;
  1069. // If school is set to other, then fill in school as what the
  1070. // user entered
  1071. user.school = req.body.school === 'Other' ? req.body.otherSchool : req.body.school;
  1072. user.name = req.body.name;
  1073. user.affil = req.body.affil;
  1074. user.activated = false;
  1075. // Validate email
  1076. if ( ( user.email === '' ) || ( !isValidEmail( user.email ) ) ) {
  1077. req.flash( 'error', 'Please enter a valid email' );
  1078. return res.redirect( '/register' );
  1079. }
  1080. // Check if password is greater than 6 characters, otherwise notify user
  1081. if ( req.body.password.length < 6 ) {
  1082. req.flash( 'error', 'Please enter a password longer than eight characters' );
  1083. return res.redirect( '/register' );
  1084. }
  1085. // Pull out hostname from email
  1086. var hostname = user.email.split( '@' ).pop();
  1087. // Check if email is from one of the special domains
  1088. if( /^(finalsclub.org|sleepless.com)$/.test( hostname ) ) {
  1089. user.admin = true;
  1090. }
  1091. // Save user to database
  1092. user.save( function( err ) {
  1093. // If error, check if it is because the user already exists, if so
  1094. // get the user information and let them know
  1095. if ( err ) {
  1096. if( /dup key/.test( err.message ) ) {
  1097. // attempting to register an existing address
  1098. User.findOne({ 'email' : user.email }, function(err, result ) {
  1099. if (result.activated) {
  1100. // If activated, make sure they know how to contact the admin
  1101. req.flash( 'error', 'There is already someone registered with this email, if this is in error contact info@finalsclub.org for help' )
  1102. return res.redirect( '/register' )
  1103. } else {
  1104. // If not activated, direct them to the resendActivation page
  1105. req.flash( 'error', 'There is already someone registered with this email, if this is you, please check your email for the activation code' )
  1106. return res.redirect( '/resendActivation' )
  1107. }
  1108. });
  1109. } else {
  1110. // If any other type of error, prompt them to enter the registration again
  1111. req.flash( 'error', 'An error occurred during registration.' );
  1112. return res.redirect( '/register' );
  1113. }
  1114. } else {
  1115. // send user activation email
  1116. sendUserActivation( user );
  1117. // Check if the hostname matches any in the approved schools
  1118. School.findOne( { 'hostnames' : hostname }, function( err, school ) {
  1119. if( school ) {
  1120. // If there is a match, send associated welcome message
  1121. sendUserWelcome( user, true );
  1122. log3('school recognized '+school.name);
  1123. // If no users exist for the school, create empty array
  1124. if (!school.users) school.users = [];
  1125. // Add user to the school
  1126. school.users.push( user._id );
  1127. // Save school to the database
  1128. school.save( function( err ) {
  1129. log3('school.save() done');
  1130. // Notify user that they have been added to the school
  1131. req.flash( 'info', 'You have automatically been added to the ' + school.name + ' network. Please check your email for the activation link' );
  1132. res.redirect( '/' );
  1133. });
  1134. // Construct admin email about user registration
  1135. var message = {
  1136. 'to' : ADMIN_EMAIL,
  1137. 'subject' : 'FC User Registration : User added to ' + school.name,
  1138. 'template' : 'userSchool',
  1139. 'locals' : {
  1140. 'user' : user
  1141. }
  1142. }
  1143. } else {
  1144. // If there isn't a match, send associated welcome message
  1145. sendUserWelcome( user, false );
  1146. // Tell user to check for activation link
  1147. req.flash( 'info', 'Your account has been created, please check your email for the activation link' )
  1148. res.redirect( '/' );
  1149. // Construct admin email about user registration
  1150. var message = {
  1151. 'to' : ADMIN_EMAIL,
  1152. 'subject' : 'FC User Registration : Email did not match any schools',
  1153. 'template' : 'userNoSchool',
  1154. 'locals' : {
  1155. 'user' : user
  1156. }
  1157. }
  1158. }
  1159. // Send email to admin
  1160. mailer.send( message, function( err, result ) {
  1161. if ( err ) {
  1162. console.log( 'Error sending user has no school email to admin\nError Message: '+err.Message );
  1163. } else {
  1164. console.log( 'Successfully sent user has no school email to admin.' );
  1165. }
  1166. })
  1167. });
  1168. }
  1169. });
  1170. });
  1171. // Display resendActivation request page
  1172. app.get( '/resendActivation', function( req, res ) {
  1173. var activateCode = req.session.activateCode;
  1174. // Check if user exists by activateCode set in their session
  1175. User.findById( activateCode, function( err, user ) {
  1176. if( ( ! user ) || ( user.activated ) ) {
  1177. res.redirect( '/' );
  1178. } else {
  1179. // Send activation and redirect to login
  1180. sendUserActivation( user );
  1181. req.flash( 'info', 'Your activation code has been resent.' );
  1182. res.redirect( '/login' );
  1183. }
  1184. });
  1185. });
  1186. // Display activation page
  1187. app.get( '/activate/:code', function( req, res ) {
  1188. var code = req.params.code;
  1189. // XXX could break this out into a middleware
  1190. if( ! code ) {
  1191. res.redirect( '/' );
  1192. }
  1193. // Find user by activation code
  1194. User.findById( code, function( err, user ) {
  1195. if( err || ! user ) {
  1196. // If not found, notify user of invalid code
  1197. req.flash( 'error', 'Invalid activation code!' );
  1198. res.redirect( '/' );
  1199. } else {
  1200. // If valid, then activate user
  1201. user.activated = true;
  1202. // Regenerate our session and log in as the new user
  1203. req.session.regenerate( function() {
  1204. user.session = req.sessionID;
  1205. // Save user to database
  1206. user.save( function( err ) {
  1207. if( err ) {
  1208. req.flash( 'error', 'Unable to activate account.' );
  1209. res.redirect( '/' );
  1210. } else {
  1211. req.flash( 'info', 'Account successfully activated. Please complete your profile.' );
  1212. res.redirect( '/profile' );
  1213. }
  1214. });
  1215. });
  1216. }
  1217. });
  1218. });
  1219. // Logut user
  1220. app.get( '/logout', function( req, res ) {
  1221. var sid = req.sessionID;
  1222. // Find user by session id
  1223. User.findOne( { 'session' : sid }, function( err, user ) {
  1224. if( user ) {
  1225. // Empty out session id
  1226. user.session = '';
  1227. // Save user to database
  1228. user.save( function( err ) {
  1229. res.redirect( '/' );
  1230. });
  1231. } else {
  1232. res.redirect( '/' );
  1233. }
  1234. });
  1235. });
  1236. // Display users profile page
  1237. app.get( '/profile', loadUser, loggedIn, function( req, res ) {
  1238. var user = req.user;
  1239. res.render( 'profile/index', { 'user' : user } );
  1240. });
  1241. // Recieve profile edit page form
  1242. app.post( '/profile', loadUser, loggedIn, function( req, res ) {
  1243. var user = req.user;
  1244. var fields = req.body;
  1245. var error = false;
  1246. var wasComplete = user.isComplete;
  1247. if( ! fields.name ) {
  1248. req.flash( 'error', 'Please enter a valid name!' );
  1249. error = true;
  1250. } else {
  1251. user.name = fields.name;
  1252. }
  1253. if( [ 'Student', 'Teachers Assistant' ].indexOf( fields.affiliation ) == -1 ) {
  1254. req.flash( 'error', 'Please select a valid affiliation!' );
  1255. error = true;
  1256. } else {
  1257. user.affil = fields.affiliation;
  1258. }
  1259. if( fields.existingPassword || fields.newPassword || fields.newPasswordConfirm ) {
  1260. // changing password
  1261. if( ( ! user.hashed ) || user.authenticate( fields.existingPassword ) ) {
  1262. if( fields.newPassword === fields.newPasswordConfirm ) {
  1263. // test password strength?
  1264. user.password = fields.newPassword;
  1265. } else {
  1266. req.flash( 'error', 'Mismatch in new password!' );
  1267. error = true;
  1268. }
  1269. } else {
  1270. req.flash( 'error', 'Please supply your existing password.' );
  1271. error = true;
  1272. }
  1273. }
  1274. user.major = fields.major;
  1275. user.bio = fields.bio;
  1276. user.showName = ( fields.showName ? true : false );
  1277. if( ! error ) {
  1278. user.save( function( err ) {
  1279. if( err ) {
  1280. req.flash( 'error', 'Unable to save user profile!' );
  1281. } else {
  1282. if( ( user.isComplete ) && ( ! wasComplete ) ) {
  1283. req.flash( 'info', 'Your account is now fully activated. Thank you for joining FinalsClub!' );
  1284. res.redirect( '/' );
  1285. } else {
  1286. res.render( 'info', 'Your profile was successfully updated!' );
  1287. res.render( 'profile/index', { 'user' : user } );
  1288. }
  1289. }
  1290. });
  1291. } else {
  1292. res.render( 'profile/index', { 'user' : user } );
  1293. }
  1294. });
  1295. // Old Notes
  1296. function loadSubject( req, res, next ) {
  1297. if( url.parse( req.url ).pathname.match(/subject/) ) {
  1298. ArchivedSubject.findOne({id: req.params.id }, function(err, subject) {
  1299. if ( err ) {
  1300. req.flash( 'error', 'Subject with this ID does not exist' )
  1301. res.redirect( '/archive' );
  1302. } else {
  1303. req.subject = subject;
  1304. next()
  1305. }
  1306. })
  1307. } else {
  1308. next()
  1309. }
  1310. }
  1311. function loadOldCourse( req, res, next ) {
  1312. if( url.parse( req.url ).pathname.match(/course/) ) {
  1313. ArchivedCourse.findOne({id: req.params.id }, function(err, course) {
  1314. if ( err ) {
  1315. req.flash( 'error', 'Course with this ID does not exist' )
  1316. res.redirect( '/archive' );
  1317. } else {
  1318. req.course = course;
  1319. next()
  1320. }
  1321. })
  1322. } else {
  1323. next()
  1324. }
  1325. }
  1326. var featuredCourses = [
  1327. {name: 'The Human Mind', 'id': 1563},
  1328. {name: 'Justice', 'id': 797},
  1329. {name: 'Protest Literature', 'id': 1681},
  1330. {name: 'Animal Cognition', 'id': 681},
  1331. {name: 'Positive Psychology', 'id': 1793},
  1332. {name: 'Social Psychology', 'id': 660},
  1333. {name: 'The Book from Gutenberg to the Internet', 'id': 1439},
  1334. {name: 'Cyberspace in Court', 'id': 1446},
  1335. {name: 'Nazi Cinema', 'id': 2586},
  1336. {name: 'Media and the American Mind', 'id': 2583},
  1337. {name: 'Social Thought in Modern America', 'id': 2585},
  1338. {name: 'Major British Writers II', 'id': 869},
  1339. {name: 'Civil Procedure', 'id': 2589},
  1340. {name: 'Evidence', 'id': 2590},
  1341. {name: 'Management of Industrial and Nonprofit Organizations', 'id': 2591},
  1342. ];
  1343. app.get( '/learn', loadUser, function( req, res ) {
  1344. res.render( 'archive/learn', { 'courses' : featuredCourses } );
  1345. })
  1346. app.get( '/learn/random', loadUser, function( req, res ) {
  1347. res.redirect( '/archive/course/'+ featuredCourses[Math.floor(Math.random()*featuredCourses.length)].id);
  1348. })
  1349. app.get( '/archive', loadUser, function( req, res ) {
  1350. ArchivedSubject.find({}).sort( 'name', '1' ).run( function( err, subjects ) {
  1351. if ( err ) {
  1352. req.flash( 'error', 'There was a problem gathering the archived courses, please try again later.' );
  1353. res.redirect( '/' );
  1354. } else {
  1355. res.render( 'archive/index', { 'subjects' : subjects } );
  1356. }
  1357. })
  1358. })
  1359. app.get( '/archive/subject/:id', loadUser, loadSubject, function( req, res ) {
  1360. ArchivedCourse.find({subject_id: req.params.id}).sort('name', '1').run(function(err, courses) {
  1361. if ( err ) {
  1362. req.flash( 'error', 'There are no archived courses' );
  1363. res.redirect( '/' );
  1364. } else {
  1365. res.render( 'archive/courses', { 'courses' : courses, 'subject': req.subject } );
  1366. }
  1367. })
  1368. })
  1369. app.get( '/archive/course/:id', loadUser, loadOldCourse, function( req, res ) {
  1370. ArchivedNote.find({course_id: req.params.id}).sort('name', '1').run(function(err, notes) {
  1371. if ( err ) {
  1372. req.flash( 'error', 'There are no notes in this course' );
  1373. res.redirect( '/archive' );
  1374. } else {
  1375. res.render( 'archive/notes', { 'notes' : notes, 'course' : req.course } );
  1376. }
  1377. })
  1378. })
  1379. app.get( '/archive/note/:id', loadUser, function( req, res ) {
  1380. ArchivedNote.findById(req.params.id, function(err, note) {
  1381. if ( err ) {
  1382. req.flash( 'error', 'This is not a valid id for a note' );
  1383. res.redirect( '/archive' );
  1384. } else {
  1385. ArchivedCourse.findOne({id: note.course_id}, function(err, course) {
  1386. if ( err ) {
  1387. req.flash( 'error', 'There is no course for this note' )
  1388. res.redirect( '/archive' )
  1389. } else {
  1390. res.render( 'archive/note', { 'layout' : 'notesLayout', 'note' : note, 'course': course } );
  1391. }
  1392. })
  1393. }
  1394. })
  1395. })
  1396. // socket.io server
  1397. // The finalsclub backchannel server uses socket.io to handle communication between the server and
  1398. // the browser which facilitates near realtime interaction. This allows the user to post questions
  1399. // and comments and other users to get those almost immediately after they are posted, without
  1400. // reloading the page or pressing a button to refresh.
  1401. //
  1402. // The server code itself is fairly simple, mainly taking incomming messages from client browsers,
  1403. // saving the data to the database, and then sending it out to everyone else connected.
  1404. //
  1405. // Data types:
  1406. // Posts - Posts are the main items in backchannel, useful for questions or discussion points
  1407. // [[ example object needed with explanation E.G:
  1408. /*
  1409. Post: { postID: '999-1',
  1410. userID: '1234',
  1411. userName: 'Bob Jones',
  1412. userAffil: 'Instructor',
  1413. body: 'This is the text content of the post.',
  1414. comments: { {<commentObj>, <commentObj>, ...},
  1415. public: true,
  1416. votes: [ <userID>, <userID>, ...],
  1417. reports: [ <userID>, <userID>, ...]
  1418. }
  1419. Comment: { body: 'foo bar', userName: 'Bob Jones', userAffil: 'Instructor' }
  1420. if anonymous: userName => 'Anonymous', userAffil => 'N/A'
  1421. */
  1422. //
  1423. //
  1424. //
  1425. // Comments - Comments are replies to posts, for clarification or answering questions
  1426. // [[ example object needed]]
  1427. // Votes - Votes signifyg a users approval of a post
  1428. // [[ example object needed]]
  1429. // Flags - Flagging a post signifies that it is against the rules, 2 flags moves it to the bottomw
  1430. // [[ example object needed]]
  1431. //
  1432. //
  1433. // Post Schema
  1434. // body - Main content of the post
  1435. // userId - Not currently used, but would contain the users id that made the post
  1436. // userName - Users name that made post
  1437. // userAffil - Users affiliation to their school
  1438. // public - Boolean which denotes if the post is public to everyone, or private to school users only
  1439. // date - Date post was made, updates when any comments are made for the post
  1440. // comments - An array of comments which contain a body, userName, and userAffil
  1441. // votes - An array of user ids which are the users that voted
  1442. // [[ example needed ]]
  1443. // reports - An array of user ids which are the users that reported the post
  1444. // [[ reports would be "this post is flagged as inappropriate"? ]]
  1445. // [[ bruml: consistent terminology needed ]]
  1446. //
  1447. // Posts and comments can be made anonymously. When a post is anonymous, the users info is stripped
  1448. // from the post and the userName is set to Anonymous and the userAffil to N/A. This is to allow
  1449. // users the ability to make posts or comments that they might not otherwise due to not wanting
  1450. // the content of the post/comment to be attributed to them.
  1451. //
  1452. // Each time a user connects to the server, it passes through authorization which checks for a cookie
  1453. // that is set by Express. If a session exists and it is for a valid logged in user, then handshake.user
  1454. // is set to the users data, otherwise it is set to false. handshake.user is used later on to check if a
  1455. // user is logged in, and if so display information that otherwise might not be visible to them if they
  1456. // aren't apart of a particular school.
  1457. //
  1458. // After the authorization step, the client browser sends the lecture id which is rendered into the html
  1459. // page on page load from Express. This is then used to assign a 'room' for the user which is grouped
  1460. // by lecture. All posts are grouped by lecture, and only exist for that lecture. After the user is
  1461. // grouped into a 'room', they are sent a payload of all existing posts for that lecture, which are then
  1462. // rendered in the browser.
  1463. //
  1464. // Everything else from this point on is handled in an event form and requires a user initiating it. The
  1465. // events are as follows.
  1466. //
  1467. // Post event
  1468. // A user makes a new post. A payload of data containing the post and lecture id is sent to the server.
  1469. // The server recieves the data, assembles a new post object for the database and then fills it with
  1470. // the appropriate data. If a user selected for the post to be anonymous, the userName and userAffil are
  1471. // replaced. If the user chose for the post to be private, then public will be set to false and it
  1472. // will be filtered from being sent to users not logged into and not having access to the school. Once
  1473. // the post has been created and saved into the database, it is sent to all connected users to that
  1474. // particular lecture, unless it is private, than only logged in users will get it.
  1475. //
  1476. // Vote event
  1477. // A user votes for a post. A payload of data containing the post id and lecture id are sent along with
  1478. // the user id. A new vote is created by first fetching the parent post, then adding the user id to the
  1479. // votes array, and then the post is subsequently saved back to the database and sent to all connected
  1480. // users unless the post is private, which then it will be only sent to logged in users.
  1481. //
  1482. // Report event
  1483. // Similar to the vote event, reports are sent as a payload of a post id, lecture id, and user id, which
  1484. // are then used to fetch the parent post, add the user id to the reports array, and then saved to the db.
  1485. // Then the report is sent out to all connected users unless it is a private post, which will be only sent
  1486. // to logged in users. On the client, once a post has more two (2) or more reports, it will be moved to the
  1487. // bottom of the interface.
  1488. //
  1489. // Comment event
  1490. // A user posts a comment to a post. A payload of data containing the post id, lecture id, comment body,
  1491. // user name, and user affiliation are sent to the server, which are then used to find the parent post
  1492. // and then a new comment object is assembled. When new comments are made, it updates the posts date
  1493. // which allows the post to be sorted by date and the posts with the freshest comments would be pushed
  1494. // to the top of the interface. The comment can be anonymous, which then will have the user
  1495. // name and affiliation stripped before saving to the database. The comment then will be sent out to all
  1496. // connected users unless the post is private, then only logged in users will recieve the comment.
  1497. var io = require( 'socket.io' ).listen( app );
  1498. var Post = mongoose.model( 'Post' );
  1499. io.set('authorization', function ( handshake, next ) {
  1500. var rawCookie = handshake.headers.cookie;
  1501. if (rawCookie) {
  1502. handshake.cookie = parseCookie(rawCookie);
  1503. handshake.sid = handshake.cookie['connect.sid'];
  1504. if ( handshake.sid ) {
  1505. app.set( 'sessionStore' ).get( handshake.sid, function( err, session ) {
  1506. if( err ) {
  1507. handshake.user = false;
  1508. return next(null, true);
  1509. } else {
  1510. // bake a new session object for full r/w
  1511. handshake.session = new Session( handshake, session );
  1512. User.findOne( { session : handshake.sid }, function( err, user ) {
  1513. if( user ) {
  1514. handshake.user = user;
  1515. return next(null, true);
  1516. } else {
  1517. handshake.user = false;
  1518. return next(null, true);
  1519. }
  1520. });
  1521. }
  1522. })
  1523. }
  1524. } else {
  1525. data.user = false;
  1526. return next(null, true);
  1527. }
  1528. });
  1529. var backchannel = new Backchannel(app, io.of('/backchannel'), {
  1530. subscribe: function(lecture, send) {
  1531. Post.find({'lecture': lecture}, function(err, posts) {
  1532. send(posts);
  1533. });
  1534. },
  1535. post: function(fillPost) {
  1536. var post = new Post;
  1537. fillPost(post, function(send) {
  1538. post.save(function(err) {
  1539. send();
  1540. });
  1541. });
  1542. },
  1543. items: function(postId, addItem) {
  1544. Post.findById(postId, function( err, post ) {
  1545. addItem(post, function(send) {
  1546. post.save(function(err) {
  1547. send();
  1548. });
  1549. })
  1550. })
  1551. }
  1552. });
  1553. var counters = {};
  1554. var counts = io
  1555. .of( '/counts' )
  1556. .on( 'connection', function( socket ) {
  1557. // pull out user/session information etc.
  1558. var handshake = socket.handshake;
  1559. var userID = handshake.user._id;
  1560. var watched = [];
  1561. var noteID = null;
  1562. var timer = null;
  1563. socket.on( 'join', function( note ) {
  1564. if (handshake.user === false) {
  1565. noteID = note;
  1566. // XXX: replace by addToSet (once it's implemented in mongoose)
  1567. Note.findById( noteID, function( err, note ) {
  1568. if( note ) {
  1569. if( note.collaborators.indexOf( userID ) == -1 ) {
  1570. note.collaborators.push( userID );
  1571. note.save();
  1572. }
  1573. }
  1574. });
  1575. }
  1576. });
  1577. socket.on( 'watch', function( l ) {
  1578. var sendCounts = function() {
  1579. var send = {};
  1580. Note.find( { '_id' : { '$in' : watched } }, function( err, notes ) {
  1581. async.forEach(
  1582. notes,
  1583. function( note, callback ) {
  1584. var id = note._id;
  1585. var count = note.collaborators.length;
  1586. send[ id ] = count;
  1587. callback();
  1588. }, function() {
  1589. socket.emit( 'counts', send );
  1590. timer = setTimeout( sendCounts, 5000 );
  1591. }
  1592. );
  1593. });
  1594. }
  1595. Note.find( { 'lecture' : l }, [ '_id' ], function( err, notes ) {
  1596. notes.forEach( function( note ) {
  1597. watched.push( note._id );
  1598. });
  1599. });
  1600. sendCounts();
  1601. });
  1602. socket.on( 'disconnect', function() {
  1603. clearTimeout( timer );
  1604. if (handshake.user === false) {
  1605. // XXX: replace with $pull once it's available
  1606. if( noteID ) {
  1607. Note.findById( noteID, function( err, note ) {
  1608. if( note ) {
  1609. var index = note.collaborators.indexOf( userID );
  1610. if( index != -1 ) {
  1611. note.collaborators.splice( index, 1 );
  1612. }
  1613. note.save();
  1614. }
  1615. });
  1616. }
  1617. }
  1618. });
  1619. });
  1620. // Exception Catch-All
  1621. process.on('uncaughtException', function (e) {
  1622. console.log("!!!!!! UNCAUGHT EXCEPTION\n" + e.stack);
  1623. });
  1624. // Launch
  1625. mongoose.connect( app.set( 'dbUri' ) );
  1626. mongoose.connection.db.serverConfig.connection.autoReconnect = true
  1627. var mailer = new Mailer( app.set('awsAccessKey'), app.set('awsSecretKey') );
  1628. app.listen( serverPort, function() {
  1629. console.log( "Express server listening on port %d in %s mode", app.address().port, app.settings.env );
  1630. // if run as root, downgrade to the owner of this file
  1631. if (process.getuid() === 0) {
  1632. require('fs').stat(__filename, function(err, stats) {
  1633. if (err) { return console.log(err); }
  1634. process.setuid(stats.uid);
  1635. });
  1636. }
  1637. });
  1638. function isValidEmail(email) {
  1639. 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])?/;
  1640. return email.match(re);
  1641. }