1 | hat = require 'hat'
|
2 |
|
3 | store = require './store'
|
4 |
|
5 | nop = (req, res, next) -> next()
|
6 | bind = (fst, snd) -> (req, res, next) -> fst req, res, -> snd req, res, next
|
7 | compose = (mw) -> mw.reduce ((state, current) -> bind state, current), nop
|
8 |
|
9 | dayDuration = 1000 * 60 * 60 * 24
|
10 |
|
11 | encodeCookie = (key, value, opts = {}) ->
|
12 | pairs = []
|
13 | pairs.push "#{key}=#{value}"
|
14 | pairs.push "Max-Age=#{opts['max-age']}" if opts['max-age']
|
15 | pairs.push "Domain=#{opts.domain}" if opts.domain
|
16 | pairs.push "Path=#{opts.path}" if opts.path
|
17 | pairs.push "Expires=#{opts.expires}" if opts.expires
|
18 | pairs.push "HttpOnly" if opts.httpOnly
|
19 | pairs.push "Secure" if opts.secure
|
20 | return pairs.join '; '
|
21 |
|
22 | setCookieMiddleware = (req, res, next) ->
|
23 | throw new Error 'missing req.cookies' unless req.cookies?
|
24 | throw new Error 'missing opts' unless @opts
|
25 | req.cookies.session_id ?= hat 128
|
26 | req.cookies.session_secret ?= hat 128
|
27 |
|
28 | for key, value of req.cookies
|
29 | res.setHeader 'Set-Cookie', encodeCookie key, value, @opts
|
30 | next()
|
31 |
|
32 | syncMiddleware = (req, res, next) ->
|
33 |
|
34 | {session_id, session_secret} = req.cookies
|
35 | store = @store
|
36 | throw new Error 'missing datastore' unless store?
|
37 | throw new Error 'missing store.setByIdAndSecret' unless store?.setByIdAndSecret?
|
38 | throw new Error 'missing store.getByIdAndSecret' unless store?.getByIdAndSecret?
|
39 |
|
40 |
|
41 | origEnd = res.end
|
42 | res.end = (data, encoding) ->
|
43 | res.end = origEnd
|
44 | store.setByIdAndSecret session_id, session_secret, req.session, (err) ->
|
45 | throw err if err?
|
46 | res.end data, encoding
|
47 |
|
48 |
|
49 | store.getByIdAndSecret session_id, session_secret, (err, session) ->
|
50 | throw err if err?
|
51 | req.session = session
|
52 | next()
|
53 |
|
54 |
|
55 | cleanupMiddleware = (req, res, next) ->
|
56 | throw new Error 'missing store.deleteOlderThen' unless @store?.deleteOlderThen?
|
57 | return next() if Math.random() > (@probability or 0.001)
|
58 | console.info 'run session cleanup'
|
59 | @days ?= 7
|
60 | today = new Date()
|
61 | cutofDate = new Date(+new Date - @days * dayDuration)
|
62 | @store.deleteOlderThen cutofDate, (err) ->
|
63 | return console.err err if err?
|
64 | console.info 'session cleanup finished'
|
65 | next()
|
66 |
|
67 | extendObject = (obj, key, value) ->
|
68 | opts = {}
|
69 | opts[key] = value: value
|
70 | Object.create obj, opts
|
71 |
|
72 | module.exports =
|
73 | cookieHandler: setCookieMiddleware
|
74 | syncHandler: syncMiddleware
|
75 | cleanupHandler: cleanupMiddleware
|
76 |
|
77 | setCookieHandler: (handler) -> Object.create @, cookieHandler: value: handler
|
78 | setCleanupeHandler: (handler) -> Object.create @, cleanupHandler: value: handler
|
79 | setSyncHandler: (handler) -> Object.create @, syncHandler: value: handler
|
80 |
|
81 | opts: {}
|
82 | set: (key, value) -> extendObject @, key, value
|
83 | setOpts: (key, value) -> extendObject @opts, key, value
|
84 |
|
85 | middleware: ->
|
86 | compose [
|
87 | @cookieHandler.bind opts: @opts
|
88 | @syncHandler.bind store: @_store
|
89 | @cleanupHandler.bind store: @_store
|
90 | ]
|
91 |
|
92 | store: (store) -> Object.create @, {_store: value: store}
|
93 | domain: (domain) -> @set 'opts', extendObject @opts, 'domain', domain
|
94 | maxAge: (maxAge) ->
|
95 | opts = Object.create @opts, {'max-age': value: maxAge}
|
96 | Object.create @, opts: value: opts
|
97 | path: (path) -> @set 'opts', @setOpts 'path', path
|
98 | expires: (expires) -> @set 'opts', @setOpts 'expires', expires
|
99 | isHttpOnly: (isHttpOnly) -> @set 'opts', @setOpts 'httpOnly', isHttpOnly
|
100 | isSecure: (isSecure) -> @set 'opts', @setOpts 'secure', isSecure
|
101 |
|
102 | module.exports.defaultStores = store
|