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

u-wave / core / 12088261549

29 Nov 2024 04:49PM UTC coverage: 83.582% (-0.01%) from 83.592%
12088261549

Pull #677

github

web-flow
Merge 1a8a082b7 into 07405b710
Pull Request #677: Only log server errors at the error level

882 of 1054 branches covered (83.68%)

Branch coverage included in aggregate %.

12 of 13 new or added lines in 3 files covered. (92.31%)

1 existing line in 1 file now uncovered.

9743 of 11658 relevant lines covered (83.57%)

85.42 hits per line

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

94.09
/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) {
134✔
78
  if (!options.secret) {
134!
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

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

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

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

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

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

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

134✔
108
  uw.httpApi
134✔
109
    .use(pinoHttp({
134✔
110
      genReqId: () => randomUUID(),
134✔
111
      quietReqLogger: true,
134✔
112
      logger,
134✔
113
    }))
134✔
114
    .use(bodyParser.json())
134✔
115
    .use(cookieParser())
134✔
116
    .use(session({
134✔
117
      secret: options.secret,
134✔
118
      resave: false,
134✔
119
      saveUninitialized: false,
134✔
120
      cookie: {
134✔
121
        secure: uw.express.get('env') === 'production',
134✔
122
        httpOnly: true,
134✔
123
      },
134✔
124
    }))
134✔
125
    .use(uw.passport.initialize())
134✔
126
    .use(addFullUrl())
134✔
127
    .use(attachUwaveMeta(uw.httpApi, uw))
134✔
128
    .use(uw.passport.authenticate('jwt'))
134✔
129
    .use(rateLimit('api-http', { max: 500, duration: 60 * 1000 }));
134✔
130

134✔
131
  uw.httpApi
134✔
132
    .use('/auth', authenticate(uw.passport, {
134✔
133
      secret: options.secret,
134✔
134
      mailTransport: options.mailTransport,
134✔
135
      recaptcha: options.recaptcha,
134✔
136
      createPasswordResetEmail:
134✔
137
        options.createPasswordResetEmail ?? defaultCreatePasswordResetEmail,
134✔
138
    }))
134✔
139
    .use('/bans', bans())
134✔
140
    .use('/import', imports())
134✔
141
    .use('/now', now())
134✔
142
    .use('/search', search())
134✔
143
    .use('/server', server())
134✔
144
    .use('/users', users());
134✔
145

134✔
146
  uw.server = http.createServer(uw.express);
134✔
147
  if (options.helmet !== false) {
134✔
148
    uw.express.use(helmet({
134✔
149
      referrerPolicy: {
134✔
150
        policy: ['origin-when-cross-origin'],
134✔
151
      },
134✔
152
    }));
134✔
153
  }
134✔
154

134✔
155
  /** @type {import('cors').CorsOptions} */
134✔
156
  const corsOptions = {
134✔
157
    origin(origin, callback) {
134✔
158
      callback(null, matchOrigin(origin, runtimeOptions.allowedOrigins));
228✔
159
    },
134✔
160
  };
134✔
161
  uw.express.options('/api/*path', cors(corsOptions));
134✔
162
  uw.express.use('/api', cors(corsOptions), uw.httpApi);
134✔
163
  // An older name
134✔
164
  uw.express.use('/v1', cors(corsOptions), uw.httpApi);
134✔
165

134✔
166
  uw.onClose(() => {
134✔
167
    unsubscribe();
134✔
168
    uw.server.close();
134✔
169
  });
134✔
170
}
134✔
171

1✔
172
/**
1✔
173
 * @param {import('./Uwave.js').Boot} uw
1✔
174
 */
1✔
175
async function errorHandling(uw) {
134✔
176
  uw.logger.debug({ ns: 'uwave:http-api' }, 'setup HTTP error handling');
134✔
177
  uw.httpApi.use(errorHandler({
134✔
178
    onError(_req, error) {
134✔
179
      if ('status' in error && typeof error.status === 'number' && error.status >= 400 && error.status < 500) {
97✔
180
        return;
97✔
181
      }
97✔
NEW
182

×
UNCOV
183
      uw.logger.error({ err: error, ns: 'uwave:http-api' });
×
184
    },
134✔
185
  }));
134✔
186
}
134✔
187

1✔
188
export default httpApi;
1✔
189
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

© 2026 Coveralls, Inc