Commit 2ddc5409 authored by Bryan Tong's avatar Bryan Tong

Merge branch 'CookieBuilder' into 'master'

Cookie Builder

See merge request kado/kado!276
parents d0e731c7 c9728e8c
Pipeline #4350 passed with stage
in 41 seconds
......@@ -9,6 +9,35 @@ The `Format` library implements a common set of output formatting methods.
## Class: Format
Format is a completely static class of loosely related methods
### static Format.cookie(name, value, options)
* `name` {string} name of the cookie
* `value` {string|object} value of the cookie, Objects automatically serialized to JSON
* `options` {object} set cookie options
* Return {string} a string ready to set sent view `res.setHeader('Set-Coookie')`
Available Options
* `domain` {string} set the domain of the cookie eg: `{ domain: 'example.com' }`
* `expires` {string} UTC date string, indicating when the cookie expires
* `httpOnly` {boolean} `true` will stop javascript access to the cookie
* `maxAge` {number} number of seconds the cookie shall be valid
* `secure` {boolean} `true` for HTTPS only cookies
* `sameSite` {string} accepts `Strict`, `Lax`, or `None` as values to control cross origin
Example
```js
const Format = require('kado/lib/Format')
const Module = require('kado/lib/Module)
class MyModule extends Module {
someRoute (req, res) {
const cookie = Format.cookie('myCookie', { id: 1 }, { httpOnly: true })
res.setHeader('Set-Cookie', cookie)
// or use the built in cookie helper like so
res.cookie('myCookie', { id: 1 }, { httpOnly: true })
}
}
```
### `static Format.toFixedFix(n, prec)`
* `n` {mixed} the number, or string containing parsable number-like data
* `prec` {number} precision; how many places after decimal to retain
......
......@@ -18,7 +18,9 @@
* You should have received a copy of the GNU General Public License
* along with Kado. If not, see <https://www.gnu.org/licenses/>.
*/
const Assert = require('./Assert')
const net = require('./Network')
const UTCRegex = /[a-z]{3}, [0-9]{1,2} [a-z]{3} [0-9]{1,5} [0-9]{2}:[0-9]{2}:[0-9]{2} [a-z]{3}/i
const Separators = class Separators {
constructor (locale) {
this.locale = ''
......@@ -55,6 +57,43 @@ module.exports = class Format {
}
}
static cookie (name, value = '', options = {}) {
if (!name || !name.length) throw new Error('Name required')
const sameSiteType = ['strict', 'lax', 'none']
let cookie = `${name}=`
if (typeof value === 'string') cookie += value
else if (typeof value === 'object') cookie += JSON.stringify(value)
if (options.domain) cookie += `; Domain=${options.domain}`
if (options.expires) {
Assert.isOk(
Assert.match(UTCRegex, options.expires),
'Invalid Expiration Date Format'
)
cookie += `; Expires=${options.expires}`
}
if (options.httpOnly) cookie += '; HttpOnly'
if (options.maxAge) {
if (typeof options.maxAge === 'number') {
options.maxAge = `${options.maxAge}`
}
Assert.isOk(Assert.match(/\d+/, options.maxAge), 'Invalid maxAge')
cookie += `; MaxAge=${options.maxAge}`
}
if (options.path) cookie += `; Path=${options.path}`
if (options.sameSite) {
let sameSite = options.sameSite
sameSite = sameSite.toLowerCase()
if (sameSiteType.indexOf(sameSite) >= 0) {
sameSite = sameSite.charAt(0).toUpperCase() + sameSite.slice(1)
cookie += `; SameSite=${sameSite}`
} else {
throw new Error('Invalid SameSite value')
}
}
if (options.secure) cookie += '; Secure'
return cookie
}
static number (n, pos, pt = false, sep = false) {
if (pt.length > 1) {
S.setLocale(pt)
......
......@@ -20,6 +20,7 @@
*/
const Assert = require('./Assert')
const ETag = require('./ETag')
const Format = require('./Format')
const fs = require('./FileSystem')
const http = require('http')
const Mapper = require('./Mapper')
......@@ -150,6 +151,10 @@ class Router {
return function standardPreparation (req, res) {
req.locals = {}
req.cookie = Parser.cookie('' + req.headers.cookie)
res.cookie = (name, value, options) => {
const cookie = Format.cookie(name, value, options)
res.setHeader('Set-cookie', cookie)
}
req.ip = Router.getRemoteIP(app, req)
req.notify = Router.notify(req)
res.json = Router.renderJSON(res)
......
......@@ -24,6 +24,102 @@ const intlOk = require('../lib/Language').hasFullIntl()
const Format = require('../lib/Format')
const format = runner.suite('Format')
// all static no constructor test needed
format.suite('.cookie()', (it) => {
const expireDate = 'Thu, 01 Jan 1970 00:00:00 GMT'
it('should require a name', () => {
try {
Format.cookie()
} catch (err) {
Assert.eq('Name required', err.message)
}
})
it('should build a blank cookie', () => {
const cookie = Format.cookie('foo')
Assert.eq('foo=', cookie)
})
it('should accept a string for a value', () => {
const cookie = Format.cookie('foo', 'bar')
Assert.eq('foo=bar', cookie)
})
it('should accept an object for a value', () => {
const cookie = Format.cookie('foo', { foo: 'bar' })
Assert.eq('foo={"foo":"bar"}', cookie)
})
it('should add a domain', () => {
const cookie = Format.cookie('foo', { foo: 'foo: bar' }, { domain: '/' })
Assert.eq('foo={"foo":"foo: bar"}; Domain=/', cookie)
})
it('should add a expires', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { expires: expireDate })
Assert.eq('foo={"foo":"bar"}; Expires=Thu, 01 Jan 1970 00:00:00 GMT', cookie)
})
it('should only accept UTC date string for expires', () => {
try {
Format.cookie('foo', { foo: 'bar' }, { expires: 'dog' })
} catch (err) {
Assert.eq('Invalid Expiration Date Format', err.message)
}
})
it('should add a httpOnly', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { httpOnly: true })
Assert.eq('foo={"foo":"bar"}; HttpOnly', cookie)
})
it('should add a maxAge', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { maxAge: 3600 })
Assert.eq('foo={"foo":"bar"}; MaxAge=3600', cookie)
})
it('should only accept integer for maxAge', () => {
try {
Format.cookie('foo', { foo: 'bar' }, { maxAge: 'potato' })
} catch (err) {
Assert.eq('Invalid maxAge', err.message)
}
})
it('should add a path', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { path: '/' })
Assert.eq('foo={"foo":"bar"}; Path=/', cookie)
})
it('should accept sameSite', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { sameSite: 'strict' })
Assert.eq('foo={"foo":"bar"}; SameSite=Strict', cookie)
})
it('should accept sameSite', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { sameSite: 'lax' })
Assert.eq('foo={"foo":"bar"}; SameSite=Lax', cookie)
})
it('should accept sameSite', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { sameSite: 'none' })
Assert.eq('foo={"foo":"bar"}; SameSite=None', cookie)
})
it('should error on sameSite', () => {
try {
Format.cookie('foo', { foo: 'bar' }, { sameSite: 'foo' })
} catch (err) {
Assert.eq('Invalid SameSite value', err.message)
}
})
it('should add a secure', () => {
const cookie = Format.cookie('foo', { foo: 'bar' }, { secure: true })
Assert.eq('foo={"foo":"bar"}; Secure', cookie)
})
it('should add all options', () => {
const options = {}
options.domain = '/'
options.expires = expireDate
options.httpOnly = true
options.maxAge = 3600
options.path = '/'
options.secure = true
options.sameSite = 'Strict'
const cookie = Format.cookie('foo', { foo: 'bar' }, options)
Assert.eq(
'foo={"foo":"bar"}; Domain=/; Expires=' +
'Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly' +
'; MaxAge=3600; Path=/; SameSite=Strict; Secure',
cookie
)
})
})
format.suite('.toFixedFix()', (it) => {
it('(1.236,2) === 1.24', () => {
Assert.eq(Format.toFixedFix(1.236, 2), 1.24)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment