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

u-wave / core / 12200614762

06 Dec 2024 02:32PM UTC coverage: 84.144% (-0.2%) from 84.36%
12200614762

Pull #682

github

goto-bus-stop
Mimick session ID for JWT auth

The main point is to allow the tests to continue to use JWT auth,
while also testing session features such as missing messages for lost
connections
Pull Request #682: Improve session handling

906 of 1085 branches covered (83.5%)

Branch coverage included in aggregate %.

83 of 154 new or added lines in 11 files covered. (53.9%)

6 existing lines in 1 file now uncovered.

9867 of 11718 relevant lines covered (84.2%)

90.35 hits per line

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

87.65
/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!
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!
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✔
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
131
          uw.redis.get(`session:${sid}`).then((data) => {
×
NEW
132
            callback(undefined, data == null ? null : JSON.parse(data));
×
NEW
133
          }, (err) => {
×
NEW
134
            callback(err);
×
NEW
135
          });
×
NEW
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✔
NEW
144
          uw.redis.set(`session:${sid}`, JSON.stringify(data)).then(() => {
×
NEW
145
            callback();
×
NEW
146
          }, (err) => {
×
NEW
147
            callback(err);
×
NEW
148
          });
×
NEW
149
        }
×
150

143✔
151
        /**
143✔
152
         * @param {string} sid
143✔
153
         * @param {(err?: Error) => void} callback
143✔
154
         */
143✔
155
        destroy(sid, callback) {
143✔
NEW
156
          uw.redis.del(`session:${sid}`).then(() => {
×
NEW
157
            callback();
×
NEW
158
          }, (err) => {
×
NEW
159
            callback(err);
×
NEW
160
          });
×
NEW
161
        }
×
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', { session: false }))
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✔
222

×
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

© 2026 Coveralls, Inc