34 | Something's fishy here. 35 |
36 |37 | The page you were trying to reach doesn't exist. 38 |
39 |40 | Why might this be happening? 41 |
42 |├── .gitignore ├── Gruntfile.js ├── README.md ├── api ├── adapters │ └── .gitkeep ├── controllers │ ├── .gitkeep │ ├── AuthController.js │ ├── HomeController.js │ ├── TodoController.js │ └── UserController.js ├── models │ ├── .gitkeep │ ├── Todo.js │ └── User.js ├── policies │ └── authenticated.js └── services │ └── .gitkeep ├── app.js ├── assets ├── favicon.ico ├── images │ └── .gitkeep ├── js │ ├── .gitkeep │ ├── app.js │ ├── lib │ │ ├── angular.js │ │ └── jquery.js │ ├── sails.io.js │ ├── socket.io.js │ └── todo.js ├── robots.txt └── styles │ └── .gitkeep ├── config ├── 404.js ├── 500.js ├── adapters.js ├── bootstrap.js ├── controllers.js ├── cors.js ├── csrf.js ├── i18n.js ├── locales │ ├── _README.md │ ├── en.json │ └── es.json ├── log.js ├── middleware.js ├── policies.js ├── routes.js ├── session.js ├── sockets.js └── views.js ├── package.json └── views ├── 404.ejs ├── 500.ejs ├── auth └── index.ejs ├── home └── index.ejs └── layout.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | ######################## 2 | # sails 3 | ######################## 4 | .sails 5 | .waterline 6 | .rigging 7 | .tmp 8 | 9 | 10 | ######################## 11 | # node.js / npm 12 | ######################## 13 | lib-cov 14 | *.seed 15 | *.log 16 | *.csv 17 | *.dat 18 | *.out 19 | *.pid 20 | *.gz 21 | 22 | pids 23 | logs 24 | results 25 | 26 | node_modules 27 | 28 | npm-debug.log 29 | 30 | 31 | ######################## 32 | # misc / editors 33 | ######################## 34 | *~ 35 | *# 36 | .DS_STORE 37 | .netbeans 38 | nbproject 39 | .idea 40 | 41 | 42 | ######################## 43 | # local config 44 | ######################## 45 | config/local.js 46 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile 3 | * 4 | * If you created your Sails app with `sails new foo --linker`, 5 | * the following files will be automatically injected (in order) 6 | * into the EJS and HTML files in your `views` and `assets` folders. 7 | * 8 | * At the top part of this file, you'll find a few of the most commonly 9 | * configured options, but Sails' integration with Grunt is also fully 10 | * customizable. If you'd like to work with your assets differently 11 | * you can change this file to do anything you like! 12 | * 13 | * More information on using Grunt to work with static assets: 14 | * http://gruntjs.com/configuring-tasks 15 | */ 16 | 17 | module.exports = function (grunt) { 18 | 19 | 20 | 21 | /** 22 | * CSS files to inject in order 23 | * (uses Grunt-style wildcard/glob/splat expressions) 24 | * 25 | * By default, Sails also supports LESS in development and production. 26 | * To use SASS/SCSS, Stylus, etc., edit the `sails-linker:devStyles` task 27 | * below for more options. For this to work, you may need to install new 28 | * dependencies, e.g. `npm install grunt-contrib-sass` 29 | */ 30 | 31 | var cssFilesToInject = [ 32 | 'linker/**/*.css' 33 | ]; 34 | 35 | 36 | /** 37 | * Javascript files to inject in order 38 | * (uses Grunt-style wildcard/glob/splat expressions) 39 | * 40 | * To use client-side CoffeeScript, TypeScript, etc., edit the 41 | * `sails-linker:devJs` task below for more options. 42 | */ 43 | 44 | var jsFilesToInject = [ 45 | 46 | // Below, as a demonstration, you'll see the built-in dependencies 47 | // linked in the proper order order 48 | 49 | // Bring in the socket.io client 50 | 'linker/js/socket.io.js', 51 | 52 | // then beef it up with some convenience logic for talking to Sails.js 53 | 'linker/js/sails.io.js', 54 | 55 | // A simpler boilerplate library for getting you up and running w/ an 56 | // automatic listener for incoming messages from Socket.io. 57 | 'linker/js/app.js', 58 | 59 | // *-> put other dependencies here <-* 60 | 61 | // All of the rest of your app scripts imported here 62 | 'linker/**/*.js' 63 | ]; 64 | 65 | 66 | /** 67 | * Client-side HTML templates are injected using the sources below 68 | * The ordering of these templates shouldn't matter. 69 | * (uses Grunt-style wildcard/glob/splat expressions) 70 | * 71 | * By default, Sails uses JST templates and precompiles them into 72 | * functions for you. If you want to use jade, handlebars, dust, etc., 73 | * edit the relevant sections below. 74 | */ 75 | 76 | var templateFilesToInject = [ 77 | 'linker/**/*.html' 78 | ]; 79 | 80 | 81 | 82 | ///////////////////////////////////////////////////////////////// 83 | ///////////////////////////////////////////////////////////////// 84 | ///////////////////////////////////////////////////////////////// 85 | ///////////////////////////////////////////////////////////////// 86 | ///////////////////////////////////////////////////////////////// 87 | ///////////////////////////////////////////////////////////////// 88 | ///////////////////////////////////////////////////////////////// 89 | ///////////////////////////////////////////////////////////////// 90 | ///////////////////////////////////////////////////////////////// 91 | ///////////////////////////////////////////////////////////////// 92 | // 93 | // DANGER: 94 | // 95 | // With great power comes great responsibility. 96 | // 97 | ///////////////////////////////////////////////////////////////// 98 | ///////////////////////////////////////////////////////////////// 99 | ///////////////////////////////////////////////////////////////// 100 | ///////////////////////////////////////////////////////////////// 101 | ///////////////////////////////////////////////////////////////// 102 | ///////////////////////////////////////////////////////////////// 103 | ///////////////////////////////////////////////////////////////// 104 | ///////////////////////////////////////////////////////////////// 105 | ///////////////////////////////////////////////////////////////// 106 | ///////////////////////////////////////////////////////////////// 107 | 108 | // Modify css file injection paths to use 109 | cssFilesToInject = cssFilesToInject.map(function (path) { 110 | return '.tmp/public/' + path; 111 | }); 112 | 113 | // Modify js file injection paths to use 114 | jsFilesToInject = jsFilesToInject.map(function (path) { 115 | return '.tmp/public/' + path; 116 | }); 117 | 118 | 119 | templateFilesToInject = templateFilesToInject.map(function (path) { 120 | return 'assets/' + path; 121 | }); 122 | 123 | 124 | // Get path to core grunt dependencies from Sails 125 | var depsPath = grunt.option('gdsrc') || 'node_modules/sails/node_modules'; 126 | grunt.loadTasks(depsPath + '/grunt-contrib-clean/tasks'); 127 | grunt.loadTasks(depsPath + '/grunt-contrib-copy/tasks'); 128 | grunt.loadTasks(depsPath + '/grunt-contrib-concat/tasks'); 129 | grunt.loadTasks(depsPath + '/grunt-sails-linker/tasks'); 130 | grunt.loadTasks(depsPath + '/grunt-contrib-jst/tasks'); 131 | grunt.loadTasks(depsPath + '/grunt-contrib-watch/tasks'); 132 | grunt.loadTasks(depsPath + '/grunt-contrib-uglify/tasks'); 133 | grunt.loadTasks(depsPath + '/grunt-contrib-cssmin/tasks'); 134 | grunt.loadTasks(depsPath + '/grunt-contrib-less/tasks'); 135 | grunt.loadTasks(depsPath + '/grunt-contrib-coffee/tasks'); 136 | 137 | // Project configuration. 138 | grunt.initConfig({ 139 | pkg: grunt.file.readJSON('package.json'), 140 | 141 | copy: { 142 | dev: { 143 | files: [ 144 | { 145 | expand: true, 146 | cwd: './assets', 147 | src: ['**/*.!(coffee)'], 148 | dest: '.tmp/public' 149 | } 150 | ] 151 | }, 152 | build: { 153 | files: [ 154 | { 155 | expand: true, 156 | cwd: '.tmp/public', 157 | src: ['**/*'], 158 | dest: 'www' 159 | } 160 | ] 161 | } 162 | }, 163 | 164 | clean: { 165 | dev: ['.tmp/public/**'], 166 | build: ['www'] 167 | }, 168 | 169 | jst: { 170 | dev: { 171 | options: { 172 | templateSettings: { 173 | interpolate: /\{\{(.+?)\}\}/g 174 | } 175 | }, 176 | files: { 177 | '.tmp/public/jst.js': templateFilesToInject 178 | } 179 | } 180 | }, 181 | 182 | less: { 183 | dev: { 184 | files: [ 185 | { 186 | expand: true, 187 | cwd: 'assets/styles/', 188 | src: ['*.less'], 189 | dest: '.tmp/public/styles/', 190 | ext: '.css' 191 | }, { 192 | expand: true, 193 | cwd: 'assets/linker/styles/', 194 | src: ['*.less'], 195 | dest: '.tmp/public/linker/styles/', 196 | ext: '.css' 197 | } 198 | ] 199 | } 200 | }, 201 | 202 | coffee: { 203 | dev: { 204 | options:{ 205 | bare:true 206 | }, 207 | files: [ 208 | { 209 | expand: true, 210 | cwd: 'assets/js/', 211 | src: ['**/*.coffee'], 212 | dest: '.tmp/public/js/', 213 | ext: '.js' 214 | }, { 215 | expand: true, 216 | cwd: 'assets/linker/js/', 217 | src: ['**/*.coffee'], 218 | dest: '.tmp/public/linker/js/', 219 | ext: '.js' 220 | } 221 | ] 222 | } 223 | }, 224 | 225 | concat: { 226 | js: { 227 | src: jsFilesToInject, 228 | dest: '.tmp/public/concat/production.js' 229 | }, 230 | css: { 231 | src: cssFilesToInject, 232 | dest: '.tmp/public/concat/production.css' 233 | } 234 | }, 235 | 236 | uglify: { 237 | dist: { 238 | src: ['.tmp/public/concat/production.js'], 239 | dest: '.tmp/public/min/production.js' 240 | } 241 | }, 242 | 243 | cssmin: { 244 | dist: { 245 | src: ['.tmp/public/concat/production.css'], 246 | dest: '.tmp/public/min/production.css' 247 | } 248 | }, 249 | 250 | 'sails-linker': { 251 | 252 | devJs: { 253 | options: { 254 | startTag: '', 255 | endTag: '', 256 | fileTmpl: '', 257 | appRoot: '.tmp/public' 258 | }, 259 | files: { 260 | '.tmp/public/**/*.html': jsFilesToInject, 261 | 'views/**/*.html': jsFilesToInject, 262 | 'views/**/*.ejs': jsFilesToInject 263 | } 264 | }, 265 | 266 | prodJs: { 267 | options: { 268 | startTag: '', 269 | endTag: '', 270 | fileTmpl: '', 271 | appRoot: '.tmp/public' 272 | }, 273 | files: { 274 | '.tmp/public/**/*.html': ['.tmp/public/min/production.js'], 275 | 'views/**/*.html': ['.tmp/public/min/production.js'], 276 | 'views/**/*.ejs': ['.tmp/public/min/production.js'] 277 | } 278 | }, 279 | 280 | devStyles: { 281 | options: { 282 | startTag: '', 283 | endTag: '', 284 | fileTmpl: '', 285 | appRoot: '.tmp/public' 286 | }, 287 | 288 | // cssFilesToInject defined up top 289 | files: { 290 | '.tmp/public/**/*.html': cssFilesToInject, 291 | 'views/**/*.html': cssFilesToInject, 292 | 'views/**/*.ejs': cssFilesToInject 293 | } 294 | }, 295 | 296 | prodStyles: { 297 | options: { 298 | startTag: '', 299 | endTag: '', 300 | fileTmpl: '', 301 | appRoot: '.tmp/public' 302 | }, 303 | files: { 304 | '.tmp/public/index.html': ['.tmp/public/min/production.css'], 305 | 'views/**/*.html': ['.tmp/public/min/production.css'], 306 | 'views/**/*.ejs': ['.tmp/public/min/production.css'] 307 | } 308 | }, 309 | 310 | // Bring in JST template object 311 | devTpl: { 312 | options: { 313 | startTag: '', 314 | endTag: '', 315 | fileTmpl: '', 316 | appRoot: '.tmp/public' 317 | }, 318 | files: { 319 | '.tmp/public/index.html': ['.tmp/public/jst.js'], 320 | 'views/**/*.html': ['.tmp/public/jst.js'], 321 | 'views/**/*.ejs': ['.tmp/public/jst.js'] 322 | } 323 | }, 324 | 325 | 326 | /******************************************* 327 | * Jade linkers (TODO: clean this up) 328 | *******************************************/ 329 | 330 | devJsJADE: { 331 | options: { 332 | startTag: '// SCRIPTS', 333 | endTag: '// SCRIPTS END', 334 | fileTmpl: 'script(type="text/javascript", src="%s")', 335 | appRoot: '.tmp/public' 336 | }, 337 | files: { 338 | 'views/**/*.jade': jsFilesToInject 339 | } 340 | }, 341 | 342 | prodJsJADE: { 343 | options: { 344 | startTag: '// SCRIPTS', 345 | endTag: '// SCRIPTS END', 346 | fileTmpl: 'script(type="text/javascript", src="%s")', 347 | appRoot: '.tmp/public' 348 | }, 349 | files: { 350 | 'views/**/*.jade': ['.tmp/public/min/production.js'] 351 | } 352 | }, 353 | 354 | devStylesJADE: { 355 | options: { 356 | startTag: '// STYLES', 357 | endTag: '// STYLES END', 358 | fileTmpl: 'link(rel="stylesheet", href="%s")', 359 | appRoot: '.tmp/public' 360 | }, 361 | files: { 362 | 'views/**/*.jade': cssFilesToInject 363 | } 364 | }, 365 | 366 | prodStylesJADE: { 367 | options: { 368 | startTag: '// STYLES', 369 | endTag: '// STYLES END', 370 | fileTmpl: 'link(rel="stylesheet", href="%s")', 371 | appRoot: '.tmp/public' 372 | }, 373 | files: { 374 | 'views/**/*.jade': ['.tmp/public/min/production.css'] 375 | } 376 | }, 377 | 378 | // Bring in JST template object 379 | devTplJADE: { 380 | options: { 381 | startTag: '// TEMPLATES', 382 | endTag: '// TEMPLATES END', 383 | fileTmpl: 'script(type="text/javascript", src="%s")', 384 | appRoot: '.tmp/public' 385 | }, 386 | files: { 387 | 'views/**/*.jade': ['.tmp/public/jst.js'] 388 | } 389 | } 390 | /************************************ 391 | * Jade linker end 392 | ************************************/ 393 | }, 394 | 395 | watch: { 396 | api: { 397 | 398 | // API files to watch: 399 | files: ['api/**/*'] 400 | }, 401 | assets: { 402 | 403 | // Assets to watch: 404 | files: ['assets/**/*'], 405 | 406 | // When assets are changed: 407 | tasks: ['compileAssets', 'linkAssets'] 408 | } 409 | } 410 | }); 411 | 412 | // When Sails is lifted: 413 | grunt.registerTask('default', [ 414 | 'compileAssets', 415 | 'linkAssets', 416 | 'watch' 417 | ]); 418 | 419 | grunt.registerTask('compileAssets', [ 420 | 'clean:dev', 421 | 'jst:dev', 422 | 'less:dev', 423 | 'copy:dev', 424 | 'coffee:dev' 425 | ]); 426 | 427 | grunt.registerTask('linkAssets', [ 428 | 429 | // Update link/script/template references in `assets` index.html 430 | 'sails-linker:devJs', 431 | 'sails-linker:devStyles', 432 | 'sails-linker:devTpl', 433 | 'sails-linker:devJsJADE', 434 | 'sails-linker:devStylesJADE', 435 | 'sails-linker:devTplJADE' 436 | ]); 437 | 438 | 439 | // Build the assets into a web accessible folder. 440 | // (handy for phone gap apps, chrome extensions, etc.) 441 | grunt.registerTask('build', [ 442 | 'compileAssets', 443 | 'linkAssets', 444 | 'clean:build', 445 | 'copy:build' 446 | ]); 447 | 448 | // When sails is lifted in production 449 | grunt.registerTask('prod', [ 450 | 'clean:dev', 451 | 'jst:dev', 452 | 'less:dev', 453 | 'copy:dev', 454 | 'coffee:dev', 455 | 'concat', 456 | 'uglify', 457 | 'cssmin', 458 | 'sails-linker:prodJs', 459 | 'sails-linker:prodStyles', 460 | 'sails-linker:devTpl', 461 | 'sails-linker:prodJsJADE', 462 | 'sails-linker:prodStylesJADE', 463 | 'sails-linker:devTplJADE' 464 | ]); 465 | 466 | // When API files are changed: 467 | // grunt.event.on('watch', function(action, filepath) { 468 | // grunt.log.writeln(filepath + ' has ' + action); 469 | 470 | // // Send a request to a development-only endpoint on the server 471 | // // which will reuptake the file that was changed. 472 | // var baseurl = grunt.option('baseurl'); 473 | // var gruntSignalRoute = grunt.option('signalpath'); 474 | // var url = baseurl + gruntSignalRoute + '?action=' + action + '&filepath=' + filepath; 475 | 476 | // require('http').get(url) 477 | // .on('error', function(e) { 478 | // console.error(filepath + ' has ' + action + ', but could not signal the Sails.js server: ' + e.message); 479 | // }); 480 | // }); 481 | }; 482 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sailsjs-angularjs-passportjs-todo 2 | ====== 3 | 4 | a simple todo app with SailsJS and AngularJS (+PassportJS) 5 | 6 | authenticate with: 7 | 8 | * Dropbox 9 | * GitHub 10 | * Google+ 11 | 12 | Passport supports authentication with an extensive [list of third-party providers](http://passportjs.org/guide/providers/). Contribute with more. 13 | 14 | # Sails 15 | 16 | Sails.js makes it easy to build custom, enterprise-grade Node.js apps. It is designed to resemble the MVC architecture from frameworks like Ruby on Rails, but with support for the more modern, data-oriented style of web app development. It's especially good for building realtime features like chat. 17 | 18 | [https://github.com/balderdashy/sails](https://github.com/balderdashy/sails) 19 | 20 | # Angular 21 | 22 | AngularJS lets you write client-side web applications as if you had a smarter browser. It lets you use good old HTML (or HAML, Jade and friends!) as your template language and lets you extend HTML’s syntax to express your application’s components clearly and succinctly. It automatically synchronizes data from your UI (view) with your JavaScript objects (model) through 2-way data binding. To help you structure your application better and make it easy to test, AngularJS teaches the browser how to do dependency injection and inversion of control. Oh yeah and it also helps with server-side communication, taming async callbacks with promises and deferreds; and make client-side navigation and deeplinking with hashbang urls or HTML5 pushState a piece of cake. The best of all: it makes development fun! 23 | 24 | [https://github.com/angular/angular.js](https://github.com/angular/angular.js) 25 | 26 | # Passport 27 | 28 | Passport is Express-compatible authentication middleware for Node.js. 29 | 30 | [https://github.com/jaredhanson/passport](https://github.com/jaredhanson/passport) 31 | 32 | ## How to 33 | 34 | ### node.js 35 | ``` 36 | $ git clone https://github.com/oitozero/sailsjs-angularjs-passportjs-todo 37 | $ cd sailsjs-angularjs-todo 38 | !! configure credentials in config/middleware.js !! 39 | $ npm install 40 | $ npm install -g sails 41 | $ sails lift 42 | 43 | \Ahoy/ 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /api/adapters/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/api/adapters/.gitkeep -------------------------------------------------------------------------------- /api/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/api/controllers/.gitkeep -------------------------------------------------------------------------------- /api/controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthController 3 | * 4 | * @module :: Controller 5 | * @description :: Contains logic for handling requests. 6 | */ 7 | 8 | var passport = require('passport'); 9 | 10 | var AuthController = { 11 | 12 | index: function (req, res) { 13 | res.view(); 14 | }, 15 | 16 | logout: function (req, res) { 17 | req.logout(); 18 | res.redirect('/'); 19 | }, 20 | 21 | 'dropbox': function (req, res) { 22 | passport.authenticate('dropbox', { failureRedirect: '/login' }, 23 | function (err, user) { 24 | req.logIn(user, function (err) { 25 | if (err) { 26 | console.log(err); 27 | res.view('500'); 28 | return; 29 | } 30 | 31 | res.redirect('/'); 32 | return; 33 | }); 34 | })(req, res); 35 | }, 36 | 37 | 'dropbox/callback': function (req, res) { 38 | passport.authenticate('dropbox', 39 | function (req, res) { 40 | res.redirect('/'); 41 | })(req, res); 42 | }, 43 | 44 | 'github': function (req, res) { 45 | passport.authenticate('github', { failureRedirect: '/login' }, 46 | function (err, user) { 47 | req.logIn(user, function (err) { 48 | if (err) { 49 | console.log(err); 50 | res.view('500'); 51 | return; 52 | } 53 | 54 | res.redirect('/'); 55 | return; 56 | }); 57 | })(req, res); 58 | }, 59 | 60 | 'github/callback': function (req, res) { 61 | passport.authenticate('github', 62 | function (req, res) { 63 | res.redirect('/'); 64 | })(req, res); 65 | }, 66 | 67 | 'google': function (req, res) { 68 | passport.authenticate('google', { failureRedirect: '/login', scope:['https://www.googleapis.com/auth/plus.login','https://www.googleapis.com/auth/userinfo.profile'] }, 69 | function (err, user) { 70 | req.logIn(user, function (err) { 71 | if (err) { 72 | console.log(err); 73 | res.view('500'); 74 | return; 75 | } 76 | 77 | res.redirect('/'); 78 | return; 79 | }); 80 | })(req, res); 81 | }, 82 | 83 | 'google/callback': function (req, res) { 84 | passport.authenticate('google', 85 | function (req, res) { 86 | res.redirect('/'); 87 | })(req, res); 88 | } 89 | 90 | }; 91 | module.exports = AuthController; -------------------------------------------------------------------------------- /api/controllers/HomeController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HomeController 3 | * 4 | * @module :: Controller 5 | * @description :: Contains logic for handling requests. 6 | */ 7 | 8 | var HomeController = { 9 | 10 | index: function (req,res) 11 | { 12 | 13 | console.log(req.user); 14 | res.view({ 15 | user: req.user 16 | }); 17 | } 18 | 19 | }; 20 | module.exports = HomeController; -------------------------------------------------------------------------------- /api/controllers/TodoController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TodoController 3 | * 4 | * @module :: Controller 5 | * @description :: Contains logic for handling requests. 6 | */ 7 | 8 | module.exports = { 9 | 10 | /* e.g. 11 | sayHello: function (req, res) { 12 | res.send('hello world!'); 13 | } 14 | */ 15 | 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /api/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UserController 3 | * 4 | * @module :: Controller 5 | * @description :: Contains logic for handling requests. 6 | */ 7 | 8 | module.exports = { 9 | 10 | /* e.g. 11 | sayHello: function (req, res) { 12 | res.send('hello world!'); 13 | } 14 | */ 15 | 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /api/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/api/models/.gitkeep -------------------------------------------------------------------------------- /api/models/Todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Todo 3 | * 4 | * @module :: Model 5 | * @description :: A short summary of how this model works and what it represents. 6 | * 7 | */ 8 | 9 | module.exports = { 10 | 11 | attributes: { 12 | title: 'STRING', 13 | completed: 'BOOLEAN' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User 3 | * 4 | * @module :: Model 5 | * @description :: A short summary of how this model works and what it represents. 6 | * 7 | */ 8 | 9 | module.exports = { 10 | 11 | attributes : { 12 | provider: 'STRING', 13 | uid: 'INTEGER', 14 | name: 'STRING' 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /api/policies/authenticated.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allow any authenticated user. 3 | */ 4 | module.exports = function (req, res, ok) { 5 | 6 | // User is allowed, proceed to controller 7 | if (req.isAuthenticated()) { 8 | return ok(); 9 | } 10 | 11 | // User is not allowed 12 | else { 13 | return res.redirect('/login'); 14 | } 15 | }; -------------------------------------------------------------------------------- /api/services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/api/services/.gitkeep -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Start sails and pass it command line arguments 2 | require('sails').lift(require('optimist').argv); 3 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/assets/favicon.ico -------------------------------------------------------------------------------- /assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/assets/images/.gitkeep -------------------------------------------------------------------------------- /assets/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/assets/js/.gitkeep -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This file contains some conventional defaults for working with Socket.io + Sails. 5 | * It is designed to get you up and running fast, but is by no means anything special. 6 | * 7 | * Feel free to change none, some, or ALL of this file to fit your needs! 8 | */ 9 | 10 | 11 | (function (io) { 12 | 13 | // as soon as this file is loaded, connect automatically, 14 | var socket = io.connect(); 15 | if (typeof console !== 'undefined') { 16 | log('Connecting to Sails.js...'); 17 | } 18 | 19 | socket.on('connect', function socketConnected() { 20 | 21 | // Listen for Comet messages from Sails 22 | socket.on('message', function messageReceived(message) { 23 | 24 | /////////////////////////////////////////////////////////// 25 | // Replace the following with your own custom logic 26 | // to run when a new message arrives from the Sails.js 27 | // server. 28 | /////////////////////////////////////////////////////////// 29 | log('New comet message received :: ', message); 30 | ////////////////////////////////////////////////////// 31 | 32 | }); 33 | 34 | 35 | /////////////////////////////////////////////////////////// 36 | // Here's where you'll want to add any custom logic for 37 | // when the browser establishes its socket connection to 38 | // the Sails.js server. 39 | /////////////////////////////////////////////////////////// 40 | log( 41 | 'Socket is now connected and globally accessible as `socket`.\n' + 42 | 'e.g. to send a GET request to Sails, try \n' + 43 | '`socket.get("/", function (response) ' + 44 | '{ console.log(response); })`' 45 | ); 46 | /////////////////////////////////////////////////////////// 47 | 48 | 49 | }); 50 | 51 | 52 | // Expose connected `socket` instance globally so that it's easy 53 | // to experiment with from the browser console while prototyping. 54 | window.socket = socket; 55 | 56 | 57 | // Simple log function to keep the example simple 58 | function log () { 59 | if (typeof console !== 'undefined') { 60 | console.log.apply(console, arguments); 61 | } 62 | } 63 | 64 | 65 | })( 66 | 67 | // In case you're wrapping socket.io to prevent pollution of the global namespace, 68 | // you can replace `window.io` with your own `io` here: 69 | window.io 70 | 71 | ); 72 | -------------------------------------------------------------------------------- /assets/js/sails.io.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sails.io.js 3 | * 4 | * This file is completely optional, and merely here for your convenience. 5 | * 6 | * It reduces the amount of browser code necessary to send and receive messages 7 | * to & from Sails by simulating a REST client interface on top of socket.io. 8 | * It models its API after the pattern in jQuery you might be familiar with. 9 | * 10 | * So to switch from using AJAX to Socket.io, instead of: 11 | * `$.post( url, [data], [cb] )` 12 | * 13 | * You would use: 14 | * `socket.post( url, [data], [cb] )` 15 | * 16 | * For more information, visit: 17 | * http://sailsjs.org/#documentation 18 | */ 19 | 20 | (function (io) { 21 | 22 | 23 | // We'll be adding methods to `io.SocketNamespace.prototype`, the prototype for the 24 | // Socket instance returned when the browser connects with `io.connect()` 25 | var Socket = io.SocketNamespace; 26 | 27 | 28 | 29 | /** 30 | * Simulate a GET request to sails 31 | * e.g. 32 | * `socket.get('/user/3', Stats.populate)` 33 | * 34 | * @param {String} url :: destination URL 35 | * @param {Object} params :: parameters to send with the request [optional] 36 | * @param {Function} cb :: callback function to call when finished [optional] 37 | */ 38 | 39 | Socket.prototype.get = function (url, data, cb) { 40 | return this.request(url, data, cb, 'get'); 41 | }; 42 | 43 | 44 | 45 | /** 46 | * Simulate a POST request to sails 47 | * e.g. 48 | * `socket.post('/event', newMeeting, $spinner.hide)` 49 | * 50 | * @param {String} url :: destination URL 51 | * @param {Object} params :: parameters to send with the request [optional] 52 | * @param {Function} cb :: callback function to call when finished [optional] 53 | */ 54 | 55 | Socket.prototype.post = function (url, data, cb) { 56 | return this.request(url, data, cb, 'post'); 57 | }; 58 | 59 | 60 | 61 | /** 62 | * Simulate a PUT request to sails 63 | * e.g. 64 | * `socket.post('/event/3', changedFields, $spinner.hide)` 65 | * 66 | * @param {String} url :: destination URL 67 | * @param {Object} params :: parameters to send with the request [optional] 68 | * @param {Function} cb :: callback function to call when finished [optional] 69 | */ 70 | 71 | Socket.prototype.put = function (url, data, cb) { 72 | return this.request(url, data, cb, 'put'); 73 | }; 74 | 75 | 76 | 77 | /** 78 | * Simulate a DELETE request to sails 79 | * e.g. 80 | * `socket.delete('/event', $spinner.hide)` 81 | * 82 | * @param {String} url :: destination URL 83 | * @param {Object} params :: parameters to send with the request [optional] 84 | * @param {Function} cb :: callback function to call when finished [optional] 85 | */ 86 | 87 | Socket.prototype['delete'] = function (url, data, cb) { 88 | return this.request(url, data, cb, 'delete'); 89 | }; 90 | 91 | 92 | 93 | 94 | /** 95 | * Simulate HTTP over Socket.io 96 | * @api private :: but exposed for backwards compatibility w/ <= sails@~0.8 97 | */ 98 | 99 | Socket.prototype.request = request; 100 | function request (url, data, cb, method) { 101 | 102 | var socket = this; 103 | 104 | var usage = 'Usage:\n socket.' + 105 | (method || 'request') + 106 | '( destinationURL, dataToSend, fnToCallWhenComplete )'; 107 | 108 | // Remove trailing slashes and spaces 109 | url = url.replace(/^(.+)\/*\s*$/, '$1'); 110 | 111 | // If method is undefined, use 'get' 112 | method = method || 'get'; 113 | 114 | 115 | if ( typeof url !== 'string' ) { 116 | throw new Error('Invalid or missing URL!\n' + usage); 117 | } 118 | 119 | // Allow data arg to be optional 120 | if ( typeof data === 'function' ) { 121 | cb = data; 122 | data = {}; 123 | } 124 | 125 | // Build to request 126 | var json = window.io.JSON.stringify({ 127 | url: url, 128 | data: data 129 | }); 130 | 131 | 132 | // Send the message over the socket 133 | socket.emit(method, json, function afterEmitted (result) { 134 | 135 | var parsedResult = result; 136 | 137 | if (result && typeof result === 'string') { 138 | try { 139 | parsedResult = window.io.JSON.parse(result); 140 | } catch (e) { 141 | if (typeof console !== 'undefined') { 142 | console.warn("Could not parse:", result, e); 143 | } 144 | throw new Error("Server response could not be parsed!\n" + result); 145 | } 146 | } 147 | 148 | // TODO: Handle errors more effectively 149 | if (parsedResult === 404) throw new Error("404: Not found"); 150 | if (parsedResult === 403) throw new Error("403: Forbidden"); 151 | if (parsedResult === 500) throw new Error("500: Server error"); 152 | 153 | cb && cb(parsedResult); 154 | 155 | }); 156 | } 157 | 158 | 159 | 160 | 161 | }) ( 162 | 163 | // In case you're wrapping socket.io to prevent pollution of the global namespace, 164 | // you can replace `window.io` with your own `io` here: 165 | window.io 166 | 167 | ); 168 | -------------------------------------------------------------------------------- /assets/js/todo.js: -------------------------------------------------------------------------------- 1 | var todoApp = angular.module('todoApp', []); 2 | 3 | todoApp.controller('TodoController', function($scope, $http) { 4 | 5 | $scope.todos = []; 6 | 7 | // Get all todos 8 | $http.get('/todo') 9 | .success(function(todos) { 10 | $scope.loaded = true; 11 | $scope.todos = todos; 12 | }).error(function(err) { 13 | // Alert if there's an error 14 | alert(err); 15 | }); 16 | 17 | $scope.addTodo = function(title) { 18 | 19 | if($.trim(title).length === 0){ 20 | ctrl.$setValidity('newTodoTitle', false); 21 | return; 22 | } 23 | 24 | $http.post('/todo', { 25 | title: title 26 | }).success(function(todo) { 27 | $scope.newTodoTitle = ''; 28 | $scope.todos.push(todo); 29 | }).error(function(err) { 30 | // Alert if there's an error 31 | return alert(err.message || "an error occurred"); 32 | }); 33 | }; 34 | 35 | $scope.changeCompleted = function(todo) { 36 | // Update the todo 37 | $http.put('/todo/' + todo.id, { 38 | completed: todo.completed 39 | }).error(function(err) { 40 | return alert(err.message || (err.errors && err.errors.completed) || "an error occurred"); 41 | }); 42 | }; 43 | 44 | $scope.removeCompletedItems = function() { 45 | $http.get('/todo', { 46 | params: { 47 | completed: true 48 | } 49 | }).success(function(todos) { 50 | todos.forEach(function(t) { deleteTodo(t); }); 51 | }); 52 | }; 53 | 54 | function deleteTodo(todo) { 55 | $http.delete('/todo/' + todo.id, { 56 | params: { 57 | completed: true 58 | } 59 | }).success(function() { 60 | // Find the index of an object with a matching id 61 | var index = $scope.todos.indexOf( 62 | $scope.todos.filter(function(t) { 63 | return t.id === todo.id; 64 | })[0]); 65 | 66 | if (index !== -1) { 67 | $scope.todos.splice(index, 1); 68 | } 69 | }).error(function(err) { 70 | alert(err.message || "an error occurred"); 71 | }); 72 | } 73 | 74 | }); -------------------------------------------------------------------------------- /assets/robots.txt: -------------------------------------------------------------------------------- 1 | # The robots.txt file is used to control how search engines index your live URLs. 2 | # See http://www.robotstxt.org/wc/norobots.html for more information. 3 | # 4 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 5 | # User-Agent: * 6 | # Disallow: / 7 | -------------------------------------------------------------------------------- /assets/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oitozero/sailsjs-angularjs-passportjs-todo/01b77a004e50a81c9c90513322f4b3552ef89888/assets/styles/.gitkeep -------------------------------------------------------------------------------- /config/404.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default 404 (not found) handler 3 | * 4 | * If no matches are found, Sails will respond using this handler: 5 | * 6 | * For more information on 404/notfound handling in Sails/Express, check out: 7 | * http://expressjs.com/faq.html#404-handling 8 | */ 9 | 10 | module.exports[404] = function pageNotFound(req, res, express404Handler) { 11 | 12 | var statusCode = 404; 13 | var result = { 14 | status: statusCode 15 | }; 16 | 17 | // If the user-agent wants a JSON response, send json 18 | if (req.wantsJSON) { 19 | return res.json(result, result.status); 20 | } 21 | 22 | // Otherwise, serve the `views/404.*` page 23 | var view = '404'; 24 | res.render(view, result, function (err) { 25 | if (err) { 26 | return express404Handler(); 27 | } 28 | res.render(view); 29 | }); 30 | 31 | }; -------------------------------------------------------------------------------- /config/500.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default error handler 3 | * 4 | * If an error is thrown, Sails will respond using this default error handler 5 | * 6 | * For more information on error handling in Sails/Express, check out: 7 | * http://expressjs.com/guide.html#error-handling 8 | */ 9 | 10 | module.exports[500] = function serverErrorOccurred(errors, req, res, expressErrorHandler) { 11 | 12 | var statusCode = 500; 13 | 14 | // Ensure that `errors` is a list 15 | var displayedErrors = (typeof errors !== 'object' || !errors.length) ? [errors] : errors; 16 | 17 | // Build data for response 18 | var response = { 19 | status: statusCode 20 | }; 21 | 22 | // Ensure that each error is formatted correctly 23 | var inspect = require('util').inspect; 24 | for (var i in displayedErrors) { 25 | 26 | // Make error easier to read, and normalize its type 27 | if (!(displayedErrors[i] instanceof Error)) { 28 | displayedErrors[i] = new Error(inspect(displayedErrors[i])); 29 | } 30 | 31 | displayedErrors[i] = { 32 | message: displayedErrors[i].message, 33 | stack: displayedErrors[i].stack 34 | }; 35 | 36 | // Log error to log adapter 37 | sails.log.error(displayedErrors[i].stack); 38 | } 39 | 40 | // In production, don't display any identifying information about the error(s) 41 | if (sails.config.environment === 'development') { 42 | response.errors = displayedErrors; 43 | } 44 | 45 | // If the user-agent wants a JSON response, 46 | // respond with a JSON-readable version of errors 47 | if (req.wantsJSON) { 48 | return res.json(response, response.status); 49 | } 50 | 51 | // Otherwise, if it can be rendered, the `views/500.*` page is rendered 52 | // If an error occurs rendering the 500 view ITSELF, 53 | // use the built-in Express error handler to render the errors 54 | var view = '500'; 55 | res.render(view, response, function (err) { 56 | if (err) { 57 | return expressErrorHandler(errors); 58 | } 59 | res.render(view, response); 60 | }); 61 | 62 | }; -------------------------------------------------------------------------------- /config/adapters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global adapter config 3 | * 4 | * The `adapters` configuration object lets you create different global "saved settings" 5 | * that you can mix and match in your models. The `default` option indicates which 6 | * "saved setting" should be used if a model doesn't have an adapter specified. 7 | * 8 | * Keep in mind that options you define directly in your model definitions 9 | * will override these settings. 10 | * 11 | * For more information on adapter configuration, check out: 12 | * http://sailsjs.org/#documentation 13 | */ 14 | 15 | module.exports.adapters = { 16 | 17 | // If you leave the adapter config unspecified 18 | // in a model definition, 'default' will be used. 19 | 'default': 'disk', 20 | 21 | // In-memory adapter for DEVELOPMENT ONLY 22 | memory: { 23 | module: 'sails-memory' 24 | }, 25 | 26 | // Persistent adapter for DEVELOPMENT ONLY 27 | // (data IS preserved when the server shuts down) 28 | disk: { 29 | module: 'sails-disk' 30 | }, 31 | 32 | // MySQL is the world's most popular relational database. 33 | // Learn more: http://en.wikipedia.org/wiki/MySQL 34 | mysql: { 35 | 36 | module: 'sails-mysql', 37 | host: 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS', 38 | user: 'YOUR_MYSQL_USER', 39 | // Psst.. You can put your password in config/local.js instead 40 | // so you don't inadvertently push it up if you're using version control 41 | password: 'YOUR_MYSQL_PASSWORD', 42 | database: 'YOUR_MYSQL_DB' 43 | } 44 | }; -------------------------------------------------------------------------------- /config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap 3 | * 4 | * An asynchronous boostrap function that runs before your Sails app gets lifted. 5 | * This gives you an opportunity to set up your data model, run jobs, or perform some special logic. 6 | * 7 | * For more information on bootstrapping your app, check out: 8 | * http://sailsjs.org/#documentation 9 | */ 10 | 11 | module.exports.bootstrap = function (cb) { 12 | 13 | // It's very important to trigger this callack method when you are finished 14 | // with the bootstrap! (otherwise your server will never lift, since it's waiting on the bootstrap) 15 | cb(); 16 | }; -------------------------------------------------------------------------------- /config/controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controllers 3 | * 4 | * By default, Sails controllers automatically bind routes for each of their functions. 5 | * Additionally, each controller will automatically bind routes for a CRUD API 6 | * controlling the model which matches its name, if one exists. 7 | * 8 | * NOTE: These settings are for the global configuration of controllers. 9 | * You may also override these settings on a per-controller basis 10 | * by modifying the 'blueprints' object in your controllers 11 | * 12 | * For more information on controller configuration and blueprints, check out: 13 | * http://sailsjs.org/#documentation 14 | */ 15 | 16 | module.exports.controllers = { 17 | 18 | 19 | blueprints: { 20 | 21 | // Optional mount path prefix for blueprints 22 | // (the automatically bound routes in your controllers) 23 | // e.g. '/api/v2' 24 | prefix: '', 25 | 26 | 27 | // Whether routes are automatically generated for every action in your controllers 28 | // (also maps `index` to /:controller) 29 | // '/:controller', '/:controller/index', and '/:controller/:action' 30 | actions: true, 31 | 32 | 33 | // ** NOTE ** 34 | // These CRUD shortcuts exist for your convenience during development, 35 | // but you'll want to disable them in production. 36 | // '/:controller/find/:id?' 37 | // '/:controller/create' 38 | // '/:controller/update/:id' 39 | // '/:controller/destroy/:id' 40 | shortcuts: true, 41 | 42 | 43 | // Automatic REST blueprints enabled? 44 | // e.g. 45 | // 'get /:controller/:id?' 46 | // 'post /:controller' 47 | // 'put /:controller/:id' 48 | // 'delete /:controller/:id' 49 | rest: true, 50 | 51 | 52 | // If a blueprint route catches a request, 53 | // only match :id param if it's an integer 54 | // 55 | // e.g. only trigger route handler if requests look like: 56 | // get /user/8 57 | // instead of: 58 | // get /user/a8j4g9jsd9ga4ghjasdha 59 | expectIntegerId: false 60 | } 61 | 62 | }; -------------------------------------------------------------------------------- /config/cors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cross-Origin Resource Sharing (CORS) 3 | * 4 | * CORS is like a more modern version of JSONP-- it allows your server/API 5 | * to successfully respond to requests from client-side JavaScript code 6 | * running on some other domain (e.g. google.com) 7 | * Unlike JSONP, it works with POST, PUT, and DELETE requests 8 | * 9 | * For more information on CORS, check out: 10 | * http://en.wikipedia.org/wiki/Cross-origin_resource_sharing 11 | * 12 | * Note that any of these settings (besides 'allRoutes') can be changed on a per-route basis 13 | * by adding a "cors" object to the route configuration: 14 | * 15 | * '/get foo': { 16 | * controller: 'foo', 17 | * action: 'bar', 18 | * cors: { 19 | * origin: 'http://foobar.com,https://owlhoot.com' 20 | * } 21 | * } 22 | * 23 | */ 24 | 25 | module.exports.cors = { 26 | 27 | // Allow CORS on all routes by default? If not, you must enable CORS on a 28 | // per-route basis by either adding a "cors" configuration object 29 | // to the route config, or setting "cors:true" in the route config to 30 | // use the default settings below. 31 | allRoutes: false, 32 | 33 | // Which domains which are allowed CORS access? 34 | // This can be a comma-delimited list of hosts (beginning with http:// or https://) 35 | // or "*" to allow all domains CORS access. 36 | origin: '*', 37 | 38 | // Allow cookies to be shared for CORS requests? 39 | credentials: true, 40 | 41 | // Which methods should be allowed for CORS requests? This is only used 42 | // in response to preflight requests (see article linked above for more info) 43 | methods: 'GET, POST, PUT, DELETE, OPTIONS, HEAD', 44 | 45 | // Which headers should be allowed for CORS requests? This is only used 46 | // in response to preflight requests. 47 | headers: 'content-type' 48 | 49 | }; -------------------------------------------------------------------------------- /config/csrf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cross-Site Request Forgery Protection 3 | * 4 | * CSRF tokens are like a tracking chip. While a session tells the server that a user 5 | * "is who they say they are", a csrf token tells the server "you are where you say you are". 6 | * 7 | * When enabled, all non-GET requests to the Sails server must be accompanied by 8 | * a special token, identified as the '_csrf' parameter. 9 | * 10 | * This option protects your Sails app against cross-site request forgery (or CSRF) attacks. 11 | * A would-be attacker needs not only a user's session cookie, but also this timestamped, 12 | * secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. 13 | * 14 | * This allows us to have certainty that our users' requests haven't been hijacked, 15 | * and that the requests they're making are intentional and legitimate. 16 | * 17 | * This token has a short-lived expiration timeline, and must be acquired by either: 18 | * 19 | * (a) For traditional view-driven web apps: 20 | * Fetching it from one of your views, where it may be accessed as 21 | * a local variable, e.g.: 22 | *
25 | * 26 | * or (b) For AJAX/Socket-heavy and/or single-page apps: 27 | * Sending a GET request to the `/csrfToken` route, where it will be returned 28 | * as JSON, e.g.: 29 | * { _csrf: 'ajg4JD(JGdajhLJALHDa' } 30 | * 31 | * 32 | * Enabling this option requires managing the token in your front-end app. 33 | * For traditional web apps, it's as easy as passing the data from a view into a form action. 34 | * In AJAX/Socket-heavy apps, just send a GET request to the /csrfToken route to get a valid token. 35 | * 36 | * For more information on CSRF, check out: 37 | * http://en.wikipedia.org/wiki/Cross-site_request_forgery 38 | */ 39 | 40 | module.exports.csrf = false; -------------------------------------------------------------------------------- /config/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * 4 | * If your app will touch people from all over the world, i18n (or internationalization) 5 | * may be an important part of your international strategy. 6 | * 7 | * 8 | * For more information, check out: 9 | * http://sailsjs.org/#documentation 10 | */ 11 | 12 | module.exports.i18n = { 13 | 14 | // Which locales are supported? 15 | locales: ['en', 'es'] 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /config/locales/_README.md: -------------------------------------------------------------------------------- 1 | # Internationalization / Localization Settings 2 | 3 | ## Locale 4 | All locale files live under `config/locales`. Here is where you can add locale data as JSON key-value pairs. The name of the file should match the language that you are supporting, which allows for automatic language detection based on the user request. 5 | 6 | Here is an example locale stringfile for the Spanish language (`config/locales/es.json`): 7 | ```json 8 | { 9 | "Hello!": "Hola!", 10 | "Hello %s, how are you today?": "¿Hola %s, como estas?", 11 | } 12 | ``` 13 | ## Usage 14 | Locales can be accessed through either `res.i18n()`, or in views through the `i18n()` function. 15 | Remember that the keys are case sensitive and require exact key matches, e.g. 16 | 17 | ```ejs 18 |40 | Why might this be happening? 41 |
42 |
47 | <%
48 | _.each(errors, function (error) { %>
49 | <%= error.stack %>
50 | <%
51 | }); %>
52 |
53 | <% } else { %>
54 |
55 |
56 | A team of highly trained sea bass is working on this as we speak.
57 | If the problem persists, please contact the system administrator and inform them of the time that the error occured, and anything you might have done that may have caused the error.
58 |
You don't have any todos! Add one now:
10 |23 | Remove completed items 24 |
25 |