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

statuscompliance / status-backend / 16411359121

21 Jul 2025 07:47AM UTC coverage: 86.937% (+0.1%) from 86.822%
16411359121

push

github

alvarobernal2412
test(encryption): added unit tests

907 of 1070 branches covered (84.77%)

Branch coverage included in aggregate %.

1855 of 2107 relevant lines covered (88.04%)

21.79 hits per line

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

91.75
/src/controllers/user.controller.js
1
import { models } from '../models/models.js';
2
import bcrypt from 'bcrypt';
3
import jwt from 'jsonwebtoken';
4
import { verifyAccessToken } from '../utils/tokenUtils.js';
5
import { getNodeRedToken } from '../utils/nodeRedToken.js';
6
import { handleControllerError } from '../utils/errorHandler.js';
7
import logger from '../config/logger.js';
8

9
const token_expiration = parseInt(process.env.JWT_EXPIRATION) || 3600;
58✔
10
const refreshToken_expiration =
11
  parseInt(process.env.JWT_REFRESH_EXPIRATION) || 3600 * 24 * 30;
58✔
12

13
// Helper function to set cookie options based on environment
14
export const getCookieOptions = (maxAge) => {
58✔
15
  if (process.env.NODE_ENV === 'development') {
37✔
16
    return { 
6✔
17
      httpOnly: true, 
18
      path: '/',
19
      maxAge: maxAge * 1000 
20
    };
21
  } else if (process.env.NODE_ENV === 'production') {
31✔
22
    return { 
6✔
23
      httpOnly: true, 
24
      path: '/',
25
      maxAge: maxAge * 1000, 
26
      sameSite: 'none', 
27
      secure: true, 
28
      partitioned: true 
29
    };
30
  } else {
31
    // Default for test or other environments
32
    return { 
25✔
33
      httpOnly: true, 
34
      path: '/',
35
      maxAge: maxAge * 1000, 
36
      sameSite: 'lax' 
37
    };
38
  }
39
};
40

41
// Helper function to get cookie clear options
42
export const getClearCookieOptions = () => {
58✔
43
  if (process.env.NODE_ENV === 'development') {
18✔
44
    return { 
4✔
45
      httpOnly: true, 
46
      path: '/' 
47
    };
48
  } else if (process.env.NODE_ENV === 'production') {
14✔
49
    return { 
4✔
50
      httpOnly: true, 
51
      path: '/',
52
      sameSite: 'none', 
53
      secure: true 
54
    };
55
  } else {
56
    return { 
10✔
57
      httpOnly: true, 
58
      path: '/',
59
      sameSite: 'lax' 
60
    };
61
  }
62
};
63

64
export async function signUp(req, res) {
65
  const { username, authority = 'USER', password, email } = req.body;
7✔
66

67
  const userEmail = await models.User.findOne({
7✔
68
    where: {
69
      email,
70
    },
71
  });
72

73
  if (userEmail) {
7✔
74
    return res.status(400).json({ message: 'Email already exists' });
1✔
75
  }
76
  const rows = await models.User.findAll({
6✔
77
    where: {
78
      username,
79
    },
80
  });
81

82
  if (rows.length > 0) {
6✔
83
    return res.status(400).json({ message: 'Username already exists' });
2✔
84
  }
85
  const hashedPassword = await bcrypt.hash(password, 10);
4✔
86
  try {
4✔
87
    await models.User.create({
4✔
88
      username,
89
      password: hashedPassword,
90
      authority,
91
      email,
92
    });
93
    res.status(201).json({
3✔
94
      message: `User ${username} created successfully with authority ${authority}`,
95
    });
96
    
97
    logger.info(`User ${username} created with authority ${authority}`, {
3✔
98
      userId: 'system',
99
      action: 'user_create',
100
      email: email
101
    });
102
  } catch (error) {
103
    return handleControllerError(res, error, 'Failed to create user');
1✔
104
  }
105
}
106

107
export async function signIn(req, res) {
108
  const { username, password } = req.body;
20✔
109

110
  if (!username || !password) {
20✔
111
    return res
2✔
112
      .status(400)
113
      .json({ message: 'Username and password are required' });
114
  }
115
  try {
18✔
116
    const user = await models.User.findOne({
18✔
117
      where: {
118
        username,
119
      },
120
    });
121

122
    if (!user || user.length === 0) {
17✔
123
      return res.status(404).json({ message: 'User not found' });
1✔
124
    }
125

126
    const hashedPassword = user.password;
16✔
127
    const isPasswordValid = await bcrypt.compare(password, hashedPassword);
16✔
128

129
    if (!isPasswordValid) {
16✔
130
      return res.status(401).json({ message: 'Invalid password' });
1✔
131
    } else {
132
      const accessToken = jwt.sign(
15✔
133
        {
134
          user_id: user.id,
135
          username: user.username,
136
          authority: user.authority,
137
        },
138
        process.env.JWT_SECRET,
139
        { expiresIn: '1h' }
140
      );
141
      const refreshToken = jwt.sign(
15✔
142
        {
143
          user_id: user.id,
144
          username: user.username,
145
          authority: user.authority,
146
        },
147
        process.env.REFRESH_JWT_SECRET,
148
        { expiresIn: '7d' }
149
      );
150

151
      await models.User.update(
15✔
152
        { refresh_token: refreshToken },
153
        { where: { username } }
154
      );
155
      
156
      let nodeRedToken = '';
15✔
157
      if (user.authority === 'DEVELOPER' || user.authority === 'ADMIN') {
15✔
158
        try {
5✔
159
          nodeRedToken = await getNodeRedToken(username, password);
5✔
160
        } catch (nodeRedError) {
161
          // If the error is authentication-related (403), handle it specifically
162
          if (nodeRedError.statusCode === 403) {
2✔
163
            logger.warn(`Node-RED authentication failed for user: ${username}`, {
1✔
164
              userId: user.id,
165
              statusCode: 403
166
            });
167
            
168
            // Do not interrupt the flow, simply do not send Node-RED token
169
            res.cookie('accessToken', accessToken, getCookieOptions(token_expiration));
1✔
170
            res.cookie('refreshToken', refreshToken, getCookieOptions(refreshToken_expiration));
1✔
171
            
172
            return res.status(200).json({
1✔
173
              username: user.username,
174
              email: user.email,
175
              authority: user.authority,
176
              accessToken: accessToken,
177
              refreshToken: refreshToken,
178
              nodeRedAccess: false,
179
              message: 'Logged in successfully, but Node-RED access was denied. Check Node-RED credentials.'
180
            });
181
          }
182
          // Rethrow any other type of error to be handled by the outer catch
183
          throw nodeRedError;
1✔
184
        }
185
      }
186

187
      res.cookie('accessToken', accessToken, getCookieOptions(token_expiration));
13✔
188
      res.cookie('refreshToken', refreshToken, getCookieOptions(refreshToken_expiration));
13✔
189
      
190
      if (nodeRedToken !== '' && nodeRedToken !== null) {
13✔
191
        res.cookie('nodeRedToken', nodeRedToken, getCookieOptions(refreshToken_expiration));
1✔
192
      }
193

194
      res.status(200).json({
13✔
195
        username: user.username,
196
        email: user.email,
197
        authority: user.authority,
198
        accessToken: accessToken,
199
        refreshToken: refreshToken,
200
        nodeRedToken: nodeRedToken,
201
        nodeRedAccess: nodeRedToken !== '' && nodeRedToken !== null,
15✔
202
      });
203
    }
204
  } catch (error) {
205
    // Check if the error has a specific status code
206
    const statusCode = error.statusCode || 500;
2✔
207
    const errorMessage = error.message || 'Internal server error';
2!
208
    
209
    logger.error(`Error during sign in: ${errorMessage}`, {
2✔
210
      userId: req.body.username || 'unknown',
2!
211
      statusCode,
212
      error
213
    });
214
    
215
    return res.status(statusCode).json({ 
2✔
216
      message: errorMessage,
217
      details: statusCode === 403 ? 'Node-RED authentication failed' : undefined
2!
218
    });
219
  }
220
}
221

222
export async function signOut(req, res) {
223
  try {
7✔
224
    const cookies = req.cookies;
7✔
225
    if (!cookies?.refreshToken) {
7✔
226
      return res.status(400).json({ message: 'No refresh token provided' });
1✔
227
    }
228

229
    const refreshToken = cookies.refreshToken;
6✔
230
    const user = await models.User.findAll({
6✔
231
      where: { refresh_token: refreshToken },
232
    });
233

234
    if (user.length === 0) {
5✔
235
      res.clearCookie('refreshToken', getClearCookieOptions());
4✔
236
      res.clearCookie('accessToken', getClearCookieOptions());
4✔
237
      res.clearCookie('nodeRedToken', getClearCookieOptions());
4✔
238
      return res
4✔
239
        .status(404)
240
        .json({ message: 'No user found for provided refresh token' });
241
    }
242

243
    await models.User.update(
1✔
244
      { refresh_token: '' },
245
      {
246
        where: { refresh_token: refreshToken },
247
      }
248
    );
249

250
    res.clearCookie('refreshToken', getClearCookieOptions());
1✔
251
    res.clearCookie('accessToken', getClearCookieOptions());
1✔
252
    res.clearCookie('nodeRedToken', getClearCookieOptions());
1✔
253

254
    return res.status(204).json({ message: 'Signed out successfully' });
1✔
255
  } catch (error) {
256
    return handleControllerError(res, error, 'Error during sign out process');
1✔
257
  }
258
}
259

260
export async function getUsers(req, res) {
261
  // THIS IS A TEST FUNCTION
262
  try {
3✔
263
    const users = await models.User.findAll();
3✔
264

265
    logger.debug('Retrieved all users', {
2✔
266
      count: users.length
267
    });
268

269
    res.status(200).json(users);
2✔
270
  } catch (error) {
271
    return handleControllerError(res, error, 'Failed to retrieve users');
1✔
272
  }
273
}
274

275
export async function getAuthority(req, res) {
276
  const accessToken = req.cookies?.accessToken;
5✔
277

278
  if (!accessToken) {
5✔
279
    return res.status(400).json({ message: 'Token is required' });
1✔
280
  }
281

282
  const { decoded, error } = await verifyAccessToken(accessToken);
4✔
283
  if (error) {
4✔
284
    logger.warn('Invalid token attempt', {
2✔
285
      error: error.message
286
    });
287
    return res.status(403).json({ message: 'Invalid token' });
2✔
288
  }
289

290
  logger.debug('Authority check successful', {
2✔
291
    userId: decoded.user_id,
292
    authority: decoded.authority
293
  });
294

295
  return res.status(200).json({ authority: decoded.authority });
2✔
296
}
297

298
export async function refreshToken(req, res) {
299
  try {
9✔
300
    const refreshToken = req.cookies?.refreshToken;
9✔
301

302
    if (!refreshToken) {
9✔
303
      return res.status(400).json({ message: 'Refresh token is required' });
1✔
304
    }
305

306
    jwt.verify(refreshToken, process.env.REFRESH_JWT_SECRET, async (err, decoded) => {
8✔
307
      if (err) {
7✔
308
        logger.warn('Token verification failed', {
1✔
309
          error: err.message,
310
          tokenType: 'refresh'
311
        });
312
        return res.status(403).json({ message: 'Invalid or expired refresh token' });
1✔
313
      }
314

315
      const user = await models.User.findOne({
6✔
316
        where: {
317
          id: decoded.user_id,
318
          refresh_token: refreshToken
319
        }
320
      });
321

322
      if (!user) {
6✔
323
        logger.warn('Token claimed by non-existent user', {
1✔
324
          userId: decoded.user_id
325
        });
326
        return res.status(403).json({ message: 'Invalid refresh token' });
1✔
327
      }
328

329
      const accessToken = jwt.sign(
5✔
330
        {
331
          user_id: user.id,
332
          username: user.username,
333
          authority: user.authority
334
        },
335
        process.env.JWT_SECRET,
336
        { expiresIn: '1h' }
337
      );
338

339
      res.cookie('accessToken', accessToken, getCookieOptions(token_expiration));
5✔
340

341
      logger.info('Access token refreshed', {
5✔
342
        userId: user.id,
343
        username: user.username
344
      });
345

346
      return res.status(200).json({
5✔
347
        accessToken
348
      });
349
    });
350
  } catch (error) {
351
    return handleControllerError(res, error, 'Failed to refresh access token');
1✔
352
  }
353
}
354

355
export async function deleteUserById(req, res) {
356
  const { id } = req.params;
3✔
357
  try {
3✔
358
    const user = await models.User.findByPk(id);
3✔
359
    if (!user) {
3✔
360
      logger.warn('Attempted to delete non-existent user', {
1✔
361
        userId: id
362
      });
363
      return res.status(404).json({ message: 'User not found' });
1✔
364
    }
365

366
    await user.destroy();
2✔
367

368
    logger.info('User deleted successfully', {
1✔
369
      userId: id,
370
      username: user.username
371
    });
372

373
    return res.status(200).json({ message: 'User deleted successfully' });
1✔
374
  } catch (error) {
375
    return handleControllerError(res, error, 'Failed to delete user');
1✔
376
  }
377
}
378

379
export async function whoami(req, res) {
380

381
  try {
×
382
    const { user } = req;
×
383

384
    if (!user) {
×
385
      return res.status(401).json({ message: 'Unauthorized: No user in request' });
×
386
    }
387

388
    const dbUser = await models.User.findByPk(user.user_id, {
×
389
      attributes: ['id', 'username', 'email', 'authority', 'createdAt', 'updatedAt']
390
    });
391
    if (!dbUser) {
×
392
      return res.status(404).json({ message: 'User not found' });
×
393
    }
394

395
    return res.status(200).json({
×
396
      id: dbUser.id,
397
      username: dbUser.username,
398
      email: dbUser.email,
399
      authority: dbUser.authority,
400
      createdAt: dbUser.createdAt,
401
      updatedAt: dbUser.updatedAt
402
    });
403
  } catch (error) {
404
    return handleControllerError(res, error, 'Failed to fetch user info');
×
405
  }
406
}
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