• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

u-wave / core / 12197793619

06 Dec 2024 11:12AM UTC coverage: 84.385% (+0.03%) from 84.36%
12197793619

Pull #682

github

goto-bus-stop
lint
Pull Request #682: Improve session handling

911 of 1089 branches covered (83.65%)

Branch coverage included in aggregate %.

85 of 122 new or added lines in 10 files covered. (69.67%)

50 existing lines in 2 files now uncovered.

9881 of 11700 relevant lines covered (84.45%)

92.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.9
/src/HttpApi.js
1
import fs from 'node:fs';
1✔
2
import http from 'node:http';
1✔
3
import { randomUUID } from 'node:crypto';
1✔
4
import express from 'express';
1✔
5
import bodyParser from 'body-parser';
1✔
6
import cookieParser from 'cookie-parser';
1✔
7
import cors from 'cors';
1✔
8
import helmet from 'helmet';
1✔
9
import session from 'express-session';
1✔
10
import qs from 'qs';
1✔
11
import { pinoHttp } from 'pino-http';
1✔
12

1✔
13
// routes
1✔
14
import authenticate from './routes/authenticate.js';
1✔
15
import bans from './routes/bans.js';
1✔
16
import search from './routes/search.js';
1✔
17
import server from './routes/server.js';
1✔
18
import users from './routes/users.js';
1✔
19
import now from './routes/now.js';
1✔
20
import imports from './routes/import.js';
1✔
21

1✔
22
// middleware
1✔
23
import addFullUrl from './middleware/addFullUrl.js';
1✔
24
import attachUwaveMeta from './middleware/attachUwaveMeta.js';
1✔
25
import rateLimit from './middleware/rateLimit.js';
1✔
26
import errorHandler from './middleware/errorHandler.js';
1✔
27

1✔
28
// utils
1✔
29
import AuthRegistry from './AuthRegistry.js';
1✔
30
import matchOrigin from './utils/matchOrigin.js';
1✔
31

1✔
32
const optionsSchema = JSON.parse(
1✔
33
  fs.readFileSync(new URL('./schemas/httpApi.json', import.meta.url), 'utf8'),
1✔
34
);
1✔
35

1✔
36
/**
1✔
37
 * @param {{ token: string, requestUrl: string }} options
1✔
38
 * @returns {import('nodemailer').SendMailOptions}
1✔
39
 */
1✔
40
function defaultCreatePasswordResetEmail({ token, requestUrl }) {
1✔
41
  const parsed = new URL(requestUrl);
1✔
42
  const { hostname } = parsed;
1✔
43
  const resetLink = new URL(`/reset/${token}`, parsed);
1✔
44
  return {
1✔
45
    from: `noreply@${hostname}`,
1✔
46
    subject: 'üWave Password Reset Request',
1✔
47
    text: `
1✔
48
      Hello,
1✔
49

1✔
50
      To reset your password, please visit:
1✔
51
      ${resetLink}
1✔
52
    `,
1✔
53
  };
1✔
54
}
1✔
55

1✔
56
/**
1✔
57
 * @typedef {express.Router & { authRegistry: AuthRegistry }} HttpApi
1✔
58
 */
1✔
59

1✔
60
/**
1✔
61
 * @typedef {object} HttpApiOptions - Static options for the HTTP API.
1✔
62
 * @prop {string|Buffer} secret
1✔
63
 * @prop {boolean} [helmet]
1✔
64
 * @prop {(error: Error) => void} [onError]
1✔
65
 * @prop {{ secret: string }} [recaptcha]
1✔
66
 * @prop {import('nodemailer').Transport} [mailTransport]
1✔
67
 * @prop {(options: { token: string, requestUrl: string }) =>
1✔
68
 *   import('nodemailer').SendMailOptions} [createPasswordResetEmail]
1✔
69
 * @typedef {object} HttpApiSettings - Runtime options for the HTTP API.
1✔
70
 * @prop {string[]} allowedOrigins
1✔
71
 */
1✔
72

1✔
73
/**
1✔
74
 * @param {import('./Uwave.js').Boot} uw
1✔
75
 * @param {HttpApiOptions} options
1✔
76
 */
1✔
77
async function httpApi(uw, options) {
143✔
78
  if (!options.secret) {
143!
UNCOV
79
    throw new TypeError('"options.secret" is empty. This option is used to sign authentication '
×
80
      + 'keys, and is required for security reasons.');
×
81
  }
×
82

143✔
83
  if (options.onError != null && typeof options.onError !== 'function') {
143!
UNCOV
84
    throw new TypeError('"options.onError" must be a function.');
×
85
  }
×
86

143✔
87
  const logger = uw.logger.child({
143✔
88
    ns: 'uwave:http-api',
143✔
89
    level: 'warn',
143✔
90
  });
143✔
91

143✔
92
  uw.config.register(optionsSchema['uw:key'], optionsSchema);
143✔
93

143✔
94
  /** @type {HttpApiSettings} */
143✔
95
  // @ts-expect-error TS2322: get() always returns a validated object here
143✔
96
  let runtimeOptions = await uw.config.get(optionsSchema['uw:key']);
143✔
97
  const unsubscribe = uw.config.subscribe('u-wave:api', /** @param {HttpApiSettings} settings */ (settings) => {
143✔
UNCOV
98
    runtimeOptions = settings;
×
99
  });
143✔
100

143✔
101
  logger.debug(runtimeOptions, 'start HttpApi');
143✔
102
  uw.httpApi = Object.assign(express.Router(), {
143✔
103
    authRegistry: new AuthRegistry(uw.redis),
143✔
104
  });
143✔
105

143✔
106
  uw.express = express();
143✔
107
  uw.express.set('query parser', /** @param {string} str */ (str) => qs.parse(str, { depth: 1 }));
143✔
108

143✔
109
  uw.httpApi
143✔
110
    .use(pinoHttp({
143✔
111
      genReqId: () => randomUUID(),
143✔
112
      quietReqLogger: true,
143✔
113
      logger,
143✔
114
    }))
143✔
115
    .use(bodyParser.json())
143✔
116
    .use(cookieParser())
143✔
117
    .use(session({
143✔
118
      secret: options.secret,
143✔
119
      resave: false,
143✔
120
      saveUninitialized: false,
143✔
121
      cookie: {
143✔
122
        secure: uw.express.get('env') === 'production',
143✔
123
        httpOnly: true,
143✔
124
      },
143✔
125
      store: new class extends session.Store {
143✔
126
        /**
143✔
127
         * @param {string} sid
143✔
128
         * @param {(err?: Error, data?: session.SessionData | null) => void} callback
143✔
129
         */
143✔
130
        get(sid, callback) {
143✔
NEW
UNCOV
131
          uw.redis.get(`session:${sid}`).then((data) => {
×
NEW
UNCOV
132
            callback(undefined, data == null ? null : JSON.parse(data));
×
NEW
133
          }, (err) => {
×
NEW
134
            callback(err);
×
NEW
UNCOV
135
          });
×
NEW
UNCOV
136
        }
×
137

143✔
138
        /**
143✔
139
         * @param {string} sid
143✔
140
         * @param {session.SessionData} data
143✔
141
         * @param {(err?: Error) => void} callback
143✔
142
         */
143✔
143
        set(sid, data, callback) {
143✔
144
          uw.redis.set(`session:${sid}`, JSON.stringify(data)).then(() => {
372✔
145
            callback();
372✔
146
          }, (err) => {
372✔
NEW
UNCOV
147
            callback(err);
×
148
          });
372✔
149
        }
372✔
150

143✔
151
        /**
143✔
152
         * @param {string} sid
143✔
153
         * @param {(err?: Error) => void} callback
143✔
154
         */
143✔
155
        destroy(sid, callback) {
143✔
156
          uw.redis.del(`session:${sid}`).then(() => {
186✔
157
            callback();
186✔
158
          }, (err) => {
186✔
NEW
UNCOV
159
            callback(err);
×
160
          });
186✔
161
        }
186✔
162
      }(),
143✔
163
    }))
143✔
164
    .use(uw.passport.initialize())
143✔
165
    .use(addFullUrl())
143✔
166
    .use(attachUwaveMeta(uw.httpApi, uw))
143✔
167
    .use(uw.passport.authenticate('jwt'))
143✔
168
    .use(uw.passport.session())
143✔
169
    .use(rateLimit('api-http', { max: 500, duration: 60 * 1000 }));
143✔
170

143✔
171
  uw.httpApi
143✔
172
    .use('/auth', authenticate(uw.passport, {
143✔
173
      secret: options.secret,
143✔
174
      mailTransport: options.mailTransport,
143✔
175
      recaptcha: options.recaptcha,
143✔
176
      createPasswordResetEmail:
143✔
177
        options.createPasswordResetEmail ?? defaultCreatePasswordResetEmail,
143✔
178
    }))
143✔
179
    .use('/bans', bans())
143✔
180
    .use('/import', imports())
143✔
181
    .use('/now', now())
143✔
182
    .use('/search', search())
143✔
183
    .use('/server', server())
143✔
184
    .use('/users', users());
143✔
185

143✔
186
  uw.server = http.createServer(uw.express);
143✔
187
  if (options.helmet !== false) {
143✔
188
    uw.express.use(helmet({
143✔
189
      referrerPolicy: {
143✔
190
        policy: ['origin-when-cross-origin'],
143✔
191
      },
143✔
192
    }));
143✔
193
  }
143✔
194

143✔
195
  /** @type {import('cors').CorsOptions} */
143✔
196
  const corsOptions = {
143✔
197
    origin(origin, callback) {
143✔
198
      callback(null, matchOrigin(origin, runtimeOptions.allowedOrigins));
249✔
199
    },
143✔
200
  };
143✔
201
  uw.express.options('/api/*path', cors(corsOptions));
143✔
202
  uw.express.use('/api', cors(corsOptions), uw.httpApi);
143✔
203
  // An older name
143✔
204
  uw.express.use('/v1', cors(corsOptions), uw.httpApi);
143✔
205

143✔
206
  uw.onClose(() => {
143✔
207
    unsubscribe();
143✔
208
    uw.server.close();
143✔
209
  });
143✔
210
}
143✔
211

1✔
212
/**
1✔
213
 * @param {import('./Uwave.js').Boot} uw
1✔
214
 */
1✔
215
async function errorHandling(uw) {
143✔
216
  uw.logger.debug({ ns: 'uwave:http-api' }, 'setup HTTP error handling');
143✔
217
  uw.httpApi.use(errorHandler({
143✔
218
    onError(_req, error) {
143✔
219
      if ('status' in error && typeof error.status === 'number' && error.status >= 400 && error.status < 500) {
106✔
220
        return;
106✔
221
      }
106✔
UNCOV
222

×
UNCOV
223
      uw.logger.error({ err: error, ns: 'uwave:http-api' });
×
224
    },
143✔
225
  }));
143✔
226
}
143✔
227

1✔
228
export default httpApi;
1✔
229
export { errorHandling };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc