-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
196 lines (166 loc) · 6.24 KB
/
main.ts
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
import { readFile } from 'fs/promises';
import dotenv from 'dotenv';
import { getNormalizedEnv } from 'server/lib/env.ts';
import winston from 'winston';
import { initLoggers, closeLoggers } from 'server/lib/logger.ts';
import { createAvatarUploadDirectory, createCoverUploadDirectory } from 'server/lib/upload';
import dbClient, { testDatabaseConnection } from 'server/lib/db.ts';
import { initQueue } from 'server/lib/queue.ts';
import initWorkers from 'server/lib/workers.ts';
import http from 'http';
import https from 'https';
import { promisify } from 'util';
import express, { ErrorRequestHandler } from 'express';
import morgan from 'morgan';
import helmet from 'server/lib/middleware/helmet.ts';
import compression from 'compression';
import bodyParser from 'body-parser';
import qs from 'qs';
import session from 'express-session';
import { PrismaSessionStore } from '@quixo3/prisma-session-store';
import rateLimit from 'server/lib/middleware/rate-limit.ts';
import { mountApiEndpoints } from 'server/api/index.ts';
import spaRoutes from 'server/lib/middleware/spa-routes.ts';
import { createServer } from 'vite';
async function main() {
dotenv.config();
const env = await getNormalizedEnv();
await initLoggers();
winston.info('TrackBear is starting up and logs are online!');
// use `winston` just as the general logger
const accessLogger = winston.loggers.get('access');
// make sure all the directories we need exist
await createAvatarUploadDirectory();
await createCoverUploadDirectory();
winston.info('Avatar and cover upload directories exist');
// test the database connection
try {
await testDatabaseConnection();
winston.info('Database connection established');
} catch(err) {
console.error(`Could not connect to the database: ${err.message}`);
winston.error(`${err.message}`, {
message: err.message,
stack: err.stack.split('\n'),
});
process.exit(2);
}
// initialize the queue
await initQueue();
// initialize the workers
initWorkers();
// let's start up the server!
const app = express();
// allow arrays in query strings with just commas
// this must be set before anything else (see https://github.com/expressjs/express/issues/4979)
app.set('query parser', (str: string) => qs.parse(str, { comma: true }));
// add security headers
app.use(await helmet());
// compress responses
app.use(compression());
// don't say that we're using Express
app.disable('x-powered-by');
// are we behind a proxy?
if(env.HAS_PROXY) {
app.set('trust proxy', 1);
}
// log requests
// always stream to the access logger
app.use(morgan('combined', { stream: { write: function(message) { accessLogger.info(message); } } }));
// also stream to the console if we're developing
if(env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// parse request bodies using application/json
app.use(bodyParser.json());
// sessions
// Allow multiple signing secrets: see using an array at https://www.npmjs.com/package/express-session#secret
const cookieSecret = (env.COOKIE_SECRET || '').split(',');
// the combination of maxAge: 2 days & rolling: true means that you'll get logged out if you don't do _something_ every 2 days
app.use(session({
cookie: {
maxAge: 2 * 24 * 60 * 60 * 1000, // in ms
secure: true,
sameSite: 'strict',
},
name: 'trackbear.sid',
secret: cookieSecret,
resave: false,
saveUninitialized: false,
rolling: true,
store: new PrismaSessionStore(dbClient, {
checkPeriod: 2 * 60 * 1000, // in ms, how often to delete expired sessions
dbRecordIdIsSessionId: true,
dbRecordIdFunction: undefined,
}),
}));
// /api: mount the API routes
if(env.NODE_ENV !== 'development') {
// add rate-limiting for the API for production
app.use('/api', rateLimit());
}
mountApiEndpoints(app);
// Serve the front-end - either statically or out of the vite server, depending
if(env.NODE_ENV === 'production') {
// serve the front-end statically out of dist/
winston.info('Serving the front-end out of dist/');
app.use(spaRoutes(['/assets', '/images', '/uploads', '/manifest.json']));
app.use('/uploads', express.static(env.UPLOADS_PATH));
app.use(express.static('./dist'));
} else {
// Serve the front end using the schmancy HMR vite server.
// This middleware has a catch-all route
winston.info('Serving the front-end dynamically using vite');
const vite = await createServer({
server: {
middlewareMode: true,
https: env.ENABLE_TLS ? {
key: env.TLS_KEY_PATH,
cert: env.TLS_CERT_PATH,
} : undefined,
},
appType: 'spa',
publicDir: './public',
});
app.use(vite.middlewares); // vite takes care of serving the front end
// NOTE: alternately, it could be appType: custom, and then app.use('*', handleServingHtml)
// see https://vitejs.dev/config/server-options.html#server-middlewaremode
}
// baseline server-side error handling
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
const lastChanceErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
console.error(err);
res.status(500).send('500 Server error');
};
app.use(lastChanceErrorHandler);
// are we doing HTTP or HTTPS?
let server: https.Server | http.Server;
if(env.ENABLE_TLS) {
server = https.createServer({
key: await readFile(env.TLS_KEY_PATH),
cert: await readFile(env.TLS_CERT_PATH),
}, app);
} else {
server = http.createServer(app);
}
server.listen(env.PORT, () => {
winston.info(`TrackBear is now listening on ${env.ENABLE_TLS ? 'https' : 'http'}://localhost:${env.PORT}/`);
});
// handle SIGINT for graceful shutdown
process.on('SIGINT', async () => {
try {
await handleGracefulShutdown('SIGINT', server);
process.exit(0);
} catch {
process.exit(1);
}
});
}
async function handleGracefulShutdown(signal: string, server: https.Server | http.Server) {
winston.info(`Received ${signal}, shutting down gracefully...`);
// no need to disconnect Prisma; it does it itself
// we only need to close off the server and the logs
await promisify(server.close)();
await closeLoggers();
}
main().catch(e => console.error(e));