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

u-wave / core / 12201221070

06 Dec 2024 03:11PM UTC coverage: 85.316% (+1.0%) from 84.36%
12201221070

push

github

web-flow
Improve session handling (#682)

* Use the session ID as the key for lost socket messages

This supports multiple connections per user.

* Restore login state from session

* No longer set uwsession

* Continue to support existing uwsession tokens

* lint

* Do not store JWT auth in session

* 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

* Test for lost connection

* use off the shelf redis store

933 of 1112 branches covered (83.9%)

Branch coverage included in aggregate %.

79 of 120 new or added lines in 11 files covered. (65.83%)

6 existing lines in 1 file now uncovered.

9984 of 11684 relevant lines covered (85.45%)

90.95 hits per line

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

76.38
/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 skipIfCurrentDJ from '../utils/skipIfCurrentDJ.js';
1✔
8
import getOffsetPagination from '../utils/getOffsetPagination.js';
1✔
9
import toItemResponse from '../utils/toItemResponse.js';
1✔
10
import toListResponse from '../utils/toListResponse.js';
1✔
11
import toPaginatedResponse from '../utils/toPaginatedResponse.js';
1✔
12
import { muteUser, unmuteUser } from './chat.js';
1✔
13
import { REDIS_ACTIVE_SESSIONS } from '../SocketServer.js';
1✔
14

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
186
/**
1✔
187
 * Remove the user ID from the online users list.
1✔
188
 *
1✔
189
 * @param {import('../Uwave.js').default} uw
1✔
190
 * @param {UserID} userID
1✔
191
 */
1✔
192
async function disconnectUser(uw, userID) {
2✔
193
  await skipIfCurrentDJ(uw, userID);
2✔
194

×
195
  try {
×
196
    await uw.waitlist.removeUser(userID);
×
197
  } catch (err) {
×
198
    // It's expected that the user would not be in the waitlist
×
199
    if (!(err instanceof UserNotInWaitlistError)) {
×
200
      throw err;
×
201
    }
×
202
  }
×
203

×
NEW
204
  await uw.redis.lrem(REDIS_ACTIVE_SESSIONS, 0, userID);
×
205

×
206
  uw.publish('user:leave', { userID });
×
207
}
2✔
208

1✔
209
/**
1✔
210
 * @typedef {object} GetHistoryParams
1✔
211
 * @prop {UserID} id
1✔
212
 */
1✔
213

1✔
214
/**
1✔
215
 * @type {import('../types.js').Controller<GetHistoryParams>}
1✔
216
 */
1✔
217
async function getHistory(req) {
×
218
  const { id } = req.params;
×
219
  const pagination = getOffsetPagination(req.query, {
×
220
    defaultSize: 25,
×
221
    maxSize: 100,
×
222
  });
×
223
  const uw = req.uwave;
×
224

×
225
  const user = await uw.users.getUser(id);
×
226
  if (!user) {
×
227
    throw new UserNotFoundError({ id });
×
228
  }
×
229

×
230
  const history = await uw.history.getUserHistory(user, pagination);
×
231

×
232
  return toPaginatedResponse(history, {
×
233
    baseUrl: req.fullUrl,
×
234
    included: {
×
235
      media: ['media.media'],
×
236
      user: ['user'],
×
237
    },
×
238
  });
×
239
}
×
240

1✔
241
export {
1✔
242
  getUsers,
1✔
243
  getUser,
1✔
244
  getUserRoles,
1✔
245
  addUserRole,
1✔
246
  removeUserRole,
1✔
247
  changeUsername,
1✔
248
  changeAvatar,
1✔
249
  disconnectUser,
1✔
250
  getHistory,
1✔
251
  muteUser,
1✔
252
  unmuteUser,
1✔
253
};
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