├── .gitignore ├── 01-co ├── README.md ├── index.js └── test.js ├── 02-hello-world ├── README.md ├── index.js └── test.js ├── 03-routing ├── README.md ├── index.js └── test.js ├── 04-bodies ├── README.md ├── index.js └── test.js ├── 05-error-handling ├── README.md ├── index.js └── test.js ├── 06-content-negotiation ├── README.md ├── index.js └── test.js ├── 07-content-headers ├── README.md ├── index.js └── test.js ├── 08-decorators ├── README.md ├── index.js └── test.js ├── 09-templating ├── README.md ├── homepage.jade ├── index.js ├── layout.jade └── test.js ├── 10-authentication ├── README.md ├── form.html ├── index.js └── test.js ├── LICENSE ├── README.md ├── Vagrantfile ├── package.json └── provision-node.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /01-co/README.md: -------------------------------------------------------------------------------- 1 | 2 | Koa uses [co](https://github.com/visionmedia/co) under the hood, 3 | so to fully understand how Koa works, 4 | you must understand co. 5 | 6 | Co uses ES6 generators. 7 | You can tell if a function is a generator if it has a star: 8 | 9 | ```js 10 | function* () { 11 | 12 | } 13 | ``` 14 | 15 | `yield` is a keyword specific to generators and allows users to __arbitrarily suspend__ functions at any `yield` point. 16 | `yield` is not a magical async function - co does all that magic behind the scenes. 17 | 18 | You can think of `co`'s use of generators like this with node callbacks: 19 | 20 | ```js 21 | function* () { 22 | var val = yield /* breakpoint */ function (next) { 23 | // #pause at the break point 24 | 25 | // execute an asynchronous function 26 | setTimeout(function () { 27 | // return the value node.js callback-style 28 | next(null, 1); 29 | }, 100); 30 | } 31 | 32 | assert(val === 1); 33 | } 34 | ``` 35 | 36 | This workshop will not cover all the intricacies of generators as that alone would 37 | be its own workshop, and most people (including myself) wouldn't be able 38 | to understand generators in less than a day. 39 | 40 | ## Yieldables 41 | 42 | You can only `yield` a few types of "async" things in Co. We call these "yieldables".: 43 | 44 | ### Thunks 45 | 46 | Thunks are asynchronous functions that only allow a callback: 47 | 48 | ```js 49 | function (done) { 50 | setTimeout(function () { 51 | done(/* error: */ null, true); 52 | }, 100) 53 | } 54 | ``` 55 | 56 | If there are additional arguments on this function, 57 | a neat trick is to simply wrap it in a thunk or return a thunk. 58 | You may be interested in [thunkify](https://github.com/visionmedia/node-thunkify), 59 | but we will learn how to "thunkify" node.js-style callbacks in this exercise. 60 | 61 | ### Promises 62 | 63 | We won't show you how to write code with promises, 64 | but you can `yield` them! 65 | 66 | ## Creating yieldables 67 | 68 | In this exercise, we will learn how to create "yieldables" from callbacks. 69 | We will not go into depth of how generators and `co` works as it is unnecessary for Koa. 70 | 71 | ## Learning more 72 | 73 | - [thunkify](https://github.com/visionmedia/node-thunkify) 74 | - [mz](https://github.com/normalize/mz) 75 | - [co-fs](https://github.com/visionmedia/co-fs) 76 | -------------------------------------------------------------------------------- /01-co/index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | 4 | /** 5 | * Create a yieldable version of `fs.stat()`: 6 | * 7 | * app.use(function* () { 8 | * var stats = yield exports.stat(__filename); 9 | * }) 10 | * 11 | * Hint: you can return a yieldable. 12 | */ 13 | 14 | exports.stat = function (filename) { 15 | 16 | }; 17 | 18 | /** 19 | * Create a yieldable version of `fs.exists()`: 20 | * 21 | * 22 | * app.use(function* () { 23 | * var exists = yield exports.exists(__filename); 24 | * }) 25 | * 26 | * Note that `fs.exists()` simply wraps `fs.stat()`. 27 | * If `fs.stat()` does not return an error, then the file exists! 28 | * 29 | * Hint: don't use fs.exists() as it's not a proper callback. 30 | * In other words, the first argument returned in its callback 31 | * is not an error object, unlike node callbacks. 32 | */ 33 | 34 | exports.exists = function (filename) { 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /01-co/test.js: -------------------------------------------------------------------------------- 1 | 2 | var co = require('co'); 3 | var assert = require('assert'); 4 | 5 | var fs = require('./index.js'); 6 | 7 | describe('.stats()', function () { 8 | it('should stat this file', co(function* () { 9 | var stats = yield fs.stat(__filename); 10 | assert.ok(stats.size); 11 | })) 12 | 13 | it('should throw on a nonexistent file', co(function* () { 14 | try { 15 | yield fs.stat(__filename + 'lkjaslkdjflaksdfasdf'); 16 | // the above expression should throw, 17 | // so this error will never be thrown 18 | throw new Error('nope'); 19 | } catch (err) { 20 | assert(err.message !== 'nope'); 21 | } 22 | })) 23 | }) 24 | 25 | describe('.exists()', function () { 26 | it('should find this file', co(function* () { 27 | assert.equal(true, yield fs.exists(__filename)) 28 | })) 29 | 30 | it('should return false for a nonexistent file', co(function* () { 31 | assert.equal(false, yield fs.exists(__filename + 'kljalskjdfklajsdf')) 32 | })) 33 | }) 34 | -------------------------------------------------------------------------------- /02-hello-world/README.md: -------------------------------------------------------------------------------- 1 | 2 | Now that you sort of have the idea of generators, 3 | the next step is to make the simplest Koa app, ever. 4 | 5 | Unlike Express where you use node.js' `req` and `res` object, 6 | Koa exposes its own very similar `this.request` and `this.response` objects. 7 | Also unlike Express and node.js, 8 | Koa uses getters and setters instead of methods. 9 | 10 | For example, 11 | in node.js, you might be used to the following: 12 | 13 | ```js 14 | res.statusCode = 200; 15 | res.setHeader('Content-Type', 'text/plain'); 16 | res.end('hello world'); 17 | ``` 18 | 19 | In Express, there is a shortcut: 20 | 21 | ```js 22 | res.send('hello world'); 23 | ``` 24 | 25 | However, in Koa, we use getter/setter: 26 | 27 | ```js 28 | app.use(function* () { 29 | this.response.body = 'hello world'; 30 | }) 31 | ``` 32 | 33 | ## Exercise 34 | 35 | Make an app that returns `hello world` for every response. 36 | Verify the following are correct: 37 | 38 | - Status Code: `200` 39 | - `Content-Length`: `11` 40 | - `Content-Type`: `text/plain; charset=utf-8` 41 | 42 | > Hint: Koa sets these headers for you with strings! 43 | -------------------------------------------------------------------------------- /02-hello-world/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | 4 | var app = module.exports = koa(); 5 | 6 | /** 7 | * Return "hello world" as a string on every request. 8 | * Hint: this only requires a single line of code. 9 | */ 10 | 11 | app.use(function* () { 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /02-hello-world/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var app = require('./index.js'); 5 | 6 | describe('Hello World', function () { 7 | it('should return hello world', function (done) { 8 | request(app.listen()) 9 | .get('/') 10 | .expect(200) 11 | .expect('Content-Length', 11) 12 | .expect('Content-Type', 'text/plain; charset=utf-8') 13 | .end(done) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /03-routing/README.md: -------------------------------------------------------------------------------- 1 | 2 | Unlike Express and many other frameworks, Koa does not include a router. 3 | Without a router, routing in Koa can be done by using `this.request.path` and `yield next`. 4 | To check if the request matches a specific path: 5 | 6 | ```js 7 | app.use(function* (next) { 8 | if (this.request.path === '/') { 9 | 10 | } 11 | }) 12 | ``` 13 | 14 | To skip this middleware: 15 | 16 | ```js 17 | app.use(function* (next) { 18 | if (skip) return yield next; 19 | }) 20 | ``` 21 | 22 | This is very similar to Express' `next()` call. 23 | 24 | Combining this together, 25 | you can route paths like this: 26 | 27 | ```js 28 | app.use(function* (next) { 29 | // skip the rest of the code if the route does not match 30 | if (this.request.path !== '/') return yield next; 31 | 32 | this.response.body = 'we are at home!'; 33 | }) 34 | ``` 35 | 36 | By `return`ing early, 37 | we don't need to bother having any nested `if` and `else` statements. 38 | Note that we're checking whether the path __does not match__, 39 | then early returning. 40 | 41 | Of course, you can write this the long way: 42 | 43 | ```js 44 | app.use(function* (next) { 45 | if (this.request.path === '/') { 46 | this.response.body = 'hello!'; 47 | } else { 48 | yield next; 49 | } 50 | }) 51 | ``` 52 | 53 | ## Exercise 54 | 55 | Create an app that returns the following responses from the following routes: 56 | 57 | - `/` - `hello world` 58 | - `/404` - `page not found` 59 | - `/500` - `internal server error`. 60 | 61 | In a real app, having each route as its own middleware is more ideal 62 | as it allows for easier refactoring. 63 | 64 | ## Learn More 65 | 66 | There are more properties you're probably interested in when routing: 67 | 68 | - `this.request.method` 69 | - `this.request.query` 70 | - `this.request.host` 71 | -------------------------------------------------------------------------------- /03-routing/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | 4 | var app = module.exports = koa(); 5 | 6 | app.use(function* () { 7 | 8 | }); 9 | 10 | app.use(function* () { 11 | 12 | }); 13 | 14 | app.use(function* () { 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /03-routing/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var app = require('./index.js'); 5 | 6 | describe('Routing', function () { 7 | it('GET / should return "hello world"', function (done) { 8 | request(app.listen()) 9 | .get('/') 10 | .expect('hello world', done); 11 | }) 12 | 13 | it('GET /404 should return "page not found"', function (done) { 14 | request(app.listen()) 15 | .get('/404') 16 | .expect('page not found', done); 17 | }) 18 | 19 | it('get /500 should return "internal server error"', function (done) { 20 | request(app.listen()) 21 | .get('/500') 22 | .expect('internal server error', done); 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /04-bodies/README.md: -------------------------------------------------------------------------------- 1 | 2 | So far, we've only used strings as bodies. 3 | Koa supports the following types of bodies: 4 | 5 | - Strings 6 | - Buffers 7 | - Streams (node) 8 | - JSON Objects 9 | 10 | If the body is not treated as the first three, 11 | Koa will assume that you want to send a JSON response, 12 | treating REST APIs as first-class citizens. 13 | 14 | ```js 15 | app.use(function* (next) { 16 | this.response.body = { 17 | message: 'this will be sent as a JSON response!' 18 | }; 19 | }) 20 | ``` 21 | 22 | When setting a stream as a body, 23 | Koa will automatically add any error handlers so you don't have to worry about error handling. 24 | 25 | ```js 26 | var fs = require('fs'); 27 | 28 | app.use(function* (next) { 29 | this.response.body = fs.createReadStream('some_file.txt'); 30 | // koa will automatically handle errors and leaks 31 | }) 32 | ``` 33 | 34 | ## Exercise 35 | 36 | Create an app that returns a stream when the client requests `/stream` and a JSON body when the client requests `/json`. 37 | 38 | ## Bonus 39 | 40 | When setting the body of the stream, Koa can't infer the `Content-Length` of the 41 | body. Set the `Content-Length` header yourself using `this.response.length=` 42 | and the "yieldable" version of `fs` you've created in exercise 1. 43 | -------------------------------------------------------------------------------- /04-bodies/index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var koa = require('koa'); 4 | 5 | var app = module.exports = koa(); 6 | 7 | /** 8 | * Create the `GET /stream` route that streams this file. 9 | * In node.js, the current file is available as a variable `__filename`. 10 | */ 11 | 12 | app.use(function* (next) { 13 | if (this.request.path !== '/stream') return yield* next; 14 | 15 | // this.response.type = 16 | // this.response.body = 17 | }); 18 | 19 | /** 20 | * Create the `GET /json` route that sends `{message:'hello world'}`. 21 | */ 22 | 23 | app.use(function* (next) { 24 | if (this.request.path !== '/json') return yield* next; 25 | 26 | // this.response.body = 27 | }); 28 | -------------------------------------------------------------------------------- /04-bodies/test.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var assert = require('assert'); 5 | var request = require('supertest'); 6 | 7 | var app = require('./index.js'); 8 | 9 | describe('Bodies', function () { 10 | it('GET /stream should return a stream', function (done) { 11 | var body = fs.readFileSync(path.join(__dirname, 'index.js'), 'utf8'); 12 | 13 | request(app.listen()) 14 | .get('/stream') 15 | .expect('Content-Type', /application\/javascript/) 16 | .expect(body, done); 17 | }) 18 | 19 | it('GET /json should return a JSON body', function (done) { 20 | request(app.listen()) 21 | .get('/json') 22 | .expect('Content-Type', /application\/json/) 23 | .end(function (err, res) { 24 | if (err) return done(err); 25 | 26 | assert.equal(res.body.message, 'hello world'); 27 | done(); 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /05-error-handling/README.md: -------------------------------------------------------------------------------- 1 | 2 | In Koa, error handling is done using `try/catch` (except with event emitters). 3 | You might not have seen this in a while if you've been working with 4 | Express and most other node frameworks. 5 | Unlike, Express, error handlers are simply decorators that you add to the __top__ of your middleware stack. 6 | 7 | ```js 8 | app.use(function* errorHandler(next) { 9 | try { 10 | // catch all downstream errors 11 | yield next; 12 | } catch (err) { 13 | // do something with the error 14 | } 15 | }) 16 | 17 | app.use(function* () { 18 | // throw an error downstream 19 | throw new Error('boom!'); 20 | }) 21 | ``` 22 | 23 | Each Koa `app` is an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) instance. 24 | All errors uncaught by any middleware are caught by `app.on('error', function (err, context) {})`. 25 | This is useful for logging. 26 | However, if you create your own error handler (i.e. catching it), 27 | you will have to manually emit these events yourself. 28 | 29 | In a real app, you would only want to `app.emit('error', err, this)` 30 | unknown errors. Client errors, such as validation errors, 31 | don't need to be logged. 32 | Using an error handler also allows you to create your own error pages. 33 | 34 | ## Exercise 35 | 36 | Create an error handler that intercepts downstream errors, 37 | responds to the client with `internal server error`, 38 | and emits the error on `app`. 39 | -------------------------------------------------------------------------------- /05-error-handling/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | 4 | var app = module.exports = koa(); 5 | 6 | app.use(function* errorHandler(next) { 7 | try { 8 | yield next; 9 | } catch (err) { 10 | // your error handling logic goes here 11 | } 12 | }); 13 | 14 | app.use(function* () { 15 | throw new Error('boom!'); 16 | }); 17 | -------------------------------------------------------------------------------- /05-error-handling/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var app = require('./index.js'); 5 | 6 | describe('Error Handling', function () { 7 | it('should return "internal server error"', function (done) { 8 | request(app.listen()) 9 | .get('/') 10 | .expect(500) 11 | .expect('internal server error', done); 12 | }) 13 | 14 | it('should emit an error event', function (done) { 15 | app.once('error', function () { 16 | done(); 17 | }); 18 | 19 | request(app.listen()) 20 | .get('/') 21 | .end(function noop(){}); 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /06-content-negotiation/README.md: -------------------------------------------------------------------------------- 1 | 2 | An important part of HTTP servers is content negotiation. 3 | Perhaps the most important is `Accept-Encoding` and `Content-Encoding`, 4 | which negotiates whether to compress content. 5 | This is the only `Accept` header supported by most CDNs. 6 | 7 | In Koa, `this.request.acceptsEncodings()` does all the content negotiation for you. 8 | Remember, if you compress your body, you should set the `Content-Encoding` header. 9 | View more about content negotiation in the [official docs](http://koajs.com/#content-negotiation). 10 | 11 | You __do not__ want to do anything like `if (~this.request.headers['accept-encoding'].indexOf('json'))`. 12 | These headers are very complex, and this type of logic is not specification-compliant. 13 | Use the `this.request.accepts()`-type methods. 14 | 15 | ## Exercise 16 | 17 | Using `this.request.acceptsEncodings()`, 18 | either send `hello world` gzipped or not gzipped (identity). 19 | For the purposes of this test, always set the `Content-Encoding` header, 20 | even if it's just `identity`. 21 | -------------------------------------------------------------------------------- /06-content-negotiation/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | 4 | /** 5 | * This is a promisified version of zlib's gzip function, 6 | * allowing you to simply `yield` it without "thunkifying" it. 7 | * 8 | * app.use(function* (next) { 9 | * this.response.set('Content-Encoding', 'gzip'); 10 | * this.response.body = yield gzip('something'); 11 | * }) 12 | */ 13 | var gzip = require('mz/zlib').gzip; 14 | 15 | var app = module.exports = koa(); 16 | 17 | app.use(function* () { 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /06-content-negotiation/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var request = require('supertest'); 4 | 5 | var app = require('./index.js'); 6 | 7 | describe('Content Negotiation', function () { 8 | it('when "Accept: gzip" it should return gzip', function (done) { 9 | request(app.listen()) 10 | .get('/') 11 | .set('Accept-Encoding', 'gzip') 12 | .expect(200) 13 | .expect('Content-Encoding', 'gzip') 14 | .expect('hello world', done); 15 | }) 16 | 17 | it('when "Accept: identity" it should not compress', function (done) { 18 | request(app.listen()) 19 | .get('/') 20 | .set('Accept-Encoding', 'identity') 21 | .expect(200) 22 | .expect('hello world', function (err, res) { 23 | if (err) return done(err); 24 | 25 | assert.equal(res.headers['content-encoding'] || 'identity', 'identity'); 26 | done(); 27 | }); 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /07-content-headers/README.md: -------------------------------------------------------------------------------- 1 | 2 | Both a request and a response could have various content headers. 3 | Some of these are: 4 | 5 | - `Content-Type` 6 | - `Content-Length` 7 | - `Content-Encoding` 8 | 9 | Among many others. We're particularly interested in `type` and `length`. 10 | Koa has getters/setters for `type` and `length`: 11 | 12 | - `this.request.type` 13 | - `this.request.length` 14 | - `this.response.type` 15 | - `this.response.length` 16 | 17 | Inferring `this.request.type` is a little difficult. 18 | For example, how do you know if the request is text? 19 | You don't want to use a regular expression or try all the possible mime types. 20 | Thus, Koa has `this.request.is()` for you: 21 | 22 | ```js 23 | this.request.is('image/*') // => image/png 24 | this.request.is('text') // => text or false 25 | ``` 26 | 27 | Learn more about [request.is()](http://koajs.com/#req-is-types-). 28 | 29 | ## Exercise 30 | 31 | Create an app that checks the `Content-Type` of the request. 32 | If it's `application/json`, return `{message: 'hi!'}` with appropriate content headers. 33 | Otherwise, return `ok` as a string. 34 | -------------------------------------------------------------------------------- /07-content-headers/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | 4 | var app = module.exports = koa(); 5 | 6 | app.use(function* () { 7 | 8 | }) 9 | -------------------------------------------------------------------------------- /07-content-headers/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var request = require('supertest'); 4 | 5 | var app = require('./index.js'); 6 | 7 | describe('Content Headers', function () { 8 | describe('when sending plain text', function () { 9 | it('should return "ok"', function (done) { 10 | request(app.listen()) 11 | .get('/') 12 | .set('Content-Type', 'text/plain') 13 | .set('Content-Length', '3') 14 | .send('lol') 15 | .expect(200) 16 | .expect('ok', done); 17 | }) 18 | }) 19 | 20 | describe('when sending JSON', function () { 21 | it('should return that JSON', function (done) { 22 | // just a random JSON body. don't bother parsing this. 23 | var body = JSON.stringify({}); 24 | 25 | request(app.listen()) 26 | .get('/') 27 | .set('Content-Type', 'application/json; charset=utf-8') 28 | .set('Content-Length', Buffer.byteLength(body)) 29 | .send(body) 30 | .expect(200) 31 | .expect('Content-Type', /application\/json/) 32 | .expect('Content-Length', 17) 33 | .end(function (err, res) { 34 | if (err) return done(err); 35 | 36 | assert.equal('hi!', res.body.message); 37 | done(); 38 | }) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /08-decorators/README.md: -------------------------------------------------------------------------------- 1 | 2 | In Koa, 3 | all middleware are essentially decorators for all following middleware: 4 | 5 | ```js 6 | app.use(function* decorator(function (subapp) { 7 | // do something before subapp executes 8 | yield* subapp; 9 | // do something after subapp executes 10 | })) 11 | 12 | app.use(function* subapp(next) { 13 | this.response.body = 'hello world'; 14 | }) 15 | ``` 16 | 17 | For more information on decorators, look up "decorator functions" or the "decorator pattern". 18 | I would add a link to a good resource, but I don't think any of the ones 19 | I've found yet are that good. 20 | 21 | ## Exercise 22 | 23 | Create a middleware that escapes all the HTML entities in the response. 24 | -------------------------------------------------------------------------------- /08-decorators/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | var escape = require('escape-html'); 4 | 5 | var app = module.exports = koa(); 6 | 7 | app.use(function* (next) { 8 | yield next; 9 | // add some logic here! 10 | }) 11 | 12 | app.use(function* body() { 13 | this.response.body = 'this following HTML should be escaped:
hi!
'; 14 | }); 15 | -------------------------------------------------------------------------------- /08-decorators/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var app = require('./index.js'); 5 | 6 | describe('Decorators', function () { 7 | it('should return the string escaped', function (done) { 8 | request(app.listen()) 9 | .get('/') 10 | .expect(200) 11 | .expect('this following HTML should be escaped: <p>hi!</p>', done); 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /09-templating/README.md: -------------------------------------------------------------------------------- 1 | 2 | There are many rendering utilities for Koa: 3 | 4 | - https://github.com/visionmedia/co-views 5 | - https://github.com/queckezz/koa-views 6 | 7 | However, in this example, we'll render using [Jade's API](https://github.com/visionmedia/jade#api). 8 | 9 | ## Exercise 10 | 11 | Render `homepage.jade` using Jade's API. 12 | Because we use Jade's `extends` feature, 13 | you probably need to use Jade's asynchronous API as well as the `.filename` option. 14 | -------------------------------------------------------------------------------- /09-templating/homepage.jade: -------------------------------------------------------------------------------- 1 | 2 | extends layout 3 | 4 | block body 5 | p Hello! 6 | -------------------------------------------------------------------------------- /09-templating/index.js: -------------------------------------------------------------------------------- 1 | 2 | var koa = require('koa'); 3 | var jade = require('jade'); 4 | var path = require('path'); 5 | 6 | var app = module.exports = koa(); 7 | 8 | app.use(function* () { 9 | var filename = path.join(__dirname, 'homepage.jade'); 10 | // this.response.body= 11 | }); 12 | -------------------------------------------------------------------------------- /09-templating/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | head 3 | html 4 | title Koa Templating 5 | body 6 | block body 7 | -------------------------------------------------------------------------------- /09-templating/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var app = require('./index.js'); 5 | 6 | describe('Templating', function () { 7 | it('should return the template', function (done) { 8 | var html = 'Hello!
'; 9 | 10 | request(app.listen()) 11 | .get('/') 12 | .expect(200) 13 | .expect('Content-Type', /text\/html/) 14 | .expect(html, done); 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /10-authentication/README.md: -------------------------------------------------------------------------------- 1 | 2 | To create an application, you probably need authentication. 3 | The simplest cookie-based session module is [koa-session](https://github.com/koajs/session). 4 | For CSRF protection, we've included [koa-csrf](https://github.com/koajs/csrf). 5 | For body parsing, we've included [co-body](https://github.com/visionmedia/co-body). 6 | Be sure to read the documentation on these middleware. 7 | 8 | ## Exercise 9 | 10 | Let's create a very simple app with login and logout features. 11 | Let's define the following routes: 12 | 13 | - `/` - If the user logs in, they should see `hello world`. 14 | Otherwise, they should see a `401` error because they aren't logged in. 15 | - `/login` - if the method is `GET`, a form should be returned. 16 | If the method is `POST`, it should validate the request body 17 | and attempt to login the user. 18 | - `/logout` - it should logout the user. 19 | 20 | We're not actually going to create users in this example. 21 | The only acceptable authentication is: 22 | 23 | ```bash 24 | username = username 25 | password = password 26 | ``` 27 | 28 | Mark the user as authenticated by populating `this.session.authenticated`. 29 | If `this.session.authenticated` exists, then the user is considered logged in. 30 | In real life, you'd want to set `this.session.userid=` or something to specify the user. 31 | 32 | For more specifics on how the app should work, consult the tests! 33 | If you'd like to test it out on your computer, 34 | run `PORT=3000 node --harmony-generators index.js` and open `localhost:3000` in your browser. 35 | -------------------------------------------------------------------------------- /10-authentication/form.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /10-authentication/index.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var koa = require('koa'); 4 | var path = require('path'); 5 | var parse = require('co-body'); 6 | var csrf = require('koa-csrf'); 7 | var session = require('koa-session'); 8 | 9 | var app = module.exports = koa(); 10 | 11 | var form = fs.readFileSync(path.join(__dirname, 'form.html'), 'utf8'); 12 | 13 | // adds .csrf among other properties to `this`. 14 | csrf(app); 15 | 16 | // use koa-session somewhere at the top of the app 17 | app.use(session()); 18 | 19 | // we need to set the `.keys` for signed cookies and the cookie-session module 20 | app.keys = ['secret1', 'secret2', 'secret3']; 21 | 22 | app.use(function* home(next) { 23 | if (this.request.path !== '/') return yield next; 24 | 25 | if (!this.session.authenticated) this.throw(401, 'user not authenticated'); 26 | 27 | this.response.body = 'hello world'; 28 | }) 29 | 30 | /** 31 | * If successful, the logged in user should be redirected to `/`. 32 | */ 33 | 34 | app.use(function* login(next) { 35 | if (this.request.path !== '/login') return yield* next; 36 | if (this.request.method === 'GET') return this.response.body = form.replace('{{csrf}}', this.csrf); 37 | 38 | }) 39 | 40 | /** 41 | * Let's 303 redirect to `/login` after every response. 42 | * If a user hits `/logout` when they're already logged out, 43 | * let's not consider that an error and rather a "success". 44 | */ 45 | 46 | app.use(function* logout(next) { 47 | if (this.request.path !== '/logout') return yield* next; 48 | 49 | }) 50 | 51 | /** 52 | * Serve this page for browser testing if not used by another module. 53 | */ 54 | 55 | if (!module.parent) app.listen(process.env.PORT || 3000); 56 | -------------------------------------------------------------------------------- /10-authentication/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var request = require('supertest'); 4 | 5 | var app = require('./index.js'); 6 | var server = app.listen(); 7 | 8 | request = request.agent(server); 9 | 10 | describe('Authentication', function () { 11 | describe('when not authenticated', function () { 12 | it('GET / should 401', function (done) { 13 | request.get('/').expect(401, done); 14 | }) 15 | 16 | it('GET /login should 200', function (done) { 17 | request.get('/login') 18 | .expect('Content-Type', /text\/html/) 19 | .expect(/^