-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.js
202 lines (178 loc) · 6.61 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
const express = require('express')
const helmet = require('helmet')
const compression = require('compression')
const session = require('express-session')
const MongoDBStore = require('connect-mongodb-session')(session)
const connectToMongo = require('./lib/mongoConnection')
const sane = require('./lib/sane')
const { protectAdminRoutes } = require('./lib/auth')
const { join } = require('path')
const fs = require('fs')
const livereload = require('livereload')
const connectLivereload = require('connect-livereload')
const { requestLogger, logger } = require('./lib/logger')
const dotenv = require('dotenv')
dotenv.config()
// Check for required .env values.
const requiredEnvs = ['PORT', 'NAME', 'NODE_ENV']
const missingEnvs = requiredEnvs.filter((e) => !process.env[e])
if (missingEnvs.length) {
console.log('\nBefore running saneJS, please set the following values in your .env:\n')
for (const v of missingEnvs) logger.error('-', v)
console.log('')
process.exit()
}
// Create Express app.
const app = express()
// Use gzip compression.
app.use(compression())
// Modify response headers for better security.
app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }))
// Set path to static dir.
const staticDirectory = join(__dirname, 'static')
// Connect to LiveReload for development: Watch static dir and nodemon refresh.
if (app.get('env') === 'development') {
const liveReloadServer = livereload.createServer()
// Only directly watch the static directory.
liveReloadServer.watch(staticDirectory)
// When Nodemon restarts, refresh the browser.
liveReloadServer.server.once('connection', () => {
setTimeout(() => {
liveReloadServer.refresh('/')
}, 100)
})
// Inject the JS snippet into page <head>.
app.use(connectLivereload())
}
// Serve static resources like images and css.
app.use(express.static(staticDirectory))
// Use simple request logger.
app.use(requestLogger)
// Allow use of modern browser form data and JSON responses.
app.use(express.urlencoded({ limit: '50mb', extended: true, parameterLimit: 50000 }))
app.use(express.json())
// Use sane templating middleware.
app.use(sane)
// Use Express Session
if (process.env.SESSION_SECRET && process.env.MONGO_URI) {
app.set('trustproxy', true)
app.use(
session({
secret: process.env.SESSION_SECRET,
saveUninitialized: true,
resave: true,
store: new MongoDBStore({
uri: process.env.MONGO_URI,
collection: '_express_sessions'
}),
cookie: { maxAge: 1000 * 60 * 60 * 2 }, // 2 hours
rolling: true
})
)
}
// Use auth middleware for protected routes.
app.use(protectAdminRoutes)
// Need to use self-executing function for the rest so we can await dynamic routes.
;(async function () {
// Dynamically add all routes found in routes/ dir excluding those prefixed with underscore.
await dynamicallyLoadRoutes(join(__dirname, 'routes'))
// If route hasn't been handled yet, serve a plain .html template if present.
app.use((req, res, next) => {
if (req.method != 'GET') return next()
if (req.path.indexOf('.') !== -1) return next() // Don't waste cpu on .css and favicons
res.render(req.path, {})
})
// Handle simple 404.
app.use((req, res, next) => {
if (req.url == '/service-worker.js') return next()
logger.error(`404 NOT FOUND: ${req.method} ${req.url}`)
res.error404()
})
// Handle all other errors.
app.use((error, req, res, next) => {
// Is this a regular 404 raised by `res.error(404)`?
if (error?.status == 404) {
logger.error(`404 NOT FOUND: ${req.method} ${req.url}`)
res.error404()
return
}
// Is this a MongoDB castError?
// Ex: /usersById/not-a-real-id causes Mongo to throw this error.
if (error?.name == 'CastError') {
// Handle as a regular 404.
logger.error('MongoDB CastError (invalid ObjectID)')
logger.error(`404 NOT FOUND: ${req.method} ${req.url}`)
res.error404()
return
}
// Show debug info for dev?
const debug = {}
if (process.env.NODE_ENV == 'development') {
debug.message = error.message || error
debug.stack = error.stack
logger.error(debug.stack)
}
res.error500(debug)
})
// Connect to Mongo
if (process.env.MONGO_URI) connectToMongo()
// Start the server.
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log('\x1b[33m', `\n👍 Listening at localhost:${port}\n`, '\x1b[0m')
})
})()
// ============================================= //
async function dynamicallyLoadRoutes(dirPath) {
await continueLoadingRoutes(dirPath)
async function continueLoadingRoutes(thisDir) {
const files = await fs.promises.readdir(thisDir)
for (const fileName of files) {
// Ignore files starting with a double underscore.
if (fileName.startsWith('__')) continue
// Set the full abs path to file.
const absPath = join(thisDir, fileName)
// Recurse into directories for nested routes.
if (fs.statSync(absPath).isDirectory()) {
await continueLoadingRoutes(absPath)
continue
}
// Set the route endpoint for .html or .md files only.
const matchRoute = absPath.match(/([\w\-\/. ]+)\.(html|md)/)
if (!matchRoute || matchRoute.length < 2) continue
const route = matchRoute[1].replace(dirPath, '')
const fileExt = matchRoute[2]
// Read the file and look for <script server>
const template = await fs.promises.readFile(absPath, 'utf-8')
const matchScript = template.match(/<script[\s]server>([\s\S]+?)<\/script>/m)
let serverBlock = matchScript && matchScript[1] + '\nreturn server'
const server = require('express').Router()
let routeHandler
if (serverBlock) {
// Parse the serverBlock, passing in refs to Express router `server`, Node `require`, and `self` route reference.
try {
routeHandler = new Function('server', 'require', 'self', serverBlock)
useRoute(route, routeHandler(server, require, route), absPath)
} catch (err) {
logger.error(`Unable to parse server block in: ${absPath}\n\n${err.stack}`)
}
}
}
}
}
const routes = {} // Keep track so we can check for duplicates.
function useRoute(route, handler, absPath) {
// Avoid adding duplicate route.
if (routes[route]) {
logger.error('Duplicate routes defined for', route, 'in:\n - ', routes[route], '\n - ', absPath)
return
}
app.use(route, handler)
routes[route] = absPath
// Also add special route for index file?
if (route.endsWith('/index')) {
const indexRoute = route.slice(0, -5)
app.use(indexRoute, handler)
routes[route] = absPath
}
}