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

u-wave / core / 21165135184

20 Jan 2026 08:49AM UTC coverage: 86.583% (+0.04%) from 86.542%
21165135184

Pull #734

github

web-flow
Merge cb6311a59 into 443e76f56
Pull Request #734: Move away from Redis for key-value usages

1025 of 1220 branches covered (84.02%)

Branch coverage included in aggregate %.

35 of 50 new or added lines in 5 files covered. (70.0%)

1 existing line in 1 file now uncovered.

10623 of 12233 relevant lines covered (86.84%)

106.27 hits per line

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

71.63
/src/controllers/users.js
1
import {
1✔
2
  HTTPError,
1✔
3
  PermissionError,
1✔
4
  UserNotFoundError,
1✔
5
  UserNotInWaitlistError,
1✔
6
} from '../errors/index.js';
1✔
7
import getOffsetPagination from '../utils/getOffsetPagination.js';
1✔
8
import toItemResponse from '../utils/toItemResponse.js';
1✔
9
import toListResponse from '../utils/toListResponse.js';
1✔
10
import toPaginatedResponse from '../utils/toPaginatedResponse.js';
1✔
11
import { muteUser, unmuteUser } from './chat.js';
1✔
12
import { KEY_ACTIVE_SESSIONS } from '../SocketServer.js';
1✔
13

1✔
14
/**
1✔
15
 * @typedef {import('../schema').UserID} UserID
1✔
16
 */
1✔
17

1✔
18
/**
1✔
19
 * @typedef {object} GetUsersQuery
1✔
20
 * @prop {string} filter
1✔
21
 */
1✔
22

1✔
23
/**
1✔
24
 * @type {import('../types.js').AuthenticatedController<{}, GetUsersQuery>}
1✔
25
 */
1✔
26
async function getUsers(req) {
1✔
27
  const { filter } = req.query;
1✔
28
  const pagination = getOffsetPagination(req.query, {
1✔
29
    defaultSize: 50,
1✔
30
  });
1✔
31
  const { users } = req.uwave;
1✔
32

1✔
33
  const userList = await users.getUsers(filter, pagination);
1✔
34

1✔
35
  return toPaginatedResponse(userList, {
1✔
36
    baseUrl: req.fullUrl,
1✔
37
  });
1✔
38
}
1✔
39

1✔
40
/**
1✔
41
 * @typedef {object} GetUserParams
1✔
42
 * @prop {UserID} id
1✔
43
 */
1✔
44

1✔
45
/**
1✔
46
 * @type {import('../types.js').Controller<GetUserParams>}
1✔
47
 */
1✔
48
async function getUser(req) {
4✔
49
  const { users } = req.uwave;
4✔
50
  const { id: userID } = req.params;
4✔
51

4✔
52
  const user = await users.getUser(userID);
4✔
53
  if (!user) {
4!
54
    throw new UserNotFoundError({ id: userID });
×
55
  }
×
56

4✔
57
  return toItemResponse(user, {
4✔
58
    url: req.fullUrl,
4✔
59
  });
4✔
60
}
4✔
61

1✔
62
/**
1✔
63
 * @typedef {object} GetUserRolesParams
1✔
64
 * @prop {UserID} id
1✔
65
 */
1✔
66

1✔
67
/**
1✔
68
 * @type {import('../types.js').Controller<GetUserRolesParams>}
1✔
69
 */
1✔
70
async function getUserRoles(req) {
×
71
  const { acl, users } = req.uwave;
×
72
  const { id } = req.params;
×
73

×
74
  const user = await users.getUser(id);
×
75
  if (!user) {
×
76
    throw new UserNotFoundError({ id });
×
77
  }
×
78

×
79
  const roles = await acl.getAllPermissions(user);
×
80

×
81
  return toListResponse(roles, {
×
82
    url: req.fullUrl,
×
83
  });
×
84
}
×
85

1✔
86
/**
1✔
87
 * @typedef {object} AddUserRoleParams
1✔
88
 * @prop {UserID} id
1✔
89
 * @prop {string} role
1✔
90
 */
1✔
91

1✔
92
/**
1✔
93
 * @type {import('../types.js').AuthenticatedController<AddUserRoleParams>}
1✔
94
 */
1✔
95
async function addUserRole(req) {
3✔
96
  const { user: moderator } = req;
3✔
97
  const { id, role } = req.params;
3✔
98
  const { acl, users } = req.uwave;
3✔
99

3✔
100
  const canModifyRoles = moderator.roles.includes('admin');
3✔
101
  if (!canModifyRoles) {
3!
102
    throw new PermissionError({ requiredRole: 'admin' });
×
103
  }
×
104

3✔
105
  const user = await users.getUser(id);
3✔
106
  if (!user) {
3✔
107
    throw new UserNotFoundError({ id });
1✔
108
  }
1✔
109

2✔
110
  await acl.allow(user, [role]);
2✔
111

1✔
112
  return toItemResponse({}, {
1✔
113
    url: req.fullUrl,
1✔
114
  });
1✔
115
}
3✔
116

1✔
117
/**
1✔
118
 * @typedef {object} RemoveUserRoleParams
1✔
119
 * @prop {UserID} id
1✔
120
 * @prop {string} role
1✔
121
 */
1✔
122

1✔
123
/**
1✔
124
 * @type {import('../types.js').AuthenticatedController<RemoveUserRoleParams>}
1✔
125
 */
1✔
126
async function removeUserRole(req) {
2✔
127
  const { user: moderator } = req;
2✔
128
  const { id, role } = req.params;
2✔
129
  const { acl, users } = req.uwave;
2✔
130

2✔
131
  const canModifyRoles = moderator.roles.includes('admin');
2✔
132
  if (!canModifyRoles) {
2!
133
    throw new PermissionError({ requiredRole: 'admin' });
×
134
  }
×
135

2✔
136
  const user = await users.getUser(id);
2✔
137
  if (!user) {
2✔
138
    throw new UserNotFoundError({ id });
1✔
139
  }
1✔
140

1✔
141
  await acl.disallow(user, [role]);
1✔
142

1✔
143
  return toItemResponse({}, {
1✔
144
    url: req.fullUrl,
1✔
145
  });
1✔
146
}
2✔
147

1✔
148
/**
1✔
149
 * @typedef {object} ChangeUsernameParams
1✔
150
 * @prop {UserID} id
1✔
151
 * @typedef {object} ChangeUsernameBody
1✔
152
 * @prop {string} username
1✔
153
 */
1✔
154

1✔
155
/**
1✔
156
 * @type {import('../types.js').AuthenticatedController<
1✔
157
 *     ChangeUsernameParams, {}, ChangeUsernameBody>}
1✔
158
 */
1✔
159
async function changeUsername(req) {
5✔
160
  const { user: moderator } = req;
5✔
161
  const { id } = req.params;
5✔
162
  const { username } = req.body;
5✔
163
  const { users } = req.uwave;
5✔
164

5✔
165
  if (id !== moderator.id) {
5✔
166
    throw new PermissionError();
1✔
167
  }
1✔
168

4✔
169
  const user = await users.updateUser(
4✔
170
    id,
4✔
171
    { username },
4✔
172
    { moderator },
4✔
173
  );
4✔
174

3✔
175
  return toItemResponse(user);
3✔
176
}
5✔
177

1✔
178
/**
1✔
179
 * @returns {Promise<import('type-fest').JsonObject>}
1✔
180
 */
1✔
181
async function changeAvatar() {
×
182
  throw new HTTPError(500, 'Not implemented');
×
183
}
×
184

1✔
185
/**
1✔
186
 * Remove the user ID from the online users list.
1✔
187
 *
1✔
188
 * @param {import('../Uwave.js').default} uw
1✔
189
 * @param {UserID} userID
1✔
190
 */
1✔
191
async function disconnectUser(uw, userID) {
×
192
  try {
×
193
    await uw.booth.removeUser(userID);
×
194
  } catch (err) {
×
195
    // It's expected that the user would not be in the waitlist
×
196
    if (!(err instanceof UserNotInWaitlistError)) {
×
197
      throw err;
×
198
    }
×
199
  }
×
200

×
201
  try {
×
202
    await uw.waitlist.removeUser(userID);
×
203
  } catch (err) {
×
204
    // It's expected that the user would not be in the waitlist
×
205
    if (!(err instanceof UserNotInWaitlistError)) {
×
206
      throw err;
×
207
    }
×
208
  }
×
209

×
NEW
210
  const userIDs = new Set(
×
NEW
211
    /** @type {import('../schema.js').UserID[] | null} */ (
×
NEW
212
      await uw.keyv.get(KEY_ACTIVE_SESSIONS)
×
NEW
213
    ) ?? [],
×
NEW
214
  );
×
NEW
215
  userIDs.delete(userID);
×
NEW
216

×
NEW
217
  await uw.keyv.set(KEY_ACTIVE_SESSIONS, Array.from(userIDs));
×
218

×
219
  uw.publish('user:leave', { userID });
×
220
}
×
221

1✔
222
/**
1✔
223
 * @typedef {object} GetHistoryParams
1✔
224
 * @prop {UserID} id
1✔
225
 */
1✔
226

1✔
227
/**
1✔
228
 * @type {import('../types.js').Controller<GetHistoryParams>}
1✔
229
 */
1✔
230
async function getHistory(req) {
×
231
  const { id } = req.params;
×
232
  const pagination = getOffsetPagination(req.query, {
×
233
    defaultSize: 25,
×
234
    maxSize: 100,
×
235
  });
×
236
  const uw = req.uwave;
×
237

×
238
  const user = await uw.users.getUser(id);
×
239
  if (!user) {
×
240
    throw new UserNotFoundError({ id });
×
241
  }
×
242

×
243
  const history = await uw.history.getUserHistory(user, pagination);
×
244

×
245
  return toPaginatedResponse(history, {
×
246
    baseUrl: req.fullUrl,
×
247
    included: {
×
248
      media: ['media.media'],
×
249
      user: ['user'],
×
250
    },
×
251
  });
×
252
}
×
253

1✔
254
export {
1✔
255
  getUsers,
1✔
256
  getUser,
1✔
257
  getUserRoles,
1✔
258
  addUserRole,
1✔
259
  removeUserRole,
1✔
260
  changeUsername,
1✔
261
  changeAvatar,
1✔
262
  disconnectUser,
1✔
263
  getHistory,
1✔
264
  muteUser,
1✔
265
  unmuteUser,
1✔
266
};
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