4e7d2e6a90cd459bd15855a3b888688dd5fefbce
[ndcode_site.git] / _config / site.jst
1 let assert = require('assert')
2 let cookie = require('cookie')
3 let crypto = require('crypto')
4 let logjson = (await import('@ndcode/logjson')).default
5 //let NodeMailerCache = require('@ndcode/nodemailer_cache')
6 let XDate = require('xdate')
7 let ZettairCache = require('@ndcode/zettair_cache')
8
9 return async (resources, root, prev_site) => {
10   class CustomSite extends _jst_server.Site {
11     constructor(resources, root, options, prev_site) {
12       super(resources, root, options, prev_site)
13       this.database = undefined
14       this.database_date = new XDate().toUTCString('yyyyMMdd')
15       this.nodemailer_cache = undefined
16       this.zettair_cache = undefined
17     }
18
19     // called when the server starts or the site.jst file is modified
20     // in latter case it will carry over the previously created resource objects
21     async start() {
22       await _jst_server.Site.prototype.start.call(this)
23
24       assert(this.database === undefined)
25       this.database = await this.resources.ref(
26         'database',
27         async () => {
28           let database = new logjson.Database()
29           await database.open(this.root + '/database.logjson')
30           return database
31         }
32       )
33
34       //assert(this.nodemailer_cache === undefined)
35       //this.nodemailer_cache = await this.resources.ref(
36       //  'nodemailer_cache',
37       //  async () => new NodeMailerCache(true)
38       //)
39
40       assert(this.zettair_cache === undefined)
41       this.zettair_cache = await this.resources.ref(
42         'zettair_cache',
43         async () => new ZettairCache(true)
44       )
45     }
46
47     // called when the server starts or the site.jst file is modified
48     // in latter case the start() method of the new CustomSite object is called
49     // first and then the stop() method of the old CustomSite object, so that the
50     // reference counting can keep the resource objects alive during changeover
51     async stop() {
52       await _jst_server.Site.prototype.stop.call(this)
53
54       assert(this.database !== undefined)
55       await this.resources.unref('database')
56
57       //assert(this.nodemailer_cache !== undefined)
58       //await this.resources.unref('nodemailer_cache')
59
60       assert(this.zettair_cache !== undefined)
61       await this.resources.unref('zettair_cache')
62     }
63
64     // called once per second, responsible for cache cleaning and flushing
65     async kick() {
66       await _jst_server.Site.prototype.kick.call(this)
67
68       assert(this.database !== undefined)
69       await this.database.kick()
70
71       let new_database_date = new XDate().toUTCString('yyyyMMdd')
72       if (new_database_date !== this.database_date) {
73         console.log(
74           'rotate database',
75           this.database_date,
76           '->',
77           new_database_date
78         )
79         await this.database.rotate(
80           `${this.root}/database.logjson.${this.database_date}`
81         )
82         this.database_date = new_database_date
83       }
84
85       //assert(this.nodemailer_cache !== undefined)
86       //this.nodemailer_cache.kick()
87
88       assert(this.zettair_cache !== undefined)
89       this.zettair_cache.kick()
90     }
91
92     //// retrieves a particular email account (as a nodemailer transport)
93     //get_nodemailer(pathname) {
94     //  return /*await*/ this.nodemailer_cache.get(this.root + pathname)
95     //}
96
97     // retrieves a particular search index (node.js wrapper of a zettair object)
98     get_zettair(pathname) {
99       return /*await*/ this.zettair_cache.get(this.root + pathname)
100     }
101
102     // customize template serving to initialize env.now, env.session_key first
103     async serve_jst(env, pathname, ...args) {
104       let template
105       try {
106         template = await this.jst_cache.get(pathname)
107       }
108       catch (err) {
109         if (!(err instanceof Error) || err.code !== 'ENOENT')
110           throw err
111         return false
112       }
113       env.site = this
114
115       // added part
116       env.now = XDate.now()
117       let transaction = await this.database.Transaction()
118       try {
119         let root = await transaction.get({})
120         let sessions = await root.get('sessions', {})
121
122         let cookies = cookie.parse(env.request.headers.cookie || '')
123         let session, expires = new XDate(env.now)
124         if (
125           Object.prototype.hasOwnProperty.call(cookies, 'session_key') &&
126             (
127               session = await sessions.get(
128                 env.session_key = cookies.session_key
129               )
130             ) !== undefined &&
131             env.now < await session.get('expires', 0)
132         )
133           // if session key is already in database, the requester supports
134           // cookies, and each access extends the session expiry by 1 month
135           expires.addMonths(1)
136         else {
137           // first request for session, maybe a bot, retain for only 1 day
138           expires.addDays(1)
139
140           do {
141             env.session_key = crypto.randomBytes(16).toString('hex')
142           } while (sessions.has(env.session_key))
143           session = transaction.LazyObject()
144           sessions.set(env.session_key, session)
145         }
146
147         await session.set('expires', expires.getTime())
148         env.response.setHeader(
149           'Set-Cookie',
150           `session_key=${env.session_key}; expires=${expires.toUTCString()}; path=/; httponly;`
151         )
152
153         await transaction.commit()
154       }
155       catch (error) {
156         transaction.rollback()
157         throw error
158       }
159
160       await template(env, ...args)
161       return true
162     }
163
164     // customize the file search/serving algorithm for this particular website
165     async respond(env) {
166       if (
167         env.parsed_url.pathname === '/node_modules' ||
168         env.parsed_url.pathname.slice(0, 14) === '/node_modules/' ||
169         env.parsed_url.pathname === '/package.json' ||
170         env.parsed_url.pathname === '/package-lock.json'
171       ) {
172         this.die(env, `banned file ${env.parsed_url.pathname}`)
173         return
174       }
175       return /*await*/ _jst_server.Site.prototype.respond.call(this, env)
176     }
177   }
178
179   return new CustomSite(
180     resources,
181     root,
182     {},
183     prev_site
184   )
185 }